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                            if (replaceElement != null && !replaceElement.getName().equals("replace")) {
1102                                mXmppConnectionService.getFileBackend().deleteFile(replacedMessage);
1103                                mXmppConnectionService.evictPreview(message.getUuid());
1104                                List<Element> thumbs = replacedMessage.getFileParams() != null ? replacedMessage.getFileParams().getThumbnails() : null;
1105                                if (thumbs != null && !thumbs.isEmpty()) {
1106                                    for (Element thumb : thumbs) {
1107                                        Uri uri = Uri.parse(thumb.getAttribute("uri"));
1108                                        if (uri.getScheme().equals("cid")) {
1109                                            Cid cid = BobTransfer.cid(uri);
1110                                            if (cid == null) continue;
1111                                            DownloadableFile f = mXmppConnectionService.getFileForCid(cid);
1112                                            if (f != null) {
1113                                                mXmppConnectionService.evictPreview(f);
1114                                                f.delete();
1115                                            }
1116                                        }
1117                                    }
1118                                }
1119                                replacedMessage.clearPayloads();
1120                                replacedMessage.setFileParams(null);
1121                                replacedMessage.addPayload(replaceElement);
1122                            } else {
1123                                replacedMessage.clearPayloads();
1124                                for (final var p : message.getPayloads()) {
1125                                    replacedMessage.addPayload(p);
1126                                }
1127                            }
1128                            replacedMessage.setInReplyTo(message.getInReplyTo());
1129
1130                            // we store the IDs of the replacing message. This is essentially unused
1131                            // today (only the fact that there are _some_ edits causes the edit icon
1132                            // to appear)
1133                            replacedMessage.putEdited(
1134                                    message.getRemoteMsgId(), message.getServerMsgId());
1135
1136                            // we used to call
1137                            // `replacedMessage.setServerMsgId(message.getServerMsgId());` so during
1138                            // catchup we could start from the edit; not the original message
1139                            // however this caused problems for things like reactions that refer to
1140                            // the serverMsgId
1141
1142                            replacedMessage.setEncryption(message.getEncryption());
1143                            if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
1144                                replacedMessage.markUnread();
1145                            }
1146                            extractChatState(
1147                                    mXmppConnectionService.find(account, counterpart.asBareJid()),
1148                                    isTypeGroupChat,
1149                                    packet);
1150                            mXmppConnectionService.updateMessage(replacedMessage, uuid);
1151                            if (mXmppConnectionService.confirmMessages()
1152                                    && replacedMessage.getStatus() == Message.STATUS_RECEIVED
1153                                    && (replacedMessage.trusted()
1154                                            || replacedMessage
1155                                                    .isPrivateMessage()) // TODO do we really want
1156                                    // to send receipts for all
1157                                    // PMs?
1158                                    && remoteMsgId != null
1159                                    && !selfAddressed
1160                                    && !isTypeGroupChat) {
1161                                processMessageReceipts(account, packet, remoteMsgId, query);
1162                            }
1163                            if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
1164                                conversation
1165                                        .getAccount()
1166                                        .getPgpDecryptionService()
1167                                        .discard(replacedMessage);
1168                                conversation
1169                                        .getAccount()
1170                                        .getPgpDecryptionService()
1171                                        .decrypt(replacedMessage, false);
1172                            }
1173                        }
1174                        mXmppConnectionService.getNotificationService().updateNotification();
1175                        return;
1176                    } else {
1177                        Log.d(
1178                                Config.LOGTAG,
1179                                account.getJid().asBareJid()
1180                                        + ": received message correction but verification didn't"
1181                                        + " check out");
1182                    }
1183                } else if (message.getBody() == null || message.getBody().equals("") || message.getBody().equals(" ")) {
1184                    return;
1185                }
1186                if (replaceElement != null && !replaceElement.getName().equals("replace")) return;
1187            }
1188
1189            boolean checkForDuplicates =
1190                    (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
1191                            || message.isPrivateMessage()
1192                            || message.getServerMsgId() != null
1193                            || (query == null
1194                                    && mXmppConnectionService
1195                                            .getMessageArchiveService()
1196                                            .isCatchupInProgress(conversation));
1197            if (checkForDuplicates) {
1198                final Message duplicate = conversation.findDuplicateMessage(message);
1199                if (duplicate != null) {
1200                    final boolean serverMsgIdUpdated;
1201                    if (duplicate.getStatus() != Message.STATUS_RECEIVED
1202                            && duplicate.getUuid().equals(message.getRemoteMsgId())
1203                            && duplicate.getServerMsgId() == null
1204                            && message.getServerMsgId() != null) {
1205                        duplicate.setServerMsgId(message.getServerMsgId());
1206                        if (mXmppConnectionService.databaseBackend.updateMessage(
1207                                duplicate, false)) {
1208                            serverMsgIdUpdated = true;
1209                        } else {
1210                            serverMsgIdUpdated = false;
1211                            Log.e(Config.LOGTAG, "failed to update message");
1212                        }
1213                    } else {
1214                        serverMsgIdUpdated = false;
1215                    }
1216                    Log.d(
1217                            Config.LOGTAG,
1218                            "skipping duplicate message with "
1219                                    + message.getCounterpart()
1220                                    + ". serverMsgIdUpdated="
1221                                    + serverMsgIdUpdated);
1222                    return;
1223                }
1224            }
1225
1226            if (query != null
1227                    && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
1228                conversation.prepend(query.getActualInThisQuery(), message);
1229            } else {
1230                conversation.add(message);
1231            }
1232            if (query != null) {
1233                query.incrementActualMessageCount();
1234            }
1235
1236            if (query == null || query.isCatchup()) { // either no mam or catchup
1237                if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
1238                    mXmppConnectionService.markRead(conversation);
1239                    if (query == null) {
1240                        activateGracePeriod(account);
1241                    }
1242                } else {
1243                    message.markUnread();
1244                    notify = true;
1245                }
1246            }
1247
1248            if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1249                notify =
1250                        conversation
1251                                .getAccount()
1252                                .getPgpDecryptionService()
1253                                .decrypt(message, notify);
1254            } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
1255                    || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1256                notify = false;
1257            }
1258
1259            if (query == null) {
1260                extractChatState(
1261                        mXmppConnectionService.find(account, counterpart.asBareJid()),
1262                        isTypeGroupChat,
1263                        packet);
1264                mXmppConnectionService.updateConversationUi();
1265            }
1266
1267            if (mXmppConnectionService.confirmMessages()
1268                    && message.getStatus() == Message.STATUS_RECEIVED
1269                    && (message.trusted() || message.isPrivateMessage())
1270                    && remoteMsgId != null
1271                    && !selfAddressed
1272                    && !isTypeGroupChat) {
1273                processMessageReceipts(account, packet, remoteMsgId, query);
1274            }
1275
1276            if (message.getFileParams() != null) {
1277                for (Cid cid : message.getFileParams().getCids()) {
1278                    File f = mXmppConnectionService.getFileForCid(cid);
1279                    if (f != null && f.canRead()) {
1280                        message.setRelativeFilePath(f.getAbsolutePath());
1281                        mXmppConnectionService.getFileBackend().updateFileParams(message, null, false);
1282                        break;
1283                    }
1284                }
1285            }
1286
1287            mXmppConnectionService.databaseBackend.createMessage(message);
1288            final HttpConnectionManager manager =
1289                    this.mXmppConnectionService.getHttpConnectionManager();
1290            if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
1291                if (message.getOob() != null && "cid".equalsIgnoreCase(message.getOob().getScheme())) {
1292                    try {
1293                        BobTransfer transfer = new BobTransfer.ForMessage(message, mXmppConnectionService);
1294                        message.setTransferable(transfer);
1295                        transfer.start();
1296                    } catch (URISyntaxException e) {
1297                        Log.d(Config.LOGTAG, "BobTransfer failed to parse URI");
1298                    }
1299                } else {
1300                    manager.createNewDownloadConnection(message);
1301                }
1302            } else if (notify) {
1303                if (query != null && query.isCatchup()) {
1304                    mXmppConnectionService.getNotificationService().pushFromBacklog(message);
1305                } else {
1306                    mXmppConnectionService.getNotificationService().push(message);
1307                }
1308            }
1309        } else if (!packet.hasChild("body")) { // no body
1310            final Conversation conversation =
1311                    mXmppConnectionService.find(account, from.asBareJid());
1312            if (axolotlEncrypted != null) {
1313                Jid origin;
1314                if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1315                    final Jid fallback =
1316                            conversation.getMucOptions().getTrueCounterpart(counterpart);
1317                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
1318                    if (origin == null) {
1319                        Log.d(
1320                                Config.LOGTAG,
1321                                "omemo key transport message in anonymous conference received");
1322                        return;
1323                    }
1324                } else if (isTypeGroupChat) {
1325                    return;
1326                } else {
1327                    origin = from;
1328                }
1329                try {
1330                    final XmppAxolotlMessage xmppAxolotlMessage =
1331                            XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
1332                    account.getAxolotlService()
1333                            .processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
1334                    Log.d(
1335                            Config.LOGTAG,
1336                            account.getJid().asBareJid()
1337                                    + ": omemo key transport message received from "
1338                                    + origin);
1339                } catch (Exception e) {
1340                    Log.d(
1341                            Config.LOGTAG,
1342                            account.getJid().asBareJid()
1343                                    + ": invalid omemo key transport message received "
1344                                    + e.getMessage());
1345                    return;
1346                }
1347            }
1348
1349            if (query == null
1350                    && extractChatState(
1351                            mXmppConnectionService.find(account, counterpart.asBareJid()),
1352                            isTypeGroupChat,
1353                            packet)) {
1354                mXmppConnectionService.updateConversationUi();
1355            }
1356
1357            if (isTypeGroupChat) {
1358                if (packet.hasChild("subject")
1359                        && !packet.hasChild("thread")) { // We already know it has no body per above
1360                    if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
1361                        conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
1362                        final LocalizedContent subject = packet.getSubject();
1363                        if (subject != null
1364                                && conversation.getMucOptions().setSubject(subject.content)) {
1365                            mXmppConnectionService.updateConversation(conversation);
1366                        }
1367                        mXmppConnectionService.updateConversationUi();
1368                        return;
1369                    }
1370                }
1371            }
1372            if (conversation != null
1373                    && mucUserElement != null
1374                    && Jid.Invalid.hasValidFrom(packet)
1375                    && from.isBareJid()) {
1376                for (Element child : mucUserElement.getChildren()) {
1377                    if ("status".equals(child.getName())) {
1378                        try {
1379                            int code = Integer.parseInt(child.getAttribute("code"));
1380                            if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
1381                                mXmppConnectionService.fetchConferenceConfiguration(conversation);
1382                                break;
1383                            }
1384                        } catch (Exception e) {
1385                            // ignored
1386                        }
1387                    } else if ("item".equals(child.getName())) {
1388                        final var user = AbstractParser.parseItem(conversation, child);
1389                        Log.d(
1390                                Config.LOGTAG,
1391                                account.getJid()
1392                                        + ": changing affiliation for "
1393                                        + user.getRealJid()
1394                                        + " to "
1395                                        + user.getAffiliation()
1396                                        + " in "
1397                                        + conversation.getJid().asBareJid());
1398                        if (!user.realJidMatchesAccount()) {
1399                            final var mucOptions = conversation.getMucOptions();
1400                            final boolean isNew = mucOptions.updateUser(user);
1401                            final var avatarService = mXmppConnectionService.getAvatarService();
1402                            if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
1403                                avatarService.clear(mucOptions);
1404                            }
1405                            avatarService.clear(user);
1406                            mXmppConnectionService.updateMucRosterUi();
1407                            mXmppConnectionService.updateConversationUi();
1408                            Contact contact = user.getContact();
1409                            if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
1410                                Jid jid = user.getRealJid();
1411                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
1412                                if (cryptoTargets.remove(user.getRealJid())) {
1413                                    Log.d(
1414                                            Config.LOGTAG,
1415                                            account.getJid().asBareJid()
1416                                                    + ": removed "
1417                                                    + jid
1418                                                    + " from crypto targets of "
1419                                                    + conversation.getName());
1420                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
1421                                    mXmppConnectionService.updateConversation(conversation);
1422                                }
1423                            } else if (isNew
1424                                    && user.getRealJid() != null
1425                                    && conversation.getMucOptions().isPrivateAndNonAnonymous()
1426                                    && (contact == null || !contact.mutualPresenceSubscription())
1427                                    && account.getAxolotlService()
1428                                            .hasEmptyDeviceList(user.getRealJid())) {
1429                                account.getAxolotlService().fetchDeviceIds(user.getRealJid());
1430                            }
1431                        }
1432                    }
1433                }
1434            }
1435            if (!isTypeGroupChat) {
1436                for (Element child : packet.getChildren()) {
1437                    if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
1438                            && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
1439                        final String action = child.getName();
1440                        final String sessionId = child.getAttribute("id");
1441                        if (sessionId == null) {
1442                            break;
1443                        }
1444                        if (query == null && offlineMessagesRetrieved) {
1445                            if (serverMsgId == null) {
1446                                serverMsgId = extractStanzaId(account, packet);
1447                            }
1448                            mXmppConnectionService
1449                                    .getJingleConnectionManager()
1450                                    .deliverMessage(
1451                                            account,
1452                                            packet.getTo(),
1453                                            packet.getFrom(),
1454                                            child,
1455                                            remoteMsgId,
1456                                            serverMsgId,
1457                                            timestamp);
1458                            final Contact contact = account.getRoster().getContact(from);
1459                            // this is the same condition that is found in JingleRtpConnection for
1460                            // the 'ringing' response. Responding with delivery receipts predates
1461                            // the 'ringing' spec'd
1462                            final boolean sendReceipts =
1463                                    contact.showInContactList()
1464                                            || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
1465                            if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
1466                                processMessageReceipts(account, packet, remoteMsgId, null);
1467                            }
1468                        } else if ((query != null && query.isCatchup())
1469                                || !offlineMessagesRetrieved) {
1470                            if ("propose".equals(action)) {
1471                                final Element description = child.findChild("description");
1472                                final String namespace =
1473                                        description == null ? null : description.getNamespace();
1474                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1475                                    final Conversation c =
1476                                            mXmppConnectionService.findOrCreateConversation(
1477                                                    account, counterpart.asBareJid(), false, false);
1478                                    final Message preExistingMessage =
1479                                            c.findRtpSession(sessionId, status);
1480                                    if (preExistingMessage != null) {
1481                                        preExistingMessage.setServerMsgId(serverMsgId);
1482                                        mXmppConnectionService.updateMessage(preExistingMessage);
1483                                        break;
1484                                    }
1485                                    final Message message =
1486                                            new Message(
1487                                                    c, status, Message.TYPE_RTP_SESSION, sessionId);
1488                                    message.setServerMsgId(serverMsgId);
1489                                    message.setTime(timestamp);
1490                                    message.setBody(new RtpSessionStatus(false, 0).toString());
1491                                    message.markUnread();
1492                                    c.add(message);
1493                                    mXmppConnectionService.getNotificationService().possiblyMissedCall(c.getUuid() + sessionId, message);
1494                                    if (query != null) query.incrementActualMessageCount();
1495                                    mXmppConnectionService.databaseBackend.createMessage(message);
1496                                }
1497                            } else if ("proceed".equals(action)) {
1498                                // status needs to be flipped to find the original propose
1499                                final Conversation c =
1500                                        mXmppConnectionService.findOrCreateConversation(
1501                                                account, counterpart.asBareJid(), false, false);
1502                                final int s =
1503                                        packet.fromAccount(account)
1504                                                ? Message.STATUS_RECEIVED
1505                                                : Message.STATUS_SEND;
1506                                final Message message = c.findRtpSession(sessionId, s);
1507                                if (message != null) {
1508                                    message.setBody(new RtpSessionStatus(true, 0).toString());
1509                                    if (serverMsgId != null) {
1510                                        message.setServerMsgId(serverMsgId);
1511                                    }
1512                                    message.setTime(timestamp);
1513                                    message.markRead();
1514                                    mXmppConnectionService.getNotificationService().possiblyMissedCall(c.getUuid() + sessionId, message);
1515                                    if (query != null) query.incrementActualMessageCount();
1516                                    mXmppConnectionService.updateMessage(message, true);
1517                                } else {
1518                                    Log.d(
1519                                            Config.LOGTAG,
1520                                            "unable to find original rtp session message for"
1521                                                    + " received propose");
1522                                }
1523
1524                            } else if ("finish".equals(action)) {
1525                                Log.d(
1526                                        Config.LOGTAG,
1527                                        "received JMI 'finish' during MAM catch-up. Can be used to"
1528                                                + " update success/failure and duration");
1529                            }
1530                        } else {
1531                            // MAM reloads (non catchups
1532                            if ("propose".equals(action)) {
1533                                final Element description = child.findChild("description");
1534                                final String namespace =
1535                                        description == null ? null : description.getNamespace();
1536                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1537                                    final Conversation c =
1538                                            mXmppConnectionService.findOrCreateConversation(
1539                                                    account, counterpart.asBareJid(), false, false);
1540                                    final Message preExistingMessage =
1541                                            c.findRtpSession(sessionId, status);
1542                                    if (preExistingMessage != null) {
1543                                        preExistingMessage.setServerMsgId(serverMsgId);
1544                                        mXmppConnectionService.updateMessage(preExistingMessage);
1545                                        break;
1546                                    }
1547                                    final Message message =
1548                                            new Message(
1549                                                    c, status, Message.TYPE_RTP_SESSION, sessionId);
1550                                    message.setServerMsgId(serverMsgId);
1551                                    message.setTime(timestamp);
1552                                    message.setBody(new RtpSessionStatus(true, 0).toString());
1553                                    if (query.getPagingOrder()
1554                                            == MessageArchiveService.PagingOrder.REVERSE) {
1555                                        c.prepend(query.getActualInThisQuery(), message);
1556                                    } else {
1557                                        c.add(message);
1558                                    }
1559                                    query.incrementActualMessageCount();
1560                                    mXmppConnectionService.databaseBackend.createMessage(message);
1561                                }
1562                            }
1563                        }
1564                        break;
1565                    }
1566                }
1567            }
1568
1569            final var received =
1570                    packet.getExtension(
1571                            im.conversations.android.xmpp.model.receipts.Received.class);
1572            if (received != null) {
1573                processReceived(received, packet, query, from);
1574            }
1575            final var displayed = packet.getExtension(Displayed.class);
1576            if (displayed != null) {
1577                processDisplayed(
1578                        displayed,
1579                        packet,
1580                        selfAddressed,
1581                        counterpart,
1582                        query,
1583                        isTypeGroupChat,
1584                        conversation,
1585                        mucUserElement,
1586                        from);
1587            }
1588
1589            // end no body
1590        }
1591
1592        if (reactions != null) {
1593            processReactions(
1594                    reactions,
1595                    mXmppConnectionService.find(account, from.asBareJid()),
1596                    isTypeGroupChat,
1597                    occupant,
1598                    counterpart,
1599                    mucTrueCounterPart,
1600                    status,
1601                    packet);
1602        }
1603
1604        final var event = original.getExtension(Event.class);
1605        if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1606            final var action = event.getAction();
1607            final var node = action == null ? null : action.getNode();
1608            if (node == null) {
1609                Log.d(
1610                        Config.LOGTAG,
1611                        account.getJid().asBareJid()
1612                                + ": no node found in PubSub event from "
1613                                + original.getFrom());
1614            } else if (action instanceof Items items) {
1615                parseEvent(items, original.getFrom(), account);
1616            } else if (action instanceof Purge purge) {
1617                parsePurgeEvent(purge, original.getFrom(), account);
1618            } else if (action instanceof Delete delete) {
1619                parseDeleteEvent(delete, from, account);
1620            }
1621        }
1622
1623        final String nick = packet.findChildContent("nick", Namespace.NICK);
1624        if (nick != null && Jid.Invalid.hasValidFrom(original)) {
1625            if (mXmppConnectionService.isMuc(account, from)) {
1626                return;
1627            }
1628            final Contact contact = account.getRoster().getContact(from);
1629            if (contact.setPresenceName(nick)) {
1630                mXmppConnectionService.syncRoster(account);
1631                mXmppConnectionService.getAvatarService().clear(contact);
1632            }
1633        }
1634    }
1635
1636    private void processReceived(
1637            final im.conversations.android.xmpp.model.receipts.Received received,
1638            final im.conversations.android.xmpp.model.stanza.Message packet,
1639            final MessageArchiveService.Query query,
1640            final Jid from) {
1641        final var id = received.getId();
1642        if (packet.fromAccount(account)) {
1643            if (query != null && id != null && packet.getTo() != null) {
1644                query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1645            }
1646        } else if (id != null) {
1647            if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1648                final String sessionId =
1649                        id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1650                mXmppConnectionService
1651                        .getJingleConnectionManager()
1652                        .updateProposedSessionDiscovered(
1653                                account,
1654                                from,
1655                                sessionId,
1656                                JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1657            } else {
1658                mXmppConnectionService.markMessage(
1659                        account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1660            }
1661        }
1662    }
1663
1664    private void processDisplayed(
1665            final Displayed displayed,
1666            final im.conversations.android.xmpp.model.stanza.Message packet,
1667            final boolean selfAddressed,
1668            final Jid counterpart,
1669            final MessageArchiveService.Query query,
1670            final boolean isTypeGroupChat,
1671            Conversation conversation,
1672            Element mucUserElement,
1673            Jid from) {
1674        final var id = displayed.getId();
1675        // TODO we don’t even use 'sender' any more. Remove this!
1676        final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1677        if (packet.fromAccount(account) && !selfAddressed) {
1678            final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1679            final Message message =
1680                    (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1681            if (message != null && (query == null || query.isCatchup())) {
1682                mXmppConnectionService.markReadUpTo(c, message);
1683            }
1684            if (query == null) {
1685                activateGracePeriod(account);
1686            }
1687        } else if (isTypeGroupChat) {
1688            final Message message;
1689            if (conversation != null && id != null) {
1690                if (sender != null) {
1691                    message = conversation.findMessageWithRemoteId(id, sender);
1692                } else {
1693                    message = conversation.findMessageWithServerMsgId(id);
1694                }
1695            } else {
1696                message = null;
1697            }
1698            if (message != null) {
1699                // TODO use occupantId to extract true counterpart from presence
1700                final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1701                // TODO try to externalize mucTrueCounterpart
1702                final Jid trueJid =
1703                        getTrueCounterpart(
1704                                (query != null && query.safeToExtractTrueCounterpart())
1705                                        ? mucUserElement
1706                                        : null,
1707                                fallback);
1708                final boolean trueJidMatchesAccount =
1709                        account.getJid()
1710                                .asBareJid()
1711                                .equals(trueJid == null ? null : trueJid.asBareJid());
1712                if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1713                    if (!message.isRead()
1714                            && (query == null || query.isCatchup())) { // checking if message is
1715                        // unread fixes race conditions
1716                        // with reflections
1717                        mXmppConnectionService.markReadUpTo(conversation, message);
1718                    }
1719                } else if (!counterpart.isBareJid() && trueJid != null) {
1720                    final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1721                    if (message.addReadByMarker(readByMarker)) {
1722                        final var mucOptions = conversation.getMucOptions();
1723                        final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1724                        final var readyBy = message.getReadyByTrue();
1725                        final var mStatus = message.getStatus();
1726                        if (mucOptions.isPrivateAndNonAnonymous()
1727                                && (mStatus == Message.STATUS_SEND_RECEIVED
1728                                        || mStatus == Message.STATUS_SEND)
1729                                && readyBy.containsAll(everyone)) {
1730                            message.setStatus(Message.STATUS_SEND_DISPLAYED);
1731                        }
1732                        mXmppConnectionService.updateMessage(message, false);
1733                    }
1734                }
1735            }
1736        } else {
1737            final Message displayedMessage =
1738                    mXmppConnectionService.markMessage(
1739                            account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1740            Message message = displayedMessage == null ? null : displayedMessage.prev();
1741            while (message != null
1742                    && message.getStatus() == Message.STATUS_SEND_RECEIVED
1743                    && message.getTimeSent() < displayedMessage.getTimeSent()) {
1744                mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1745                message = message.prev();
1746            }
1747            if (displayedMessage != null && selfAddressed) {
1748                dismissNotification(account, counterpart, query, id);
1749            }
1750        }
1751    }
1752
1753    private void processReactions(
1754            final Reactions reactions,
1755            final Conversation conversation,
1756            final boolean isTypeGroupChat,
1757            final OccupantId occupant,
1758            final Jid counterpart,
1759            final Jid mucTrueCounterPart,
1760            final int status,
1761            final im.conversations.android.xmpp.model.stanza.Message packet) {
1762        final String reactingTo = reactions.getId();
1763        if (conversation != null && reactingTo != null) {
1764            if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1765                final var mucOptions = conversation.getMucOptions();
1766                final var occupantId = occupant == null ? null : occupant.getId();
1767                if (occupantId != null) {
1768                    final boolean isReceived = !mucOptions.isSelf(occupantId);
1769                    final Message message;
1770                    final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1771                    if (inMemoryMessage != null) {
1772                        message = inMemoryMessage;
1773                    } else {
1774                        message =
1775                                mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1776                                        conversation, reactingTo);
1777                    }
1778                    if (message != null) {
1779                        final var newReactions = new HashSet<>(reactions.getReactions());
1780                        newReactions.removeAll(message.getReactions().stream().filter(r -> occupantId.equals(r.occupantId)).map(r -> r.reaction).collect(Collectors.toList()));
1781                        final var combinedReactions =
1782                                Reaction.withOccupantId(
1783                                        message.getReactions(),
1784                                        reactions.getReactions(),
1785                                        isReceived,
1786                                        counterpart,
1787                                        mucTrueCounterPart,
1788                                        occupantId,
1789                                        message.getRemoteMsgId());
1790                        message.setReactions(combinedReactions);
1791                        mXmppConnectionService.updateMessage(message, false);
1792                        if (isReceived) mXmppConnectionService.getNotificationService().push(message, counterpart, occupantId, newReactions);
1793                    } else {
1794                        Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1795                    }
1796                } else {
1797                    Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1798                }
1799            } else {
1800                final Message message;
1801                final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1802                if (inMemoryMessage != null) {
1803                    message = inMemoryMessage;
1804                } else {
1805                    message =
1806                            mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1807                                    conversation, reactingTo);
1808                }
1809                if (message == null) {
1810                    Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1811                    return;
1812                }
1813                final boolean isReceived;
1814                final Jid reactionFrom;
1815                if (conversation.getMode() == Conversational.MODE_MULTI) {
1816                    Log.d(Config.LOGTAG, "received reaction as MUC PM. triggering validation");
1817                    final var mucOptions = conversation.getMucOptions();
1818                    final var occupantId = occupant == null ? null : occupant.getId();
1819                    if (occupantId == null) {
1820                        Log.d(
1821                                Config.LOGTAG,
1822                                "received reaction via PM channel w/o occupant ids. ignoring");
1823                        return;
1824                    }
1825                    isReceived = !mucOptions.isSelf(occupantId);
1826                    if (isReceived) {
1827                        reactionFrom = counterpart;
1828                    } else {
1829                        if (!occupantId.equals(message.getOccupantId())) {
1830                            Log.d(
1831                                    Config.LOGTAG,
1832                                    "reaction received via MUC PM did not pass validation");
1833                            return;
1834                        }
1835                        reactionFrom = account.getJid().asBareJid();
1836                    }
1837                } else {
1838                    if (packet.fromAccount(account)) {
1839                        isReceived = false;
1840                        reactionFrom = account.getJid().asBareJid();
1841                    } else {
1842                        isReceived = true;
1843                        reactionFrom = counterpart;
1844                    }
1845                }
1846                final var newReactions = new HashSet<>(reactions.getReactions());
1847                newReactions.removeAll(message.getReactions().stream().filter(r -> reactionFrom.equals(r.from)).map(r -> r.reaction).collect(Collectors.toList()));
1848                final var combinedReactions =
1849                        Reaction.withFrom(
1850                                message.getReactions(),
1851                                reactions.getReactions(),
1852                                isReceived,
1853                                reactionFrom,
1854                                message.getRemoteMsgId());
1855                message.setReactions(combinedReactions);
1856                mXmppConnectionService.updateMessage(message, false);
1857                if (status < Message.STATUS_SEND) mXmppConnectionService.getNotificationService().push(message, counterpart, null, newReactions);
1858            }
1859        }
1860    }
1861
1862    private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1863            getForwardedMessagePacket(
1864                    final im.conversations.android.xmpp.model.stanza.Message original,
1865                    Class<? extends Extension> clazz) {
1866        final var extension = original.getExtension(clazz);
1867        final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1868        if (forwarded == null) {
1869            return null;
1870        }
1871        final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1872        final var forwardedMessage = forwarded.getMessage();
1873        if (forwardedMessage == null) {
1874            return null;
1875        }
1876        return new Pair<>(forwardedMessage, timestamp);
1877    }
1878
1879    private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1880            getForwardedMessagePacket(
1881                    final im.conversations.android.xmpp.model.stanza.Message original,
1882                    final String name,
1883                    final String namespace) {
1884        final Element wrapper = original.findChild(name, namespace);
1885        final var forwardedElement =
1886                wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1887        if (forwardedElement instanceof Forwarded forwarded) {
1888            final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1889            final var forwardedMessage = forwarded.getMessage();
1890            if (forwardedMessage == null) {
1891                return null;
1892            }
1893            return new Pair<>(forwardedMessage, timestamp);
1894        }
1895        return null;
1896    }
1897
1898    private void dismissNotification(
1899            Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1900        final Conversation conversation =
1901                mXmppConnectionService.find(account, counterpart.asBareJid());
1902        if (conversation != null && (query == null || query.isCatchup())) {
1903            final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1904            if (displayableId != null && displayableId.equals(id)) {
1905                mXmppConnectionService.markRead(conversation);
1906            } else {
1907                Log.w(
1908                        Config.LOGTAG,
1909                        account.getJid().asBareJid()
1910                                + ": received dismissing display marker that did not match our last"
1911                                + " id in that conversation");
1912            }
1913        }
1914    }
1915
1916    private void processMessageReceipts(
1917            final Account account,
1918            final im.conversations.android.xmpp.model.stanza.Message packet,
1919            final String remoteMsgId,
1920            final MessageArchiveService.Query query) {
1921        final var request = packet.hasExtension(Request.class);
1922        if (query == null) {
1923            if (request) {
1924                final var receipt =
1925                        mXmppConnectionService
1926                                .getMessageGenerator()
1927                                .received(packet.getFrom(), remoteMsgId, packet.getType());
1928                mXmppConnectionService.sendMessagePacket(account, receipt);
1929            }
1930        } else if (query.isCatchup()) {
1931            if (request) {
1932                query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1933            }
1934        }
1935    }
1936
1937    private void activateGracePeriod(Account account) {
1938        long duration =
1939                mXmppConnectionService.getLongPreference(
1940                                "grace_period_length", R.integer.grace_period)
1941                        * 1000;
1942        Log.d(
1943                Config.LOGTAG,
1944                account.getJid().asBareJid()
1945                        + ": activating grace period till "
1946                        + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1947        account.activateGracePeriod(duration);
1948    }
1949
1950    private class Invite {
1951        final Jid jid;
1952        final String password;
1953        final boolean direct;
1954        final Jid inviter;
1955
1956        Invite(Jid jid, String password, boolean direct, Jid inviter) {
1957            this.jid = jid;
1958            this.password = password;
1959            this.direct = direct;
1960            this.inviter = inviter;
1961        }
1962
1963        public boolean execute(final Account account) {
1964            if (this.jid == null) {
1965                return false;
1966            }
1967            final Contact contact = this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1968            if (contact != null && contact.isBlocked()) {
1969                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite from "+contact.getJid()+" because contact is blocked");
1970                return false;
1971            }
1972            final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1973            conversation.setAttribute("inviter", inviter.toString());
1974            if (conversation.getMucOptions().online()) {
1975                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " but muc is considered to be online");
1976                mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1977            } else {
1978                conversation.getMucOptions().setPassword(password);
1979                mXmppConnectionService.databaseBackend.updateConversation(conversation);
1980                mXmppConnectionService.joinMuc(conversation, contact != null && contact.showInContactList());
1981                mXmppConnectionService.updateConversationUi();
1982            }
1983            return true;
1984        }
1985    }
1986
1987    private static int parseInt(String value) {
1988        try {
1989            return Integer.parseInt(value);
1990        } catch (NumberFormatException e) {
1991            return 0;
1992        }
1993    }
1994}