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