MessageParser.java

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