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