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