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.InvalidJid;
  35import eu.siacs.conversations.xmpp.Jid;
  36import eu.siacs.conversations.xmpp.chatstate.ChatState;
  37import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  38import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
  39import eu.siacs.conversations.xmpp.pep.Avatar;
  40import im.conversations.android.xmpp.model.Extension;
  41import im.conversations.android.xmpp.model.axolotl.Encrypted;
  42import im.conversations.android.xmpp.model.carbons.Received;
  43import im.conversations.android.xmpp.model.carbons.Sent;
  44import im.conversations.android.xmpp.model.correction.Replace;
  45import im.conversations.android.xmpp.model.forward.Forwarded;
  46import im.conversations.android.xmpp.model.markers.Displayed;
  47import im.conversations.android.xmpp.model.occupant.OccupantId;
  48import im.conversations.android.xmpp.model.reactions.Reactions;
  49import java.text.SimpleDateFormat;
  50import java.util.ArrayList;
  51import java.util.Arrays;
  52import java.util.Collections;
  53import java.util.Date;
  54import java.util.List;
  55import java.util.Locale;
  56import java.util.Map;
  57import java.util.Set;
  58import java.util.UUID;
  59import java.util.function.Consumer;
  60
  61public class MessageParser extends AbstractParser
  62        implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
  63
  64    private static final SimpleDateFormat TIME_FORMAT =
  65            new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
  66
  67    private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
  68            Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
  69
  70    public MessageParser(final XmppConnectionService service, final Account account) {
  71        super(service, account);
  72    }
  73
  74    private static String extractStanzaId(
  75            Element packet, boolean isTypeGroupChat, Conversation conversation) {
  76        final Jid by;
  77        final boolean safeToExtract;
  78        if (isTypeGroupChat) {
  79            by = conversation.getJid().asBareJid();
  80            safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
  81        } else {
  82            Account account = conversation.getAccount();
  83            by = account.getJid().asBareJid();
  84            safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
  85        }
  86        return safeToExtract ? extractStanzaId(packet, by) : null;
  87    }
  88
  89    private static String extractStanzaId(Account account, Element packet) {
  90        final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
  91        return safeToExtract ? extractStanzaId(packet, account.getJid().asBareJid()) : null;
  92    }
  93
  94    private static String extractStanzaId(Element packet, Jid by) {
  95        for (Element child : packet.getChildren()) {
  96            if (child.getName().equals("stanza-id")
  97                    && Namespace.STANZA_IDS.equals(child.getNamespace())
  98                    && by.equals(InvalidJid.getNullForInvalid(child.getAttributeAsJid("by")))) {
  99                return child.getAttribute("id");
 100            }
 101        }
 102        return null;
 103    }
 104
 105    private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
 106        final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
 107        Jid result =
 108                item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
 109        return result != null ? result : fallback;
 110    }
 111
 112    private boolean extractChatState(
 113            Conversation c,
 114            final boolean isTypeGroupChat,
 115            final im.conversations.android.xmpp.model.stanza.Message packet) {
 116        ChatState state = ChatState.parse(packet);
 117        if (state != null && c != null) {
 118            final Account account = c.getAccount();
 119            final Jid from = packet.getFrom();
 120            if (from.asBareJid().equals(account.getJid().asBareJid())) {
 121                c.setOutgoingChatState(state);
 122                if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
 123                    if (c.getContact().isSelf()) {
 124                        return false;
 125                    }
 126                    mXmppConnectionService.markRead(c);
 127                    activateGracePeriod(account);
 128                }
 129                return false;
 130            } else {
 131                if (isTypeGroupChat) {
 132                    MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
 133                    if (user != null) {
 134                        return user.setChatState(state);
 135                    } else {
 136                        return false;
 137                    }
 138                } else {
 139                    return c.setIncomingChatState(state);
 140                }
 141            }
 142        }
 143        return false;
 144    }
 145
 146    private Message parseAxolotlChat(
 147            final Encrypted axolotlMessage,
 148            final Jid from,
 149            final Conversation conversation,
 150            final int status,
 151            final boolean checkedForDuplicates,
 152            final boolean postpone) {
 153        final AxolotlService service = conversation.getAccount().getAxolotlService();
 154        final XmppAxolotlMessage xmppAxolotlMessage;
 155        try {
 156            xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
 157        } catch (Exception e) {
 158            Log.d(
 159                    Config.LOGTAG,
 160                    conversation.getAccount().getJid().asBareJid()
 161                            + ": invalid omemo message received "
 162                            + e.getMessage());
 163            return null;
 164        }
 165        if (xmppAxolotlMessage.hasPayload()) {
 166            final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
 167            try {
 168                plaintextMessage =
 169                        service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
 170            } catch (BrokenSessionException e) {
 171                if (checkedForDuplicates) {
 172                    if (service.trustedOrPreviouslyResponded(from.asBareJid())) {
 173                        service.reportBrokenSessionException(e, postpone);
 174                        return new Message(
 175                                conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
 176                    } else {
 177                        Log.d(
 178                                Config.LOGTAG,
 179                                "ignoring broken session exception because contact was not"
 180                                        + " trusted");
 181                        return new Message(
 182                                conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
 183                    }
 184                } else {
 185                    Log.d(
 186                            Config.LOGTAG,
 187                            "ignoring broken session exception because checkForDuplicates failed");
 188                    return null;
 189                }
 190            } catch (NotEncryptedForThisDeviceException e) {
 191                return new Message(
 192                        conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
 193            } catch (OutdatedSenderException e) {
 194                return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
 195            }
 196            if (plaintextMessage != null) {
 197                Message finishedMessage =
 198                        new Message(
 199                                conversation,
 200                                plaintextMessage.getPlaintext(),
 201                                Message.ENCRYPTION_AXOLOTL,
 202                                status);
 203                finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
 204                Log.d(
 205                        Config.LOGTAG,
 206                        AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())
 207                                + " Received Message with session fingerprint: "
 208                                + plaintextMessage.getFingerprint());
 209                return finishedMessage;
 210            }
 211        } else {
 212            Log.d(
 213                    Config.LOGTAG,
 214                    conversation.getAccount().getJid().asBareJid()
 215                            + ": received OMEMO key transport message");
 216            service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
 217        }
 218        return null;
 219    }
 220
 221    private Invite extractInvite(final Element message) {
 222        final Element mucUser = message.findChild("x", Namespace.MUC_USER);
 223        if (mucUser != null) {
 224            final Element invite = mucUser.findChild("invite");
 225            if (invite != null) {
 226                final String password = mucUser.findChildContent("password");
 227                final Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
 228                final Jid to = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("to"));
 229                if (to != null && from == null) {
 230                    Log.d(Config.LOGTAG, "do not parse outgoing mediated invite " + message);
 231                    return null;
 232                }
 233                final Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
 234                if (room == null) {
 235                    return null;
 236                }
 237                return new Invite(room, password, false, from);
 238            }
 239        }
 240        final Element conference = message.findChild("x", "jabber:x:conference");
 241        if (conference != null) {
 242            Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
 243            Jid room = InvalidJid.getNullForInvalid(conference.getAttributeAsJid("jid"));
 244            if (room == null) {
 245                return null;
 246            }
 247            return new Invite(room, conference.getAttribute("password"), true, from);
 248        }
 249        return null;
 250    }
 251
 252    private void parseEvent(final Element event, final Jid from, final Account account) {
 253        final Element items = event.findChild("items");
 254        final String node = items == null ? null : items.getAttribute("node");
 255        if ("urn:xmpp:avatar:metadata".equals(node)) {
 256            Avatar avatar = Avatar.parseMetadata(items);
 257            if (avatar != null) {
 258                avatar.owner = from.asBareJid();
 259                if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
 260                    if (account.getJid().asBareJid().equals(from)) {
 261                        if (account.setAvatar(avatar.getFilename())) {
 262                            mXmppConnectionService.databaseBackend.updateAccount(account);
 263                            mXmppConnectionService.notifyAccountAvatarHasChanged(account);
 264                        }
 265                        mXmppConnectionService.getAvatarService().clear(account);
 266                        mXmppConnectionService.updateConversationUi();
 267                        mXmppConnectionService.updateAccountUi();
 268                    } else {
 269                        final Contact contact = account.getRoster().getContact(from);
 270                        contact.setAvatar(avatar);
 271                        mXmppConnectionService.syncRoster(account);
 272                        mXmppConnectionService.getAvatarService().clear(contact);
 273                        mXmppConnectionService.updateConversationUi();
 274                        mXmppConnectionService.updateRosterUi();
 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 = InvalidJid.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 || !InvalidJid.isValid(from) || !InvalidJid.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                InvalidJid.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().toEscapedString());
 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                    && InvalidJid.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                        MucOptions.User 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                            boolean isNew = conversation.getMucOptions().updateUser(user);
1190                            mXmppConnectionService.getAvatarService().clear(conversation);
1191                            mXmppConnectionService.updateMucRosterUi();
1192                            mXmppConnectionService.updateConversationUi();
1193                            Contact contact = user.getContact();
1194                            if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
1195                                Jid jid = user.getRealJid();
1196                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
1197                                if (cryptoTargets.remove(user.getRealJid())) {
1198                                    Log.d(
1199                                            Config.LOGTAG,
1200                                            account.getJid().asBareJid()
1201                                                    + ": removed "
1202                                                    + jid
1203                                                    + " from crypto targets of "
1204                                                    + conversation.getName());
1205                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
1206                                    mXmppConnectionService.updateConversation(conversation);
1207                                }
1208                            } else if (isNew
1209                                    && user.getRealJid() != null
1210                                    && conversation.getMucOptions().isPrivateAndNonAnonymous()
1211                                    && (contact == null || !contact.mutualPresenceSubscription())
1212                                    && account.getAxolotlService()
1213                                            .hasEmptyDeviceList(user.getRealJid())) {
1214                                account.getAxolotlService().fetchDeviceIds(user.getRealJid());
1215                            }
1216                        }
1217                    }
1218                }
1219            }
1220            if (!isTypeGroupChat) {
1221                for (Element child : packet.getChildren()) {
1222                    if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
1223                            && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
1224                        final String action = child.getName();
1225                        final String sessionId = child.getAttribute("id");
1226                        if (sessionId == null) {
1227                            break;
1228                        }
1229                        if (query == null && offlineMessagesRetrieved) {
1230                            if (serverMsgId == null) {
1231                                serverMsgId = extractStanzaId(account, packet);
1232                            }
1233                            mXmppConnectionService
1234                                    .getJingleConnectionManager()
1235                                    .deliverMessage(
1236                                            account,
1237                                            packet.getTo(),
1238                                            packet.getFrom(),
1239                                            child,
1240                                            remoteMsgId,
1241                                            serverMsgId,
1242                                            timestamp);
1243                            final Contact contact = account.getRoster().getContact(from);
1244                            // this is the same condition that is found in JingleRtpConnection for
1245                            // the 'ringing' response. Responding with delivery receipts predates
1246                            // the 'ringing' spec'd
1247                            final boolean sendReceipts =
1248                                    contact.showInContactList()
1249                                            || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
1250                            if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
1251                                processMessageReceipts(account, packet, remoteMsgId, null);
1252                            }
1253                        } else if ((query != null && query.isCatchup())
1254                                || !offlineMessagesRetrieved) {
1255                            if ("propose".equals(action)) {
1256                                final Element description = child.findChild("description");
1257                                final String namespace =
1258                                        description == null ? null : description.getNamespace();
1259                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1260                                    final Conversation c =
1261                                            mXmppConnectionService.findOrCreateConversation(
1262                                                    account, counterpart.asBareJid(), false, false);
1263                                    final Message preExistingMessage =
1264                                            c.findRtpSession(sessionId, status);
1265                                    if (preExistingMessage != null) {
1266                                        preExistingMessage.setServerMsgId(serverMsgId);
1267                                        mXmppConnectionService.updateMessage(preExistingMessage);
1268                                        break;
1269                                    }
1270                                    final Message message =
1271                                            new Message(
1272                                                    c, status, Message.TYPE_RTP_SESSION, sessionId);
1273                                    message.setServerMsgId(serverMsgId);
1274                                    message.setTime(timestamp);
1275                                    message.setBody(new RtpSessionStatus(false, 0).toString());
1276                                    c.add(message);
1277                                    mXmppConnectionService.databaseBackend.createMessage(message);
1278                                }
1279                            } else if ("proceed".equals(action)) {
1280                                // status needs to be flipped to find the original propose
1281                                final Conversation c =
1282                                        mXmppConnectionService.findOrCreateConversation(
1283                                                account, counterpart.asBareJid(), false, false);
1284                                final int s =
1285                                        packet.fromAccount(account)
1286                                                ? Message.STATUS_RECEIVED
1287                                                : Message.STATUS_SEND;
1288                                final Message message = c.findRtpSession(sessionId, s);
1289                                if (message != null) {
1290                                    message.setBody(new RtpSessionStatus(true, 0).toString());
1291                                    if (serverMsgId != null) {
1292                                        message.setServerMsgId(serverMsgId);
1293                                    }
1294                                    message.setTime(timestamp);
1295                                    mXmppConnectionService.updateMessage(message, true);
1296                                } else {
1297                                    Log.d(
1298                                            Config.LOGTAG,
1299                                            "unable to find original rtp session message for"
1300                                                    + " received propose");
1301                                }
1302
1303                            } else if ("finish".equals(action)) {
1304                                Log.d(
1305                                        Config.LOGTAG,
1306                                        "received JMI 'finish' during MAM catch-up. Can be used to"
1307                                                + " update success/failure and duration");
1308                            }
1309                        } else {
1310                            // MAM reloads (non catchups
1311                            if ("propose".equals(action)) {
1312                                final Element description = child.findChild("description");
1313                                final String namespace =
1314                                        description == null ? null : description.getNamespace();
1315                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
1316                                    final Conversation c =
1317                                            mXmppConnectionService.findOrCreateConversation(
1318                                                    account, counterpart.asBareJid(), false, false);
1319                                    final Message preExistingMessage =
1320                                            c.findRtpSession(sessionId, status);
1321                                    if (preExistingMessage != null) {
1322                                        preExistingMessage.setServerMsgId(serverMsgId);
1323                                        mXmppConnectionService.updateMessage(preExistingMessage);
1324                                        break;
1325                                    }
1326                                    final Message message =
1327                                            new Message(
1328                                                    c, status, Message.TYPE_RTP_SESSION, sessionId);
1329                                    message.setServerMsgId(serverMsgId);
1330                                    message.setTime(timestamp);
1331                                    message.setBody(new RtpSessionStatus(true, 0).toString());
1332                                    if (query.getPagingOrder()
1333                                            == MessageArchiveService.PagingOrder.REVERSE) {
1334                                        c.prepend(query.getActualInThisQuery(), message);
1335                                    } else {
1336                                        c.add(message);
1337                                    }
1338                                    query.incrementActualMessageCount();
1339                                    mXmppConnectionService.databaseBackend.createMessage(message);
1340                                }
1341                            }
1342                        }
1343                        break;
1344                    }
1345                }
1346            }
1347
1348            final var received =
1349                    packet.getExtension(
1350                            im.conversations.android.xmpp.model.receipts.Received.class);
1351            if (received != null) {
1352                processReceived(received, packet, query, from);
1353            }
1354            final var displayed = packet.getExtension(Displayed.class);
1355            if (displayed != null) {
1356                processDisplayed(
1357                        displayed,
1358                        packet,
1359                        selfAddressed,
1360                        counterpart,
1361                        query,
1362                        isTypeGroupChat,
1363                        conversation,
1364                        mucUserElement,
1365                        from);
1366            }
1367            final Reactions reactions = packet.getExtension(Reactions.class);
1368            if (reactions != null) {
1369                processReactions(
1370                        reactions,
1371                        conversation,
1372                        isTypeGroupChat,
1373                        occupant,
1374                        counterpart,
1375                        mucTrueCounterPart,
1376                        packet);
1377            }
1378
1379            // end no body
1380        }
1381
1382        final Element event =
1383                original.findChild("event", "http://jabber.org/protocol/pubsub#event");
1384        if (event != null && InvalidJid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1385            if (event.hasChild("items")) {
1386                parseEvent(event, original.getFrom(), account);
1387            } else if (event.hasChild("delete")) {
1388                parseDeleteEvent(event, original.getFrom(), account);
1389            } else if (event.hasChild("purge")) {
1390                parsePurgeEvent(event, original.getFrom(), account);
1391            }
1392        }
1393
1394        final String nick = packet.findChildContent("nick", Namespace.NICK);
1395        if (nick != null && InvalidJid.hasValidFrom(original)) {
1396            if (mXmppConnectionService.isMuc(account, from)) {
1397                return;
1398            }
1399            final Contact contact = account.getRoster().getContact(from);
1400            if (contact.setPresenceName(nick)) {
1401                mXmppConnectionService.syncRoster(account);
1402                mXmppConnectionService.getAvatarService().clear(contact);
1403            }
1404        }
1405    }
1406
1407    private void processReceived(
1408            final im.conversations.android.xmpp.model.receipts.Received received,
1409            final im.conversations.android.xmpp.model.stanza.Message packet,
1410            final MessageArchiveService.Query query,
1411            final Jid from) {
1412        final var id = received.getId();
1413        if (packet.fromAccount(account)) {
1414            if (query != null && id != null && packet.getTo() != null) {
1415                query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1416            }
1417        } else if (id != null) {
1418            if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1419                final String sessionId =
1420                        id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1421                mXmppConnectionService
1422                        .getJingleConnectionManager()
1423                        .updateProposedSessionDiscovered(
1424                                account,
1425                                from,
1426                                sessionId,
1427                                JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1428            } else {
1429                mXmppConnectionService.markMessage(
1430                        account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1431            }
1432        }
1433    }
1434
1435    private void processDisplayed(
1436            final Displayed displayed,
1437            final im.conversations.android.xmpp.model.stanza.Message packet,
1438            final boolean selfAddressed,
1439            final Jid counterpart,
1440            final MessageArchiveService.Query query,
1441            final boolean isTypeGroupChat,
1442            Conversation conversation,
1443            Element mucUserElement,
1444            Jid from) {
1445        final var id = displayed.getId();
1446        // TODO we don’t even use 'sender' any more. Remove this!
1447        final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1448        if (packet.fromAccount(account) && !selfAddressed) {
1449            final Conversation c = mXmppConnectionService.find(account, counterpart.asBareJid());
1450            final Message message =
1451                    (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1452            if (message != null && (query == null || query.isCatchup())) {
1453                mXmppConnectionService.markReadUpTo(c, message);
1454            }
1455            if (query == null) {
1456                activateGracePeriod(account);
1457            }
1458        } else if (isTypeGroupChat) {
1459            final Message message;
1460            if (conversation != null && id != null) {
1461                if (sender != null) {
1462                    message = conversation.findMessageWithRemoteId(id, sender);
1463                } else {
1464                    message = conversation.findMessageWithServerMsgId(id);
1465                }
1466            } else {
1467                message = null;
1468            }
1469            if (message != null) {
1470                // TODO use occupantId to extract true counterpart from presence
1471                final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1472                // TODO try to externalize mucTrueCounterpart
1473                final Jid trueJid =
1474                        getTrueCounterpart(
1475                                (query != null && query.safeToExtractTrueCounterpart())
1476                                        ? mucUserElement
1477                                        : null,
1478                                fallback);
1479                final boolean trueJidMatchesAccount =
1480                        account.getJid()
1481                                .asBareJid()
1482                                .equals(trueJid == null ? null : trueJid.asBareJid());
1483                if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1484                    if (!message.isRead()
1485                            && (query == null || query.isCatchup())) { // checking if message is
1486                        // unread fixes race conditions
1487                        // with reflections
1488                        mXmppConnectionService.markReadUpTo(conversation, message);
1489                    }
1490                } else if (!counterpart.isBareJid() && trueJid != null) {
1491                    final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1492                    if (message.addReadByMarker(readByMarker)) {
1493                        final var mucOptions = conversation.getMucOptions();
1494                        final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1495                        final var readyBy = message.getReadyByTrue();
1496                        final var mStatus = message.getStatus();
1497                        if (mucOptions.isPrivateAndNonAnonymous()
1498                                && (mStatus == Message.STATUS_SEND_RECEIVED
1499                                        || mStatus == Message.STATUS_SEND)
1500                                && readyBy.containsAll(everyone)) {
1501                            message.setStatus(Message.STATUS_SEND_DISPLAYED);
1502                        }
1503                        mXmppConnectionService.updateMessage(message, false);
1504                    }
1505                }
1506            }
1507        } else {
1508            final Message displayedMessage =
1509                    mXmppConnectionService.markMessage(
1510                            account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1511            Message message = displayedMessage == null ? null : displayedMessage.prev();
1512            while (message != null
1513                    && message.getStatus() == Message.STATUS_SEND_RECEIVED
1514                    && message.getTimeSent() < displayedMessage.getTimeSent()) {
1515                mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1516                message = message.prev();
1517            }
1518            if (displayedMessage != null && selfAddressed) {
1519                dismissNotification(account, counterpart, query, id);
1520            }
1521        }
1522    }
1523
1524    private void processReactions(
1525            final Reactions reactions,
1526            final Conversation conversation,
1527            final boolean isTypeGroupChat,
1528            final OccupantId occupant,
1529            final Jid counterpart,
1530            final Jid mucTrueCounterPart,
1531            final im.conversations.android.xmpp.model.stanza.Message packet) {
1532        final String reactingTo = reactions.getId();
1533        if (conversation != null && reactingTo != null) {
1534            if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1535                final var mucOptions = conversation.getMucOptions();
1536                final var occupantId = occupant == null ? null : occupant.getId();
1537                if (occupantId != null) {
1538                    final boolean isReceived = !mucOptions.isSelf(occupantId);
1539                    final Message message;
1540                    final var inMemoryMessage = conversation.findMessageWithServerMsgId(reactingTo);
1541                    if (inMemoryMessage != null) {
1542                        message = inMemoryMessage;
1543                    } else {
1544                        message =
1545                                mXmppConnectionService.databaseBackend.getMessageWithServerMsgId(
1546                                        conversation, reactingTo);
1547                    }
1548                    if (message != null) {
1549                        final var combinedReactions =
1550                                Reaction.withOccupantId(
1551                                        message.getReactions(),
1552                                        reactions.getReactions(),
1553                                        isReceived,
1554                                        counterpart,
1555                                        mucTrueCounterPart,
1556                                        occupantId);
1557                        message.setReactions(combinedReactions);
1558                        mXmppConnectionService.updateMessage(message, false);
1559                    } else {
1560                        Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1561                    }
1562                } else {
1563                    Log.d(Config.LOGTAG, "received reaction in channel w/o occupant ids. ignoring");
1564                }
1565            } else if (conversation.getMode() == Conversational.MODE_SINGLE) {
1566                final Message message;
1567                final var inMemoryMessage = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1568                if (inMemoryMessage != null) {
1569                    message = inMemoryMessage;
1570                } else {
1571                    message =
1572                            mXmppConnectionService.databaseBackend.getMessageWithUuidOrRemoteId(
1573                                    conversation, reactingTo);
1574                }
1575                final boolean isReceived;
1576                final Jid reactionFrom;
1577                if (packet.fromAccount(account)) {
1578                    isReceived = false;
1579                    reactionFrom = account.getJid().asBareJid();
1580                } else {
1581                    isReceived = true;
1582                    reactionFrom = counterpart;
1583                }
1584                packet.fromAccount(account);
1585                if (message != null) {
1586                    final var combinedReactions =
1587                            Reaction.withFrom(
1588                                    message.getReactions(),
1589                                    reactions.getReactions(),
1590                                    isReceived,
1591                                    reactionFrom);
1592                    message.setReactions(combinedReactions);
1593                    mXmppConnectionService.updateMessage(message, false);
1594                } else {
1595                    Log.d(Config.LOGTAG, "message with id " + reactingTo + " not found");
1596                }
1597            }
1598        }
1599    }
1600
1601    private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1602            getForwardedMessagePacket(
1603                    final im.conversations.android.xmpp.model.stanza.Message original,
1604                    Class<? extends Extension> clazz) {
1605        final var extension = original.getExtension(clazz);
1606        final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1607        if (forwarded == null) {
1608            return null;
1609        }
1610        final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1611        final var forwardedMessage = forwarded.getMessage();
1612        if (forwardedMessage == null) {
1613            return null;
1614        }
1615        return new Pair<>(forwardedMessage, timestamp);
1616    }
1617
1618    private static Pair<im.conversations.android.xmpp.model.stanza.Message, Long>
1619            getForwardedMessagePacket(
1620                    final im.conversations.android.xmpp.model.stanza.Message original,
1621                    final String name,
1622                    final String namespace) {
1623        final Element wrapper = original.findChild(name, namespace);
1624        final var forwardedElement =
1625                wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD);
1626        if (forwardedElement instanceof Forwarded forwarded) {
1627            final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1628            final var forwardedMessage = forwarded.getMessage();
1629            if (forwardedMessage == null) {
1630                return null;
1631            }
1632            return new Pair<>(forwardedMessage, timestamp);
1633        }
1634        return null;
1635    }
1636
1637    private void dismissNotification(
1638            Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1639        final Conversation conversation =
1640                mXmppConnectionService.find(account, counterpart.asBareJid());
1641        if (conversation != null && (query == null || query.isCatchup())) {
1642            final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1643            if (displayableId != null && displayableId.equals(id)) {
1644                mXmppConnectionService.markRead(conversation);
1645            } else {
1646                Log.w(
1647                        Config.LOGTAG,
1648                        account.getJid().asBareJid()
1649                                + ": received dismissing display marker that did not match our last"
1650                                + " id in that conversation");
1651            }
1652        }
1653    }
1654
1655    private void processMessageReceipts(
1656            final Account account,
1657            final im.conversations.android.xmpp.model.stanza.Message packet,
1658            final String remoteMsgId,
1659            MessageArchiveService.Query query) {
1660        final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
1661        final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
1662        if (query == null) {
1663            final ArrayList<String> receiptsNamespaces = new ArrayList<>();
1664            if (markable) {
1665                receiptsNamespaces.add("urn:xmpp:chat-markers:0");
1666            }
1667            if (request) {
1668                receiptsNamespaces.add("urn:xmpp:receipts");
1669            }
1670            if (receiptsNamespaces.size() > 0) {
1671                final var receipt =
1672                        mXmppConnectionService
1673                                .getMessageGenerator()
1674                                .received(
1675                                        account,
1676                                        packet.getFrom(),
1677                                        remoteMsgId,
1678                                        receiptsNamespaces,
1679                                        packet.getType());
1680                mXmppConnectionService.sendMessagePacket(account, receipt);
1681            }
1682        } else if (query.isCatchup()) {
1683            if (request) {
1684                query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1685            }
1686        }
1687    }
1688
1689    private void activateGracePeriod(Account account) {
1690        long duration =
1691                mXmppConnectionService.getLongPreference(
1692                                "grace_period_length", R.integer.grace_period)
1693                        * 1000;
1694        Log.d(
1695                Config.LOGTAG,
1696                account.getJid().asBareJid()
1697                        + ": activating grace period till "
1698                        + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1699        account.activateGracePeriod(duration);
1700    }
1701
1702    private class Invite {
1703        final Jid jid;
1704        final String password;
1705        final boolean direct;
1706        final Jid inviter;
1707
1708        Invite(Jid jid, String password, boolean direct, Jid inviter) {
1709            this.jid = jid;
1710            this.password = password;
1711            this.direct = direct;
1712            this.inviter = inviter;
1713        }
1714
1715        public boolean execute(final Account account) {
1716            if (this.jid == null) {
1717                return false;
1718            }
1719            final Contact contact =
1720                    this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1721            if (contact != null && contact.isBlocked()) {
1722                Log.d(
1723                        Config.LOGTAG,
1724                        account.getJid().asBareJid()
1725                                + ": ignore invite from "
1726                                + contact.getJid()
1727                                + " because contact is blocked");
1728                return false;
1729            }
1730            final AppSettings appSettings = new AppSettings(mXmppConnectionService);
1731            if ((contact != null && contact.showInContactList())
1732                    || appSettings.isAcceptInvitesFromStrangers()) {
1733                final Conversation conversation =
1734                        mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1735                if (conversation.getMucOptions().online()) {
1736                    Log.d(
1737                            Config.LOGTAG,
1738                            account.getJid().asBareJid()
1739                                    + ": received invite to "
1740                                    + jid
1741                                    + " but muc is considered to be online");
1742                    mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1743                } else {
1744                    conversation.getMucOptions().setPassword(password);
1745                    mXmppConnectionService.databaseBackend.updateConversation(conversation);
1746                    mXmppConnectionService.joinMuc(
1747                            conversation, contact != null && contact.showInContactList());
1748                    mXmppConnectionService.updateConversationUi();
1749                }
1750                return true;
1751            } else {
1752                Log.d(
1753                        Config.LOGTAG,
1754                        account.getJid().asBareJid()
1755                                + ": ignoring invite from "
1756                                + this.inviter
1757                                + " because we are not accepting invites from strangers. direct="
1758                                + direct);
1759                return false;
1760            }
1761        }
1762    }
1763}