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 String getOtrFingerprint() {
440 if (this.otrFingerprint == null) {
441 try {
442 if (getOtrSession() == null) {
443 return "";
444 }
445 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession()
446 .getRemotePublicKey();
447 StringBuilder builder = new StringBuilder(
448 new OtrCryptoEngineImpl().getFingerprint(remotePubKey));
449 builder.insert(8, " ");
450 builder.insert(17, " ");
451 builder.insert(26, " ");
452 builder.insert(35, " ");
453 this.otrFingerprint = builder.toString();
454 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
455
456 }
457 }
458 return this.otrFingerprint;
459 }
460
461 public void verifyOtrFingerprint() {
462 getContact().addOtrFingerprint(getOtrFingerprint());
463 }
464
465 public boolean isOtrFingerprintVerified() {
466 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
467 }
468
469 public synchronized MucOptions getMucOptions() {
470 if (this.mucOptions == null) {
471 this.mucOptions = new MucOptions(this);
472 }
473 return this.mucOptions;
474 }
475
476 public void resetMucOptions() {
477 this.mucOptions = null;
478 }
479
480 public void setContactJid(final Jid jid) {
481 this.contactJid = jid;
482 }
483
484 public void setNextCounterpart(Jid jid) {
485 this.nextCounterpart = jid;
486 }
487
488 public Jid getNextCounterpart() {
489 return this.nextCounterpart;
490 }
491
492 public int getLatestEncryption() {
493 int latestEncryption = this.getLatestMessage().getEncryption();
494 if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
495 || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
496 return Message.ENCRYPTION_PGP;
497 } else {
498 return latestEncryption;
499 }
500 }
501
502 public int getNextEncryption(boolean force) {
503 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
504 if (next == -1) {
505 int latest = this.getLatestEncryption();
506 if (latest == Message.ENCRYPTION_NONE) {
507 if (force && getMode() == MODE_SINGLE) {
508 return Message.ENCRYPTION_OTR;
509 } else if (getContact().getPresences().size() == 1) {
510 if (getContact().getOtrFingerprints().size() >= 1) {
511 return Message.ENCRYPTION_OTR;
512 } else {
513 return latest;
514 }
515 } else {
516 return latest;
517 }
518 } else {
519 return latest;
520 }
521 }
522 if (next == Message.ENCRYPTION_NONE && force
523 && getMode() == MODE_SINGLE) {
524 return Message.ENCRYPTION_OTR;
525 } else {
526 return next;
527 }
528 }
529
530 public void setNextEncryption(int encryption) {
531 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
532 }
533
534 public String getNextMessage() {
535 if (this.nextMessage == null) {
536 return "";
537 } else {
538 return this.nextMessage;
539 }
540 }
541
542 public boolean smpRequested() {
543 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
544 }
545
546 public void setNextMessage(String message) {
547 this.nextMessage = message;
548 }
549
550 public void setSymmetricKey(byte[] key) {
551 this.symmetricKey = key;
552 }
553
554 public byte[] getSymmetricKey() {
555 return this.symmetricKey;
556 }
557
558 public void setBookmark(Bookmark bookmark) {
559 this.bookmark = bookmark;
560 this.bookmark.setConversation(this);
561 }
562
563 public void deregisterWithBookmark() {
564 if (this.bookmark != null) {
565 this.bookmark.setConversation(null);
566 }
567 }
568
569 public Bookmark getBookmark() {
570 return this.bookmark;
571 }
572
573 public boolean hasDuplicateMessage(Message message) {
574 synchronized (this.messages) {
575 for (int i = this.messages.size() - 1; i >= 0; --i) {
576 if (this.messages.get(i).equals(message)) {
577 return true;
578 }
579 }
580 }
581 return false;
582 }
583
584 public Message findSentMessageWithBody(String body) {
585 synchronized (this.messages) {
586 for (int i = this.messages.size() - 1; i >= 0; --i) {
587 Message message = this.messages.get(i);
588 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
589 return message;
590 }
591 }
592 return null;
593 }
594 }
595
596 public boolean setLastMessageTransmitted(long value) {
597 long before = getLastMessageTransmitted();
598 if (value - before > 1000) {
599 this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
600 return true;
601 } else {
602 return false;
603 }
604 }
605
606 public long getLastMessageTransmitted() {
607 long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
608 if (timestamp == 0) {
609 synchronized (this.messages) {
610 for(int i = this.messages.size() - 1; i >= 0; --i) {
611 Message message = this.messages.get(i);
612 if (message.getStatus() == Message.STATUS_RECEIVED) {
613 return message.getTimeSent();
614 }
615 }
616 }
617 }
618 return timestamp;
619 }
620
621 public void setMutedTill(long value) {
622 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
623 }
624
625 public boolean isMuted() {
626 return SystemClock.elapsedRealtime() < this.getLongAttribute(
627 ATTRIBUTE_MUTED_TILL, 0);
628 }
629
630 public boolean setAttribute(String key, String value) {
631 try {
632 this.attributes.put(key, value);
633 return true;
634 } catch (JSONException e) {
635 return false;
636 }
637 }
638
639 public String getAttribute(String key) {
640 try {
641 return this.attributes.getString(key);
642 } catch (JSONException e) {
643 return null;
644 }
645 }
646
647 public int getIntAttribute(String key, int defaultValue) {
648 String value = this.getAttribute(key);
649 if (value == null) {
650 return defaultValue;
651 } else {
652 try {
653 return Integer.parseInt(value);
654 } catch (NumberFormatException e) {
655 return defaultValue;
656 }
657 }
658 }
659
660 public long getLongAttribute(String key, long defaultValue) {
661 String value = this.getAttribute(key);
662 if (value == null) {
663 return defaultValue;
664 } else {
665 try {
666 return Long.parseLong(value);
667 } catch (NumberFormatException e) {
668 return defaultValue;
669 }
670 }
671 }
672
673 public void add(Message message) {
674 message.setConversation(this);
675 synchronized (this.messages) {
676 this.messages.add(message);
677 }
678 }
679
680 public void addAll(int index, List<Message> messages) {
681 synchronized (this.messages) {
682 this.messages.addAll(index, messages);
683 }
684 }
685
686 public void sort() {
687 synchronized (this.messages) {
688 Collections.sort(this.messages, new Comparator<Message>() {
689 @Override
690 public int compare(Message left, Message right) {
691 if (left.getTimeSent() < right.getTimeSent()) {
692 return -1;
693 } else if (left.getTimeSent() > right.getTimeSent()) {
694 return 1;
695 } else {
696 return 0;
697 }
698 }
699 });
700 for(Message message : this.messages) {
701 message.untie();
702 }
703 }
704 }
705
706 public class Smp {
707 public static final int STATUS_NONE = 0;
708 public static final int STATUS_CONTACT_REQUESTED = 1;
709 public static final int STATUS_WE_REQUESTED = 2;
710 public static final int STATUS_FAILED = 3;
711 public static final int STATUS_FINISHED = 4;
712
713 public String secret = null;
714 public String hint = null;
715 public int status = 0;
716 }
717}