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