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