MessageParser.java

  1package eu.siacs.conversations.parser;
  2
  3import android.util.Log;
  4import android.util.Pair;
  5
  6import java.net.URL;
  7import java.text.SimpleDateFormat;
  8import java.util.ArrayList;
  9import java.util.Collections;
 10import java.util.Date;
 11import java.util.List;
 12import java.util.Locale;
 13import java.util.Set;
 14import java.util.UUID;
 15
 16import eu.siacs.conversations.Config;
 17import eu.siacs.conversations.R;
 18import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 19import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
 20import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
 21import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 22import eu.siacs.conversations.entities.Account;
 23import eu.siacs.conversations.entities.Contact;
 24import eu.siacs.conversations.entities.Conversation;
 25import eu.siacs.conversations.entities.Conversational;
 26import eu.siacs.conversations.entities.Message;
 27import eu.siacs.conversations.entities.MucOptions;
 28import eu.siacs.conversations.entities.ReadByMarker;
 29import eu.siacs.conversations.entities.ReceiptRequest;
 30import eu.siacs.conversations.http.HttpConnectionManager;
 31import eu.siacs.conversations.http.P1S3UrlStreamHandler;
 32import eu.siacs.conversations.services.MessageArchiveService;
 33import eu.siacs.conversations.services.QuickConversationsService;
 34import eu.siacs.conversations.services.XmppConnectionService;
 35import eu.siacs.conversations.utils.CryptoHelper;
 36import eu.siacs.conversations.xml.Namespace;
 37import eu.siacs.conversations.xml.Element;
 38import eu.siacs.conversations.xmpp.InvalidJid;
 39import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
 40import eu.siacs.conversations.xmpp.chatstate.ChatState;
 41import eu.siacs.conversations.xmpp.pep.Avatar;
 42import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 43import rocks.xmpp.addr.Jid;
 44
 45public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
 46
 47    private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
 48
 49    public MessageParser(XmppConnectionService service) {
 50        super(service);
 51    }
 52
 53    private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) {
 54        final Jid by;
 55        final boolean safeToExtract;
 56        if (isTypeGroupChat) {
 57            by = conversation.getJid().asBareJid();
 58            safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
 59        } else {
 60            Account account = conversation.getAccount();
 61            by = account.getJid().asBareJid();
 62            safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
 63        }
 64        return safeToExtract ? extractStanzaId(packet, by) : null;
 65    }
 66
 67    private static String extractStanzaId(Element packet, Jid by) {
 68        for (Element child : packet.getChildren()) {
 69            if (child.getName().equals("stanza-id")
 70                    && Namespace.STANZA_IDS.equals(child.getNamespace())
 71                    && by.equals(InvalidJid.getNullForInvalid(child.getAttributeAsJid("by")))) {
 72                return child.getAttribute("id");
 73            }
 74        }
 75        return null;
 76    }
 77
 78    private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
 79        final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
 80        Jid result = item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
 81        return result != null ? result : fallback;
 82    }
 83
 84    private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
 85        ChatState state = ChatState.parse(packet);
 86        if (state != null && c != null) {
 87            final Account account = c.getAccount();
 88            Jid from = packet.getFrom();
 89            if (from.asBareJid().equals(account.getJid().asBareJid())) {
 90                c.setOutgoingChatState(state);
 91                if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
 92                    mXmppConnectionService.markRead(c);
 93                    activateGracePeriod(account);
 94                }
 95                return false;
 96            } else {
 97                if (isTypeGroupChat) {
 98                    MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
 99                    if (user != null) {
100                        return user.setChatState(state);
101                    } else {
102                        return false;
103                    }
104                } else {
105                    return c.setIncomingChatState(state);
106                }
107            }
108        }
109        return false;
110    }
111
112    private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, boolean checkedForDuplicates, boolean postpone) {
113        final AxolotlService service = conversation.getAccount().getAxolotlService();
114        final XmppAxolotlMessage xmppAxolotlMessage;
115        try {
116            xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
117        } catch (Exception e) {
118            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": invalid omemo message received " + e.getMessage());
119            return null;
120        }
121        if (xmppAxolotlMessage.hasPayload()) {
122            final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
123            try {
124                plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
125            } catch (BrokenSessionException e) {
126                if (checkedForDuplicates) {
127                    service.reportBrokenSessionException(e, postpone);
128                    return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
129                } else {
130                    Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicase failed");
131                    return null;
132                }
133            } catch (NotEncryptedForThisDeviceException e) {
134                return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
135            }
136            if (plaintextMessage != null) {
137                Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
138                finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
139                Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount()) + " Received Message with session fingerprint: " + plaintextMessage.getFingerprint());
140                return finishedMessage;
141            }
142        } else {
143            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OMEMO key transport message");
144            service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
145        }
146        return null;
147    }
148
149    private Invite extractInvite(Account account, Element message) {
150        Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
151        if (x != null) {
152            Element invite = x.findChild("invite");
153            if (invite != null) {
154                String password = x.findChildContent("password");
155                Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
156                Contact contact = from == null ? null : account.getRoster().getContact(from);
157                Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
158                if (room == null) {
159                    return null;
160                }
161                return new Invite(room, password, contact);
162            }
163        } else {
164            x = message.findChild("x", "jabber:x:conference");
165            if (x != null) {
166                Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
167                Contact contact = from == null ? null : account.getRoster().getContact(from);
168                Jid room = InvalidJid.getNullForInvalid(x.getAttributeAsJid("jid"));
169                if (room == null) {
170                    return null;
171                }
172                return new Invite(room, x.getAttribute("password"), contact);
173            }
174        }
175        return null;
176    }
177
178    private void parseEvent(final Element event, final Jid from, final Account account) {
179        Element items = event.findChild("items");
180        String node = items == null ? null : items.getAttribute("node");
181        if ("urn:xmpp:avatar:metadata".equals(node)) {
182            Avatar avatar = Avatar.parseMetadata(items);
183            if (avatar != null) {
184                avatar.owner = from.asBareJid();
185                if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
186                    if (account.getJid().asBareJid().equals(from)) {
187                        if (account.setAvatar(avatar.getFilename())) {
188                            mXmppConnectionService.databaseBackend.updateAccount(account);
189                            mXmppConnectionService.notifyAccountAvatarHasChanged(account);
190                        }
191                        mXmppConnectionService.getAvatarService().clear(account);
192                        mXmppConnectionService.updateConversationUi();
193                        mXmppConnectionService.updateAccountUi();
194                    } else {
195                        Contact contact = account.getRoster().getContact(from);
196                        if (contact.setAvatar(avatar)) {
197                            mXmppConnectionService.syncRoster(account);
198                            mXmppConnectionService.getAvatarService().clear(contact);
199                            mXmppConnectionService.updateConversationUi();
200                            mXmppConnectionService.updateRosterUi();
201                        }
202                    }
203                } else if (mXmppConnectionService.isDataSaverDisabled()) {
204                    mXmppConnectionService.fetchAvatar(account, avatar);
205                }
206            }
207        } else if (Namespace.NICK.equals(node)) {
208            final Element i = items.findChild("item");
209            final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
210            if (nick != null) {
211                setNick(account, from, nick);
212            }
213        } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
214            Element item = items.findChild("item");
215            Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
216            Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
217            AxolotlService axolotlService = account.getAxolotlService();
218            axolotlService.registerDevices(from, deviceIds);
219        } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
220            if (account.getXmppConnection().getFeatures().bookmarksConversion()) {
221                final Element i = items.findChild("item");
222                final Element storage = i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
223                mXmppConnectionService.processBookmarks(account, storage, true);
224                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": processing bookmark PEP event");
225            } else {
226                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring bookmark PEP event because bookmark conversion was not detected");
227            }
228        }
229    }
230
231    private void parseDeleteEvent(final Element event, final Jid from, final Account account) {
232        final Element delete = event.findChild("delete");
233        if (delete == null) {
234            return;
235        }
236        String node = delete.getAttribute("node");
237        if (Namespace.NICK.equals(node)) {
238            Log.d(Config.LOGTAG, "parsing nick delete event from " + from);
239            setNick(account, from, null);
240        }
241    }
242
243    private void setNick(Account account, Jid user, String nick) {
244        if (user.asBareJid().equals(account.getJid().asBareJid())) {
245            account.setDisplayName(nick);
246            if (QuickConversationsService.isQuicksy()) {
247                mXmppConnectionService.getAvatarService().clear(account);
248            }
249        } else {
250            Contact contact = account.getRoster().getContact(user);
251            if (contact.setPresenceName(nick)) {
252                mXmppConnectionService.getAvatarService().clear(contact);
253            }
254        }
255        mXmppConnectionService.updateConversationUi();
256        mXmppConnectionService.updateAccountUi();
257    }
258
259    private boolean handleErrorMessage(Account account, MessagePacket packet) {
260        if (packet.getType() == MessagePacket.TYPE_ERROR) {
261            Jid from = packet.getFrom();
262            if (from != null) {
263                mXmppConnectionService.markMessage(account,
264                        from.asBareJid(),
265                        packet.getId(),
266                        Message.STATUS_SEND_FAILED,
267                        extractErrorMessage(packet));
268                final Element error = packet.findChild("error");
269                final boolean notAcceptable = error != null && error.hasChild("not-acceptable");
270                if (notAcceptable) {
271                    Conversation conversation = mXmppConnectionService.find(account,from);
272                    if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) {
273                        if (conversation.getMucOptions().online()) {
274                            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received not-acceptable error for seemingly online muc at "+from);
275                            mXmppConnectionService.mucSelfPingAndRejoin(conversation);
276                        }
277                    }
278                }
279            }
280            return true;
281        }
282        return false;
283    }
284
285    @Override
286    public void onMessagePacketReceived(Account account, MessagePacket original) {
287        if (handleErrorMessage(account, original)) {
288            return;
289        }
290        final MessagePacket packet;
291        Long timestamp = null;
292        boolean isCarbon = false;
293        String serverMsgId = null;
294        final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
295        if (fin != null) {
296            mXmppConnectionService.getMessageArchiveService().processFinLegacy(fin, original.getFrom());
297            return;
298        }
299        final Element result = MessageArchiveService.Version.findResult(original);
300        final MessageArchiveService.Query query = result == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid"));
301        if (query != null && query.validFrom(original.getFrom())) {
302            Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", query.version.namespace);
303            if (f == null) {
304                return;
305            }
306            timestamp = f.second;
307            packet = f.first;
308            serverMsgId = result.getAttribute("id");
309            query.incrementMessageCount();
310        } else if (query != null) {
311            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result from invalid sender");
312            return;
313        } else if (original.fromServer(account)) {
314            Pair<MessagePacket, Long> f;
315            f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
316            f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f;
317            packet = f != null ? f.first : original;
318            if (handleErrorMessage(account, packet)) {
319                return;
320            }
321            timestamp = f != null ? f.second : null;
322            isCarbon = f != null;
323        } else {
324            packet = original;
325        }
326
327        if (timestamp == null) {
328            timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
329        }
330        final String body = packet.getBody();
331        final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
332        final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
333        final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
334        final Element oob = packet.findChild("x", Namespace.OOB);
335        final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER);
336        final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
337        final String oobUrl = oob != null ? oob.findChildContent("url") : null;
338        final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
339        final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
340        int status;
341        final Jid counterpart;
342        final Jid to = packet.getTo();
343        final Jid from = packet.getFrom();
344        final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
345        final String remoteMsgId;
346        if (originId != null && originId.getAttribute("id") != null) {
347            remoteMsgId = originId.getAttribute("id");
348        } else {
349            remoteMsgId = packet.getId();
350        }
351        boolean notify = false;
352
353        if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) {
354            Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
355            return;
356        }
357
358        boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
359        if (query != null && !query.muc() && isTypeGroupChat) {
360            Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
361            return;
362        }
363        boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
364        boolean selfAddressed;
365        if (packet.fromAccount(account)) {
366            status = Message.STATUS_SEND;
367            selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
368            if (selfAddressed) {
369                counterpart = from;
370            } else {
371                counterpart = to != null ? to : account.getJid();
372            }
373        } else {
374            status = Message.STATUS_RECEIVED;
375            counterpart = from;
376            selfAddressed = false;
377        }
378
379        Invite invite = extractInvite(account, packet);
380        if (invite != null && invite.execute(account)) {
381            return;
382        }
383
384        if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
385            final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain());
386            final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
387            final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
388
389            if (serverMsgId == null) {
390                serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
391            }
392
393
394            if (selfAddressed) {
395                if (mXmppConnectionService.markMessage(conversation, remoteMsgId, Message.STATUS_SEND_RECEIVED, serverMsgId)) {
396                    return;
397                }
398                status = Message.STATUS_RECEIVED;
399                if (remoteMsgId != null && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
400                    return;
401                }
402            }
403
404            if (isTypeGroupChat) {
405                if (conversation.getMucOptions().isSelf(counterpart)) {
406                    status = Message.STATUS_SEND_RECEIVED;
407                    isCarbon = true; //not really carbon but received from another resource
408                    if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId)) {
409                        return;
410                    } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
411                        Message message = conversation.findSentMessageWithBody(packet.getBody());
412                        if (message != null) {
413                            mXmppConnectionService.markMessage(message, status);
414                            return;
415                        }
416                    }
417                } else {
418                    status = Message.STATUS_RECEIVED;
419                }
420            }
421            final Message message;
422            if (xP1S3url != null) {
423                message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
424                message.setOob(true);
425                if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) {
426                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
427                }
428            } else if (pgpEncrypted != null && Config.supportOpenPgp()) {
429                message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
430            } else if (axolotlEncrypted != null && Config.supportOmemo()) {
431                Jid origin;
432                Set<Jid> fallbacksBySourceId = Collections.emptySet();
433                if (conversationMultiMode) {
434                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
435                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
436                    if (origin == null) {
437                        try {
438                            fallbacksBySourceId = account.getAxolotlService().findCounterpartsBySourceId(XmppAxolotlMessage.parseSourceId(axolotlEncrypted));
439                        } catch (IllegalArgumentException e) {
440                            //ignoring
441                        }
442                    }
443                    if (origin == null && fallbacksBySourceId.size() == 0) {
444                        Log.d(Config.LOGTAG, "axolotl message in anonymous conference received and no possible fallbacks");
445                        return;
446                    }
447                } else {
448                    fallbacksBySourceId = Collections.emptySet();
449                    origin = from;
450                }
451
452                final boolean checkedForDuplicates = serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId);
453
454                if (origin != null) {
455                    message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status,  checkedForDuplicates,query != null);
456                } else {
457                    Message trial = null;
458                    for (Jid fallback : fallbacksBySourceId) {
459                        trial = parseAxolotlChat(axolotlEncrypted, fallback, conversation, status, checkedForDuplicates && fallbacksBySourceId.size() == 1, query != null);
460                        if (trial != null) {
461                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": decoded muc message using fallback");
462                            origin = fallback;
463                            break;
464                        }
465                    }
466                    message = trial;
467                }
468                if (message == null) {
469                    if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
470                        mXmppConnectionService.updateConversationUi();
471                    }
472                    if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
473                        Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
474                        if (previouslySent != null && previouslySent.getServerMsgId() == null && serverMsgId != null) {
475                            previouslySent.setServerMsgId(serverMsgId);
476                            mXmppConnectionService.databaseBackend.updateMessage(previouslySent, false);
477                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered previously sent OMEMO message without serverId. updating...");
478                        }
479                    }
480                    return;
481                }
482                if (conversationMultiMode) {
483                    message.setTrueCounterpart(origin);
484                }
485            } else if (body == null && oobUrl != null) {
486                message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
487                message.setOob(true);
488                if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
489                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
490                }
491            } else {
492                message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
493            }
494
495            message.setCounterpart(counterpart);
496            message.setRemoteMsgId(remoteMsgId);
497            message.setServerMsgId(serverMsgId);
498            message.setCarbon(isCarbon);
499            message.setTime(timestamp);
500            if (body != null && body.equals(oobUrl)) {
501                message.setOob(true);
502                if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
503                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
504                }
505            }
506            message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
507            if (conversationMultiMode) {
508                message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));
509                final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
510                Jid trueCounterpart;
511                if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
512                    trueCounterpart = message.getTrueCounterpart();
513                } else if (query != null && query.safeToExtractTrueCounterpart()) {
514                    trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
515                } else {
516                    trueCounterpart = fallback;
517                }
518                if (trueCounterpart != null && isTypeGroupChat) {
519                    if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
520                        status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
521                    } else {
522                        status = Message.STATUS_RECEIVED;
523                        message.setCarbon(false);
524                    }
525                }
526                message.setStatus(status);
527                message.setTrueCounterpart(trueCounterpart);
528                if (!isTypeGroupChat) {
529                    message.setType(Message.TYPE_PRIVATE);
530                }
531            } else {
532                updateLastseen(account, from);
533            }
534
535            if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
536                final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
537                        counterpart,
538                        message.getStatus() == Message.STATUS_RECEIVED,
539                        message.isCarbon());
540                if (replacedMessage != null) {
541                    final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
542                            || replacedMessage.getFingerprint().equals(message.getFingerprint());
543                    final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
544                            && replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
545                    final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
546                    final boolean duplicate = conversation.hasDuplicateMessage(message);
547                    if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
548                        Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
549                        synchronized (replacedMessage) {
550                            final String uuid = replacedMessage.getUuid();
551                            replacedMessage.setUuid(UUID.randomUUID().toString());
552                            replacedMessage.setBody(message.getBody());
553                            replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
554                            replacedMessage.setRemoteMsgId(remoteMsgId);
555                            if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
556                                replacedMessage.setServerMsgId(message.getServerMsgId());
557                            }
558                            replacedMessage.setEncryption(message.getEncryption());
559                            if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
560                                replacedMessage.markUnread();
561                            }
562                            extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
563                            mXmppConnectionService.updateMessage(replacedMessage, uuid);
564                            if (mXmppConnectionService.confirmMessages()
565                                    && replacedMessage.getStatus() == Message.STATUS_RECEIVED
566                                    && (replacedMessage.trusted() || replacedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs?
567                                    && remoteMsgId != null
568                                    && !selfAddressed
569                                    && !isTypeGroupChat) {
570                                processMessageReceipts(account, packet, query);
571                            }
572                            if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
573                                conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
574                                conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
575                            }
576                        }
577                        mXmppConnectionService.getNotificationService().updateNotification();
578                        return;
579                    } else {
580                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
581                    }
582                }
583            }
584
585            long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
586            if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
587                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
588                return;
589            }
590
591            boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
592                    || message.isPrivateMessage()
593                    || message.getServerMsgId() != null
594                    || (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation));
595            if (checkForDuplicates) {
596                final Message duplicate = conversation.findDuplicateMessage(message);
597                if (duplicate != null) {
598                    final boolean serverMsgIdUpdated;
599                    if (duplicate.getStatus() != Message.STATUS_RECEIVED
600                            && duplicate.getUuid().equals(message.getRemoteMsgId())
601                            && duplicate.getServerMsgId() == null
602                            && message.getServerMsgId() != null) {
603                        duplicate.setServerMsgId(message.getServerMsgId());
604                        if (mXmppConnectionService.databaseBackend.updateMessage(duplicate, false)) {
605                            serverMsgIdUpdated = true;
606                        } else {
607                            serverMsgIdUpdated = false;
608                            Log.e(Config.LOGTAG, "failed to update message");
609                        }
610                    } else {
611                        serverMsgIdUpdated = false;
612                    }
613                    Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + Boolean.toString(serverMsgIdUpdated));
614                    return;
615                }
616            }
617
618            if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
619                conversation.prepend(query.getActualInThisQuery(), message);
620            } else {
621                conversation.add(message);
622            }
623            if (query != null) {
624                query.incrementActualMessageCount();
625            }
626
627            if (query == null || query.isCatchup()) { //either no mam or catchup
628                if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
629                    mXmppConnectionService.markRead(conversation);
630                    if (query == null) {
631                        activateGracePeriod(account);
632                    }
633                } else {
634                    message.markUnread();
635                    notify = true;
636                }
637            }
638
639            if (message.getEncryption() == Message.ENCRYPTION_PGP) {
640                notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
641            } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
642                notify = false;
643            }
644
645            if (query == null) {
646                extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
647                mXmppConnectionService.updateConversationUi();
648            }
649
650            if (mXmppConnectionService.confirmMessages()
651                    && message.getStatus() == Message.STATUS_RECEIVED
652                    && (message.trusted() || message.isPrivateMessage())
653                    && remoteMsgId != null
654                    && !selfAddressed
655                    && !isTypeGroupChat) {
656                processMessageReceipts(account, packet, query);
657            }
658
659            mXmppConnectionService.databaseBackend.createMessage(message);
660            final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
661            if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
662                manager.createNewDownloadConnection(message);
663            } else if (notify) {
664                if (query != null && query.isCatchup()) {
665                    mXmppConnectionService.getNotificationService().pushFromBacklog(message);
666                } else {
667                    mXmppConnectionService.getNotificationService().push(message);
668                }
669            }
670        } else if (!packet.hasChild("body")) { //no body
671
672            final Conversation conversation = mXmppConnectionService.find(account, from.asBareJid());
673            if (axolotlEncrypted != null) {
674                Jid origin;
675                if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
676                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
677                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
678                    if (origin == null) {
679                        Log.d(Config.LOGTAG, "omemo key transport message in anonymous conference received");
680                        return;
681                    }
682                } else if (isTypeGroupChat) {
683                    return;
684                } else {
685                    origin = from;
686                }
687                try {
688                    final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
689                    account.getAxolotlService().processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
690                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": omemo key transport message received from " + origin);
691                } catch (Exception e) {
692                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": invalid omemo key transport message received " + e.getMessage());
693                    return;
694                }
695            }
696
697            if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
698                mXmppConnectionService.updateConversationUi();
699            }
700
701            if (isTypeGroupChat) {
702                if (packet.hasChild("subject")) {
703                    if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
704                        conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
705                        String subject = packet.findInternationalizedChildContent("subject");
706                        if (conversation.getMucOptions().setSubject(subject)) {
707                            mXmppConnectionService.updateConversation(conversation);
708                        }
709                        mXmppConnectionService.updateConversationUi();
710                        return;
711                    }
712                }
713            }
714            if (conversation != null && mucUserElement != null && InvalidJid.hasValidFrom(packet) && from.isBareJid()) {
715                for (Element child : mucUserElement.getChildren()) {
716                    if ("status".equals(child.getName())) {
717                        try {
718                            int code = Integer.parseInt(child.getAttribute("code"));
719                            if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
720                                mXmppConnectionService.fetchConferenceConfiguration(conversation);
721                                break;
722                            }
723                        } catch (Exception e) {
724                            //ignored
725                        }
726                    } else if ("item".equals(child.getName())) {
727                        MucOptions.User user = AbstractParser.parseItem(conversation, child);
728                        Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for "
729                                + user.getRealJid() + " to " + user.getAffiliation() + " in "
730                                + conversation.getJid().asBareJid());
731                        if (!user.realJidMatchesAccount()) {
732                            boolean isNew = conversation.getMucOptions().updateUser(user);
733                            mXmppConnectionService.getAvatarService().clear(conversation);
734                            mXmppConnectionService.updateMucRosterUi();
735                            mXmppConnectionService.updateConversationUi();
736                            Contact contact = user.getContact();
737                            if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
738                                Jid jid = user.getRealJid();
739                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
740                                if (cryptoTargets.remove(user.getRealJid())) {
741                                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
742                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
743                                    mXmppConnectionService.updateConversation(conversation);
744                                }
745                            } else if (isNew
746                                    && user.getRealJid() != null
747                                    && conversation.getMucOptions().isPrivateAndNonAnonymous()
748                                    && (contact == null || !contact.mutualPresenceSubscription())
749                                    && account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) {
750                                account.getAxolotlService().fetchDeviceIds(user.getRealJid());
751                            }
752                        }
753                    }
754                }
755            }
756        }
757
758        Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
759        if (received == null) {
760            received = packet.findChild("received", "urn:xmpp:receipts");
761        }
762        if (received != null) {
763            String id = received.getAttribute("id");
764            if (packet.fromAccount(account)) {
765                if (query != null && id != null && packet.getTo() != null) {
766                    query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
767                }
768            } else {
769                mXmppConnectionService.markMessage(account, from.asBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED);
770            }
771        }
772        Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
773        if (displayed != null) {
774            final String id = displayed.getAttribute("id");
775            final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
776            if (packet.fromAccount(account) && !selfAddressed) {
777                dismissNotification(account, counterpart, query);
778            } else if (isTypeGroupChat) {
779                Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
780                if (conversation != null && id != null && sender != null) {
781                    Message message = conversation.findMessageWithRemoteId(id, sender);
782                    if (message != null) {
783                        final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
784                        final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
785                        final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
786                        if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
787                            if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
788                                mXmppConnectionService.markRead(conversation);
789                            }
790                        } else if (!counterpart.isBareJid() && trueJid != null) {
791                            ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
792                            if (message.addReadByMarker(readByMarker)) {
793                                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'");
794                                mXmppConnectionService.updateMessage(message, false);
795                            }
796                        }
797                    }
798                }
799            } else {
800                final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
801                Message message = displayedMessage == null ? null : displayedMessage.prev();
802                while (message != null
803                        && message.getStatus() == Message.STATUS_SEND_RECEIVED
804                        && message.getTimeSent() < displayedMessage.getTimeSent()) {
805                    mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
806                    message = message.prev();
807                }
808                if (displayedMessage != null && selfAddressed) {
809                    dismissNotification(account, counterpart, query);
810                }
811            }
812        }
813
814        final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
815        if (event != null && InvalidJid.hasValidFrom(original)) {
816            if (event.hasChild("items")) {
817                parseEvent(event, original.getFrom(), account);
818            } else if (event.hasChild("delete")) {
819                parseDeleteEvent(event, original.getFrom(), account);
820            }
821        }
822
823        final String nick = packet.findChildContent("nick", Namespace.NICK);
824        if (nick != null && InvalidJid.hasValidFrom(original)) {
825            Contact contact = account.getRoster().getContact(from);
826            if (contact.setPresenceName(nick)) {
827                mXmppConnectionService.getAvatarService().clear(contact);
828            }
829        }
830    }
831
832    private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query) {
833        Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
834        if (conversation != null && (query == null || query.isCatchup())) {
835            mXmppConnectionService.markRead(conversation); //TODO only mark messages read that are older than timestamp
836        }
837    }
838
839    private void processMessageReceipts(Account account, MessagePacket packet, MessageArchiveService.Query query) {
840        final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
841        final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
842        if (query == null) {
843            final ArrayList<String> receiptsNamespaces = new ArrayList<>();
844            if (markable) {
845                receiptsNamespaces.add("urn:xmpp:chat-markers:0");
846            }
847            if (request) {
848                receiptsNamespaces.add("urn:xmpp:receipts");
849            }
850            if (receiptsNamespaces.size() > 0) {
851                MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
852                        packet,
853                        receiptsNamespaces,
854                        packet.getType());
855                mXmppConnectionService.sendMessagePacket(account, receipt);
856            }
857        } else if (query.isCatchup()) {
858            if (request) {
859                query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), packet.getId()));
860            }
861        }
862    }
863
864    private void activateGracePeriod(Account account) {
865        long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000;
866        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
867        account.activateGracePeriod(duration);
868    }
869
870    private class Invite {
871        final Jid jid;
872        final String password;
873        final Contact inviter;
874
875        Invite(Jid jid, String password, Contact inviter) {
876            this.jid = jid;
877            this.password = password;
878            this.inviter = inviter;
879        }
880
881        public boolean execute(Account account) {
882            if (jid != null) {
883                Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
884                if (conversation.getMucOptions().online()) {
885                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received invite to "+jid+" but muc is considered to be online");
886                    mXmppConnectionService.mucSelfPingAndRejoin(conversation);
887                } else {
888                    conversation.getMucOptions().setPassword(password);
889                    mXmppConnectionService.databaseBackend.updateConversation(conversation);
890                    mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
891                    mXmppConnectionService.updateConversationUi();
892                }
893                return true;
894            }
895            return false;
896        }
897    }
898}