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