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 {
606 mucTrueCounterPart = null;
607 occupant = null;
608 }
609 boolean isMucStatusMessage =
610 Jid.Invalid.hasValidFrom(packet)
611 && from.isBareJid()
612 && mucUserElement != null
613 && mucUserElement.hasChild("status");
614 boolean selfAddressed;
615 if (packet.fromAccount(account)) {
616 status = Message.STATUS_SEND;
617 selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
618 if (selfAddressed) {
619 counterpart = from;
620 } else {
621 counterpart = to;
622 }
623 } else {
624 status = Message.STATUS_RECEIVED;
625 counterpart = from;
626 selfAddressed = false;
627 }
628
629 final Invite invite = extractInvite(packet);
630 if (invite != null) {
631 if (invite.jid.asBareJid().equals(account.getJid().asBareJid())) {
632 Log.d(
633 Config.LOGTAG,
634 account.getJid().asBareJid()
635 + ": ignore invite to "
636 + invite.jid
637 + " because it matches account");
638 } else if (isTypeGroupChat) {
639 Log.d(
640 Config.LOGTAG,
641 account.getJid().asBareJid()
642 + ": ignoring invite to "
643 + invite.jid
644 + " because it was received as group chat");
645 } else if (invite.direct
646 && (mucUserElement != null
647 || invite.inviter == null
648 || mXmppConnectionService.isMuc(account, invite.inviter))) {
649 Log.d(
650 Config.LOGTAG,
651 account.getJid().asBareJid()
652 + ": ignoring direct invite to "
653 + invite.jid
654 + " because it was received in MUC");
655 } else {
656 invite.execute(account);
657 return;
658 }
659 }
660
661 if ((body != null
662 || pgpEncrypted != null
663 || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload"))
664 || oobUrl != null)
665 && !isMucStatusMessage) {
666 final boolean conversationIsProbablyMuc =
667 isTypeGroupChat
668 || mucUserElement != null
669 || account.getXmppConnection()
670 .getMucServersWithholdAccount()
671 .contains(counterpart.getDomain().toString());
672 final Conversation conversation =
673 mXmppConnectionService.findOrCreateConversation(
674 account,
675 counterpart.asBareJid(),
676 conversationIsProbablyMuc,
677 false,
678 query,
679 false);
680 final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
681
682 if (serverMsgId == null) {
683 serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
684 }
685
686 if (selfAddressed) {
687 // don’t store serverMsgId on reflections for edits
688 final var reflectedServerMsgId =
689 Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
690 if (mXmppConnectionService.markMessage(
691 conversation,
692 remoteMsgId,
693 Message.STATUS_SEND_RECEIVED,
694 reflectedServerMsgId)) {
695 return;
696 }
697 status = Message.STATUS_RECEIVED;
698 if (remoteMsgId != null
699 && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
700 return;
701 }
702 }
703
704 if (isTypeGroupChat) {
705 if (conversation.getMucOptions().isSelf(counterpart)) {
706 status = Message.STATUS_SEND_RECEIVED;
707 isCarbon = true; // not really carbon but received from another resource
708 // don’t store serverMsgId on reflections for edits
709 final var reflectedServerMsgId =
710 Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
711 if (mXmppConnectionService.markMessage(
712 conversation, remoteMsgId, status, reflectedServerMsgId, body)) {
713 return;
714 } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
715 if (body != null) {
716 Message message = conversation.findSentMessageWithBody(body.content);
717 if (message != null) {
718 mXmppConnectionService.markMessage(message, status);
719 return;
720 }
721 }
722 }
723 } else {
724 status = Message.STATUS_RECEIVED;
725 }
726 }
727 final Message message;
728 if (pgpEncrypted != null && Config.supportOpenPgp()) {
729 message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
730 } else if (axolotlEncrypted != null && Config.supportOmemo()) {
731 Jid origin;
732 Set<Jid> fallbacksBySourceId = Collections.emptySet();
733 if (conversationMultiMode) {
734 final Jid fallback =
735 conversation.getMucOptions().getTrueCounterpart(counterpart);
736 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
737 if (origin == null) {
738 try {
739 fallbacksBySourceId =
740 account.getAxolotlService()
741 .findCounterpartsBySourceId(
742 XmppAxolotlMessage.parseSourceId(
743 axolotlEncrypted));
744 } catch (IllegalArgumentException e) {
745 // ignoring
746 }
747 }
748 if (origin == null && fallbacksBySourceId.isEmpty()) {
749 Log.d(
750 Config.LOGTAG,
751 "axolotl message in anonymous conference received and no possible"
752 + " fallbacks");
753 return;
754 }
755 } else {
756 fallbacksBySourceId = Collections.emptySet();
757 origin = from;
758 }
759
760 final boolean liveMessage =
761 query == null && !isTypeGroupChat && mucUserElement == null;
762 final boolean checkedForDuplicates =
763 liveMessage
764 || (serverMsgId != null
765 && remoteMsgId != null
766 && !conversation.possibleDuplicate(
767 serverMsgId, remoteMsgId));
768
769 if (origin != null) {
770 message =
771 parseAxolotlChat(
772 axolotlEncrypted,
773 origin,
774 conversation,
775 status,
776 checkedForDuplicates,
777 query != null);
778 } else {
779 Message trial = null;
780 for (Jid fallback : fallbacksBySourceId) {
781 trial =
782 parseAxolotlChat(
783 axolotlEncrypted,
784 fallback,
785 conversation,
786 status,
787 checkedForDuplicates && fallbacksBySourceId.size() == 1,
788 query != null);
789 if (trial != null) {
790 Log.d(
791 Config.LOGTAG,
792 account.getJid().asBareJid()
793 + ": decoded muc message using fallback");
794 origin = fallback;
795 break;
796 }
797 }
798 message = trial;
799 }
800 if (message == null) {
801 if (query == null
802 && extractChatState(
803 mXmppConnectionService.find(account, counterpart.asBareJid()),
804 isTypeGroupChat,
805 packet)) {
806 mXmppConnectionService.updateConversationUi();
807 }
808 if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
809 Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
810 if (previouslySent != null
811 && previouslySent.getServerMsgId() == null
812 && serverMsgId != null) {
813 previouslySent.setServerMsgId(serverMsgId);
814 mXmppConnectionService.databaseBackend.updateMessage(
815 previouslySent, false);
816 Log.d(
817 Config.LOGTAG,
818 account.getJid().asBareJid()
819 + ": encountered previously sent OMEMO message without"
820 + " serverId. updating...");
821 }
822 }
823 return;
824 }
825 if (conversationMultiMode) {
826 message.setTrueCounterpart(origin);
827 }
828 } else if (body == null && oobUrl != null) {
829 message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
830 message.setOob(true);
831 if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
832 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
833 }
834 } else {
835 message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
836 if (body.count > 1) {
837 message.setBodyLanguage(body.language);
838 }
839 }
840
841 message.setCounterpart(counterpart);
842 message.setRemoteMsgId(remoteMsgId);
843 message.setServerMsgId(serverMsgId);
844 message.setCarbon(isCarbon);
845 message.setTime(timestamp);
846 if (body != null && body.content != null && body.content.equals(oobUrl)) {
847 message.setOob(true);
848 if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
849 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
850 }
851 }
852 message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
853 if (conversationMultiMode) {
854 final var mucOptions = conversation.getMucOptions();
855 if (occupant != null) {
856 message.setOccupantId(occupant.getId());
857 }
858 message.setMucUser(mucOptions.findUserByFullJid(counterpart));
859 final Jid fallback = mucOptions.getTrueCounterpart(counterpart);
860 Jid trueCounterpart;
861 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
862 trueCounterpart = message.getTrueCounterpart();
863 } else if (query != null && query.safeToExtractTrueCounterpart()) {
864 trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
865 } else {
866 trueCounterpart = fallback;
867 }
868 if (trueCounterpart != null && isTypeGroupChat) {
869 if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
870 status =
871 isTypeGroupChat
872 ? Message.STATUS_SEND_RECEIVED
873 : Message.STATUS_SEND;
874 } else {
875 status = Message.STATUS_RECEIVED;
876 message.setCarbon(false);
877 }
878 }
879 message.setStatus(status);
880 message.setTrueCounterpart(trueCounterpart);
881 if (!isTypeGroupChat) {
882 message.setType(Message.TYPE_PRIVATE);
883 }
884 } else {
885 updateLastseen(account, from);
886 }
887
888 if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
889 final Message replacedMessage =
890 conversation.findMessageWithRemoteIdAndCounterpart(
891 replacementId,
892 counterpart,
893 message.getStatus() == Message.STATUS_RECEIVED,
894 message.isCarbon());
895 if (replacedMessage != null) {
896 final boolean fingerprintsMatch =
897 replacedMessage.getFingerprint() == null
898 || replacedMessage
899 .getFingerprint()
900 .equals(message.getFingerprint());
901 final boolean trueCountersMatch =
902 replacedMessage.getTrueCounterpart() != null
903 && message.getTrueCounterpart() != null
904 && replacedMessage
905 .getTrueCounterpart()
906 .asBareJid()
907 .equals(message.getTrueCounterpart().asBareJid());
908 final boolean occupantIdMatch =
909 replacedMessage.getOccupantId() != null
910 && replacedMessage
911 .getOccupantId()
912 .equals(message.getOccupantId());
913 final boolean mucUserMatches =
914 query == null
915 && replacedMessage.sameMucUser(
916 message); // can not be checked when using mam
917 final boolean duplicate = conversation.hasDuplicateMessage(message);
918 if (fingerprintsMatch
919 && (trueCountersMatch
920 || occupantIdMatch
921 || !conversationMultiMode
922 || mucUserMatches)
923 && !duplicate) {
924 synchronized (replacedMessage) {
925 final String uuid = replacedMessage.getUuid();
926 replacedMessage.setUuid(UUID.randomUUID().toString());
927 replacedMessage.setBody(message.getBody());
928 // we store the IDs of the replacing message. This is essentially unused
929 // today (only the fact that there are _some_ edits causes the edit icon
930 // to appear)
931 replacedMessage.putEdited(
932 message.getRemoteMsgId(), message.getServerMsgId());
933
934 // we used to call
935 // `replacedMessage.setServerMsgId(message.getServerMsgId());` so during
936 // catchup we could start from the edit; not the original message
937 // however this caused problems for things like reactions that refer to
938 // the serverMsgId
939
940 replacedMessage.setEncryption(message.getEncryption());
941 if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
942 replacedMessage.markUnread();
943 }
944 extractChatState(
945 mXmppConnectionService.find(account, counterpart.asBareJid()),
946 isTypeGroupChat,
947 packet);
948 mXmppConnectionService.updateMessage(replacedMessage, uuid);
949 if (mXmppConnectionService.confirmMessages()
950 && replacedMessage.getStatus() == Message.STATUS_RECEIVED
951 && (replacedMessage.trusted()
952 || replacedMessage
953 .isPrivateMessage()) // TODO do we really want
954 // to send receipts for all
955 // PMs?
956 && remoteMsgId != null
957 && !selfAddressed
958 && !isTypeGroupChat) {
959 processMessageReceipts(account, packet, remoteMsgId, query);
960 }
961 if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
962 conversation
963 .getAccount()
964 .getPgpDecryptionService()
965 .discard(replacedMessage);
966 conversation
967 .getAccount()
968 .getPgpDecryptionService()
969 .decrypt(replacedMessage, false);
970 }
971 }
972 mXmppConnectionService.getNotificationService().updateNotification();
973 return;
974 } else {
975 Log.d(
976 Config.LOGTAG,
977 account.getJid().asBareJid()
978 + ": received message correction but verification didn't"
979 + " check out");
980 }
981 }
982 }
983
984 long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
985 if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
986 Log.d(
987 Config.LOGTAG,
988 account.getJid().asBareJid()
989 + ": skipping message from "
990 + message.getCounterpart().toString()
991 + " because it was sent prior to our deletion date");
992 return;
993 }
994
995 boolean checkForDuplicates =
996 (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
997 || message.isPrivateMessage()
998 || message.getServerMsgId() != null
999 || (query == null
1000 && mXmppConnectionService
1001 .getMessageArchiveService()
1002 .isCatchupInProgress(conversation));
1003 if (checkForDuplicates) {
1004 final Message duplicate = conversation.findDuplicateMessage(message);
1005 if (duplicate != null) {
1006 final boolean serverMsgIdUpdated;
1007 if (duplicate.getStatus() != Message.STATUS_RECEIVED
1008 && duplicate.getUuid().equals(message.getRemoteMsgId())
1009 && duplicate.getServerMsgId() == null
1010 && message.getServerMsgId() != null) {
1011 duplicate.setServerMsgId(message.getServerMsgId());
1012 if (mXmppConnectionService.databaseBackend.updateMessage(
1013 duplicate, false)) {
1014 serverMsgIdUpdated = true;
1015 } else {
1016 serverMsgIdUpdated = false;
1017 Log.e(Config.LOGTAG, "failed to update message");
1018 }
1019 } else {
1020 serverMsgIdUpdated = false;
1021 }
1022 Log.d(
1023 Config.LOGTAG,
1024 "skipping duplicate message with "
1025 + message.getCounterpart()
1026 + ". serverMsgIdUpdated="
1027 + serverMsgIdUpdated);
1028 return;
1029 }
1030 }
1031
1032 if (query != null
1033 && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
1034 conversation.prepend(query.getActualInThisQuery(), message);
1035 } else {
1036 conversation.add(message);
1037 }
1038 if (query != null) {
1039 query.incrementActualMessageCount();
1040 }
1041
1042 if (query == null || query.isCatchup()) { // either no mam or catchup
1043 if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
1044 mXmppConnectionService.markRead(conversation);
1045 if (query == null) {
1046 activateGracePeriod(account);
1047 }
1048 } else {
1049 message.markUnread();
1050 notify = true;
1051 }
1052 }
1053
1054 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1055 notify =
1056 conversation
1057 .getAccount()
1058 .getPgpDecryptionService()
1059 .decrypt(message, notify);
1060 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
1061 || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1062 notify = false;
1063 }
1064
1065 if (query == null) {
1066 extractChatState(
1067 mXmppConnectionService.find(account, counterpart.asBareJid()),
1068 isTypeGroupChat,
1069 packet);
1070 mXmppConnectionService.updateConversationUi();
1071 }
1072
1073 if (mXmppConnectionService.confirmMessages()
1074 && message.getStatus() == Message.STATUS_RECEIVED
1075 && (message.trusted() || message.isPrivateMessage())
1076 && remoteMsgId != null
1077 && !selfAddressed
1078 && !isTypeGroupChat) {
1079 processMessageReceipts(account, packet, remoteMsgId, query);
1080 }
1081
1082 mXmppConnectionService.databaseBackend.createMessage(message);
1083 final HttpConnectionManager manager =
1084 this.mXmppConnectionService.getHttpConnectionManager();
1085 if (message.trusted()
1086 && message.treatAsDownloadable()
1087 && manager.getAutoAcceptFileSize() > 0) {
1088 manager.createNewDownloadConnection(message);
1089 } else if (notify) {
1090 if (query != null && query.isCatchup()) {
1091 mXmppConnectionService.getNotificationService().pushFromBacklog(message);
1092 } else {
1093 mXmppConnectionService.getNotificationService().push(message);
1094 }
1095 }
1096 } else if (!packet.hasChild("body")) { // no body
1097
1098 final Conversation conversation =
1099 mXmppConnectionService.find(account, from.asBareJid());
1100 if (axolotlEncrypted != null) {
1101 Jid origin;
1102 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1103 final Jid fallback =
1104 conversation.getMucOptions().getTrueCounterpart(counterpart);
1105 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
1106 if (origin == null) {
1107 Log.d(
1108 Config.LOGTAG,
1109 "omemo key transport message in anonymous conference received");
1110 return;
1111 }
1112 } else if (isTypeGroupChat) {
1113 return;
1114 } else {
1115 origin = from;
1116 }
1117 try {
1118 final XmppAxolotlMessage xmppAxolotlMessage =
1119 XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
1120 account.getAxolotlService()
1121 .processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
1122 Log.d(
1123 Config.LOGTAG,
1124 account.getJid().asBareJid()
1125 + ": omemo key transport message received from "
1126 + origin);
1127 } catch (Exception e) {
1128 Log.d(
1129 Config.LOGTAG,
1130 account.getJid().asBareJid()
1131 + ": invalid omemo key transport message received "
1132 + e.getMessage());
1133 return;
1134 }
1135 }
1136
1137 if (query == null
1138 && extractChatState(
1139 mXmppConnectionService.find(account, counterpart.asBareJid()),
1140 isTypeGroupChat,
1141 packet)) {
1142 mXmppConnectionService.updateConversationUi();
1143 }
1144
1145 if (isTypeGroupChat) {
1146 if (packet.hasChild("subject")
1147 && !packet.hasChild("thread")) { // We already know it has no body per above
1148 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1149 conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
1150 final LocalizedContent subject =
1151 packet.findInternationalizedChildContentInDefaultNamespace(
1152 "subject");
1153 if (subject != null
1154 && conversation.getMucOptions().setSubject(subject.content)) {
1155 mXmppConnectionService.updateConversation(conversation);
1156 }
1157 mXmppConnectionService.updateConversationUi();
1158 return;
1159 }
1160 }
1161 }
1162 if (conversation != null
1163 && mucUserElement != null
1164 && Jid.Invalid.hasValidFrom(packet)
1165 && from.isBareJid()) {
1166 for (Element child : mucUserElement.getChildren()) {
1167 if ("status".equals(child.getName())) {
1168 try {
1169 int code = Integer.parseInt(child.getAttribute("code"));
1170 if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
1171 mXmppConnectionService.fetchConferenceConfiguration(conversation);
1172 break;
1173 }
1174 } catch (Exception e) {
1175 // ignored
1176 }
1177 } else if ("item".equals(child.getName())) {
1178 final var user = AbstractParser.parseItem(conversation, child);
1179 Log.d(
1180 Config.LOGTAG,
1181 account.getJid()
1182 + ": changing affiliation for "
1183 + user.getRealJid()
1184 + " to "
1185 + user.getAffiliation()
1186 + " in "
1187 + conversation.getJid().asBareJid());
1188 if (!user.realJidMatchesAccount()) {
1189 final var mucOptions = conversation.getMucOptions();
1190 final boolean isNew = mucOptions.updateUser(user);
1191 final var avatarService = mXmppConnectionService.getAvatarService();
1192 if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
1193 avatarService.clear(mucOptions);
1194 }
1195 avatarService.clear(user);
1196 mXmppConnectionService.updateMucRosterUi();
1197 mXmppConnectionService.updateConversationUi();
1198 Contact contact = user.getContact();
1199 if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
1200 Jid jid = user.getRealJid();
1201 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
1202 if (cryptoTargets.remove(user.getRealJid())) {
1203 Log.d(
1204 Config.LOGTAG,
1205 account.getJid().asBareJid()
1206 + ": removed "
1207 + jid
1208 + " from crypto targets of "
1209 + conversation.getName());
1210 conversation.setAcceptedCryptoTargets(cryptoTargets);
1211 mXmppConnectionService.updateConversation(conversation);
1212 }
1213 } else if (isNew
1214 && user.getRealJid() != null
1215 && conversation.getMucOptions().isPrivateAndNonAnonymous()
1216 && (contact == null || !contact.mutualPresenceSubscription())
1217 && account.getAxolotlService()
1218 .hasEmptyDeviceList(user.getRealJid())) {
1219 account.getAxolotlService().fetchDeviceIds(user.getRealJid());
1220 }
1221 }
1222 }
1223 }
1224 }
1225 if (!isTypeGroupChat) {
1226 for (Element child : packet.getChildren()) {
1227 if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
1228 && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
1229 final String action = child.getName();
1230 final String sessionId = child.getAttribute("id");
1231 if (sessionId == null) {
1232 break;
1233 }
1234 if (query == null && offlineMessagesRetrieved) {
1235 if (serverMsgId == null) {
1236 serverMsgId = extractStanzaId(account, packet);
1237 }
1238 mXmppConnectionService
1239 .getJingleConnectionManager()
1240 .deliverMessage(
1241 account,
1242 packet.getTo(),
1243 packet.getFrom(),
1244 child,
1245 remoteMsgId,
1246 serverMsgId,
1247 timestamp);
1248 final Contact contact = account.getRoster().getContact(from);
1249 // this is the same condition that is found in JingleRtpConnection for
1250 // the 'ringing' response. Responding with delivery receipts predates
1251 // the 'ringing' spec'd
1252 final boolean sendReceipts =
1253 contact.showInContactList()
1254 || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
1255 if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
1256 processMessageReceipts(account, packet, remoteMsgId, null);
1257 }
1258 } else if ((query != null && query.isCatchup())
1259 || !offlineMessagesRetrieved) {
1260 if ("propose".equals(action)) {
1261 final Element description = child.findChild("description");
1262 final String namespace =
1263 description == null ? null : description.getNamespace();
1264 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1265 final Conversation c =
1266 mXmppConnectionService.findOrCreateConversation(
1267 account, counterpart.asBareJid(), false, false);
1268 final Message preExistingMessage =
1269 c.findRtpSession(sessionId, status);
1270 if (preExistingMessage != null) {
1271 preExistingMessage.setServerMsgId(serverMsgId);
1272 mXmppConnectionService.updateMessage(preExistingMessage);
1273 break;
1274 }
1275 final Message message =
1276 new Message(
1277 c, status, Message.TYPE_RTP_SESSION, sessionId);
1278 message.setServerMsgId(serverMsgId);
1279 message.setTime(timestamp);
1280 message.setBody(new RtpSessionStatus(false, 0).toString());
1281 c.add(message);
1282 mXmppConnectionService.databaseBackend.createMessage(message);
1283 }
1284 } else if ("proceed".equals(action)) {
1285 // status needs to be flipped to find the original propose
1286 final Conversation c =
1287 mXmppConnectionService.findOrCreateConversation(
1288 account, counterpart.asBareJid(), false, false);
1289 final int s =
1290 packet.fromAccount(account)
1291 ? Message.STATUS_RECEIVED
1292 : Message.STATUS_SEND;
1293 final Message message = c.findRtpSession(sessionId, s);
1294 if (message != null) {
1295 message.setBody(new RtpSessionStatus(true, 0).toString());
1296 if (serverMsgId != null) {
1297 message.setServerMsgId(serverMsgId);
1298 }
1299 message.setTime(timestamp);
1300 mXmppConnectionService.updateMessage(message, true);
1301 } else {
1302 Log.d(
1303 Config.LOGTAG,
1304 "unable to find original rtp session message for"
1305 + " received propose");
1306 }
1307
1308 } else if ("finish".equals(action)) {
1309 Log.d(
1310 Config.LOGTAG,
1311 "received JMI 'finish' during MAM catch-up. Can be used to"
1312 + " update success/failure and duration");
1313 }
1314 } else {
1315 // MAM reloads (non catchups
1316 if ("propose".equals(action)) {
1317 final Element description = child.findChild("description");
1318 final String namespace =
1319 description == null ? null : description.getNamespace();
1320 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1321 final Conversation c =
1322 mXmppConnectionService.findOrCreateConversation(
1323 account, counterpart.asBareJid(), false, false);
1324 final Message preExistingMessage =
1325 c.findRtpSession(sessionId, status);
1326 if (preExistingMessage != null) {
1327 preExistingMessage.setServerMsgId(serverMsgId);
1328 mXmppConnectionService.updateMessage(preExistingMessage);
1329 break;
1330 }
1331 final Message message =
1332 new Message(
1333 c, status, Message.TYPE_RTP_SESSION, sessionId);
1334 message.setServerMsgId(serverMsgId);
1335 message.setTime(timestamp);
1336 message.setBody(new RtpSessionStatus(true, 0).toString());
1337 if (query.getPagingOrder()
1338 == MessageArchiveService.PagingOrder.REVERSE) {
1339 c.prepend(query.getActualInThisQuery(), message);
1340 } else {
1341 c.add(message);
1342 }
1343 query.incrementActualMessageCount();
1344 mXmppConnectionService.databaseBackend.createMessage(message);
1345 }
1346 }
1347 }
1348 break;
1349 }
1350 }
1351 }
1352
1353 final var received =
1354 packet.getExtension(
1355 im.conversations.android.xmpp.model.receipts.Received.class);
1356 if (received != null) {
1357 processReceived(received, packet, query, from);
1358 }
1359 final var displayed = packet.getExtension(Displayed.class);
1360 if (displayed != null) {
1361 processDisplayed(
1362 displayed,
1363 packet,
1364 selfAddressed,
1365 counterpart,
1366 query,
1367 isTypeGroupChat,
1368 conversation,
1369 mucUserElement,
1370 from);
1371 }
1372 final Reactions reactions = packet.getExtension(Reactions.class);
1373 if (reactions != null) {
1374 processReactions(
1375 reactions,
1376 conversation,
1377 isTypeGroupChat,
1378 occupant,
1379 counterpart,
1380 mucTrueCounterPart,
1381 packet);
1382 }
1383
1384 // end no body
1385 }
1386
1387 final Element event =
1388 original.findChild("event", "http://jabber.org/protocol/pubsub#event");
1389 if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1390 if (event.hasChild("items")) {
1391 parseEvent(event, original.getFrom(), account);
1392 } else if (event.hasChild("delete")) {
1393 parseDeleteEvent(event, original.getFrom(), account);
1394 } else if (event.hasChild("purge")) {
1395 parsePurgeEvent(event, original.getFrom(), account);
1396 }
1397 }
1398
1399 final String nick = packet.findChildContent("nick", Namespace.NICK);
1400 if (nick != null && Jid.Invalid.hasValidFrom(original)) {
1401 if (mXmppConnectionService.isMuc(account, from)) {
1402 return;
1403 }
1404 final Contact contact = account.getRoster().getContact(from);
1405 if (contact.setPresenceName(nick)) {
1406 mXmppConnectionService.syncRoster(account);
1407 mXmppConnectionService.getAvatarService().clear(contact);
1408 }
1409 }
1410 }
1411
1412 private void processReceived(
1413 final im.conversations.android.xmpp.model.receipts.Received received,
1414 final im.conversations.android.xmpp.model.stanza.Message packet,
1415 final MessageArchiveService.Query query,
1416 final Jid from) {
1417 final var id = received.getId();
1418 if (packet.fromAccount(account)) {
1419 if (query != null && id != null && packet.getTo() != null) {
1420 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1421 }
1422 } else if (id != null) {
1423 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1424 final String sessionId =
1425 id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1426 mXmppConnectionService
1427 .getJingleConnectionManager()
1428 .updateProposedSessionDiscovered(
1429 account,
1430 from,
1431 sessionId,
1432 JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1433 } else {
1434 mXmppConnectionService.markMessage(
1435 account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1436 }
1437 }
1438 }
1439
1440 private void processDisplayed(
1441 final Displayed displayed,
1442 final im.conversations.android.xmpp.model.stanza.Message packet,
1443 final boolean selfAddressed,
1444 final Jid counterpart,
1445 final MessageArchiveService.Query query,
1446 final boolean isTypeGroupChat,
1447 Conversation conversation,
1448 Element mucUserElement,
1449 Jid from) {
1450 final var id = displayed.getId();
1451 // TODO we don’t even use 'sender' any more. Remove this!
1452 final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1453 if (packet.fromAccount(account) && !selfAddressed) {
1454 final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1455 final Message message =
1456 (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1457 if (message != null && (query == null || query.isCatchup())) {
1458 mXmppConnectionService.markReadUpTo(c, message);
1459 }
1460 if (query == null) {
1461 activateGracePeriod(account);
1462 }
1463 } else if (isTypeGroupChat) {
1464 final Message message;
1465 if (conversation != null && id != null) {
1466 if (sender != null) {
1467 message = conversation.findMessageWithRemoteId(id, sender);
1468 } else {
1469 message = conversation.findMessageWithServerMsgId(id);
1470 }
1471 } else {
1472 message = null;
1473 }
1474 if (message != null) {
1475 // TODO use occupantId to extract true counterpart from presence
1476 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1477 // TODO try to externalize mucTrueCounterpart
1478 final Jid trueJid =
1479 getTrueCounterpart(
1480 (query != null && query.safeToExtractTrueCounterpart())
1481 ? mucUserElement
1482 : null,
1483 fallback);
1484 final boolean trueJidMatchesAccount =
1485 account.getJid()
1486 .asBareJid()
1487 .equals(trueJid == null ? null : trueJid.asBareJid());
1488 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1489 if (!message.isRead()
1490 && (query == null || query.isCatchup())) { // checking if message is
1491 // unread fixes race conditions
1492 // with reflections
1493 mXmppConnectionService.markReadUpTo(conversation, message);
1494 }
1495 } else if (!counterpart.isBareJid() && trueJid != null) {
1496 final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1497 if (message.addReadByMarker(readByMarker)) {
1498 final var mucOptions = conversation.getMucOptions();
1499 final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1500 final var readyBy = message.getReadyByTrue();
1501 final var mStatus = message.getStatus();
1502 if (mucOptions.isPrivateAndNonAnonymous()
1503 && (mStatus == Message.STATUS_SEND_RECEIVED
1504 || mStatus == Message.STATUS_SEND)
1505 && readyBy.containsAll(everyone)) {
1506 message.setStatus(Message.STATUS_SEND_DISPLAYED);
1507 }
1508 mXmppConnectionService.updateMessage(message, false);
1509 }
1510 }
1511 }
1512 } else {
1513 final Message displayedMessage =
1514 mXmppConnectionService.markMessage(
1515 account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1516 Message message = displayedMessage == null ? null : displayedMessage.prev();
1517 while (message != null
1518 && message.getStatus() == Message.STATUS_SEND_RECEIVED
1519 && message.getTimeSent() < displayedMessage.getTimeSent()) {
1520 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1521 message = message.prev();
1522 }
1523 if (displayedMessage != null && selfAddressed) {
1524 dismissNotification(account, counterpart, query, id);
1525 }
1526 }
1527 }
1528
1529 private void processReactions(
1530 final Reactions reactions,
1531 final Conversation conversation,
1532 final boolean isTypeGroupChat,
1533 final OccupantId occupant,
1534 final Jid counterpart,
1535 final Jid mucTrueCounterPart,
1536 final im.conversations.android.xmpp.model.stanza.Message packet) {
1537 final String reactingTo = reactions.getId();
1538 if (conversation != null && reactingTo != null) {
1539 if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1540 final var mucOptions = conversation.getMucOptions();
1541 final var occupantId = occupant == null ? null : occupant.getId();
1542 if (occupantId != null) {
1543 final boolean isReceived = !mucOptions.isSelf(occupantId);
1544 final Message message;
1545 final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1546 if (inMemoryMessage != null) {
1547 message = inMemoryMessage;
1548 } else {
1549 message =
1550 mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1551 conversation, reactingTo);
1552 }
1553 if (message != null) {
1554 final var combinedReactions =
1555 Reaction.withOccupantId(
1556 message.getReactions(),
1557 reactions.getReactions(),
1558 isReceived,
1559 counterpart,
1560 mucTrueCounterPart,
1561 occupantId);
1562 message.setReactions(combinedReactions);
1563 mXmppConnectionService.updateMessage(message, false);
1564 } else {
1565 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1566 }
1567 } else {
1568 Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1569 }
1570 } else if (conversation.getMode() == Conversational.MODE_SINGLE) {
1571 final Message message;
1572 final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1573 if (inMemoryMessage != null) {
1574 message = inMemoryMessage;
1575 } else {
1576 message =
1577 mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1578 conversation, reactingTo);
1579 }
1580 final boolean isReceived;
1581 final Jid reactionFrom;
1582 if (packet.fromAccount(account)) {
1583 isReceived = false;
1584 reactionFrom = account.getJid().asBareJid();
1585 } else {
1586 isReceived = true;
1587 reactionFrom = counterpart;
1588 }
1589 packet.fromAccount(account);
1590 if (message != null) {
1591 final var combinedReactions =
1592 Reaction.withFrom(
1593 message.getReactions(),
1594 reactions.getReactions(),
1595 isReceived,
1596 reactionFrom);
1597 message.setReactions(combinedReactions);
1598 mXmppConnectionService.updateMessage(message, false);
1599 } else {
1600 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1601 }
1602 }
1603 }
1604 }
1605
1606 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1607 getForwardedMessagePacket(
1608 final im.conversations.android.xmpp.model.stanza.Message original,
1609 Class<? extends Extension> clazz) {
1610 final var extension = original.getExtension(clazz);
1611 final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1612 if (forwarded == null) {
1613 return null;
1614 }
1615 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1616 final var forwardedMessage = forwarded.getMessage();
1617 if (forwardedMessage == null) {
1618 return null;
1619 }
1620 return new Pair<>(forwardedMessage, timestamp);
1621 }
1622
1623 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1624 getForwardedMessagePacket(
1625 final im.conversations.android.xmpp.model.stanza.Message original,
1626 final String name,
1627 final String namespace) {
1628 final Element wrapper = original.findChild(name, namespace);
1629 final var forwardedElement =
1630 wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1631 if (forwardedElement instanceof Forwarded forwarded) {
1632 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1633 final var forwardedMessage = forwarded.getMessage();
1634 if (forwardedMessage == null) {
1635 return null;
1636 }
1637 return new Pair<>(forwardedMessage, timestamp);
1638 }
1639 return null;
1640 }
1641
1642 private void dismissNotification(
1643 Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1644 final Conversation conversation =
1645 mXmppConnectionService.find(account, counterpart.asBareJid());
1646 if (conversation != null && (query == null || query.isCatchup())) {
1647 final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1648 if (displayableId != null && displayableId.equals(id)) {
1649 mXmppConnectionService.markRead(conversation);
1650 } else {
1651 Log.w(
1652 Config.LOGTAG,
1653 account.getJid().asBareJid()
1654 + ": received dismissing display marker that did not match our last"
1655 + " id in that conversation");
1656 }
1657 }
1658 }
1659
1660 private void processMessageReceipts(
1661 final Account account,
1662 final im.conversations.android.xmpp.model.stanza.Message packet,
1663 final String remoteMsgId,
1664 MessageArchiveService.Query query) {
1665 final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
1666 final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
1667 if (query == null) {
1668 final ArrayList<String> receiptsNamespaces = new ArrayList<>();
1669 if (markable) {
1670 receiptsNamespaces.add("urn:xmpp:chat-markers:0");
1671 }
1672 if (request) {
1673 receiptsNamespaces.add("urn:xmpp:receipts");
1674 }
1675 if (receiptsNamespaces.size() > 0) {
1676 final var receipt =
1677 mXmppConnectionService
1678 .getMessageGenerator()
1679 .received(
1680 account,
1681 packet.getFrom(),
1682 remoteMsgId,
1683 receiptsNamespaces,
1684 packet.getType());
1685 mXmppConnectionService.sendMessagePacket(account, receipt);
1686 }
1687 } else if (query.isCatchup()) {
1688 if (request) {
1689 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1690 }
1691 }
1692 }
1693
1694 private void activateGracePeriod(Account account) {
1695 long duration =
1696 mXmppConnectionService.getLongPreference(
1697 "grace_period_length", R.integer.grace_period)
1698 * 1000;
1699 Log.d(
1700 Config.LOGTAG,
1701 account.getJid().asBareJid()
1702 + ": activating grace period till "
1703 + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1704 account.activateGracePeriod(duration);
1705 }
1706
1707 private class Invite {
1708 final Jid jid;
1709 final String password;
1710 final boolean direct;
1711 final Jid inviter;
1712
1713 Invite(Jid jid, String password, boolean direct, Jid inviter) {
1714 this.jid = jid;
1715 this.password = password;
1716 this.direct = direct;
1717 this.inviter = inviter;
1718 }
1719
1720 public boolean execute(final Account account) {
1721 if (this.jid == null) {
1722 return false;
1723 }
1724 final Contact contact =
1725 this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1726 if (contact != null && contact.isBlocked()) {
1727 Log.d(
1728 Config.LOGTAG,
1729 account.getJid().asBareJid()
1730 + ": ignore invite from "
1731 + contact.getJid()
1732 + " because contact is blocked");
1733 return false;
1734 }
1735 final AppSettings appSettings = new AppSettings(mXmppConnectionService);
1736 if ((contact != null && contact.showInContactList())
1737 || appSettings.isAcceptInvitesFromStrangers()) {
1738 final Conversation conversation =
1739 mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1740 if (conversation.getMucOptions().online()) {
1741 Log.d(
1742 Config.LOGTAG,
1743 account.getJid().asBareJid()
1744 + ": received invite to "
1745 + jid
1746 + " but muc is considered to be online");
1747 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1748 } else {
1749 conversation.getMucOptions().setPassword(password);
1750 mXmppConnectionService.databaseBackend.updateConversation(conversation);
1751 mXmppConnectionService.joinMuc(
1752 conversation, contact != null && contact.showInContactList());
1753 mXmppConnectionService.updateConversationUi();
1754 }
1755 return true;
1756 } else {
1757 Log.d(
1758 Config.LOGTAG,
1759 account.getJid().asBareJid()
1760 + ": ignoring invite from "
1761 + this.inviter
1762 + " because we are not accepting invites from strangers. direct="
1763 + direct);
1764 return false;
1765 }
1766 }
1767 }
1768}