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