1package eu.siacs.conversations.parser;
2
3import android.net.Uri;
4import android.util.Log;
5import android.util.Pair;
6
7import com.cheogram.android.BobTransfer;
8import com.cheogram.android.WebxdcUpdate;
9
10import androidx.annotation.NonNull;
11import com.google.common.base.Strings;
12import com.google.common.collect.ImmutableSet;
13
14import java.io.File;
15import java.net.URISyntaxException;
16import java.text.SimpleDateFormat;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collections;
20import java.util.Date;
21import java.util.HashSet;
22import java.util.HashMap;
23import java.util.LinkedHashSet;
24import java.util.List;
25import java.util.Locale;
26import java.util.Map;
27import java.util.Set;
28import java.util.UUID;
29import java.util.function.Consumer;
30import java.util.stream.Collectors;
31
32import io.ipfs.cid.Cid;
33
34import eu.siacs.conversations.AppSettings;
35import eu.siacs.conversations.Config;
36import eu.siacs.conversations.R;
37import eu.siacs.conversations.crypto.axolotl.AxolotlService;
38import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
39import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
40import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException;
41import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
42import eu.siacs.conversations.entities.Account;
43import eu.siacs.conversations.entities.Bookmark;
44import eu.siacs.conversations.entities.Contact;
45import eu.siacs.conversations.entities.Conversation;
46import eu.siacs.conversations.entities.Conversational;
47import eu.siacs.conversations.entities.DownloadableFile;
48import eu.siacs.conversations.entities.Message;
49import eu.siacs.conversations.entities.MucOptions;
50import eu.siacs.conversations.entities.Reaction;
51import eu.siacs.conversations.entities.ReadByMarker;
52import eu.siacs.conversations.entities.ReceiptRequest;
53import eu.siacs.conversations.entities.RtpSessionStatus;
54import eu.siacs.conversations.http.HttpConnectionManager;
55import eu.siacs.conversations.services.MessageArchiveService;
56import eu.siacs.conversations.services.QuickConversationsService;
57import eu.siacs.conversations.services.XmppConnectionService;
58import eu.siacs.conversations.utils.CryptoHelper;
59import eu.siacs.conversations.utils.Emoticons;
60import eu.siacs.conversations.xml.Element;
61import eu.siacs.conversations.xml.LocalizedContent;
62import eu.siacs.conversations.xml.Namespace;
63import eu.siacs.conversations.xmpp.Jid;
64import eu.siacs.conversations.xmpp.chatstate.ChatState;
65import eu.siacs.conversations.xmpp.forms.Data;
66import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
67import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
68import eu.siacs.conversations.xmpp.pep.Avatar;
69import im.conversations.android.xmpp.model.Extension;
70import im.conversations.android.xmpp.model.avatar.Metadata;
71import im.conversations.android.xmpp.model.axolotl.DeviceList;
72import im.conversations.android.xmpp.model.axolotl.Encrypted;
73import im.conversations.android.xmpp.model.bookmark.Storage;
74import im.conversations.android.xmpp.model.bookmark2.Conference;
75import im.conversations.android.xmpp.model.carbons.Received;
76import im.conversations.android.xmpp.model.carbons.Sent;
77import im.conversations.android.xmpp.model.correction.Replace;
78import im.conversations.android.xmpp.model.forward.Forwarded;
79import im.conversations.android.xmpp.model.markers.Displayed;
80import im.conversations.android.xmpp.model.nick.Nick;
81import im.conversations.android.xmpp.model.occupant.OccupantId;
82import im.conversations.android.xmpp.model.oob.OutOfBandData;
83import im.conversations.android.xmpp.model.pubsub.Items;
84import im.conversations.android.xmpp.model.pubsub.event.Delete;
85import im.conversations.android.xmpp.model.pubsub.event.Event;
86import im.conversations.android.xmpp.model.pubsub.event.Purge;
87import im.conversations.android.xmpp.model.reactions.Reactions;
88import im.conversations.android.xmpp.model.receipts.Request;
89import im.conversations.android.xmpp.model.unique.StanzaId;
90import java.text.SimpleDateFormat;
91import java.util.Arrays;
92import java.util.Collections;
93import java.util.Date;
94import java.util.HashSet;
95import java.util.List;
96import java.util.Locale;
97import java.util.Map;
98import java.util.Set;
99import java.util.UUID;
100import java.util.function.Consumer;
101
102public class MessageParser extends AbstractParser
103 implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
104
105 private static final SimpleDateFormat TIME_FORMAT =
106 new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
107
108 private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
109 Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
110
111 public MessageParser(final XmppConnectionService service, final Account account) {
112 super(service, account);
113 }
114
115 private static String extractStanzaId(
116 final im.conversations.android.xmpp.model.stanza.Message packet,
117 final boolean isTypeGroupChat,
118 final Conversation conversation) {
119 final Jid by;
120 final boolean safeToExtract;
121 if (isTypeGroupChat) {
122 by = conversation.getJid().asBareJid();
123 safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
124 } else {
125 Account account = conversation.getAccount();
126 by = account.getJid().asBareJid();
127 safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
128 }
129 return safeToExtract ? StanzaId.get(packet, by) : null;
130 }
131
132 private static String extractStanzaId(
133 final Account account,
134 final im.conversations.android.xmpp.model.stanza.Message packet) {
135 final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
136 return safeToExtract ? StanzaId.get(packet, account.getJid().asBareJid()) : null;
137 }
138
139 private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
140 final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
141 Jid result =
142 item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
143 return result != null ? result : fallback;
144 }
145
146 private boolean extractChatState(
147 Conversation c,
148 final boolean isTypeGroupChat,
149 final im.conversations.android.xmpp.model.stanza.Message packet) {
150 ChatState state = ChatState.parse(packet);
151 if (state != null && c != null) {
152 final Account account = c.getAccount();
153 final Jid from = packet.getFrom();
154 if (from.asBareJid().equals(account.getJid().asBareJid())) {
155 c.setOutgoingChatState(state);
156 if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
157 if (c.getContact().isSelf()) {
158 return false;
159 }
160 mXmppConnectionService.markRead(c);
161 activateGracePeriod(account);
162 }
163 return false;
164 } else {
165 if (isTypeGroupChat) {
166 MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
167 if (user != null) {
168 return user.setChatState(state);
169 } else {
170 return false;
171 }
172 } else {
173 return c.setIncomingChatState(state);
174 }
175 }
176 }
177 return false;
178 }
179
180 private Message parseAxolotlChat(
181 final Encrypted axolotlMessage,
182 final Jid from,
183 final Conversation conversation,
184 final int status,
185 final boolean checkedForDuplicates,
186 final boolean postpone) {
187 final AxolotlService service = conversation.getAccount().getAxolotlService();
188 final XmppAxolotlMessage xmppAxolotlMessage;
189 try {
190 xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
191 } catch (final Exception e) {
192 Log.d(
193 Config.LOGTAG,
194 conversation.getAccount().getJid().asBareJid()
195 + ": invalid omemo message received "
196 + e.getMessage());
197 return null;
198 }
199 if (xmppAxolotlMessage.hasPayload()) {
200 final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
201 try {
202 plaintextMessage =
203 service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
204 } catch (BrokenSessionException e) {
205 if (checkedForDuplicates) {
206 if (service.trustedOrPreviouslyResponded(from.asBareJid())) {
207 service.reportBrokenSessionException(e, postpone);
208 return new Message(
209 conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
210 } else {
211 Log.d(
212 Config.LOGTAG,
213 "ignoring broken session exception because contact was not"
214 + " trusted");
215 return new Message(
216 conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
217 }
218 } else {
219 Log.d(
220 Config.LOGTAG,
221 "ignoring broken session exception because checkForDuplicates failed");
222 return null;
223 }
224 } catch (NotEncryptedForThisDeviceException e) {
225 return new Message(
226 conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
227 } catch (OutdatedSenderException e) {
228 return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
229 }
230 if (plaintextMessage != null) {
231 Message finishedMessage =
232 new Message(
233 conversation,
234 plaintextMessage.getPlaintext(),
235 Message.ENCRYPTION_AXOLOTL,
236 status);
237 finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
238 Log.d(
239 Config.LOGTAG,
240 AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())
241 + " Received Message with session fingerprint: "
242 + plaintextMessage.getFingerprint());
243 return finishedMessage;
244 }
245 } else {
246 Log.d(
247 Config.LOGTAG,
248 conversation.getAccount().getJid().asBareJid()
249 + ": received OMEMO key transport message");
250 service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
251 }
252 return null;
253 }
254
255 private Invite extractInvite(final Element message) {
256 final Element mucUser = message.findChild("x", Namespace.MUC_USER);
257 if (mucUser != null) {
258 final Element invite = mucUser.findChild("invite");
259 if (invite != null) {
260 final String password = mucUser.findChildContent("password");
261 final Jid from = Jid.Invalid.getNullForInvalid(invite.getAttributeAsJid("from"));
262 final Jid to = Jid.Invalid.getNullForInvalid(invite.getAttributeAsJid("to"));
263 if (to != null && from == null) {
264 Log.d(Config.LOGTAG, "do not parse outgoing mediated invite " + message);
265 return null;
266 }
267 final Jid room = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
268 if (room == null) {
269 return null;
270 }
271 return new Invite(room, password, false, from);
272 }
273 }
274 final Element conference = message.findChild("x", "jabber:x:conference");
275 if (conference != null) {
276 Jid from = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
277 Jid room = Jid.Invalid.getNullForInvalid(conference.getAttributeAsJid("jid"));
278 if (room == null) {
279 return null;
280 }
281 return new Invite(room, conference.getAttribute("password"), true, from);
282 }
283 return null;
284 }
285
286 private void parseEvent(final Items items, final Jid from, final Account account) {
287 final String node = items.getNode();
288 if ("urn:xmpp:avatar:metadata".equals(node)) {
289 // TODO support retract
290 final var entry = items.getFirstItemWithId(Metadata.class);
291 final var avatar =
292 entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue());
293 if (avatar != null) {
294 avatar.owner = from.asBareJid();
295 if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
296 if (account.getJid().asBareJid().equals(from)) {
297 if (account.setAvatar(avatar.getFilename())) {
298 mXmppConnectionService.databaseBackend.updateAccount(account);
299 mXmppConnectionService.notifyAccountAvatarHasChanged(account);
300 }
301 mXmppConnectionService.getAvatarService().clear(account);
302 mXmppConnectionService.updateConversationUi();
303 mXmppConnectionService.updateAccountUi();
304 } else {
305 final Contact contact = account.getRoster().getContact(from);
306 if (contact.setAvatar(avatar)) {
307 mXmppConnectionService.syncRoster(account);
308 mXmppConnectionService.getAvatarService().clear(contact);
309 mXmppConnectionService.updateConversationUi();
310 mXmppConnectionService.updateRosterUi(XmppConnectionService.UpdateRosterReason.AVATAR);
311 }
312 }
313 } else if (mXmppConnectionService.isDataSaverDisabled()) {
314 mXmppConnectionService.fetchAvatar(account, avatar);
315 }
316 }
317 } else if (Namespace.NICK.equals(node)) {
318 final var nickItem = items.getFirstItem(Nick.class);
319 final String nick = nickItem == null ? null : nickItem.getContent();
320 if (nick != null) {
321 setNick(account, from, nick);
322 }
323 } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
324 final var deviceList = items.getFirstItem(DeviceList.class);
325 if (deviceList != null) {
326 final Set<Integer> deviceIds = deviceList.getDeviceIds();
327 Log.d(
328 Config.LOGTAG,
329 AxolotlService.getLogprefix(account)
330 + "Received PEP device list "
331 + deviceIds
332 + " update from "
333 + from
334 + ", processing... ");
335 final AxolotlService axolotlService = account.getAxolotlService();
336 axolotlService.registerDevices(from, new HashSet<>(deviceIds));
337 }
338
339 } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
340 final var connection = account.getXmppConnection();
341 if (connection.getFeatures().bookmarksConversion()) {
342 if (connection.getFeatures().bookmarks2()) {
343 Log.w(
344 Config.LOGTAG,
345 account.getJid().asBareJid()
346 + ": received storage:bookmark notification even though we"
347 + " opted into bookmarks:1");
348 }
349 final var storage = items.getFirstItem(Storage.class);
350 final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
351 mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
352 Log.d(
353 Config.LOGTAG,
354 account.getJid().asBareJid() + ": processing bookmark PEP event");
355 } else {
356 Log.d(
357 Config.LOGTAG,
358 account.getJid().asBareJid()
359 + ": ignoring bookmark PEP event because bookmark conversion was"
360 + " not detected");
361 }
362 } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
363 final var retractions = items.getRetractions();
364 for (final var item : items.getItemMap(Conference.class).entrySet()) {
365 final Bookmark bookmark =
366 Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
367 if (bookmark == null) {
368 continue;
369 }
370 account.putBookmark(bookmark);
371 mXmppConnectionService.processModifiedBookmark(bookmark);
372 mXmppConnectionService.updateConversationUi();
373 }
374 for (final var retract : retractions) {
375 final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
376 if (id != null) {
377 account.removeBookmark(id);
378 Log.d(
379 Config.LOGTAG,
380 account.getJid().asBareJid() + ": deleted bookmark for " + id);
381 mXmppConnectionService.processDeletedBookmark(account, id);
382 mXmppConnectionService.updateConversationUi();
383 }
384 }
385 } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
386 && Namespace.MDS_DISPLAYED.equals(node)
387 && account.getJid().asBareJid().equals(from)) {
388 for (final var item :
389 items.getItemMap(im.conversations.android.xmpp.model.mds.Displayed.class)
390 .entrySet()) {
391 mXmppConnectionService.processMdsItem(account, item);
392 }
393 }
394 }
395
396 private void parseDeleteEvent(final Delete delete, final Jid from, final Account account) {
397 final String node = delete.getNode();
398 if (Namespace.NICK.equals(node)) {
399 setNick(account, from, null);
400 } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
401 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
402 deleteAllBookmarks(account);
403 } else if (Namespace.AVATAR_METADATA.equals(node)) {
404 final boolean isAccount = account.getJid().asBareJid().equals(from);
405 if (isAccount) {
406 account.setAvatar(null);
407 mXmppConnectionService.databaseBackend.updateAccount(account);
408 mXmppConnectionService.getAvatarService().clear(account);
409 Log.d(
410 Config.LOGTAG,
411 account.getJid().asBareJid() + ": deleted avatar metadata node");
412 }
413 }
414 }
415
416 private void parsePurgeEvent(
417 @NonNull final Purge purge, final Jid from, final Account account) {
418 final String node = purge.getNode();
419 if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
420 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
421 deleteAllBookmarks(account);
422 }
423 }
424
425 private void deleteAllBookmarks(final Account account) {
426 final var previous = account.getBookmarkedJids();
427 account.setBookmarks(Collections.emptyMap());
428 mXmppConnectionService.processDeletedBookmarks(account, previous);
429 }
430
431 private void setNick(final Account account, final Jid user, final String nick) {
432 if (user.asBareJid().equals(account.getJid().asBareJid())) {
433 account.setDisplayName(nick);
434 if (QuickConversationsService.isQuicksy()) {
435 mXmppConnectionService.getAvatarService().clear(account);
436 }
437 mXmppConnectionService.checkMucRequiresRename();
438 } else {
439 Contact contact = account.getRoster().getContact(user);
440 if (contact.setPresenceName(nick)) {
441 mXmppConnectionService.syncRoster(account);
442 mXmppConnectionService.getAvatarService().clear(contact);
443 }
444 }
445 mXmppConnectionService.updateConversationUi();
446 mXmppConnectionService.updateAccountUi();
447 }
448
449 private boolean handleErrorMessage(
450 final Account account,
451 final im.conversations.android.xmpp.model.stanza.Message packet) {
452 if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) {
453 if (packet.fromServer(account)) {
454 final var forwarded =
455 getForwardedMessagePacket(packet, "received", Namespace.CARBONS);
456 if (forwarded != null) {
457 return handleErrorMessage(account, forwarded.first);
458 }
459 }
460 final Jid from = packet.getFrom();
461 final String id = packet.getId();
462 if (from != null && id != null) {
463 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
464 final String sessionId =
465 id.substring(
466 JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
467 mXmppConnectionService
468 .getJingleConnectionManager()
469 .updateProposedSessionDiscovered(
470 account,
471 from,
472 sessionId,
473 JingleConnectionManager.DeviceDiscoveryState.FAILED);
474 return true;
475 }
476 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
477 final String sessionId =
478 id.substring(
479 JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
480 final String message = extractErrorMessage(packet);
481 mXmppConnectionService
482 .getJingleConnectionManager()
483 .failProceed(account, from, sessionId, message);
484 return true;
485 }
486 mXmppConnectionService.markMessage(
487 account,
488 from.asBareJid(),
489 id,
490 Message.STATUS_SEND_FAILED,
491 extractErrorMessage(packet));
492 final Element error = packet.findChild("error");
493 final boolean pingWorthyError =
494 error != null
495 && (error.hasChild("not-acceptable")
496 || error.hasChild("remote-server-timeout")
497 || error.hasChild("remote-server-not-found"));
498 if (pingWorthyError) {
499 Conversation conversation = mXmppConnectionService.find(account, from);
500 if (conversation != null
501 && conversation.getMode() == Conversational.MODE_MULTI) {
502 if (conversation.getMucOptions().online()) {
503 Log.d(
504 Config.LOGTAG,
505 account.getJid().asBareJid()
506 + ": received ping worthy error for seemingly online"
507 + " muc at "
508 + from);
509 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
510 }
511 }
512 }
513 }
514 return true;
515 }
516 return false;
517 }
518
519 @Override
520 public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
521 if (handleErrorMessage(account, original)) {
522 return;
523 }
524 final im.conversations.android.xmpp.model.stanza.Message packet;
525 Long timestamp = null;
526 boolean isCarbon = false;
527 String serverMsgId = null;
528 final Element fin =
529 original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
530 if (fin != null) {
531 mXmppConnectionService
532 .getMessageArchiveService()
533 .processFinLegacy(fin, original.getFrom());
534 return;
535 }
536 final Element result = MessageArchiveService.Version.findResult(original);
537 final String queryId = result == null ? null : result.getAttribute("queryid");
538 final MessageArchiveService.Query query =
539 queryId == null
540 ? null
541 : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
542 final boolean offlineMessagesRetrieved =
543 account.getXmppConnection().isOfflineMessagesRetrieved();
544 if (query != null && query.validFrom(original.getFrom())) {
545 final var f = getForwardedMessagePacket(original, "result", query.version.namespace);
546 if (f == null) {
547 return;
548 }
549 timestamp = f.second;
550 packet = f.first;
551 serverMsgId = result.getAttribute("id");
552 query.incrementMessageCount();
553 if (handleErrorMessage(account, packet)) {
554 return;
555 }
556 final var contact = packet.getFrom() == null || packet.getFrom() instanceof Jid.Invalid ? null : account.getRoster().getContact(packet.getFrom());
557 if (contact != null && contact.isBlocked()) {
558 Log.d(Config.LOGTAG, "Got MAM result from blocked contact, ignoring...");
559 return;
560 }
561 } else if (query != null) {
562 Log.d(
563 Config.LOGTAG,
564 account.getJid().asBareJid()
565 + ": received mam result with invalid from ("
566 + original.getFrom()
567 + ") or queryId ("
568 + queryId
569 + ")");
570 return;
571 } else if (original.fromServer(account)
572 && original.getType()
573 != im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT) {
574 Pair<im.conversations.android.xmpp.model.stanza.Message, Long> f;
575 f = getForwardedMessagePacket(original, Received.class);
576 f = f == null ? getForwardedMessagePacket(original, Sent.class) : f;
577 packet = f != null ? f.first : original;
578 if (handleErrorMessage(account, packet)) {
579 return;
580 }
581 timestamp = f != null ? f.second : null;
582 isCarbon = f != null;
583 } else {
584 packet = original;
585 }
586
587 if (timestamp == null) {
588 timestamp =
589 AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
590 }
591
592 final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
593 final boolean isTypeGroupChat =
594 packet.getType()
595 == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
596 final var encrypted =
597 packet.getOnlyExtension(im.conversations.android.xmpp.model.pgp.Encrypted.class);
598 final String pgpEncrypted = encrypted == null ? null : encrypted.getContent();
599
600 Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
601 Set<Message.FileParams> attachments = new LinkedHashSet<>();
602 for (Element child : packet.getChildren()) {
603 // SIMS first so they get preference in the set
604 if (child.getName().equals("reference") && child.getNamespace().equals("urn:xmpp:reference:0")) {
605 if (child.findChild("media-sharing", "urn:xmpp:sims:1") != null) {
606 attachments.add(new Message.FileParams(child));
607 }
608 }
609 }
610 for (Element child : packet.getChildren()) {
611 if (child.getName().equals("x") && child.getNamespace().equals(Namespace.OOB)) {
612 attachments.add(new Message.FileParams(child));
613 }
614 }
615 String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
616 if (replacementId == null) {
617 final Element fasten = packet.findChild("apply-to", "urn:xmpp:fasten:0");
618 if (fasten != null) {
619 replaceElement = fasten.findChild("retract", "urn:xmpp:message-retract:0");
620 if (replaceElement == null) replaceElement = fasten.findChild("moderated", "urn:xmpp:message-moderate:0");
621 }
622 if (replaceElement == null) replaceElement = packet.findChild("retract", "urn:xmpp:message-retract:1");
623 if (replaceElement == null) replaceElement = packet.findChild("moderate", "urn:xmpp:message-moderate:1");
624 if (replaceElement != null) {
625 var reason = replaceElement.findChildContent("reason", "urn:xmpp:message-moderate:0");
626 if (reason == null) reason = replaceElement.findChildContent("reason", "urn:xmpp:message-moderate:1");
627 replacementId = (fasten == null ? replaceElement : fasten).getAttribute("id");
628 packet.setBody(reason == null ? "" : reason);
629 }
630 }
631 LocalizedContent body = packet.getBody();
632
633 final var reactions = packet.getExtension(Reactions.class);
634
635 final var oob = packet.getExtension(OutOfBandData.class);
636 final String oobUrl = oob != null ? oob.getURL() : null;
637 final var axolotlEncrypted = packet.getOnlyExtension(Encrypted.class);
638 int status;
639 final Jid counterpart;
640 final Jid to = packet.getTo();
641 final Jid from = packet.getFrom();
642 final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
643 final String remoteMsgId;
644 if (originId != null && originId.getAttribute("id") != null) {
645 remoteMsgId = originId.getAttribute("id");
646 } else {
647 remoteMsgId = packet.getId();
648 }
649 boolean notify = false;
650
651 Element html = packet.findChild("html", "http://jabber.org/protocol/xhtml-im");
652 if (html != null && html.findChild("body", "http://www.w3.org/1999/xhtml") == null) {
653 html = null;
654 }
655
656 if (from == null || !Jid.Invalid.isValid(from) || !Jid.Invalid.isValid(to)) {
657 Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
658 return;
659 }
660 if (query != null && !query.muc() && isTypeGroupChat) {
661 Log.e(
662 Config.LOGTAG,
663 account.getJid().asBareJid()
664 + ": received groupchat ("
665 + from
666 + ") message on regular MAM request. skipping");
667 return;
668 }
669 final Jid mucTrueCounterPart;
670 final OccupantId occupant;
671 if (isTypeGroupChat) {
672 final Conversation conversation =
673 mXmppConnectionService.find(account, from.asBareJid());
674 final Jid mucTrueCounterPartByPresence;
675 if (conversation != null) {
676 final var mucOptions = conversation.getMucOptions();
677 occupant =
678 mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null;
679 final var user =
680 occupant == null ? null : mucOptions.findUserByOccupantId(occupant.getId(), from);
681 mucTrueCounterPartByPresence = user == null ? null : user.getRealJid();
682 } else {
683 occupant = null;
684 mucTrueCounterPartByPresence = null;
685 }
686 mucTrueCounterPart =
687 getTrueCounterpart(
688 (query != null && query.safeToExtractTrueCounterpart())
689 ? mucUserElement
690 : null,
691 mucTrueCounterPartByPresence);
692 } else if (mucUserElement != null) {
693 final Conversation conversation =
694 mXmppConnectionService.find(account, from.asBareJid());
695 if (conversation != null) {
696 final var mucOptions = conversation.getMucOptions();
697 occupant =
698 mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null;
699 } else {
700 occupant = null;
701 }
702 mucTrueCounterPart = null;
703 } else {
704 mucTrueCounterPart = null;
705 occupant = null;
706 }
707 boolean isMucStatusMessage =
708 Jid.Invalid.hasValidFrom(packet)
709 && from.isBareJid()
710 && mucUserElement != null
711 && mucUserElement.hasChild("status");
712 boolean selfAddressed;
713 if (packet.fromAccount(account)) {
714 status = Message.STATUS_SEND;
715 selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
716 if (selfAddressed) {
717 counterpart = from;
718 } else {
719 counterpart = to != null ? to : account.getJid();
720 }
721 } else {
722 status = Message.STATUS_RECEIVED;
723 counterpart = from;
724 selfAddressed = false;
725 }
726
727 final Invite invite = extractInvite(packet);
728 if (invite != null) {
729 if (invite.jid.asBareJid().equals(account.getJid().asBareJid())) {
730 Log.d(
731 Config.LOGTAG,
732 account.getJid().asBareJid()
733 + ": ignore invite to "
734 + invite.jid
735 + " because it matches account");
736 } else if (isTypeGroupChat) {
737 Log.d(
738 Config.LOGTAG,
739 account.getJid().asBareJid()
740 + ": ignoring invite to "
741 + invite.jid
742 + " because it was received as group chat");
743 } else if (invite.direct
744 && (mucUserElement != null
745 || invite.inviter == null
746 || mXmppConnectionService.isMuc(account, invite.inviter))) {
747 Log.d(
748 Config.LOGTAG,
749 account.getJid().asBareJid()
750 + ": ignoring direct invite to "
751 + invite.jid
752 + " because it was received in MUC");
753 } else {
754 invite.execute(account);
755 return;
756 }
757 }
758
759 final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toString());
760 final Element webxdc = packet.findChild("x", "urn:xmpp:webxdc:0");
761 final Element thread = packet.findChild("thread");
762 if (webxdc != null && thread != null) {
763 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
764 Jid webxdcSender = counterpart.asBareJid();
765 if (conversation.getMode() == Conversation.MODE_MULTI) {
766 if(conversation.getMucOptions().nonanonymous()) {
767 webxdcSender = conversation.getMucOptions().getTrueCounterpart(counterpart);
768 } else {
769 webxdcSender = counterpart;
770 }
771 }
772 final var document = webxdc.findChildContent("document", "urn:xmpp:webxdc:0");
773 final var summary = webxdc.findChildContent("summary", "urn:xmpp:webxdc:0");
774 final var payload = webxdc.findChildContent("json", "urn:xmpp:json:0");
775 if (document != null || summary != null || payload != null) {
776 mXmppConnectionService.insertWebxdcUpdate(new WebxdcUpdate(
777 conversation,
778 remoteMsgId,
779 counterpart,
780 thread,
781 body == null ? null : body.content,
782 document,
783 summary,
784 payload
785 ));
786 }
787
788 final var realtime = webxdc.findChildContent("data", "urn:xmpp:webxdc:0");
789 if (realtime != null) conversation.webxdcRealtimeData(thread, realtime);
790
791 mXmppConnectionService.updateConversationUi();
792 }
793
794 // Basic visibility for voice requests
795 if (body == null && html == null && pgpEncrypted == null && axolotlEncrypted == null && !isMucStatusMessage) {
796 final Element formEl = packet.findChild("x", "jabber:x:data");
797 if (formEl != null) {
798 final Data form = Data.parse(formEl);
799 final String role = form.getValue("muc#role");
800 final String nick = form.getValue("muc#roomnick");
801 if ("http://jabber.org/protocol/muc#request".equals(form.getFormType()) && "participant".equals(role)) {
802 body = new LocalizedContent("" + nick + " is requesting to speak", "en", 1);
803 }
804 }
805 }
806
807 if (reactions == null && (body != null
808 || pgpEncrypted != null
809 || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload"))
810 || !attachments.isEmpty() || html != null || (packet.hasChild("subject") && packet.hasChild("thread")))
811 && !isMucStatusMessage) {
812 final Conversation conversation =
813 mXmppConnectionService.findOrCreateConversation(
814 account,
815 counterpart.asBareJid(),
816 conversationIsProbablyMuc,
817 false,
818 query,
819 false);
820 final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
821
822 if (serverMsgId == null) {
823 serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
824 }
825
826 if (selfAddressed) {
827 // don’t store serverMsgId on reflections for edits
828 final var reflectedServerMsgId =
829 Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
830 if (mXmppConnectionService.markMessage(
831 conversation,
832 remoteMsgId,
833 Message.STATUS_SEND_RECEIVED,
834 reflectedServerMsgId)) {
835 return;
836 }
837 status = Message.STATUS_RECEIVED;
838 if (remoteMsgId != null
839 && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
840 return;
841 }
842 }
843
844 if (isTypeGroupChat) {
845 if (conversation.getMucOptions().isSelf(counterpart)) {
846 status = Message.STATUS_SEND_RECEIVED;
847 isCarbon = true; // not really carbon but received from another resource
848 // don’t store serverMsgId on reflections for edits
849 final var reflectedServerMsgId =
850 Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
851 if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, reflectedServerMsgId, body, html, packet.findChildContent("subject"), packet.findChild("thread"), attachments)) {
852 return;
853 } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
854 if (body != null) {
855 Message message = conversation.findSentMessageWithBody(body.content);
856 if (message != null) {
857 mXmppConnectionService.markMessage(message, status);
858 return;
859 }
860 }
861 }
862 } else {
863 status = Message.STATUS_RECEIVED;
864 }
865 }
866 final Message message;
867 if (pgpEncrypted != null && Config.supportOpenPgp()) {
868 message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
869 } else if (axolotlEncrypted != null && Config.supportOmemo()) {
870 Jid origin;
871 Set<Jid> fallbacksBySourceId = Collections.emptySet();
872 if (conversationMultiMode) {
873 final Jid fallback =
874 conversation.getMucOptions().getTrueCounterpart(counterpart);
875 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
876 if (origin == null) {
877 try {
878 fallbacksBySourceId =
879 account.getAxolotlService()
880 .findCounterpartsBySourceId(
881 XmppAxolotlMessage.parseSourceId(
882 axolotlEncrypted));
883 } catch (IllegalArgumentException e) {
884 // ignoring
885 }
886 }
887 if (origin == null && fallbacksBySourceId.isEmpty()) {
888 Log.d(
889 Config.LOGTAG,
890 "axolotl message in anonymous conference received and no possible"
891 + " fallbacks");
892 return;
893 }
894 } else {
895 fallbacksBySourceId = Collections.emptySet();
896 origin = from;
897 }
898
899 final boolean liveMessage =
900 query == null && !isTypeGroupChat && mucUserElement == null;
901 final boolean checkedForDuplicates =
902 liveMessage
903 || (serverMsgId != null
904 && remoteMsgId != null
905 && !conversation.possibleDuplicate(
906 serverMsgId, remoteMsgId));
907
908 if (origin != null) {
909 message =
910 parseAxolotlChat(
911 axolotlEncrypted,
912 origin,
913 conversation,
914 status,
915 checkedForDuplicates,
916 query != null);
917 } else {
918 Message trial = null;
919 for (Jid fallback : fallbacksBySourceId) {
920 trial =
921 parseAxolotlChat(
922 axolotlEncrypted,
923 fallback,
924 conversation,
925 status,
926 checkedForDuplicates && fallbacksBySourceId.size() == 1,
927 query != null);
928 if (trial != null) {
929 Log.d(
930 Config.LOGTAG,
931 account.getJid().asBareJid()
932 + ": decoded muc message using fallback");
933 origin = fallback;
934 break;
935 }
936 }
937 message = trial;
938 }
939 if (message == null) {
940 if (query == null
941 && extractChatState(
942 mXmppConnectionService.find(account, counterpart.asBareJid()),
943 isTypeGroupChat,
944 packet)) {
945 mXmppConnectionService.updateConversationUi();
946 }
947 if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
948 Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
949 if (previouslySent != null
950 && previouslySent.getServerMsgId() == null
951 && serverMsgId != null) {
952 previouslySent.setServerMsgId(serverMsgId);
953 mXmppConnectionService.databaseBackend.updateMessage(
954 previouslySent, false);
955 Log.d(
956 Config.LOGTAG,
957 account.getJid().asBareJid()
958 + ": encountered previously sent OMEMO message without"
959 + " serverId. updating...");
960 }
961 }
962 return;
963 }
964 if (conversationMultiMode) {
965 message.setTrueCounterpart(origin);
966 }
967 } else if (body == null && !attachments.isEmpty()) {
968 message = new Message(conversation, "", Message.ENCRYPTION_NONE, status);
969 } else {
970 message = new Message(conversation, body == null ? null : body.content, Message.ENCRYPTION_NONE, status);
971 if (body != null && body.count > 1) {
972 message.setBodyLanguage(body.language);
973 }
974 }
975
976 Element addresses = packet.findChild("addresses", "http://jabber.org/protocol/address");
977 if (status == Message.STATUS_RECEIVED && addresses != null) {
978 for (Element address : addresses.getChildren()) {
979 if (!address.getName().equals("address") || !address.getNamespace().equals("http://jabber.org/protocol/address")) continue;
980
981 if (address.getAttribute("type").equals("ofrom") && address.getAttribute("jid") != null) {
982 Jid ofrom = address.getAttributeAsJid("jid");
983 if (Jid.Invalid.isValid(ofrom) && ofrom.getDomain().equals(counterpart.getDomain()) &&
984 conversation.getAccount().getRoster().getContact(counterpart.getDomain()).getPresences().anySupport("http://jabber.org/protocol/address")) {
985
986 message.setTrueCounterpart(ofrom);
987 }
988 }
989 }
990 }
991
992 if (html != null) message.addPayload(html);
993 message.setSubject(packet.findChildContent("subject"));
994 message.setCounterpart(counterpart);
995 message.setRemoteMsgId(remoteMsgId);
996 message.setServerMsgId(serverMsgId);
997 message.setCarbon(isCarbon);
998 message.setTime(timestamp);
999 if (!attachments.isEmpty()) {
1000 message.setFileParams(attachments.iterator().next());
1001 if (CryptoHelper.isPgpEncryptedUrl(message.getFileParams().url)) {
1002 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1003 }
1004 }
1005 message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
1006 for (Element el : packet.getChildren()) {
1007 if ((el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) ||
1008 (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0"))) {
1009 message.addPayload(el);
1010 }
1011 if (el.getName().equals("thread") && (el.getNamespace() == null || el.getNamespace().equals("jabber:client"))) {
1012 el.setAttribute("xmlns", "jabber:client");
1013 message.addPayload(el);
1014 }
1015 if (el.getName().equals("reply") && el.getNamespace() != null && el.getNamespace().equals("urn:xmpp:reply:0")) {
1016 message.addPayload(el);
1017 if (el.getAttribute("id") != null) {
1018 for (final var parent : mXmppConnectionService.getMessageFuzzyIds(conversation, List.of(el.getAttribute("id"))).entrySet()) {
1019 message.setInReplyTo(parent.getValue());
1020 }
1021 }
1022 }
1023 if (el.getName().equals("attention") && el.getNamespace() != null && el.getNamespace().equals("urn:xmpp:attention:0")) {
1024 message.addPayload(el);
1025 }
1026 if (el.getName().equals("Description") && el.getNamespace() != null && el.getNamespace().equals("http://www.w3.org/1999/02/22-rdf-syntax-ns#")) {
1027 message.addPayload(el);
1028 }
1029 }
1030 if (conversationMultiMode) {
1031 final var mucOptions = conversation.getMucOptions();
1032 if (occupant != null) {
1033 message.setOccupantId(occupant.getId());
1034 }
1035 message.setMucUser(mucOptions.findUserByFullJid(counterpart));
1036 final Jid fallback = mucOptions.getTrueCounterpart(counterpart);
1037 Jid trueCounterpart;
1038 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
1039 trueCounterpart = message.getTrueCounterpart();
1040 } else if (query != null && query.safeToExtractTrueCounterpart()) {
1041 trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
1042 } else {
1043 trueCounterpart = fallback;
1044 }
1045 if (trueCounterpart != null && isTypeGroupChat) {
1046 if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
1047 status =
1048 isTypeGroupChat
1049 ? Message.STATUS_SEND_RECEIVED
1050 : Message.STATUS_SEND;
1051 } else {
1052 status = Message.STATUS_RECEIVED;
1053 message.setCarbon(false);
1054 }
1055 }
1056 message.setStatus(status);
1057 message.setTrueCounterpart(trueCounterpart);
1058 if (!isTypeGroupChat) {
1059 message.setType(Message.TYPE_PRIVATE);
1060 }
1061 } else {
1062 updateLastseen(account, from);
1063 }
1064
1065 if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
1066 final Message replacedMessage =
1067 conversation.findMessageWithRemoteIdAndCounterpart(
1068 replacementId,
1069 counterpart);
1070 if (replacedMessage != null) {
1071 final boolean fingerprintsMatch =
1072 replacedMessage.getFingerprint() == null
1073 || replacedMessage
1074 .getFingerprint()
1075 .equals(message.getFingerprint());
1076 final boolean trueCountersMatch =
1077 replacedMessage.getTrueCounterpart() != null
1078 && message.getTrueCounterpart() != null
1079 && replacedMessage
1080 .getTrueCounterpart()
1081 .asBareJid()
1082 .equals(message.getTrueCounterpart().asBareJid());
1083 final boolean occupantIdMatch =
1084 replacedMessage.getOccupantId() != null
1085 && replacedMessage
1086 .getOccupantId()
1087 .equals(message.getOccupantId());
1088 final boolean mucUserMatches =
1089 query == null
1090 && replacedMessage.sameMucUser(
1091 message); // can not be checked when using mam
1092 final boolean duplicate = conversation.hasDuplicateMessage(message);
1093 if (fingerprintsMatch && (trueCountersMatch || occupantIdMatch || !conversationMultiMode || mucUserMatches || counterpart.isBareJid()) && !duplicate) {
1094 synchronized (replacedMessage) {
1095 final String uuid = replacedMessage.getUuid();
1096 replacedMessage.setUuid(UUID.randomUUID().toString());
1097 replacedMessage.setBody(message.getBody());
1098 replacedMessage.setSubject(message.getSubject());
1099 replacedMessage.setThread(message.getThread());
1100 replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
1101 if (replaceElement != null && !replaceElement.getName().equals("replace")) {
1102 mXmppConnectionService.getFileBackend().deleteFile(replacedMessage);
1103 mXmppConnectionService.evictPreview(message.getUuid());
1104 List<Element> thumbs = replacedMessage.getFileParams() != null ? replacedMessage.getFileParams().getThumbnails() : null;
1105 if (thumbs != null && !thumbs.isEmpty()) {
1106 for (Element thumb : thumbs) {
1107 Uri uri = Uri.parse(thumb.getAttribute("uri"));
1108 if (uri.getScheme().equals("cid")) {
1109 Cid cid = BobTransfer.cid(uri);
1110 if (cid == null) continue;
1111 DownloadableFile f = mXmppConnectionService.getFileForCid(cid);
1112 if (f != null) {
1113 mXmppConnectionService.evictPreview(f);
1114 f.delete();
1115 }
1116 }
1117 }
1118 }
1119 replacedMessage.clearPayloads();
1120 replacedMessage.setFileParams(null);
1121 replacedMessage.addPayload(replaceElement);
1122 } else {
1123 replacedMessage.clearPayloads();
1124 for (final var p : message.getPayloads()) {
1125 replacedMessage.addPayload(p);
1126 }
1127 }
1128 replacedMessage.setInReplyTo(message.getInReplyTo());
1129
1130 // we store the IDs of the replacing message. This is essentially unused
1131 // today (only the fact that there are _some_ edits causes the edit icon
1132 // to appear)
1133 replacedMessage.putEdited(
1134 message.getRemoteMsgId(), message.getServerMsgId());
1135
1136 // we used to call
1137 // `replacedMessage.setServerMsgId(message.getServerMsgId());` so during
1138 // catchup we could start from the edit; not the original message
1139 // however this caused problems for things like reactions that refer to
1140 // the serverMsgId
1141
1142 replacedMessage.setEncryption(message.getEncryption());
1143 if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
1144 replacedMessage.markUnread();
1145 }
1146 extractChatState(
1147 mXmppConnectionService.find(account, counterpart.asBareJid()),
1148 isTypeGroupChat,
1149 packet);
1150 mXmppConnectionService.updateMessage(replacedMessage, uuid);
1151 if (mXmppConnectionService.confirmMessages()
1152 && replacedMessage.getStatus() == Message.STATUS_RECEIVED
1153 && (replacedMessage.trusted()
1154 || replacedMessage
1155 .isPrivateMessage()) // TODO do we really want
1156 // to send receipts for all
1157 // PMs?
1158 && remoteMsgId != null
1159 && !selfAddressed
1160 && !isTypeGroupChat) {
1161 processMessageReceipts(account, packet, remoteMsgId, query);
1162 }
1163 if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
1164 conversation
1165 .getAccount()
1166 .getPgpDecryptionService()
1167 .discard(replacedMessage);
1168 conversation
1169 .getAccount()
1170 .getPgpDecryptionService()
1171 .decrypt(replacedMessage, false);
1172 }
1173 }
1174 mXmppConnectionService.getNotificationService().updateNotification();
1175 return;
1176 } else {
1177 Log.d(
1178 Config.LOGTAG,
1179 account.getJid().asBareJid()
1180 + ": received message correction but verification didn't"
1181 + " check out");
1182 }
1183 } else if (message.getBody() == null || message.getBody().equals("") || message.getBody().equals(" ")) {
1184 return;
1185 }
1186 if (replaceElement != null && !replaceElement.getName().equals("replace")) return;
1187 }
1188
1189 boolean checkForDuplicates =
1190 (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
1191 || message.isPrivateMessage()
1192 || message.getServerMsgId() != null
1193 || (query == null
1194 && mXmppConnectionService
1195 .getMessageArchiveService()
1196 .isCatchupInProgress(conversation));
1197 if (checkForDuplicates) {
1198 final Message duplicate = conversation.findDuplicateMessage(message);
1199 if (duplicate != null) {
1200 final boolean serverMsgIdUpdated;
1201 if (duplicate.getStatus() != Message.STATUS_RECEIVED
1202 && duplicate.getUuid().equals(message.getRemoteMsgId())
1203 && duplicate.getServerMsgId() == null
1204 && message.getServerMsgId() != null) {
1205 duplicate.setServerMsgId(message.getServerMsgId());
1206 if (mXmppConnectionService.databaseBackend.updateMessage(
1207 duplicate, false)) {
1208 serverMsgIdUpdated = true;
1209 } else {
1210 serverMsgIdUpdated = false;
1211 Log.e(Config.LOGTAG, "failed to update message");
1212 }
1213 } else {
1214 serverMsgIdUpdated = false;
1215 }
1216 Log.d(
1217 Config.LOGTAG,
1218 "skipping duplicate message with "
1219 + message.getCounterpart()
1220 + ". serverMsgIdUpdated="
1221 + serverMsgIdUpdated);
1222 return;
1223 }
1224 }
1225
1226 if (query != null
1227 && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
1228 conversation.prepend(query.getActualInThisQuery(), message);
1229 } else {
1230 conversation.add(message);
1231 }
1232 if (query != null) {
1233 query.incrementActualMessageCount();
1234 }
1235
1236 if (query == null || query.isCatchup()) { // either no mam or catchup
1237 if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
1238 mXmppConnectionService.markRead(conversation);
1239 if (query == null) {
1240 activateGracePeriod(account);
1241 }
1242 } else {
1243 message.markUnread();
1244 notify = true;
1245 }
1246 }
1247
1248 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1249 notify =
1250 conversation
1251 .getAccount()
1252 .getPgpDecryptionService()
1253 .decrypt(message, notify);
1254 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
1255 || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1256 notify = false;
1257 }
1258
1259 if (query == null) {
1260 extractChatState(
1261 mXmppConnectionService.find(account, counterpart.asBareJid()),
1262 isTypeGroupChat,
1263 packet);
1264 mXmppConnectionService.updateConversationUi();
1265 }
1266
1267 if (mXmppConnectionService.confirmMessages()
1268 && message.getStatus() == Message.STATUS_RECEIVED
1269 && (message.trusted() || message.isPrivateMessage())
1270 && remoteMsgId != null
1271 && !selfAddressed
1272 && !isTypeGroupChat) {
1273 processMessageReceipts(account, packet, remoteMsgId, query);
1274 }
1275
1276 if (message.getFileParams() != null) {
1277 for (Cid cid : message.getFileParams().getCids()) {
1278 File f = mXmppConnectionService.getFileForCid(cid);
1279 if (f != null && f.canRead()) {
1280 message.setRelativeFilePath(f.getAbsolutePath());
1281 mXmppConnectionService.getFileBackend().updateFileParams(message, null, false);
1282 break;
1283 }
1284 }
1285 }
1286
1287 mXmppConnectionService.databaseBackend.createMessage(message);
1288 final HttpConnectionManager manager =
1289 this.mXmppConnectionService.getHttpConnectionManager();
1290 if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
1291 if (message.getOob() != null && "cid".equalsIgnoreCase(message.getOob().getScheme())) {
1292 try {
1293 BobTransfer transfer = new BobTransfer.ForMessage(message, mXmppConnectionService);
1294 message.setTransferable(transfer);
1295 transfer.start();
1296 } catch (URISyntaxException e) {
1297 Log.d(Config.LOGTAG, "BobTransfer failed to parse URI");
1298 }
1299 } else {
1300 manager.createNewDownloadConnection(message);
1301 }
1302 } else if (notify) {
1303 if (query != null && query.isCatchup()) {
1304 mXmppConnectionService.getNotificationService().pushFromBacklog(message);
1305 } else {
1306 mXmppConnectionService.getNotificationService().push(message);
1307 }
1308 }
1309 } else if (!packet.hasChild("body")) { // no body
1310 final Conversation conversation =
1311 mXmppConnectionService.find(account, from.asBareJid());
1312 if (axolotlEncrypted != null) {
1313 Jid origin;
1314 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1315 final Jid fallback =
1316 conversation.getMucOptions().getTrueCounterpart(counterpart);
1317 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
1318 if (origin == null) {
1319 Log.d(
1320 Config.LOGTAG,
1321 "omemo key transport message in anonymous conference received");
1322 return;
1323 }
1324 } else if (isTypeGroupChat) {
1325 return;
1326 } else {
1327 origin = from;
1328 }
1329 try {
1330 final XmppAxolotlMessage xmppAxolotlMessage =
1331 XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
1332 account.getAxolotlService()
1333 .processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
1334 Log.d(
1335 Config.LOGTAG,
1336 account.getJid().asBareJid()
1337 + ": omemo key transport message received from "
1338 + origin);
1339 } catch (Exception e) {
1340 Log.d(
1341 Config.LOGTAG,
1342 account.getJid().asBareJid()
1343 + ": invalid omemo key transport message received "
1344 + e.getMessage());
1345 return;
1346 }
1347 }
1348
1349 if (query == null
1350 && extractChatState(
1351 mXmppConnectionService.find(account, counterpart.asBareJid()),
1352 isTypeGroupChat,
1353 packet)) {
1354 mXmppConnectionService.updateConversationUi();
1355 }
1356
1357 if (isTypeGroupChat) {
1358 if (packet.hasChild("subject")
1359 && !packet.hasChild("thread")) { // We already know it has no body per above
1360 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1361 conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
1362 final LocalizedContent subject = packet.getSubject();
1363 if (subject != null
1364 && conversation.getMucOptions().setSubject(subject.content)) {
1365 mXmppConnectionService.updateConversation(conversation);
1366 }
1367 mXmppConnectionService.updateConversationUi();
1368 return;
1369 }
1370 }
1371 }
1372 if (conversation != null
1373 && mucUserElement != null
1374 && Jid.Invalid.hasValidFrom(packet)
1375 && from.isBareJid()) {
1376 for (Element child : mucUserElement.getChildren()) {
1377 if ("status".equals(child.getName())) {
1378 try {
1379 int code = Integer.parseInt(child.getAttribute("code"));
1380 if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
1381 mXmppConnectionService.fetchConferenceConfiguration(conversation);
1382 break;
1383 }
1384 } catch (Exception e) {
1385 // ignored
1386 }
1387 } else if ("item".equals(child.getName())) {
1388 final var user = AbstractParser.parseItem(conversation, child);
1389 Log.d(
1390 Config.LOGTAG,
1391 account.getJid()
1392 + ": changing affiliation for "
1393 + user.getRealJid()
1394 + " to "
1395 + user.getAffiliation()
1396 + " in "
1397 + conversation.getJid().asBareJid());
1398 if (!user.realJidMatchesAccount()) {
1399 final var mucOptions = conversation.getMucOptions();
1400 final boolean isNew = mucOptions.updateUser(user);
1401 final var avatarService = mXmppConnectionService.getAvatarService();
1402 if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
1403 avatarService.clear(mucOptions);
1404 }
1405 avatarService.clear(user);
1406 mXmppConnectionService.updateMucRosterUi();
1407 mXmppConnectionService.updateConversationUi();
1408 Contact contact = user.getContact();
1409 if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
1410 Jid jid = user.getRealJid();
1411 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
1412 if (cryptoTargets.remove(user.getRealJid())) {
1413 Log.d(
1414 Config.LOGTAG,
1415 account.getJid().asBareJid()
1416 + ": removed "
1417 + jid
1418 + " from crypto targets of "
1419 + conversation.getName());
1420 conversation.setAcceptedCryptoTargets(cryptoTargets);
1421 mXmppConnectionService.updateConversation(conversation);
1422 }
1423 } else if (isNew
1424 && user.getRealJid() != null
1425 && conversation.getMucOptions().isPrivateAndNonAnonymous()
1426 && (contact == null || !contact.mutualPresenceSubscription())
1427 && account.getAxolotlService()
1428 .hasEmptyDeviceList(user.getRealJid())) {
1429 account.getAxolotlService().fetchDeviceIds(user.getRealJid());
1430 }
1431 }
1432 }
1433 }
1434 }
1435 if (!isTypeGroupChat) {
1436 for (Element child : packet.getChildren()) {
1437 if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
1438 && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
1439 final String action = child.getName();
1440 final String sessionId = child.getAttribute("id");
1441 if (sessionId == null) {
1442 break;
1443 }
1444 if (query == null && offlineMessagesRetrieved) {
1445 if (serverMsgId == null) {
1446 serverMsgId = extractStanzaId(account, packet);
1447 }
1448 mXmppConnectionService
1449 .getJingleConnectionManager()
1450 .deliverMessage(
1451 account,
1452 packet.getTo(),
1453 packet.getFrom(),
1454 child,
1455 remoteMsgId,
1456 serverMsgId,
1457 timestamp);
1458 final Contact contact = account.getRoster().getContact(from);
1459 // this is the same condition that is found in JingleRtpConnection for
1460 // the 'ringing' response. Responding with delivery receipts predates
1461 // the 'ringing' spec'd
1462 final boolean sendReceipts =
1463 contact.showInContactList()
1464 || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
1465 if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
1466 processMessageReceipts(account, packet, remoteMsgId, null);
1467 }
1468 } else if ((query != null && query.isCatchup())
1469 || !offlineMessagesRetrieved) {
1470 if ("propose".equals(action)) {
1471 final Element description = child.findChild("description");
1472 final String namespace =
1473 description == null ? null : description.getNamespace();
1474 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1475 final Conversation c =
1476 mXmppConnectionService.findOrCreateConversation(
1477 account, counterpart.asBareJid(), false, false);
1478 final Message preExistingMessage =
1479 c.findRtpSession(sessionId, status);
1480 if (preExistingMessage != null) {
1481 preExistingMessage.setServerMsgId(serverMsgId);
1482 mXmppConnectionService.updateMessage(preExistingMessage);
1483 break;
1484 }
1485 final Message message =
1486 new Message(
1487 c, status, Message.TYPE_RTP_SESSION, sessionId);
1488 message.setServerMsgId(serverMsgId);
1489 message.setTime(timestamp);
1490 message.setBody(new RtpSessionStatus(false, 0).toString());
1491 message.markUnread();
1492 c.add(message);
1493 mXmppConnectionService.getNotificationService().possiblyMissedCall(c.getUuid() + sessionId, message);
1494 if (query != null) query.incrementActualMessageCount();
1495 mXmppConnectionService.databaseBackend.createMessage(message);
1496 }
1497 } else if ("proceed".equals(action)) {
1498 // status needs to be flipped to find the original propose
1499 final Conversation c =
1500 mXmppConnectionService.findOrCreateConversation(
1501 account, counterpart.asBareJid(), false, false);
1502 final int s =
1503 packet.fromAccount(account)
1504 ? Message.STATUS_RECEIVED
1505 : Message.STATUS_SEND;
1506 final Message message = c.findRtpSession(sessionId, s);
1507 if (message != null) {
1508 message.setBody(new RtpSessionStatus(true, 0).toString());
1509 if (serverMsgId != null) {
1510 message.setServerMsgId(serverMsgId);
1511 }
1512 message.setTime(timestamp);
1513 message.markRead();
1514 mXmppConnectionService.getNotificationService().possiblyMissedCall(c.getUuid() + sessionId, message);
1515 if (query != null) query.incrementActualMessageCount();
1516 mXmppConnectionService.updateMessage(message, true);
1517 } else {
1518 Log.d(
1519 Config.LOGTAG,
1520 "unable to find original rtp session message for"
1521 + " received propose");
1522 }
1523
1524 } else if ("finish".equals(action)) {
1525 Log.d(
1526 Config.LOGTAG,
1527 "received JMI 'finish' during MAM catch-up. Can be used to"
1528 + " update success/failure and duration");
1529 }
1530 } else {
1531 // MAM reloads (non catchups
1532 if ("propose".equals(action)) {
1533 final Element description = child.findChild("description");
1534 final String namespace =
1535 description == null ? null : description.getNamespace();
1536 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1537 final Conversation c =
1538 mXmppConnectionService.findOrCreateConversation(
1539 account, counterpart.asBareJid(), false, false);
1540 final Message preExistingMessage =
1541 c.findRtpSession(sessionId, status);
1542 if (preExistingMessage != null) {
1543 preExistingMessage.setServerMsgId(serverMsgId);
1544 mXmppConnectionService.updateMessage(preExistingMessage);
1545 break;
1546 }
1547 final Message message =
1548 new Message(
1549 c, status, Message.TYPE_RTP_SESSION, sessionId);
1550 message.setServerMsgId(serverMsgId);
1551 message.setTime(timestamp);
1552 message.setBody(new RtpSessionStatus(true, 0).toString());
1553 if (query.getPagingOrder()
1554 == MessageArchiveService.PagingOrder.REVERSE) {
1555 c.prepend(query.getActualInThisQuery(), message);
1556 } else {
1557 c.add(message);
1558 }
1559 query.incrementActualMessageCount();
1560 mXmppConnectionService.databaseBackend.createMessage(message);
1561 }
1562 }
1563 }
1564 break;
1565 }
1566 }
1567 }
1568
1569 final var received =
1570 packet.getExtension(
1571 im.conversations.android.xmpp.model.receipts.Received.class);
1572 if (received != null) {
1573 processReceived(received, packet, query, from);
1574 }
1575 final var displayed = packet.getExtension(Displayed.class);
1576 if (displayed != null) {
1577 processDisplayed(
1578 displayed,
1579 packet,
1580 selfAddressed,
1581 counterpart,
1582 query,
1583 isTypeGroupChat,
1584 conversation,
1585 mucUserElement,
1586 from);
1587 }
1588
1589 // end no body
1590 }
1591
1592 if (reactions != null) {
1593 processReactions(
1594 reactions,
1595 mXmppConnectionService.find(account, from.asBareJid()),
1596 isTypeGroupChat,
1597 occupant,
1598 counterpart,
1599 mucTrueCounterPart,
1600 status,
1601 packet);
1602 }
1603
1604 final var event = original.getExtension(Event.class);
1605 if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1606 final var action = event.getAction();
1607 final var node = action == null ? null : action.getNode();
1608 if (node == null) {
1609 Log.d(
1610 Config.LOGTAG,
1611 account.getJid().asBareJid()
1612 + ": no node found in PubSub event from "
1613 + original.getFrom());
1614 } else if (action instanceof Items items) {
1615 parseEvent(items, original.getFrom(), account);
1616 } else if (action instanceof Purge purge) {
1617 parsePurgeEvent(purge, original.getFrom(), account);
1618 } else if (action instanceof Delete delete) {
1619 parseDeleteEvent(delete, from, account);
1620 }
1621 }
1622
1623 final String nick = packet.findChildContent("nick", Namespace.NICK);
1624 if (nick != null && Jid.Invalid.hasValidFrom(original)) {
1625 if (mXmppConnectionService.isMuc(account, from)) {
1626 return;
1627 }
1628 final Contact contact = account.getRoster().getContact(from);
1629 if (contact.setPresenceName(nick)) {
1630 mXmppConnectionService.syncRoster(account);
1631 mXmppConnectionService.getAvatarService().clear(contact);
1632 }
1633 }
1634 }
1635
1636 private void processReceived(
1637 final im.conversations.android.xmpp.model.receipts.Received received,
1638 final im.conversations.android.xmpp.model.stanza.Message packet,
1639 final MessageArchiveService.Query query,
1640 final Jid from) {
1641 final var id = received.getId();
1642 if (packet.fromAccount(account)) {
1643 if (query != null && id != null && packet.getTo() != null) {
1644 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1645 }
1646 } else if (id != null) {
1647 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1648 final String sessionId =
1649 id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1650 mXmppConnectionService
1651 .getJingleConnectionManager()
1652 .updateProposedSessionDiscovered(
1653 account,
1654 from,
1655 sessionId,
1656 JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1657 } else {
1658 mXmppConnectionService.markMessage(
1659 account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1660 }
1661 }
1662 }
1663
1664 private void processDisplayed(
1665 final Displayed displayed,
1666 final im.conversations.android.xmpp.model.stanza.Message packet,
1667 final boolean selfAddressed,
1668 final Jid counterpart,
1669 final MessageArchiveService.Query query,
1670 final boolean isTypeGroupChat,
1671 Conversation conversation,
1672 Element mucUserElement,
1673 Jid from) {
1674 final var id = displayed.getId();
1675 // TODO we don’t even use 'sender' any more. Remove this!
1676 final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1677 if (packet.fromAccount(account) && !selfAddressed) {
1678 final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1679 final Message message =
1680 (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1681 if (message != null && (query == null || query.isCatchup())) {
1682 mXmppConnectionService.markReadUpTo(c, message);
1683 }
1684 if (query == null) {
1685 activateGracePeriod(account);
1686 }
1687 } else if (isTypeGroupChat) {
1688 final Message message;
1689 if (conversation != null && id != null) {
1690 if (sender != null) {
1691 message = conversation.findMessageWithRemoteId(id, sender);
1692 } else {
1693 message = conversation.findMessageWithServerMsgId(id);
1694 }
1695 } else {
1696 message = null;
1697 }
1698 if (message != null) {
1699 // TODO use occupantId to extract true counterpart from presence
1700 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1701 // TODO try to externalize mucTrueCounterpart
1702 final Jid trueJid =
1703 getTrueCounterpart(
1704 (query != null && query.safeToExtractTrueCounterpart())
1705 ? mucUserElement
1706 : null,
1707 fallback);
1708 final boolean trueJidMatchesAccount =
1709 account.getJid()
1710 .asBareJid()
1711 .equals(trueJid == null ? null : trueJid.asBareJid());
1712 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1713 if (!message.isRead()
1714 && (query == null || query.isCatchup())) { // checking if message is
1715 // unread fixes race conditions
1716 // with reflections
1717 mXmppConnectionService.markReadUpTo(conversation, message);
1718 }
1719 } else if (!counterpart.isBareJid() && trueJid != null) {
1720 final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1721 if (message.addReadByMarker(readByMarker)) {
1722 final var mucOptions = conversation.getMucOptions();
1723 final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1724 final var readyBy = message.getReadyByTrue();
1725 final var mStatus = message.getStatus();
1726 if (mucOptions.isPrivateAndNonAnonymous()
1727 && (mStatus == Message.STATUS_SEND_RECEIVED
1728 || mStatus == Message.STATUS_SEND)
1729 && readyBy.containsAll(everyone)) {
1730 message.setStatus(Message.STATUS_SEND_DISPLAYED);
1731 }
1732 mXmppConnectionService.updateMessage(message, false);
1733 }
1734 }
1735 }
1736 } else {
1737 final Message displayedMessage =
1738 mXmppConnectionService.markMessage(
1739 account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1740 Message message = displayedMessage == null ? null : displayedMessage.prev();
1741 while (message != null
1742 && message.getStatus() == Message.STATUS_SEND_RECEIVED
1743 && message.getTimeSent() < displayedMessage.getTimeSent()) {
1744 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1745 message = message.prev();
1746 }
1747 if (displayedMessage != null && selfAddressed) {
1748 dismissNotification(account, counterpart, query, id);
1749 }
1750 }
1751 }
1752
1753 private void processReactions(
1754 final Reactions reactions,
1755 final Conversation conversation,
1756 final boolean isTypeGroupChat,
1757 final OccupantId occupant,
1758 final Jid counterpart,
1759 final Jid mucTrueCounterPart,
1760 final int status,
1761 final im.conversations.android.xmpp.model.stanza.Message packet) {
1762 final String reactingTo = reactions.getId();
1763 if (conversation != null && reactingTo != null) {
1764 if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1765 final var mucOptions = conversation.getMucOptions();
1766 final var occupantId = occupant == null ? null : occupant.getId();
1767 if (occupantId != null) {
1768 final boolean isReceived = !mucOptions.isSelf(occupantId);
1769 final Message message;
1770 final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1771 if (inMemoryMessage != null) {
1772 message = inMemoryMessage;
1773 } else {
1774 message =
1775 mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1776 conversation, reactingTo);
1777 }
1778 if (message != null) {
1779 final var newReactions = new HashSet<>(reactions.getReactions());
1780 newReactions.removeAll(message.getReactions().stream().filter(r -> occupantId.equals(r.occupantId)).map(r -> r.reaction).collect(Collectors.toList()));
1781 final var combinedReactions =
1782 Reaction.withOccupantId(
1783 message.getReactions(),
1784 reactions.getReactions(),
1785 isReceived,
1786 counterpart,
1787 mucTrueCounterPart,
1788 occupantId,
1789 message.getRemoteMsgId());
1790 message.setReactions(combinedReactions);
1791 mXmppConnectionService.updateMessage(message, false);
1792 if (isReceived) mXmppConnectionService.getNotificationService().push(message, counterpart, occupantId, newReactions);
1793 } else {
1794 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1795 }
1796 } else {
1797 Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1798 }
1799 } else {
1800 final Message message;
1801 final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1802 if (inMemoryMessage != null) {
1803 message = inMemoryMessage;
1804 } else {
1805 message =
1806 mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1807 conversation, reactingTo);
1808 }
1809 if (message == null) {
1810 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1811 return;
1812 }
1813 final boolean isReceived;
1814 final Jid reactionFrom;
1815 if (conversation.getMode() == Conversational.MODE_MULTI) {
1816 Log.d(Config.LOGTAG, "received reaction as MUC PM. triggering validation");
1817 final var mucOptions = conversation.getMucOptions();
1818 final var occupantId = occupant == null ? null : occupant.getId();
1819 if (occupantId == null) {
1820 Log.d(
1821 Config.LOGTAG,
1822 "received reaction via PM channel w/o occupant ids. ignoring");
1823 return;
1824 }
1825 isReceived = !mucOptions.isSelf(occupantId);
1826 if (isReceived) {
1827 reactionFrom = counterpart;
1828 } else {
1829 if (!occupantId.equals(message.getOccupantId())) {
1830 Log.d(
1831 Config.LOGTAG,
1832 "reaction received via MUC PM did not pass validation");
1833 return;
1834 }
1835 reactionFrom = account.getJid().asBareJid();
1836 }
1837 } else {
1838 if (packet.fromAccount(account)) {
1839 isReceived = false;
1840 reactionFrom = account.getJid().asBareJid();
1841 } else {
1842 isReceived = true;
1843 reactionFrom = counterpart;
1844 }
1845 }
1846 final var newReactions = new HashSet<>(reactions.getReactions());
1847 newReactions.removeAll(message.getReactions().stream().filter(r -> reactionFrom.equals(r.from)).map(r -> r.reaction).collect(Collectors.toList()));
1848 final var combinedReactions =
1849 Reaction.withFrom(
1850 message.getReactions(),
1851 reactions.getReactions(),
1852 isReceived,
1853 reactionFrom,
1854 message.getRemoteMsgId());
1855 message.setReactions(combinedReactions);
1856 mXmppConnectionService.updateMessage(message, false);
1857 if (status < Message.STATUS_SEND) mXmppConnectionService.getNotificationService().push(message, counterpart, null, newReactions);
1858 }
1859 }
1860 }
1861
1862 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1863 getForwardedMessagePacket(
1864 final im.conversations.android.xmpp.model.stanza.Message original,
1865 Class<? extends Extension> clazz) {
1866 final var extension = original.getExtension(clazz);
1867 final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1868 if (forwarded == null) {
1869 return null;
1870 }
1871 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1872 final var forwardedMessage = forwarded.getMessage();
1873 if (forwardedMessage == null) {
1874 return null;
1875 }
1876 return new Pair<>(forwardedMessage, timestamp);
1877 }
1878
1879 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1880 getForwardedMessagePacket(
1881 final im.conversations.android.xmpp.model.stanza.Message original,
1882 final String name,
1883 final String namespace) {
1884 final Element wrapper = original.findChild(name, namespace);
1885 final var forwardedElement =
1886 wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1887 if (forwardedElement instanceof Forwarded forwarded) {
1888 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1889 final var forwardedMessage = forwarded.getMessage();
1890 if (forwardedMessage == null) {
1891 return null;
1892 }
1893 return new Pair<>(forwardedMessage, timestamp);
1894 }
1895 return null;
1896 }
1897
1898 private void dismissNotification(
1899 Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1900 final Conversation conversation =
1901 mXmppConnectionService.find(account, counterpart.asBareJid());
1902 if (conversation != null && (query == null || query.isCatchup())) {
1903 final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1904 if (displayableId != null && displayableId.equals(id)) {
1905 mXmppConnectionService.markRead(conversation);
1906 } else {
1907 Log.w(
1908 Config.LOGTAG,
1909 account.getJid().asBareJid()
1910 + ": received dismissing display marker that did not match our last"
1911 + " id in that conversation");
1912 }
1913 }
1914 }
1915
1916 private void processMessageReceipts(
1917 final Account account,
1918 final im.conversations.android.xmpp.model.stanza.Message packet,
1919 final String remoteMsgId,
1920 final MessageArchiveService.Query query) {
1921 final var request = packet.hasExtension(Request.class);
1922 if (query == null) {
1923 if (request) {
1924 final var receipt =
1925 mXmppConnectionService
1926 .getMessageGenerator()
1927 .received(packet.getFrom(), remoteMsgId, packet.getType());
1928 mXmppConnectionService.sendMessagePacket(account, receipt);
1929 }
1930 } else if (query.isCatchup()) {
1931 if (request) {
1932 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1933 }
1934 }
1935 }
1936
1937 private void activateGracePeriod(Account account) {
1938 long duration =
1939 mXmppConnectionService.getLongPreference(
1940 "grace_period_length", R.integer.grace_period)
1941 * 1000;
1942 Log.d(
1943 Config.LOGTAG,
1944 account.getJid().asBareJid()
1945 + ": activating grace period till "
1946 + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1947 account.activateGracePeriod(duration);
1948 }
1949
1950 private class Invite {
1951 final Jid jid;
1952 final String password;
1953 final boolean direct;
1954 final Jid inviter;
1955
1956 Invite(Jid jid, String password, boolean direct, Jid inviter) {
1957 this.jid = jid;
1958 this.password = password;
1959 this.direct = direct;
1960 this.inviter = inviter;
1961 }
1962
1963 public boolean execute(final Account account) {
1964 if (this.jid == null) {
1965 return false;
1966 }
1967 final Contact contact = this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1968 if (contact != null && contact.isBlocked()) {
1969 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite from "+contact.getJid()+" because contact is blocked");
1970 return false;
1971 }
1972 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1973 conversation.setAttribute("inviter", inviter.toString());
1974 if (conversation.getMucOptions().online()) {
1975 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " but muc is considered to be online");
1976 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1977 } else {
1978 conversation.getMucOptions().setPassword(password);
1979 mXmppConnectionService.databaseBackend.updateConversation(conversation);
1980 mXmppConnectionService.joinMuc(conversation, contact != null && contact.showInContactList());
1981 mXmppConnectionService.updateConversationUi();
1982 }
1983 return true;
1984 }
1985 }
1986
1987 private static int parseInt(String value) {
1988 try {
1989 return Integer.parseInt(value);
1990 } catch (NumberFormatException e) {
1991 return 0;
1992 }
1993 }
1994}