MessageParser.java

   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(final Account account, final Jid user, final 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            mXmppConnectionService.checkMucRequiresRename();
 347        } else {
 348            Contact contact = account.getRoster().getContact(user);
 349            if (contact.setPresenceName(nick)) {
 350                mXmppConnectionService.syncRoster(account);
 351                mXmppConnectionService.getAvatarService().clear(contact);
 352            }
 353        }
 354        mXmppConnectionService.updateConversationUi();
 355        mXmppConnectionService.updateAccountUi();
 356    }
 357
 358    private boolean handleErrorMessage(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) {
 359        if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) {
 360            if (packet.fromServer(account)) {
 361                final var forwarded = getForwardedMessagePacket(packet,"received", Namespace.CARBONS);
 362                if (forwarded != null) {
 363                    return handleErrorMessage(account, forwarded.first);
 364                }
 365            }
 366            final Jid from = packet.getFrom();
 367            final String id = packet.getId();
 368            if (from != null && id != null) {
 369                if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
 370                    final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
 371                    mXmppConnectionService.getJingleConnectionManager()
 372                            .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.FAILED);
 373                    return true;
 374                }
 375                if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
 376                    final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
 377                    final String message = extractErrorMessage(packet);
 378                    mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, message);
 379                    return true;
 380                }
 381                mXmppConnectionService.markMessage(account,
 382                        from.asBareJid(),
 383                        id,
 384                        Message.STATUS_SEND_FAILED,
 385                        extractErrorMessage(packet));
 386                final Element error = packet.findChild("error");
 387                final boolean pingWorthyError = error != null && (error.hasChild("not-acceptable") || error.hasChild("remote-server-timeout") || error.hasChild("remote-server-not-found"));
 388                if (pingWorthyError) {
 389                    Conversation conversation = mXmppConnectionService.find(account, from);
 390                    if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) {
 391                        if (conversation.getMucOptions().online()) {
 392                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ping worthy error for seemingly online muc at " + from);
 393                            mXmppConnectionService.mucSelfPingAndRejoin(conversation);
 394                        }
 395                    }
 396                }
 397            }
 398            return true;
 399        }
 400        return false;
 401    }
 402
 403    @Override
 404    public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
 405        if (handleErrorMessage(account, original)) {
 406            return;
 407        }
 408        final im.conversations.android.xmpp.model.stanza.Message packet;
 409        Long timestamp = null;
 410        boolean isCarbon = false;
 411        String serverMsgId = null;
 412        final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
 413        if (fin != null) {
 414            mXmppConnectionService.getMessageArchiveService().processFinLegacy(fin, original.getFrom());
 415            return;
 416        }
 417        final Element result = MessageArchiveService.Version.findResult(original);
 418        final String queryId = result == null ? null : result.getAttribute("queryid");
 419        final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
 420        final boolean offlineMessagesRetrieved = account.getXmppConnection().isOfflineMessagesRetrieved();
 421        if (query != null && query.validFrom(original.getFrom())) {
 422            final var f = getForwardedMessagePacket(original,"result", query.version.namespace);
 423            if (f == null) {
 424                return;
 425            }
 426            timestamp = f.second;
 427            packet = f.first;
 428            serverMsgId = result.getAttribute("id");
 429            query.incrementMessageCount();
 430            if (handleErrorMessage(account, packet)) {
 431                return;
 432            }
 433        } else if (query != null) {
 434            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from (" + original.getFrom() + ") or queryId (" + queryId + ")");
 435            return;
 436        } else if (original.fromServer(account)
 437                && original.getType() != im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT) {
 438            Pair<im.conversations.android.xmpp.model.stanza.Message, Long> f;
 439            f = getForwardedMessagePacket(original, Received.class);
 440            f = f == null ? getForwardedMessagePacket(original, Sent.class) : f;
 441            packet = f != null ? f.first : original;
 442            if (handleErrorMessage(account, packet)) {
 443                return;
 444            }
 445            timestamp = f != null ? f.second : null;
 446            isCarbon = f != null;
 447        } else {
 448            packet = original;
 449        }
 450
 451        if (timestamp == null) {
 452            timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
 453        }
 454        final Reactions reactions = packet.getExtension(Reactions.class);
 455        final LocalizedContent body = packet.getBody();
 456        final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
 457        final boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
 458        final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
 459        final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
 460        final Element oob = packet.findChild("x", Namespace.OOB);
 461        final String oobUrl = oob != null ? oob.findChildContent("url") : null;
 462        final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
 463        final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
 464        int status;
 465        final Jid counterpart;
 466        final Jid to = packet.getTo();
 467        final Jid from = packet.getFrom();
 468        final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
 469        final String remoteMsgId;
 470        if (originId != null && originId.getAttribute("id") != null) {
 471            remoteMsgId = originId.getAttribute("id");
 472        } else {
 473            remoteMsgId = packet.getId();
 474        }
 475        boolean notify = false;
 476
 477        if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) {
 478            Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
 479            return;
 480        }
 481        if (query != null && !query.muc() && isTypeGroupChat) {
 482            Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
 483            return;
 484        }
 485        final Jid mucTrueCounterPart;
 486        final OccupantId occupant;
 487        if (isTypeGroupChat) {
 488            final Conversation conversation =
 489                    mXmppConnectionService.find(account, from.asBareJid());
 490            final Jid mucTrueCounterPartByPresence;
 491            if (conversation != null) {
 492                final var mucOptions = conversation.getMucOptions();
 493                occupant = mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null;
 494                final var user =
 495                        occupant == null ? null : mucOptions.findUserByOccupantId(occupant.getId());
 496                mucTrueCounterPartByPresence = user == null ? null : user.getRealJid();
 497            } else {
 498                occupant = null;
 499                mucTrueCounterPartByPresence = null;
 500            }
 501            mucTrueCounterPart =
 502                    getTrueCounterpart(
 503                            (query != null && query.safeToExtractTrueCounterpart())
 504                                    ? mucUserElement
 505                                    : null,
 506                            mucTrueCounterPartByPresence);
 507        } else {
 508            mucTrueCounterPart = null;
 509            occupant = null;
 510        }
 511        boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
 512        boolean selfAddressed;
 513        if (packet.fromAccount(account)) {
 514            status = Message.STATUS_SEND;
 515            selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
 516            if (selfAddressed) {
 517                counterpart = from;
 518            } else {
 519                counterpart = to;
 520            }
 521        } else {
 522            status = Message.STATUS_RECEIVED;
 523            counterpart = from;
 524            selfAddressed = false;
 525        }
 526
 527        final Invite invite = extractInvite(packet);
 528        if (invite != null) {
 529            if (invite.jid.asBareJid().equals(account.getJid().asBareJid())) {
 530                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite to "+invite.jid+" because it matches account");
 531            } else if (isTypeGroupChat) {
 532                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because it was received as group chat");
 533            } else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) {
 534                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring direct invite to " + invite.jid + " because it was received in MUC");
 535            } else {
 536                invite.execute(account);
 537                return;
 538            }
 539        }
 540
 541        if (reactions == null && (body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) {
 542            final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
 543            final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
 544            final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
 545
 546            if (serverMsgId == null) {
 547                serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
 548            }
 549
 550
 551            if (selfAddressed) {
 552                if (mXmppConnectionService.markMessage(conversation, remoteMsgId, Message.STATUS_SEND_RECEIVED, serverMsgId)) {
 553                    return;
 554                }
 555                status = Message.STATUS_RECEIVED;
 556                if (remoteMsgId != null && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
 557                    return;
 558                }
 559            }
 560
 561            if (isTypeGroupChat) {
 562                if (conversation.getMucOptions().isSelf(counterpart)) {
 563                    status = Message.STATUS_SEND_RECEIVED;
 564                    isCarbon = true; //not really carbon but received from another resource
 565                    if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId, body)) {
 566                        return;
 567                    } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
 568                        if (body != null) {
 569                            Message message = conversation.findSentMessageWithBody(body.content);
 570                            if (message != null) {
 571                                mXmppConnectionService.markMessage(message, status);
 572                                return;
 573                            }
 574                        }
 575                    }
 576                } else {
 577                    status = Message.STATUS_RECEIVED;
 578                }
 579            }
 580            final Message message;
 581            if (pgpEncrypted != null && Config.supportOpenPgp()) {
 582                message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
 583            } else if (axolotlEncrypted != null && Config.supportOmemo()) {
 584                Jid origin;
 585                Set<Jid> fallbacksBySourceId = Collections.emptySet();
 586                if (conversationMultiMode) {
 587                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
 588                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
 589                    if (origin == null) {
 590                        try {
 591                            fallbacksBySourceId = account.getAxolotlService().findCounterpartsBySourceId(XmppAxolotlMessage.parseSourceId(axolotlEncrypted));
 592                        } catch (IllegalArgumentException e) {
 593                            //ignoring
 594                        }
 595                    }
 596                    if (origin == null && fallbacksBySourceId.size() == 0) {
 597                        Log.d(Config.LOGTAG, "axolotl message in anonymous conference received and no possible fallbacks");
 598                        return;
 599                    }
 600                } else {
 601                    fallbacksBySourceId = Collections.emptySet();
 602                    origin = from;
 603                }
 604
 605                final boolean liveMessage = query == null && !isTypeGroupChat && mucUserElement == null;
 606                final boolean checkedForDuplicates = liveMessage || (serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId));
 607
 608                if (origin != null) {
 609                    message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, checkedForDuplicates, query != null);
 610                } else {
 611                    Message trial = null;
 612                    for (Jid fallback : fallbacksBySourceId) {
 613                        trial = parseAxolotlChat(axolotlEncrypted, fallback, conversation, status, checkedForDuplicates && fallbacksBySourceId.size() == 1, query != null);
 614                        if (trial != null) {
 615                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": decoded muc message using fallback");
 616                            origin = fallback;
 617                            break;
 618                        }
 619                    }
 620                    message = trial;
 621                }
 622                if (message == null) {
 623                    if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
 624                        mXmppConnectionService.updateConversationUi();
 625                    }
 626                    if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
 627                        Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
 628                        if (previouslySent != null && previouslySent.getServerMsgId() == null && serverMsgId != null) {
 629                            previouslySent.setServerMsgId(serverMsgId);
 630                            mXmppConnectionService.databaseBackend.updateMessage(previouslySent, false);
 631                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered previously sent OMEMO message without serverId. updating...");
 632                        }
 633                    }
 634                    return;
 635                }
 636                if (conversationMultiMode) {
 637                    message.setTrueCounterpart(origin);
 638                }
 639            } else if (body == null && oobUrl != null) {
 640                message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
 641                message.setOob(true);
 642                if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
 643                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 644                }
 645            } else {
 646                message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
 647                if (body.count > 1) {
 648                    message.setBodyLanguage(body.language);
 649                }
 650            }
 651
 652            message.setCounterpart(counterpart);
 653            message.setRemoteMsgId(remoteMsgId);
 654            message.setServerMsgId(serverMsgId);
 655            message.setCarbon(isCarbon);
 656            message.setTime(timestamp);
 657            if (body != null && body.content != null && body.content.equals(oobUrl)) {
 658                message.setOob(true);
 659                if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
 660                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 661                }
 662            }
 663            message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
 664            if (conversationMultiMode) {
 665                final var mucOptions = conversation.getMucOptions();
 666                if (occupant != null) {
 667                    message.setOccupantId(occupant.getId());
 668                }
 669                message.setMucUser(mucOptions.findUserByFullJid(counterpart));
 670                final Jid fallback = mucOptions.getTrueCounterpart(counterpart);
 671                Jid trueCounterpart;
 672                if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 673                    trueCounterpart = message.getTrueCounterpart();
 674                } else if (query != null && query.safeToExtractTrueCounterpart()) {
 675                    trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
 676                } else {
 677                    trueCounterpart = fallback;
 678                }
 679                if (trueCounterpart != null && isTypeGroupChat) {
 680                    if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
 681                        status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
 682                    } else {
 683                        status = Message.STATUS_RECEIVED;
 684                        message.setCarbon(false);
 685                    }
 686                }
 687                message.setStatus(status);
 688                message.setTrueCounterpart(trueCounterpart);
 689                if (!isTypeGroupChat) {
 690                    message.setType(Message.TYPE_PRIVATE);
 691                }
 692            } else {
 693                updateLastseen(account, from);
 694            }
 695
 696            if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
 697                final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
 698                        counterpart,
 699                        message.getStatus() == Message.STATUS_RECEIVED,
 700                        message.isCarbon());
 701                if (replacedMessage != null) {
 702                    final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
 703                            || replacedMessage.getFingerprint().equals(message.getFingerprint());
 704                    final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
 705                            && message.getTrueCounterpart() != null
 706                            && replacedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid());
 707                    final boolean occupantIdMatch =
 708                            replacedMessage.getOccupantId() != null
 709                                    && replacedMessage
 710                                            .getOccupantId()
 711                                            .equals(message.getOccupantId());
 712                    final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
 713                    final boolean duplicate = conversation.hasDuplicateMessage(message);
 714                    if (fingerprintsMatch && (trueCountersMatch || occupantIdMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
 715                        synchronized (replacedMessage) {
 716                            final String uuid = replacedMessage.getUuid();
 717                            replacedMessage.setUuid(UUID.randomUUID().toString());
 718                            replacedMessage.setBody(message.getBody());
 719                            replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
 720                            replacedMessage.setRemoteMsgId(remoteMsgId);
 721                            if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
 722                                replacedMessage.setServerMsgId(message.getServerMsgId());
 723                            }
 724                            replacedMessage.setEncryption(message.getEncryption());
 725                            if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
 726                                replacedMessage.markUnread();
 727                            }
 728                            extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
 729                            mXmppConnectionService.updateMessage(replacedMessage, uuid);
 730                            if (mXmppConnectionService.confirmMessages()
 731                                    && replacedMessage.getStatus() == Message.STATUS_RECEIVED
 732                                    && (replacedMessage.trusted() || replacedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs?
 733                                    && remoteMsgId != null
 734                                    && !selfAddressed
 735                                    && !isTypeGroupChat) {
 736                                processMessageReceipts(account, packet, remoteMsgId, query);
 737                            }
 738                            if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
 739                                conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
 740                                conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
 741                            }
 742                        }
 743                        mXmppConnectionService.getNotificationService().updateNotification();
 744                        return;
 745                    } else {
 746                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
 747                    }
 748                }
 749            }
 750
 751            long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
 752            if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
 753                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
 754                return;
 755            }
 756
 757            boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
 758                    || message.isPrivateMessage()
 759                    || message.getServerMsgId() != null
 760                    || (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation));
 761            if (checkForDuplicates) {
 762                final Message duplicate = conversation.findDuplicateMessage(message);
 763                if (duplicate != null) {
 764                    final boolean serverMsgIdUpdated;
 765                    if (duplicate.getStatus() != Message.STATUS_RECEIVED
 766                            && duplicate.getUuid().equals(message.getRemoteMsgId())
 767                            && duplicate.getServerMsgId() == null
 768                            && message.getServerMsgId() != null) {
 769                        duplicate.setServerMsgId(message.getServerMsgId());
 770                        if (mXmppConnectionService.databaseBackend.updateMessage(duplicate, false)) {
 771                            serverMsgIdUpdated = true;
 772                        } else {
 773                            serverMsgIdUpdated = false;
 774                            Log.e(Config.LOGTAG, "failed to update message");
 775                        }
 776                    } else {
 777                        serverMsgIdUpdated = false;
 778                    }
 779                    Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + serverMsgIdUpdated);
 780                    return;
 781                }
 782            }
 783
 784            if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
 785                conversation.prepend(query.getActualInThisQuery(), message);
 786            } else {
 787                conversation.add(message);
 788            }
 789            if (query != null) {
 790                query.incrementActualMessageCount();
 791            }
 792
 793            if (query == null || query.isCatchup()) { //either no mam or catchup
 794                if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
 795                    mXmppConnectionService.markRead(conversation);
 796                    if (query == null) {
 797                        activateGracePeriod(account);
 798                    }
 799                } else {
 800                    message.markUnread();
 801                    notify = true;
 802                }
 803            }
 804
 805            if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 806                notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
 807            } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
 808                notify = false;
 809            }
 810
 811            if (query == null) {
 812                extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
 813                mXmppConnectionService.updateConversationUi();
 814            }
 815
 816            if (mXmppConnectionService.confirmMessages()
 817                    && message.getStatus() == Message.STATUS_RECEIVED
 818                    && (message.trusted() || message.isPrivateMessage())
 819                    && remoteMsgId != null
 820                    && !selfAddressed
 821                    && !isTypeGroupChat) {
 822                processMessageReceipts(account, packet, remoteMsgId, query);
 823            }
 824
 825            mXmppConnectionService.databaseBackend.createMessage(message);
 826            final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
 827            if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
 828                manager.createNewDownloadConnection(message);
 829            } else if (notify) {
 830                if (query != null && query.isCatchup()) {
 831                    mXmppConnectionService.getNotificationService().pushFromBacklog(message);
 832                } else {
 833                    mXmppConnectionService.getNotificationService().push(message);
 834                }
 835            }
 836        } else if (!packet.hasChild("body")) { //no body
 837
 838            final Conversation conversation = mXmppConnectionService.find(account, from.asBareJid());
 839            if (axolotlEncrypted != null) {
 840                Jid origin;
 841                if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
 842                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
 843                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
 844                    if (origin == null) {
 845                        Log.d(Config.LOGTAG, "omemo key transport message in anonymous conference received");
 846                        return;
 847                    }
 848                } else if (isTypeGroupChat) {
 849                    return;
 850                } else {
 851                    origin = from;
 852                }
 853                try {
 854                    final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
 855                    account.getAxolotlService().processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
 856                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": omemo key transport message received from " + origin);
 857                } catch (Exception e) {
 858                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": invalid omemo key transport message received " + e.getMessage());
 859                    return;
 860                }
 861            }
 862
 863            if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
 864                mXmppConnectionService.updateConversationUi();
 865            }
 866
 867            if (isTypeGroupChat) {
 868                if (packet.hasChild("subject") && !packet.hasChild("thread")) { // We already know it has no body per above
 869                    if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
 870                        conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
 871                        final LocalizedContent subject = packet.findInternationalizedChildContentInDefaultNamespace("subject");
 872                        if (subject != null && conversation.getMucOptions().setSubject(subject.content)) {
 873                            mXmppConnectionService.updateConversation(conversation);
 874                        }
 875                        mXmppConnectionService.updateConversationUi();
 876                        return;
 877                    }
 878                }
 879            }
 880            if (conversation != null && mucUserElement != null && InvalidJid.hasValidFrom(packet) && from.isBareJid()) {
 881                for (Element child : mucUserElement.getChildren()) {
 882                    if ("status".equals(child.getName())) {
 883                        try {
 884                            int code = Integer.parseInt(child.getAttribute("code"));
 885                            if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
 886                                mXmppConnectionService.fetchConferenceConfiguration(conversation);
 887                                break;
 888                            }
 889                        } catch (Exception e) {
 890                            //ignored
 891                        }
 892                    } else if ("item".equals(child.getName())) {
 893                        MucOptions.User user = AbstractParser.parseItem(conversation, child);
 894                        Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for "
 895                                + user.getRealJid() + " to " + user.getAffiliation() + " in "
 896                                + conversation.getJid().asBareJid());
 897                        if (!user.realJidMatchesAccount()) {
 898                            boolean isNew = conversation.getMucOptions().updateUser(user);
 899                            mXmppConnectionService.getAvatarService().clear(conversation);
 900                            mXmppConnectionService.updateMucRosterUi();
 901                            mXmppConnectionService.updateConversationUi();
 902                            Contact contact = user.getContact();
 903                            if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
 904                                Jid jid = user.getRealJid();
 905                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
 906                                if (cryptoTargets.remove(user.getRealJid())) {
 907                                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
 908                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
 909                                    mXmppConnectionService.updateConversation(conversation);
 910                                }
 911                            } else if (isNew
 912                                    && user.getRealJid() != null
 913                                    && conversation.getMucOptions().isPrivateAndNonAnonymous()
 914                                    && (contact == null || !contact.mutualPresenceSubscription())
 915                                    && account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) {
 916                                account.getAxolotlService().fetchDeviceIds(user.getRealJid());
 917                            }
 918                        }
 919                    }
 920                }
 921            }
 922            if (!isTypeGroupChat) {
 923                for (Element child : packet.getChildren()) {
 924                    if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
 925                        final String action = child.getName();
 926                        final String sessionId = child.getAttribute("id");
 927                        if (sessionId == null) {
 928                            break;
 929                        }
 930                        if (query == null && offlineMessagesRetrieved) {
 931                            if (serverMsgId == null) {
 932                                serverMsgId = extractStanzaId(account, packet);
 933                            }
 934                            mXmppConnectionService
 935                                    .getJingleConnectionManager()
 936                                    .deliverMessage(
 937                                            account,
 938                                            packet.getTo(),
 939                                            packet.getFrom(),
 940                                            child,
 941                                            remoteMsgId,
 942                                            serverMsgId,
 943                                            timestamp);
 944                            final Contact contact = account.getRoster().getContact(from);
 945                            // this is the same condition that is found in JingleRtpConnection for
 946                            // the 'ringing' response. Responding with delivery receipts predates
 947                            // the 'ringing' spec'd
 948                            final boolean sendReceipts =
 949                                    contact.showInContactList()
 950                                            || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
 951                            if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
 952                                processMessageReceipts(account, packet, remoteMsgId, null);
 953                            }
 954                        } else if ((query != null && query.isCatchup()) || !offlineMessagesRetrieved) {
 955                            if ("propose".equals(action)) {
 956                                final Element description = child.findChild("description");
 957                                final String namespace =
 958                                        description == null ? null : description.getNamespace();
 959                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
 960                                    final Conversation c =
 961                                            mXmppConnectionService.findOrCreateConversation(
 962                                                    account, counterpart.asBareJid(), false, false);
 963                                    final Message preExistingMessage =
 964                                            c.findRtpSession(sessionId, status);
 965                                    if (preExistingMessage != null) {
 966                                        preExistingMessage.setServerMsgId(serverMsgId);
 967                                        mXmppConnectionService.updateMessage(preExistingMessage);
 968                                        break;
 969                                    }
 970                                    final Message message =
 971                                            new Message(
 972                                                    c, status, Message.TYPE_RTP_SESSION, sessionId);
 973                                    message.setServerMsgId(serverMsgId);
 974                                    message.setTime(timestamp);
 975                                    message.setBody(new RtpSessionStatus(false, 0).toString());
 976                                    c.add(message);
 977                                    mXmppConnectionService.databaseBackend.createMessage(message);
 978                                }
 979                            } else if ("proceed".equals(action)) {
 980                                // status needs to be flipped to find the original propose
 981                                final Conversation c =
 982                                        mXmppConnectionService.findOrCreateConversation(
 983                                                account, counterpart.asBareJid(), false, false);
 984                                final int s =
 985                                        packet.fromAccount(account)
 986                                                ? Message.STATUS_RECEIVED
 987                                                : Message.STATUS_SEND;
 988                                final Message message = c.findRtpSession(sessionId, s);
 989                                if (message != null) {
 990                                    message.setBody(new RtpSessionStatus(true, 0).toString());
 991                                    if (serverMsgId != null) {
 992                                        message.setServerMsgId(serverMsgId);
 993                                    }
 994                                    message.setTime(timestamp);
 995                                    mXmppConnectionService.updateMessage(message, true);
 996                                } else {
 997                                    Log.d(
 998                                            Config.LOGTAG,
 999                                            "unable to find original rtp session message for received propose");
1000                                }
1001
1002                            } else if ("finish".equals(action)) {
1003                                Log.d(
1004                                        Config.LOGTAG,
1005                                        "received JMI 'finish' during MAM catch-up. Can be used to update success/failure and duration");
1006                            }
1007                        } else {
1008                            //MAM reloads (non catchups
1009                            if ("propose".equals(action)) {
1010                                final Element description = child.findChild("description");
1011                                final String namespace = description == null ? null : description.getNamespace();
1012                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1013                                    final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false);
1014                                    final Message preExistingMessage = c.findRtpSession(sessionId, status);
1015                                    if (preExistingMessage != null) {
1016                                        preExistingMessage.setServerMsgId(serverMsgId);
1017                                        mXmppConnectionService.updateMessage(preExistingMessage);
1018                                        break;
1019                                    }
1020                                    final Message message = new Message(
1021                                            c,
1022                                            status,
1023                                            Message.TYPE_RTP_SESSION,
1024                                            sessionId
1025                                    );
1026                                    message.setServerMsgId(serverMsgId);
1027                                    message.setTime(timestamp);
1028                                    message.setBody(new RtpSessionStatus(true, 0).toString());
1029                                    if (query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
1030                                        c.prepend(query.getActualInThisQuery(), message);
1031                                    } else {
1032                                        c.add(message);
1033                                    }
1034                                    query.incrementActualMessageCount();
1035                                    mXmppConnectionService.databaseBackend.createMessage(message);
1036                                }
1037                            }
1038                        }
1039                        break;
1040                    }
1041                }
1042            }
1043        }
1044
1045        Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
1046        if (received == null) {
1047            received = packet.findChild("received", "urn:xmpp:receipts");
1048        }
1049        if (received != null) {
1050            String id = received.getAttribute("id");
1051            if (packet.fromAccount(account)) {
1052                if (query != null && id != null && packet.getTo() != null) {
1053                    query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1054                }
1055            } else if (id != null) {
1056                if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1057                    final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1058                    mXmppConnectionService.getJingleConnectionManager()
1059                            .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1060                } else {
1061                    mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1062                }
1063            }
1064        }
1065        final Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
1066        if (displayed != null) {
1067            final String id = displayed.getAttribute("id");
1068            // TODO we don’t even use 'sender' any more. Remove this!
1069            final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1070            if (packet.fromAccount(account) && !selfAddressed) {
1071                final Conversation c =
1072                        mXmppConnectionService.find(account, counterpart.asBareJid());
1073                final Message message =
1074                        (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1075                if (message != null && (query == null || query.isCatchup())) {
1076                    mXmppConnectionService.markReadUpTo(c, message);
1077                }
1078                if (query == null) {
1079                    activateGracePeriod(account);
1080                }
1081            } else if (isTypeGroupChat) {
1082                final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
1083                final Message message;
1084                if (conversation != null && id != null) {
1085                    if (sender != null) {
1086                        message = conversation.findMessageWithRemoteId(id, sender);
1087                    } else {
1088                        message = conversation.findMessageWithServerMsgId(id);
1089                    }
1090                } else {
1091                    message = null;
1092                }
1093                if (message != null) {
1094                    // TODO use occupantId to extract true counterpart from presence
1095                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1096                    // TODO try to externalize mucTrueCounterpart
1097                    final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
1098                    final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
1099                    if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1100                        if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
1101                            mXmppConnectionService.markReadUpTo(conversation, message);
1102                        }
1103                    } else if (!counterpart.isBareJid() && trueJid != null) {
1104                        final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1105                        if (message.addReadByMarker(readByMarker)) {
1106                            final var mucOptions = conversation.getMucOptions();
1107                            final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1108                            final var readyBy = message.getReadyByTrue();
1109                            final var mStatus = message.getStatus();
1110                            if (mucOptions.isPrivateAndNonAnonymous()
1111                                    && (mStatus == Message.STATUS_SEND_RECEIVED
1112                                            || mStatus == Message.STATUS_SEND)
1113                                    && readyBy.containsAll(everyone)) {
1114                                message.setStatus(Message.STATUS_SEND_DISPLAYED);
1115                            }
1116                            mXmppConnectionService.updateMessage(message, false);
1117                        }
1118                    }
1119                }
1120            } else {
1121                final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1122                Message message = displayedMessage == null ? null : displayedMessage.prev();
1123                while (message != null
1124                        && message.getStatus() == Message.STATUS_SEND_RECEIVED
1125                        && message.getTimeSent() < displayedMessage.getTimeSent()) {
1126                    mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1127                    message = message.prev();
1128                }
1129                if (displayedMessage != null && selfAddressed) {
1130                    dismissNotification(account, counterpart, query, id);
1131                }
1132            }
1133        }
1134
1135        if (reactions != null) {
1136            final String reactingTo = reactions.getId();
1137            final Conversation conversation =
1138                    mXmppConnectionService.find(account, counterpart.asBareJid());
1139
1140            if (conversation != null && reactingTo != null) {
1141                if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1142                    final var mucOptions = conversation.getMucOptions();
1143                    final var occupantId = occupant == null ? null : occupant.getId();
1144                    if (occupantId != null) {
1145                        final boolean isReceived = !mucOptions.isSelf(occupantId);
1146                        final Message message;
1147                        final var inMemoryMessage =
1148                                conversation.findMessageWithServerMsgId(reactingTo);
1149                        if (inMemoryMessage != null) {
1150                            message = inMemoryMessage;
1151                        } else {
1152                            message =
1153                                    mXmppConnectionService.databaseBackend
1154                                            .getMessageWithServerMsgId(conversation, reactingTo);
1155                        }
1156                        if (message != null) {
1157                            final var combinedReactions =
1158                                    Reaction.withOccupantId(
1159                                            message.getReactions(),
1160                                            reactions.getReactions(),
1161                                            isReceived,
1162                                            counterpart,
1163                                            mucTrueCounterPart,
1164                                            occupantId);
1165                            message.setReactions(combinedReactions);
1166                            mXmppConnectionService.updateMessage(message, false);
1167                        } else {
1168                            Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1169                        }
1170                    } else {
1171                        Log.d(
1172                                Config.LOGTAG,
1173                                "received reaction in channel w/o occupant ids. ignoring");
1174                    }
1175                } else if (conversation.getMode() == Conversational.MODE_SINGLE) {
1176                    final Message message;
1177                    final var inMemoryMessage =
1178                            conversation.findMessageWithUuidOrRemoteId(reactingTo);
1179                    if (inMemoryMessage != null) {
1180                        message = inMemoryMessage;
1181                    } else {
1182                        message =
1183                                mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1184                                        conversation, reactingTo);
1185                    }
1186                    final boolean isReceived;
1187                    final Jid reactionFrom;
1188                    if (packet.fromAccount(account)) {
1189                        isReceived = false;
1190                        reactionFrom = account.getJid().asBareJid();
1191                    } else {
1192                        isReceived = true;
1193                        reactionFrom = counterpart;
1194                    }
1195                    packet.fromAccount(account);
1196                    if (message != null) {
1197                        final var combinedReactions =
1198                                Reaction.withFrom(
1199                                        message.getReactions(),
1200                                        reactions.getReactions(),
1201                                        isReceived,
1202                                        reactionFrom);
1203                        message.setReactions(combinedReactions);
1204                        mXmppConnectionService.updateMessage(message, false);
1205                    } else {
1206                        Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1207                    }
1208                }
1209            }
1210        }
1211
1212        final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
1213        if (event != null && InvalidJid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1214            if (event.hasChild("items")) {
1215                parseEvent(event, original.getFrom(), account);
1216            } else if (event.hasChild("delete")) {
1217                parseDeleteEvent(event, original.getFrom(), account);
1218            } else if (event.hasChild("purge")) {
1219                parsePurgeEvent(event, original.getFrom(), account);
1220            }
1221        }
1222
1223        final String nick = packet.findChildContent("nick", Namespace.NICK);
1224        if (nick != null && InvalidJid.hasValidFrom(original)) {
1225            if (mXmppConnectionService.isMuc(account, from)) {
1226                return;
1227            }
1228            final Contact contact = account.getRoster().getContact(from);
1229            if (contact.setPresenceName(nick)) {
1230                mXmppConnectionService.syncRoster(account);
1231                mXmppConnectionService.getAvatarService().clear(contact);
1232            }
1233        }
1234    }
1235
1236    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) {
1237        final var extension = original.getExtension(clazz);
1238        final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1239        if (forwarded == null) {
1240            return null;
1241        }
1242        final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1243        final var forwardedMessage = forwarded.getMessage();
1244        if (forwardedMessage == null) {
1245            return null;
1246        }
1247        return new Pair<>(forwardedMessage,timestamp);
1248    }
1249
1250    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) {
1251        final Element wrapper = original.findChild(name, namespace);
1252        final var forwardedElement = wrapper == null ? null : wrapper.findChild("forwarded",Namespace.FORWARD);
1253        if (forwardedElement instanceof Forwarded forwarded) {
1254            final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1255            final var forwardedMessage = forwarded.getMessage();
1256            if (forwardedMessage == null) {
1257                return null;
1258            }
1259            return new Pair<>(forwardedMessage,timestamp);
1260        }
1261        return null;
1262    }
1263
1264    private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1265        final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
1266        if (conversation != null && (query == null || query.isCatchup())) {
1267            final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1268            if (displayableId != null && displayableId.equals(id)) {
1269                mXmppConnectionService.markRead(conversation);
1270            } else {
1271                Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": received dismissing display marker that did not match our last id in that conversation");
1272            }
1273        }
1274    }
1275
1276    private void processMessageReceipts(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet, final String remoteMsgId, MessageArchiveService.Query query) {
1277        final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
1278        final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
1279        if (query == null) {
1280            final ArrayList<String> receiptsNamespaces = new ArrayList<>();
1281            if (markable) {
1282                receiptsNamespaces.add("urn:xmpp:chat-markers:0");
1283            }
1284            if (request) {
1285                receiptsNamespaces.add("urn:xmpp:receipts");
1286            }
1287            if (receiptsNamespaces.size() > 0) {
1288                final var receipt = mXmppConnectionService.getMessageGenerator().received(account,
1289                        packet.getFrom(),
1290                        remoteMsgId,
1291                        receiptsNamespaces,
1292                        packet.getType());
1293                mXmppConnectionService.sendMessagePacket(account, receipt);
1294            }
1295        } else if (query.isCatchup()) {
1296            if (request) {
1297                query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1298            }
1299        }
1300    }
1301
1302    private void activateGracePeriod(Account account) {
1303        long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000;
1304        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1305        account.activateGracePeriod(duration);
1306    }
1307
1308    private class Invite {
1309        final Jid jid;
1310        final String password;
1311        final boolean direct;
1312        final Jid inviter;
1313
1314        Invite(Jid jid, String password, boolean direct, Jid inviter) {
1315            this.jid = jid;
1316            this.password = password;
1317            this.direct = direct;
1318            this.inviter = inviter;
1319        }
1320
1321        public boolean execute(final Account account) {
1322            if (this.jid == null) {
1323                return false;
1324            }
1325            final Contact contact =
1326                    this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1327            if (contact != null && contact.isBlocked()) {
1328                Log.d(
1329                        Config.LOGTAG,
1330                        account.getJid().asBareJid()
1331                                + ": ignore invite from "
1332                                + contact.getJid()
1333                                + " because contact is blocked");
1334                return false;
1335            }
1336            final AppSettings appSettings = new AppSettings(mXmppConnectionService);
1337            if ((contact != null && contact.showInContactList())
1338                    || appSettings.isAcceptInvitesFromStrangers()) {
1339                final Conversation conversation =
1340                        mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1341                if (conversation.getMucOptions().online()) {
1342                    Log.d(
1343                            Config.LOGTAG,
1344                            account.getJid().asBareJid()
1345                                    + ": received invite to "
1346                                    + jid
1347                                    + " but muc is considered to be online");
1348                    mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1349                } else {
1350                    conversation.getMucOptions().setPassword(password);
1351                    mXmppConnectionService.databaseBackend.updateConversation(conversation);
1352                    mXmppConnectionService.joinMuc(
1353                            conversation, contact != null && contact.showInContactList());
1354                    mXmppConnectionService.updateConversationUi();
1355                }
1356                return true;
1357            } else {
1358                Log.d(
1359                        Config.LOGTAG,
1360                        account.getJid().asBareJid()
1361                                + ": ignoring invite from "
1362                                + this.inviter
1363                                + " because we are not accepting invites from strangers. direct="
1364                                + direct);
1365                return false;
1366            }
1367        }
1368    }
1369}