MessageParser.java

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