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