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