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