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 replacedMessage.setRemoteMsgId(remoteMsgId);
1102 if (replaceElement != null && !replaceElement.getName().equals("replace")) {
1103 mXmppConnectionService.getFileBackend().deleteFile(replacedMessage);
1104 mXmppConnectionService.evictPreview(message.getUuid());
1105 List<Element> thumbs = replacedMessage.getFileParams() != null ? replacedMessage.getFileParams().getThumbnails() : null;
1106 if (thumbs != null && !thumbs.isEmpty()) {
1107 for (Element thumb : thumbs) {
1108 Uri uri = Uri.parse(thumb.getAttribute("uri"));
1109 if (uri.getScheme().equals("cid")) {
1110 Cid cid = BobTransfer.cid(uri);
1111 if (cid == null) continue;
1112 DownloadableFile f = mXmppConnectionService.getFileForCid(cid);
1113 if (f != null) {
1114 mXmppConnectionService.evictPreview(f);
1115 f.delete();
1116 }
1117 }
1118 }
1119 }
1120 replacedMessage.clearPayloads();
1121 replacedMessage.setFileParams(null);
1122 replacedMessage.addPayload(replaceElement);
1123 } else {
1124 replacedMessage.clearPayloads();
1125 for (final var p : message.getPayloads()) {
1126 replacedMessage.addPayload(p);
1127 }
1128 }
1129 replacedMessage.setInReplyTo(message.getInReplyTo());
1130
1131 // we store the IDs of the replacing message. This is essentially unused
1132 // today (only the fact that there are _some_ edits causes the edit icon
1133 // to appear)
1134 replacedMessage.putEdited(
1135 message.getRemoteMsgId(), message.getServerMsgId());
1136
1137 // we used to call
1138 // `replacedMessage.setServerMsgId(message.getServerMsgId());` so during
1139 // catchup we could start from the edit; not the original message
1140 // however this caused problems for things like reactions that refer to
1141 // the serverMsgId
1142
1143 replacedMessage.setEncryption(message.getEncryption());
1144 if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
1145 replacedMessage.markUnread();
1146 }
1147 extractChatState(
1148 mXmppConnectionService.find(account, counterpart.asBareJid()),
1149 isTypeGroupChat,
1150 packet);
1151 mXmppConnectionService.updateMessage(replacedMessage, uuid);
1152 if (mXmppConnectionService.confirmMessages()
1153 && replacedMessage.getStatus() == Message.STATUS_RECEIVED
1154 && (replacedMessage.trusted()
1155 || replacedMessage
1156 .isPrivateMessage()) // TODO do we really want
1157 // to send receipts for all
1158 // PMs?
1159 && remoteMsgId != null
1160 && !selfAddressed
1161 && !isTypeGroupChat) {
1162 processMessageReceipts(account, packet, remoteMsgId, query);
1163 }
1164 if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
1165 conversation
1166 .getAccount()
1167 .getPgpDecryptionService()
1168 .discard(replacedMessage);
1169 conversation
1170 .getAccount()
1171 .getPgpDecryptionService()
1172 .decrypt(replacedMessage, false);
1173 }
1174 }
1175 mXmppConnectionService.getNotificationService().updateNotification();
1176 return;
1177 } else {
1178 Log.d(
1179 Config.LOGTAG,
1180 account.getJid().asBareJid()
1181 + ": received message correction but verification didn't"
1182 + " check out");
1183 }
1184 } else if (message.getBody() == null || message.getBody().equals("") || message.getBody().equals(" ")) {
1185 return;
1186 }
1187 if (replaceElement != null && !replaceElement.getName().equals("replace")) return;
1188 }
1189
1190 boolean checkForDuplicates =
1191 (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
1192 || message.isPrivateMessage()
1193 || message.getServerMsgId() != null
1194 || (query == null
1195 && mXmppConnectionService
1196 .getMessageArchiveService()
1197 .isCatchupInProgress(conversation));
1198 if (checkForDuplicates) {
1199 final Message duplicate = conversation.findDuplicateMessage(message);
1200 if (duplicate != null) {
1201 final boolean serverMsgIdUpdated;
1202 if (duplicate.getStatus() != Message.STATUS_RECEIVED
1203 && duplicate.getUuid().equals(message.getRemoteMsgId())
1204 && duplicate.getServerMsgId() == null
1205 && message.getServerMsgId() != null) {
1206 duplicate.setServerMsgId(message.getServerMsgId());
1207 if (mXmppConnectionService.databaseBackend.updateMessage(
1208 duplicate, false)) {
1209 serverMsgIdUpdated = true;
1210 } else {
1211 serverMsgIdUpdated = false;
1212 Log.e(Config.LOGTAG, "failed to update message");
1213 }
1214 } else {
1215 serverMsgIdUpdated = false;
1216 }
1217 Log.d(
1218 Config.LOGTAG,
1219 "skipping duplicate message with "
1220 + message.getCounterpart()
1221 + ". serverMsgIdUpdated="
1222 + serverMsgIdUpdated);
1223 return;
1224 }
1225 }
1226
1227 if (query != null
1228 && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
1229 conversation.prepend(query.getActualInThisQuery(), message);
1230 } else {
1231 conversation.add(message);
1232 }
1233 if (query != null) {
1234 query.incrementActualMessageCount();
1235 }
1236
1237 if (query == null || query.isCatchup()) { // either no mam or catchup
1238 if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
1239 mXmppConnectionService.markRead(conversation);
1240 if (query == null) {
1241 activateGracePeriod(account);
1242 }
1243 } else {
1244 message.markUnread();
1245 notify = true;
1246 }
1247 }
1248
1249 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1250 notify =
1251 conversation
1252 .getAccount()
1253 .getPgpDecryptionService()
1254 .decrypt(message, notify);
1255 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
1256 || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1257 notify = false;
1258 }
1259
1260 if (query == null) {
1261 extractChatState(
1262 mXmppConnectionService.find(account, counterpart.asBareJid()),
1263 isTypeGroupChat,
1264 packet);
1265 mXmppConnectionService.updateConversationUi();
1266 }
1267
1268 if (mXmppConnectionService.confirmMessages()
1269 && message.getStatus() == Message.STATUS_RECEIVED
1270 && (message.trusted() || message.isPrivateMessage())
1271 && remoteMsgId != null
1272 && !selfAddressed
1273 && !isTypeGroupChat) {
1274 processMessageReceipts(account, packet, remoteMsgId, query);
1275 }
1276
1277 if (message.getFileParams() != null) {
1278 for (Cid cid : message.getFileParams().getCids()) {
1279 File f = mXmppConnectionService.getFileForCid(cid);
1280 if (f != null && f.canRead()) {
1281 message.setRelativeFilePath(f.getAbsolutePath());
1282 mXmppConnectionService.getFileBackend().updateFileParams(message, null, false);
1283 break;
1284 }
1285 }
1286 }
1287
1288 mXmppConnectionService.databaseBackend.createMessage(message);
1289 final HttpConnectionManager manager =
1290 this.mXmppConnectionService.getHttpConnectionManager();
1291 if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
1292 if (message.getOob() != null && "cid".equalsIgnoreCase(message.getOob().getScheme())) {
1293 try {
1294 BobTransfer transfer = new BobTransfer.ForMessage(message, mXmppConnectionService);
1295 message.setTransferable(transfer);
1296 transfer.start();
1297 } catch (URISyntaxException e) {
1298 Log.d(Config.LOGTAG, "BobTransfer failed to parse URI");
1299 }
1300 } else {
1301 manager.createNewDownloadConnection(message);
1302 }
1303 } else if (notify) {
1304 if (query != null && query.isCatchup()) {
1305 mXmppConnectionService.getNotificationService().pushFromBacklog(message);
1306 } else {
1307 mXmppConnectionService.getNotificationService().push(message);
1308 }
1309 }
1310 } else if (!packet.hasChild("body")) { // no body
1311 final Conversation conversation =
1312 mXmppConnectionService.find(account, from.asBareJid());
1313 if (axolotlEncrypted != null) {
1314 Jid origin;
1315 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1316 final Jid fallback =
1317 conversation.getMucOptions().getTrueCounterpart(counterpart);
1318 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
1319 if (origin == null) {
1320 Log.d(
1321 Config.LOGTAG,
1322 "omemo key transport message in anonymous conference received");
1323 return;
1324 }
1325 } else if (isTypeGroupChat) {
1326 return;
1327 } else {
1328 origin = from;
1329 }
1330 try {
1331 final XmppAxolotlMessage xmppAxolotlMessage =
1332 XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
1333 account.getAxolotlService()
1334 .processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
1335 Log.d(
1336 Config.LOGTAG,
1337 account.getJid().asBareJid()
1338 + ": omemo key transport message received from "
1339 + origin);
1340 } catch (Exception e) {
1341 Log.d(
1342 Config.LOGTAG,
1343 account.getJid().asBareJid()
1344 + ": invalid omemo key transport message received "
1345 + e.getMessage());
1346 return;
1347 }
1348 }
1349
1350 if (query == null
1351 && extractChatState(
1352 mXmppConnectionService.find(account, counterpart.asBareJid()),
1353 isTypeGroupChat,
1354 packet)) {
1355 mXmppConnectionService.updateConversationUi();
1356 }
1357
1358 if (isTypeGroupChat) {
1359 if (packet.hasChild("subject")
1360 && !packet.hasChild("thread")) { // We already know it has no body per above
1361 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1362 conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
1363 final LocalizedContent subject = packet.getSubject();
1364 if (subject != null
1365 && conversation.getMucOptions().setSubject(subject.content)) {
1366 mXmppConnectionService.updateConversation(conversation);
1367 }
1368 mXmppConnectionService.updateConversationUi();
1369 return;
1370 }
1371 }
1372 }
1373 if (conversation != null
1374 && mucUserElement != null
1375 && Jid.Invalid.hasValidFrom(packet)
1376 && from.isBareJid()) {
1377 for (Element child : mucUserElement.getChildren()) {
1378 if ("status".equals(child.getName())) {
1379 try {
1380 int code = Integer.parseInt(child.getAttribute("code"));
1381 if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
1382 mXmppConnectionService.fetchConferenceConfiguration(conversation);
1383 break;
1384 }
1385 } catch (Exception e) {
1386 // ignored
1387 }
1388 } else if ("item".equals(child.getName())) {
1389 final var user = AbstractParser.parseItem(conversation, child);
1390 Log.d(
1391 Config.LOGTAG,
1392 account.getJid()
1393 + ": changing affiliation for "
1394 + user.getRealJid()
1395 + " to "
1396 + user.getAffiliation()
1397 + " in "
1398 + conversation.getJid().asBareJid());
1399 if (!user.realJidMatchesAccount()) {
1400 final var mucOptions = conversation.getMucOptions();
1401 final boolean isNew = mucOptions.updateUser(user);
1402 final var avatarService = mXmppConnectionService.getAvatarService();
1403 if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
1404 avatarService.clear(mucOptions);
1405 }
1406 avatarService.clear(user);
1407 mXmppConnectionService.updateMucRosterUi();
1408 mXmppConnectionService.updateConversationUi();
1409 Contact contact = user.getContact();
1410 if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
1411 Jid jid = user.getRealJid();
1412 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
1413 if (cryptoTargets.remove(user.getRealJid())) {
1414 Log.d(
1415 Config.LOGTAG,
1416 account.getJid().asBareJid()
1417 + ": removed "
1418 + jid
1419 + " from crypto targets of "
1420 + conversation.getName());
1421 conversation.setAcceptedCryptoTargets(cryptoTargets);
1422 mXmppConnectionService.updateConversation(conversation);
1423 }
1424 } else if (isNew
1425 && user.getRealJid() != null
1426 && conversation.getMucOptions().isPrivateAndNonAnonymous()
1427 && (contact == null || !contact.mutualPresenceSubscription())
1428 && account.getAxolotlService()
1429 .hasEmptyDeviceList(user.getRealJid())) {
1430 account.getAxolotlService().fetchDeviceIds(user.getRealJid());
1431 }
1432 }
1433 }
1434 }
1435 }
1436 if (!isTypeGroupChat) {
1437 for (Element child : packet.getChildren()) {
1438 if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
1439 && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
1440 final String action = child.getName();
1441 final String sessionId = child.getAttribute("id");
1442 if (sessionId == null) {
1443 break;
1444 }
1445 if (query == null && offlineMessagesRetrieved) {
1446 if (serverMsgId == null) {
1447 serverMsgId = extractStanzaId(account, packet);
1448 }
1449 mXmppConnectionService
1450 .getJingleConnectionManager()
1451 .deliverMessage(
1452 account,
1453 packet.getTo(),
1454 packet.getFrom(),
1455 child,
1456 remoteMsgId,
1457 serverMsgId,
1458 timestamp);
1459 final Contact contact = account.getRoster().getContact(from);
1460 // this is the same condition that is found in JingleRtpConnection for
1461 // the 'ringing' response. Responding with delivery receipts predates
1462 // the 'ringing' spec'd
1463 final boolean sendReceipts =
1464 contact.showInContactList()
1465 || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
1466 if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
1467 processMessageReceipts(account, packet, remoteMsgId, null);
1468 }
1469 } else if ((query != null && query.isCatchup())
1470 || !offlineMessagesRetrieved) {
1471 if ("propose".equals(action)) {
1472 final Element description = child.findChild("description");
1473 final String namespace =
1474 description == null ? null : description.getNamespace();
1475 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1476 final Conversation c =
1477 mXmppConnectionService.findOrCreateConversation(
1478 account, counterpart.asBareJid(), false, false);
1479 final Message preExistingMessage =
1480 c.findRtpSession(sessionId, status);
1481 if (preExistingMessage != null) {
1482 preExistingMessage.setServerMsgId(serverMsgId);
1483 mXmppConnectionService.updateMessage(preExistingMessage);
1484 break;
1485 }
1486 final Message message =
1487 new Message(
1488 c, status, Message.TYPE_RTP_SESSION, sessionId);
1489 message.setServerMsgId(serverMsgId);
1490 message.setTime(timestamp);
1491 message.setBody(new RtpSessionStatus(false, 0).toString());
1492 message.markUnread();
1493 c.add(message);
1494 mXmppConnectionService.getNotificationService().possiblyMissedCall(c.getUuid() + sessionId, message);
1495 if (query != null) query.incrementActualMessageCount();
1496 mXmppConnectionService.databaseBackend.createMessage(message);
1497 }
1498 } else if ("proceed".equals(action)) {
1499 // status needs to be flipped to find the original propose
1500 final Conversation c =
1501 mXmppConnectionService.findOrCreateConversation(
1502 account, counterpart.asBareJid(), false, false);
1503 final int s =
1504 packet.fromAccount(account)
1505 ? Message.STATUS_RECEIVED
1506 : Message.STATUS_SEND;
1507 final Message message = c.findRtpSession(sessionId, s);
1508 if (message != null) {
1509 message.setBody(new RtpSessionStatus(true, 0).toString());
1510 if (serverMsgId != null) {
1511 message.setServerMsgId(serverMsgId);
1512 }
1513 message.setTime(timestamp);
1514 message.markRead();
1515 mXmppConnectionService.getNotificationService().possiblyMissedCall(c.getUuid() + sessionId, message);
1516 if (query != null) query.incrementActualMessageCount();
1517 mXmppConnectionService.updateMessage(message, true);
1518 } else {
1519 Log.d(
1520 Config.LOGTAG,
1521 "unable to find original rtp session message for"
1522 + " received propose");
1523 }
1524
1525 } else if ("finish".equals(action)) {
1526 Log.d(
1527 Config.LOGTAG,
1528 "received JMI 'finish' during MAM catch-up. Can be used to"
1529 + " update success/failure and duration");
1530 }
1531 } else {
1532 // MAM reloads (non catchups
1533 if ("propose".equals(action)) {
1534 final Element description = child.findChild("description");
1535 final String namespace =
1536 description == null ? null : description.getNamespace();
1537 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1538 final Conversation c =
1539 mXmppConnectionService.findOrCreateConversation(
1540 account, counterpart.asBareJid(), false, false);
1541 final Message preExistingMessage =
1542 c.findRtpSession(sessionId, status);
1543 if (preExistingMessage != null) {
1544 preExistingMessage.setServerMsgId(serverMsgId);
1545 mXmppConnectionService.updateMessage(preExistingMessage);
1546 break;
1547 }
1548 final Message message =
1549 new Message(
1550 c, status, Message.TYPE_RTP_SESSION, sessionId);
1551 message.setServerMsgId(serverMsgId);
1552 message.setTime(timestamp);
1553 message.setBody(new RtpSessionStatus(true, 0).toString());
1554 if (query.getPagingOrder()
1555 == MessageArchiveService.PagingOrder.REVERSE) {
1556 c.prepend(query.getActualInThisQuery(), message);
1557 } else {
1558 c.add(message);
1559 }
1560 query.incrementActualMessageCount();
1561 mXmppConnectionService.databaseBackend.createMessage(message);
1562 }
1563 }
1564 }
1565 break;
1566 }
1567 }
1568 }
1569
1570 final var received =
1571 packet.getExtension(
1572 im.conversations.android.xmpp.model.receipts.Received.class);
1573 if (received != null) {
1574 processReceived(received, packet, query, from);
1575 }
1576 final var displayed = packet.getExtension(Displayed.class);
1577 if (displayed != null) {
1578 processDisplayed(
1579 displayed,
1580 packet,
1581 selfAddressed,
1582 counterpart,
1583 query,
1584 isTypeGroupChat,
1585 conversation,
1586 mucUserElement,
1587 from);
1588 }
1589
1590 // end no body
1591 }
1592
1593 if (reactions != null) {
1594 processReactions(
1595 reactions,
1596 mXmppConnectionService.find(account, from.asBareJid()),
1597 isTypeGroupChat,
1598 occupant,
1599 counterpart,
1600 mucTrueCounterPart,
1601 status,
1602 packet);
1603 }
1604
1605 final var event = original.getExtension(Event.class);
1606 if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1607 final var action = event.getAction();
1608 final var node = action == null ? null : action.getNode();
1609 if (node == null) {
1610 Log.d(
1611 Config.LOGTAG,
1612 account.getJid().asBareJid()
1613 + ": no node found in PubSub event from "
1614 + original.getFrom());
1615 } else if (action instanceof Items items) {
1616 parseEvent(items, original.getFrom(), account);
1617 } else if (action instanceof Purge purge) {
1618 parsePurgeEvent(purge, original.getFrom(), account);
1619 } else if (action instanceof Delete delete) {
1620 parseDeleteEvent(delete, from, account);
1621 }
1622 }
1623
1624 final String nick = packet.findChildContent("nick", Namespace.NICK);
1625 if (nick != null && Jid.Invalid.hasValidFrom(original)) {
1626 if (mXmppConnectionService.isMuc(account, from)) {
1627 return;
1628 }
1629 final Contact contact = account.getRoster().getContact(from);
1630 if (contact.setPresenceName(nick)) {
1631 mXmppConnectionService.syncRoster(account);
1632 mXmppConnectionService.getAvatarService().clear(contact);
1633 }
1634 }
1635 }
1636
1637 private void processReceived(
1638 final im.conversations.android.xmpp.model.receipts.Received received,
1639 final im.conversations.android.xmpp.model.stanza.Message packet,
1640 final MessageArchiveService.Query query,
1641 final Jid from) {
1642 final var id = received.getId();
1643 if (packet.fromAccount(account)) {
1644 if (query != null && id != null && packet.getTo() != null) {
1645 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1646 }
1647 } else if (id != null) {
1648 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1649 final String sessionId =
1650 id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1651 mXmppConnectionService
1652 .getJingleConnectionManager()
1653 .updateProposedSessionDiscovered(
1654 account,
1655 from,
1656 sessionId,
1657 JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1658 } else {
1659 mXmppConnectionService.markMessage(
1660 account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1661 }
1662 }
1663 }
1664
1665 private void processDisplayed(
1666 final Displayed displayed,
1667 final im.conversations.android.xmpp.model.stanza.Message packet,
1668 final boolean selfAddressed,
1669 final Jid counterpart,
1670 final MessageArchiveService.Query query,
1671 final boolean isTypeGroupChat,
1672 Conversation conversation,
1673 Element mucUserElement,
1674 Jid from) {
1675 final var id = displayed.getId();
1676 // TODO we don’t even use 'sender' any more. Remove this!
1677 final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1678 if (packet.fromAccount(account) && !selfAddressed) {
1679 final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1680 final Message message =
1681 (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1682 if (message != null && (query == null || query.isCatchup())) {
1683 mXmppConnectionService.markReadUpTo(c, message);
1684 }
1685 if (query == null) {
1686 activateGracePeriod(account);
1687 }
1688 } else if (isTypeGroupChat) {
1689 final Message message;
1690 if (conversation != null && id != null) {
1691 if (sender != null) {
1692 message = conversation.findMessageWithRemoteId(id, sender);
1693 } else {
1694 message = conversation.findMessageWithServerMsgId(id);
1695 }
1696 } else {
1697 message = null;
1698 }
1699 if (message != null) {
1700 // TODO use occupantId to extract true counterpart from presence
1701 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1702 // TODO try to externalize mucTrueCounterpart
1703 final Jid trueJid =
1704 getTrueCounterpart(
1705 (query != null && query.safeToExtractTrueCounterpart())
1706 ? mucUserElement
1707 : null,
1708 fallback);
1709 final boolean trueJidMatchesAccount =
1710 account.getJid()
1711 .asBareJid()
1712 .equals(trueJid == null ? null : trueJid.asBareJid());
1713 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1714 if (!message.isRead()
1715 && (query == null || query.isCatchup())) { // checking if message is
1716 // unread fixes race conditions
1717 // with reflections
1718 mXmppConnectionService.markReadUpTo(conversation, message);
1719 }
1720 } else if (!counterpart.isBareJid() && trueJid != null) {
1721 final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1722 if (message.addReadByMarker(readByMarker)) {
1723 final var mucOptions = conversation.getMucOptions();
1724 final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1725 final var readyBy = message.getReadyByTrue();
1726 final var mStatus = message.getStatus();
1727 if (mucOptions.isPrivateAndNonAnonymous()
1728 && (mStatus == Message.STATUS_SEND_RECEIVED
1729 || mStatus == Message.STATUS_SEND)
1730 && readyBy.containsAll(everyone)) {
1731 message.setStatus(Message.STATUS_SEND_DISPLAYED);
1732 }
1733 mXmppConnectionService.updateMessage(message, false);
1734 }
1735 }
1736 }
1737 } else {
1738 final Message displayedMessage =
1739 mXmppConnectionService.markMessage(
1740 account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1741 Message message = displayedMessage == null ? null : displayedMessage.prev();
1742 while (message != null
1743 && message.getStatus() == Message.STATUS_SEND_RECEIVED
1744 && message.getTimeSent() < displayedMessage.getTimeSent()) {
1745 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1746 message = message.prev();
1747 }
1748 if (displayedMessage != null && selfAddressed) {
1749 dismissNotification(account, counterpart, query, id);
1750 }
1751 }
1752 }
1753
1754 private void processReactions(
1755 final Reactions reactions,
1756 final Conversation conversation,
1757 final boolean isTypeGroupChat,
1758 final OccupantId occupant,
1759 final Jid counterpart,
1760 final Jid mucTrueCounterPart,
1761 final int status,
1762 final im.conversations.android.xmpp.model.stanza.Message packet) {
1763 final String reactingTo = reactions.getId();
1764 if (conversation != null && reactingTo != null) {
1765 if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1766 final var mucOptions = conversation.getMucOptions();
1767 final var occupantId = occupant == null ? null : occupant.getId();
1768 if (occupantId != null) {
1769 final boolean isReceived = !mucOptions.isSelf(occupantId);
1770 final Message message;
1771 final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1772 if (inMemoryMessage != null) {
1773 message = inMemoryMessage;
1774 } else {
1775 message =
1776 mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1777 conversation, reactingTo);
1778 }
1779 if (message != null) {
1780 final var newReactions = new HashSet<>(reactions.getReactions());
1781 newReactions.removeAll(message.getReactions().stream().filter(r -> occupantId.equals(r.occupantId)).map(r -> r.reaction).collect(Collectors.toList()));
1782 final var combinedReactions =
1783 Reaction.withOccupantId(
1784 message.getReactions(),
1785 reactions.getReactions(),
1786 isReceived,
1787 counterpart,
1788 mucTrueCounterPart,
1789 occupantId,
1790 message.getRemoteMsgId());
1791 message.setReactions(combinedReactions);
1792 mXmppConnectionService.updateMessage(message, false);
1793 if (isReceived) mXmppConnectionService.getNotificationService().push(message, counterpart, occupantId, newReactions);
1794 } else {
1795 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1796 }
1797 } else {
1798 Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1799 }
1800 } else {
1801 final Message message;
1802 final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1803 if (inMemoryMessage != null) {
1804 message = inMemoryMessage;
1805 } else {
1806 message =
1807 mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1808 conversation, reactingTo);
1809 }
1810 if (message == null) {
1811 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1812 return;
1813 }
1814 final boolean isReceived;
1815 final Jid reactionFrom;
1816 if (conversation.getMode() == Conversational.MODE_MULTI) {
1817 Log.d(Config.LOGTAG, "received reaction as MUC PM. triggering validation");
1818 final var mucOptions = conversation.getMucOptions();
1819 final var occupantId = occupant == null ? null : occupant.getId();
1820 if (occupantId == null) {
1821 Log.d(
1822 Config.LOGTAG,
1823 "received reaction via PM channel w/o occupant ids. ignoring");
1824 return;
1825 }
1826 isReceived = !mucOptions.isSelf(occupantId);
1827 if (isReceived) {
1828 reactionFrom = counterpart;
1829 } else {
1830 if (!occupantId.equals(message.getOccupantId())) {
1831 Log.d(
1832 Config.LOGTAG,
1833 "reaction received via MUC PM did not pass validation");
1834 return;
1835 }
1836 reactionFrom = account.getJid().asBareJid();
1837 }
1838 } else {
1839 if (packet.fromAccount(account)) {
1840 isReceived = false;
1841 reactionFrom = account.getJid().asBareJid();
1842 } else {
1843 isReceived = true;
1844 reactionFrom = counterpart;
1845 }
1846 }
1847 final var newReactions = new HashSet<>(reactions.getReactions());
1848 newReactions.removeAll(message.getReactions().stream().filter(r -> reactionFrom.equals(r.from)).map(r -> r.reaction).collect(Collectors.toList()));
1849 final var combinedReactions =
1850 Reaction.withFrom(
1851 message.getReactions(),
1852 reactions.getReactions(),
1853 isReceived,
1854 reactionFrom,
1855 message.getRemoteMsgId());
1856 message.setReactions(combinedReactions);
1857 mXmppConnectionService.updateMessage(message, false);
1858 if (status < Message.STATUS_SEND) mXmppConnectionService.getNotificationService().push(message, counterpart, null, newReactions);
1859 }
1860 }
1861 }
1862
1863 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1864 getForwardedMessagePacket(
1865 final im.conversations.android.xmpp.model.stanza.Message original,
1866 Class<? extends Extension> clazz) {
1867 final var extension = original.getExtension(clazz);
1868 final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1869 if (forwarded == null) {
1870 return null;
1871 }
1872 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1873 final var forwardedMessage = forwarded.getMessage();
1874 if (forwardedMessage == null) {
1875 return null;
1876 }
1877 return new Pair<>(forwardedMessage, timestamp);
1878 }
1879
1880 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1881 getForwardedMessagePacket(
1882 final im.conversations.android.xmpp.model.stanza.Message original,
1883 final String name,
1884 final String namespace) {
1885 final Element wrapper = original.findChild(name, namespace);
1886 final var forwardedElement =
1887 wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1888 if (forwardedElement instanceof Forwarded forwarded) {
1889 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1890 final var forwardedMessage = forwarded.getMessage();
1891 if (forwardedMessage == null) {
1892 return null;
1893 }
1894 return new Pair<>(forwardedMessage, timestamp);
1895 }
1896 return null;
1897 }
1898
1899 private void dismissNotification(
1900 Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1901 final Conversation conversation =
1902 mXmppConnectionService.find(account, counterpart.asBareJid());
1903 if (conversation != null && (query == null || query.isCatchup())) {
1904 final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1905 if (displayableId != null && displayableId.equals(id)) {
1906 mXmppConnectionService.markRead(conversation);
1907 } else {
1908 Log.w(
1909 Config.LOGTAG,
1910 account.getJid().asBareJid()
1911 + ": received dismissing display marker that did not match our last"
1912 + " id in that conversation");
1913 }
1914 }
1915 }
1916
1917 private void processMessageReceipts(
1918 final Account account,
1919 final im.conversations.android.xmpp.model.stanza.Message packet,
1920 final String remoteMsgId,
1921 final MessageArchiveService.Query query) {
1922 final var request = packet.hasExtension(Request.class);
1923 if (query == null) {
1924 if (request) {
1925 final var receipt =
1926 mXmppConnectionService
1927 .getMessageGenerator()
1928 .received(packet.getFrom(), remoteMsgId, packet.getType());
1929 mXmppConnectionService.sendMessagePacket(account, receipt);
1930 }
1931 } else if (query.isCatchup()) {
1932 if (request) {
1933 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1934 }
1935 }
1936 }
1937
1938 private void activateGracePeriod(Account account) {
1939 long duration =
1940 mXmppConnectionService.getLongPreference(
1941 "grace_period_length", R.integer.grace_period)
1942 * 1000;
1943 Log.d(
1944 Config.LOGTAG,
1945 account.getJid().asBareJid()
1946 + ": activating grace period till "
1947 + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1948 account.activateGracePeriod(duration);
1949 }
1950
1951 private class Invite {
1952 final Jid jid;
1953 final String password;
1954 final boolean direct;
1955 final Jid inviter;
1956
1957 Invite(Jid jid, String password, boolean direct, Jid inviter) {
1958 this.jid = jid;
1959 this.password = password;
1960 this.direct = direct;
1961 this.inviter = inviter;
1962 }
1963
1964 public boolean execute(final Account account) {
1965 if (this.jid == null) {
1966 return false;
1967 }
1968 final Contact contact = this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1969 if (contact != null && contact.isBlocked()) {
1970 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite from "+contact.getJid()+" because contact is blocked");
1971 return false;
1972 }
1973 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1974 conversation.setAttribute("inviter", inviter.toString());
1975 if (conversation.getMucOptions().online()) {
1976 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " but muc is considered to be online");
1977 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1978 } else {
1979 conversation.getMucOptions().setPassword(password);
1980 mXmppConnectionService.databaseBackend.updateConversation(conversation);
1981 mXmppConnectionService.joinMuc(conversation, contact != null && contact.showInContactList());
1982 mXmppConnectionService.updateConversationUi();
1983 }
1984 return true;
1985 }
1986 }
1987
1988 private static int parseInt(String value) {
1989 try {
1990 return Integer.parseInt(value);
1991 } catch (NumberFormatException e) {
1992 return 0;
1993 }
1994 }
1995}