MessageParser.java

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