MessageParser.java

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