MessageParser.java

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