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