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