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