1package eu.siacs.conversations.parser;
2
3import android.util.Log;
4import android.util.Pair;
5import com.google.common.base.Strings;
6import com.google.common.collect.ImmutableSet;
7import eu.siacs.conversations.AppSettings;
8import eu.siacs.conversations.Config;
9import eu.siacs.conversations.R;
10import eu.siacs.conversations.crypto.axolotl.AxolotlService;
11import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
12import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
13import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException;
14import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
15import eu.siacs.conversations.entities.Account;
16import eu.siacs.conversations.entities.Contact;
17import eu.siacs.conversations.entities.Conversation;
18import eu.siacs.conversations.entities.Conversational;
19import eu.siacs.conversations.entities.Message;
20import eu.siacs.conversations.entities.MucOptions;
21import eu.siacs.conversations.entities.Reaction;
22import eu.siacs.conversations.entities.ReadByMarker;
23import eu.siacs.conversations.entities.ReceiptRequest;
24import eu.siacs.conversations.entities.RtpSessionStatus;
25import eu.siacs.conversations.http.HttpConnectionManager;
26import eu.siacs.conversations.services.MessageArchiveService;
27import eu.siacs.conversations.services.XmppConnectionService;
28import eu.siacs.conversations.utils.CryptoHelper;
29import eu.siacs.conversations.xml.Element;
30import eu.siacs.conversations.xml.LocalizedContent;
31import eu.siacs.conversations.xml.Namespace;
32import eu.siacs.conversations.xmpp.Jid;
33import eu.siacs.conversations.xmpp.XmppConnection;
34import eu.siacs.conversations.xmpp.chatstate.ChatState;
35import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
36import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
37import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
38import eu.siacs.conversations.xmpp.manager.PubSubManager;
39import eu.siacs.conversations.xmpp.manager.RosterManager;
40import im.conversations.android.xmpp.model.Extension;
41import im.conversations.android.xmpp.model.axolotl.Encrypted;
42import im.conversations.android.xmpp.model.carbons.Received;
43import im.conversations.android.xmpp.model.carbons.Sent;
44import im.conversations.android.xmpp.model.conference.DirectInvite;
45import im.conversations.android.xmpp.model.correction.Replace;
46import im.conversations.android.xmpp.model.forward.Forwarded;
47import im.conversations.android.xmpp.model.markers.Displayed;
48import im.conversations.android.xmpp.model.muc.user.MucUser;
49import im.conversations.android.xmpp.model.occupant.OccupantId;
50import im.conversations.android.xmpp.model.oob.OutOfBandData;
51import im.conversations.android.xmpp.model.pubsub.event.Event;
52import im.conversations.android.xmpp.model.reactions.Reactions;
53import im.conversations.android.xmpp.model.receipts.Request;
54import im.conversations.android.xmpp.model.unique.StanzaId;
55import java.text.SimpleDateFormat;
56import java.util.Arrays;
57import java.util.Collections;
58import java.util.Date;
59import java.util.List;
60import java.util.Locale;
61import java.util.Set;
62import java.util.UUID;
63import java.util.function.Consumer;
64
65public class MessageParser extends AbstractParser
66 implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
67
68 private static final SimpleDateFormat TIME_FORMAT =
69 new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
70
71 private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
72 Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
73
74 public MessageParser(final XmppConnectionService service, final XmppConnection connection) {
75 super(service, connection);
76 }
77
78 private static String extractStanzaId(
79 final im.conversations.android.xmpp.model.stanza.Message packet,
80 final boolean isTypeGroupChat,
81 final Conversation conversation) {
82 final Jid by;
83 final boolean safeToExtract;
84 if (isTypeGroupChat) {
85 by = conversation.getJid().asBareJid();
86 safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
87 } else {
88 Account account = conversation.getAccount();
89 by = account.getJid().asBareJid();
90 safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
91 }
92 return safeToExtract ? StanzaId.get(packet, by) : null;
93 }
94
95 private static String extractStanzaId(
96 final Account account,
97 final im.conversations.android.xmpp.model.stanza.Message packet) {
98 final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
99 return safeToExtract ? StanzaId.get(packet, account.getJid().asBareJid()) : null;
100 }
101
102 private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
103 final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
104 Jid result =
105 item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
106 return result != null ? result : fallback;
107 }
108
109 private boolean extractChatState(
110 Conversation c,
111 final boolean isTypeGroupChat,
112 final im.conversations.android.xmpp.model.stanza.Message packet) {
113 ChatState state = ChatState.parse(packet);
114 if (state != null && c != null) {
115 final Account account = c.getAccount();
116 final Jid from = packet.getFrom();
117 if (from.asBareJid().equals(account.getJid().asBareJid())) {
118 c.setOutgoingChatState(state);
119 if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
120 if (c.getContact().isSelf()) {
121 return false;
122 }
123 mXmppConnectionService.markRead(c);
124 activateGracePeriod(account);
125 }
126 return false;
127 } else {
128 if (isTypeGroupChat) {
129 MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
130 if (user != null) {
131 return user.setChatState(state);
132 } else {
133 return false;
134 }
135 } else {
136 return c.setIncomingChatState(state);
137 }
138 }
139 }
140 return false;
141 }
142
143 private Message parseAxolotlChat(
144 final Encrypted axolotlMessage,
145 final Jid from,
146 final Conversation conversation,
147 final int status,
148 final boolean checkedForDuplicates,
149 final boolean postpone) {
150 final AxolotlService service = conversation.getAccount().getAxolotlService();
151 final XmppAxolotlMessage xmppAxolotlMessage;
152 try {
153 xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
154 } catch (final Exception e) {
155 Log.d(
156 Config.LOGTAG,
157 conversation.getAccount().getJid().asBareJid()
158 + ": invalid omemo message received "
159 + e.getMessage());
160 return null;
161 }
162 if (xmppAxolotlMessage.hasPayload()) {
163 final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
164 try {
165 plaintextMessage =
166 service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
167 } catch (BrokenSessionException e) {
168 if (checkedForDuplicates) {
169 if (service.trustedOrPreviouslyResponded(from.asBareJid())) {
170 service.reportBrokenSessionException(e, postpone);
171 return new Message(
172 conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
173 } else {
174 Log.d(
175 Config.LOGTAG,
176 "ignoring broken session exception because contact was not"
177 + " trusted");
178 return new Message(
179 conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
180 }
181 } else {
182 Log.d(
183 Config.LOGTAG,
184 "ignoring broken session exception because checkForDuplicates failed");
185 return null;
186 }
187 } catch (NotEncryptedForThisDeviceException e) {
188 return new Message(
189 conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
190 } catch (OutdatedSenderException e) {
191 return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
192 }
193 if (plaintextMessage != null) {
194 Message finishedMessage =
195 new Message(
196 conversation,
197 plaintextMessage.getPlaintext(),
198 Message.ENCRYPTION_AXOLOTL,
199 status);
200 finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
201 Log.d(
202 Config.LOGTAG,
203 AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())
204 + " Received Message with session fingerprint: "
205 + plaintextMessage.getFingerprint());
206 return finishedMessage;
207 }
208 } else {
209 Log.d(
210 Config.LOGTAG,
211 conversation.getAccount().getJid().asBareJid()
212 + ": received OMEMO key transport message");
213 service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
214 }
215 return null;
216 }
217
218 private Invite extractInvite(final im.conversations.android.xmpp.model.stanza.Message message) {
219 final Element mucUser = message.findChild("x", Namespace.MUC_USER);
220 if (mucUser != null) {
221 final Element invite = mucUser.findChild("invite");
222 if (invite != null) {
223 final String password = mucUser.findChildContent("password");
224 final Jid from = Jid.Invalid.getNullForInvalid(invite.getAttributeAsJid("from"));
225 final Jid to = Jid.Invalid.getNullForInvalid(invite.getAttributeAsJid("to"));
226 if (to != null && from == null) {
227 Log.d(Config.LOGTAG, "do not parse outgoing mediated invite " + message);
228 return null;
229 }
230 final Jid room = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
231 if (room == null) {
232 return null;
233 }
234 return new Invite(room, password, false, from);
235 }
236 }
237 final var conference = message.getExtension(DirectInvite.class);
238 if (conference != null) {
239 Jid from = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
240 Jid room = Jid.Invalid.getNullForInvalid(conference.getAttributeAsJid("jid"));
241 if (room == null) {
242 return null;
243 }
244 return new Invite(room, conference.getAttribute("password"), true, from);
245 }
246 return null;
247 }
248
249 private boolean handleErrorMessage(
250 final Account account,
251 final im.conversations.android.xmpp.model.stanza.Message packet) {
252 if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) {
253 if (packet.fromServer(account)) {
254 final var forwarded =
255 getForwardedMessagePacket(packet, "received", Namespace.CARBONS);
256 if (forwarded != null) {
257 return handleErrorMessage(account, forwarded.first);
258 }
259 }
260 final Jid from = packet.getFrom();
261 final String id = packet.getId();
262 if (from != null && id != null) {
263 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
264 final String sessionId =
265 id.substring(
266 JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
267 mXmppConnectionService
268 .getJingleConnectionManager()
269 .updateProposedSessionDiscovered(
270 account,
271 from,
272 sessionId,
273 JingleConnectionManager.DeviceDiscoveryState.FAILED);
274 return true;
275 }
276 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
277 final String sessionId =
278 id.substring(
279 JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
280 final String message = extractErrorMessage(packet);
281 mXmppConnectionService
282 .getJingleConnectionManager()
283 .failProceed(account, from, sessionId, message);
284 return true;
285 }
286 mXmppConnectionService.markMessage(
287 account,
288 from.asBareJid(),
289 id,
290 Message.STATUS_SEND_FAILED,
291 extractErrorMessage(packet));
292 final Element error = packet.findChild("error");
293 final boolean pingWorthyError =
294 error != null
295 && (error.hasChild("not-acceptable")
296 || error.hasChild("remote-server-timeout")
297 || error.hasChild("remote-server-not-found"));
298 if (pingWorthyError) {
299 Conversation conversation = mXmppConnectionService.find(account, from);
300 if (conversation != null
301 && conversation.getMode() == Conversational.MODE_MULTI) {
302 if (conversation.getMucOptions().online()) {
303 Log.d(
304 Config.LOGTAG,
305 account.getJid().asBareJid()
306 + ": received ping worthy error for seemingly online"
307 + " muc at "
308 + from);
309 getManager(MultiUserChatManager.class).pingAndRejoin(conversation);
310 }
311 }
312 }
313 }
314 return true;
315 }
316 return false;
317 }
318
319 @Override
320 public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
321 final var account = connection.getAccount();
322 if (handleErrorMessage(account, original)) {
323 return;
324 }
325 final im.conversations.android.xmpp.model.stanza.Message packet;
326 Long timestamp = null;
327 boolean isCarbon = false;
328 String serverMsgId = null;
329 final Element fin =
330 original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
331 if (fin != null) {
332 mXmppConnectionService
333 .getMessageArchiveService()
334 .processFinLegacy(fin, original.getFrom());
335 return;
336 }
337 final Element result = MessageArchiveService.Version.findResult(original);
338 final String queryId = result == null ? null : result.getAttribute("queryid");
339 final MessageArchiveService.Query query =
340 queryId == null
341 ? null
342 : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
343 final boolean offlineMessagesRetrieved = connection.isOfflineMessagesRetrieved();
344 if (query != null && query.validFrom(original.getFrom())) {
345 final var f = getForwardedMessagePacket(original, "result", query.version.namespace);
346 if (f == null) {
347 return;
348 }
349 timestamp = f.second;
350 packet = f.first;
351 serverMsgId = result.getAttribute("id");
352 query.incrementMessageCount();
353
354 if (query.isImplausibleFrom(packet.getFrom())) {
355 Log.d(Config.LOGTAG, "found implausible from in MUC MAM archive");
356 return;
357 }
358
359 if (handleErrorMessage(account, packet)) {
360 return;
361 }
362 } else if (query != null) {
363 Log.d(
364 Config.LOGTAG,
365 account.getJid().asBareJid()
366 + ": received mam result with invalid from ("
367 + original.getFrom()
368 + ") or queryId ("
369 + queryId
370 + ")");
371 return;
372 } else if (original.fromServer(account)
373 && original.getType()
374 != im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT) {
375 Pair<im.conversations.android.xmpp.model.stanza.Message, Long> f;
376 f = getForwardedMessagePacket(original, Received.class);
377 f = f == null ? getForwardedMessagePacket(original, Sent.class) : f;
378 packet = f != null ? f.first : original;
379 if (handleErrorMessage(account, packet)) {
380 return;
381 }
382 timestamp = f != null ? f.second : null;
383 isCarbon = f != null;
384 } else {
385 packet = original;
386 }
387
388 if (timestamp == null) {
389 timestamp =
390 AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
391 }
392 final LocalizedContent body = packet.getBody();
393 final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
394 final boolean isTypeGroupChat =
395 packet.getType()
396 == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
397 final var encrypted =
398 packet.getOnlyExtension(im.conversations.android.xmpp.model.pgp.Encrypted.class);
399 final String pgpEncrypted = encrypted == null ? null : encrypted.getContent();
400
401 final var oob = packet.getExtension(OutOfBandData.class);
402 final String oobUrl = oob != null ? oob.getURL() : null;
403 final var replace = packet.getExtension(Replace.class);
404 final var replacementId = replace == null ? null : replace.getId();
405 final var axolotlEncrypted = packet.getOnlyExtension(Encrypted.class);
406 int status;
407 final Jid counterpart;
408 final Jid to = packet.getTo();
409 final Jid from = packet.getFrom();
410 final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
411 final String remoteMsgId;
412 if (originId != null && originId.getAttribute("id") != null) {
413 remoteMsgId = originId.getAttribute("id");
414 } else {
415 remoteMsgId = packet.getId();
416 }
417 boolean notify = false;
418
419 if (from == null || !Jid.Invalid.isValid(from) || !Jid.Invalid.isValid(to)) {
420 Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
421 return;
422 }
423 if (query != null && !query.muc() && isTypeGroupChat) {
424 Log.e(
425 Config.LOGTAG,
426 account.getJid().asBareJid()
427 + ": received groupchat ("
428 + from
429 + ") message on regular MAM request. skipping");
430 return;
431 }
432 final Jid mucTrueCounterPart;
433 final OccupantId occupant;
434 if (isTypeGroupChat) {
435 final Conversation conversation =
436 mXmppConnectionService.find(account, from.asBareJid());
437 final Jid mucTrueCounterPartByPresence;
438 if (conversation != null) {
439 final var mucOptions = conversation.getMucOptions();
440 occupant =
441 mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null;
442 final var user =
443 occupant == null ? null : mucOptions.findUserByOccupantId(occupant.getId());
444 mucTrueCounterPartByPresence = user == null ? null : user.getRealJid();
445 } else {
446 occupant = null;
447 mucTrueCounterPartByPresence = null;
448 }
449 mucTrueCounterPart =
450 getTrueCounterpart(
451 (query != null && query.safeToExtractTrueCounterpart())
452 ? mucUserElement
453 : null,
454 mucTrueCounterPartByPresence);
455 } else if (mucUserElement != null) {
456 final Conversation conversation =
457 mXmppConnectionService.find(account, from.asBareJid());
458 if (conversation != null) {
459 final var mucOptions = conversation.getMucOptions();
460 occupant =
461 mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null;
462 } else {
463 occupant = null;
464 }
465 mucTrueCounterPart = null;
466 } else {
467 mucTrueCounterPart = null;
468 occupant = null;
469 }
470 boolean isMucStatusMessage =
471 Jid.Invalid.hasValidFrom(packet)
472 && from.isBareJid()
473 && mucUserElement != null
474 && mucUserElement.hasChild("status");
475 boolean selfAddressed;
476 if (packet.fromAccount(account)) {
477 status = Message.STATUS_SEND;
478 selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
479 if (selfAddressed) {
480 counterpart = from;
481 } else {
482 counterpart = to;
483 }
484 } else {
485 status = Message.STATUS_RECEIVED;
486 counterpart = from;
487 selfAddressed = false;
488 }
489
490 final Invite invite = extractInvite(packet);
491 if (invite != null) {
492 if (invite.jid.asBareJid().equals(account.getJid().asBareJid())) {
493 Log.d(
494 Config.LOGTAG,
495 account.getJid().asBareJid()
496 + ": ignore invite to "
497 + invite.jid
498 + " because it matches account");
499 } else if (isTypeGroupChat) {
500 Log.d(
501 Config.LOGTAG,
502 account.getJid().asBareJid()
503 + ": ignoring invite to "
504 + invite.jid
505 + " because it was received as group chat");
506 } else if (invite.direct
507 && (mucUserElement != null
508 || invite.inviter == null
509 || mXmppConnectionService.isMuc(account, invite.inviter))) {
510 Log.d(
511 Config.LOGTAG,
512 account.getJid().asBareJid()
513 + ": ignoring direct invite to "
514 + invite.jid
515 + " because it was received in MUC");
516 } else {
517 invite.execute(account);
518 return;
519 }
520 }
521
522 if ((body != null
523 || pgpEncrypted != null
524 || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload"))
525 || oobUrl != null)
526 && !isMucStatusMessage) {
527 final boolean conversationIsProbablyMuc =
528 isTypeGroupChat
529 || mucUserElement != null
530 || connection
531 .getMucServersWithholdAccount()
532 .contains(counterpart.getDomain());
533 final Conversation conversation =
534 mXmppConnectionService.findOrCreateConversation(
535 account,
536 counterpart.asBareJid(),
537 conversationIsProbablyMuc,
538 false,
539 query,
540 false);
541 final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
542
543 if (serverMsgId == null) {
544 serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
545 }
546
547 if (selfAddressed) {
548 // don’t store serverMsgId on reflections for edits
549 final var reflectedServerMsgId =
550 Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
551 if (mXmppConnectionService.markMessage(
552 conversation,
553 remoteMsgId,
554 Message.STATUS_SEND_RECEIVED,
555 reflectedServerMsgId)) {
556 return;
557 }
558 status = Message.STATUS_RECEIVED;
559 if (remoteMsgId != null
560 && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
561 return;
562 }
563 }
564
565 if (isTypeGroupChat) {
566 if (conversation.getMucOptions().isSelf(counterpart)) {
567 status = Message.STATUS_SEND_RECEIVED;
568 isCarbon = true; // not really carbon but received from another resource
569 // don’t store serverMsgId on reflections for edits
570 final var reflectedServerMsgId =
571 Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
572 if (mXmppConnectionService.markMessage(
573 conversation, remoteMsgId, status, reflectedServerMsgId, body)) {
574 return;
575 } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
576 if (body != null) {
577 Message message = conversation.findSentMessageWithBody(body.content);
578 if (message != null) {
579 mXmppConnectionService.markMessage(message, status);
580 return;
581 }
582 }
583 }
584 } else {
585 status = Message.STATUS_RECEIVED;
586 }
587 }
588 final Message message;
589 if (pgpEncrypted != null && Config.supportOpenPgp()) {
590 message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
591 } else if (axolotlEncrypted != null && Config.supportOmemo()) {
592 Jid origin;
593 Set<Jid> fallbacksBySourceId = Collections.emptySet();
594 if (conversationMultiMode) {
595 final Jid fallback =
596 conversation.getMucOptions().getTrueCounterpart(counterpart);
597 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
598 if (origin == null) {
599 try {
600 fallbacksBySourceId =
601 account.getAxolotlService()
602 .findCounterpartsBySourceId(
603 XmppAxolotlMessage.parseSourceId(
604 axolotlEncrypted));
605 } catch (IllegalArgumentException e) {
606 // ignoring
607 }
608 }
609 if (origin == null && fallbacksBySourceId.isEmpty()) {
610 Log.d(
611 Config.LOGTAG,
612 "axolotl message in anonymous conference received and no possible"
613 + " fallbacks");
614 return;
615 }
616 } else {
617 fallbacksBySourceId = Collections.emptySet();
618 origin = from;
619 }
620
621 final boolean liveMessage =
622 query == null && !isTypeGroupChat && mucUserElement == null;
623 final boolean checkedForDuplicates =
624 liveMessage
625 || (serverMsgId != null
626 && remoteMsgId != null
627 && !conversation.possibleDuplicate(
628 serverMsgId, remoteMsgId));
629
630 if (origin != null) {
631 message =
632 parseAxolotlChat(
633 axolotlEncrypted,
634 origin,
635 conversation,
636 status,
637 checkedForDuplicates,
638 query != null);
639 } else {
640 Message trial = null;
641 for (Jid fallback : fallbacksBySourceId) {
642 trial =
643 parseAxolotlChat(
644 axolotlEncrypted,
645 fallback,
646 conversation,
647 status,
648 checkedForDuplicates && fallbacksBySourceId.size() == 1,
649 query != null);
650 if (trial != null) {
651 Log.d(
652 Config.LOGTAG,
653 account.getJid().asBareJid()
654 + ": decoded muc message using fallback");
655 origin = fallback;
656 break;
657 }
658 }
659 message = trial;
660 }
661 if (message == null) {
662 if (query == null
663 && extractChatState(
664 mXmppConnectionService.find(account, counterpart.asBareJid()),
665 isTypeGroupChat,
666 packet)) {
667 mXmppConnectionService.updateConversationUi();
668 }
669 if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
670 Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
671 if (previouslySent != null
672 && previouslySent.getServerMsgId() == null
673 && serverMsgId != null) {
674 previouslySent.setServerMsgId(serverMsgId);
675 mXmppConnectionService.databaseBackend.updateMessage(
676 previouslySent, false);
677 Log.d(
678 Config.LOGTAG,
679 account.getJid().asBareJid()
680 + ": encountered previously sent OMEMO message without"
681 + " serverId. updating...");
682 }
683 }
684 return;
685 }
686 if (conversationMultiMode) {
687 message.setTrueCounterpart(origin);
688 }
689 } else if (body == null && oobUrl != null) {
690 message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
691 message.setOob(true);
692 if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
693 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
694 }
695 } else {
696 message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
697 if (body.count > 1) {
698 message.setBodyLanguage(body.language);
699 }
700 }
701
702 message.setCounterpart(counterpart);
703 message.setRemoteMsgId(remoteMsgId);
704 message.setServerMsgId(serverMsgId);
705 message.setCarbon(isCarbon);
706 message.setTime(timestamp);
707 if (body != null && body.content != null && body.content.equals(oobUrl)) {
708 message.setOob(true);
709 if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
710 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
711 }
712 }
713 message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
714 if (conversationMultiMode) {
715 final var mucOptions = conversation.getMucOptions();
716 if (occupant != null) {
717 message.setOccupantId(occupant.getId());
718 }
719 message.setMucUser(mucOptions.findUserByFullJid(counterpart));
720 final Jid fallback = mucOptions.getTrueCounterpart(counterpart);
721 Jid trueCounterpart;
722 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
723 trueCounterpart = message.getTrueCounterpart();
724 } else if (query != null && query.safeToExtractTrueCounterpart()) {
725 trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
726 } else {
727 trueCounterpart = fallback;
728 }
729 if (trueCounterpart != null && isTypeGroupChat) {
730 if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
731 status =
732 isTypeGroupChat
733 ? Message.STATUS_SEND_RECEIVED
734 : Message.STATUS_SEND;
735 } else {
736 status = Message.STATUS_RECEIVED;
737 message.setCarbon(false);
738 }
739 }
740 message.setStatus(status);
741 message.setTrueCounterpart(trueCounterpart);
742 if (!isTypeGroupChat) {
743 message.setType(Message.TYPE_PRIVATE);
744 }
745 } else {
746 updateLastseen(account, from);
747 }
748
749 if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
750 final Message replacedMessage =
751 conversation.findMessageWithRemoteIdAndCounterpart(
752 replacementId,
753 counterpart,
754 message.getStatus() == Message.STATUS_RECEIVED,
755 message.isCarbon());
756 if (replacedMessage != null) {
757 final boolean fingerprintsMatch =
758 replacedMessage.getFingerprint() == null
759 || replacedMessage
760 .getFingerprint()
761 .equals(message.getFingerprint());
762 final boolean trueCountersMatch =
763 replacedMessage.getTrueCounterpart() != null
764 && message.getTrueCounterpart() != null
765 && replacedMessage
766 .getTrueCounterpart()
767 .asBareJid()
768 .equals(message.getTrueCounterpart().asBareJid());
769 final boolean occupantIdMatch =
770 replacedMessage.getOccupantId() != null
771 && replacedMessage
772 .getOccupantId()
773 .equals(message.getOccupantId());
774 final boolean mucUserMatches =
775 query == null
776 && replacedMessage.sameMucUser(
777 message); // can not be checked when using mam
778 final boolean duplicate = conversation.hasDuplicateMessage(message);
779 if (fingerprintsMatch
780 && (trueCountersMatch
781 || occupantIdMatch
782 || !conversationMultiMode
783 || mucUserMatches)
784 && !duplicate) {
785 synchronized (replacedMessage) {
786 final String uuid = replacedMessage.getUuid();
787 replacedMessage.setUuid(UUID.randomUUID().toString());
788 replacedMessage.setBody(message.getBody());
789 // we store the IDs of the replacing message. This is essentially unused
790 // today (only the fact that there are _some_ edits causes the edit icon
791 // to appear)
792 replacedMessage.putEdited(
793 message.getRemoteMsgId(), message.getServerMsgId());
794
795 // we used to call
796 // `replacedMessage.setServerMsgId(message.getServerMsgId());` so during
797 // catchup we could start from the edit; not the original message
798 // however this caused problems for things like reactions that refer to
799 // the serverMsgId
800
801 replacedMessage.setEncryption(message.getEncryption());
802 if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
803 replacedMessage.markUnread();
804 }
805 extractChatState(
806 mXmppConnectionService.find(account, counterpart.asBareJid()),
807 isTypeGroupChat,
808 packet);
809 mXmppConnectionService.updateMessage(replacedMessage, uuid);
810 if (mXmppConnectionService.confirmMessages()
811 && replacedMessage.getStatus() == Message.STATUS_RECEIVED
812 && (replacedMessage.trusted()
813 || replacedMessage
814 .isPrivateMessage()) // TODO do we really want
815 // to send receipts for all
816 // PMs?
817 && remoteMsgId != null
818 && !selfAddressed
819 && !isTypeGroupChat) {
820 processMessageReceipts(account, packet, remoteMsgId, query);
821 }
822 if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
823 conversation
824 .getAccount()
825 .getPgpDecryptionService()
826 .discard(replacedMessage);
827 conversation
828 .getAccount()
829 .getPgpDecryptionService()
830 .decrypt(replacedMessage, false);
831 }
832 }
833 mXmppConnectionService.getNotificationService().updateNotification();
834 return;
835 } else {
836 Log.d(
837 Config.LOGTAG,
838 account.getJid().asBareJid()
839 + ": received message correction but verification didn't"
840 + " check out");
841 }
842 }
843 }
844
845 long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
846 if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
847 Log.d(
848 Config.LOGTAG,
849 account.getJid().asBareJid()
850 + ": skipping message from "
851 + message.getCounterpart().toString()
852 + " because it was sent prior to our deletion date");
853 return;
854 }
855
856 boolean checkForDuplicates =
857 (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
858 || message.isPrivateMessage()
859 || message.getServerMsgId() != null
860 || (query == null
861 && mXmppConnectionService
862 .getMessageArchiveService()
863 .isCatchupInProgress(conversation));
864 if (checkForDuplicates) {
865 final Message duplicate = conversation.findDuplicateMessage(message);
866 if (duplicate != null) {
867 final boolean serverMsgIdUpdated;
868 if (duplicate.getStatus() != Message.STATUS_RECEIVED
869 && duplicate.getUuid().equals(message.getRemoteMsgId())
870 && duplicate.getServerMsgId() == null
871 && message.getServerMsgId() != null) {
872 duplicate.setServerMsgId(message.getServerMsgId());
873 if (mXmppConnectionService.databaseBackend.updateMessage(
874 duplicate, false)) {
875 serverMsgIdUpdated = true;
876 } else {
877 serverMsgIdUpdated = false;
878 Log.e(Config.LOGTAG, "failed to update message");
879 }
880 } else {
881 serverMsgIdUpdated = false;
882 }
883 Log.d(
884 Config.LOGTAG,
885 "skipping duplicate message with "
886 + message.getCounterpart()
887 + ". serverMsgIdUpdated="
888 + serverMsgIdUpdated);
889 return;
890 }
891 }
892
893 if (query != null
894 && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
895 conversation.prepend(query.getActualInThisQuery(), message);
896 } else {
897 conversation.add(message);
898 }
899 if (query != null) {
900 query.incrementActualMessageCount();
901 }
902
903 if (query == null || query.isCatchup()) { // either no mam or catchup
904 if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
905 mXmppConnectionService.markRead(conversation);
906 if (query == null) {
907 activateGracePeriod(account);
908 }
909 } else {
910 message.markUnread();
911 notify = true;
912 }
913 }
914
915 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
916 notify =
917 conversation
918 .getAccount()
919 .getPgpDecryptionService()
920 .decrypt(message, notify);
921 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
922 || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
923 notify = false;
924 }
925
926 if (query == null) {
927 extractChatState(
928 mXmppConnectionService.find(account, counterpart.asBareJid()),
929 isTypeGroupChat,
930 packet);
931 mXmppConnectionService.updateConversationUi();
932 }
933
934 if (mXmppConnectionService.confirmMessages()
935 && message.getStatus() == Message.STATUS_RECEIVED
936 && (message.trusted() || message.isPrivateMessage())
937 && remoteMsgId != null
938 && !selfAddressed
939 && !isTypeGroupChat) {
940 processMessageReceipts(account, packet, remoteMsgId, query);
941 }
942
943 mXmppConnectionService.databaseBackend.createMessage(message);
944 final HttpConnectionManager manager =
945 this.mXmppConnectionService.getHttpConnectionManager();
946 if (message.trusted()
947 && message.treatAsDownloadable()
948 && manager.getAutoAcceptFileSize() > 0) {
949 manager.createNewDownloadConnection(message);
950 } else if (notify) {
951 if (query != null && query.isCatchup()) {
952 mXmppConnectionService.getNotificationService().pushFromBacklog(message);
953 } else {
954 mXmppConnectionService.getNotificationService().push(message);
955 }
956 }
957 } else if (!packet.hasChild("body")) { // no body
958
959 final Conversation conversation =
960 mXmppConnectionService.find(account, from.asBareJid());
961 if (axolotlEncrypted != null) {
962 Jid origin;
963 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
964 final Jid fallback =
965 conversation.getMucOptions().getTrueCounterpart(counterpart);
966 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
967 if (origin == null) {
968 Log.d(
969 Config.LOGTAG,
970 "omemo key transport message in anonymous conference received");
971 return;
972 }
973 } else if (isTypeGroupChat) {
974 return;
975 } else {
976 origin = from;
977 }
978 try {
979 final XmppAxolotlMessage xmppAxolotlMessage =
980 XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
981 account.getAxolotlService()
982 .processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
983 Log.d(
984 Config.LOGTAG,
985 account.getJid().asBareJid()
986 + ": omemo key transport message received from "
987 + origin);
988 } catch (Exception e) {
989 Log.d(
990 Config.LOGTAG,
991 account.getJid().asBareJid()
992 + ": invalid omemo key transport message received "
993 + e.getMessage());
994 return;
995 }
996 }
997
998 if (query == null
999 && extractChatState(
1000 mXmppConnectionService.find(account, counterpart.asBareJid()),
1001 isTypeGroupChat,
1002 packet)) {
1003 mXmppConnectionService.updateConversationUi();
1004 }
1005
1006 if (isTypeGroupChat) {
1007 if (packet.hasChild("subject")
1008 && !packet.hasChild("thread")) { // We already know it has no body per above
1009 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1010 conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
1011 final LocalizedContent subject = packet.getSubject();
1012 if (subject != null
1013 && conversation.getMucOptions().setSubject(subject.content)) {
1014 mXmppConnectionService.updateConversation(conversation);
1015 }
1016 mXmppConnectionService.updateConversationUi();
1017 return;
1018 }
1019 }
1020 }
1021
1022 if (original.hasExtension(MucUser.class)) {
1023 getManager(MultiUserChatManager.class).handleStatusMessage(original);
1024 }
1025
1026 if (!isTypeGroupChat) {
1027 for (Element child : packet.getChildren()) {
1028 if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
1029 && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
1030 final String action = child.getName();
1031 final String sessionId = child.getAttribute("id");
1032 if (sessionId == null) {
1033 break;
1034 }
1035 if (query == null && offlineMessagesRetrieved) {
1036 if (serverMsgId == null) {
1037 serverMsgId = extractStanzaId(account, packet);
1038 }
1039 mXmppConnectionService
1040 .getJingleConnectionManager()
1041 .deliverMessage(
1042 account,
1043 packet.getTo(),
1044 packet.getFrom(),
1045 child,
1046 remoteMsgId,
1047 serverMsgId,
1048 timestamp);
1049 final Contact contact = account.getRoster().getContact(from);
1050 // this is the same condition that is found in JingleRtpConnection for
1051 // the 'ringing' response. Responding with delivery receipts predates
1052 // the 'ringing' spec'd
1053 final boolean sendReceipts =
1054 contact.showInContactList()
1055 || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
1056 if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
1057 processMessageReceipts(account, packet, remoteMsgId, null);
1058 }
1059 } else if ((query != null && query.isCatchup())
1060 || !offlineMessagesRetrieved) {
1061 if ("propose".equals(action)) {
1062 final Element description = child.findChild("description");
1063 final String namespace =
1064 description == null ? null : description.getNamespace();
1065 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1066 final Conversation c =
1067 mXmppConnectionService.findOrCreateConversation(
1068 account, counterpart.asBareJid(), false, false);
1069 final Message preExistingMessage =
1070 c.findRtpSession(sessionId, status);
1071 if (preExistingMessage != null) {
1072 preExistingMessage.setServerMsgId(serverMsgId);
1073 mXmppConnectionService.updateMessage(preExistingMessage);
1074 break;
1075 }
1076 final Message message =
1077 new Message(
1078 c, status, Message.TYPE_RTP_SESSION, sessionId);
1079 message.setServerMsgId(serverMsgId);
1080 message.setTime(timestamp);
1081 message.setBody(new RtpSessionStatus(false, 0).toString());
1082 c.add(message);
1083 mXmppConnectionService.databaseBackend.createMessage(message);
1084 }
1085 } else if ("proceed".equals(action)) {
1086 // status needs to be flipped to find the original propose
1087 final Conversation c =
1088 mXmppConnectionService.findOrCreateConversation(
1089 account, counterpart.asBareJid(), false, false);
1090 final int s =
1091 packet.fromAccount(account)
1092 ? Message.STATUS_RECEIVED
1093 : Message.STATUS_SEND;
1094 final Message message = c.findRtpSession(sessionId, s);
1095 if (message != null) {
1096 message.setBody(new RtpSessionStatus(true, 0).toString());
1097 if (serverMsgId != null) {
1098 message.setServerMsgId(serverMsgId);
1099 }
1100 message.setTime(timestamp);
1101 mXmppConnectionService.updateMessage(message, true);
1102 } else {
1103 Log.d(
1104 Config.LOGTAG,
1105 "unable to find original rtp session message for"
1106 + " received propose");
1107 }
1108
1109 } else if ("finish".equals(action)) {
1110 Log.d(
1111 Config.LOGTAG,
1112 "received JMI 'finish' during MAM catch-up. Can be used to"
1113 + " update success/failure and duration");
1114 }
1115 } else {
1116 // MAM reloads (non catchups
1117 if ("propose".equals(action)) {
1118 final Element description = child.findChild("description");
1119 final String namespace =
1120 description == null ? null : description.getNamespace();
1121 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1122 final Conversation c =
1123 mXmppConnectionService.findOrCreateConversation(
1124 account, counterpart.asBareJid(), false, false);
1125 final Message preExistingMessage =
1126 c.findRtpSession(sessionId, status);
1127 if (preExistingMessage != null) {
1128 preExistingMessage.setServerMsgId(serverMsgId);
1129 mXmppConnectionService.updateMessage(preExistingMessage);
1130 break;
1131 }
1132 final Message message =
1133 new Message(
1134 c, status, Message.TYPE_RTP_SESSION, sessionId);
1135 message.setServerMsgId(serverMsgId);
1136 message.setTime(timestamp);
1137 message.setBody(new RtpSessionStatus(true, 0).toString());
1138 if (query.getPagingOrder()
1139 == MessageArchiveService.PagingOrder.REVERSE) {
1140 c.prepend(query.getActualInThisQuery(), message);
1141 } else {
1142 c.add(message);
1143 }
1144 query.incrementActualMessageCount();
1145 mXmppConnectionService.databaseBackend.createMessage(message);
1146 }
1147 }
1148 }
1149 break;
1150 }
1151 }
1152 }
1153
1154 final var received =
1155 packet.getExtension(
1156 im.conversations.android.xmpp.model.receipts.Received.class);
1157 if (received != null) {
1158 processReceived(received, packet, query, from);
1159 }
1160 final var displayed = packet.getExtension(Displayed.class);
1161 if (displayed != null) {
1162 processDisplayed(
1163 displayed,
1164 packet,
1165 selfAddressed,
1166 counterpart,
1167 query,
1168 isTypeGroupChat,
1169 conversation,
1170 mucUserElement,
1171 from);
1172 }
1173 final Reactions reactions = packet.getExtension(Reactions.class);
1174 if (reactions != null) {
1175 processReactions(
1176 reactions,
1177 conversation,
1178 isTypeGroupChat,
1179 occupant,
1180 counterpart,
1181 mucTrueCounterPart,
1182 packet);
1183 }
1184
1185 // end no body
1186 }
1187
1188 if (original.hasExtension(Event.class)) {
1189 getManager(PubSubManager.class).handleEvent(original);
1190 }
1191
1192 final String nick = packet.findChildContent("nick", Namespace.NICK);
1193 if (nick != null && Jid.Invalid.isValid(from)) {
1194 if (mXmppConnectionService.isMuc(account, from)) {
1195 return;
1196 }
1197 final Contact contact = account.getRoster().getContact(from);
1198 if (contact.setPresenceName(nick)) {
1199 connection.getManager(RosterManager.class).writeToDatabaseAsync();
1200 mXmppConnectionService.getAvatarService().clear(contact);
1201 }
1202 }
1203 }
1204
1205 private void processReceived(
1206 final im.conversations.android.xmpp.model.receipts.Received received,
1207 final im.conversations.android.xmpp.model.stanza.Message packet,
1208 final MessageArchiveService.Query query,
1209 final Jid from) {
1210 final var account = this.connection.getAccount();
1211 final var id = received.getId();
1212 if (packet.fromAccount(account)) {
1213 if (query != null && id != null && packet.getTo() != null) {
1214 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1215 }
1216 } else if (id != null) {
1217 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1218 final String sessionId =
1219 id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1220 mXmppConnectionService
1221 .getJingleConnectionManager()
1222 .updateProposedSessionDiscovered(
1223 account,
1224 from,
1225 sessionId,
1226 JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1227 } else {
1228 mXmppConnectionService.markMessage(
1229 account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1230 }
1231 }
1232 }
1233
1234 private void processDisplayed(
1235 final Displayed displayed,
1236 final im.conversations.android.xmpp.model.stanza.Message packet,
1237 final boolean selfAddressed,
1238 final Jid counterpart,
1239 final MessageArchiveService.Query query,
1240 final boolean isTypeGroupChat,
1241 final Conversation conversation,
1242 final Element mucUserElement,
1243 final Jid from) {
1244 final var account = getAccount();
1245 final var id = displayed.getId();
1246 // TODO we don’t even use 'sender' any more. Remove this!
1247 final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1248 if (packet.fromAccount(account) && !selfAddressed) {
1249 final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1250 final Message message =
1251 (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1252 if (message != null && (query == null || query.isCatchup())) {
1253 mXmppConnectionService.markReadUpTo(c, message);
1254 }
1255 if (query == null) {
1256 activateGracePeriod(account);
1257 }
1258 } else if (isTypeGroupChat) {
1259 final Message message;
1260 if (conversation != null && id != null) {
1261 if (sender != null) {
1262 message = conversation.findMessageWithRemoteId(id, sender);
1263 } else {
1264 message = conversation.findMessageWithServerMsgId(id);
1265 }
1266 } else {
1267 message = null;
1268 }
1269 if (message != null) {
1270 // TODO use occupantId to extract true counterpart from presence
1271 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1272 // TODO try to externalize mucTrueCounterpart
1273 final Jid trueJid =
1274 getTrueCounterpart(
1275 (query != null && query.safeToExtractTrueCounterpart())
1276 ? mucUserElement
1277 : null,
1278 fallback);
1279 final boolean trueJidMatchesAccount =
1280 account.getJid()
1281 .asBareJid()
1282 .equals(trueJid == null ? null : trueJid.asBareJid());
1283 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1284 if (!message.isRead()
1285 && (query == null || query.isCatchup())) { // checking if message is
1286 // unread fixes race conditions
1287 // with reflections
1288 mXmppConnectionService.markReadUpTo(conversation, message);
1289 }
1290 } else if (!counterpart.isBareJid() && trueJid != null) {
1291 final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1292 if (message.addReadByMarker(readByMarker)) {
1293 final var mucOptions = conversation.getMucOptions();
1294 final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1295 final var readyBy = message.getReadyByTrue();
1296 final var mStatus = message.getStatus();
1297 if (mucOptions.isPrivateAndNonAnonymous()
1298 && (mStatus == Message.STATUS_SEND_RECEIVED
1299 || mStatus == Message.STATUS_SEND)
1300 && readyBy.containsAll(everyone)) {
1301 message.setStatus(Message.STATUS_SEND_DISPLAYED);
1302 }
1303 mXmppConnectionService.updateMessage(message, false);
1304 }
1305 }
1306 }
1307 } else {
1308 final Message displayedMessage =
1309 mXmppConnectionService.markMessage(
1310 account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1311 Message message = displayedMessage == null ? null : displayedMessage.prev();
1312 while (message != null
1313 && message.getStatus() == Message.STATUS_SEND_RECEIVED
1314 && message.getTimeSent() < displayedMessage.getTimeSent()) {
1315 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1316 message = message.prev();
1317 }
1318 if (displayedMessage != null && selfAddressed) {
1319 dismissNotification(account, counterpart, query, id);
1320 }
1321 }
1322 }
1323
1324 private void processReactions(
1325 final Reactions reactions,
1326 final Conversation conversation,
1327 final boolean isTypeGroupChat,
1328 final OccupantId occupant,
1329 final Jid counterpart,
1330 final Jid mucTrueCounterPart,
1331 final im.conversations.android.xmpp.model.stanza.Message packet) {
1332 final var account = getAccount();
1333 final String reactingTo = reactions.getId();
1334 if (conversation != null && reactingTo != null) {
1335 if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1336 final var mucOptions = conversation.getMucOptions();
1337 final var occupantId = occupant == null ? null : occupant.getId();
1338 if (occupantId != null) {
1339 final boolean isReceived = !mucOptions.isSelf(occupantId);
1340 final Message message;
1341 final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1342 if (inMemoryMessage != null) {
1343 message = inMemoryMessage;
1344 } else {
1345 message =
1346 mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1347 conversation, reactingTo);
1348 }
1349 if (message != null) {
1350 final var combinedReactions =
1351 Reaction.withOccupantId(
1352 message.getReactions(),
1353 reactions.getReactions(),
1354 isReceived,
1355 counterpart,
1356 mucTrueCounterPart,
1357 occupantId);
1358 message.setReactions(combinedReactions);
1359 mXmppConnectionService.updateMessage(message, false);
1360 } else {
1361 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1362 }
1363 } else {
1364 Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1365 }
1366 } else {
1367 final Message message;
1368 final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1369 if (inMemoryMessage != null) {
1370 message = inMemoryMessage;
1371 } else {
1372 message =
1373 mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1374 conversation, reactingTo);
1375 }
1376 if (message == null) {
1377 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1378 return;
1379 }
1380 final boolean isReceived;
1381 final Jid reactionFrom;
1382 if (conversation.getMode() == Conversational.MODE_MULTI) {
1383 Log.d(Config.LOGTAG, "received reaction as MUC PM. triggering validation");
1384 final var mucOptions = conversation.getMucOptions();
1385 final var occupantId = occupant == null ? null : occupant.getId();
1386 if (occupantId == null) {
1387 Log.d(
1388 Config.LOGTAG,
1389 "received reaction via PM channel w/o occupant ids. ignoring");
1390 return;
1391 }
1392 isReceived = !mucOptions.isSelf(occupantId);
1393 if (isReceived) {
1394 reactionFrom = counterpart;
1395 } else {
1396 if (!occupantId.equals(message.getOccupantId())) {
1397 Log.d(
1398 Config.LOGTAG,
1399 "reaction received via MUC PM did not pass validation");
1400 return;
1401 }
1402 reactionFrom = account.getJid().asBareJid();
1403 }
1404 } else {
1405 if (packet.fromAccount(account)) {
1406 isReceived = false;
1407 reactionFrom = account.getJid().asBareJid();
1408 } else {
1409 isReceived = true;
1410 reactionFrom = counterpart;
1411 }
1412 }
1413 final var combinedReactions =
1414 Reaction.withFrom(
1415 message.getReactions(),
1416 reactions.getReactions(),
1417 isReceived,
1418 reactionFrom);
1419 message.setReactions(combinedReactions);
1420 mXmppConnectionService.updateMessage(message, false);
1421 }
1422 }
1423 }
1424
1425 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1426 getForwardedMessagePacket(
1427 final im.conversations.android.xmpp.model.stanza.Message original,
1428 Class<? extends Extension> clazz) {
1429 final var extension = original.getExtension(clazz);
1430 final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1431 if (forwarded == null) {
1432 return null;
1433 }
1434 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1435 final var forwardedMessage = forwarded.getMessage();
1436 if (forwardedMessage == null) {
1437 return null;
1438 }
1439 return new Pair<>(forwardedMessage, timestamp);
1440 }
1441
1442 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1443 getForwardedMessagePacket(
1444 final im.conversations.android.xmpp.model.stanza.Message original,
1445 final String name,
1446 final String namespace) {
1447 final Element wrapper = original.findChild(name, namespace);
1448 final var forwardedElement =
1449 wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1450 if (forwardedElement instanceof Forwarded forwarded) {
1451 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1452 final var forwardedMessage = forwarded.getMessage();
1453 if (forwardedMessage == null) {
1454 return null;
1455 }
1456 return new Pair<>(forwardedMessage, timestamp);
1457 }
1458 return null;
1459 }
1460
1461 private void dismissNotification(
1462 Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1463 final Conversation conversation =
1464 mXmppConnectionService.find(account, counterpart.asBareJid());
1465 if (conversation != null && (query == null || query.isCatchup())) {
1466 final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1467 if (displayableId != null && displayableId.equals(id)) {
1468 mXmppConnectionService.markRead(conversation);
1469 } else {
1470 Log.w(
1471 Config.LOGTAG,
1472 account.getJid().asBareJid()
1473 + ": received dismissing display marker that did not match our last"
1474 + " id in that conversation");
1475 }
1476 }
1477 }
1478
1479 private void processMessageReceipts(
1480 final Account account,
1481 final im.conversations.android.xmpp.model.stanza.Message packet,
1482 final String remoteMsgId,
1483 final MessageArchiveService.Query query) {
1484 final var request = packet.hasExtension(Request.class);
1485 if (query == null) {
1486 if (request) {
1487 final var receipt =
1488 mXmppConnectionService
1489 .getMessageGenerator()
1490 .received(packet.getFrom(), remoteMsgId, packet.getType());
1491 mXmppConnectionService.sendMessagePacket(account, receipt);
1492 }
1493 } else if (query.isCatchup()) {
1494 if (request) {
1495 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1496 }
1497 }
1498 }
1499
1500 private void activateGracePeriod(Account account) {
1501 long duration =
1502 mXmppConnectionService.getLongPreference(
1503 "grace_period_length", R.integer.grace_period)
1504 * 1000;
1505 Log.d(
1506 Config.LOGTAG,
1507 account.getJid().asBareJid()
1508 + ": activating grace period till "
1509 + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1510 account.activateGracePeriod(duration);
1511 }
1512
1513 private class Invite {
1514 final Jid jid;
1515 final String password;
1516 final boolean direct;
1517 final Jid inviter;
1518
1519 Invite(Jid jid, String password, boolean direct, Jid inviter) {
1520 this.jid = jid;
1521 this.password = password;
1522 this.direct = direct;
1523 this.inviter = inviter;
1524 }
1525
1526 public boolean execute(final Account account) {
1527 if (this.jid == null) {
1528 return false;
1529 }
1530 final Contact contact =
1531 this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1532 if (contact != null && contact.isBlocked()) {
1533 Log.d(
1534 Config.LOGTAG,
1535 account.getJid().asBareJid()
1536 + ": ignore invite from "
1537 + contact.getJid()
1538 + " because contact is blocked");
1539 return false;
1540 }
1541 final AppSettings appSettings = new AppSettings(mXmppConnectionService);
1542 if ((contact != null && contact.showInContactList())
1543 || appSettings.isAcceptInvitesFromStrangers()) {
1544 final Conversation conversation =
1545 mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1546 if (conversation.getMucOptions().online()) {
1547 Log.d(
1548 Config.LOGTAG,
1549 account.getJid().asBareJid()
1550 + ": received invite to "
1551 + jid
1552 + " but muc is considered to be online");
1553 getManager(MultiUserChatManager.class).pingAndRejoin(conversation);
1554 } else {
1555 conversation.getMucOptions().setPassword(password);
1556 mXmppConnectionService.databaseBackend.updateConversation(conversation);
1557 if (contact != null && contact.showInContactList()) {
1558 getManager(MultiUserChatManager.class).joinFollowingInvite(conversation);
1559 } else {
1560 getManager(MultiUserChatManager.class).join(conversation);
1561 }
1562 mXmppConnectionService.updateConversationUi();
1563 }
1564 return true;
1565 } else {
1566 Log.d(
1567 Config.LOGTAG,
1568 account.getJid().asBareJid()
1569 + ": ignoring invite from "
1570 + this.inviter
1571 + " because we are not accepting invites from strangers. direct="
1572 + direct);
1573 return false;
1574 }
1575 }
1576 }
1577}