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