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 Downloadable downloadable = 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 Downloadable getDownloadable() {
312 return this.downloadable;
313 }
314
315 public void setDownloadable(Downloadable downloadable) {
316 this.downloadable = downloadable;
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.getDownloadable() == null &&
368 message.getDownloadable() == 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 enum Decision {
439 MUST,
440 SHOULD,
441 NEVER,
442 }
443
444 private static String extractRelevantExtension(URL url) {
445 String path = url.getPath();
446 if (path == null || path.isEmpty()) {
447 return null;
448 }
449 String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
450 String[] extensionParts = filename.split("\\.");
451 if (extensionParts.length == 2) {
452 return extensionParts[extensionParts.length - 1];
453 } else if (extensionParts.length == 3 && Arrays
454 .asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
455 .contains(extensionParts[extensionParts.length - 1])) {
456 return extensionParts[extensionParts.length -2];
457 }
458 return null;
459 }
460
461 public String getMimeType() {
462 if (relativeFilePath != null) {
463 int start = relativeFilePath.lastIndexOf('.') + 1;
464 if (start < relativeFilePath.length()) {
465 return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
466 } else {
467 return null;
468 }
469 } else {
470 try {
471 return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
472 } catch (MalformedURLException e) {
473 return null;
474 }
475 }
476 }
477
478 public Decision treatAsDownloadable() {
479 if (body.trim().contains(" ")) {
480 return Decision.NEVER;
481 }
482 try {
483 URL url = new URL(body);
484 if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
485 return Decision.NEVER;
486 }
487 String extension = extractRelevantExtension(url);
488 if (extension == null) {
489 return Decision.NEVER;
490 }
491 String ref = url.getRef();
492 boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
493
494 if (encrypted) {
495 if (MimeUtils.guessMimeTypeFromExtension(extension) != null) {
496 return Decision.MUST;
497 } else {
498 return Decision.NEVER;
499 }
500 } else if (Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(extension)
501 || Arrays.asList(Downloadable.WELL_KNOWN_EXTENSIONS).contains(extension)) {
502 return Decision.SHOULD;
503 } else {
504 return Decision.NEVER;
505 }
506
507 } catch (MalformedURLException e) {
508 return Decision.NEVER;
509 }
510 }
511
512 public boolean bodyIsHeart() {
513 return body != null && UIHelper.HEARTS.contains(body.trim());
514 }
515
516 public FileParams getFileParams() {
517 FileParams params = getLegacyFileParams();
518 if (params != null) {
519 return params;
520 }
521 params = new FileParams();
522 if (this.downloadable != null) {
523 params.size = this.downloadable.getFileSize();
524 }
525 if (body == null) {
526 return params;
527 }
528 String parts[] = body.split("\\|");
529 switch (parts.length) {
530 case 1:
531 try {
532 params.size = Long.parseLong(parts[0]);
533 } catch (NumberFormatException e) {
534 try {
535 params.url = new URL(parts[0]);
536 } catch (MalformedURLException e1) {
537 params.url = null;
538 }
539 }
540 break;
541 case 2:
542 case 4:
543 try {
544 params.url = new URL(parts[0]);
545 } catch (MalformedURLException e1) {
546 params.url = null;
547 }
548 try {
549 params.size = Long.parseLong(parts[1]);
550 } catch (NumberFormatException e) {
551 params.size = 0;
552 }
553 try {
554 params.width = Integer.parseInt(parts[2]);
555 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
556 params.width = 0;
557 }
558 try {
559 params.height = Integer.parseInt(parts[3]);
560 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
561 params.height = 0;
562 }
563 break;
564 case 3:
565 try {
566 params.size = Long.parseLong(parts[0]);
567 } catch (NumberFormatException e) {
568 params.size = 0;
569 }
570 try {
571 params.width = Integer.parseInt(parts[1]);
572 } catch (NumberFormatException e) {
573 params.width = 0;
574 }
575 try {
576 params.height = Integer.parseInt(parts[2]);
577 } catch (NumberFormatException e) {
578 params.height = 0;
579 }
580 break;
581 }
582 return params;
583 }
584
585 public FileParams getLegacyFileParams() {
586 FileParams params = new FileParams();
587 if (body == null) {
588 return params;
589 }
590 String parts[] = body.split(",");
591 if (parts.length == 3) {
592 try {
593 params.size = Long.parseLong(parts[0]);
594 } catch (NumberFormatException e) {
595 return null;
596 }
597 try {
598 params.width = Integer.parseInt(parts[1]);
599 } catch (NumberFormatException e) {
600 return null;
601 }
602 try {
603 params.height = Integer.parseInt(parts[2]);
604 } catch (NumberFormatException e) {
605 return null;
606 }
607 return params;
608 } else {
609 return null;
610 }
611 }
612
613 public void untie() {
614 this.mNextMessage = null;
615 this.mPreviousMessage = null;
616 }
617
618 public boolean isFileOrImage() {
619 return type == TYPE_FILE || type == TYPE_IMAGE;
620 }
621
622 public boolean hasFileOnRemoteHost() {
623 return isFileOrImage() && getFileParams().url != null;
624 }
625
626 public boolean needsUploading() {
627 return isFileOrImage() && getFileParams().url == null;
628 }
629
630 public class FileParams {
631 public URL url;
632 public long size = 0;
633 public int width = 0;
634 public int height = 0;
635 }
636}