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