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