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.oob.OutOfBandData;
48import im.conversations.android.xmpp.model.pubsub.event.Event;
49import im.conversations.android.xmpp.model.reactions.Reactions;
50import im.conversations.android.xmpp.model.receipts.Request;
51import im.conversations.android.xmpp.model.unique.StanzaId;
52import java.text.SimpleDateFormat;
53import java.util.Arrays;
54import java.util.Collections;
55import java.util.Date;
56import java.util.List;
57import java.util.Locale;
58import java.util.Map;
59import java.util.Set;
60import java.util.UUID;
61import java.util.function.Consumer;
62
63public class MessageParser extends AbstractParser
64 implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
65
66 private static final SimpleDateFormat TIME_FORMAT =
67 new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
68
69 private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
70 Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
71
72 public MessageParser(final XmppConnectionService service, final Account account) {
73 super(service, account);
74 }
75
76 private static String extractStanzaId(
77 final im.conversations.android.xmpp.model.stanza.Message packet,
78 final boolean isTypeGroupChat,
79 final Conversation conversation) {
80 final Jid by;
81 final boolean safeToExtract;
82 if (isTypeGroupChat) {
83 by = conversation.getJid().asBareJid();
84 safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
85 } else {
86 Account account = conversation.getAccount();
87 by = account.getJid().asBareJid();
88 safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
89 }
90 return safeToExtract ? StanzaId.get(packet, by) : null;
91 }
92
93 private static String extractStanzaId(
94 final Account account,
95 final im.conversations.android.xmpp.model.stanza.Message packet) {
96 final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
97 return safeToExtract ? StanzaId.get(packet, account.getJid().asBareJid()) : null;
98 }
99
100 private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
101 final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
102 Jid result =
103 item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
104 return result != null ? result : fallback;
105 }
106
107 private boolean extractChatState(
108 Conversation c,
109 final boolean isTypeGroupChat,
110 final im.conversations.android.xmpp.model.stanza.Message packet) {
111 ChatState state = ChatState.parse(packet);
112 if (state != null && c != null) {
113 final Account account = c.getAccount();
114 final Jid from = packet.getFrom();
115 if (from.asBareJid().equals(account.getJid().asBareJid())) {
116 c.setOutgoingChatState(state);
117 if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
118 if (c.getContact().isSelf()) {
119 return false;
120 }
121 mXmppConnectionService.markRead(c);
122 activateGracePeriod(account);
123 }
124 return false;
125 } else {
126 if (isTypeGroupChat) {
127 MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
128 if (user != null) {
129 return user.setChatState(state);
130 } else {
131 return false;
132 }
133 } else {
134 return c.setIncomingChatState(state);
135 }
136 }
137 }
138 return false;
139 }
140
141 private Message parseAxolotlChat(
142 final Encrypted axolotlMessage,
143 final Jid from,
144 final Conversation conversation,
145 final int status,
146 final boolean checkedForDuplicates,
147 final boolean postpone) {
148 final AxolotlService service = conversation.getAccount().getAxolotlService();
149 final XmppAxolotlMessage xmppAxolotlMessage;
150 try {
151 xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
152 } catch (final Exception e) {
153 Log.d(
154 Config.LOGTAG,
155 conversation.getAccount().getJid().asBareJid()
156 + ": invalid omemo message received "
157 + e.getMessage());
158 return null;
159 }
160 if (xmppAxolotlMessage.hasPayload()) {
161 final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
162 try {
163 plaintextMessage =
164 service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
165 } catch (BrokenSessionException e) {
166 if (checkedForDuplicates) {
167 if (service.trustedOrPreviouslyResponded(from.asBareJid())) {
168 service.reportBrokenSessionException(e, postpone);
169 return new Message(
170 conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
171 } else {
172 Log.d(
173 Config.LOGTAG,
174 "ignoring broken session exception because contact was not"
175 + " trusted");
176 return new Message(
177 conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
178 }
179 } else {
180 Log.d(
181 Config.LOGTAG,
182 "ignoring broken session exception because checkForDuplicates failed");
183 return null;
184 }
185 } catch (NotEncryptedForThisDeviceException e) {
186 return new Message(
187 conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
188 } catch (OutdatedSenderException e) {
189 return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
190 }
191 if (plaintextMessage != null) {
192 Message finishedMessage =
193 new Message(
194 conversation,
195 plaintextMessage.getPlaintext(),
196 Message.ENCRYPTION_AXOLOTL,
197 status);
198 finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
199 Log.d(
200 Config.LOGTAG,
201 AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())
202 + " Received Message with session fingerprint: "
203 + plaintextMessage.getFingerprint());
204 return finishedMessage;
205 }
206 } else {
207 Log.d(
208 Config.LOGTAG,
209 conversation.getAccount().getJid().asBareJid()
210 + ": received OMEMO key transport message");
211 service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
212 }
213 return null;
214 }
215
216 private Invite extractInvite(final Element message) {
217 final Element mucUser = message.findChild("x", Namespace.MUC_USER);
218 if (mucUser != null) {
219 final Element invite = mucUser.findChild("invite");
220 if (invite != null) {
221 final String password = mucUser.findChildContent("password");
222 final Jid from = Jid.Invalid.getNullForInvalid(invite.getAttributeAsJid("from"));
223 final Jid to = Jid.Invalid.getNullForInvalid(invite.getAttributeAsJid("to"));
224 if (to != null && from == null) {
225 Log.d(Config.LOGTAG, "do not parse outgoing mediated invite " + message);
226 return null;
227 }
228 final Jid room = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
229 if (room == null) {
230 return null;
231 }
232 return new Invite(room, password, false, from);
233 }
234 }
235 final Element conference = message.findChild("x", "jabber:x:conference");
236 if (conference != null) {
237 Jid from = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
238 Jid room = Jid.Invalid.getNullForInvalid(conference.getAttributeAsJid("jid"));
239 if (room == null) {
240 return null;
241 }
242 return new Invite(room, conference.getAttribute("password"), true, from);
243 }
244 return null;
245 }
246
247 private void parseEvent(final Event event, final Jid from, final Account account) {
248 final Element items = event.findChild("items");
249 final String node = items == null ? null : items.getAttribute("node");
250 if ("urn:xmpp:avatar:metadata".equals(node)) {
251 Avatar avatar = Avatar.parseMetadata(items);
252 if (avatar != null) {
253 avatar.owner = from.asBareJid();
254 if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
255 if (account.getJid().asBareJid().equals(from)) {
256 if (account.setAvatar(avatar.getFilename())) {
257 mXmppConnectionService.databaseBackend.updateAccount(account);
258 mXmppConnectionService.notifyAccountAvatarHasChanged(account);
259 }
260 mXmppConnectionService.getAvatarService().clear(account);
261 mXmppConnectionService.updateConversationUi();
262 mXmppConnectionService.updateAccountUi();
263 } else {
264 final Contact contact = account.getRoster().getContact(from);
265 if (contact.setAvatar(avatar)) {
266 mXmppConnectionService.syncRoster(account);
267 mXmppConnectionService.getAvatarService().clear(contact);
268 mXmppConnectionService.updateConversationUi();
269 mXmppConnectionService.updateRosterUi();
270 }
271 }
272 } else if (mXmppConnectionService.isDataSaverDisabled()) {
273 mXmppConnectionService.fetchAvatar(account, avatar);
274 }
275 }
276 } else if (Namespace.NICK.equals(node)) {
277 final Element i = items.findChild("item");
278 final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
279 if (nick != null) {
280 setNick(account, from, nick);
281 }
282 } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
283 Element item = items.findChild("item");
284 final Set<Integer> deviceIds = IqParser.deviceIds(item);
285 Log.d(
286 Config.LOGTAG,
287 AxolotlService.getLogprefix(account)
288 + "Received PEP device list "
289 + deviceIds
290 + " update from "
291 + from
292 + ", processing... ");
293 final AxolotlService axolotlService = account.getAxolotlService();
294 axolotlService.registerDevices(from, deviceIds);
295 } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
296 final var connection = account.getXmppConnection();
297 if (connection.getFeatures().bookmarksConversion()) {
298 if (connection.getFeatures().bookmarks2()) {
299 Log.w(
300 Config.LOGTAG,
301 account.getJid().asBareJid()
302 + ": received storage:bookmark notification even though we"
303 + " opted into bookmarks:1");
304 }
305 final Element i = items.findChild("item");
306 final Element storage =
307 i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
308 final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
309 mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
310 Log.d(
311 Config.LOGTAG,
312 account.getJid().asBareJid() + ": processing bookmark PEP event");
313 } else {
314 Log.d(
315 Config.LOGTAG,
316 account.getJid().asBareJid()
317 + ": ignoring bookmark PEP event because bookmark conversion was"
318 + " not detected");
319 }
320 } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
321 final Element item = items.findChild("item");
322 final Element retract = items.findChild("retract");
323 if (item != null) {
324 final Bookmark bookmark = Bookmark.parseFromItem(item, account);
325 if (bookmark != null) {
326 account.putBookmark(bookmark);
327 mXmppConnectionService.processModifiedBookmark(bookmark);
328 mXmppConnectionService.updateConversationUi();
329 }
330 }
331 if (retract != null) {
332 final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
333 if (id != null) {
334 account.removeBookmark(id);
335 Log.d(
336 Config.LOGTAG,
337 account.getJid().asBareJid() + ": deleted bookmark for " + id);
338 mXmppConnectionService.processDeletedBookmark(account, id);
339 mXmppConnectionService.updateConversationUi();
340 }
341 }
342 } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
343 && Namespace.MDS_DISPLAYED.equals(node)
344 && account.getJid().asBareJid().equals(from)) {
345 final Element item = items.findChild("item");
346 mXmppConnectionService.processMdsItem(account, item);
347 } else {
348 Log.d(
349 Config.LOGTAG,
350 account.getJid().asBareJid()
351 + " received pubsub notification for node="
352 + node);
353 }
354 }
355
356 private void parseDeleteEvent(final Element event, final Jid from, final Account account) {
357 final Element delete = event.findChild("delete");
358 final String node = delete == null ? null : delete.getAttribute("node");
359 if (Namespace.NICK.equals(node)) {
360 Log.d(Config.LOGTAG, "parsing nick delete event from " + from);
361 setNick(account, from, null);
362 } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
363 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
364 deleteAllBookmarks(account);
365 } else if (Namespace.AVATAR_METADATA.equals(node)
366 && account.getJid().asBareJid().equals(from)) {
367 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted avatar metadata node");
368 }
369 }
370
371 private void parsePurgeEvent(final Element event, final Jid from, final Account account) {
372 final Element purge = event.findChild("purge");
373 final String node = purge == null ? null : purge.getAttribute("node");
374 if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
375 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
376 deleteAllBookmarks(account);
377 }
378 }
379
380 private void deleteAllBookmarks(final Account account) {
381 final var previous = account.getBookmarkedJids();
382 account.setBookmarks(Collections.emptyMap());
383 mXmppConnectionService.processDeletedBookmarks(account, previous);
384 }
385
386 private void setNick(final Account account, final Jid user, final String nick) {
387 if (user.asBareJid().equals(account.getJid().asBareJid())) {
388 account.setDisplayName(nick);
389 if (QuickConversationsService.isQuicksy()) {
390 mXmppConnectionService.getAvatarService().clear(account);
391 }
392 mXmppConnectionService.checkMucRequiresRename();
393 } else {
394 Contact contact = account.getRoster().getContact(user);
395 if (contact.setPresenceName(nick)) {
396 mXmppConnectionService.syncRoster(account);
397 mXmppConnectionService.getAvatarService().clear(contact);
398 }
399 }
400 mXmppConnectionService.updateConversationUi();
401 mXmppConnectionService.updateAccountUi();
402 }
403
404 private boolean handleErrorMessage(
405 final Account account,
406 final im.conversations.android.xmpp.model.stanza.Message packet) {
407 if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) {
408 if (packet.fromServer(account)) {
409 final var forwarded =
410 getForwardedMessagePacket(packet, "received", Namespace.CARBONS);
411 if (forwarded != null) {
412 return handleErrorMessage(account, forwarded.first);
413 }
414 }
415 final Jid from = packet.getFrom();
416 final String id = packet.getId();
417 if (from != null && id != null) {
418 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
419 final String sessionId =
420 id.substring(
421 JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
422 mXmppConnectionService
423 .getJingleConnectionManager()
424 .updateProposedSessionDiscovered(
425 account,
426 from,
427 sessionId,
428 JingleConnectionManager.DeviceDiscoveryState.FAILED);
429 return true;
430 }
431 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
432 final String sessionId =
433 id.substring(
434 JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
435 final String message = extractErrorMessage(packet);
436 mXmppConnectionService
437 .getJingleConnectionManager()
438 .failProceed(account, from, sessionId, message);
439 return true;
440 }
441 mXmppConnectionService.markMessage(
442 account,
443 from.asBareJid(),
444 id,
445 Message.STATUS_SEND_FAILED,
446 extractErrorMessage(packet));
447 final Element error = packet.findChild("error");
448 final boolean pingWorthyError =
449 error != null
450 && (error.hasChild("not-acceptable")
451 || error.hasChild("remote-server-timeout")
452 || error.hasChild("remote-server-not-found"));
453 if (pingWorthyError) {
454 Conversation conversation = mXmppConnectionService.find(account, from);
455 if (conversation != null
456 && conversation.getMode() == Conversational.MODE_MULTI) {
457 if (conversation.getMucOptions().online()) {
458 Log.d(
459 Config.LOGTAG,
460 account.getJid().asBareJid()
461 + ": received ping worthy error for seemingly online"
462 + " muc at "
463 + from);
464 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
465 }
466 }
467 }
468 }
469 return true;
470 }
471 return false;
472 }
473
474 @Override
475 public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
476 if (handleErrorMessage(account, original)) {
477 return;
478 }
479 final im.conversations.android.xmpp.model.stanza.Message packet;
480 Long timestamp = null;
481 boolean isCarbon = false;
482 String serverMsgId = null;
483 final Element fin =
484 original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
485 if (fin != null) {
486 mXmppConnectionService
487 .getMessageArchiveService()
488 .processFinLegacy(fin, original.getFrom());
489 return;
490 }
491 final Element result = MessageArchiveService.Version.findResult(original);
492 final String queryId = result == null ? null : result.getAttribute("queryid");
493 final MessageArchiveService.Query query =
494 queryId == null
495 ? null
496 : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
497 final boolean offlineMessagesRetrieved =
498 account.getXmppConnection().isOfflineMessagesRetrieved();
499 if (query != null && query.validFrom(original.getFrom())) {
500 final var f = getForwardedMessagePacket(original, "result", query.version.namespace);
501 if (f == null) {
502 return;
503 }
504 timestamp = f.second;
505 packet = f.first;
506 serverMsgId = result.getAttribute("id");
507 query.incrementMessageCount();
508 if (handleErrorMessage(account, packet)) {
509 return;
510 }
511 } else if (query != null) {
512 Log.d(
513 Config.LOGTAG,
514 account.getJid().asBareJid()
515 + ": received mam result with invalid from ("
516 + original.getFrom()
517 + ") or queryId ("
518 + queryId
519 + ")");
520 return;
521 } else if (original.fromServer(account)
522 && original.getType()
523 != im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT) {
524 Pair<im.conversations.android.xmpp.model.stanza.Message, Long> f;
525 f = getForwardedMessagePacket(original, Received.class);
526 f = f == null ? getForwardedMessagePacket(original, Sent.class) : f;
527 packet = f != null ? f.first : original;
528 if (handleErrorMessage(account, packet)) {
529 return;
530 }
531 timestamp = f != null ? f.second : null;
532 isCarbon = f != null;
533 } else {
534 packet = original;
535 }
536
537 if (timestamp == null) {
538 timestamp =
539 AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
540 }
541 final LocalizedContent body = packet.getBody();
542 final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
543 final boolean isTypeGroupChat =
544 packet.getType()
545 == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
546 final var encrypted =
547 packet.getOnlyExtension(im.conversations.android.xmpp.model.pgp.Encrypted.class);
548 final String pgpEncrypted = encrypted == null ? null : encrypted.getContent();
549
550 final var oob = packet.getExtension(OutOfBandData.class);
551 final String oobUrl = oob != null ? oob.getURL() : null;
552 final var replace = packet.getExtension(Replace.class);
553 final var replacementId = replace == null ? null : replace.getId();
554 final var axolotlEncrypted = packet.getOnlyExtension(Encrypted.class);
555 int status;
556 final Jid counterpart;
557 final Jid to = packet.getTo();
558 final Jid from = packet.getFrom();
559 final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
560 final String remoteMsgId;
561 if (originId != null && originId.getAttribute("id") != null) {
562 remoteMsgId = originId.getAttribute("id");
563 } else {
564 remoteMsgId = packet.getId();
565 }
566 boolean notify = false;
567
568 if (from == null || !Jid.Invalid.isValid(from) || !Jid.Invalid.isValid(to)) {
569 Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
570 return;
571 }
572 if (query != null && !query.muc() && isTypeGroupChat) {
573 Log.e(
574 Config.LOGTAG,
575 account.getJid().asBareJid()
576 + ": received groupchat ("
577 + from
578 + ") message on regular MAM request. skipping");
579 return;
580 }
581 final Jid mucTrueCounterPart;
582 final OccupantId occupant;
583 if (isTypeGroupChat) {
584 final Conversation conversation =
585 mXmppConnectionService.find(account, from.asBareJid());
586 final Jid mucTrueCounterPartByPresence;
587 if (conversation != null) {
588 final var mucOptions = conversation.getMucOptions();
589 occupant =
590 mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null;
591 final var user =
592 occupant == null ? null : mucOptions.findUserByOccupantId(occupant.getId());
593 mucTrueCounterPartByPresence = user == null ? null : user.getRealJid();
594 } else {
595 occupant = null;
596 mucTrueCounterPartByPresence = null;
597 }
598 mucTrueCounterPart =
599 getTrueCounterpart(
600 (query != null && query.safeToExtractTrueCounterpart())
601 ? mucUserElement
602 : null,
603 mucTrueCounterPartByPresence);
604 } else if (mucUserElement != null) {
605 final Conversation conversation =
606 mXmppConnectionService.find(account, from.asBareJid());
607 if (conversation != null) {
608 final var mucOptions = conversation.getMucOptions();
609 occupant =
610 mucOptions.occupantId() ? packet.getOnlyExtension(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 var event = original.getExtension(Event.class);
1396 if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1397 if (event.hasChild("items")) {
1398 parseEvent(event, original.getFrom(), account);
1399 } else if (event.hasChild("delete")) {
1400 parseDeleteEvent(event, original.getFrom(), account);
1401 } else if (event.hasChild("purge")) {
1402 parsePurgeEvent(event, original.getFrom(), account);
1403 }
1404 }
1405
1406 final String nick = packet.findChildContent("nick", Namespace.NICK);
1407 if (nick != null && Jid.Invalid.hasValidFrom(original)) {
1408 if (mXmppConnectionService.isMuc(account, from)) {
1409 return;
1410 }
1411 final Contact contact = account.getRoster().getContact(from);
1412 if (contact.setPresenceName(nick)) {
1413 mXmppConnectionService.syncRoster(account);
1414 mXmppConnectionService.getAvatarService().clear(contact);
1415 }
1416 }
1417 }
1418
1419 private void processReceived(
1420 final im.conversations.android.xmpp.model.receipts.Received received,
1421 final im.conversations.android.xmpp.model.stanza.Message packet,
1422 final MessageArchiveService.Query query,
1423 final Jid from) {
1424 final var id = received.getId();
1425 if (packet.fromAccount(account)) {
1426 if (query != null && id != null && packet.getTo() != null) {
1427 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1428 }
1429 } else if (id != null) {
1430 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1431 final String sessionId =
1432 id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1433 mXmppConnectionService
1434 .getJingleConnectionManager()
1435 .updateProposedSessionDiscovered(
1436 account,
1437 from,
1438 sessionId,
1439 JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1440 } else {
1441 mXmppConnectionService.markMessage(
1442 account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1443 }
1444 }
1445 }
1446
1447 private void processDisplayed(
1448 final Displayed displayed,
1449 final im.conversations.android.xmpp.model.stanza.Message packet,
1450 final boolean selfAddressed,
1451 final Jid counterpart,
1452 final MessageArchiveService.Query query,
1453 final boolean isTypeGroupChat,
1454 Conversation conversation,
1455 Element mucUserElement,
1456 Jid from) {
1457 final var id = displayed.getId();
1458 // TODO we don’t even use 'sender' any more. Remove this!
1459 final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1460 if (packet.fromAccount(account) && !selfAddressed) {
1461 final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1462 final Message message =
1463 (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1464 if (message != null && (query == null || query.isCatchup())) {
1465 mXmppConnectionService.markReadUpTo(c, message);
1466 }
1467 if (query == null) {
1468 activateGracePeriod(account);
1469 }
1470 } else if (isTypeGroupChat) {
1471 final Message message;
1472 if (conversation != null && id != null) {
1473 if (sender != null) {
1474 message = conversation.findMessageWithRemoteId(id, sender);
1475 } else {
1476 message = conversation.findMessageWithServerMsgId(id);
1477 }
1478 } else {
1479 message = null;
1480 }
1481 if (message != null) {
1482 // TODO use occupantId to extract true counterpart from presence
1483 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1484 // TODO try to externalize mucTrueCounterpart
1485 final Jid trueJid =
1486 getTrueCounterpart(
1487 (query != null && query.safeToExtractTrueCounterpart())
1488 ? mucUserElement
1489 : null,
1490 fallback);
1491 final boolean trueJidMatchesAccount =
1492 account.getJid()
1493 .asBareJid()
1494 .equals(trueJid == null ? null : trueJid.asBareJid());
1495 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1496 if (!message.isRead()
1497 && (query == null || query.isCatchup())) { // checking if message is
1498 // unread fixes race conditions
1499 // with reflections
1500 mXmppConnectionService.markReadUpTo(conversation, message);
1501 }
1502 } else if (!counterpart.isBareJid() && trueJid != null) {
1503 final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1504 if (message.addReadByMarker(readByMarker)) {
1505 final var mucOptions = conversation.getMucOptions();
1506 final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1507 final var readyBy = message.getReadyByTrue();
1508 final var mStatus = message.getStatus();
1509 if (mucOptions.isPrivateAndNonAnonymous()
1510 && (mStatus == Message.STATUS_SEND_RECEIVED
1511 || mStatus == Message.STATUS_SEND)
1512 && readyBy.containsAll(everyone)) {
1513 message.setStatus(Message.STATUS_SEND_DISPLAYED);
1514 }
1515 mXmppConnectionService.updateMessage(message, false);
1516 }
1517 }
1518 }
1519 } else {
1520 final Message displayedMessage =
1521 mXmppConnectionService.markMessage(
1522 account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1523 Message message = displayedMessage == null ? null : displayedMessage.prev();
1524 while (message != null
1525 && message.getStatus() == Message.STATUS_SEND_RECEIVED
1526 && message.getTimeSent() < displayedMessage.getTimeSent()) {
1527 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1528 message = message.prev();
1529 }
1530 if (displayedMessage != null && selfAddressed) {
1531 dismissNotification(account, counterpart, query, id);
1532 }
1533 }
1534 }
1535
1536 private void processReactions(
1537 final Reactions reactions,
1538 final Conversation conversation,
1539 final boolean isTypeGroupChat,
1540 final OccupantId occupant,
1541 final Jid counterpart,
1542 final Jid mucTrueCounterPart,
1543 final im.conversations.android.xmpp.model.stanza.Message packet) {
1544 final String reactingTo = reactions.getId();
1545 if (conversation != null && reactingTo != null) {
1546 if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1547 final var mucOptions = conversation.getMucOptions();
1548 final var occupantId = occupant == null ? null : occupant.getId();
1549 if (occupantId != null) {
1550 final boolean isReceived = !mucOptions.isSelf(occupantId);
1551 final Message message;
1552 final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1553 if (inMemoryMessage != null) {
1554 message = inMemoryMessage;
1555 } else {
1556 message =
1557 mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1558 conversation, reactingTo);
1559 }
1560 if (message != null) {
1561 final var combinedReactions =
1562 Reaction.withOccupantId(
1563 message.getReactions(),
1564 reactions.getReactions(),
1565 isReceived,
1566 counterpart,
1567 mucTrueCounterPart,
1568 occupantId);
1569 message.setReactions(combinedReactions);
1570 mXmppConnectionService.updateMessage(message, false);
1571 } else {
1572 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1573 }
1574 } else {
1575 Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1576 }
1577 } else {
1578 final Message message;
1579 final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1580 if (inMemoryMessage != null) {
1581 message = inMemoryMessage;
1582 } else {
1583 message =
1584 mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1585 conversation, reactingTo);
1586 }
1587 if (message == null) {
1588 Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1589 return;
1590 }
1591 final boolean isReceived;
1592 final Jid reactionFrom;
1593 if (conversation.getMode() == Conversational.MODE_MULTI) {
1594 Log.d(Config.LOGTAG, "received reaction as MUC PM. triggering validation");
1595 final var mucOptions = conversation.getMucOptions();
1596 final var occupantId = occupant == null ? null : occupant.getId();
1597 if (occupantId == null) {
1598 Log.d(
1599 Config.LOGTAG,
1600 "received reaction via PM channel w/o occupant ids. ignoring");
1601 return;
1602 }
1603 isReceived = !mucOptions.isSelf(occupantId);
1604 if (isReceived) {
1605 reactionFrom = counterpart;
1606 } else {
1607 if (!occupantId.equals(message.getOccupantId())) {
1608 Log.d(
1609 Config.LOGTAG,
1610 "reaction received via MUC PM did not pass validation");
1611 return;
1612 }
1613 reactionFrom = account.getJid().asBareJid();
1614 }
1615 } else {
1616 if (packet.fromAccount(account)) {
1617 isReceived = false;
1618 reactionFrom = account.getJid().asBareJid();
1619 } else {
1620 isReceived = true;
1621 reactionFrom = counterpart;
1622 }
1623 }
1624 final var combinedReactions =
1625 Reaction.withFrom(
1626 message.getReactions(),
1627 reactions.getReactions(),
1628 isReceived,
1629 reactionFrom);
1630 message.setReactions(combinedReactions);
1631 mXmppConnectionService.updateMessage(message, false);
1632 }
1633 }
1634 }
1635
1636 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1637 getForwardedMessagePacket(
1638 final im.conversations.android.xmpp.model.stanza.Message original,
1639 Class<? extends Extension> clazz) {
1640 final var extension = original.getExtension(clazz);
1641 final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1642 if (forwarded == null) {
1643 return null;
1644 }
1645 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1646 final var forwardedMessage = forwarded.getMessage();
1647 if (forwardedMessage == null) {
1648 return null;
1649 }
1650 return new Pair<>(forwardedMessage, timestamp);
1651 }
1652
1653 private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1654 getForwardedMessagePacket(
1655 final im.conversations.android.xmpp.model.stanza.Message original,
1656 final String name,
1657 final String namespace) {
1658 final Element wrapper = original.findChild(name, namespace);
1659 final var forwardedElement =
1660 wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1661 if (forwardedElement instanceof Forwarded forwarded) {
1662 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1663 final var forwardedMessage = forwarded.getMessage();
1664 if (forwardedMessage == null) {
1665 return null;
1666 }
1667 return new Pair<>(forwardedMessage, timestamp);
1668 }
1669 return null;
1670 }
1671
1672 private void dismissNotification(
1673 Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1674 final Conversation conversation =
1675 mXmppConnectionService.find(account, counterpart.asBareJid());
1676 if (conversation != null && (query == null || query.isCatchup())) {
1677 final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1678 if (displayableId != null && displayableId.equals(id)) {
1679 mXmppConnectionService.markRead(conversation);
1680 } else {
1681 Log.w(
1682 Config.LOGTAG,
1683 account.getJid().asBareJid()
1684 + ": received dismissing display marker that did not match our last"
1685 + " id in that conversation");
1686 }
1687 }
1688 }
1689
1690 private void processMessageReceipts(
1691 final Account account,
1692 final im.conversations.android.xmpp.model.stanza.Message packet,
1693 final String remoteMsgId,
1694 final MessageArchiveService.Query query) {
1695 final var request = packet.hasExtension(Request.class);
1696 if (query == null) {
1697 if (request) {
1698 final var receipt =
1699 mXmppConnectionService
1700 .getMessageGenerator()
1701 .received(packet.getFrom(), remoteMsgId, packet.getType());
1702 mXmppConnectionService.sendMessagePacket(account, receipt);
1703 }
1704 } else if (query.isCatchup()) {
1705 if (request) {
1706 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1707 }
1708 }
1709 }
1710
1711 private void activateGracePeriod(Account account) {
1712 long duration =
1713 mXmppConnectionService.getLongPreference(
1714 "grace_period_length", R.integer.grace_period)
1715 * 1000;
1716 Log.d(
1717 Config.LOGTAG,
1718 account.getJid().asBareJid()
1719 + ": activating grace period till "
1720 + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1721 account.activateGracePeriod(duration);
1722 }
1723
1724 private class Invite {
1725 final Jid jid;
1726 final String password;
1727 final boolean direct;
1728 final Jid inviter;
1729
1730 Invite(Jid jid, String password, boolean direct, Jid inviter) {
1731 this.jid = jid;
1732 this.password = password;
1733 this.direct = direct;
1734 this.inviter = inviter;
1735 }
1736
1737 public boolean execute(final Account account) {
1738 if (this.jid == null) {
1739 return false;
1740 }
1741 final Contact contact =
1742 this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1743 if (contact != null && contact.isBlocked()) {
1744 Log.d(
1745 Config.LOGTAG,
1746 account.getJid().asBareJid()
1747 + ": ignore invite from "
1748 + contact.getJid()
1749 + " because contact is blocked");
1750 return false;
1751 }
1752 final AppSettings appSettings = new AppSettings(mXmppConnectionService);
1753 if ((contact != null && contact.showInContactList())
1754 || appSettings.isAcceptInvitesFromStrangers()) {
1755 final Conversation conversation =
1756 mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1757 if (conversation.getMucOptions().online()) {
1758 Log.d(
1759 Config.LOGTAG,
1760 account.getJid().asBareJid()
1761 + ": received invite to "
1762 + jid
1763 + " but muc is considered to be online");
1764 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1765 } else {
1766 conversation.getMucOptions().setPassword(password);
1767 mXmppConnectionService.databaseBackend.updateConversation(conversation);
1768 mXmppConnectionService.joinMuc(
1769 conversation, contact != null && contact.showInContactList());
1770 mXmppConnectionService.updateConversationUi();
1771 }
1772 return true;
1773 } else {
1774 Log.d(
1775 Config.LOGTAG,
1776 account.getJid().asBareJid()
1777 + ": ignoring invite from "
1778 + this.inviter
1779 + " because we are not accepting invites from strangers. direct="
1780 + direct);
1781 return false;
1782 }
1783 }
1784 }
1785}