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