1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.graphics.Color;
6import android.util.Log;
7import com.google.common.base.Strings;
8import com.google.common.collect.Collections2;
9import com.google.common.collect.ImmutableSet;
10import com.google.common.primitives.Longs;
11import eu.siacs.conversations.Config;
12import eu.siacs.conversations.crypto.axolotl.AxolotlService;
13import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
14import eu.siacs.conversations.http.URL;
15import eu.siacs.conversations.services.AvatarService;
16import eu.siacs.conversations.ui.util.PresenceSelector;
17import eu.siacs.conversations.utils.CryptoHelper;
18import eu.siacs.conversations.utils.Emoticons;
19import eu.siacs.conversations.utils.GeoHelper;
20import eu.siacs.conversations.utils.MessageUtils;
21import eu.siacs.conversations.utils.MimeUtils;
22import eu.siacs.conversations.utils.UIHelper;
23import eu.siacs.conversations.xmpp.Jid;
24import java.lang.ref.WeakReference;
25import java.util.ArrayList;
26import java.util.Collection;
27import java.util.Collections;
28import java.util.Iterator;
29import java.util.List;
30import java.util.Set;
31import java.util.concurrent.CopyOnWriteArraySet;
32import org.json.JSONException;
33
34public class Message extends AbstractEntity implements AvatarService.Avatarable {
35
36 public static final String TABLENAME = "messages";
37
38 public static final int STATUS_RECEIVED = 0;
39 public static final int STATUS_UNSEND = 1;
40 public static final int STATUS_SEND = 2;
41 public static final int STATUS_SEND_FAILED = 3;
42 public static final int STATUS_WAITING = 5;
43 public static final int STATUS_OFFERED = 6;
44 public static final int STATUS_SEND_RECEIVED = 7;
45 public static final int STATUS_SEND_DISPLAYED = 8;
46
47 public static final int ENCRYPTION_NONE = 0;
48 public static final int ENCRYPTION_PGP = 1;
49 public static final int ENCRYPTION_OTR = 2;
50 public static final int ENCRYPTION_DECRYPTED = 3;
51 public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
52 public static final int ENCRYPTION_AXOLOTL = 5;
53 public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
54 public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
55
56 public static final int TYPE_TEXT = 0;
57 public static final int TYPE_IMAGE = 1;
58 public static final int TYPE_FILE = 2;
59 public static final int TYPE_STATUS = 3;
60 public static final int TYPE_PRIVATE = 4;
61 public static final int TYPE_PRIVATE_FILE = 5;
62 public static final int TYPE_RTP_SESSION = 6;
63
64 public static final String CONVERSATION = "conversationUuid";
65 public static final String COUNTERPART = "counterpart";
66 public static final String TRUE_COUNTERPART = "trueCounterpart";
67 public static final String BODY = "body";
68 public static final String BODY_LANGUAGE = "bodyLanguage";
69 public static final String TIME_SENT = "timeSent";
70 public static final String ENCRYPTION = "encryption";
71 public static final String STATUS = "status";
72 public static final String TYPE = "type";
73 public static final String CARBON = "carbon";
74 public static final String OOB = "oob";
75 public static final String EDITED = "edited";
76 public static final String REMOTE_MSG_ID = "remoteMsgId";
77 public static final String SERVER_MSG_ID = "serverMsgId";
78 public static final String RELATIVE_FILE_PATH = "relativeFilePath";
79 public static final String FINGERPRINT = "axolotl_fingerprint";
80 public static final String READ = "read";
81 public static final String ERROR_MESSAGE = "errorMsg";
82 public static final String READ_BY_MARKERS = "readByMarkers";
83 public static final String MARKABLE = "markable";
84 public static final String DELETED = "deleted";
85 public static final String OCCUPANT_ID = "occupantId";
86 public static final String REACTIONS = "reactions";
87 public static final String ME_COMMAND = "/me ";
88
89 public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
90
91 public boolean markable = false;
92 protected String conversationUuid;
93 protected Jid counterpart;
94 protected Jid trueCounterpart;
95 protected String body;
96 protected String encryptedBody;
97 protected long timeSent;
98 protected int encryption;
99 protected int status;
100 protected int type;
101 protected boolean deleted = false;
102 protected boolean carbon = false;
103 protected boolean oob = false;
104 protected List<Edit> edits = new ArrayList<>();
105 protected String relativeFilePath;
106 protected boolean read = true;
107 protected String remoteMsgId = null;
108 private String bodyLanguage = null;
109 protected String serverMsgId = null;
110 private final Conversational conversation;
111 protected Transferable transferable = null;
112 private Message mNextMessage = null;
113 private Message mPreviousMessage = null;
114 private String axolotlFingerprint = null;
115 private String errorMessage = null;
116 private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
117 private String occupantId;
118 private Collection<Reaction> reactions = Collections.emptyList();
119
120 private Boolean isGeoUri = null;
121 private Boolean isEmojisOnly = null;
122 private Boolean treatAsDownloadable = null;
123 private FileParams fileParams = null;
124 private List<MucOptions.User> counterparts;
125 private WeakReference<MucOptions.User> user;
126
127 protected Message(Conversational conversation) {
128 this.conversation = conversation;
129 }
130
131 public Message(Conversational conversation, String body, int encryption) {
132 this(conversation, body, encryption, STATUS_UNSEND);
133 }
134
135 public Message(Conversational conversation, String body, int encryption, int status) {
136 this(
137 conversation,
138 java.util.UUID.randomUUID().toString(),
139 conversation.getUuid(),
140 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
141 null,
142 body,
143 System.currentTimeMillis(),
144 encryption,
145 status,
146 TYPE_TEXT,
147 false,
148 null,
149 null,
150 null,
151 null,
152 true,
153 null,
154 false,
155 null,
156 null,
157 false,
158 false,
159 null,
160 null,
161 Collections.emptyList());
162 }
163
164 public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
165 this(
166 conversation,
167 java.util.UUID.randomUUID().toString(),
168 conversation.getUuid(),
169 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
170 null,
171 null,
172 System.currentTimeMillis(),
173 Message.ENCRYPTION_NONE,
174 status,
175 type,
176 false,
177 remoteMsgId,
178 null,
179 null,
180 null,
181 true,
182 null,
183 false,
184 null,
185 null,
186 false,
187 false,
188 null,
189 null,
190 Collections.emptyList());
191 }
192
193 protected Message(
194 final Conversational conversation,
195 final String uuid,
196 final String conversationUUid,
197 final Jid counterpart,
198 final Jid trueCounterpart,
199 final String body,
200 final long timeSent,
201 final int encryption,
202 final int status,
203 final int type,
204 final boolean carbon,
205 final String remoteMsgId,
206 final String relativeFilePath,
207 final String serverMsgId,
208 final String fingerprint,
209 final boolean read,
210 final String edited,
211 final boolean oob,
212 final String errorMessage,
213 final Set<ReadByMarker> readByMarkers,
214 final boolean markable,
215 final boolean deleted,
216 final String bodyLanguage,
217 final String occupantId,
218 final Collection<Reaction> reactions) {
219 this.conversation = conversation;
220 this.uuid = uuid;
221 this.conversationUuid = conversationUUid;
222 this.counterpart = counterpart;
223 this.trueCounterpart = trueCounterpart;
224 this.body = body == null ? "" : body;
225 this.timeSent = timeSent;
226 this.encryption = encryption;
227 this.status = status;
228 this.type = type;
229 this.carbon = carbon;
230 this.remoteMsgId = remoteMsgId;
231 this.relativeFilePath = relativeFilePath;
232 this.serverMsgId = serverMsgId;
233 this.axolotlFingerprint = fingerprint;
234 this.read = read;
235 this.edits = Edit.fromJson(edited);
236 this.oob = oob;
237 this.errorMessage = errorMessage;
238 this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers;
239 this.markable = markable;
240 this.deleted = deleted;
241 this.bodyLanguage = bodyLanguage;
242 this.occupantId = occupantId;
243 this.reactions = reactions;
244 }
245
246 public static Message fromCursor(final Cursor cursor, final Conversation conversation) {
247 return new Message(
248 conversation,
249 cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
250 cursor.getString(cursor.getColumnIndexOrThrow(CONVERSATION)),
251 fromString(cursor.getString(cursor.getColumnIndexOrThrow(COUNTERPART))),
252 fromString(cursor.getString(cursor.getColumnIndexOrThrow(TRUE_COUNTERPART))),
253 cursor.getString(cursor.getColumnIndexOrThrow(BODY)),
254 cursor.getLong(cursor.getColumnIndexOrThrow(TIME_SENT)),
255 cursor.getInt(cursor.getColumnIndexOrThrow(ENCRYPTION)),
256 cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)),
257 cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)),
258 cursor.getInt(cursor.getColumnIndexOrThrow(CARBON)) > 0,
259 cursor.getString(cursor.getColumnIndexOrThrow(REMOTE_MSG_ID)),
260 cursor.getString(cursor.getColumnIndexOrThrow(RELATIVE_FILE_PATH)),
261 cursor.getString(cursor.getColumnIndexOrThrow(SERVER_MSG_ID)),
262 cursor.getString(cursor.getColumnIndexOrThrow(FINGERPRINT)),
263 cursor.getInt(cursor.getColumnIndexOrThrow(READ)) > 0,
264 cursor.getString(cursor.getColumnIndexOrThrow(EDITED)),
265 cursor.getInt(cursor.getColumnIndexOrThrow(OOB)) > 0,
266 cursor.getString(cursor.getColumnIndexOrThrow(ERROR_MESSAGE)),
267 ReadByMarker.fromJsonString(
268 cursor.getString(cursor.getColumnIndexOrThrow(READ_BY_MARKERS))),
269 cursor.getInt(cursor.getColumnIndexOrThrow(MARKABLE)) > 0,
270 cursor.getInt(cursor.getColumnIndexOrThrow(DELETED)) > 0,
271 cursor.getString(cursor.getColumnIndexOrThrow(BODY_LANGUAGE)),
272 cursor.getString(cursor.getColumnIndexOrThrow(OCCUPANT_ID)),
273 Reaction.fromString(cursor.getString(cursor.getColumnIndexOrThrow(REACTIONS))));
274 }
275
276 private static Jid fromString(String value) {
277 try {
278 if (value != null) {
279 return Jid.of(value);
280 }
281 } catch (IllegalArgumentException e) {
282 return null;
283 }
284 return null;
285 }
286
287 public static Message createStatusMessage(Conversation conversation, String body) {
288 final Message message = new Message(conversation);
289 message.setType(Message.TYPE_STATUS);
290 message.setStatus(Message.STATUS_RECEIVED);
291 message.body = body;
292 return message;
293 }
294
295 public static Message createLoadMoreMessage(Conversation conversation) {
296 final Message message = new Message(conversation);
297 message.setType(Message.TYPE_STATUS);
298 message.body = "LOAD_MORE";
299 return message;
300 }
301
302 @Override
303 public ContentValues getContentValues() {
304 final var values = new ContentValues();
305 values.put(UUID, uuid);
306 values.put(CONVERSATION, conversationUuid);
307 if (counterpart == null) {
308 values.putNull(COUNTERPART);
309 } else {
310 values.put(COUNTERPART, counterpart.toString());
311 }
312 if (trueCounterpart == null) {
313 values.putNull(TRUE_COUNTERPART);
314 } else {
315 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
316 }
317 values.put(
318 BODY,
319 body.length() > Config.MAX_STORAGE_MESSAGE_CHARS
320 ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS)
321 : body);
322 values.put(TIME_SENT, timeSent);
323 values.put(ENCRYPTION, encryption);
324 values.put(STATUS, status);
325 values.put(TYPE, type);
326 values.put(CARBON, carbon ? 1 : 0);
327 values.put(REMOTE_MSG_ID, remoteMsgId);
328 values.put(RELATIVE_FILE_PATH, relativeFilePath);
329 values.put(SERVER_MSG_ID, serverMsgId);
330 values.put(FINGERPRINT, axolotlFingerprint);
331 values.put(READ, read ? 1 : 0);
332 try {
333 values.put(EDITED, Edit.toJson(edits));
334 } catch (JSONException e) {
335 Log.e(Config.LOGTAG, "error persisting json for edits", e);
336 }
337 values.put(OOB, oob ? 1 : 0);
338 values.put(ERROR_MESSAGE, errorMessage);
339 values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
340 values.put(MARKABLE, markable ? 1 : 0);
341 values.put(DELETED, deleted ? 1 : 0);
342 values.put(BODY_LANGUAGE, bodyLanguage);
343 values.put(OCCUPANT_ID, occupantId);
344 values.put(REACTIONS, Reaction.toString(this.reactions));
345 return values;
346 }
347
348 public String getConversationUuid() {
349 return conversationUuid;
350 }
351
352 public Conversational getConversation() {
353 return this.conversation;
354 }
355
356 public Jid getCounterpart() {
357 return counterpart;
358 }
359
360 public void setCounterpart(final Jid counterpart) {
361 this.counterpart = counterpart;
362 }
363
364 public Contact getContact() {
365 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
366 return this.conversation.getContact();
367 } else {
368 if (this.trueCounterpart == null) {
369 return null;
370 } else {
371 return this.conversation
372 .getAccount()
373 .getRoster()
374 .getContactFromContactList(this.trueCounterpart);
375 }
376 }
377 }
378
379 public String getBody() {
380 return body;
381 }
382
383 public synchronized void setBody(String body) {
384 if (body == null) {
385 throw new Error("You should not set the message body to null");
386 }
387 this.body = body;
388 this.isGeoUri = null;
389 this.isEmojisOnly = null;
390 this.treatAsDownloadable = null;
391 this.fileParams = null;
392 }
393
394 public void setMucUser(MucOptions.User user) {
395 this.user = new WeakReference<>(user);
396 }
397
398 public boolean sameMucUser(Message otherMessage) {
399 final MucOptions.User thisUser = this.user == null ? null : this.user.get();
400 final MucOptions.User otherUser =
401 otherMessage.user == null ? null : otherMessage.user.get();
402 return thisUser != null && thisUser == otherUser;
403 }
404
405 public String getErrorMessage() {
406 return errorMessage;
407 }
408
409 public boolean setErrorMessage(String message) {
410 boolean changed =
411 (message != null && !message.equals(errorMessage))
412 || (message == null && errorMessage != null);
413 this.errorMessage = message;
414 return changed;
415 }
416
417 public long getTimeSent() {
418 return timeSent;
419 }
420
421 public int getEncryption() {
422 return encryption;
423 }
424
425 public void setEncryption(int encryption) {
426 this.encryption = encryption;
427 }
428
429 public int getStatus() {
430 return status;
431 }
432
433 public void setStatus(int status) {
434 this.status = status;
435 }
436
437 public String getRelativeFilePath() {
438 return this.relativeFilePath;
439 }
440
441 public void setRelativeFilePath(String path) {
442 this.relativeFilePath = path;
443 }
444
445 public String getRemoteMsgId() {
446 return this.remoteMsgId;
447 }
448
449 public void setRemoteMsgId(String id) {
450 this.remoteMsgId = id;
451 }
452
453 public String getServerMsgId() {
454 return this.serverMsgId;
455 }
456
457 public void setServerMsgId(String id) {
458 this.serverMsgId = id;
459 }
460
461 public boolean isRead() {
462 return this.read;
463 }
464
465 public boolean isDeleted() {
466 return this.deleted;
467 }
468
469 public void setDeleted(boolean deleted) {
470 this.deleted = deleted;
471 }
472
473 public void markRead() {
474 this.read = true;
475 }
476
477 public void markUnread() {
478 this.read = false;
479 }
480
481 public void setTime(long time) {
482 this.timeSent = time;
483 }
484
485 public String getEncryptedBody() {
486 return this.encryptedBody;
487 }
488
489 public void setEncryptedBody(String body) {
490 this.encryptedBody = body;
491 }
492
493 public int getType() {
494 return this.type;
495 }
496
497 public void setType(int type) {
498 this.type = type;
499 }
500
501 public boolean isCarbon() {
502 return carbon;
503 }
504
505 public void setCarbon(boolean carbon) {
506 this.carbon = carbon;
507 }
508
509 public void putEdited(String edited, String serverMsgId) {
510 final Edit edit = new Edit(edited, serverMsgId);
511 if (this.edits.size() < 128 && !this.edits.contains(edit)) {
512 this.edits.add(edit);
513 }
514 }
515
516 public String getBodyLanguage() {
517 return this.bodyLanguage;
518 }
519
520 public void setBodyLanguage(String language) {
521 this.bodyLanguage = language;
522 }
523
524 public boolean edited() {
525 return !this.edits.isEmpty();
526 }
527
528 public void setTrueCounterpart(Jid trueCounterpart) {
529 this.trueCounterpart = trueCounterpart;
530 }
531
532 public Jid getTrueCounterpart() {
533 return this.trueCounterpart;
534 }
535
536 public Transferable getTransferable() {
537 return this.transferable;
538 }
539
540 public synchronized void setTransferable(Transferable transferable) {
541 this.fileParams = null;
542 this.transferable = transferable;
543 }
544
545 public boolean addReadByMarker(final ReadByMarker readByMarker) {
546 if (readByMarker.getRealJid() != null) {
547 if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
548 return false;
549 }
550 } else if (readByMarker.getFullJid() != null) {
551 if (readByMarker.getFullJid().equals(counterpart)) {
552 return false;
553 }
554 }
555 if (this.readByMarkers.add(readByMarker)) {
556 if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
557 Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
558 while (iterator.hasNext()) {
559 ReadByMarker marker = iterator.next();
560 if (marker.getRealJid() == null
561 && readByMarker.getFullJid().equals(marker.getFullJid())) {
562 iterator.remove();
563 }
564 }
565 }
566 return true;
567 } else {
568 return false;
569 }
570 }
571
572 public Set<ReadByMarker> getReadByMarkers() {
573 return ImmutableSet.copyOf(this.readByMarkers);
574 }
575
576 public Set<Jid> getReadyByTrue() {
577 return ImmutableSet.copyOf(
578 Collections2.transform(
579 Collections2.filter(this.readByMarkers, m -> m.getRealJid() != null),
580 ReadByMarker::getRealJid));
581 }
582
583 boolean similar(Message message) {
584 if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
585 return this.serverMsgId.equals(message.getServerMsgId())
586 || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
587 } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
588 return true;
589 } else if (this.body == null || this.counterpart == null) {
590 return false;
591 } else {
592 String body, otherBody;
593 if (this.hasFileOnRemoteHost()) {
594 body = getFileParams().url;
595 otherBody = message.body == null ? null : message.body.trim();
596 } else {
597 body = this.body;
598 otherBody = message.body;
599 }
600 final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
601 if (message.getRemoteMsgId() != null) {
602 final boolean hasUuid =
603 CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
604 if (hasUuid
605 && matchingCounterpart
606 && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
607 return true;
608 }
609 return (message.getRemoteMsgId().equals(this.remoteMsgId)
610 || message.getRemoteMsgId().equals(this.uuid))
611 && matchingCounterpart
612 && (body.equals(otherBody)
613 || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
614 } else {
615 return this.remoteMsgId == null
616 && matchingCounterpart
617 && body.equals(otherBody)
618 && Math.abs(this.getTimeSent() - message.getTimeSent())
619 < Config.MESSAGE_MERGE_WINDOW * 1000;
620 }
621 }
622 }
623
624 public Message next() {
625 if (this.conversation instanceof Conversation c) {
626 synchronized (c.messages) {
627 if (this.mNextMessage == null) {
628 int index = c.messages.indexOf(this);
629 if (index < 0 || index >= c.messages.size() - 1) {
630 this.mNextMessage = null;
631 } else {
632 this.mNextMessage = c.messages.get(index + 1);
633 }
634 }
635 return this.mNextMessage;
636 }
637 } else {
638 throw new AssertionError("Calling next should be disabled for stubs");
639 }
640 }
641
642 public Message prev() {
643 if (this.conversation instanceof Conversation c) {
644 synchronized (c.messages) {
645 if (this.mPreviousMessage == null) {
646 int index = c.messages.indexOf(this);
647 if (index <= 0 || index > c.messages.size()) {
648 this.mPreviousMessage = null;
649 } else {
650 this.mPreviousMessage = c.messages.get(index - 1);
651 }
652 }
653 }
654 return this.mPreviousMessage;
655 } else {
656 throw new AssertionError("Calling prev should be disabled for stubs");
657 }
658 }
659
660 public boolean isLastCorrectableMessage() {
661 Message next = next();
662 while (next != null) {
663 if (next.isEditable()) {
664 return false;
665 }
666 next = next.next();
667 }
668 return isEditable();
669 }
670
671 public boolean isEditable() {
672 return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
673 }
674
675 public void setCounterparts(List<MucOptions.User> counterparts) {
676 this.counterparts = counterparts;
677 }
678
679 public List<MucOptions.User> getCounterparts() {
680 return this.counterparts;
681 }
682
683 @Override
684 public int getAvatarBackgroundColor() {
685 if (type == Message.TYPE_STATUS
686 && getCounterparts() != null
687 && getCounterparts().size() > 1) {
688 return Color.TRANSPARENT;
689 } else {
690 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
691 }
692 }
693
694 @Override
695 public String getAvatarName() {
696 return UIHelper.getMessageDisplayName(this);
697 }
698
699 public boolean isOOb() {
700 return oob;
701 }
702
703 public void setOccupantId(final String id) {
704 this.occupantId = id;
705 }
706
707 public String getOccupantId() {
708 return this.occupantId;
709 }
710
711 public Collection<Reaction> getReactions() {
712 return this.reactions;
713 }
714
715 public Reaction.Aggregated getAggregatedReactions() {
716 return Reaction.aggregated(this.reactions);
717 }
718
719 public void setReactions(final Collection<Reaction> reactions) {
720 this.reactions = reactions;
721 }
722
723 public boolean hasMeCommand() {
724 return this.body.trim().startsWith(ME_COMMAND);
725 }
726
727 public boolean trusted() {
728 Contact contact = this.getContact();
729 return status > STATUS_RECEIVED
730 || (contact != null && (contact.showInContactList() || contact.isSelf()));
731 }
732
733 public boolean fixCounterpart() {
734 final Presences presences = conversation.getContact().getPresences();
735 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
736 return true;
737 } else if (presences.isEmpty()) {
738 counterpart = null;
739 return false;
740 } else {
741 counterpart =
742 PresenceSelector.getNextCounterpart(
743 getContact(), presences.toResourceArray()[0]);
744 return true;
745 }
746 }
747
748 public void setUuid(String uuid) {
749 this.uuid = uuid;
750 }
751
752 public String getEditedId() {
753 if (this.edits.isEmpty()) {
754 throw new IllegalStateException("Attempting to access unedited message");
755 }
756 return edits.get(edits.size() - 1).getEditedId();
757 }
758
759 public String getEditedIdWireFormat() {
760 if (this.edits.isEmpty()) {
761 throw new IllegalStateException("Attempting to access unedited message");
762 }
763 return edits.get(0).getEditedId();
764 }
765
766 public void setOob(boolean isOob) {
767 this.oob = isOob;
768 }
769
770 public String getMimeType() {
771 String extension;
772 if (relativeFilePath != null) {
773 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
774 } else {
775 final String url = URL.tryParse(body.split("\n")[0]);
776 if (url == null) {
777 return null;
778 }
779 extension = MimeUtils.extractRelevantExtension(url);
780 }
781 return MimeUtils.guessMimeTypeFromExtension(extension);
782 }
783
784 public synchronized boolean treatAsDownloadable() {
785 if (treatAsDownloadable == null) {
786 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
787 }
788 return treatAsDownloadable;
789 }
790
791 public synchronized boolean bodyIsOnlyEmojis() {
792 if (isEmojisOnly == null) {
793 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
794 }
795 return isEmojisOnly;
796 }
797
798 public synchronized boolean isGeoUri() {
799 if (isGeoUri == null) {
800 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
801 }
802 return isGeoUri;
803 }
804
805 public synchronized void resetFileParams() {
806 this.fileParams = null;
807 }
808
809 public synchronized FileParams getFileParams() {
810 if (fileParams == null) {
811 fileParams = new FileParams();
812 if (this.transferable != null) {
813 fileParams.size = this.transferable.getFileSize();
814 }
815 final String[] parts = body == null ? new String[0] : body.split("\\|");
816 switch (parts.length) {
817 case 1:
818 try {
819 fileParams.size = Long.parseLong(parts[0]);
820 } catch (final NumberFormatException e) {
821 fileParams.url = URL.tryParse(parts[0]);
822 }
823 break;
824 case 5:
825 fileParams.runtime = parseInt(parts[4]);
826 case 4:
827 fileParams.width = parseInt(parts[2]);
828 fileParams.height = parseInt(parts[3]);
829 case 2:
830 fileParams.url = URL.tryParse(parts[0]);
831 fileParams.size = Longs.tryParse(parts[1]);
832 break;
833 case 3:
834 fileParams.size = Longs.tryParse(parts[0]);
835 fileParams.width = parseInt(parts[1]);
836 fileParams.height = parseInt(parts[2]);
837 break;
838 }
839 }
840 return fileParams;
841 }
842
843 private static int parseInt(String value) {
844 try {
845 return Integer.parseInt(value);
846 } catch (NumberFormatException e) {
847 return 0;
848 }
849 }
850
851 public void untie() {
852 this.mNextMessage = null;
853 this.mPreviousMessage = null;
854 }
855
856 public boolean isPrivateMessage() {
857 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
858 }
859
860 public boolean isFileOrImage() {
861 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
862 }
863
864 public boolean isTypeText() {
865 return type == TYPE_TEXT || type == TYPE_PRIVATE;
866 }
867
868 public boolean hasFileOnRemoteHost() {
869 return isFileOrImage() && getFileParams().url != null;
870 }
871
872 public boolean needsUploading() {
873 return isFileOrImage() && getFileParams().url == null;
874 }
875
876 public static class FileParams {
877 public String url;
878 public Long size = null;
879 public int width = 0;
880 public int height = 0;
881 public int runtime = 0;
882
883 public long getSize() {
884 return size == null ? 0 : size;
885 }
886 }
887
888 public void setFingerprint(String fingerprint) {
889 this.axolotlFingerprint = fingerprint;
890 }
891
892 public String getFingerprint() {
893 return axolotlFingerprint;
894 }
895
896 public boolean isTrusted() {
897 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
898 final FingerprintStatus s =
899 axolotlService != null
900 ? axolotlService.getFingerprintTrust(axolotlFingerprint)
901 : null;
902 return s != null && s.isTrusted();
903 }
904
905 private int getPreviousEncryption() {
906 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
907 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
908 continue;
909 }
910 return iterator.getEncryption();
911 }
912 return ENCRYPTION_NONE;
913 }
914
915 private int getNextEncryption() {
916 if (this.conversation instanceof Conversation c) {
917 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
918 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
919 continue;
920 }
921 return iterator.getEncryption();
922 }
923 return c.getNextEncryption();
924 } else {
925 throw new AssertionError(
926 "This should never be called since isInValidSession should be disabled for"
927 + " stubs");
928 }
929 }
930
931 public boolean isValidInSession() {
932 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
933 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
934
935 boolean inUnencryptedSession =
936 pastEncryption == ENCRYPTION_NONE
937 || futureEncryption == ENCRYPTION_NONE
938 || pastEncryption != futureEncryption;
939
940 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
941 }
942
943 private static int getCleanedEncryption(int encryption) {
944 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
945 return ENCRYPTION_PGP;
946 }
947 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
948 || encryption == ENCRYPTION_AXOLOTL_FAILED) {
949 return ENCRYPTION_AXOLOTL;
950 }
951 return encryption;
952 }
953
954 public static boolean configurePrivateMessage(final Message message) {
955 return configurePrivateMessage(message, false);
956 }
957
958 public static boolean configurePrivateFileMessage(final Message message) {
959 return configurePrivateMessage(message, true);
960 }
961
962 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
963 final Conversation conversation;
964 if (message.conversation instanceof Conversation) {
965 conversation = (Conversation) message.conversation;
966 } else {
967 return false;
968 }
969 if (conversation.getMode() == Conversation.MODE_MULTI) {
970 final Jid nextCounterpart = conversation.getNextCounterpart();
971 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
972 }
973 return false;
974 }
975
976 public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
977 final Conversation conversation;
978 if (message.conversation instanceof Conversation) {
979 conversation = (Conversation) message.conversation;
980 } else {
981 return false;
982 }
983 return configurePrivateMessage(conversation, message, counterpart, false);
984 }
985
986 private static boolean configurePrivateMessage(
987 final Conversation conversation,
988 final Message message,
989 final Jid counterpart,
990 final boolean isFile) {
991 if (counterpart == null) {
992 return false;
993 }
994 message.setCounterpart(counterpart);
995 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
996 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
997 return true;
998 }
999}