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