MessageParser.java

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