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