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.List;
20
21import eu.siacs.conversations.xmpp.jid.InvalidJidException;
22import eu.siacs.conversations.xmpp.jid.Jid;
23
24public class Conversation extends AbstractEntity {
25 public static final String TABLENAME = "conversations";
26
27 public static final int STATUS_AVAILABLE = 0;
28 public static final int STATUS_ARCHIVED = 1;
29 public static final int STATUS_DELETED = 2;
30
31 public static final int MODE_MULTI = 1;
32 public static final int MODE_SINGLE = 0;
33
34 public static final String NAME = "name";
35 public static final String ACCOUNT = "accountUuid";
36 public static final String CONTACT = "contactUuid";
37 public static final String CONTACTJID = "contactJid";
38 public static final String STATUS = "status";
39 public static final String CREATED = "created";
40 public static final String MODE = "mode";
41 public static final String ATTRIBUTES = "attributes";
42
43 public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
44 public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
45 public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
46
47 private String name;
48 private String contactUuid;
49 private String accountUuid;
50 private Jid contactJid;
51 private int status;
52 private long created;
53 private int mode;
54
55 private JSONObject attributes = new JSONObject();
56
57 private Jid nextCounterpart;
58
59 protected final ArrayList<Message> messages = new ArrayList<>();
60 protected Account account = null;
61
62 private transient SessionImpl otrSession;
63
64 private transient String otrFingerprint = null;
65 private Smp mSmp = new Smp();
66
67 private String nextMessage;
68
69 private transient MucOptions mucOptions = null;
70
71 private byte[] symmetricKey;
72
73 private Bookmark bookmark;
74
75 public Conversation(final String name, final Account account, final Jid contactJid,
76 final int mode) {
77 this(java.util.UUID.randomUUID().toString(), name, null, account
78 .getUuid(), contactJid, System.currentTimeMillis(),
79 STATUS_AVAILABLE, mode, "");
80 this.account = account;
81 }
82
83 public Conversation(final String uuid, final String name, final String contactUuid,
84 final String accountUuid, final Jid contactJid, final long created, final int status,
85 final int mode, final String attributes) {
86 this.uuid = uuid;
87 this.name = name;
88 this.contactUuid = contactUuid;
89 this.accountUuid = accountUuid;
90 this.contactJid = contactJid;
91 this.created = created;
92 this.status = status;
93 this.mode = mode;
94 try {
95 this.attributes = new JSONObject(attributes == null ? "" : attributes);
96 } catch (JSONException e) {
97 this.attributes = new JSONObject();
98 }
99 }
100
101 public List<Message> getMessages() {
102 return messages;
103 }
104
105 public boolean isRead() {
106 return (this.messages == null) || (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
107 }
108
109 public void markRead() {
110 if (this.messages == null) {
111 return;
112 }
113 for (int i = this.messages.size() - 1; i >= 0; --i) {
114 if (messages.get(i).isRead()) {
115 break;
116 }
117 this.messages.get(i).markRead();
118 }
119 }
120
121 public Message getLatestMarkableMessage() {
122 if (this.messages == null) {
123 return null;
124 }
125 for (int i = this.messages.size() - 1; i >= 0; --i) {
126 if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
127 && this.messages.get(i).markable) {
128 if (this.messages.get(i).isRead()) {
129 return null;
130 } else {
131 return this.messages.get(i);
132 }
133 }
134 }
135 return null;
136 }
137
138 public Message getLatestMessage() {
139 if ((this.messages == null) || (this.messages.size() == 0)) {
140 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
141 message.setTime(getCreated());
142 return message;
143 } else {
144 Message message = this.messages.get(this.messages.size() - 1);
145 message.setConversation(this);
146 return message;
147 }
148 }
149
150 public String getName() {
151 if (getMode() == MODE_MULTI) {
152 if (getMucOptions().getSubject() != null) {
153 return getMucOptions().getSubject();
154 } else if (bookmark != null && bookmark.getName() != null) {
155 return bookmark.getName();
156 } else {
157 String generatedName = getMucOptions().createNameFromParticipants();
158 if (generatedName != null) {
159 return generatedName;
160 } else {
161 return getContactJid().getLocalpart();
162 }
163 }
164 } else {
165 return this.getContact().getDisplayName();
166 }
167 }
168
169 public String getProfilePhotoString() {
170 return this.getContact().getProfilePhoto();
171 }
172
173 public String getAccountUuid() {
174 return this.accountUuid;
175 }
176
177 public Account getAccount() {
178 return this.account;
179 }
180
181 public Contact getContact() {
182 return this.account.getRoster().getContact(this.contactJid);
183 }
184
185 public void setAccount(Account account) {
186 this.account = account;
187 }
188
189 public Jid getContactJid() {
190 return this.contactJid;
191 }
192
193 public int getStatus() {
194 return this.status;
195 }
196
197 public long getCreated() {
198 return this.created;
199 }
200
201 public ContentValues getContentValues() {
202 ContentValues values = new ContentValues();
203 values.put(UUID, uuid);
204 values.put(NAME, name);
205 values.put(CONTACT, contactUuid);
206 values.put(ACCOUNT, accountUuid);
207 values.put(CONTACTJID, contactJid.toString());
208 values.put(CREATED, created);
209 values.put(STATUS, status);
210 values.put(MODE, mode);
211 values.put(ATTRIBUTES, attributes.toString());
212 return values;
213 }
214
215 public static Conversation fromCursor(Cursor cursor) {
216 Jid jid;
217 try {
218 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)));
219 } catch (final InvalidJidException e) {
220 // Borked DB..
221 jid = null;
222 }
223 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
224 cursor.getString(cursor.getColumnIndex(NAME)),
225 cursor.getString(cursor.getColumnIndex(CONTACT)),
226 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
227 jid,
228 cursor.getLong(cursor.getColumnIndex(CREATED)),
229 cursor.getInt(cursor.getColumnIndex(STATUS)),
230 cursor.getInt(cursor.getColumnIndex(MODE)),
231 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
232 }
233
234 public void setStatus(int status) {
235 this.status = status;
236 }
237
238 public int getMode() {
239 return this.mode;
240 }
241
242 public void setMode(int mode) {
243 this.mode = mode;
244 }
245
246 public SessionImpl startOtrSession(String presence, boolean sendStart) {
247 if (this.otrSession != null) {
248 return this.otrSession;
249 } else {
250 final SessionID sessionId = new SessionID(this.getContactJid().toBareJid().toString(),
251 presence,
252 "xmpp");
253 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine());
254 try {
255 if (sendStart) {
256 this.otrSession.startSession();
257 return this.otrSession;
258 }
259 return this.otrSession;
260 } catch (OtrException e) {
261 return null;
262 }
263 }
264
265 }
266
267 public SessionImpl getOtrSession() {
268 return this.otrSession;
269 }
270
271 public void resetOtrSession() {
272 this.otrFingerprint = null;
273 this.otrSession = null;
274 this.mSmp.hint = null;
275 this.mSmp.secret = null;
276 this.mSmp.status = Smp.STATUS_NONE;
277 }
278
279 public Smp smp() {
280 return mSmp;
281 }
282
283 public void startOtrIfNeeded() {
284 if (this.otrSession != null
285 && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
286 try {
287 this.otrSession.startSession();
288 } catch (OtrException e) {
289 this.resetOtrSession();
290 }
291 }
292 }
293
294 public boolean endOtrIfNeeded() {
295 if (this.otrSession != null) {
296 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
297 try {
298 this.otrSession.endSession();
299 this.resetOtrSession();
300 return true;
301 } catch (OtrException e) {
302 this.resetOtrSession();
303 return false;
304 }
305 } else {
306 this.resetOtrSession();
307 return false;
308 }
309 } else {
310 return false;
311 }
312 }
313
314 public boolean hasValidOtrSession() {
315 return this.otrSession != null;
316 }
317
318 public String getOtrFingerprint() {
319 if (this.otrFingerprint == null) {
320 try {
321 if (getOtrSession() == null) {
322 return "";
323 }
324 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession()
325 .getRemotePublicKey();
326 StringBuilder builder = new StringBuilder(
327 new OtrCryptoEngineImpl().getFingerprint(remotePubKey));
328 builder.insert(8, " ");
329 builder.insert(17, " ");
330 builder.insert(26, " ");
331 builder.insert(35, " ");
332 this.otrFingerprint = builder.toString();
333 } catch (final OtrCryptoException ignored) {
334
335 }
336 }
337 return this.otrFingerprint;
338 }
339
340 public void verifyOtrFingerprint() {
341 getContact().addOtrFingerprint(getOtrFingerprint());
342 }
343
344 public boolean isOtrFingerprintVerified() {
345 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
346 }
347
348 public synchronized MucOptions getMucOptions() {
349 if (this.mucOptions == null) {
350 this.mucOptions = new MucOptions(this);
351 }
352 return this.mucOptions;
353 }
354
355 public void resetMucOptions() {
356 this.mucOptions = null;
357 }
358
359 public void setContactJid(final Jid jid) {
360 this.contactJid = jid;
361 }
362
363 public void setNextCounterpart(Jid jid) {
364 this.nextCounterpart = jid;
365 }
366
367 public Jid getNextCounterpart() {
368 return this.nextCounterpart;
369 }
370
371 public int getLatestEncryption() {
372 int latestEncryption = this.getLatestMessage().getEncryption();
373 if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
374 || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
375 return Message.ENCRYPTION_PGP;
376 } else {
377 return latestEncryption;
378 }
379 }
380
381 public int getNextEncryption(boolean force) {
382 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
383 if (next == -1) {
384 int latest = this.getLatestEncryption();
385 if (latest == Message.ENCRYPTION_NONE) {
386 if (force && getMode() == MODE_SINGLE) {
387 return Message.ENCRYPTION_OTR;
388 } else if (getContact().getPresences().size() == 1) {
389 if (getContact().getOtrFingerprints().size() >= 1) {
390 return Message.ENCRYPTION_OTR;
391 } else {
392 return latest;
393 }
394 } else {
395 return latest;
396 }
397 } else {
398 return latest;
399 }
400 }
401 if (next == Message.ENCRYPTION_NONE && force
402 && getMode() == MODE_SINGLE) {
403 return Message.ENCRYPTION_OTR;
404 } else {
405 return next;
406 }
407 }
408
409 public void setNextEncryption(int encryption) {
410 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
411 }
412
413 public String getNextMessage() {
414 if (this.nextMessage == null) {
415 return "";
416 } else {
417 return this.nextMessage;
418 }
419 }
420
421 public boolean smpRequested() {
422 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
423 }
424
425 public void setNextMessage(String message) {
426 this.nextMessage = message;
427 }
428
429 public void setSymmetricKey(byte[] key) {
430 this.symmetricKey = key;
431 }
432
433 public byte[] getSymmetricKey() {
434 return this.symmetricKey;
435 }
436
437 public void setBookmark(Bookmark bookmark) {
438 this.bookmark = bookmark;
439 this.bookmark.setConversation(this);
440 }
441
442 public void deregisterWithBookmark() {
443 if (this.bookmark != null) {
444 this.bookmark.setConversation(null);
445 }
446 }
447
448 public Bookmark getBookmark() {
449 return this.bookmark;
450 }
451
452 public boolean hasDuplicateMessage(Message message) {
453 for (int i = this.getMessages().size() - 1; i >= 0; --i) {
454 if (this.messages.get(i).equals(message)) {
455 return true;
456 }
457 }
458 return false;
459 }
460
461 public void setMutedTill(long value) {
462 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
463 }
464
465 public boolean isMuted() {
466 return SystemClock.elapsedRealtime() < this.getLongAttribute(
467 ATTRIBUTE_MUTED_TILL, 0);
468 }
469
470 public boolean setAttribute(String key, String value) {
471 try {
472 this.attributes.put(key, value);
473 return true;
474 } catch (JSONException e) {
475 return false;
476 }
477 }
478
479 public String getAttribute(String key) {
480 try {
481 return this.attributes.getString(key);
482 } catch (JSONException e) {
483 return null;
484 }
485 }
486
487 public int getIntAttribute(String key, int defaultValue) {
488 String value = this.getAttribute(key);
489 if (value == null) {
490 return defaultValue;
491 } else {
492 try {
493 return Integer.parseInt(value);
494 } catch (NumberFormatException e) {
495 return defaultValue;
496 }
497 }
498 }
499
500 public long getLongAttribute(String key, long defaultValue) {
501 String value = this.getAttribute(key);
502 if (value == null) {
503 return defaultValue;
504 } else {
505 try {
506 return Long.parseLong(value);
507 } catch (NumberFormatException e) {
508 return defaultValue;
509 }
510 }
511 }
512
513 public void add(Message message) {
514 message.setConversation(this);
515 synchronized (this.messages) {
516 this.messages.add(message);
517 }
518 }
519
520 public void addAll(int index, List<Message> messages) {
521 synchronized (this.messages) {
522 this.messages.addAll(index, messages);
523 }
524 }
525
526 public class Smp {
527 public static final int STATUS_NONE = 0;
528 public static final int STATUS_CONTACT_REQUESTED = 1;
529 public static final int STATUS_WE_REQUESTED = 2;
530 public static final int STATUS_FAILED = 3;
531 public static final int STATUS_FINISHED = 4;
532
533 public String secret = null;
534 public String hint = null;
535 public int status = 0;
536 }
537}