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