MessageParser.java

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