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 ;
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 if (handleErrorMessage(account, original)) {
494 return;
495 }
496 final im.conversations.android.xmpp.model.stanza.Message packet;
497 Long timestamp = null;
498 boolean isCarbon = false;
499 String serverMsgId = null;
500 final Element fin =
501 original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
502 if (fin != null) {
503 mXmppConnectionService
504 .getMessageArchiveService()
505 .processFinLegacy(fin, original.getFrom());
506 return;
507 }
508 final Element result = MessageArchiveService.Version.findResult(original);
509 final String queryId = result == null ? null : result.getAttribute("queryid");
510 final MessageArchiveService.Query query =
511 queryId == null
512 ? null
513 : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
514 final boolean offlineMessagesRetrieved =
515 account.getXmppConnection().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 || account.getXmppConnection()
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 id = received.getId();
1450 if (packet.fromAccount(account)) {
1451 if (query != null && id != null && packet.getTo() != null) {
1452 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1453 }
1454 } else if (id != null) {
1455 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1456 final String sessionId =
1457 id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1458 mXmppConnectionService
1459 .getJingleConnectionManager()
1460 .updateProposedSessionDiscovered(
1461 account,
1462 from,
1463 sessionId,
1464 JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1465 } else {
1466 mXmppConnectionService.markMessage(
1467 account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1468 }
1469 }
1470 }
1471
1472 private void processDisplayed(
1473 final Displayed displayed,
1474 final im.conversations.android.xmpp.model.stanza.Message packet,
1475 final boolean selfAddressed,
1476 final Jid counterpart,
1477 final MessageArchiveService.Query query,
1478 final boolean isTypeGroupChat,
1479 Conversation conversation,
1480 Element mucUserElement,
1481 Jid from) {
1482 final var id = displayed.getId();
1483 // TODO we don’t even use 'sender' any more. Remove this!
1484 final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1485 if (packet.fromAccount(account) && !selfAddressed) {
1486 final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1487 final Message message =
1488 (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1489 if (message != null && (query == null || query.isCatchup())) {
1490 mXmppConnectionService.markReadUpTo(c, message);
1491 }
1492 if (query == null) {
1493 activateGracePeriod(account);
1494 }
1495 } else if (isTypeGroupChat) {
1496 final Message message;
1497 if (conversation != null && id != null) {
1498 if (sender != null) {
1499 message = conversation.findMessageWithRemoteId(id, sender);
1500 } else {
1501 message = conversation.findMessageWithServerMsgId(id);
1502 }
1503 } else {
1504 message = null;
1505 }
1506 if (message != null) {
1507 // TODO use occupantId to extract true counterpart from presence
1508 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1509 // TODO try to externalize mucTrueCounterpart
1510 final Jid trueJid =
1511 getTrueCounterpart(
1512 (query != null && query.safeToExtractTrueCounterpart())
1513 ? mucUserElement
1514 : null,
1515 fallback);
1516 final boolean trueJidMatchesAccount =
1517 account.getJid()
1518 .asBareJid()
1519 .equals(trueJid == null ? null : trueJid.asBareJid());
1520 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1521 if (!message.isRead()
1522 && (query == null || query.isCatchup())) { // checking if message is
1523 // unread fixes race conditions
1524 // with reflections
1525 mXmppConnectionService.markReadUpTo(conversation, message);
1526 }
1527 } else if (!counterpart.isBareJid() && trueJid != null) {
1528 final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1529 if (message.addReadByMarker(readByMarker)) {
1530 final var mucOptions = conversation.getMucOptions();
1531 final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1532 final var readyBy = message.getReadyByTrue();
1533 final var mStatus = message.getStatus();
1534 if (mucOptions.isPrivateAndNonAnonymous()
1535 && (mStatus == Message.STATUS_SEND_RECEIVED
1536 || mStatus == Message.STATUS_SEND)
1537 && readyBy.containsAll(everyone)) {
1538 message.setStatus(Message.STATUS_SEND_DISPLAYED);
1539 }
1540 mXmppConnectionService.updateMessage(message, false);
1541 }
1542 }
1543 }
1544 } else {
1545 final Message displayedMessage =
1546 mXmppConnectionService.markMessage(
1547 account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1548 Message message = displayedMessage == null ? null : displayedMessage.prev();
1549 while (message != null
1550 && message.getStatus() == Message.STATUS_SEND_RECEIVED
1551 && message.getTimeSent() < displayedMessage.getTimeSent()) {
1552 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1553 message = message.prev();
1554 }
1555 if (displayedMessage != null && selfAddressed) {
1556 dismissNotification(account, counterpart, query, id);
1557 }
1558 }
1559 }
1560
1561 private void processReactions(
1562 final Reactions reactions,
1563 final Conversation conversation,
1564 final boolean isTypeGroupChat,
1565 final OccupantId occupant,
1566 final Jid counterpart,
1567 final Jid mucTrueCounterPart,
1568 final im.conversations.android.xmpp.model.stanza.Message packet) {
1569 final String reactingTo = reactions.getId();
1570 if (conversation != null && reactingTo != null) {
1571 if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1572 final var mucOptions = conversation.getMucOptions();
1573 final var occupantId = occupant == null ? null : occupant.getId();
1574 if (occupantId != null) {
1575 final boolean isReceived = !mucOptions.isSelf(occupantId);
1576 final Message message;
1577 final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1578 if (inMemoryMessage != null) {
1579 message = inMemoryMessage;
1580 } else {
1581 message =
1582 mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1583 conversation, reactingTo);
1584 }
1585 if (message != null) {
1586 final var combinedReactions =
1587 Reaction.withOccupantId(
1588 message.getReactions(),
1589 reactions.getReactions(),
1590 isReceived,
1591 counterpart,
1592 mucTrueCounterPart,
1593 occupantId);
1594 message.setReactions(combinedReactions);
1595 mXmppConnectionService.updateMessage(message, false);
1596 } else {
1597 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1598 }
1599 } else {
1600 Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1601 }
1602 } else {
1603 final Message message;
1604 final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1605 if (inMemoryMessage != null) {
1606 message = inMemoryMessage;
1607 } else {
1608 message =
1609 mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1610 conversation, reactingTo);
1611 }
1612 if (message == null) {
1613 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1614 return;
1615 }
1616 final boolean isReceived;
1617 final Jid reactionFrom;
1618 if (conversation.getMode() == Conversational.MODE_MULTI) {
1619 Log.d(Config.LOGTAG, "received reaction as MUC PM. triggering validation");
1620 final var mucOptions = conversation.getMucOptions();
1621 final var occupantId = occupant == null ? null : occupant.getId();
1622 if (occupantId == null) {
1623 Log.d(
1624 Config.LOGTAG,
1625 "received reaction via PM channel w/o occupant ids. ignoring");
1626 return;
1627 }
1628 isReceived = !mucOptions.isSelf(occupantId);
1629 if (isReceived) {
1630 reactionFrom = counterpart;
1631 } else {
1632 if (!occupantId.equals(message.getOccupantId())) {
1633 Log.d(
1634 Config.LOGTAG,
1635 "reaction received via MUC PM did not pass validation");
1636 return;
1637 }
1638 reactionFrom = account.getJid().asBareJid();
1639 }
1640 } else {
1641 if (packet.fromAccount(account)) {
1642 isReceived = false;
1643 reactionFrom = account.getJid().asBareJid();
1644 } else {
1645 isReceived = true;
1646 reactionFrom = counterpart;
1647 }
1648 }
1649 final var combinedReactions =
1650 Reaction.withFrom(
1651 message.getReactions(),
1652 reactions.getReactions(),
1653 isReceived,
1654 reactionFrom);
1655 message.setReactions(combinedReactions);
1656 mXmppConnectionService.updateMessage(message, false);
1657 }
1658 }
1659 }
1660
1661 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1662 getForwardedMessagePacket(
1663 final im.conversations.android.xmpp.model.stanza.Message original,
1664 Class<? extends Extension> clazz) {
1665 final var extension = original.getExtension(clazz);
1666 final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1667 if (forwarded == null) {
1668 return null;
1669 }
1670 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1671 final var forwardedMessage = forwarded.getMessage();
1672 if (forwardedMessage == null) {
1673 return null;
1674 }
1675 return new Pair<>(forwardedMessage, timestamp);
1676 }
1677
1678 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1679 getForwardedMessagePacket(
1680 final im.conversations.android.xmpp.model.stanza.Message original,
1681 final String name,
1682 final String namespace) {
1683 final Element wrapper = original.findChild(name, namespace);
1684 final var forwardedElement =
1685 wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1686 if (forwardedElement instanceof Forwarded forwarded) {
1687 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1688 final var forwardedMessage = forwarded.getMessage();
1689 if (forwardedMessage == null) {
1690 return null;
1691 }
1692 return new Pair<>(forwardedMessage, timestamp);
1693 }
1694 return null;
1695 }
1696
1697 private void dismissNotification(
1698 Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1699 final Conversation conversation =
1700 mXmppConnectionService.find(account, counterpart.asBareJid());
1701 if (conversation != null && (query == null || query.isCatchup())) {
1702 final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1703 if (displayableId != null && displayableId.equals(id)) {
1704 mXmppConnectionService.markRead(conversation);
1705 } else {
1706 Log.w(
1707 Config.LOGTAG,
1708 account.getJid().asBareJid()
1709 + ": received dismissing display marker that did not match our last"
1710 + " id in that conversation");
1711 }
1712 }
1713 }
1714
1715 private void processMessageReceipts(
1716 final Account account,
1717 final im.conversations.android.xmpp.model.stanza.Message packet,
1718 final String remoteMsgId,
1719 final MessageArchiveService.Query query) {
1720 final var request = packet.hasExtension(Request.class);
1721 if (query == null) {
1722 if (request) {
1723 final var receipt =
1724 mXmppConnectionService
1725 .getMessageGenerator()
1726 .received(packet.getFrom(), remoteMsgId, packet.getType());
1727 mXmppConnectionService.sendMessagePacket(account, receipt);
1728 }
1729 } else if (query.isCatchup()) {
1730 if (request) {
1731 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1732 }
1733 }
1734 }
1735
1736 private void activateGracePeriod(Account account) {
1737 long duration =
1738 mXmppConnectionService.getLongPreference(
1739 "grace_period_length", R.integer.grace_period)
1740 * 1000;
1741 Log.d(
1742 Config.LOGTAG,
1743 account.getJid().asBareJid()
1744 + ": activating grace period till "
1745 + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1746 account.activateGracePeriod(duration);
1747 }
1748
1749 private class Invite {
1750 final Jid jid;
1751 final String password;
1752 final boolean direct;
1753 final Jid inviter;
1754
1755 Invite(Jid jid, String password, boolean direct, Jid inviter) {
1756 this.jid = jid;
1757 this.password = password;
1758 this.direct = direct;
1759 this.inviter = inviter;
1760 }
1761
1762 public boolean execute(final Account account) {
1763 if (this.jid == null) {
1764 return false;
1765 }
1766 final Contact contact =
1767 this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1768 if (contact != null && contact.isBlocked()) {
1769 Log.d(
1770 Config.LOGTAG,
1771 account.getJid().asBareJid()
1772 + ": ignore invite from "
1773 + contact.getJid()
1774 + " because contact is blocked");
1775 return false;
1776 }
1777 final AppSettings appSettings = new AppSettings(mXmppConnectionService);
1778 if ((contact != null && contact.showInContactList())
1779 || appSettings.isAcceptInvitesFromStrangers()) {
1780 final Conversation conversation =
1781 mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1782 if (conversation.getMucOptions().online()) {
1783 Log.d(
1784 Config.LOGTAG,
1785 account.getJid().asBareJid()
1786 + ": received invite to "
1787 + jid
1788 + " but muc is considered to be online");
1789 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1790 } else {
1791 conversation.getMucOptions().setPassword(password);
1792 mXmppConnectionService.databaseBackend.updateConversation(conversation);
1793 mXmppConnectionService.joinMuc(
1794 conversation, contact != null && contact.showInContactList());
1795 mXmppConnectionService.updateConversationUi();
1796 }
1797 return true;
1798 } else {
1799 Log.d(
1800 Config.LOGTAG,
1801 account.getJid().asBareJid()
1802 + ": ignoring invite from "
1803 + this.inviter
1804 + " because we are not accepting invites from strangers. direct="
1805 + direct);
1806 return false;
1807 }
1808 }
1809 }
1810}