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