MessageParser.java

   1package eu.siacs.conversations.parser;
   2
   3import android.net.Uri;
   4import android.util.Log;
   5import android.util.Pair;
   6
   7import com.cheogram.android.BobTransfer;
   8import com.cheogram.android.WebxdcUpdate;
   9
  10import androidx.annotation.NonNull;
  11import com.google.common.base.Strings;
  12import com.google.common.collect.ImmutableSet;
  13
  14import java.io.File;
  15import java.net.URISyntaxException;
  16import java.text.SimpleDateFormat;
  17import java.util.ArrayList;
  18import java.util.Arrays;
  19import java.util.Collections;
  20import java.util.Date;
  21import java.util.HashSet;
  22import java.util.HashMap;
  23import java.util.LinkedHashSet;
  24import java.util.List;
  25import java.util.Locale;
  26import java.util.Map;
  27import java.util.Set;
  28import java.util.UUID;
  29import java.util.function.Consumer;
  30import java.util.stream.Collectors;
  31
  32import io.ipfs.cid.Cid;
  33
  34import eu.siacs.conversations.AppSettings;
  35import eu.siacs.conversations.Config;
  36import eu.siacs.conversations.R;
  37import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  38import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
  39import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
  40import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException;
  41import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
  42import eu.siacs.conversations.entities.Account;
  43import eu.siacs.conversations.entities.Bookmark;
  44import eu.siacs.conversations.entities.Contact;
  45import eu.siacs.conversations.entities.Conversation;
  46import eu.siacs.conversations.entities.Conversational;
  47import eu.siacs.conversations.entities.DownloadableFile;
  48import eu.siacs.conversations.entities.Message;
  49import eu.siacs.conversations.entities.MucOptions;
  50import eu.siacs.conversations.entities.Reaction;
  51import eu.siacs.conversations.entities.ReadByMarker;
  52import eu.siacs.conversations.entities.ReceiptRequest;
  53import eu.siacs.conversations.entities.RtpSessionStatus;
  54import eu.siacs.conversations.http.HttpConnectionManager;
  55import eu.siacs.conversations.services.MessageArchiveService;
  56import eu.siacs.conversations.services.QuickConversationsService;
  57import eu.siacs.conversations.services.XmppConnectionService;
  58import eu.siacs.conversations.utils.CryptoHelper;
  59import eu.siacs.conversations.utils.Emoticons;
  60import eu.siacs.conversations.xml.Element;
  61import eu.siacs.conversations.xml.LocalizedContent;
  62import eu.siacs.conversations.xml.Namespace;
  63import eu.siacs.conversations.xmpp.Jid;
  64import eu.siacs.conversations.xmpp.chatstate.ChatState;
  65import eu.siacs.conversations.xmpp.forms.Data;
  66import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  67import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
  68import eu.siacs.conversations.xmpp.pep.Avatar;
  69import im.conversations.android.xmpp.model.Extension;
  70import im.conversations.android.xmpp.model.avatar.Metadata;
  71import im.conversations.android.xmpp.model.axolotl.DeviceList;
  72import im.conversations.android.xmpp.model.axolotl.Encrypted;
  73import im.conversations.android.xmpp.model.bookmark.Storage;
  74import im.conversations.android.xmpp.model.bookmark2.Conference;
  75import im.conversations.android.xmpp.model.carbons.Received;
  76import im.conversations.android.xmpp.model.carbons.Sent;
  77import im.conversations.android.xmpp.model.correction.Replace;
  78import im.conversations.android.xmpp.model.forward.Forwarded;
  79import im.conversations.android.xmpp.model.markers.Displayed;
  80import im.conversations.android.xmpp.model.nick.Nick;
  81import im.conversations.android.xmpp.model.occupant.OccupantId;
  82import im.conversations.android.xmpp.model.oob.OutOfBandData;
  83import im.conversations.android.xmpp.model.pubsub.Items;
  84import im.conversations.android.xmpp.model.pubsub.event.Delete;
  85import im.conversations.android.xmpp.model.pubsub.event.Event;
  86import im.conversations.android.xmpp.model.pubsub.event.Purge;
  87import im.conversations.android.xmpp.model.reactions.Reactions;
  88import im.conversations.android.xmpp.model.receipts.Request;
  89import im.conversations.android.xmpp.model.unique.StanzaId;
  90import java.text.SimpleDateFormat;
  91import java.util.Arrays;
  92import java.util.Collections;
  93import java.util.Date;
  94import java.util.HashSet;
  95import java.util.List;
  96import java.util.Locale;
  97import java.util.Map;
  98import java.util.Set;
  99import java.util.UUID;
 100import java.util.function.Consumer;
 101
 102public class MessageParser extends AbstractParser
 103        implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
 104
 105    private static final SimpleDateFormat TIME_FORMAT =
 106            new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
 107
 108    private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
 109            Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
 110
 111    public MessageParser(final XmppConnectionService service, final Account account) {
 112        super(service, account);
 113    }
 114
 115    private static String extractStanzaId(
 116            final im.conversations.android.xmpp.model.stanza.Message packet,
 117            final boolean isTypeGroupChat,
 118            final Conversation conversation) {
 119        final Jid by;
 120        final boolean safeToExtract;
 121        if (isTypeGroupChat) {
 122            by = conversation.getJid().asBareJid();
 123            safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
 124        } else {
 125            Account account = conversation.getAccount();
 126            by = account.getJid().asBareJid();
 127            safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
 128        }
 129        return safeToExtract ? StanzaId.get(packet, by) : null;
 130    }
 131
 132    private static String extractStanzaId(
 133            final Account account,
 134            final im.conversations.android.xmpp.model.stanza.Message packet) {
 135        final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
 136        return safeToExtract ? StanzaId.get(packet, account.getJid().asBareJid()) : null;
 137    }
 138
 139    private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
 140        final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
 141        Jid result =
 142                item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
 143        return result != null ? result : fallback;
 144    }
 145
 146    private boolean extractChatState(
 147            Conversation c,
 148            final boolean isTypeGroupChat,
 149            final im.conversations.android.xmpp.model.stanza.Message packet) {
 150        ChatState state = ChatState.parse(packet);
 151        if (state != null && c != null) {
 152            final Account account = c.getAccount();
 153            final Jid from = packet.getFrom();
 154            if (from.asBareJid().equals(account.getJid().asBareJid())) {
 155                c.setOutgoingChatState(state);
 156                if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
 157                    if (c.getContact().isSelf()) {
 158                        return false;
 159                    }
 160                    mXmppConnectionService.markRead(c);
 161                    activateGracePeriod(account);
 162                }
 163                return false;
 164            } else {
 165                if (isTypeGroupChat) {
 166                    MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
 167                    if (user != null) {
 168                        return user.setChatState(state);
 169                    } else {
 170                        return false;
 171                    }
 172                } else {
 173                    return c.setIncomingChatState(state);
 174                }
 175            }
 176        }
 177        return false;
 178    }
 179
 180    private Message parseAxolotlChat(
 181            final Encrypted axolotlMessage,
 182            final Jid from,
 183            final Conversation conversation,
 184            final int status,
 185            final boolean checkedForDuplicates,
 186            final boolean postpone) {
 187        final AxolotlService service = conversation.getAccount().getAxolotlService();
 188        final XmppAxolotlMessage xmppAxolotlMessage;
 189        try {
 190            xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
 191        } catch (final Exception e) {
 192            Log.d(
 193                    Config.LOGTAG,
 194                    conversation.getAccount().getJid().asBareJid()
 195                            + ": invalid omemo message received "
 196                            + e.getMessage());
 197            return null;
 198        }
 199        if (xmppAxolotlMessage.hasPayload()) {
 200            final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
 201            try {
 202                plaintextMessage =
 203                        service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
 204            } catch (BrokenSessionException e) {
 205                if (checkedForDuplicates) {
 206                    if (service.trustedOrPreviouslyResponded(from.asBareJid())) {
 207                        service.reportBrokenSessionException(e, postpone);
 208                        return new Message(
 209                                conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
 210                    } else {
 211                        Log.d(
 212                                Config.LOGTAG,
 213                                "ignoring broken session exception because contact was not"
 214                                        + " trusted");
 215                        return new Message(
 216                                conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
 217                    }
 218                } else {
 219                    Log.d(
 220                            Config.LOGTAG,
 221                            "ignoring broken session exception because checkForDuplicates failed");
 222                    return null;
 223                }
 224            } catch (NotEncryptedForThisDeviceException e) {
 225                return new Message(
 226                        conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
 227            } catch (OutdatedSenderException e) {
 228                return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
 229            }
 230            if (plaintextMessage != null) {
 231                Message finishedMessage =
 232                        new Message(
 233                                conversation,
 234                                plaintextMessage.getPlaintext(),
 235                                Message.ENCRYPTION_AXOLOTL,
 236                                status);
 237                finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
 238                Log.d(
 239                        Config.LOGTAG,
 240                        AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())
 241                                + " Received Message with session fingerprint: "
 242                                + plaintextMessage.getFingerprint());
 243                return finishedMessage;
 244            }
 245        } else {
 246            Log.d(
 247                    Config.LOGTAG,
 248                    conversation.getAccount().getJid().asBareJid()
 249                            + ": received OMEMO key transport message");
 250            service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
 251        }
 252        return null;
 253    }
 254
 255    private Invite extractInvite(final Element message) {
 256        final Element mucUser = message.findChild("x", Namespace.MUC_USER);
 257        if (mucUser != null) {
 258            final Element invite = mucUser.findChild("invite");
 259            if (invite != null) {
 260                final String password = mucUser.findChildContent("password");
 261                final Jid from = Jid.Invalid.getNullForInvalid(invite.getAttributeAsJid("from"));
 262                final Jid to = Jid.Invalid.getNullForInvalid(invite.getAttributeAsJid("to"));
 263                if (to != null && from == null) {
 264                    Log.d(Config.LOGTAG, "do not parse outgoing mediated invite " + message);
 265                    return null;
 266                }
 267                final Jid room = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
 268                if (room == null) {
 269                    return null;
 270                }
 271                return new Invite(room, password, false, from);
 272            }
 273        }
 274        final Element conference = message.findChild("x", "jabber:x:conference");
 275        if (conference != null) {
 276            Jid from = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
 277            Jid room = Jid.Invalid.getNullForInvalid(conference.getAttributeAsJid("jid"));
 278            if (room == null) {
 279                return null;
 280            }
 281            return new Invite(room, conference.getAttribute("password"), true, from);
 282        }
 283        return null;
 284    }
 285
 286    private void parseEvent(final Items items, final Jid from, final Account account) {
 287        final String node = items.getNode();
 288        if ("urn:xmpp:avatar:metadata".equals(node)) {
 289            // TODO support retract
 290            final var entry = items.getFirstItemWithId(Metadata.class);
 291            final var avatar =
 292                    entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue());
 293            if (avatar != null) {
 294                avatar.owner = from.asBareJid();
 295                if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
 296                    if (account.getJid().asBareJid().equals(from)) {
 297                        if (account.setAvatar(avatar.getFilename())) {
 298                            mXmppConnectionService.databaseBackend.updateAccount(account);
 299                            mXmppConnectionService.notifyAccountAvatarHasChanged(account);
 300                        }
 301                        mXmppConnectionService.getAvatarService().clear(account);
 302                        mXmppConnectionService.updateConversationUi();
 303                        mXmppConnectionService.updateAccountUi();
 304                    } else {
 305                        final Contact contact = account.getRoster().getContact(from);
 306                        if (contact.setAvatar(avatar)) {
 307                            mXmppConnectionService.syncRoster(account);
 308                            mXmppConnectionService.getAvatarService().clear(contact);
 309                            mXmppConnectionService.updateConversationUi();
 310                            mXmppConnectionService.updateRosterUi(XmppConnectionService.UpdateRosterReason.AVATAR);
 311                        }
 312                    }
 313                } else if (mXmppConnectionService.isDataSaverDisabled()) {
 314                    mXmppConnectionService.fetchAvatar(account, avatar);
 315                }
 316            }
 317        } else if (Namespace.NICK.equals(node)) {
 318            final var nickItem = items.getFirstItem(Nick.class);
 319            final String nick = nickItem == null ? null : nickItem.getContent();
 320            if (nick != null) {
 321                setNick(account, from, nick);
 322            }
 323        } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
 324            final var deviceList = items.getFirstItem(DeviceList.class);
 325            if (deviceList != null) {
 326                final Set<Integer> deviceIds = deviceList.getDeviceIds();
 327                Log.d(
 328                        Config.LOGTAG,
 329                        AxolotlService.getLogprefix(account)
 330                                + "Received PEP device list "
 331                                + deviceIds
 332                                + " update from "
 333                                + from
 334                                + ", processing... ");
 335                final AxolotlService axolotlService = account.getAxolotlService();
 336                axolotlService.registerDevices(from, new HashSet<>(deviceIds));
 337            }
 338
 339        } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
 340            final var connection = account.getXmppConnection();
 341            if (connection.getFeatures().bookmarksConversion()) {
 342                if (connection.getFeatures().bookmarks2()) {
 343                    Log.w(
 344                            Config.LOGTAG,
 345                            account.getJid().asBareJid()
 346                                    + ": received storage:bookmark notification even though we"
 347                                    + " opted into bookmarks:1");
 348                }
 349                final var storage = items.getFirstItem(Storage.class);
 350                final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
 351                mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
 352                Log.d(
 353                        Config.LOGTAG,
 354                        account.getJid().asBareJid() + ": processing bookmark PEP event");
 355            } else {
 356                Log.d(
 357                        Config.LOGTAG,
 358                        account.getJid().asBareJid()
 359                                + ": ignoring bookmark PEP event because bookmark conversion was"
 360                                + " not detected");
 361            }
 362        } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
 363            final var retractions = items.getRetractions();
 364            for (final var item : items.getItemMap(Conference.class).entrySet()) {
 365                final Bookmark bookmark =
 366                        Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
 367                if (bookmark == null) {
 368                    continue;
 369                }
 370                account.putBookmark(bookmark);
 371                mXmppConnectionService.processModifiedBookmark(bookmark);
 372                mXmppConnectionService.updateConversationUi();
 373            }
 374            for (final var retract : retractions) {
 375                final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
 376                if (id != null) {
 377                    account.removeBookmark(id);
 378                    Log.d(
 379                            Config.LOGTAG,
 380                            account.getJid().asBareJid() + ": deleted bookmark for " + id);
 381                    mXmppConnectionService.processDeletedBookmark(account, id);
 382                    mXmppConnectionService.updateConversationUi();
 383                }
 384            }
 385        } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
 386                && Namespace.MDS_DISPLAYED.equals(node)
 387                && account.getJid().asBareJid().equals(from)) {
 388            for (final var item :
 389                    items.getItemMap(im.conversations.android.xmpp.model.mds.Displayed.class)
 390                            .entrySet()) {
 391                mXmppConnectionService.processMdsItem(account, item);
 392            }
 393        }
 394    }
 395
 396    private void parseDeleteEvent(final Delete delete, final Jid from, final Account account) {
 397        final String node = delete.getNode();
 398        if (Namespace.NICK.equals(node)) {
 399            setNick(account, from, null);
 400        } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
 401            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
 402            deleteAllBookmarks(account);
 403        } else if (Namespace.AVATAR_METADATA.equals(node)) {
 404            final boolean isAccount = account.getJid().asBareJid().equals(from);
 405            if (isAccount) {
 406                account.setAvatar(null);
 407                mXmppConnectionService.databaseBackend.updateAccount(account);
 408                mXmppConnectionService.getAvatarService().clear(account);
 409                Log.d(
 410                        Config.LOGTAG,
 411                        account.getJid().asBareJid() + ": deleted avatar metadata node");
 412            }
 413        }
 414    }
 415
 416    private void parsePurgeEvent(
 417            @NonNull final Purge purge, final Jid from, final Account account) {
 418        final String node = purge.getNode();
 419        if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
 420            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
 421            deleteAllBookmarks(account);
 422        }
 423    }
 424
 425    private void deleteAllBookmarks(final Account account) {
 426        final var previous = account.getBookmarkedJids();
 427        account.setBookmarks(Collections.emptyMap());
 428        mXmppConnectionService.processDeletedBookmarks(account, previous);
 429    }
 430
 431    private void setNick(final Account account, final Jid user, final String nick) {
 432        if (user.asBareJid().equals(account.getJid().asBareJid())) {
 433            account.setDisplayName(nick);
 434            if (QuickConversationsService.isQuicksy()) {
 435                mXmppConnectionService.getAvatarService().clear(account);
 436            }
 437            mXmppConnectionService.checkMucRequiresRename();
 438        } else {
 439            Contact contact = account.getRoster().getContact(user);
 440            if (contact.setPresenceName(nick)) {
 441                mXmppConnectionService.syncRoster(account);
 442                mXmppConnectionService.getAvatarService().clear(contact);
 443            }
 444        }
 445        mXmppConnectionService.updateConversationUi();
 446        mXmppConnectionService.updateAccountUi();
 447    }
 448
 449    private boolean handleErrorMessage(
 450            final Account account,
 451            final im.conversations.android.xmpp.model.stanza.Message packet) {
 452        if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) {
 453            if (packet.fromServer(account)) {
 454                final var forwarded =
 455                        getForwardedMessagePacket(packet, "received", Namespace.CARBONS);
 456                if (forwarded != null) {
 457                    return handleErrorMessage(account, forwarded.first);
 458                }
 459            }
 460            final Jid from = packet.getFrom();
 461            final String id = packet.getId();
 462            if (from != null && id != null) {
 463                if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
 464                    final String sessionId =
 465                            id.substring(
 466                                    JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
 467                    mXmppConnectionService
 468                            .getJingleConnectionManager()
 469                            .updateProposedSessionDiscovered(
 470                                    account,
 471                                    from,
 472                                    sessionId,
 473                                    JingleConnectionManager.DeviceDiscoveryState.FAILED);
 474                    return true;
 475                }
 476                if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
 477                    final String sessionId =
 478                            id.substring(
 479                                    JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
 480                    final String message = extractErrorMessage(packet);
 481                    mXmppConnectionService
 482                            .getJingleConnectionManager()
 483                            .failProceed(account, from, sessionId, message);
 484                    return true;
 485                }
 486                mXmppConnectionService.markMessage(
 487                        account,
 488                        from.asBareJid(),
 489                        id,
 490                        Message.STATUS_SEND_FAILED,
 491                        extractErrorMessage(packet));
 492                final Element error = packet.findChild("error");
 493                final boolean pingWorthyError =
 494                        error != null
 495                                && (error.hasChild("not-acceptable")
 496                                        || error.hasChild("remote-server-timeout")
 497                                        || error.hasChild("remote-server-not-found"));
 498                if (pingWorthyError) {
 499                    Conversation conversation = mXmppConnectionService.find(account, from);
 500                    if (conversation != null
 501                            && conversation.getMode() == Conversational.MODE_MULTI) {
 502                        if (conversation.getMucOptions().online()) {
 503                            Log.d(
 504                                    Config.LOGTAG,
 505                                    account.getJid().asBareJid()
 506                                            + ": received ping worthy error for seemingly online"
 507                                            + " muc at "
 508                                            + from);
 509                            mXmppConnectionService.mucSelfPingAndRejoin(conversation);
 510                        }
 511                    }
 512                }
 513            }
 514            return true;
 515        }
 516        return false;
 517    }
 518
 519    @Override
 520    public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
 521        if (handleErrorMessage(account, original)) {
 522            return;
 523        }
 524        final im.conversations.android.xmpp.model.stanza.Message packet;
 525        Long timestamp = null;
 526        boolean isCarbon = false;
 527        String serverMsgId = null;
 528        final Element fin =
 529                original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
 530        if (fin != null) {
 531            mXmppConnectionService
 532                    .getMessageArchiveService()
 533                    .processFinLegacy(fin, original.getFrom());
 534            return;
 535        }
 536        final Element result = MessageArchiveService.Version.findResult(original);
 537        final String queryId = result == null ? null : result.getAttribute("queryid");
 538        final MessageArchiveService.Query query =
 539                queryId == null
 540                        ? null
 541                        : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
 542        final boolean offlineMessagesRetrieved =
 543                account.getXmppConnection().isOfflineMessagesRetrieved();
 544        if (query != null && query.validFrom(original.getFrom())) {
 545            final var f = getForwardedMessagePacket(original, "result", query.version.namespace);
 546            if (f == null) {
 547                return;
 548            }
 549            timestamp = f.second;
 550            packet = f.first;
 551            serverMsgId = result.getAttribute("id");
 552            query.incrementMessageCount();
 553            if (handleErrorMessage(account, packet)) {
 554                return;
 555            }
 556            final var contact = packet.getFrom() == null || packet.getFrom() instanceof Jid.Invalid ? null : account.getRoster().getContact(packet.getFrom());
 557            if (contact != null && contact.isBlocked()) {
 558                Log.d(Config.LOGTAG, "Got MAM result from blocked contact, ignoring...");
 559                return;
 560            }
 561        } else if (query != null) {
 562            Log.d(
 563                    Config.LOGTAG,
 564                    account.getJid().asBareJid()
 565                            + ": received mam result with invalid from ("
 566                            + original.getFrom()
 567                            + ") or queryId ("
 568                            + queryId
 569                            + ")");
 570            return;
 571        } else if (original.fromServer(account)
 572                && original.getType()
 573                        != im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT) {
 574            Pair<im.conversations.android.xmpp.model.stanza.Message, Long> f;
 575            f = getForwardedMessagePacket(original, Received.class);
 576            f = f == null ? getForwardedMessagePacket(original, Sent.class) : f;
 577            packet = f != null ? f.first : original;
 578            if (handleErrorMessage(account, packet)) {
 579                return;
 580            }
 581            timestamp = f != null ? f.second : null;
 582            isCarbon = f != null;
 583        } else {
 584            packet = original;
 585        }
 586
 587        if (timestamp == null) {
 588            timestamp =
 589                    AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
 590        }
 591
 592        final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
 593        final boolean isTypeGroupChat =
 594                packet.getType()
 595                        == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
 596        final var encrypted =
 597                packet.getOnlyExtension(im.conversations.android.xmpp.model.pgp.Encrypted.class);
 598        final String pgpEncrypted = encrypted == null ? null : encrypted.getContent();
 599
 600        Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
 601        Set<Message.FileParams> attachments = new LinkedHashSet<>();
 602        for (Element child : packet.getChildren()) {
 603            // SIMS first so they get preference in the set
 604            if (child.getName().equals("reference") && child.getNamespace().equals("urn:xmpp:reference:0")) {
 605                if (child.findChild("media-sharing", "urn:xmpp:sims:1") != null) {
 606                    attachments.add(new Message.FileParams(child));
 607                }
 608            }
 609        }
 610        for (Element child : packet.getChildren()) {
 611            if (child.getName().equals("x") && child.getNamespace().equals(Namespace.OOB)) {
 612                attachments.add(new Message.FileParams(child));
 613            }
 614        }
 615        String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
 616        if (replacementId == null) {
 617            final Element fasten = packet.findChild("apply-to", "urn:xmpp:fasten:0");
 618            if (fasten != null) {
 619                replaceElement = fasten.findChild("retract", "urn:xmpp:message-retract:0");
 620                if (replaceElement == null) replaceElement = fasten.findChild("moderated", "urn:xmpp:message-moderate:0");
 621            }
 622            if (replaceElement == null) replaceElement = packet.findChild("retract", "urn:xmpp:message-retract:1");
 623            if (replaceElement == null) replaceElement = packet.findChild("moderate", "urn:xmpp:message-moderate:1");
 624            if (replaceElement != null) {
 625                var reason = replaceElement.findChildContent("reason", "urn:xmpp:message-moderate:0");
 626                if (reason == null) reason = replaceElement.findChildContent("reason", "urn:xmpp:message-moderate:1");
 627                replacementId = (fasten == null ? replaceElement : fasten).getAttribute("id");
 628                packet.setBody(reason == null ? "" : reason);
 629            }
 630        }
 631        LocalizedContent body = packet.getBody();
 632
 633        final var reactions = packet.getExtension(Reactions.class);
 634
 635        final var oob = packet.getExtension(OutOfBandData.class);
 636        final String oobUrl = oob != null ? oob.getURL() : null;
 637        final var axolotlEncrypted = packet.getOnlyExtension(Encrypted.class);
 638        int status;
 639        final Jid counterpart;
 640        final Jid to = packet.getTo();
 641        final Jid from = packet.getFrom();
 642        final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
 643        final String remoteMsgId;
 644        if (originId != null && originId.getAttribute("id") != null) {
 645            remoteMsgId = originId.getAttribute("id");
 646        } else {
 647            remoteMsgId = packet.getId();
 648        }
 649        boolean notify = false;
 650
 651        Element html = packet.findChild("html", "http://jabber.org/protocol/xhtml-im");
 652        if (html != null && html.findChild("body", "http://www.w3.org/1999/xhtml") == null) {
 653            html = null;
 654        }
 655
 656        if (from == null || !Jid.Invalid.isValid(from) || !Jid.Invalid.isValid(to)) {
 657            Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
 658            return;
 659        }
 660        if (query != null && !query.muc() && isTypeGroupChat) {
 661            Log.e(
 662                    Config.LOGTAG,
 663                    account.getJid().asBareJid()
 664                            + ": received groupchat ("
 665                            + from
 666                            + ") message on regular MAM request. skipping");
 667            return;
 668        }
 669        final Jid mucTrueCounterPart;
 670        final OccupantId occupant;
 671        if (isTypeGroupChat) {
 672            final Conversation conversation =
 673                    mXmppConnectionService.find(account, from.asBareJid());
 674            final Jid mucTrueCounterPartByPresence;
 675            if (conversation != null) {
 676                final var mucOptions = conversation.getMucOptions();
 677                occupant =
 678                        mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null;
 679                final var user =
 680                        occupant == null ? null : mucOptions.findUserByOccupantId(occupant.getId(), from);
 681                mucTrueCounterPartByPresence = user == null ? null : user.getRealJid();
 682            } else {
 683                occupant = null;
 684                mucTrueCounterPartByPresence = null;
 685            }
 686            mucTrueCounterPart =
 687                    getTrueCounterpart(
 688                            (query != null && query.safeToExtractTrueCounterpart())
 689                                    ? mucUserElement
 690                                    : null,
 691                            mucTrueCounterPartByPresence);
 692        } else if (mucUserElement != null) {
 693            final Conversation conversation =
 694                    mXmppConnectionService.find(account, from.asBareJid());
 695            if (conversation != null) {
 696                final var mucOptions = conversation.getMucOptions();
 697                occupant =
 698                        mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null;
 699            } else {
 700                occupant = null;
 701            }
 702            mucTrueCounterPart = null;
 703        } else {
 704            mucTrueCounterPart = null;
 705            occupant = null;
 706        }
 707        boolean isMucStatusMessage =
 708                Jid.Invalid.hasValidFrom(packet)
 709                        && from.isBareJid()
 710                        && mucUserElement != null
 711                        && mucUserElement.hasChild("status");
 712        boolean selfAddressed;
 713        if (packet.fromAccount(account)) {
 714            status = Message.STATUS_SEND;
 715            selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
 716            if (selfAddressed) {
 717                counterpart = from;
 718            } else {
 719                counterpart = to != null ? to : account.getJid();
 720            }
 721        } else {
 722            status = Message.STATUS_RECEIVED;
 723            counterpart = from;
 724            selfAddressed = false;
 725        }
 726
 727        final Invite invite = extractInvite(packet);
 728        if (invite != null) {
 729            if (invite.jid.asBareJid().equals(account.getJid().asBareJid())) {
 730                Log.d(
 731                        Config.LOGTAG,
 732                        account.getJid().asBareJid()
 733                                + ": ignore invite to "
 734                                + invite.jid
 735                                + " because it matches account");
 736            } else if (isTypeGroupChat) {
 737                Log.d(
 738                        Config.LOGTAG,
 739                        account.getJid().asBareJid()
 740                                + ": ignoring invite to "
 741                                + invite.jid
 742                                + " because it was received as group chat");
 743            } else if (invite.direct
 744                    && (mucUserElement != null
 745                            || invite.inviter == null
 746                            || mXmppConnectionService.isMuc(account, invite.inviter))) {
 747                Log.d(
 748                        Config.LOGTAG,
 749                        account.getJid().asBareJid()
 750                                + ": ignoring direct invite to "
 751                                + invite.jid
 752                                + " because it was received in MUC");
 753            } else {
 754                invite.execute(account);
 755                return;
 756            }
 757        }
 758
 759        final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toString());
 760        final Element webxdc = packet.findChild("x", "urn:xmpp:webxdc:0");
 761        final Element thread = packet.findChild("thread");
 762        if (webxdc != null && thread != null) {
 763            final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
 764            Jid webxdcSender = counterpart.asBareJid();
 765            if (conversation.getMode() == Conversation.MODE_MULTI) {
 766                if(conversation.getMucOptions().nonanonymous()) {
 767                    webxdcSender = conversation.getMucOptions().getTrueCounterpart(counterpart);
 768                } else {
 769                    webxdcSender = counterpart;
 770                }
 771            }
 772            final var document = webxdc.findChildContent("document", "urn:xmpp:webxdc:0");
 773            final var summary = webxdc.findChildContent("summary", "urn:xmpp:webxdc:0");
 774            final var payload = webxdc.findChildContent("json", "urn:xmpp:json:0");
 775            if (document != null || summary != null || payload != null) {
 776                mXmppConnectionService.insertWebxdcUpdate(new WebxdcUpdate(
 777                    conversation,
 778                    remoteMsgId,
 779                    counterpart,
 780                    thread,
 781                    body == null ? null : body.content,
 782                    document,
 783                    summary,
 784                    payload
 785                ));
 786            }
 787
 788            final var realtime = webxdc.findChildContent("data", "urn:xmpp:webxdc:0");
 789            if (realtime != null) conversation.webxdcRealtimeData(thread, realtime);
 790
 791            mXmppConnectionService.updateConversationUi();
 792        }
 793
 794        // Basic visibility for voice requests
 795        if (body == null && html == null && pgpEncrypted == null && axolotlEncrypted == null && !isMucStatusMessage) {
 796            final Element formEl = packet.findChild("x", "jabber:x:data");
 797            if (formEl != null) {
 798                final Data form = Data.parse(formEl);
 799                final String role = form.getValue("muc#role");
 800                final String nick = form.getValue("muc#roomnick");
 801                if ("http://jabber.org/protocol/muc#request".equals(form.getFormType()) && "participant".equals(role)) {
 802                    body = new LocalizedContent("" + nick + " is requesting to speak", "en", 1);
 803                }
 804            }
 805        }
 806
 807        if (reactions == null && (body != null
 808                        || pgpEncrypted != null
 809                        || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload"))
 810                        || !attachments.isEmpty() || html != null || (packet.hasChild("subject") && packet.hasChild("thread")))
 811                && !isMucStatusMessage) {
 812            final Conversation conversation =
 813                    mXmppConnectionService.findOrCreateConversation(
 814                            account,
 815                            counterpart.asBareJid(),
 816                            conversationIsProbablyMuc,
 817                            false,
 818                            query,
 819                            false);
 820            final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
 821
 822            if (serverMsgId == null) {
 823                serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
 824            }
 825
 826            if (selfAddressed) {
 827                // don’t store serverMsgId on reflections for edits
 828                final var reflectedServerMsgId =
 829                        Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
 830                if (mXmppConnectionService.markMessage(
 831                        conversation,
 832                        remoteMsgId,
 833                        Message.STATUS_SEND_RECEIVED,
 834                        reflectedServerMsgId)) {
 835                    return;
 836                }
 837                status = Message.STATUS_RECEIVED;
 838                if (remoteMsgId != null
 839                        && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
 840                    return;
 841                }
 842            }
 843
 844            if (isTypeGroupChat) {
 845                if (conversation.getMucOptions().isSelf(counterpart)) {
 846                    status = Message.STATUS_SEND_RECEIVED;
 847                    isCarbon = true; // not really carbon but received from another resource
 848                    // don’t store serverMsgId on reflections for edits
 849                    final var reflectedServerMsgId =
 850                            Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
 851                    if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, reflectedServerMsgId, body, html, packet.findChildContent("subject"), packet.findChild("thread"), attachments)) {
 852                        return;
 853                    } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
 854                        if (body != null) {
 855                            Message message = conversation.findSentMessageWithBody(body.content);
 856                            if (message != null) {
 857                                mXmppConnectionService.markMessage(message, status);
 858                                return;
 859                            }
 860                        }
 861                    }
 862                } else {
 863                    status = Message.STATUS_RECEIVED;
 864                }
 865            }
 866            final Message message;
 867            if (pgpEncrypted != null && Config.supportOpenPgp()) {
 868                message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
 869            } else if (axolotlEncrypted != null && Config.supportOmemo()) {
 870                Jid origin;
 871                Set<Jid> fallbacksBySourceId = Collections.emptySet();
 872                if (conversationMultiMode) {
 873                    final Jid fallback =
 874                            conversation.getMucOptions().getTrueCounterpart(counterpart);
 875                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
 876                    if (origin == null) {
 877                        try {
 878                            fallbacksBySourceId =
 879                                    account.getAxolotlService()
 880                                            .findCounterpartsBySourceId(
 881                                                    XmppAxolotlMessage.parseSourceId(
 882                                                            axolotlEncrypted));
 883                        } catch (IllegalArgumentException e) {
 884                            // ignoring
 885                        }
 886                    }
 887                    if (origin == null && fallbacksBySourceId.isEmpty()) {
 888                        Log.d(
 889                                Config.LOGTAG,
 890                                "axolotl message in anonymous conference received and no possible"
 891                                        + " fallbacks");
 892                        return;
 893                    }
 894                } else {
 895                    fallbacksBySourceId = Collections.emptySet();
 896                    origin = from;
 897                }
 898
 899                final boolean liveMessage =
 900                        query == null && !isTypeGroupChat && mucUserElement == null;
 901                final boolean checkedForDuplicates =
 902                        liveMessage
 903                                || (serverMsgId != null
 904                                        && remoteMsgId != null
 905                                        && !conversation.possibleDuplicate(
 906                                                serverMsgId, remoteMsgId));
 907
 908                if (origin != null) {
 909                    message =
 910                            parseAxolotlChat(
 911                                    axolotlEncrypted,
 912                                    origin,
 913                                    conversation,
 914                                    status,
 915                                    checkedForDuplicates,
 916                                    query != null);
 917                } else {
 918                    Message trial = null;
 919                    for (Jid fallback : fallbacksBySourceId) {
 920                        trial =
 921                                parseAxolotlChat(
 922                                        axolotlEncrypted,
 923                                        fallback,
 924                                        conversation,
 925                                        status,
 926                                        checkedForDuplicates && fallbacksBySourceId.size() == 1,
 927                                        query != null);
 928                        if (trial != null) {
 929                            Log.d(
 930                                    Config.LOGTAG,
 931                                    account.getJid().asBareJid()
 932                                            + ": decoded muc message using fallback");
 933                            origin = fallback;
 934                            break;
 935                        }
 936                    }
 937                    message = trial;
 938                }
 939                if (message == null) {
 940                    if (query == null
 941                            && extractChatState(
 942                                    mXmppConnectionService.find(account, counterpart.asBareJid()),
 943                                    isTypeGroupChat,
 944                                    packet)) {
 945                        mXmppConnectionService.updateConversationUi();
 946                    }
 947                    if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
 948                        Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
 949                        if (previouslySent != null
 950                                && previouslySent.getServerMsgId() == null
 951                                && serverMsgId != null) {
 952                            previouslySent.setServerMsgId(serverMsgId);
 953                            mXmppConnectionService.databaseBackend.updateMessage(
 954                                    previouslySent, false);
 955                            Log.d(
 956                                    Config.LOGTAG,
 957                                    account.getJid().asBareJid()
 958                                            + ": encountered previously sent OMEMO message without"
 959                                            + " serverId. updating...");
 960                        }
 961                    }
 962                    return;
 963                }
 964                if (conversationMultiMode) {
 965                    message.setTrueCounterpart(origin);
 966                }
 967            } else if (body == null && !attachments.isEmpty()) {
 968                message = new Message(conversation, "", Message.ENCRYPTION_NONE, status);
 969            } else {
 970                message = new Message(conversation, body == null ? null : body.content, Message.ENCRYPTION_NONE, status);
 971                if (body != null && body.count > 1) {
 972                    message.setBodyLanguage(body.language);
 973                }
 974            }
 975
 976            Element addresses = packet.findChild("addresses", "http://jabber.org/protocol/address");
 977            if (status == Message.STATUS_RECEIVED && addresses != null) {
 978                for (Element address : addresses.getChildren()) {
 979                    if (!address.getName().equals("address") || !address.getNamespace().equals("http://jabber.org/protocol/address")) continue;
 980
 981                    if (address.getAttribute("type").equals("ofrom") && address.getAttribute("jid") != null) {
 982                        Jid ofrom = address.getAttributeAsJid("jid");
 983                        if (Jid.Invalid.isValid(ofrom) && ofrom.getDomain().equals(counterpart.getDomain()) &&
 984                            conversation.getAccount().getRoster().getContact(counterpart.getDomain()).getPresences().anySupport("http://jabber.org/protocol/address")) {
 985
 986                            message.setTrueCounterpart(ofrom);
 987                        }
 988                    }
 989                }
 990            }
 991
 992            if (html != null) message.addPayload(html);
 993            message.setSubject(packet.findChildContent("subject"));
 994            message.setCounterpart(counterpart);
 995            message.setRemoteMsgId(remoteMsgId);
 996            message.setServerMsgId(serverMsgId);
 997            message.setCarbon(isCarbon);
 998            message.setTime(timestamp);
 999            if (!attachments.isEmpty()) {
1000                message.setFileParams(attachments.iterator().next());
1001                if (CryptoHelper.isPgpEncryptedUrl(message.getFileParams().url)) {
1002                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1003                }
1004            }
1005            message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
1006            for (Element el : packet.getChildren()) {
1007                if ((el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) ||
1008                    (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0"))) {
1009                    message.addPayload(el);
1010                }
1011                if (el.getName().equals("thread") && (el.getNamespace() == null || el.getNamespace().equals("jabber:client"))) {
1012                    el.setAttribute("xmlns", "jabber:client");
1013                    message.addPayload(el);
1014                }
1015                if (el.getName().equals("reply") && el.getNamespace() != null && el.getNamespace().equals("urn:xmpp:reply:0")) {
1016                    message.addPayload(el);
1017                    if (el.getAttribute("id") != null) {
1018                        for (final var parent : mXmppConnectionService.getMessageFuzzyIds(conversation, List.of(el.getAttribute("id"))).entrySet()) {
1019                            message.setInReplyTo(parent.getValue());
1020                        }
1021                    }
1022                }
1023                if (el.getName().equals("attention") && el.getNamespace() != null && el.getNamespace().equals("urn:xmpp:attention:0")) {
1024                    message.addPayload(el);
1025                }
1026                if (el.getName().equals("Description") && el.getNamespace() != null && el.getNamespace().equals("http://www.w3.org/1999/02/22-rdf-syntax-ns#")) {
1027                    message.addPayload(el);
1028                }
1029            }
1030            if (conversationMultiMode) {
1031                final var mucOptions = conversation.getMucOptions();
1032                if (occupant != null) {
1033                    message.setOccupantId(occupant.getId());
1034                }
1035                message.setMucUser(mucOptions.findUserByFullJid(counterpart));
1036                final Jid fallback = mucOptions.getTrueCounterpart(counterpart);
1037                Jid trueCounterpart;
1038                if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
1039                    trueCounterpart = message.getTrueCounterpart();
1040                } else if (query != null && query.safeToExtractTrueCounterpart()) {
1041                    trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
1042                } else {
1043                    trueCounterpart = fallback;
1044                }
1045                if (trueCounterpart != null && isTypeGroupChat) {
1046                    if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
1047                        status =
1048                                isTypeGroupChat
1049                                        ? Message.STATUS_SEND_RECEIVED
1050                                        : Message.STATUS_SEND;
1051                    } else {
1052                        status = Message.STATUS_RECEIVED;
1053                        message.setCarbon(false);
1054                    }
1055                }
1056                message.setStatus(status);
1057                message.setTrueCounterpart(trueCounterpart);
1058                if (!isTypeGroupChat) {
1059                    message.setType(Message.TYPE_PRIVATE);
1060                }
1061            } else {
1062                updateLastseen(account, from);
1063            }
1064
1065            if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
1066                final Message replacedMessage =
1067                        conversation.findMessageWithRemoteIdAndCounterpart(
1068                                replacementId,
1069                                counterpart);
1070                if (replacedMessage != null) {
1071                    final boolean fingerprintsMatch =
1072                            replacedMessage.getFingerprint() == null
1073                                    || replacedMessage
1074                                            .getFingerprint()
1075                                            .equals(message.getFingerprint());
1076                    final boolean trueCountersMatch =
1077                            replacedMessage.getTrueCounterpart() != null
1078                                    && message.getTrueCounterpart() != null
1079                                    && replacedMessage
1080                                            .getTrueCounterpart()
1081                                            .asBareJid()
1082                                            .equals(message.getTrueCounterpart().asBareJid());
1083                    final boolean occupantIdMatch =
1084                            replacedMessage.getOccupantId() != null
1085                                    && replacedMessage
1086                                            .getOccupantId()
1087                                            .equals(message.getOccupantId());
1088                    final boolean mucUserMatches =
1089                            query == null
1090                                    && replacedMessage.sameMucUser(
1091                                            message); // can not be checked when using mam
1092                    final boolean duplicate = conversation.hasDuplicateMessage(message);
1093                    if (fingerprintsMatch && (trueCountersMatch || occupantIdMatch || !conversationMultiMode || mucUserMatches || counterpart.isBareJid()) && !duplicate) {
1094                        synchronized (replacedMessage) {
1095                            final String uuid = replacedMessage.getUuid();
1096                            replacedMessage.setUuid(UUID.randomUUID().toString());
1097                            replacedMessage.setBody(message.getBody());
1098                            replacedMessage.setSubject(message.getSubject());
1099                            replacedMessage.setThread(message.getThread());
1100                            replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
1101                            replacedMessage.setRemoteMsgId(remoteMsgId);
1102                            if (replaceElement != null && !replaceElement.getName().equals("replace")) {
1103                                mXmppConnectionService.getFileBackend().deleteFile(replacedMessage);
1104                                mXmppConnectionService.evictPreview(message.getUuid());
1105                                List<Element> thumbs = replacedMessage.getFileParams() != null ? replacedMessage.getFileParams().getThumbnails() : null;
1106                                if (thumbs != null && !thumbs.isEmpty()) {
1107                                    for (Element thumb : thumbs) {
1108                                        Uri uri = Uri.parse(thumb.getAttribute("uri"));
1109                                        if (uri.getScheme().equals("cid")) {
1110                                            Cid cid = BobTransfer.cid(uri);
1111                                            if (cid == null) continue;
1112                                            DownloadableFile f = mXmppConnectionService.getFileForCid(cid);
1113                                            if (f != null) {
1114                                                mXmppConnectionService.evictPreview(f);
1115                                                f.delete();
1116                                            }
1117                                        }
1118                                    }
1119                                }
1120                                replacedMessage.clearPayloads();
1121                                replacedMessage.setFileParams(null);
1122                                replacedMessage.addPayload(replaceElement);
1123                            } else {
1124                                replacedMessage.clearPayloads();
1125                                for (final var p : message.getPayloads()) {
1126                                    replacedMessage.addPayload(p);
1127                                }
1128                            }
1129                            replacedMessage.setInReplyTo(message.getInReplyTo());
1130
1131                            // we store the IDs of the replacing message. This is essentially unused
1132                            // today (only the fact that there are _some_ edits causes the edit icon
1133                            // to appear)
1134                            replacedMessage.putEdited(
1135                                    message.getRemoteMsgId(), message.getServerMsgId());
1136
1137                            // we used to call
1138                            // `replacedMessage.setServerMsgId(message.getServerMsgId());` so during
1139                            // catchup we could start from the edit; not the original message
1140                            // however this caused problems for things like reactions that refer to
1141                            // the serverMsgId
1142
1143                            replacedMessage.setEncryption(message.getEncryption());
1144                            if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
1145                                replacedMessage.markUnread();
1146                            }
1147                            extractChatState(
1148                                    mXmppConnectionService.find(account, counterpart.asBareJid()),
1149                                    isTypeGroupChat,
1150                                    packet);
1151                            mXmppConnectionService.updateMessage(replacedMessage, uuid);
1152                            if (mXmppConnectionService.confirmMessages()
1153                                    && replacedMessage.getStatus() == Message.STATUS_RECEIVED
1154                                    && (replacedMessage.trusted()
1155                                            || replacedMessage
1156                                                    .isPrivateMessage()) // TODO do we really want
1157                                    // to send receipts for all
1158                                    // PMs?
1159                                    && remoteMsgId != null
1160                                    && !selfAddressed
1161                                    && !isTypeGroupChat) {
1162                                processMessageReceipts(account, packet, remoteMsgId, query);
1163                            }
1164                            if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
1165                                conversation
1166                                        .getAccount()
1167                                        .getPgpDecryptionService()
1168                                        .discard(replacedMessage);
1169                                conversation
1170                                        .getAccount()
1171                                        .getPgpDecryptionService()
1172                                        .decrypt(replacedMessage, false);
1173                            }
1174                        }
1175                        mXmppConnectionService.getNotificationService().updateNotification();
1176                        return;
1177                    } else {
1178                        Log.d(
1179                                Config.LOGTAG,
1180                                account.getJid().asBareJid()
1181                                        + ": received message correction but verification didn't"
1182                                        + " check out");
1183                    }
1184                } else if (message.getBody() == null || message.getBody().equals("") || message.getBody().equals(" ")) {
1185                    return;
1186                }
1187                if (replaceElement != null && !replaceElement.getName().equals("replace")) return;
1188            }
1189
1190            boolean checkForDuplicates =
1191                    (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
1192                            || message.isPrivateMessage()
1193                            || message.getServerMsgId() != null
1194                            || (query == null
1195                                    && mXmppConnectionService
1196                                            .getMessageArchiveService()
1197                                            .isCatchupInProgress(conversation));
1198            if (checkForDuplicates) {
1199                final Message duplicate = conversation.findDuplicateMessage(message);
1200                if (duplicate != null) {
1201                    final boolean serverMsgIdUpdated;
1202                    if (duplicate.getStatus() != Message.STATUS_RECEIVED
1203                            && duplicate.getUuid().equals(message.getRemoteMsgId())
1204                            && duplicate.getServerMsgId() == null
1205                            && message.getServerMsgId() != null) {
1206                        duplicate.setServerMsgId(message.getServerMsgId());
1207                        if (mXmppConnectionService.databaseBackend.updateMessage(
1208                                duplicate, false)) {
1209                            serverMsgIdUpdated = true;
1210                        } else {
1211                            serverMsgIdUpdated = false;
1212                            Log.e(Config.LOGTAG, "failed to update message");
1213                        }
1214                    } else {
1215                        serverMsgIdUpdated = false;
1216                    }
1217                    Log.d(
1218                            Config.LOGTAG,
1219                            "skipping duplicate message with "
1220                                    + message.getCounterpart()
1221                                    + ". serverMsgIdUpdated="
1222                                    + serverMsgIdUpdated);
1223                    return;
1224                }
1225            }
1226
1227            if (query != null
1228                    && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
1229                conversation.prepend(query.getActualInThisQuery(), message);
1230            } else {
1231                conversation.add(message);
1232            }
1233            if (query != null) {
1234                query.incrementActualMessageCount();
1235            }
1236
1237            if (query == null || query.isCatchup()) { // either no mam or catchup
1238                if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
1239                    mXmppConnectionService.markRead(conversation);
1240                    if (query == null) {
1241                        activateGracePeriod(account);
1242                    }
1243                } else {
1244                    message.markUnread();
1245                    notify = true;
1246                }
1247            }
1248
1249            if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1250                notify =
1251                        conversation
1252                                .getAccount()
1253                                .getPgpDecryptionService()
1254                                .decrypt(message, notify);
1255            } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
1256                    || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1257                notify = false;
1258            }
1259
1260            if (query == null) {
1261                extractChatState(
1262                        mXmppConnectionService.find(account, counterpart.asBareJid()),
1263                        isTypeGroupChat,
1264                        packet);
1265                mXmppConnectionService.updateConversationUi();
1266            }
1267
1268            if (mXmppConnectionService.confirmMessages()
1269                    && message.getStatus() == Message.STATUS_RECEIVED
1270                    && (message.trusted() || message.isPrivateMessage())
1271                    && remoteMsgId != null
1272                    && !selfAddressed
1273                    && !isTypeGroupChat) {
1274                processMessageReceipts(account, packet, remoteMsgId, query);
1275            }
1276
1277            if (message.getFileParams() != null) {
1278                for (Cid cid : message.getFileParams().getCids()) {
1279                    File f = mXmppConnectionService.getFileForCid(cid);
1280                    if (f != null && f.canRead()) {
1281                        message.setRelativeFilePath(f.getAbsolutePath());
1282                        mXmppConnectionService.getFileBackend().updateFileParams(message, null, false);
1283                        break;
1284                    }
1285                }
1286            }
1287
1288            mXmppConnectionService.databaseBackend.createMessage(message);
1289            final HttpConnectionManager manager =
1290                    this.mXmppConnectionService.getHttpConnectionManager();
1291            if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
1292                if (message.getOob() != null && "cid".equalsIgnoreCase(message.getOob().getScheme())) {
1293                    try {
1294                        BobTransfer transfer = new BobTransfer.ForMessage(message, mXmppConnectionService);
1295                        message.setTransferable(transfer);
1296                        transfer.start();
1297                    } catch (URISyntaxException e) {
1298                        Log.d(Config.LOGTAG, "BobTransfer failed to parse URI");
1299                    }
1300                } else {
1301                    manager.createNewDownloadConnection(message);
1302                }
1303            } else if (notify) {
1304                if (query != null && query.isCatchup()) {
1305                    mXmppConnectionService.getNotificationService().pushFromBacklog(message);
1306                } else {
1307                    mXmppConnectionService.getNotificationService().push(message);
1308                }
1309            }
1310        } else if (!packet.hasChild("body")) { // no body
1311            final Conversation conversation =
1312                    mXmppConnectionService.find(account, from.asBareJid());
1313            if (axolotlEncrypted != null) {
1314                Jid origin;
1315                if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1316                    final Jid fallback =
1317                            conversation.getMucOptions().getTrueCounterpart(counterpart);
1318                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
1319                    if (origin == null) {
1320                        Log.d(
1321                                Config.LOGTAG,
1322                                "omemo key transport message in anonymous conference received");
1323                        return;
1324                    }
1325                } else if (isTypeGroupChat) {
1326                    return;
1327                } else {
1328                    origin = from;
1329                }
1330                try {
1331                    final XmppAxolotlMessage xmppAxolotlMessage =
1332                            XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
1333                    account.getAxolotlService()
1334                            .processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
1335                    Log.d(
1336                            Config.LOGTAG,
1337                            account.getJid().asBareJid()
1338                                    + ": omemo key transport message received from "
1339                                    + origin);
1340                } catch (Exception e) {
1341                    Log.d(
1342                            Config.LOGTAG,
1343                            account.getJid().asBareJid()
1344                                    + ": invalid omemo key transport message received "
1345                                    + e.getMessage());
1346                    return;
1347                }
1348            }
1349
1350            if (query == null
1351                    && extractChatState(
1352                            mXmppConnectionService.find(account, counterpart.asBareJid()),
1353                            isTypeGroupChat,
1354                            packet)) {
1355                mXmppConnectionService.updateConversationUi();
1356            }
1357
1358            if (isTypeGroupChat) {
1359                if (packet.hasChild("subject")
1360                        && !packet.hasChild("thread")) { // We already know it has no body per above
1361                    if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1362                        conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
1363                        final LocalizedContent subject = packet.getSubject();
1364                        if (subject != null
1365                                && conversation.getMucOptions().setSubject(subject.content)) {
1366                            mXmppConnectionService.updateConversation(conversation);
1367                        }
1368                        mXmppConnectionService.updateConversationUi();
1369                        return;
1370                    }
1371                }
1372            }
1373            if (conversation != null
1374                    && mucUserElement != null
1375                    && Jid.Invalid.hasValidFrom(packet)
1376                    && from.isBareJid()) {
1377                for (Element child : mucUserElement.getChildren()) {
1378                    if ("status".equals(child.getName())) {
1379                        try {
1380                            int code = Integer.parseInt(child.getAttribute("code"));
1381                            if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
1382                                mXmppConnectionService.fetchConferenceConfiguration(conversation);
1383                                break;
1384                            }
1385                        } catch (Exception e) {
1386                            // ignored
1387                        }
1388                    } else if ("item".equals(child.getName())) {
1389                        final var user = AbstractParser.parseItem(conversation, child);
1390                        Log.d(
1391                                Config.LOGTAG,
1392                                account.getJid()
1393                                        + ": changing affiliation for "
1394                                        + user.getRealJid()
1395                                        + " to "
1396                                        + user.getAffiliation()
1397                                        + " in "
1398                                        + conversation.getJid().asBareJid());
1399                        if (!user.realJidMatchesAccount()) {
1400                            final var mucOptions = conversation.getMucOptions();
1401                            final boolean isNew = mucOptions.updateUser(user);
1402                            final var avatarService = mXmppConnectionService.getAvatarService();
1403                            if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
1404                                avatarService.clear(mucOptions);
1405                            }
1406                            avatarService.clear(user);
1407                            mXmppConnectionService.updateMucRosterUi();
1408                            mXmppConnectionService.updateConversationUi();
1409                            Contact contact = user.getContact();
1410                            if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
1411                                Jid jid = user.getRealJid();
1412                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
1413                                if (cryptoTargets.remove(user.getRealJid())) {
1414                                    Log.d(
1415                                            Config.LOGTAG,
1416                                            account.getJid().asBareJid()
1417                                                    + ": removed "
1418                                                    + jid
1419                                                    + " from crypto targets of "
1420                                                    + conversation.getName());
1421                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
1422                                    mXmppConnectionService.updateConversation(conversation);
1423                                }
1424                            } else if (isNew
1425                                    && user.getRealJid() != null
1426                                    && conversation.getMucOptions().isPrivateAndNonAnonymous()
1427                                    && (contact == null || !contact.mutualPresenceSubscription())
1428                                    && account.getAxolotlService()
1429                                            .hasEmptyDeviceList(user.getRealJid())) {
1430                                account.getAxolotlService().fetchDeviceIds(user.getRealJid());
1431                            }
1432                        }
1433                    }
1434                }
1435            }
1436            if (!isTypeGroupChat) {
1437                for (Element child : packet.getChildren()) {
1438                    if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
1439                            && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
1440                        final String action = child.getName();
1441                        final String sessionId = child.getAttribute("id");
1442                        if (sessionId == null) {
1443                            break;
1444                        }
1445                        if (query == null && offlineMessagesRetrieved) {
1446                            if (serverMsgId == null) {
1447                                serverMsgId = extractStanzaId(account, packet);
1448                            }
1449                            mXmppConnectionService
1450                                    .getJingleConnectionManager()
1451                                    .deliverMessage(
1452                                            account,
1453                                            packet.getTo(),
1454                                            packet.getFrom(),
1455                                            child,
1456                                            remoteMsgId,
1457                                            serverMsgId,
1458                                            timestamp);
1459                            final Contact contact = account.getRoster().getContact(from);
1460                            // this is the same condition that is found in JingleRtpConnection for
1461                            // the 'ringing' response. Responding with delivery receipts predates
1462                            // the 'ringing' spec'd
1463                            final boolean sendReceipts =
1464                                    contact.showInContactList()
1465                                            || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
1466                            if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
1467                                processMessageReceipts(account, packet, remoteMsgId, null);
1468                            }
1469                        } else if ((query != null && query.isCatchup())
1470                                || !offlineMessagesRetrieved) {
1471                            if ("propose".equals(action)) {
1472                                final Element description = child.findChild("description");
1473                                final String namespace =
1474                                        description == null ? null : description.getNamespace();
1475                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1476                                    final Conversation c =
1477                                            mXmppConnectionService.findOrCreateConversation(
1478                                                    account, counterpart.asBareJid(), false, false);
1479                                    final Message preExistingMessage =
1480                                            c.findRtpSession(sessionId, status);
1481                                    if (preExistingMessage != null) {
1482                                        preExistingMessage.setServerMsgId(serverMsgId);
1483                                        mXmppConnectionService.updateMessage(preExistingMessage);
1484                                        break;
1485                                    }
1486                                    final Message message =
1487                                            new Message(
1488                                                    c, status, Message.TYPE_RTP_SESSION, sessionId);
1489                                    message.setServerMsgId(serverMsgId);
1490                                    message.setTime(timestamp);
1491                                    message.setBody(new RtpSessionStatus(false, 0).toString());
1492                                    message.markUnread();
1493                                    c.add(message);
1494                                    mXmppConnectionService.getNotificationService().possiblyMissedCall(c.getUuid() + sessionId, message);
1495                                    if (query != null) query.incrementActualMessageCount();
1496                                    mXmppConnectionService.databaseBackend.createMessage(message);
1497                                }
1498                            } else if ("proceed".equals(action)) {
1499                                // status needs to be flipped to find the original propose
1500                                final Conversation c =
1501                                        mXmppConnectionService.findOrCreateConversation(
1502                                                account, counterpart.asBareJid(), false, false);
1503                                final int s =
1504                                        packet.fromAccount(account)
1505                                                ? Message.STATUS_RECEIVED
1506                                                : Message.STATUS_SEND;
1507                                final Message message = c.findRtpSession(sessionId, s);
1508                                if (message != null) {
1509                                    message.setBody(new RtpSessionStatus(true, 0).toString());
1510                                    if (serverMsgId != null) {
1511                                        message.setServerMsgId(serverMsgId);
1512                                    }
1513                                    message.setTime(timestamp);
1514                                    message.markRead();
1515                                    mXmppConnectionService.getNotificationService().possiblyMissedCall(c.getUuid() + sessionId, message);
1516                                    if (query != null) query.incrementActualMessageCount();
1517                                    mXmppConnectionService.updateMessage(message, true);
1518                                } else {
1519                                    Log.d(
1520                                            Config.LOGTAG,
1521                                            "unable to find original rtp session message for"
1522                                                    + " received propose");
1523                                }
1524
1525                            } else if ("finish".equals(action)) {
1526                                Log.d(
1527                                        Config.LOGTAG,
1528                                        "received JMI 'finish' during MAM catch-up. Can be used to"
1529                                                + " update success/failure and duration");
1530                            }
1531                        } else {
1532                            // MAM reloads (non catchups
1533                            if ("propose".equals(action)) {
1534                                final Element description = child.findChild("description");
1535                                final String namespace =
1536                                        description == null ? null : description.getNamespace();
1537                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1538                                    final Conversation c =
1539                                            mXmppConnectionService.findOrCreateConversation(
1540                                                    account, counterpart.asBareJid(), false, false);
1541                                    final Message preExistingMessage =
1542                                            c.findRtpSession(sessionId, status);
1543                                    if (preExistingMessage != null) {
1544                                        preExistingMessage.setServerMsgId(serverMsgId);
1545                                        mXmppConnectionService.updateMessage(preExistingMessage);
1546                                        break;
1547                                    }
1548                                    final Message message =
1549                                            new Message(
1550                                                    c, status, Message.TYPE_RTP_SESSION, sessionId);
1551                                    message.setServerMsgId(serverMsgId);
1552                                    message.setTime(timestamp);
1553                                    message.setBody(new RtpSessionStatus(true, 0).toString());
1554                                    if (query.getPagingOrder()
1555                                            == MessageArchiveService.PagingOrder.REVERSE) {
1556                                        c.prepend(query.getActualInThisQuery(), message);
1557                                    } else {
1558                                        c.add(message);
1559                                    }
1560                                    query.incrementActualMessageCount();
1561                                    mXmppConnectionService.databaseBackend.createMessage(message);
1562                                }
1563                            }
1564                        }
1565                        break;
1566                    }
1567                }
1568            }
1569
1570            final var received =
1571                    packet.getExtension(
1572                            im.conversations.android.xmpp.model.receipts.Received.class);
1573            if (received != null) {
1574                processReceived(received, packet, query, from);
1575            }
1576            final var displayed = packet.getExtension(Displayed.class);
1577            if (displayed != null) {
1578                processDisplayed(
1579                        displayed,
1580                        packet,
1581                        selfAddressed,
1582                        counterpart,
1583                        query,
1584                        isTypeGroupChat,
1585                        conversation,
1586                        mucUserElement,
1587                        from);
1588            }
1589
1590            // end no body
1591        }
1592
1593        if (reactions != null) {
1594            processReactions(
1595                    reactions,
1596                    mXmppConnectionService.find(account, from.asBareJid()),
1597                    isTypeGroupChat,
1598                    occupant,
1599                    counterpart,
1600                    mucTrueCounterPart,
1601                    status,
1602                    packet);
1603        }
1604
1605        final var event = original.getExtension(Event.class);
1606        if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1607            final var action = event.getAction();
1608            final var node = action == null ? null : action.getNode();
1609            if (node == null) {
1610                Log.d(
1611                        Config.LOGTAG,
1612                        account.getJid().asBareJid()
1613                                + ": no node found in PubSub event from "
1614                                + original.getFrom());
1615            } else if (action instanceof Items items) {
1616                parseEvent(items, original.getFrom(), account);
1617            } else if (action instanceof Purge purge) {
1618                parsePurgeEvent(purge, original.getFrom(), account);
1619            } else if (action instanceof Delete delete) {
1620                parseDeleteEvent(delete, from, account);
1621            }
1622        }
1623
1624        final String nick = packet.findChildContent("nick", Namespace.NICK);
1625        if (nick != null && Jid.Invalid.hasValidFrom(original)) {
1626            if (mXmppConnectionService.isMuc(account, from)) {
1627                return;
1628            }
1629            final Contact contact = account.getRoster().getContact(from);
1630            if (contact.setPresenceName(nick)) {
1631                mXmppConnectionService.syncRoster(account);
1632                mXmppConnectionService.getAvatarService().clear(contact);
1633            }
1634        }
1635    }
1636
1637    private void processReceived(
1638            final im.conversations.android.xmpp.model.receipts.Received received,
1639            final im.conversations.android.xmpp.model.stanza.Message packet,
1640            final MessageArchiveService.Query query,
1641            final Jid from) {
1642        final var id = received.getId();
1643        if (packet.fromAccount(account)) {
1644            if (query != null && id != null && packet.getTo() != null) {
1645                query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1646            }
1647        } else if (id != null) {
1648            if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1649                final String sessionId =
1650                        id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1651                mXmppConnectionService
1652                        .getJingleConnectionManager()
1653                        .updateProposedSessionDiscovered(
1654                                account,
1655                                from,
1656                                sessionId,
1657                                JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1658            } else {
1659                mXmppConnectionService.markMessage(
1660                        account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1661            }
1662        }
1663    }
1664
1665    private void processDisplayed(
1666            final Displayed displayed,
1667            final im.conversations.android.xmpp.model.stanza.Message packet,
1668            final boolean selfAddressed,
1669            final Jid counterpart,
1670            final MessageArchiveService.Query query,
1671            final boolean isTypeGroupChat,
1672            Conversation conversation,
1673            Element mucUserElement,
1674            Jid from) {
1675        final var id = displayed.getId();
1676        // TODO we don’t even use 'sender' any more. Remove this!
1677        final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1678        if (packet.fromAccount(account) && !selfAddressed) {
1679            final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1680            final Message message =
1681                    (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1682            if (message != null && (query == null || query.isCatchup())) {
1683                mXmppConnectionService.markReadUpTo(c, message);
1684            }
1685            if (query == null) {
1686                activateGracePeriod(account);
1687            }
1688        } else if (isTypeGroupChat) {
1689            final Message message;
1690            if (conversation != null && id != null) {
1691                if (sender != null) {
1692                    message = conversation.findMessageWithRemoteId(id, sender);
1693                } else {
1694                    message = conversation.findMessageWithServerMsgId(id);
1695                }
1696            } else {
1697                message = null;
1698            }
1699            if (message != null) {
1700                // TODO use occupantId to extract true counterpart from presence
1701                final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1702                // TODO try to externalize mucTrueCounterpart
1703                final Jid trueJid =
1704                        getTrueCounterpart(
1705                                (query != null && query.safeToExtractTrueCounterpart())
1706                                        ? mucUserElement
1707                                        : null,
1708                                fallback);
1709                final boolean trueJidMatchesAccount =
1710                        account.getJid()
1711                                .asBareJid()
1712                                .equals(trueJid == null ? null : trueJid.asBareJid());
1713                if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1714                    if (!message.isRead()
1715                            && (query == null || query.isCatchup())) { // checking if message is
1716                        // unread fixes race conditions
1717                        // with reflections
1718                        mXmppConnectionService.markReadUpTo(conversation, message);
1719                    }
1720                } else if (!counterpart.isBareJid() && trueJid != null) {
1721                    final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1722                    if (message.addReadByMarker(readByMarker)) {
1723                        final var mucOptions = conversation.getMucOptions();
1724                        final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1725                        final var readyBy = message.getReadyByTrue();
1726                        final var mStatus = message.getStatus();
1727                        if (mucOptions.isPrivateAndNonAnonymous()
1728                                && (mStatus == Message.STATUS_SEND_RECEIVED
1729                                        || mStatus == Message.STATUS_SEND)
1730                                && readyBy.containsAll(everyone)) {
1731                            message.setStatus(Message.STATUS_SEND_DISPLAYED);
1732                        }
1733                        mXmppConnectionService.updateMessage(message, false);
1734                    }
1735                }
1736            }
1737        } else {
1738            final Message displayedMessage =
1739                    mXmppConnectionService.markMessage(
1740                            account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1741            Message message = displayedMessage == null ? null : displayedMessage.prev();
1742            while (message != null
1743                    && message.getStatus() == Message.STATUS_SEND_RECEIVED
1744                    && message.getTimeSent() < displayedMessage.getTimeSent()) {
1745                mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1746                message = message.prev();
1747            }
1748            if (displayedMessage != null && selfAddressed) {
1749                dismissNotification(account, counterpart, query, id);
1750            }
1751        }
1752    }
1753
1754    private void processReactions(
1755            final Reactions reactions,
1756            final Conversation conversation,
1757            final boolean isTypeGroupChat,
1758            final OccupantId occupant,
1759            final Jid counterpart,
1760            final Jid mucTrueCounterPart,
1761            final int status,
1762            final im.conversations.android.xmpp.model.stanza.Message packet) {
1763        final String reactingTo = reactions.getId();
1764        if (conversation != null && reactingTo != null) {
1765            if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1766                final var mucOptions = conversation.getMucOptions();
1767                final var occupantId = occupant == null ? null : occupant.getId();
1768                if (occupantId != null) {
1769                    final boolean isReceived = !mucOptions.isSelf(occupantId);
1770                    final Message message;
1771                    final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1772                    if (inMemoryMessage != null) {
1773                        message = inMemoryMessage;
1774                    } else {
1775                        message =
1776                                mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1777                                        conversation, reactingTo);
1778                    }
1779                    if (message != null) {
1780                        final var newReactions = new HashSet<>(reactions.getReactions());
1781                        newReactions.removeAll(message.getReactions().stream().filter(r -> occupantId.equals(r.occupantId)).map(r -> r.reaction).collect(Collectors.toList()));
1782                        final var combinedReactions =
1783                                Reaction.withOccupantId(
1784                                        message.getReactions(),
1785                                        reactions.getReactions(),
1786                                        isReceived,
1787                                        counterpart,
1788                                        mucTrueCounterPart,
1789                                        occupantId,
1790                                        message.getRemoteMsgId());
1791                        message.setReactions(combinedReactions);
1792                        mXmppConnectionService.updateMessage(message, false);
1793                        if (isReceived) mXmppConnectionService.getNotificationService().push(message, counterpart, occupantId, newReactions);
1794                    } else {
1795                        Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1796                    }
1797                } else {
1798                    Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1799                }
1800            } else {
1801                final Message message;
1802                final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1803                if (inMemoryMessage != null) {
1804                    message = inMemoryMessage;
1805                } else {
1806                    message =
1807                            mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1808                                    conversation, reactingTo);
1809                }
1810                if (message == null) {
1811                    Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1812                    return;
1813                }
1814                final boolean isReceived;
1815                final Jid reactionFrom;
1816                if (conversation.getMode() == Conversational.MODE_MULTI) {
1817                    Log.d(Config.LOGTAG, "received reaction as MUC PM. triggering validation");
1818                    final var mucOptions = conversation.getMucOptions();
1819                    final var occupantId = occupant == null ? null : occupant.getId();
1820                    if (occupantId == null) {
1821                        Log.d(
1822                                Config.LOGTAG,
1823                                "received reaction via PM channel w/o occupant ids. ignoring");
1824                        return;
1825                    }
1826                    isReceived = !mucOptions.isSelf(occupantId);
1827                    if (isReceived) {
1828                        reactionFrom = counterpart;
1829                    } else {
1830                        if (!occupantId.equals(message.getOccupantId())) {
1831                            Log.d(
1832                                    Config.LOGTAG,
1833                                    "reaction received via MUC PM did not pass validation");
1834                            return;
1835                        }
1836                        reactionFrom = account.getJid().asBareJid();
1837                    }
1838                } else {
1839                    if (packet.fromAccount(account)) {
1840                        isReceived = false;
1841                        reactionFrom = account.getJid().asBareJid();
1842                    } else {
1843                        isReceived = true;
1844                        reactionFrom = counterpart;
1845                    }
1846                }
1847                final var newReactions = new HashSet<>(reactions.getReactions());
1848                newReactions.removeAll(message.getReactions().stream().filter(r -> reactionFrom.equals(r.from)).map(r -> r.reaction).collect(Collectors.toList()));
1849                final var combinedReactions =
1850                        Reaction.withFrom(
1851                                message.getReactions(),
1852                                reactions.getReactions(),
1853                                isReceived,
1854                                reactionFrom,
1855                                message.getRemoteMsgId());
1856                message.setReactions(combinedReactions);
1857                mXmppConnectionService.updateMessage(message, false);
1858                if (status < Message.STATUS_SEND) mXmppConnectionService.getNotificationService().push(message, counterpart, null, newReactions);
1859            }
1860        }
1861    }
1862
1863    private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1864            getForwardedMessagePacket(
1865                    final im.conversations.android.xmpp.model.stanza.Message original,
1866                    Class<? extends Extension> clazz) {
1867        final var extension = original.getExtension(clazz);
1868        final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1869        if (forwarded == null) {
1870            return null;
1871        }
1872        final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1873        final var forwardedMessage = forwarded.getMessage();
1874        if (forwardedMessage == null) {
1875            return null;
1876        }
1877        return new Pair<>(forwardedMessage, timestamp);
1878    }
1879
1880    private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1881            getForwardedMessagePacket(
1882                    final im.conversations.android.xmpp.model.stanza.Message original,
1883                    final String name,
1884                    final String namespace) {
1885        final Element wrapper = original.findChild(name, namespace);
1886        final var forwardedElement =
1887                wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1888        if (forwardedElement instanceof Forwarded forwarded) {
1889            final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1890            final var forwardedMessage = forwarded.getMessage();
1891            if (forwardedMessage == null) {
1892                return null;
1893            }
1894            return new Pair<>(forwardedMessage, timestamp);
1895        }
1896        return null;
1897    }
1898
1899    private void dismissNotification(
1900            Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1901        final Conversation conversation =
1902                mXmppConnectionService.find(account, counterpart.asBareJid());
1903        if (conversation != null && (query == null || query.isCatchup())) {
1904            final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1905            if (displayableId != null && displayableId.equals(id)) {
1906                mXmppConnectionService.markRead(conversation);
1907            } else {
1908                Log.w(
1909                        Config.LOGTAG,
1910                        account.getJid().asBareJid()
1911                                + ": received dismissing display marker that did not match our last"
1912                                + " id in that conversation");
1913            }
1914        }
1915    }
1916
1917    private void processMessageReceipts(
1918            final Account account,
1919            final im.conversations.android.xmpp.model.stanza.Message packet,
1920            final String remoteMsgId,
1921            final MessageArchiveService.Query query) {
1922        final var request = packet.hasExtension(Request.class);
1923        if (query == null) {
1924            if (request) {
1925                final var receipt =
1926                        mXmppConnectionService
1927                                .getMessageGenerator()
1928                                .received(packet.getFrom(), remoteMsgId, packet.getType());
1929                mXmppConnectionService.sendMessagePacket(account, receipt);
1930            }
1931        } else if (query.isCatchup()) {
1932            if (request) {
1933                query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1934            }
1935        }
1936    }
1937
1938    private void activateGracePeriod(Account account) {
1939        long duration =
1940                mXmppConnectionService.getLongPreference(
1941                                "grace_period_length", R.integer.grace_period)
1942                        * 1000;
1943        Log.d(
1944                Config.LOGTAG,
1945                account.getJid().asBareJid()
1946                        + ": activating grace period till "
1947                        + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1948        account.activateGracePeriod(duration);
1949    }
1950
1951    private class Invite {
1952        final Jid jid;
1953        final String password;
1954        final boolean direct;
1955        final Jid inviter;
1956
1957        Invite(Jid jid, String password, boolean direct, Jid inviter) {
1958            this.jid = jid;
1959            this.password = password;
1960            this.direct = direct;
1961            this.inviter = inviter;
1962        }
1963
1964        public boolean execute(final Account account) {
1965            if (this.jid == null) {
1966                return false;
1967            }
1968            final Contact contact = this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1969            if (contact != null && contact.isBlocked()) {
1970                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite from "+contact.getJid()+" because contact is blocked");
1971                return false;
1972            }
1973            final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1974            conversation.setAttribute("inviter", inviter.toString());
1975            if (conversation.getMucOptions().online()) {
1976                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " but muc is considered to be online");
1977                mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1978            } else {
1979                conversation.getMucOptions().setPassword(password);
1980                mXmppConnectionService.databaseBackend.updateConversation(conversation);
1981                mXmppConnectionService.joinMuc(conversation, contact != null && contact.showInContactList());
1982                mXmppConnectionService.updateConversationUi();
1983            }
1984            return true;
1985        }
1986    }
1987
1988    private static int parseInt(String value) {
1989        try {
1990            return Integer.parseInt(value);
1991        } catch (NumberFormatException e) {
1992            return 0;
1993        }
1994    }
1995}