1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5
6import java.net.MalformedURLException;
7import java.net.URL;
8import java.util.Arrays;
9
10import eu.siacs.conversations.Config;
11import eu.siacs.conversations.utils.GeoHelper;
12import eu.siacs.conversations.utils.MimeUtils;
13import eu.siacs.conversations.utils.UIHelper;
14import eu.siacs.conversations.xmpp.jid.InvalidJidException;
15import eu.siacs.conversations.xmpp.jid.Jid;
16
17public class Message extends AbstractEntity {
18
19 public static final String TABLENAME = "messages";
20
21 public static final String MERGE_SEPARATOR = " \u200B\n\n";
22
23 public static final int STATUS_RECEIVED = 0;
24 public static final int STATUS_UNSEND = 1;
25 public static final int STATUS_SEND = 2;
26 public static final int STATUS_SEND_FAILED = 3;
27 public static final int STATUS_WAITING = 5;
28 public static final int STATUS_OFFERED = 6;
29 public static final int STATUS_SEND_RECEIVED = 7;
30 public static final int STATUS_SEND_DISPLAYED = 8;
31
32 public static final int ENCRYPTION_NONE = 0;
33 public static final int ENCRYPTION_PGP = 1;
34 public static final int ENCRYPTION_OTR = 2;
35 public static final int ENCRYPTION_DECRYPTED = 3;
36 public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
37
38 public static final int TYPE_TEXT = 0;
39 public static final int TYPE_IMAGE = 1;
40 public static final int TYPE_FILE = 2;
41 public static final int TYPE_STATUS = 3;
42 public static final int TYPE_PRIVATE = 4;
43
44 public static final String CONVERSATION = "conversationUuid";
45 public static final String COUNTERPART = "counterpart";
46 public static final String TRUE_COUNTERPART = "trueCounterpart";
47 public static final String BODY = "body";
48 public static final String TIME_SENT = "timeSent";
49 public static final String ENCRYPTION = "encryption";
50 public static final String STATUS = "status";
51 public static final String TYPE = "type";
52 public static final String REMOTE_MSG_ID = "remoteMsgId";
53 public static final String SERVER_MSG_ID = "serverMsgId";
54 public static final String RELATIVE_FILE_PATH = "relativeFilePath";
55 public static final String ME_COMMAND = "/me ";
56
57
58 public boolean markable = false;
59 protected String conversationUuid;
60 protected Jid counterpart;
61 protected Jid trueCounterpart;
62 protected String body;
63 protected String encryptedBody;
64 protected long timeSent;
65 protected int encryption;
66 protected int status;
67 protected int type;
68 private boolean isTrusted = true;
69 protected String relativeFilePath;
70 protected boolean read = true;
71 protected String remoteMsgId = null;
72 protected String serverMsgId = null;
73 protected Conversation conversation = null;
74 protected Transferable transferable = null;
75 private Message mNextMessage = null;
76 private Message mPreviousMessage = null;
77
78 private Message() {
79
80 }
81
82 public Message(Conversation conversation, String body, int encryption) {
83 this(conversation, body, encryption, STATUS_UNSEND);
84 }
85
86 public Message(Conversation conversation, String body, int encryption, int status) {
87 this(java.util.UUID.randomUUID().toString(),
88 conversation.getUuid(),
89 conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
90 null,
91 body,
92 System.currentTimeMillis(),
93 encryption,
94 status,
95 TYPE_TEXT,
96 null,
97 null,
98 null);
99 this.conversation = conversation;
100 }
101
102 private Message(final String uuid, final String conversationUUid, final Jid counterpart,
103 final Jid trueCounterpart, final String body, final long timeSent,
104 final int encryption, final int status, final int type, final String remoteMsgId,
105 final String relativeFilePath, final String serverMsgId) {
106 this.uuid = uuid;
107 this.conversationUuid = conversationUUid;
108 this.counterpart = counterpart;
109 this.trueCounterpart = trueCounterpart;
110 this.body = body;
111 this.timeSent = timeSent;
112 this.encryption = encryption;
113 this.status = status;
114 this.type = type;
115 this.remoteMsgId = remoteMsgId;
116 this.relativeFilePath = relativeFilePath;
117 this.serverMsgId = serverMsgId;
118 }
119
120 public static Message fromCursor(Cursor cursor) {
121 Jid jid;
122 try {
123 String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
124 if (value != null) {
125 jid = Jid.fromString(value, true);
126 } else {
127 jid = null;
128 }
129 } catch (InvalidJidException e) {
130 jid = null;
131 }
132 Jid trueCounterpart;
133 try {
134 String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
135 if (value != null) {
136 trueCounterpart = Jid.fromString(value, true);
137 } else {
138 trueCounterpart = null;
139 }
140 } catch (InvalidJidException e) {
141 trueCounterpart = null;
142 }
143 return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
144 cursor.getString(cursor.getColumnIndex(CONVERSATION)),
145 jid,
146 trueCounterpart,
147 cursor.getString(cursor.getColumnIndex(BODY)),
148 cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
149 cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
150 cursor.getInt(cursor.getColumnIndex(STATUS)),
151 cursor.getInt(cursor.getColumnIndex(TYPE)),
152 cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
153 cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
154 cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
155 }
156
157 public static Message createStatusMessage(Conversation conversation, String body) {
158 Message message = new Message();
159 message.setType(Message.TYPE_STATUS);
160 message.setConversation(conversation);
161 message.setBody(body);
162 return message;
163 }
164
165 @Override
166 public ContentValues getContentValues() {
167 ContentValues values = new ContentValues();
168 values.put(UUID, uuid);
169 values.put(CONVERSATION, conversationUuid);
170 if (counterpart == null) {
171 values.putNull(COUNTERPART);
172 } else {
173 values.put(COUNTERPART, counterpart.toString());
174 }
175 if (trueCounterpart == null) {
176 values.putNull(TRUE_COUNTERPART);
177 } else {
178 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
179 }
180 values.put(BODY, body);
181 values.put(TIME_SENT, timeSent);
182 values.put(ENCRYPTION, encryption);
183 values.put(STATUS, status);
184 values.put(TYPE, type);
185 values.put(REMOTE_MSG_ID, remoteMsgId);
186 values.put(RELATIVE_FILE_PATH, relativeFilePath);
187 values.put(SERVER_MSG_ID, serverMsgId);
188 return values;
189 }
190
191 public String getConversationUuid() {
192 return conversationUuid;
193 }
194
195 public Conversation getConversation() {
196 return this.conversation;
197 }
198
199 public void setConversation(Conversation conv) {
200 this.conversation = conv;
201 }
202
203 public Jid getCounterpart() {
204 return counterpart;
205 }
206
207 public void setCounterpart(final Jid counterpart) {
208 this.counterpart = counterpart;
209 }
210
211 public Contact getContact() {
212 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
213 return this.conversation.getContact();
214 } else {
215 if (this.trueCounterpart == null) {
216 return null;
217 } else {
218 return this.conversation.getAccount().getRoster()
219 .getContactFromRoster(this.trueCounterpart);
220 }
221 }
222 }
223
224 public String getBody() {
225 return body;
226 }
227
228 public void setBody(String body) {
229 this.body = body;
230 }
231
232 public long getTimeSent() {
233 return timeSent;
234 }
235
236 public int getEncryption() {
237 return encryption;
238 }
239
240 public void setEncryption(int encryption) {
241 this.encryption = encryption;
242 }
243
244 public int getStatus() {
245 return status;
246 }
247
248 public void setStatus(int status) {
249 this.status = status;
250 }
251
252 public String getRelativeFilePath() {
253 return this.relativeFilePath;
254 }
255
256 public void setRelativeFilePath(String path) {
257 this.relativeFilePath = path;
258 }
259
260 public String getRemoteMsgId() {
261 return this.remoteMsgId;
262 }
263
264 public void setRemoteMsgId(String id) {
265 this.remoteMsgId = id;
266 }
267
268 public String getServerMsgId() {
269 return this.serverMsgId;
270 }
271
272 public void setServerMsgId(String id) {
273 this.serverMsgId = id;
274 }
275
276 public boolean isRead() {
277 return this.read;
278 }
279
280 public void markRead() {
281 this.read = true;
282 }
283
284 public void markUnread() {
285 this.read = false;
286 }
287
288 public void setTime(long time) {
289 this.timeSent = time;
290 }
291
292 public String getEncryptedBody() {
293 return this.encryptedBody;
294 }
295
296 public void setEncryptedBody(String body) {
297 this.encryptedBody = body;
298 }
299
300 public int getType() {
301 return this.type;
302 }
303
304 public void setType(int type) {
305 this.type = type;
306 }
307
308 public void setTrueCounterpart(Jid trueCounterpart) {
309 this.trueCounterpart = trueCounterpart;
310 }
311
312 public Transferable getTransferable() {
313 return this.transferable;
314 }
315
316 public void setTransferable(Transferable transferable) {
317 this.transferable = transferable;
318 }
319
320 public boolean equals(Message message) {
321 if (this.serverMsgId != null && message.getServerMsgId() != null) {
322 return this.serverMsgId.equals(message.getServerMsgId());
323 } else if (this.body == null || this.counterpart == null) {
324 return false;
325 } else {
326 String body, otherBody;
327 if (this.hasFileOnRemoteHost()) {
328 body = getFileParams().url.toString();
329 otherBody = message.body == null ? null : message.body.trim();
330 } else {
331 body = this.body;
332 otherBody = message.body;
333 }
334 if (message.getRemoteMsgId() != null) {
335 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
336 && this.counterpart.equals(message.getCounterpart())
337 && body.equals(otherBody);
338 } else {
339 return this.remoteMsgId == null
340 && this.counterpart.equals(message.getCounterpart())
341 && body.equals(otherBody)
342 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
343 }
344 }
345 }
346
347 public Message next() {
348 synchronized (this.conversation.messages) {
349 if (this.mNextMessage == null) {
350 int index = this.conversation.messages.indexOf(this);
351 if (index < 0 || index >= this.conversation.messages.size() - 1) {
352 this.mNextMessage = null;
353 } else {
354 this.mNextMessage = this.conversation.messages.get(index + 1);
355 }
356 }
357 return this.mNextMessage;
358 }
359 }
360
361 public Message prev() {
362 synchronized (this.conversation.messages) {
363 if (this.mPreviousMessage == null) {
364 int index = this.conversation.messages.indexOf(this);
365 if (index <= 0 || index > this.conversation.messages.size()) {
366 this.mPreviousMessage = null;
367 } else {
368 this.mPreviousMessage = this.conversation.messages.get(index - 1);
369 }
370 }
371 return this.mPreviousMessage;
372 }
373 }
374
375 public boolean mergeable(final Message message) {
376 return message != null &&
377 (message.getType() == Message.TYPE_TEXT &&
378 this.getTransferable() == null &&
379 message.getTransferable() == null &&
380 message.getEncryption() != Message.ENCRYPTION_PGP &&
381 this.getType() == message.getType() &&
382 //this.getStatus() == message.getStatus() &&
383 isStatusMergeable(this.getStatus(), message.getStatus()) &&
384 this.getEncryption() == message.getEncryption() &&
385 this.getCounterpart() != null &&
386 this.getCounterpart().equals(message.getCounterpart()) &&
387 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
388 !GeoHelper.isGeoUri(message.getBody()) &&
389 !GeoHelper.isGeoUri(this.body) &&
390 message.treatAsDownloadable() == Decision.NEVER &&
391 this.treatAsDownloadable() == Decision.NEVER &&
392 !message.getBody().startsWith(ME_COMMAND) &&
393 !this.getBody().startsWith(ME_COMMAND) &&
394 !this.bodyIsHeart() &&
395 !message.bodyIsHeart()
396 );
397 }
398
399 private static boolean isStatusMergeable(int a, int b) {
400 return a == b || (
401 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
402 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
403 || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND)
404 || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED)
405 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
406 || (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED)
407 );
408 }
409
410 public String getMergedBody() {
411 final Message next = this.next();
412 if (this.mergeable(next)) {
413 return getBody().trim() + MERGE_SEPARATOR + next.getMergedBody();
414 }
415 return getBody().trim();
416 }
417
418 public boolean hasMeCommand() {
419 return getMergedBody().startsWith(ME_COMMAND);
420 }
421
422 public int getMergedStatus() {
423 final Message next = this.next();
424 if (this.mergeable(next)) {
425 return next.getStatus();
426 }
427 return getStatus();
428 }
429
430 public long getMergedTimeSent() {
431 Message next = this.next();
432 if (this.mergeable(next)) {
433 return next.getMergedTimeSent();
434 } else {
435 return getTimeSent();
436 }
437 }
438
439 public boolean wasMergedIntoPrevious() {
440 Message prev = this.prev();
441 return prev != null && prev.mergeable(this);
442 }
443
444 public boolean trusted() {
445 Contact contact = this.getContact();
446 return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
447 }
448
449 public boolean fixCounterpart() {
450 Presences presences = conversation.getContact().getPresences();
451 if (counterpart != null && presences.has(counterpart.getResourcepart())) {
452 return true;
453 } else if (presences.size() >= 1) {
454 try {
455 counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
456 conversation.getJid().getDomainpart(),
457 presences.asStringArray()[0]);
458 return true;
459 } catch (InvalidJidException e) {
460 counterpart = null;
461 return false;
462 }
463 } else {
464 counterpart = null;
465 return false;
466 }
467 }
468
469 public enum Decision {
470 MUST,
471 SHOULD,
472 NEVER,
473 }
474
475 private static String extractRelevantExtension(URL url) {
476 String path = url.getPath();
477 if (path == null || path.isEmpty()) {
478 return null;
479 }
480 String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
481 String[] extensionParts = filename.split("\\.");
482 if (extensionParts.length == 2) {
483 return extensionParts[extensionParts.length - 1];
484 } else if (extensionParts.length == 3 && Arrays
485 .asList(Transferable.VALID_CRYPTO_EXTENSIONS)
486 .contains(extensionParts[extensionParts.length - 1])) {
487 return extensionParts[extensionParts.length -2];
488 }
489 return null;
490 }
491
492 public String getMimeType() {
493 if (relativeFilePath != null) {
494 int start = relativeFilePath.lastIndexOf('.') + 1;
495 if (start < relativeFilePath.length()) {
496 return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
497 } else {
498 return null;
499 }
500 } else {
501 try {
502 return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
503 } catch (MalformedURLException e) {
504 return null;
505 }
506 }
507 }
508
509 public Decision treatAsDownloadable() {
510 if (body.trim().contains(" ")) {
511 return Decision.NEVER;
512 }
513 try {
514 URL url = new URL(body);
515 if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
516 return Decision.NEVER;
517 }
518 String extension = extractRelevantExtension(url);
519 if (extension == null) {
520 return Decision.NEVER;
521 }
522 String ref = url.getRef();
523 boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
524
525 if (encrypted) {
526 if (MimeUtils.guessMimeTypeFromExtension(extension) != null) {
527 return Decision.MUST;
528 } else {
529 return Decision.NEVER;
530 }
531 } else if (Arrays.asList(Transferable.VALID_IMAGE_EXTENSIONS).contains(extension)
532 || Arrays.asList(Transferable.WELL_KNOWN_EXTENSIONS).contains(extension)) {
533 return Decision.SHOULD;
534 } else {
535 return Decision.NEVER;
536 }
537
538 } catch (MalformedURLException e) {
539 return Decision.NEVER;
540 }
541 }
542
543 public boolean bodyIsHeart() {
544 return body != null && UIHelper.HEARTS.contains(body.trim());
545 }
546
547 public FileParams getFileParams() {
548 FileParams params = getLegacyFileParams();
549 if (params != null) {
550 return params;
551 }
552 params = new FileParams();
553 if (this.transferable != null) {
554 params.size = this.transferable.getFileSize();
555 }
556 if (body == null) {
557 return params;
558 }
559 String parts[] = body.split("\\|");
560 switch (parts.length) {
561 case 1:
562 try {
563 params.size = Long.parseLong(parts[0]);
564 } catch (NumberFormatException e) {
565 try {
566 params.url = new URL(parts[0]);
567 } catch (MalformedURLException e1) {
568 params.url = null;
569 }
570 }
571 break;
572 case 2:
573 case 4:
574 try {
575 params.url = new URL(parts[0]);
576 } catch (MalformedURLException e1) {
577 params.url = null;
578 }
579 try {
580 params.size = Long.parseLong(parts[1]);
581 } catch (NumberFormatException e) {
582 params.size = 0;
583 }
584 try {
585 params.width = Integer.parseInt(parts[2]);
586 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
587 params.width = 0;
588 }
589 try {
590 params.height = Integer.parseInt(parts[3]);
591 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
592 params.height = 0;
593 }
594 break;
595 case 3:
596 try {
597 params.size = Long.parseLong(parts[0]);
598 } catch (NumberFormatException e) {
599 params.size = 0;
600 }
601 try {
602 params.width = Integer.parseInt(parts[1]);
603 } catch (NumberFormatException e) {
604 params.width = 0;
605 }
606 try {
607 params.height = Integer.parseInt(parts[2]);
608 } catch (NumberFormatException e) {
609 params.height = 0;
610 }
611 break;
612 }
613 return params;
614 }
615
616 public FileParams getLegacyFileParams() {
617 FileParams params = new FileParams();
618 if (body == null) {
619 return params;
620 }
621 String parts[] = body.split(",");
622 if (parts.length == 3) {
623 try {
624 params.size = Long.parseLong(parts[0]);
625 } catch (NumberFormatException e) {
626 return null;
627 }
628 try {
629 params.width = Integer.parseInt(parts[1]);
630 } catch (NumberFormatException e) {
631 return null;
632 }
633 try {
634 params.height = Integer.parseInt(parts[2]);
635 } catch (NumberFormatException e) {
636 return null;
637 }
638 return params;
639 } else {
640 return null;
641 }
642 }
643
644 public void untie() {
645 this.mNextMessage = null;
646 this.mPreviousMessage = null;
647 }
648
649 public boolean isFileOrImage() {
650 return type == TYPE_FILE || type == TYPE_IMAGE;
651 }
652
653 public boolean hasFileOnRemoteHost() {
654 return isFileOrImage() && getFileParams().url != null;
655 }
656
657 public boolean needsUploading() {
658 return isFileOrImage() && getFileParams().url == null;
659 }
660
661 public class FileParams {
662 public URL url;
663 public long size = 0;
664 public int width = 0;
665 public int height = 0;
666 }
667
668 public void trust() {
669 this.isTrusted = true;
670 }
671
672 public void distrust() {
673 this.isTrusted = false;
674 }
675
676 public boolean isTrusted() {
677 return this.isTrusted;
678 }
679}