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