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