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