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 de.gultsch.common.Patterns;
12import eu.siacs.conversations.Config;
13import eu.siacs.conversations.crypto.axolotl.AxolotlService;
14import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
15import eu.siacs.conversations.http.URL;
16import eu.siacs.conversations.services.AvatarService;
17import eu.siacs.conversations.ui.util.PresenceSelector;
18import eu.siacs.conversations.utils.CryptoHelper;
19import eu.siacs.conversations.utils.Emoticons;
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()) < 20_000;
619 }
620 }
621 }
622
623 public Message next() {
624 if (this.conversation instanceof Conversation c) {
625 synchronized (c.messages) {
626 if (this.mNextMessage == null) {
627 int index = c.messages.indexOf(this);
628 if (index < 0 || index >= c.messages.size() - 1) {
629 this.mNextMessage = null;
630 } else {
631 this.mNextMessage = c.messages.get(index + 1);
632 }
633 }
634 return this.mNextMessage;
635 }
636 } else {
637 throw new AssertionError("Calling next should be disabled for stubs");
638 }
639 }
640
641 public Message prev() {
642 if (this.conversation instanceof Conversation c) {
643 synchronized (c.messages) {
644 if (this.mPreviousMessage == null) {
645 int index = c.messages.indexOf(this);
646 if (index <= 0 || index > c.messages.size()) {
647 this.mPreviousMessage = null;
648 } else {
649 this.mPreviousMessage = c.messages.get(index - 1);
650 }
651 }
652 }
653 return this.mPreviousMessage;
654 } else {
655 throw new AssertionError("Calling prev should be disabled for stubs");
656 }
657 }
658
659 public boolean isLastCorrectableMessage() {
660 Message next = next();
661 while (next != null) {
662 if (next.isEditable()) {
663 return false;
664 }
665 next = next.next();
666 }
667 return isEditable();
668 }
669
670 public boolean isEditable() {
671 return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
672 }
673
674 public void setCounterparts(List<MucOptions.User> counterparts) {
675 this.counterparts = counterparts;
676 }
677
678 public List<MucOptions.User> getCounterparts() {
679 return this.counterparts;
680 }
681
682 @Override
683 public int getAvatarBackgroundColor() {
684 if (type == Message.TYPE_STATUS
685 && getCounterparts() != null
686 && getCounterparts().size() > 1) {
687 return Color.TRANSPARENT;
688 } else {
689 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
690 }
691 }
692
693 @Override
694 public String getAvatarName() {
695 return UIHelper.getMessageDisplayName(this);
696 }
697
698 public boolean isOOb() {
699 return oob;
700 }
701
702 public void setOccupantId(final String id) {
703 this.occupantId = id;
704 }
705
706 public String getOccupantId() {
707 return this.occupantId;
708 }
709
710 public Collection<Reaction> getReactions() {
711 return this.reactions;
712 }
713
714 public Reaction.Aggregated getAggregatedReactions() {
715 return Reaction.aggregated(this.reactions);
716 }
717
718 public void setReactions(final Collection<Reaction> reactions) {
719 this.reactions = reactions;
720 }
721
722 public boolean hasMeCommand() {
723 return this.body.trim().startsWith(ME_COMMAND);
724 }
725
726 public boolean trusted() {
727 final var contact = this.getContact();
728 return status > STATUS_RECEIVED
729 || (contact != null && (contact.showInContactList() || contact.isSelf()));
730 }
731
732 public boolean fixCounterpart() {
733 final Presences presences = conversation.getContact().getPresences();
734 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
735 return true;
736 } else if (presences.isEmpty()) {
737 counterpart = null;
738 return false;
739 } else {
740 counterpart =
741 PresenceSelector.getNextCounterpart(
742 getContact(), presences.toResourceArray()[0]);
743 return true;
744 }
745 }
746
747 public void setUuid(String uuid) {
748 this.uuid = uuid;
749 }
750
751 public String getEditedId() {
752 if (this.edits.isEmpty()) {
753 throw new IllegalStateException("Attempting to access unedited message");
754 }
755 return edits.get(edits.size() - 1).getEditedId();
756 }
757
758 public String getEditedIdWireFormat() {
759 if (this.edits.isEmpty()) {
760 throw new IllegalStateException("Attempting to access unedited message");
761 }
762 return edits.get(0).getEditedId();
763 }
764
765 public void setOob(boolean isOob) {
766 this.oob = isOob;
767 }
768
769 public String getMimeType() {
770 String extension;
771 if (relativeFilePath != null) {
772 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
773 } else {
774 final String url = URL.tryParse(body.split("\n")[0]);
775 if (url == null) {
776 return null;
777 }
778 extension = MimeUtils.extractRelevantExtension(url);
779 }
780 return MimeUtils.guessMimeTypeFromExtension(extension);
781 }
782
783 public synchronized boolean treatAsDownloadable() {
784 if (treatAsDownloadable == null) {
785 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
786 }
787 return treatAsDownloadable;
788 }
789
790 public synchronized boolean bodyIsOnlyEmojis() {
791 if (isEmojisOnly == null) {
792 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
793 }
794 return isEmojisOnly;
795 }
796
797 public synchronized boolean isGeoUri() {
798 if (isGeoUri == null) {
799 isGeoUri = Patterns.URI_GEO.matcher(body).matches();
800 }
801 return isGeoUri;
802 }
803
804 public synchronized void resetFileParams() {
805 this.fileParams = null;
806 }
807
808 public synchronized FileParams getFileParams() {
809 if (fileParams == null) {
810 fileParams = new FileParams();
811 if (this.transferable != null) {
812 fileParams.size = this.transferable.getFileSize();
813 }
814 final String[] parts = body == null ? new String[0] : body.split("\\|");
815 switch (parts.length) {
816 case 1:
817 try {
818 fileParams.size = Long.parseLong(parts[0]);
819 } catch (final NumberFormatException e) {
820 fileParams.url = URL.tryParse(parts[0]);
821 }
822 break;
823 case 5:
824 fileParams.runtime = parseInt(parts[4]);
825 case 4:
826 fileParams.width = parseInt(parts[2]);
827 fileParams.height = parseInt(parts[3]);
828 case 2:
829 fileParams.url = URL.tryParse(parts[0]);
830 fileParams.size = Longs.tryParse(parts[1]);
831 break;
832 case 3:
833 fileParams.size = Longs.tryParse(parts[0]);
834 fileParams.width = parseInt(parts[1]);
835 fileParams.height = parseInt(parts[2]);
836 break;
837 }
838 }
839 return fileParams;
840 }
841
842 private static int parseInt(String value) {
843 try {
844 return Integer.parseInt(value);
845 } catch (NumberFormatException e) {
846 return 0;
847 }
848 }
849
850 public void untie() {
851 this.mNextMessage = null;
852 this.mPreviousMessage = null;
853 }
854
855 public boolean isPrivateMessage() {
856 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
857 }
858
859 public boolean isFileOrImage() {
860 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
861 }
862
863 public boolean isTypeText() {
864 return type == TYPE_TEXT || type == TYPE_PRIVATE;
865 }
866
867 public boolean hasFileOnRemoteHost() {
868 return isFileOrImage() && getFileParams().url != null;
869 }
870
871 public boolean needsUploading() {
872 return isFileOrImage() && getFileParams().url == null;
873 }
874
875 public static class FileParams {
876 public String url;
877 public Long size = null;
878 public int width = 0;
879 public int height = 0;
880 public int runtime = 0;
881
882 public long getSize() {
883 return size == null ? 0 : size;
884 }
885 }
886
887 public void setFingerprint(String fingerprint) {
888 this.axolotlFingerprint = fingerprint;
889 }
890
891 public String getFingerprint() {
892 return axolotlFingerprint;
893 }
894
895 public boolean isTrusted() {
896 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
897 final FingerprintStatus s =
898 axolotlService != null
899 ? axolotlService.getFingerprintTrust(axolotlFingerprint)
900 : null;
901 return s != null && s.isTrusted();
902 }
903
904 private int getPreviousEncryption() {
905 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
906 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
907 continue;
908 }
909 return iterator.getEncryption();
910 }
911 return ENCRYPTION_NONE;
912 }
913
914 private int getNextEncryption() {
915 if (this.conversation instanceof Conversation c) {
916 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
917 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
918 continue;
919 }
920 return iterator.getEncryption();
921 }
922 return c.getNextEncryption();
923 } else {
924 throw new AssertionError(
925 "This should never be called since isInValidSession should be disabled for"
926 + " stubs");
927 }
928 }
929
930 public boolean isValidInSession() {
931 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
932 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
933
934 boolean inUnencryptedSession =
935 pastEncryption == ENCRYPTION_NONE
936 || futureEncryption == ENCRYPTION_NONE
937 || pastEncryption != futureEncryption;
938
939 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
940 }
941
942 private static int getCleanedEncryption(int encryption) {
943 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
944 return ENCRYPTION_PGP;
945 }
946 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
947 || encryption == ENCRYPTION_AXOLOTL_FAILED) {
948 return ENCRYPTION_AXOLOTL;
949 }
950 return encryption;
951 }
952
953 public static void configurePrivateMessage(final Message message) {
954 configurePrivateMessage(message, false);
955 }
956
957 public static boolean configurePrivateFileMessage(final Message message) {
958 return configurePrivateMessage(message, true);
959 }
960
961 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
962 if (message.conversation instanceof Conversation conversation) {
963 if (conversation.getMode() == Conversation.MODE_MULTI) {
964 final Jid nextCounterpart = conversation.getNextCounterpart();
965 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
966 }
967 }
968 return false;
969 }
970
971 public static void configurePrivateMessage(final Message message, final Jid counterpart) {
972 if (message.conversation instanceof Conversation conversation) {
973 configurePrivateMessage(conversation, message, counterpart, false);
974 }
975 }
976
977 private static boolean configurePrivateMessage(
978 final Conversation conversation,
979 final Message message,
980 final Jid counterpart,
981 final boolean isFile) {
982 if (counterpart == null) {
983 return false;
984 }
985 message.setCounterpart(counterpart);
986 final var mucOptions = conversation.getMucOptions();
987 if (counterpart.equals(mucOptions.getSelf().getFullJid())) {
988 message.setTrueCounterpart(conversation.getAccount().getJid().asBareJid());
989 } else {
990 final var user = mucOptions.findUserByFullJid(counterpart);
991 if (user != null) {
992 message.setTrueCounterpart(user.getRealJid());
993 message.setOccupantId(user.getOccupantId());
994 }
995 }
996 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
997 return true;
998 }
999}