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