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