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 = packet.getSubject();
1161 if (subject != null
1162 && conversation.getMucOptions().setSubject(subject.content)) {
1163 mXmppConnectionService.updateConversation(conversation);
1164 }
1165 mXmppConnectionService.updateConversationUi();
1166 return;
1167 }
1168 }
1169 }
1170 if (conversation != null
1171 && mucUserElement != null
1172 && Jid.Invalid.hasValidFrom(packet)
1173 && from.isBareJid()) {
1174 for (Element child : mucUserElement.getChildren()) {
1175 if ("status".equals(child.getName())) {
1176 try {
1177 int code = Integer.parseInt(child.getAttribute("code"));
1178 if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
1179 mXmppConnectionService.fetchConferenceConfiguration(conversation);
1180 break;
1181 }
1182 } catch (Exception e) {
1183 // ignored
1184 }
1185 } else if ("item".equals(child.getName())) {
1186 final var user = AbstractParser.parseItem(conversation, child);
1187 Log.d(
1188 Config.LOGTAG,
1189 account.getJid()
1190 + ": changing affiliation for "
1191 + user.getRealJid()
1192 + " to "
1193 + user.getAffiliation()
1194 + " in "
1195 + conversation.getJid().asBareJid());
1196 if (!user.realJidMatchesAccount()) {
1197 final var mucOptions = conversation.getMucOptions();
1198 final boolean isNew = mucOptions.updateUser(user);
1199 final var avatarService = mXmppConnectionService.getAvatarService();
1200 if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
1201 avatarService.clear(mucOptions);
1202 }
1203 avatarService.clear(user);
1204 mXmppConnectionService.updateMucRosterUi();
1205 mXmppConnectionService.updateConversationUi();
1206 Contact contact = user.getContact();
1207 if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
1208 Jid jid = user.getRealJid();
1209 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
1210 if (cryptoTargets.remove(user.getRealJid())) {
1211 Log.d(
1212 Config.LOGTAG,
1213 account.getJid().asBareJid()
1214 + ": removed "
1215 + jid
1216 + " from crypto targets of "
1217 + conversation.getName());
1218 conversation.setAcceptedCryptoTargets(cryptoTargets);
1219 mXmppConnectionService.updateConversation(conversation);
1220 }
1221 } else if (isNew
1222 && user.getRealJid() != null
1223 && conversation.getMucOptions().isPrivateAndNonAnonymous()
1224 && (contact == null || !contact.mutualPresenceSubscription())
1225 && account.getAxolotlService()
1226 .hasEmptyDeviceList(user.getRealJid())) {
1227 account.getAxolotlService().fetchDeviceIds(user.getRealJid());
1228 }
1229 }
1230 }
1231 }
1232 }
1233 if (!isTypeGroupChat) {
1234 for (Element child : packet.getChildren()) {
1235 if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
1236 && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
1237 final String action = child.getName();
1238 final String sessionId = child.getAttribute("id");
1239 if (sessionId == null) {
1240 break;
1241 }
1242 if (query == null && offlineMessagesRetrieved) {
1243 if (serverMsgId == null) {
1244 serverMsgId = extractStanzaId(account, packet);
1245 }
1246 mXmppConnectionService
1247 .getJingleConnectionManager()
1248 .deliverMessage(
1249 account,
1250 packet.getTo(),
1251 packet.getFrom(),
1252 child,
1253 remoteMsgId,
1254 serverMsgId,
1255 timestamp);
1256 final Contact contact = account.getRoster().getContact(from);
1257 // this is the same condition that is found in JingleRtpConnection for
1258 // the 'ringing' response. Responding with delivery receipts predates
1259 // the 'ringing' spec'd
1260 final boolean sendReceipts =
1261 contact.showInContactList()
1262 || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
1263 if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
1264 processMessageReceipts(account, packet, remoteMsgId, null);
1265 }
1266 } else if ((query != null && query.isCatchup())
1267 || !offlineMessagesRetrieved) {
1268 if ("propose".equals(action)) {
1269 final Element description = child.findChild("description");
1270 final String namespace =
1271 description == null ? null : description.getNamespace();
1272 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1273 final Conversation c =
1274 mXmppConnectionService.findOrCreateConversation(
1275 account, counterpart.asBareJid(), false, false);
1276 final Message preExistingMessage =
1277 c.findRtpSession(sessionId, status);
1278 if (preExistingMessage != null) {
1279 preExistingMessage.setServerMsgId(serverMsgId);
1280 mXmppConnectionService.updateMessage(preExistingMessage);
1281 break;
1282 }
1283 final Message message =
1284 new Message(
1285 c, status, Message.TYPE_RTP_SESSION, sessionId);
1286 message.setServerMsgId(serverMsgId);
1287 message.setTime(timestamp);
1288 message.setBody(new RtpSessionStatus(false, 0).toString());
1289 c.add(message);
1290 mXmppConnectionService.databaseBackend.createMessage(message);
1291 }
1292 } else if ("proceed".equals(action)) {
1293 // status needs to be flipped to find the original propose
1294 final Conversation c =
1295 mXmppConnectionService.findOrCreateConversation(
1296 account, counterpart.asBareJid(), false, false);
1297 final int s =
1298 packet.fromAccount(account)
1299 ? Message.STATUS_RECEIVED
1300 : Message.STATUS_SEND;
1301 final Message message = c.findRtpSession(sessionId, s);
1302 if (message != null) {
1303 message.setBody(new RtpSessionStatus(true, 0).toString());
1304 if (serverMsgId != null) {
1305 message.setServerMsgId(serverMsgId);
1306 }
1307 message.setTime(timestamp);
1308 mXmppConnectionService.updateMessage(message, true);
1309 } else {
1310 Log.d(
1311 Config.LOGTAG,
1312 "unable to find original rtp session message for"
1313 + " received propose");
1314 }
1315
1316 } else if ("finish".equals(action)) {
1317 Log.d(
1318 Config.LOGTAG,
1319 "received JMI 'finish' during MAM catch-up. Can be used to"
1320 + " update success/failure and duration");
1321 }
1322 } else {
1323 // MAM reloads (non catchups
1324 if ("propose".equals(action)) {
1325 final Element description = child.findChild("description");
1326 final String namespace =
1327 description == null ? null : description.getNamespace();
1328 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1329 final Conversation c =
1330 mXmppConnectionService.findOrCreateConversation(
1331 account, counterpart.asBareJid(), false, false);
1332 final Message preExistingMessage =
1333 c.findRtpSession(sessionId, status);
1334 if (preExistingMessage != null) {
1335 preExistingMessage.setServerMsgId(serverMsgId);
1336 mXmppConnectionService.updateMessage(preExistingMessage);
1337 break;
1338 }
1339 final Message message =
1340 new Message(
1341 c, status, Message.TYPE_RTP_SESSION, sessionId);
1342 message.setServerMsgId(serverMsgId);
1343 message.setTime(timestamp);
1344 message.setBody(new RtpSessionStatus(true, 0).toString());
1345 if (query.getPagingOrder()
1346 == MessageArchiveService.PagingOrder.REVERSE) {
1347 c.prepend(query.getActualInThisQuery(), message);
1348 } else {
1349 c.add(message);
1350 }
1351 query.incrementActualMessageCount();
1352 mXmppConnectionService.databaseBackend.createMessage(message);
1353 }
1354 }
1355 }
1356 break;
1357 }
1358 }
1359 }
1360
1361 final var received =
1362 packet.getExtension(
1363 im.conversations.android.xmpp.model.receipts.Received.class);
1364 if (received != null) {
1365 processReceived(received, packet, query, from);
1366 }
1367 final var displayed = packet.getExtension(Displayed.class);
1368 if (displayed != null) {
1369 processDisplayed(
1370 displayed,
1371 packet,
1372 selfAddressed,
1373 counterpart,
1374 query,
1375 isTypeGroupChat,
1376 conversation,
1377 mucUserElement,
1378 from);
1379 }
1380 final Reactions reactions = packet.getExtension(Reactions.class);
1381 if (reactions != null) {
1382 processReactions(
1383 reactions,
1384 conversation,
1385 isTypeGroupChat,
1386 occupant,
1387 counterpart,
1388 mucTrueCounterPart,
1389 packet);
1390 }
1391
1392 // end no body
1393 }
1394
1395 final Element event =
1396 original.findChild("event", "http://jabber.org/protocol/pubsub#event");
1397 if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1398 if (event.hasChild("items")) {
1399 parseEvent(event, original.getFrom(), account);
1400 } else if (event.hasChild("delete")) {
1401 parseDeleteEvent(event, original.getFrom(), account);
1402 } else if (event.hasChild("purge")) {
1403 parsePurgeEvent(event, original.getFrom(), account);
1404 }
1405 }
1406
1407 final String nick = packet.findChildContent("nick", Namespace.NICK);
1408 if (nick != null && Jid.Invalid.hasValidFrom(original)) {
1409 if (mXmppConnectionService.isMuc(account, from)) {
1410 return;
1411 }
1412 final Contact contact = account.getRoster().getContact(from);
1413 if (contact.setPresenceName(nick)) {
1414 mXmppConnectionService.syncRoster(account);
1415 mXmppConnectionService.getAvatarService().clear(contact);
1416 }
1417 }
1418 }
1419
1420 private void processReceived(
1421 final im.conversations.android.xmpp.model.receipts.Received received,
1422 final im.conversations.android.xmpp.model.stanza.Message packet,
1423 final MessageArchiveService.Query query,
1424 final Jid from) {
1425 final var id = received.getId();
1426 if (packet.fromAccount(account)) {
1427 if (query != null && id != null && packet.getTo() != null) {
1428 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1429 }
1430 } else if (id != null) {
1431 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1432 final String sessionId =
1433 id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1434 mXmppConnectionService
1435 .getJingleConnectionManager()
1436 .updateProposedSessionDiscovered(
1437 account,
1438 from,
1439 sessionId,
1440 JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1441 } else {
1442 mXmppConnectionService.markMessage(
1443 account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1444 }
1445 }
1446 }
1447
1448 private void processDisplayed(
1449 final Displayed displayed,
1450 final im.conversations.android.xmpp.model.stanza.Message packet,
1451 final boolean selfAddressed,
1452 final Jid counterpart,
1453 final MessageArchiveService.Query query,
1454 final boolean isTypeGroupChat,
1455 Conversation conversation,
1456 Element mucUserElement,
1457 Jid from) {
1458 final var id = displayed.getId();
1459 // TODO we don’t even use 'sender' any more. Remove this!
1460 final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1461 if (packet.fromAccount(account) && !selfAddressed) {
1462 final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1463 final Message message =
1464 (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1465 if (message != null && (query == null || query.isCatchup())) {
1466 mXmppConnectionService.markReadUpTo(c, message);
1467 }
1468 if (query == null) {
1469 activateGracePeriod(account);
1470 }
1471 } else if (isTypeGroupChat) {
1472 final Message message;
1473 if (conversation != null && id != null) {
1474 if (sender != null) {
1475 message = conversation.findMessageWithRemoteId(id, sender);
1476 } else {
1477 message = conversation.findMessageWithServerMsgId(id);
1478 }
1479 } else {
1480 message = null;
1481 }
1482 if (message != null) {
1483 // TODO use occupantId to extract true counterpart from presence
1484 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1485 // TODO try to externalize mucTrueCounterpart
1486 final Jid trueJid =
1487 getTrueCounterpart(
1488 (query != null && query.safeToExtractTrueCounterpart())
1489 ? mucUserElement
1490 : null,
1491 fallback);
1492 final boolean trueJidMatchesAccount =
1493 account.getJid()
1494 .asBareJid()
1495 .equals(trueJid == null ? null : trueJid.asBareJid());
1496 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1497 if (!message.isRead()
1498 && (query == null || query.isCatchup())) { // checking if message is
1499 // unread fixes race conditions
1500 // with reflections
1501 mXmppConnectionService.markReadUpTo(conversation, message);
1502 }
1503 } else if (!counterpart.isBareJid() && trueJid != null) {
1504 final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1505 if (message.addReadByMarker(readByMarker)) {
1506 final var mucOptions = conversation.getMucOptions();
1507 final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1508 final var readyBy = message.getReadyByTrue();
1509 final var mStatus = message.getStatus();
1510 if (mucOptions.isPrivateAndNonAnonymous()
1511 && (mStatus == Message.STATUS_SEND_RECEIVED
1512 || mStatus == Message.STATUS_SEND)
1513 && readyBy.containsAll(everyone)) {
1514 message.setStatus(Message.STATUS_SEND_DISPLAYED);
1515 }
1516 mXmppConnectionService.updateMessage(message, false);
1517 }
1518 }
1519 }
1520 } else {
1521 final Message displayedMessage =
1522 mXmppConnectionService.markMessage(
1523 account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1524 Message message = displayedMessage == null ? null : displayedMessage.prev();
1525 while (message != null
1526 && message.getStatus() == Message.STATUS_SEND_RECEIVED
1527 && message.getTimeSent() < displayedMessage.getTimeSent()) {
1528 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1529 message = message.prev();
1530 }
1531 if (displayedMessage != null && selfAddressed) {
1532 dismissNotification(account, counterpart, query, id);
1533 }
1534 }
1535 }
1536
1537 private void processReactions(
1538 final Reactions reactions,
1539 final Conversation conversation,
1540 final boolean isTypeGroupChat,
1541 final OccupantId occupant,
1542 final Jid counterpart,
1543 final Jid mucTrueCounterPart,
1544 final im.conversations.android.xmpp.model.stanza.Message packet) {
1545 final String reactingTo = reactions.getId();
1546 if (conversation != null && reactingTo != null) {
1547 if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1548 final var mucOptions = conversation.getMucOptions();
1549 final var occupantId = occupant == null ? null : occupant.getId();
1550 if (occupantId != null) {
1551 final boolean isReceived = !mucOptions.isSelf(occupantId);
1552 final Message message;
1553 final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1554 if (inMemoryMessage != null) {
1555 message = inMemoryMessage;
1556 } else {
1557 message =
1558 mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1559 conversation, reactingTo);
1560 }
1561 if (message != null) {
1562 final var combinedReactions =
1563 Reaction.withOccupantId(
1564 message.getReactions(),
1565 reactions.getReactions(),
1566 isReceived,
1567 counterpart,
1568 mucTrueCounterPart,
1569 occupantId);
1570 message.setReactions(combinedReactions);
1571 mXmppConnectionService.updateMessage(message, false);
1572 } else {
1573 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1574 }
1575 } else {
1576 Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1577 }
1578 } else {
1579 final Message message;
1580 final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1581 if (inMemoryMessage != null) {
1582 message = inMemoryMessage;
1583 } else {
1584 message =
1585 mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1586 conversation, reactingTo);
1587 }
1588 if (message == null) {
1589 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1590 return;
1591 }
1592 final boolean isReceived;
1593 final Jid reactionFrom;
1594 if (conversation.getMode() == Conversational.MODE_MULTI) {
1595 Log.d(Config.LOGTAG, "received reaction as MUC PM. triggering validation");
1596 final var mucOptions = conversation.getMucOptions();
1597 final var occupantId = occupant == null ? null : occupant.getId();
1598 if (occupantId == null) {
1599 Log.d(
1600 Config.LOGTAG,
1601 "received reaction via PM channel w/o occupant ids. ignoring");
1602 return;
1603 }
1604 isReceived = !mucOptions.isSelf(occupantId);
1605 if (isReceived) {
1606 reactionFrom = counterpart;
1607 } else {
1608 if (!occupantId.equals(message.getOccupantId())) {
1609 Log.d(
1610 Config.LOGTAG,
1611 "reaction received via MUC PM did not pass validation");
1612 return;
1613 }
1614 reactionFrom = account.getJid().asBareJid();
1615 }
1616 } else {
1617 if (packet.fromAccount(account)) {
1618 isReceived = false;
1619 reactionFrom = account.getJid().asBareJid();
1620 } else {
1621 isReceived = true;
1622 reactionFrom = counterpart;
1623 }
1624 }
1625 final var combinedReactions =
1626 Reaction.withFrom(
1627 message.getReactions(),
1628 reactions.getReactions(),
1629 isReceived,
1630 reactionFrom);
1631 message.setReactions(combinedReactions);
1632 mXmppConnectionService.updateMessage(message, false);
1633 }
1634 }
1635 }
1636
1637 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1638 getForwardedMessagePacket(
1639 final im.conversations.android.xmpp.model.stanza.Message original,
1640 Class<? extends Extension> clazz) {
1641 final var extension = original.getExtension(clazz);
1642 final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1643 if (forwarded == null) {
1644 return null;
1645 }
1646 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1647 final var forwardedMessage = forwarded.getMessage();
1648 if (forwardedMessage == null) {
1649 return null;
1650 }
1651 return new Pair<>(forwardedMessage, timestamp);
1652 }
1653
1654 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1655 getForwardedMessagePacket(
1656 final im.conversations.android.xmpp.model.stanza.Message original,
1657 final String name,
1658 final String namespace) {
1659 final Element wrapper = original.findChild(name, namespace);
1660 final var forwardedElement =
1661 wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1662 if (forwardedElement instanceof Forwarded forwarded) {
1663 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1664 final var forwardedMessage = forwarded.getMessage();
1665 if (forwardedMessage == null) {
1666 return null;
1667 }
1668 return new Pair<>(forwardedMessage, timestamp);
1669 }
1670 return null;
1671 }
1672
1673 private void dismissNotification(
1674 Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1675 final Conversation conversation =
1676 mXmppConnectionService.find(account, counterpart.asBareJid());
1677 if (conversation != null && (query == null || query.isCatchup())) {
1678 final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1679 if (displayableId != null && displayableId.equals(id)) {
1680 mXmppConnectionService.markRead(conversation);
1681 } else {
1682 Log.w(
1683 Config.LOGTAG,
1684 account.getJid().asBareJid()
1685 + ": received dismissing display marker that did not match our last"
1686 + " id in that conversation");
1687 }
1688 }
1689 }
1690
1691 private void processMessageReceipts(
1692 final Account account,
1693 final im.conversations.android.xmpp.model.stanza.Message packet,
1694 final String remoteMsgId,
1695 MessageArchiveService.Query query) {
1696 final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
1697 final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
1698 if (query == null) {
1699 final ArrayList<String> receiptsNamespaces = new ArrayList<>();
1700 if (markable) {
1701 receiptsNamespaces.add("urn:xmpp:chat-markers:0");
1702 }
1703 if (request) {
1704 receiptsNamespaces.add("urn:xmpp:receipts");
1705 }
1706 if (receiptsNamespaces.size() > 0) {
1707 final var receipt =
1708 mXmppConnectionService
1709 .getMessageGenerator()
1710 .received(
1711 account,
1712 packet.getFrom(),
1713 remoteMsgId,
1714 receiptsNamespaces,
1715 packet.getType());
1716 mXmppConnectionService.sendMessagePacket(account, receipt);
1717 }
1718 } else if (query.isCatchup()) {
1719 if (request) {
1720 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1721 }
1722 }
1723 }
1724
1725 private void activateGracePeriod(Account account) {
1726 long duration =
1727 mXmppConnectionService.getLongPreference(
1728 "grace_period_length", R.integer.grace_period)
1729 * 1000;
1730 Log.d(
1731 Config.LOGTAG,
1732 account.getJid().asBareJid()
1733 + ": activating grace period till "
1734 + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1735 account.activateGracePeriod(duration);
1736 }
1737
1738 private class Invite {
1739 final Jid jid;
1740 final String password;
1741 final boolean direct;
1742 final Jid inviter;
1743
1744 Invite(Jid jid, String password, boolean direct, Jid inviter) {
1745 this.jid = jid;
1746 this.password = password;
1747 this.direct = direct;
1748 this.inviter = inviter;
1749 }
1750
1751 public boolean execute(final Account account) {
1752 if (this.jid == null) {
1753 return false;
1754 }
1755 final Contact contact =
1756 this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1757 if (contact != null && contact.isBlocked()) {
1758 Log.d(
1759 Config.LOGTAG,
1760 account.getJid().asBareJid()
1761 + ": ignore invite from "
1762 + contact.getJid()
1763 + " because contact is blocked");
1764 return false;
1765 }
1766 final AppSettings appSettings = new AppSettings(mXmppConnectionService);
1767 if ((contact != null && contact.showInContactList())
1768 || appSettings.isAcceptInvitesFromStrangers()) {
1769 final Conversation conversation =
1770 mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1771 if (conversation.getMucOptions().online()) {
1772 Log.d(
1773 Config.LOGTAG,
1774 account.getJid().asBareJid()
1775 + ": received invite to "
1776 + jid
1777 + " but muc is considered to be online");
1778 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1779 } else {
1780 conversation.getMucOptions().setPassword(password);
1781 mXmppConnectionService.databaseBackend.updateConversation(conversation);
1782 mXmppConnectionService.joinMuc(
1783 conversation, contact != null && contact.showInContactList());
1784 mXmppConnectionService.updateConversationUi();
1785 }
1786 return true;
1787 } else {
1788 Log.d(
1789 Config.LOGTAG,
1790 account.getJid().asBareJid()
1791 + ": ignoring invite from "
1792 + this.inviter
1793 + " because we are not accepting invites from strangers. direct="
1794 + direct);
1795 return false;
1796 }
1797 }
1798 }
1799}