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