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