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 findUnsentMessagesWithOtrEncryption(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() == Message.ENCRYPTION_OTR)) {
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
253 public interface OnMessageFound {
254 public void onMessageFound(final Message message);
255 }
256
257 public Conversation(final String name, final Account account, final Jid contactJid,
258 final int mode) {
259 this(java.util.UUID.randomUUID().toString(), name, null, account
260 .getUuid(), contactJid, System.currentTimeMillis(),
261 STATUS_AVAILABLE, mode, "");
262 this.account = account;
263 }
264
265 public Conversation(final String uuid, final String name, final String contactUuid,
266 final String accountUuid, final Jid contactJid, final long created, final int status,
267 final int mode, final String attributes) {
268 this.uuid = uuid;
269 this.name = name;
270 this.contactUuid = contactUuid;
271 this.accountUuid = accountUuid;
272 this.contactJid = contactJid;
273 this.created = created;
274 this.status = status;
275 this.mode = mode;
276 try {
277 this.attributes = new JSONObject(attributes == null ? "" : attributes);
278 } catch (JSONException e) {
279 this.attributes = new JSONObject();
280 }
281 }
282
283 public boolean isRead() {
284 return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
285 }
286
287 public void markRead() {
288 for (int i = this.messages.size() - 1; i >= 0; --i) {
289 if (messages.get(i).isRead()) {
290 break;
291 }
292 this.messages.get(i).markRead();
293 }
294 }
295
296 public Message getLatestMarkableMessage() {
297 for (int i = this.messages.size() - 1; i >= 0; --i) {
298 if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
299 && this.messages.get(i).markable) {
300 if (this.messages.get(i).isRead()) {
301 return null;
302 } else {
303 return this.messages.get(i);
304 }
305 }
306 }
307 return null;
308 }
309
310 public Message getLatestMessage() {
311 if (this.messages.size() == 0) {
312 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
313 message.setTime(getCreated());
314 return message;
315 } else {
316 Message message = this.messages.get(this.messages.size() - 1);
317 message.setConversation(this);
318 return message;
319 }
320 }
321
322 public String getName() {
323 if (getMode() == MODE_MULTI) {
324 if (getMucOptions().getSubject() != null) {
325 return getMucOptions().getSubject();
326 } else if (bookmark != null && bookmark.getName() != null) {
327 return bookmark.getName();
328 } else {
329 String generatedName = getMucOptions().createNameFromParticipants();
330 if (generatedName != null) {
331 return generatedName;
332 } else {
333 return getJid().getLocalpart();
334 }
335 }
336 } else {
337 return this.getContact().getDisplayName();
338 }
339 }
340
341 public String getAccountUuid() {
342 return this.accountUuid;
343 }
344
345 public Account getAccount() {
346 return this.account;
347 }
348
349 public Contact getContact() {
350 return this.account.getRoster().getContact(this.contactJid);
351 }
352
353 public void setAccount(final Account account) {
354 this.account = account;
355 }
356
357 @Override
358 public Jid getJid() {
359 return this.contactJid;
360 }
361
362 public int getStatus() {
363 return this.status;
364 }
365
366 public long getCreated() {
367 return this.created;
368 }
369
370 public ContentValues getContentValues() {
371 ContentValues values = new ContentValues();
372 values.put(UUID, uuid);
373 values.put(NAME, name);
374 values.put(CONTACT, contactUuid);
375 values.put(ACCOUNT, accountUuid);
376 values.put(CONTACTJID, contactJid.toString());
377 values.put(CREATED, created);
378 values.put(STATUS, status);
379 values.put(MODE, mode);
380 values.put(ATTRIBUTES, attributes.toString());
381 return values;
382 }
383
384 public static Conversation fromCursor(Cursor cursor) {
385 Jid jid;
386 try {
387 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
388 } catch (final InvalidJidException e) {
389 // Borked DB..
390 jid = null;
391 }
392 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
393 cursor.getString(cursor.getColumnIndex(NAME)),
394 cursor.getString(cursor.getColumnIndex(CONTACT)),
395 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
396 jid,
397 cursor.getLong(cursor.getColumnIndex(CREATED)),
398 cursor.getInt(cursor.getColumnIndex(STATUS)),
399 cursor.getInt(cursor.getColumnIndex(MODE)),
400 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
401 }
402
403 public void setStatus(int status) {
404 this.status = status;
405 }
406
407 public int getMode() {
408 return this.mode;
409 }
410
411 public void setMode(int mode) {
412 this.mode = mode;
413 }
414
415 public SessionImpl startOtrSession(String presence, boolean sendStart) {
416 if (this.otrSession != null) {
417 return this.otrSession;
418 } else {
419 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
420 presence,
421 "xmpp");
422 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
423 try {
424 if (sendStart) {
425 this.otrSession.startSession();
426 return this.otrSession;
427 }
428 return this.otrSession;
429 } catch (OtrException e) {
430 return null;
431 }
432 }
433
434 }
435
436 public SessionImpl getOtrSession() {
437 return this.otrSession;
438 }
439
440 public void resetOtrSession() {
441 this.otrFingerprint = null;
442 this.otrSession = null;
443 this.mSmp.hint = null;
444 this.mSmp.secret = null;
445 this.mSmp.status = Smp.STATUS_NONE;
446 }
447
448 public Smp smp() {
449 return mSmp;
450 }
451
452 public void startOtrIfNeeded() {
453 if (this.otrSession != null
454 && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
455 try {
456 this.otrSession.startSession();
457 } catch (OtrException e) {
458 this.resetOtrSession();
459 }
460 }
461 }
462
463 public boolean endOtrIfNeeded() {
464 if (this.otrSession != null) {
465 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
466 try {
467 this.otrSession.endSession();
468 this.resetOtrSession();
469 return true;
470 } catch (OtrException e) {
471 this.resetOtrSession();
472 return false;
473 }
474 } else {
475 this.resetOtrSession();
476 return false;
477 }
478 } else {
479 return false;
480 }
481 }
482
483 public boolean hasValidOtrSession() {
484 return this.otrSession != null;
485 }
486
487 public synchronized String getOtrFingerprint() {
488 if (this.otrFingerprint == null) {
489 try {
490 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
491 return null;
492 }
493 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
494 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
495 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
496 return null;
497 }
498 }
499 return this.otrFingerprint;
500 }
501
502 public boolean verifyOtrFingerprint() {
503 final String fingerprint = getOtrFingerprint();
504 if (fingerprint != null) {
505 getContact().addOtrFingerprint(fingerprint);
506 return true;
507 } else {
508 return false;
509 }
510 }
511
512 public boolean isOtrFingerprintVerified() {
513 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
514 }
515
516 public synchronized MucOptions getMucOptions() {
517 if (this.mucOptions == null) {
518 this.mucOptions = new MucOptions(this);
519 }
520 return this.mucOptions;
521 }
522
523 public void resetMucOptions() {
524 this.mucOptions = null;
525 }
526
527 public void setContactJid(final Jid jid) {
528 this.contactJid = jid;
529 }
530
531 public void setNextCounterpart(Jid jid) {
532 this.nextCounterpart = jid;
533 }
534
535 public Jid getNextCounterpart() {
536 return this.nextCounterpart;
537 }
538
539 public int getLatestEncryption() {
540 int latestEncryption = this.getLatestMessage().getEncryption();
541 if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
542 || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
543 return Message.ENCRYPTION_PGP;
544 } else {
545 return latestEncryption;
546 }
547 }
548
549 public int getNextEncryption(boolean force) {
550 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
551 if (next == -1) {
552 int latest = this.getLatestEncryption();
553 if (latest == Message.ENCRYPTION_NONE) {
554 if (force && getMode() == MODE_SINGLE) {
555 return Message.ENCRYPTION_OTR;
556 } else if (getContact().getPresences().size() == 1) {
557 if (getContact().getOtrFingerprints().size() >= 1) {
558 return Message.ENCRYPTION_OTR;
559 } else {
560 return latest;
561 }
562 } else {
563 return latest;
564 }
565 } else {
566 return latest;
567 }
568 }
569 if (next == Message.ENCRYPTION_NONE && force
570 && getMode() == MODE_SINGLE) {
571 return Message.ENCRYPTION_OTR;
572 } else {
573 return next;
574 }
575 }
576
577 public void setNextEncryption(int encryption) {
578 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
579 }
580
581 public String getNextMessage() {
582 if (this.nextMessage == null) {
583 return "";
584 } else {
585 return this.nextMessage;
586 }
587 }
588
589 public boolean smpRequested() {
590 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
591 }
592
593 public void setNextMessage(String message) {
594 this.nextMessage = message;
595 }
596
597 public void setSymmetricKey(byte[] key) {
598 this.symmetricKey = key;
599 }
600
601 public byte[] getSymmetricKey() {
602 return this.symmetricKey;
603 }
604
605 public void setBookmark(Bookmark bookmark) {
606 this.bookmark = bookmark;
607 this.bookmark.setConversation(this);
608 }
609
610 public void deregisterWithBookmark() {
611 if (this.bookmark != null) {
612 this.bookmark.setConversation(null);
613 }
614 }
615
616 public Bookmark getBookmark() {
617 return this.bookmark;
618 }
619
620 public boolean hasDuplicateMessage(Message message) {
621 synchronized (this.messages) {
622 for (int i = this.messages.size() - 1; i >= 0; --i) {
623 if (this.messages.get(i).equals(message)) {
624 return true;
625 }
626 }
627 }
628 return false;
629 }
630
631 public Message findSentMessageWithBody(String body) {
632 synchronized (this.messages) {
633 for (int i = this.messages.size() - 1; i >= 0; --i) {
634 Message message = this.messages.get(i);
635 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
636 return message;
637 }
638 }
639 return null;
640 }
641 }
642
643 public boolean setLastMessageTransmitted(long value) {
644 long before = getLastMessageTransmitted();
645 if (value - before > 1000) {
646 this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
647 return true;
648 } else {
649 return false;
650 }
651 }
652
653 public long getLastMessageTransmitted() {
654 long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
655 if (timestamp == 0) {
656 synchronized (this.messages) {
657 for(int i = this.messages.size() - 1; i >= 0; --i) {
658 Message message = this.messages.get(i);
659 if (message.getStatus() == Message.STATUS_RECEIVED) {
660 return message.getTimeSent();
661 }
662 }
663 }
664 }
665 return timestamp;
666 }
667
668 public void setMutedTill(long value) {
669 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
670 }
671
672 public boolean isMuted() {
673 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
674 }
675
676 public boolean setAttribute(String key, String value) {
677 try {
678 this.attributes.put(key, value);
679 return true;
680 } catch (JSONException e) {
681 return false;
682 }
683 }
684
685 public String getAttribute(String key) {
686 try {
687 return this.attributes.getString(key);
688 } catch (JSONException e) {
689 return null;
690 }
691 }
692
693 public int getIntAttribute(String key, int defaultValue) {
694 String value = this.getAttribute(key);
695 if (value == null) {
696 return defaultValue;
697 } else {
698 try {
699 return Integer.parseInt(value);
700 } catch (NumberFormatException e) {
701 return defaultValue;
702 }
703 }
704 }
705
706 public long getLongAttribute(String key, long defaultValue) {
707 String value = this.getAttribute(key);
708 if (value == null) {
709 return defaultValue;
710 } else {
711 try {
712 return Long.parseLong(value);
713 } catch (NumberFormatException e) {
714 return defaultValue;
715 }
716 }
717 }
718
719 public void add(Message message) {
720 message.setConversation(this);
721 synchronized (this.messages) {
722 this.messages.add(message);
723 }
724 }
725
726 public void addAll(int index, List<Message> messages) {
727 synchronized (this.messages) {
728 this.messages.addAll(index, messages);
729 }
730 }
731
732 public void sort() {
733 synchronized (this.messages) {
734 Collections.sort(this.messages, new Comparator<Message>() {
735 @Override
736 public int compare(Message left, Message right) {
737 if (left.getTimeSent() < right.getTimeSent()) {
738 return -1;
739 } else if (left.getTimeSent() > right.getTimeSent()) {
740 return 1;
741 } else {
742 return 0;
743 }
744 }
745 });
746 for(Message message : this.messages) {
747 message.untie();
748 }
749 }
750 }
751
752 public int unreadCount() {
753 synchronized (this.messages) {
754 int count = 0;
755 for(int i = this.messages.size() - 1; i >= 0; --i) {
756 if (this.messages.get(i).isRead()) {
757 return count;
758 }
759 ++count;
760 }
761 return count;
762 }
763 }
764
765 public class Smp {
766 public static final int STATUS_NONE = 0;
767 public static final int STATUS_CONTACT_REQUESTED = 1;
768 public static final int STATUS_WE_REQUESTED = 2;
769 public static final int STATUS_FAILED = 3;
770 public static final int STATUS_VERIFIED = 4;
771
772 public String secret = null;
773 public String hint = null;
774 public int status = 0;
775 }
776}