1package eu.siacs.conversations.parser;
2
3import android.util.Log;
4import android.util.Pair;
5
6import com.google.common.collect.ImmutableSet;
7
8import java.text.SimpleDateFormat;
9import java.util.ArrayList;
10import java.util.Arrays;
11import java.util.Collections;
12import java.util.Date;
13import java.util.List;
14import java.util.Locale;
15import java.util.Map;
16import java.util.Set;
17import java.util.UUID;
18import java.util.function.Consumer;
19
20import eu.siacs.conversations.AppSettings;
21import eu.siacs.conversations.Config;
22import eu.siacs.conversations.R;
23import eu.siacs.conversations.crypto.axolotl.AxolotlService;
24import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
25import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
26import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException;
27import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
28import eu.siacs.conversations.entities.Account;
29import eu.siacs.conversations.entities.Bookmark;
30import eu.siacs.conversations.entities.Contact;
31import eu.siacs.conversations.entities.Conversation;
32import eu.siacs.conversations.entities.Conversational;
33import eu.siacs.conversations.entities.Message;
34import eu.siacs.conversations.entities.MucOptions;
35import eu.siacs.conversations.entities.ReadByMarker;
36import eu.siacs.conversations.entities.ReceiptRequest;
37import eu.siacs.conversations.entities.RtpSessionStatus;
38import eu.siacs.conversations.http.HttpConnectionManager;
39import eu.siacs.conversations.services.MessageArchiveService;
40import eu.siacs.conversations.services.QuickConversationsService;
41import eu.siacs.conversations.services.XmppConnectionService;
42import eu.siacs.conversations.utils.CryptoHelper;
43import eu.siacs.conversations.xml.Element;
44import eu.siacs.conversations.xml.LocalizedContent;
45import eu.siacs.conversations.xml.Namespace;
46import eu.siacs.conversations.xmpp.InvalidJid;
47import eu.siacs.conversations.xmpp.Jid;
48import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
49import eu.siacs.conversations.xmpp.chatstate.ChatState;
50import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
51import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
52import eu.siacs.conversations.xmpp.pep.Avatar;
53import im.conversations.android.xmpp.model.Extension;
54import im.conversations.android.xmpp.model.carbons.Received;
55import im.conversations.android.xmpp.model.carbons.Sent;
56import im.conversations.android.xmpp.model.forward.Forwarded;
57import im.conversations.android.xmpp.model.occupant.OccupantId;
58
59public class MessageParser extends AbstractParser implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
60
61 private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
62
63 private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
64 Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
65
66 public MessageParser(final XmppConnectionService service, final Account account) {
67 super(service, account);
68 }
69
70 private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) {
71 final Jid by;
72 final boolean safeToExtract;
73 if (isTypeGroupChat) {
74 by = conversation.getJid().asBareJid();
75 safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
76 } else {
77 Account account = conversation.getAccount();
78 by = account.getJid().asBareJid();
79 safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
80 }
81 return safeToExtract ? extractStanzaId(packet, by) : null;
82 }
83
84 private static String extractStanzaId(Account account, Element packet) {
85 final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
86 return safeToExtract ? extractStanzaId(packet, account.getJid().asBareJid()) : null;
87 }
88
89 private static String extractStanzaId(Element packet, Jid by) {
90 for (Element child : packet.getChildren()) {
91 if (child.getName().equals("stanza-id")
92 && Namespace.STANZA_IDS.equals(child.getNamespace())
93 && by.equals(InvalidJid.getNullForInvalid(child.getAttributeAsJid("by")))) {
94 return child.getAttribute("id");
95 }
96 }
97 return null;
98 }
99
100 private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
101 final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
102 Jid result = item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
103 return result != null ? result : fallback;
104 }
105
106 private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final im.conversations.android.xmpp.model.stanza.Message packet) {
107 ChatState state = ChatState.parse(packet);
108 if (state != null && c != null) {
109 final Account account = c.getAccount();
110 final Jid from = packet.getFrom();
111 if (from.asBareJid().equals(account.getJid().asBareJid())) {
112 c.setOutgoingChatState(state);
113 if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
114 if (c.getContact().isSelf()) {
115 return false;
116 }
117 mXmppConnectionService.markRead(c);
118 activateGracePeriod(account);
119 }
120 return false;
121 } else {
122 if (isTypeGroupChat) {
123 MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
124 if (user != null) {
125 return user.setChatState(state);
126 } else {
127 return false;
128 }
129 } else {
130 return c.setIncomingChatState(state);
131 }
132 }
133 }
134 return false;
135 }
136
137 private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) {
138 final AxolotlService service = conversation.getAccount().getAxolotlService();
139 final XmppAxolotlMessage xmppAxolotlMessage;
140 try {
141 xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
142 } catch (Exception e) {
143 Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": invalid omemo message received " + e.getMessage());
144 return null;
145 }
146 if (xmppAxolotlMessage.hasPayload()) {
147 final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
148 try {
149 plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
150 } catch (BrokenSessionException e) {
151 if (checkedForDuplicates) {
152 if (service.trustedOrPreviouslyResponded(from.asBareJid())) {
153 service.reportBrokenSessionException(e, postpone);
154 return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
155 } else {
156 Log.d(Config.LOGTAG, "ignoring broken session exception because contact was not trusted");
157 return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
158 }
159 } else {
160 Log.d(Config.LOGTAG, "ignoring broken session exception because checkForDuplicates failed");
161 return null;
162 }
163 } catch (NotEncryptedForThisDeviceException e) {
164 return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
165 } catch (OutdatedSenderException e) {
166 return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
167 }
168 if (plaintextMessage != null) {
169 Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
170 finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
171 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount()) + " Received Message with session fingerprint: " + plaintextMessage.getFingerprint());
172 return finishedMessage;
173 }
174 } else {
175 Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OMEMO key transport message");
176 service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
177 }
178 return null;
179 }
180
181 private Invite extractInvite(final Element message) {
182 final Element mucUser = message.findChild("x", Namespace.MUC_USER);
183 if (mucUser != null) {
184 final Element invite = mucUser.findChild("invite");
185 if (invite != null) {
186 final String password = mucUser.findChildContent("password");
187 final Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
188 final Jid to = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("to"));
189 if (to != null && from == null) {
190 Log.d(Config.LOGTAG,"do not parse outgoing mediated invite "+message);
191 return null;
192 }
193 final Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
194 if (room == null) {
195 return null;
196 }
197 return new Invite(room, password, false, from);
198 }
199 }
200 final Element conference = message.findChild("x", "jabber:x:conference");
201 if (conference != null) {
202 Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
203 Jid room = InvalidJid.getNullForInvalid(conference.getAttributeAsJid("jid"));
204 if (room == null) {
205 return null;
206 }
207 return new Invite(room, conference.getAttribute("password"), true, from);
208 }
209 return null;
210 }
211
212 private void parseEvent(final Element event, final Jid from, final Account account) {
213 final Element items = event.findChild("items");
214 final String node = items == null ? null : items.getAttribute("node");
215 if ("urn:xmpp:avatar:metadata".equals(node)) {
216 Avatar avatar = Avatar.parseMetadata(items);
217 if (avatar != null) {
218 avatar.owner = from.asBareJid();
219 if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
220 if (account.getJid().asBareJid().equals(from)) {
221 if (account.setAvatar(avatar.getFilename())) {
222 mXmppConnectionService.databaseBackend.updateAccount(account);
223 mXmppConnectionService.notifyAccountAvatarHasChanged(account);
224 }
225 mXmppConnectionService.getAvatarService().clear(account);
226 mXmppConnectionService.updateConversationUi();
227 mXmppConnectionService.updateAccountUi();
228 } else {
229 final Contact contact = account.getRoster().getContact(from);
230 contact.setAvatar(avatar);
231 mXmppConnectionService.syncRoster(account);
232 mXmppConnectionService.getAvatarService().clear(contact);
233 mXmppConnectionService.updateConversationUi();
234 mXmppConnectionService.updateRosterUi();
235 }
236 } else if (mXmppConnectionService.isDataSaverDisabled()) {
237 mXmppConnectionService.fetchAvatar(account, avatar);
238 }
239 }
240 } else if (Namespace.NICK.equals(node)) {
241 final Element i = items.findChild("item");
242 final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
243 if (nick != null) {
244 setNick(account, from, nick);
245 }
246 } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
247 Element item = items.findChild("item");
248 final Set<Integer> deviceIds = IqParser.deviceIds(item);
249 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
250 final AxolotlService axolotlService = account.getAxolotlService();
251 axolotlService.registerDevices(from, deviceIds);
252 } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
253 final var connection = account.getXmppConnection();
254 if (connection.getFeatures().bookmarksConversion()) {
255 if (connection.getFeatures().bookmarks2()) {
256 Log.w(
257 Config.LOGTAG,
258 account.getJid().asBareJid()
259 + ": received storage:bookmark notification even though we opted into bookmarks:1");
260 }
261 final Element i = items.findChild("item");
262 final Element storage =
263 i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
264 final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
265 mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
266 Log.d(
267 Config.LOGTAG,
268 account.getJid().asBareJid() + ": processing bookmark PEP event");
269 } else {
270 Log.d(
271 Config.LOGTAG,
272 account.getJid().asBareJid()
273 + ": ignoring bookmark PEP event because bookmark conversion was not detected");
274 }
275 } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
276 final Element item = items.findChild("item");
277 final Element retract = items.findChild("retract");
278 if (item != null) {
279 final Bookmark bookmark = Bookmark.parseFromItem(item, account);
280 if (bookmark != null) {
281 account.putBookmark(bookmark);
282 mXmppConnectionService.processModifiedBookmark(bookmark);
283 mXmppConnectionService.updateConversationUi();
284 }
285 }
286 if (retract != null) {
287 final Jid id = InvalidJid.getNullForInvalid(retract.getAttributeAsJid("id"));
288 if (id != null) {
289 account.removeBookmark(id);
290 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id);
291 mXmppConnectionService.processDeletedBookmark(account, id);
292 mXmppConnectionService.updateConversationUi();
293 }
294 }
295 } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
296 && Namespace.MDS_DISPLAYED.equals(node)
297 && account.getJid().asBareJid().equals(from)) {
298 final Element item = items.findChild("item");
299 mXmppConnectionService.processMdsItem(account, item);
300 } else {
301 Log.d(
302 Config.LOGTAG,
303 account.getJid().asBareJid()
304 + " received pubsub notification for node="
305 + node);
306 }
307 }
308
309 private void parseDeleteEvent(final Element event, final Jid from, final Account account) {
310 final Element delete = event.findChild("delete");
311 final String node = delete == null ? null : delete.getAttribute("node");
312 if (Namespace.NICK.equals(node)) {
313 Log.d(Config.LOGTAG, "parsing nick delete event from " + from);
314 setNick(account, from, null);
315 } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
316 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
317 deleteAllBookmarks(account);
318 } else if (Namespace.AVATAR_METADATA.equals(node) && account.getJid().asBareJid().equals(from)) {
319 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted avatar metadata node");
320 }
321 }
322
323 private void parsePurgeEvent(final Element event, final Jid from, final Account account) {
324 final Element purge = event.findChild("purge");
325 final String node = purge == null ? null : purge.getAttribute("node");
326 if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
327 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
328 deleteAllBookmarks(account);
329 }
330 }
331
332 private void deleteAllBookmarks(final Account account) {
333 final var previous = account.getBookmarkedJids();
334 account.setBookmarks(Collections.emptyMap());
335 mXmppConnectionService.processDeletedBookmarks(account, previous);
336 }
337
338 private void setNick(Account account, Jid user, String nick) {
339 if (user.asBareJid().equals(account.getJid().asBareJid())) {
340 account.setDisplayName(nick);
341 if (QuickConversationsService.isQuicksy()) {
342 mXmppConnectionService.getAvatarService().clear(account);
343 }
344 } else {
345 Contact contact = account.getRoster().getContact(user);
346 if (contact.setPresenceName(nick)) {
347 mXmppConnectionService.syncRoster(account);
348 mXmppConnectionService.getAvatarService().clear(contact);
349 }
350 }
351 mXmppConnectionService.updateConversationUi();
352 mXmppConnectionService.updateAccountUi();
353 }
354
355 private boolean handleErrorMessage(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) {
356 if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) {
357 if (packet.fromServer(account)) {
358 final var forwarded = getForwardedMessagePacket(packet,"received", Namespace.CARBONS);
359 if (forwarded != null) {
360 return handleErrorMessage(account, forwarded.first);
361 }
362 }
363 final Jid from = packet.getFrom();
364 final String id = packet.getId();
365 if (from != null && id != null) {
366 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
367 final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
368 mXmppConnectionService.getJingleConnectionManager()
369 .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.FAILED);
370 return true;
371 }
372 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
373 final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
374 final String message = extractErrorMessage(packet);
375 mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, message);
376 return true;
377 }
378 mXmppConnectionService.markMessage(account,
379 from.asBareJid(),
380 id,
381 Message.STATUS_SEND_FAILED,
382 extractErrorMessage(packet));
383 final Element error = packet.findChild("error");
384 final boolean pingWorthyError = error != null && (error.hasChild("not-acceptable") || error.hasChild("remote-server-timeout") || error.hasChild("remote-server-not-found"));
385 if (pingWorthyError) {
386 Conversation conversation = mXmppConnectionService.find(account, from);
387 if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) {
388 if (conversation.getMucOptions().online()) {
389 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ping worthy error for seemingly online muc at " + from);
390 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
391 }
392 }
393 }
394 }
395 return true;
396 }
397 return false;
398 }
399
400 @Override
401 public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
402 if (handleErrorMessage(account, original)) {
403 return;
404 }
405 final im.conversations.android.xmpp.model.stanza.Message packet;
406 Long timestamp = null;
407 boolean isCarbon = false;
408 String serverMsgId = null;
409 final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
410 if (fin != null) {
411 mXmppConnectionService.getMessageArchiveService().processFinLegacy(fin, original.getFrom());
412 return;
413 }
414 final Element result = MessageArchiveService.Version.findResult(original);
415 final String queryId = result == null ? null : result.getAttribute("queryid");
416 final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
417 final boolean offlineMessagesRetrieved = account.getXmppConnection().isOfflineMessagesRetrieved();
418 if (query != null && query.validFrom(original.getFrom())) {
419 final var f = getForwardedMessagePacket(original,"result", query.version.namespace);
420 if (f == null) {
421 return;
422 }
423 timestamp = f.second;
424 packet = f.first;
425 serverMsgId = result.getAttribute("id");
426 query.incrementMessageCount();
427 if (handleErrorMessage(account, packet)) {
428 return;
429 }
430 } else if (query != null) {
431 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from (" + original.getFrom() + ") or queryId (" + queryId + ")");
432 return;
433 } else if (original.fromServer(account)) {
434 Pair<im.conversations.android.xmpp.model.stanza.Message, Long> f;
435 f = getForwardedMessagePacket(original, Received.class);
436 f = f == null ? getForwardedMessagePacket(original, Sent.class) : f;
437 packet = f != null ? f.first : original;
438 if (handleErrorMessage(account, packet)) {
439 return;
440 }
441 timestamp = f != null ? f.second : null;
442 isCarbon = f != null;
443 } else {
444 packet = original;
445 }
446
447 if (timestamp == null) {
448 timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
449 }
450 final LocalizedContent body = packet.getBody();
451 final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
452 final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
453 final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
454 final Element oob = packet.findChild("x", Namespace.OOB);
455 final String oobUrl = oob != null ? oob.findChildContent("url") : null;
456 final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
457 final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
458 int status;
459 final Jid counterpart;
460 final Jid to = packet.getTo();
461 final Jid from = packet.getFrom();
462 final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
463 final String remoteMsgId;
464 if (originId != null && originId.getAttribute("id") != null) {
465 remoteMsgId = originId.getAttribute("id");
466 } else {
467 remoteMsgId = packet.getId();
468 }
469 boolean notify = false;
470
471 if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) {
472 Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
473 return;
474 }
475
476 boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
477 if (query != null && !query.muc() && isTypeGroupChat) {
478 Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
479 return;
480 }
481 boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
482 boolean selfAddressed;
483 if (packet.fromAccount(account)) {
484 status = Message.STATUS_SEND;
485 selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
486 if (selfAddressed) {
487 counterpart = from;
488 } else {
489 counterpart = to;
490 }
491 } else {
492 status = Message.STATUS_RECEIVED;
493 counterpart = from;
494 selfAddressed = false;
495 }
496
497 final Invite invite = extractInvite(packet);
498 if (invite != null) {
499 if (invite.jid.asBareJid().equals(account.getJid().asBareJid())) {
500 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite to "+invite.jid+" because it matches account");
501 } else if (isTypeGroupChat) {
502 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because it was received as group chat");
503 } else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) {
504 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring direct invite to " + invite.jid + " because it was received in MUC");
505 } else {
506 invite.execute(account);
507 return;
508 }
509 }
510
511 if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) {
512 final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
513 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
514 final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
515
516 if (serverMsgId == null) {
517 serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
518 }
519
520
521 if (selfAddressed) {
522 if (mXmppConnectionService.markMessage(conversation, remoteMsgId, Message.STATUS_SEND_RECEIVED, serverMsgId)) {
523 return;
524 }
525 status = Message.STATUS_RECEIVED;
526 if (remoteMsgId != null && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
527 return;
528 }
529 }
530
531 if (isTypeGroupChat) {
532 if (conversation.getMucOptions().isSelf(counterpart)) {
533 status = Message.STATUS_SEND_RECEIVED;
534 isCarbon = true; //not really carbon but received from another resource
535 if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId, body)) {
536 return;
537 } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
538 if (body != null) {
539 Message message = conversation.findSentMessageWithBody(body.content);
540 if (message != null) {
541 mXmppConnectionService.markMessage(message, status);
542 return;
543 }
544 }
545 }
546 } else {
547 status = Message.STATUS_RECEIVED;
548 }
549 }
550 final Message message;
551 if (pgpEncrypted != null && Config.supportOpenPgp()) {
552 message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
553 } else if (axolotlEncrypted != null && Config.supportOmemo()) {
554 Jid origin;
555 Set<Jid> fallbacksBySourceId = Collections.emptySet();
556 if (conversationMultiMode) {
557 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
558 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
559 if (origin == null) {
560 try {
561 fallbacksBySourceId = account.getAxolotlService().findCounterpartsBySourceId(XmppAxolotlMessage.parseSourceId(axolotlEncrypted));
562 } catch (IllegalArgumentException e) {
563 //ignoring
564 }
565 }
566 if (origin == null && fallbacksBySourceId.size() == 0) {
567 Log.d(Config.LOGTAG, "axolotl message in anonymous conference received and no possible fallbacks");
568 return;
569 }
570 } else {
571 fallbacksBySourceId = Collections.emptySet();
572 origin = from;
573 }
574
575 final boolean liveMessage = query == null && !isTypeGroupChat && mucUserElement == null;
576 final boolean checkedForDuplicates = liveMessage || (serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId));
577
578 if (origin != null) {
579 message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, checkedForDuplicates, query != null);
580 } else {
581 Message trial = null;
582 for (Jid fallback : fallbacksBySourceId) {
583 trial = parseAxolotlChat(axolotlEncrypted, fallback, conversation, status, checkedForDuplicates && fallbacksBySourceId.size() == 1, query != null);
584 if (trial != null) {
585 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": decoded muc message using fallback");
586 origin = fallback;
587 break;
588 }
589 }
590 message = trial;
591 }
592 if (message == null) {
593 if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
594 mXmppConnectionService.updateConversationUi();
595 }
596 if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
597 Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
598 if (previouslySent != null && previouslySent.getServerMsgId() == null && serverMsgId != null) {
599 previouslySent.setServerMsgId(serverMsgId);
600 mXmppConnectionService.databaseBackend.updateMessage(previouslySent, false);
601 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered previously sent OMEMO message without serverId. updating...");
602 }
603 }
604 return;
605 }
606 if (conversationMultiMode) {
607 message.setTrueCounterpart(origin);
608 }
609 } else if (body == null && oobUrl != null) {
610 message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
611 message.setOob(true);
612 if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
613 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
614 }
615 } else {
616 message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
617 if (body.count > 1) {
618 message.setBodyLanguage(body.language);
619 }
620 }
621
622 message.setCounterpart(counterpart);
623 message.setRemoteMsgId(remoteMsgId);
624 message.setServerMsgId(serverMsgId);
625 message.setCarbon(isCarbon);
626 message.setTime(timestamp);
627 if (body != null && body.content != null && body.content.equals(oobUrl)) {
628 message.setOob(true);
629 if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
630 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
631 }
632 }
633 message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
634 if (conversationMultiMode) {
635 final var mucOptions = conversation.getMucOptions();
636 final var occupantId =
637 mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null;
638 if (occupantId != null) {
639 message.setOccupantId(occupantId.getId());
640 }
641 message.setMucUser(mucOptions.findUserByFullJid(counterpart));
642 final Jid fallback = mucOptions.getTrueCounterpart(counterpart);
643 Jid trueCounterpart;
644 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
645 trueCounterpart = message.getTrueCounterpart();
646 } else if (query != null && query.safeToExtractTrueCounterpart()) {
647 trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
648 } else {
649 trueCounterpart = fallback;
650 }
651 if (trueCounterpart != null && isTypeGroupChat) {
652 if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
653 status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
654 } else {
655 status = Message.STATUS_RECEIVED;
656 message.setCarbon(false);
657 }
658 }
659 message.setStatus(status);
660 message.setTrueCounterpart(trueCounterpart);
661 if (!isTypeGroupChat) {
662 message.setType(Message.TYPE_PRIVATE);
663 }
664 } else {
665 updateLastseen(account, from);
666 }
667
668 if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
669 final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
670 counterpart,
671 message.getStatus() == Message.STATUS_RECEIVED,
672 message.isCarbon());
673 if (replacedMessage != null) {
674 final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
675 || replacedMessage.getFingerprint().equals(message.getFingerprint());
676 final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
677 && message.getTrueCounterpart() != null
678 && replacedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid());
679 final boolean occupantIdMatch =
680 replacedMessage.getOccupantId() != null
681 && replacedMessage
682 .getOccupantId()
683 .equals(message.getOccupantId());
684 final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
685 final boolean duplicate = conversation.hasDuplicateMessage(message);
686 if (fingerprintsMatch && (trueCountersMatch || occupantIdMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
687 synchronized (replacedMessage) {
688 final String uuid = replacedMessage.getUuid();
689 replacedMessage.setUuid(UUID.randomUUID().toString());
690 replacedMessage.setBody(message.getBody());
691 replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
692 replacedMessage.setRemoteMsgId(remoteMsgId);
693 if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
694 replacedMessage.setServerMsgId(message.getServerMsgId());
695 }
696 replacedMessage.setEncryption(message.getEncryption());
697 if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
698 replacedMessage.markUnread();
699 }
700 extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
701 mXmppConnectionService.updateMessage(replacedMessage, uuid);
702 if (mXmppConnectionService.confirmMessages()
703 && replacedMessage.getStatus() == Message.STATUS_RECEIVED
704 && (replacedMessage.trusted() || replacedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs?
705 && remoteMsgId != null
706 && !selfAddressed
707 && !isTypeGroupChat) {
708 processMessageReceipts(account, packet, remoteMsgId, query);
709 }
710 if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
711 conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
712 conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
713 }
714 }
715 mXmppConnectionService.getNotificationService().updateNotification();
716 return;
717 } else {
718 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
719 }
720 }
721 }
722
723 long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
724 if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
725 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
726 return;
727 }
728
729 boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
730 || message.isPrivateMessage()
731 || message.getServerMsgId() != null
732 || (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation));
733 if (checkForDuplicates) {
734 final Message duplicate = conversation.findDuplicateMessage(message);
735 if (duplicate != null) {
736 final boolean serverMsgIdUpdated;
737 if (duplicate.getStatus() != Message.STATUS_RECEIVED
738 && duplicate.getUuid().equals(message.getRemoteMsgId())
739 && duplicate.getServerMsgId() == null
740 && message.getServerMsgId() != null) {
741 duplicate.setServerMsgId(message.getServerMsgId());
742 if (mXmppConnectionService.databaseBackend.updateMessage(duplicate, false)) {
743 serverMsgIdUpdated = true;
744 } else {
745 serverMsgIdUpdated = false;
746 Log.e(Config.LOGTAG, "failed to update message");
747 }
748 } else {
749 serverMsgIdUpdated = false;
750 }
751 Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + serverMsgIdUpdated);
752 return;
753 }
754 }
755
756 if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
757 conversation.prepend(query.getActualInThisQuery(), message);
758 } else {
759 conversation.add(message);
760 }
761 if (query != null) {
762 query.incrementActualMessageCount();
763 }
764
765 if (query == null || query.isCatchup()) { //either no mam or catchup
766 if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
767 mXmppConnectionService.markRead(conversation);
768 if (query == null) {
769 activateGracePeriod(account);
770 }
771 } else {
772 message.markUnread();
773 notify = true;
774 }
775 }
776
777 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
778 notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
779 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
780 notify = false;
781 }
782
783 if (query == null) {
784 extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
785 mXmppConnectionService.updateConversationUi();
786 }
787
788 if (mXmppConnectionService.confirmMessages()
789 && message.getStatus() == Message.STATUS_RECEIVED
790 && (message.trusted() || message.isPrivateMessage())
791 && remoteMsgId != null
792 && !selfAddressed
793 && !isTypeGroupChat) {
794 processMessageReceipts(account, packet, remoteMsgId, query);
795 }
796
797 mXmppConnectionService.databaseBackend.createMessage(message);
798 final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
799 if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
800 manager.createNewDownloadConnection(message);
801 } else if (notify) {
802 if (query != null && query.isCatchup()) {
803 mXmppConnectionService.getNotificationService().pushFromBacklog(message);
804 } else {
805 mXmppConnectionService.getNotificationService().push(message);
806 }
807 }
808 } else if (!packet.hasChild("body")) { //no body
809
810 final Conversation conversation = mXmppConnectionService.find(account, from.asBareJid());
811 if (axolotlEncrypted != null) {
812 Jid origin;
813 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
814 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
815 origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
816 if (origin == null) {
817 Log.d(Config.LOGTAG, "omemo key transport message in anonymous conference received");
818 return;
819 }
820 } else if (isTypeGroupChat) {
821 return;
822 } else {
823 origin = from;
824 }
825 try {
826 final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
827 account.getAxolotlService().processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
828 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": omemo key transport message received from " + origin);
829 } catch (Exception e) {
830 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": invalid omemo key transport message received " + e.getMessage());
831 return;
832 }
833 }
834
835 if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
836 mXmppConnectionService.updateConversationUi();
837 }
838
839 if (isTypeGroupChat) {
840 if (packet.hasChild("subject") && !packet.hasChild("thread")) { // We already know it has no body per above
841 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
842 conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
843 final LocalizedContent subject = packet.findInternationalizedChildContentInDefaultNamespace("subject");
844 if (subject != null && conversation.getMucOptions().setSubject(subject.content)) {
845 mXmppConnectionService.updateConversation(conversation);
846 }
847 mXmppConnectionService.updateConversationUi();
848 return;
849 }
850 }
851 }
852 if (conversation != null && mucUserElement != null && InvalidJid.hasValidFrom(packet) && from.isBareJid()) {
853 for (Element child : mucUserElement.getChildren()) {
854 if ("status".equals(child.getName())) {
855 try {
856 int code = Integer.parseInt(child.getAttribute("code"));
857 if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
858 mXmppConnectionService.fetchConferenceConfiguration(conversation);
859 break;
860 }
861 } catch (Exception e) {
862 //ignored
863 }
864 } else if ("item".equals(child.getName())) {
865 MucOptions.User user = AbstractParser.parseItem(conversation, child);
866 Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for "
867 + user.getRealJid() + " to " + user.getAffiliation() + " in "
868 + conversation.getJid().asBareJid());
869 if (!user.realJidMatchesAccount()) {
870 boolean isNew = conversation.getMucOptions().updateUser(user);
871 mXmppConnectionService.getAvatarService().clear(conversation);
872 mXmppConnectionService.updateMucRosterUi();
873 mXmppConnectionService.updateConversationUi();
874 Contact contact = user.getContact();
875 if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
876 Jid jid = user.getRealJid();
877 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
878 if (cryptoTargets.remove(user.getRealJid())) {
879 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
880 conversation.setAcceptedCryptoTargets(cryptoTargets);
881 mXmppConnectionService.updateConversation(conversation);
882 }
883 } else if (isNew
884 && user.getRealJid() != null
885 && conversation.getMucOptions().isPrivateAndNonAnonymous()
886 && (contact == null || !contact.mutualPresenceSubscription())
887 && account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) {
888 account.getAxolotlService().fetchDeviceIds(user.getRealJid());
889 }
890 }
891 }
892 }
893 }
894 if (!isTypeGroupChat) {
895 for (Element child : packet.getChildren()) {
896 if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
897 final String action = child.getName();
898 final String sessionId = child.getAttribute("id");
899 if (sessionId == null) {
900 break;
901 }
902 if (query == null && offlineMessagesRetrieved) {
903 if (serverMsgId == null) {
904 serverMsgId = extractStanzaId(account, packet);
905 }
906 mXmppConnectionService
907 .getJingleConnectionManager()
908 .deliverMessage(
909 account,
910 packet.getTo(),
911 packet.getFrom(),
912 child,
913 remoteMsgId,
914 serverMsgId,
915 timestamp);
916 final Contact contact = account.getRoster().getContact(from);
917 // this is the same condition that is found in JingleRtpConnection for
918 // the 'ringing' response. Responding with delivery receipts predates
919 // the 'ringing' spec'd
920 final boolean sendReceipts =
921 (mXmppConnectionService.confirmMessages()
922 && contact.showInContactList())
923 || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
924 if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
925 processMessageReceipts(account, packet, remoteMsgId, null);
926 }
927 } else if ((query != null && query.isCatchup()) || !offlineMessagesRetrieved) {
928 if ("propose".equals(action)) {
929 final Element description = child.findChild("description");
930 final String namespace =
931 description == null ? null : description.getNamespace();
932 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
933 final Conversation c =
934 mXmppConnectionService.findOrCreateConversation(
935 account, counterpart.asBareJid(), false, false);
936 final Message preExistingMessage =
937 c.findRtpSession(sessionId, status);
938 if (preExistingMessage != null) {
939 preExistingMessage.setServerMsgId(serverMsgId);
940 mXmppConnectionService.updateMessage(preExistingMessage);
941 break;
942 }
943 final Message message =
944 new Message(
945 c, status, Message.TYPE_RTP_SESSION, sessionId);
946 message.setServerMsgId(serverMsgId);
947 message.setTime(timestamp);
948 message.setBody(new RtpSessionStatus(false, 0).toString());
949 c.add(message);
950 mXmppConnectionService.databaseBackend.createMessage(message);
951 }
952 } else if ("proceed".equals(action)) {
953 // status needs to be flipped to find the original propose
954 final Conversation c =
955 mXmppConnectionService.findOrCreateConversation(
956 account, counterpart.asBareJid(), false, false);
957 final int s =
958 packet.fromAccount(account)
959 ? Message.STATUS_RECEIVED
960 : Message.STATUS_SEND;
961 final Message message = c.findRtpSession(sessionId, s);
962 if (message != null) {
963 message.setBody(new RtpSessionStatus(true, 0).toString());
964 if (serverMsgId != null) {
965 message.setServerMsgId(serverMsgId);
966 }
967 message.setTime(timestamp);
968 mXmppConnectionService.updateMessage(message, true);
969 } else {
970 Log.d(
971 Config.LOGTAG,
972 "unable to find original rtp session message for received propose");
973 }
974
975 } else if ("finish".equals(action)) {
976 Log.d(
977 Config.LOGTAG,
978 "received JMI 'finish' during MAM catch-up. Can be used to update success/failure and duration");
979 }
980 } else {
981 //MAM reloads (non catchups
982 if ("propose".equals(action)) {
983 final Element description = child.findChild("description");
984 final String namespace = description == null ? null : description.getNamespace();
985 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
986 final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false);
987 final Message preExistingMessage = c.findRtpSession(sessionId, status);
988 if (preExistingMessage != null) {
989 preExistingMessage.setServerMsgId(serverMsgId);
990 mXmppConnectionService.updateMessage(preExistingMessage);
991 break;
992 }
993 final Message message = new Message(
994 c,
995 status,
996 Message.TYPE_RTP_SESSION,
997 sessionId
998 );
999 message.setServerMsgId(serverMsgId);
1000 message.setTime(timestamp);
1001 message.setBody(new RtpSessionStatus(true, 0).toString());
1002 if (query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
1003 c.prepend(query.getActualInThisQuery(), message);
1004 } else {
1005 c.add(message);
1006 }
1007 query.incrementActualMessageCount();
1008 mXmppConnectionService.databaseBackend.createMessage(message);
1009 }
1010 }
1011 }
1012 break;
1013 }
1014 }
1015 }
1016 }
1017
1018 Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
1019 if (received == null) {
1020 received = packet.findChild("received", "urn:xmpp:receipts");
1021 }
1022 if (received != null) {
1023 String id = received.getAttribute("id");
1024 if (packet.fromAccount(account)) {
1025 if (query != null && id != null && packet.getTo() != null) {
1026 query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1027 }
1028 } else if (id != null) {
1029 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1030 final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1031 mXmppConnectionService.getJingleConnectionManager()
1032 .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1033 } else {
1034 mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1035 }
1036 }
1037 }
1038 final Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
1039 if (displayed != null) {
1040 final String id = displayed.getAttribute("id");
1041 final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1042 if (packet.fromAccount(account) && !selfAddressed) {
1043 final Conversation c =
1044 mXmppConnectionService.find(account, counterpart.asBareJid());
1045 final Message message =
1046 (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1047 if (message != null && (query == null || query.isCatchup())) {
1048 mXmppConnectionService.markReadUpTo(c, message);
1049 }
1050 if (query == null) {
1051 activateGracePeriod(account);
1052 }
1053 } else if (isTypeGroupChat) {
1054 final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
1055 final Message message;
1056 if (conversation != null && id != null) {
1057 if (sender != null) {
1058 message = conversation.findMessageWithRemoteId(id, sender);
1059 } else {
1060 message = conversation.findMessageWithServerMsgId(id);
1061 }
1062 } else {
1063 message = null;
1064 }
1065 if (message != null) {
1066 final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1067 final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
1068 final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
1069 if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1070 if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
1071 mXmppConnectionService.markReadUpTo(conversation, message);
1072 }
1073 } else if (!counterpart.isBareJid() && trueJid != null) {
1074 final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1075 if (message.addReadByMarker(readByMarker)) {
1076 final var mucOptions = conversation.getMucOptions();
1077 final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1078 final var readyBy = message.getReadyByTrue();
1079 final var mStatus = message.getStatus();
1080 if (mucOptions.isPrivateAndNonAnonymous()
1081 && (mStatus == Message.STATUS_SEND_RECEIVED
1082 || mStatus == Message.STATUS_SEND)
1083 && readyBy.containsAll(everyone)) {
1084 message.setStatus(Message.STATUS_SEND_DISPLAYED);
1085 }
1086 mXmppConnectionService.updateMessage(message, false);
1087 }
1088 }
1089 }
1090 } else {
1091 final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1092 Message message = displayedMessage == null ? null : displayedMessage.prev();
1093 while (message != null
1094 && message.getStatus() == Message.STATUS_SEND_RECEIVED
1095 && message.getTimeSent() < displayedMessage.getTimeSent()) {
1096 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1097 message = message.prev();
1098 }
1099 if (displayedMessage != null && selfAddressed) {
1100 dismissNotification(account, counterpart, query, id);
1101 }
1102 }
1103 }
1104
1105 final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
1106 if (event != null && InvalidJid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1107 if (event.hasChild("items")) {
1108 parseEvent(event, original.getFrom(), account);
1109 } else if (event.hasChild("delete")) {
1110 parseDeleteEvent(event, original.getFrom(), account);
1111 } else if (event.hasChild("purge")) {
1112 parsePurgeEvent(event, original.getFrom(), account);
1113 }
1114 }
1115
1116 final String nick = packet.findChildContent("nick", Namespace.NICK);
1117 if (nick != null && InvalidJid.hasValidFrom(original)) {
1118 if (mXmppConnectionService.isMuc(account, from)) {
1119 return;
1120 }
1121 final Contact contact = account.getRoster().getContact(from);
1122 if (contact.setPresenceName(nick)) {
1123 mXmppConnectionService.syncRoster(account);
1124 mXmppConnectionService.getAvatarService().clear(contact);
1125 }
1126 }
1127 }
1128
1129 private static Pair<im.conversations.android.xmpp.model.stanza.Message,Long> getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, Class<? extends Extension> clazz) {
1130 final var extension = original.getExtension(clazz);
1131 final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1132 if (forwarded == null) {
1133 return null;
1134 }
1135 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1136 final var forwardedMessage = forwarded.getMessage();
1137 if (forwardedMessage == null) {
1138 return null;
1139 }
1140 return new Pair<>(forwardedMessage,timestamp);
1141 }
1142
1143 private static Pair<im.conversations.android.xmpp.model.stanza.Message,Long> getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, final String name, final String namespace) {
1144 final Element wrapper = original.findChild(name, namespace);
1145 final var forwardedElement = wrapper == null ? null : wrapper.findChild("forwarded",Namespace.FORWARD);
1146 if (forwardedElement instanceof Forwarded forwarded) {
1147 final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1148 final var forwardedMessage = forwarded.getMessage();
1149 if (forwardedMessage == null) {
1150 return null;
1151 }
1152 return new Pair<>(forwardedMessage,timestamp);
1153 }
1154 return null;
1155 }
1156
1157 private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1158 final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
1159 if (conversation != null && (query == null || query.isCatchup())) {
1160 final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1161 if (displayableId != null && displayableId.equals(id)) {
1162 mXmppConnectionService.markRead(conversation);
1163 } else {
1164 Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": received dismissing display marker that did not match our last id in that conversation");
1165 }
1166 }
1167 }
1168
1169 private void processMessageReceipts(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet, final String remoteMsgId, MessageArchiveService.Query query) {
1170 final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
1171 final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
1172 if (query == null) {
1173 final ArrayList<String> receiptsNamespaces = new ArrayList<>();
1174 if (markable) {
1175 receiptsNamespaces.add("urn:xmpp:chat-markers:0");
1176 }
1177 if (request) {
1178 receiptsNamespaces.add("urn:xmpp:receipts");
1179 }
1180 if (receiptsNamespaces.size() > 0) {
1181 final var receipt = mXmppConnectionService.getMessageGenerator().received(account,
1182 packet.getFrom(),
1183 remoteMsgId,
1184 receiptsNamespaces,
1185 packet.getType());
1186 mXmppConnectionService.sendMessagePacket(account, receipt);
1187 }
1188 } else if (query.isCatchup()) {
1189 if (request) {
1190 query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1191 }
1192 }
1193 }
1194
1195 private void activateGracePeriod(Account account) {
1196 long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000;
1197 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1198 account.activateGracePeriod(duration);
1199 }
1200
1201 private class Invite {
1202 final Jid jid;
1203 final String password;
1204 final boolean direct;
1205 final Jid inviter;
1206
1207 Invite(Jid jid, String password, boolean direct, Jid inviter) {
1208 this.jid = jid;
1209 this.password = password;
1210 this.direct = direct;
1211 this.inviter = inviter;
1212 }
1213
1214 public boolean execute(final Account account) {
1215 if (this.jid == null) {
1216 return false;
1217 }
1218 final Contact contact =
1219 this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1220 if (contact != null && contact.isBlocked()) {
1221 Log.d(
1222 Config.LOGTAG,
1223 account.getJid().asBareJid()
1224 + ": ignore invite from "
1225 + contact.getJid()
1226 + " because contact is blocked");
1227 return false;
1228 }
1229 final AppSettings appSettings = new AppSettings(mXmppConnectionService);
1230 if ((contact != null && contact.showInContactList())
1231 || appSettings.isAcceptInvitesFromStrangers()) {
1232 final Conversation conversation =
1233 mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1234 if (conversation.getMucOptions().online()) {
1235 Log.d(
1236 Config.LOGTAG,
1237 account.getJid().asBareJid()
1238 + ": received invite to "
1239 + jid
1240 + " but muc is considered to be online");
1241 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1242 } else {
1243 conversation.getMucOptions().setPassword(password);
1244 mXmppConnectionService.databaseBackend.updateConversation(conversation);
1245 mXmppConnectionService.joinMuc(
1246 conversation, contact != null && contact.showInContactList());
1247 mXmppConnectionService.updateConversationUi();
1248 }
1249 return true;
1250 } else {
1251 Log.d(
1252 Config.LOGTAG,
1253 account.getJid().asBareJid()
1254 + ": ignoring invite from "
1255 + this.inviter
1256 + " because we are not accepting invites from strangers. direct="
1257 + direct);
1258 return false;
1259 }
1260 }
1261 }
1262}