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