MessageParser.java

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