1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.os.SystemClock;
6
7import net.java.otr4j.OtrException;
8import net.java.otr4j.crypto.OtrCryptoEngineImpl;
9import net.java.otr4j.crypto.OtrCryptoException;
10import net.java.otr4j.session.SessionID;
11import net.java.otr4j.session.SessionImpl;
12import net.java.otr4j.session.SessionStatus;
13
14import org.json.JSONException;
15import org.json.JSONObject;
16
17import java.security.interfaces.DSAPublicKey;
18import java.util.ArrayList;
19import java.util.Collections;
20import java.util.Comparator;
21import java.util.List;
22
23import eu.siacs.conversations.Config;
24import eu.siacs.conversations.xmpp.jid.InvalidJidException;
25import eu.siacs.conversations.xmpp.jid.Jid;
26
27public class Conversation extends AbstractEntity implements Blockable {
28 public static final String TABLENAME = "conversations";
29
30 public static final int STATUS_AVAILABLE = 0;
31 public static final int STATUS_ARCHIVED = 1;
32 public static final int STATUS_DELETED = 2;
33
34 public static final int MODE_MULTI = 1;
35 public static final int MODE_SINGLE = 0;
36
37 public static final String NAME = "name";
38 public static final String ACCOUNT = "accountUuid";
39 public static final String CONTACT = "contactUuid";
40 public static final String CONTACTJID = "contactJid";
41 public static final String STATUS = "status";
42 public static final String CREATED = "created";
43 public static final String MODE = "mode";
44 public static final String ATTRIBUTES = "attributes";
45
46 public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
47 public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
48 public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
49 public static final String ATTRIBUTE_LAST_MESSAGE_TRANSMITTED = "last_message_transmitted";
50
51 private String name;
52 private String contactUuid;
53 private String accountUuid;
54 private Jid contactJid;
55 private int status;
56 private long created;
57 private int mode;
58
59 private JSONObject attributes = new JSONObject();
60
61 private Jid nextCounterpart;
62
63 protected final ArrayList<Message> messages = new ArrayList<>();
64 protected Account account = null;
65
66 private transient SessionImpl otrSession;
67
68 private transient String otrFingerprint = null;
69 private Smp mSmp = new Smp();
70
71 private String nextMessage;
72
73 private transient MucOptions mucOptions = null;
74
75 private byte[] symmetricKey;
76
77 private Bookmark bookmark;
78
79 private boolean messagesLeftOnServer = true;
80
81 public boolean hasMessagesLeftOnServer() {
82 return messagesLeftOnServer;
83 }
84
85 public void setHasMessagesLeftOnServer(boolean value) {
86 this.messagesLeftOnServer = value;
87 }
88
89 public Message findUnsentMessageWithUuid(String uuid) {
90 synchronized(this.messages) {
91 for (final Message message : this.messages) {
92 final int s = message.getStatus();
93 if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
94 return message;
95 }
96 }
97 }
98 return null;
99 }
100
101 public void findWaitingMessages(OnMessageFound onMessageFound) {
102 synchronized (this.messages) {
103 for(Message message : this.messages) {
104 if (message.getStatus() == Message.STATUS_WAITING) {
105 onMessageFound.onMessageFound(message);
106 }
107 }
108 }
109 }
110
111 public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
112 synchronized (this.messages) {
113 for (final Message message : this.messages) {
114 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
115 && message.getEncryption() != Message.ENCRYPTION_PGP) {
116 onMessageFound.onMessageFound(message);
117 }
118 }
119 }
120 }
121
122 public Message findMessageWithFileAndUuid(final String uuid) {
123 synchronized (this.messages) {
124 for (final Message message : this.messages) {
125 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
126 && message.getEncryption() != Message.ENCRYPTION_PGP
127 && message.getUuid().equals(uuid)) {
128 return message;
129 }
130 }
131 }
132 return null;
133 }
134
135 public void clearMessages() {
136 synchronized (this.messages) {
137 this.messages.clear();
138 }
139 }
140
141 public void trim() {
142 synchronized (this.messages) {
143 final int size = messages.size();
144 final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
145 if (size > maxsize) {
146 this.messages.subList(0, size - maxsize).clear();
147 }
148 }
149 }
150
151 public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) {
152 synchronized (this.messages) {
153 for (Message message : this.messages) {
154 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
155 && (message.getEncryption() == Message.ENCRYPTION_OTR)) {
156 onMessageFound.onMessageFound(message);
157 }
158 }
159 }
160 }
161
162 public void findUnsentTextMessages(OnMessageFound onMessageFound) {
163 synchronized (this.messages) {
164 for (Message message : this.messages) {
165 if (message.getType() != Message.TYPE_IMAGE
166 && message.getStatus() == Message.STATUS_UNSEND) {
167 onMessageFound.onMessageFound(message);
168 }
169 }
170 }
171 }
172
173 public Message findSentMessageWithUuid(String uuid) {
174 synchronized (this.messages) {
175 for (Message message : this.messages) {
176 if (uuid.equals(message.getUuid())
177 || (message.getStatus() >= Message.STATUS_SEND && uuid
178 .equals(message.getRemoteMsgId()))) {
179 return message;
180 }
181 }
182 }
183 return null;
184 }
185
186 public void populateWithMessages(final List<Message> messages) {
187 synchronized (this.messages) {
188 messages.clear();
189 messages.addAll(this.messages);
190 }
191 }
192
193 @Override
194 public boolean isBlocked() {
195 return getContact().isBlocked();
196 }
197
198 @Override
199 public boolean isDomainBlocked() {
200 return getContact().isDomainBlocked();
201 }
202
203 @Override
204 public Jid getBlockedJid() {
205 return getContact().getBlockedJid();
206 }
207
208
209 public interface OnMessageFound {
210 public void onMessageFound(final Message message);
211 }
212
213 public Conversation(final String name, final Account account, final Jid contactJid,
214 final int mode) {
215 this(java.util.UUID.randomUUID().toString(), name, null, account
216 .getUuid(), contactJid, System.currentTimeMillis(),
217 STATUS_AVAILABLE, mode, "");
218 this.account = account;
219 }
220
221 public Conversation(final String uuid, final String name, final String contactUuid,
222 final String accountUuid, final Jid contactJid, final long created, final int status,
223 final int mode, final String attributes) {
224 this.uuid = uuid;
225 this.name = name;
226 this.contactUuid = contactUuid;
227 this.accountUuid = accountUuid;
228 this.contactJid = contactJid;
229 this.created = created;
230 this.status = status;
231 this.mode = mode;
232 try {
233 this.attributes = new JSONObject(attributes == null ? "" : attributes);
234 } catch (JSONException e) {
235 this.attributes = new JSONObject();
236 }
237 }
238
239 public boolean isRead() {
240 return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
241 }
242
243 public void markRead() {
244 for (int i = this.messages.size() - 1; i >= 0; --i) {
245 if (messages.get(i).isRead()) {
246 break;
247 }
248 this.messages.get(i).markRead();
249 }
250 }
251
252 public Message getLatestMarkableMessage() {
253 for (int i = this.messages.size() - 1; i >= 0; --i) {
254 if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
255 && this.messages.get(i).markable) {
256 if (this.messages.get(i).isRead()) {
257 return null;
258 } else {
259 return this.messages.get(i);
260 }
261 }
262 }
263 return null;
264 }
265
266 public Message getLatestMessage() {
267 if (this.messages.size() == 0) {
268 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
269 message.setTime(getCreated());
270 return message;
271 } else {
272 Message message = this.messages.get(this.messages.size() - 1);
273 message.setConversation(this);
274 return message;
275 }
276 }
277
278 public String getName() {
279 if (getMode() == MODE_MULTI) {
280 if (getMucOptions().getSubject() != null) {
281 return getMucOptions().getSubject();
282 } else if (bookmark != null && bookmark.getName() != null) {
283 return bookmark.getName();
284 } else {
285 String generatedName = getMucOptions().createNameFromParticipants();
286 if (generatedName != null) {
287 return generatedName;
288 } else {
289 return getJid().getLocalpart();
290 }
291 }
292 } else {
293 return this.getContact().getDisplayName();
294 }
295 }
296
297 public String getAccountUuid() {
298 return this.accountUuid;
299 }
300
301 public Account getAccount() {
302 return this.account;
303 }
304
305 public Contact getContact() {
306 return this.account.getRoster().getContact(this.contactJid);
307 }
308
309 public void setAccount(final Account account) {
310 this.account = account;
311 }
312
313 @Override
314 public Jid getJid() {
315 return this.contactJid;
316 }
317
318 public int getStatus() {
319 return this.status;
320 }
321
322 public long getCreated() {
323 return this.created;
324 }
325
326 public ContentValues getContentValues() {
327 ContentValues values = new ContentValues();
328 values.put(UUID, uuid);
329 values.put(NAME, name);
330 values.put(CONTACT, contactUuid);
331 values.put(ACCOUNT, accountUuid);
332 values.put(CONTACTJID, contactJid.toString());
333 values.put(CREATED, created);
334 values.put(STATUS, status);
335 values.put(MODE, mode);
336 values.put(ATTRIBUTES, attributes.toString());
337 return values;
338 }
339
340 public static Conversation fromCursor(Cursor cursor) {
341 Jid jid;
342 try {
343 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)));
344 } catch (final InvalidJidException e) {
345 // Borked DB..
346 jid = null;
347 }
348 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
349 cursor.getString(cursor.getColumnIndex(NAME)),
350 cursor.getString(cursor.getColumnIndex(CONTACT)),
351 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
352 jid,
353 cursor.getLong(cursor.getColumnIndex(CREATED)),
354 cursor.getInt(cursor.getColumnIndex(STATUS)),
355 cursor.getInt(cursor.getColumnIndex(MODE)),
356 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
357 }
358
359 public void setStatus(int status) {
360 this.status = status;
361 }
362
363 public int getMode() {
364 return this.mode;
365 }
366
367 public void setMode(int mode) {
368 this.mode = mode;
369 }
370
371 public SessionImpl startOtrSession(String presence, boolean sendStart) {
372 if (this.otrSession != null) {
373 return this.otrSession;
374 } else {
375 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
376 presence,
377 "xmpp");
378 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine());
379 try {
380 if (sendStart) {
381 this.otrSession.startSession();
382 return this.otrSession;
383 }
384 return this.otrSession;
385 } catch (OtrException e) {
386 return null;
387 }
388 }
389
390 }
391
392 public SessionImpl getOtrSession() {
393 return this.otrSession;
394 }
395
396 public void resetOtrSession() {
397 this.otrFingerprint = null;
398 this.otrSession = null;
399 this.mSmp.hint = null;
400 this.mSmp.secret = null;
401 this.mSmp.status = Smp.STATUS_NONE;
402 }
403
404 public Smp smp() {
405 return mSmp;
406 }
407
408 public void startOtrIfNeeded() {
409 if (this.otrSession != null
410 && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
411 try {
412 this.otrSession.startSession();
413 } catch (OtrException e) {
414 this.resetOtrSession();
415 }
416 }
417 }
418
419 public boolean endOtrIfNeeded() {
420 if (this.otrSession != null) {
421 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
422 try {
423 this.otrSession.endSession();
424 this.resetOtrSession();
425 return true;
426 } catch (OtrException e) {
427 this.resetOtrSession();
428 return false;
429 }
430 } else {
431 this.resetOtrSession();
432 return false;
433 }
434 } else {
435 return false;
436 }
437 }
438
439 public boolean hasValidOtrSession() {
440 return this.otrSession != null;
441 }
442
443 public synchronized String getOtrFingerprint() {
444 if (this.otrFingerprint == null) {
445 try {
446 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
447 return null;
448 }
449 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
450 this.otrFingerprint = getAccount().getOtrEngine().getFingerprint(remotePubKey);
451 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
452 return null;
453 }
454 }
455 return this.otrFingerprint;
456 }
457
458 public boolean verifyOtrFingerprint() {
459 final String fingerprint = getOtrFingerprint();
460 if (fingerprint != null) {
461 getContact().addOtrFingerprint(fingerprint);
462 return true;
463 } else {
464 return false;
465 }
466 }
467
468 public boolean isOtrFingerprintVerified() {
469 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
470 }
471
472 public synchronized MucOptions getMucOptions() {
473 if (this.mucOptions == null) {
474 this.mucOptions = new MucOptions(this);
475 }
476 return this.mucOptions;
477 }
478
479 public void resetMucOptions() {
480 this.mucOptions = null;
481 }
482
483 public void setContactJid(final Jid jid) {
484 this.contactJid = jid;
485 }
486
487 public void setNextCounterpart(Jid jid) {
488 this.nextCounterpart = jid;
489 }
490
491 public Jid getNextCounterpart() {
492 return this.nextCounterpart;
493 }
494
495 public int getLatestEncryption() {
496 int latestEncryption = this.getLatestMessage().getEncryption();
497 if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
498 || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
499 return Message.ENCRYPTION_PGP;
500 } else {
501 return latestEncryption;
502 }
503 }
504
505 public int getNextEncryption(boolean force) {
506 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
507 if (next == -1) {
508 int latest = this.getLatestEncryption();
509 if (latest == Message.ENCRYPTION_NONE) {
510 if (force && getMode() == MODE_SINGLE) {
511 return Message.ENCRYPTION_OTR;
512 } else if (getContact().getPresences().size() == 1) {
513 if (getContact().getOtrFingerprints().size() >= 1) {
514 return Message.ENCRYPTION_OTR;
515 } else {
516 return latest;
517 }
518 } else {
519 return latest;
520 }
521 } else {
522 return latest;
523 }
524 }
525 if (next == Message.ENCRYPTION_NONE && force
526 && getMode() == MODE_SINGLE) {
527 return Message.ENCRYPTION_OTR;
528 } else {
529 return next;
530 }
531 }
532
533 public void setNextEncryption(int encryption) {
534 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
535 }
536
537 public String getNextMessage() {
538 if (this.nextMessage == null) {
539 return "";
540 } else {
541 return this.nextMessage;
542 }
543 }
544
545 public boolean smpRequested() {
546 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
547 }
548
549 public void setNextMessage(String message) {
550 this.nextMessage = message;
551 }
552
553 public void setSymmetricKey(byte[] key) {
554 this.symmetricKey = key;
555 }
556
557 public byte[] getSymmetricKey() {
558 return this.symmetricKey;
559 }
560
561 public void setBookmark(Bookmark bookmark) {
562 this.bookmark = bookmark;
563 this.bookmark.setConversation(this);
564 }
565
566 public void deregisterWithBookmark() {
567 if (this.bookmark != null) {
568 this.bookmark.setConversation(null);
569 }
570 }
571
572 public Bookmark getBookmark() {
573 return this.bookmark;
574 }
575
576 public boolean hasDuplicateMessage(Message message) {
577 synchronized (this.messages) {
578 for (int i = this.messages.size() - 1; i >= 0; --i) {
579 if (this.messages.get(i).equals(message)) {
580 return true;
581 }
582 }
583 }
584 return false;
585 }
586
587 public Message findSentMessageWithBody(String body) {
588 synchronized (this.messages) {
589 for (int i = this.messages.size() - 1; i >= 0; --i) {
590 Message message = this.messages.get(i);
591 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
592 return message;
593 }
594 }
595 return null;
596 }
597 }
598
599 public boolean setLastMessageTransmitted(long value) {
600 long before = getLastMessageTransmitted();
601 if (value - before > 1000) {
602 this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
603 return true;
604 } else {
605 return false;
606 }
607 }
608
609 public long getLastMessageTransmitted() {
610 long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
611 if (timestamp == 0) {
612 synchronized (this.messages) {
613 for(int i = this.messages.size() - 1; i >= 0; --i) {
614 Message message = this.messages.get(i);
615 if (message.getStatus() == Message.STATUS_RECEIVED) {
616 return message.getTimeSent();
617 }
618 }
619 }
620 }
621 return timestamp;
622 }
623
624 public void setMutedTill(long value) {
625 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
626 }
627
628 public boolean isMuted() {
629 return SystemClock.elapsedRealtime() < this.getLongAttribute(
630 ATTRIBUTE_MUTED_TILL, 0);
631 }
632
633 public boolean setAttribute(String key, String value) {
634 try {
635 this.attributes.put(key, value);
636 return true;
637 } catch (JSONException e) {
638 return false;
639 }
640 }
641
642 public String getAttribute(String key) {
643 try {
644 return this.attributes.getString(key);
645 } catch (JSONException e) {
646 return null;
647 }
648 }
649
650 public int getIntAttribute(String key, int defaultValue) {
651 String value = this.getAttribute(key);
652 if (value == null) {
653 return defaultValue;
654 } else {
655 try {
656 return Integer.parseInt(value);
657 } catch (NumberFormatException e) {
658 return defaultValue;
659 }
660 }
661 }
662
663 public long getLongAttribute(String key, long defaultValue) {
664 String value = this.getAttribute(key);
665 if (value == null) {
666 return defaultValue;
667 } else {
668 try {
669 return Long.parseLong(value);
670 } catch (NumberFormatException e) {
671 return defaultValue;
672 }
673 }
674 }
675
676 public void add(Message message) {
677 message.setConversation(this);
678 synchronized (this.messages) {
679 this.messages.add(message);
680 }
681 }
682
683 public void addAll(int index, List<Message> messages) {
684 synchronized (this.messages) {
685 this.messages.addAll(index, messages);
686 }
687 }
688
689 public void sort() {
690 synchronized (this.messages) {
691 Collections.sort(this.messages, new Comparator<Message>() {
692 @Override
693 public int compare(Message left, Message right) {
694 if (left.getTimeSent() < right.getTimeSent()) {
695 return -1;
696 } else if (left.getTimeSent() > right.getTimeSent()) {
697 return 1;
698 } else {
699 return 0;
700 }
701 }
702 });
703 for(Message message : this.messages) {
704 message.untie();
705 }
706 }
707 }
708
709 public class Smp {
710 public static final int STATUS_NONE = 0;
711 public static final int STATUS_CONTACT_REQUESTED = 1;
712 public static final int STATUS_WE_REQUESTED = 2;
713 public static final int STATUS_FAILED = 3;
714 public static final int STATUS_VERIFIED = 4;
715
716 public String secret = null;
717 public String hint = null;
718 public int status = 0;
719 }
720}