package de.thedevstack.conversationsplus.parser; import android.util.Log; import android.util.Pair; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlMessageParser; import de.thedevstack.conversationsplus.crypto.otr.OtrMessageParser; import de.thedevstack.conversationsplus.crypto.otr.OtrUtil; import de.thedevstack.conversationsplus.enums.MessageDirection; import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import de.thedevstack.conversationsplus.services.avatar.AvatarCache; import de.thedevstack.conversationsplus.services.filetransfer.http.download.AutomaticFileDownload; import de.thedevstack.conversationsplus.services.filetransfer.http.download.HttpRetrieveHead; import de.thedevstack.conversationsplus.utils.ConversationUtil; import de.thedevstack.conversationsplus.utils.MessageParserUtil; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.UiUpdateHelper; import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacket; import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketParser; import de.thedevstack.conversationsplus.xmpp.carbons.Carbons; import de.thedevstack.conversationsplus.xmpp.chatmarkers.ChatMarkers; import de.thedevstack.conversationsplus.xmpp.httpuploadim.HttpUploadHint; import de.thedevstack.conversationsplus.xmpp.mam.Mam; import de.thedevstack.conversationsplus.xmpp.muc.MucPacketParser; import de.thedevstack.conversationsplus.xmpp.openpgp.OpenPgpXep; import de.thedevstack.conversationsplus.xmpp.receipts.MessageDeliveryReceipts; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacketReceiver; import java.util.Set; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.utils.AvatarUtil; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceImpl; import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlMessage; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Bookmark; import de.thedevstack.conversationsplus.entities.Contact; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.MucOptions; import de.thedevstack.conversationsplus.services.avatar.AvatarService; import de.thedevstack.conversationsplus.services.mam.MessageArchiveService; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnMessagePacketReceived; import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; import de.thedevstack.conversationsplus.xmpp.jid.Jid; import de.thedevstack.conversationsplus.dto.Avatar; import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; public class MessageParser extends AbstractParser implements OnMessagePacketReceived { public MessageParser(XmppConnectionService service) { super(service); } private boolean extractChatState(Conversation conversation, final MessagePacket packet) { ChatState state = ChatState.parse(packet); if (state != null && conversation != null) { final Account account = conversation.getAccount(); Jid from = packet.getFrom(); if (from.toBareJid().equals(account.getJid().toBareJid())) { conversation.setOutgoingChatState(state); if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) { Logging.d("markRead", "MessageParser.extractChatState (" + conversation.getName() + ")"); mXmppConnectionService.markRead(conversation); account.activateGracePeriod(); } return false; } else { updateLastseen(packet, account, true); // Todo: Should the timestamp be extracted here? return conversation.setIncomingChatState(state); } } return false; } private static String extractStanzaId(Element packet, Jid by) { for(Element child : packet.getChildren()) { if (child.getName().equals("stanza-id") && "urn:xmpp:sid:0".equals(child.getNamespace()) && by.equals(child.getAttributeAsJid("by"))) { return child.getAttribute("id"); } } return null; } private void parseEvent(final Element event, final Jid from, final Account account) { Element items = event.findChild("items"); String node = items == null ? null : items.getAttribute("node"); if (AvatarPacket.NAMESPACE_AVATAR_METADATA.equals(node)) { Avatar avatar = AvatarPacketParser.parseMetadata(items); if (avatar != null) { avatar.owner = from.toBareJid(); if (AvatarUtil.isAvatarCached(avatar)) { if (account.getJid().toBareJid().equals(from)) { if (account.setAvatar(avatar.getFilename())) { DatabaseBackend.getInstance().updateAccount(account); } AvatarCache.clear(account); UiUpdateHelper.updateConversationUi(); UiUpdateHelper.updateAccountUi(); } else { Contact contact = account.getRoster().getContact(from); contact.setAvatar(avatar); AvatarCache.clear(contact); UiUpdateHelper.updateConversationUi(); UiUpdateHelper.updateRosterUi(); } } else { AvatarService.getInstance().fetchAvatar(account, avatar); } } } else if ("http://jabber.org/protocol/nick".equals(node)) { Element i = items.findChild("item"); Element nick = i == null ? null : i.findChild("nick", "http://jabber.org/protocol/nick"); if (nick != null && nick.getContent() != null) { Contact contact = account.getRoster().getContact(from); contact.setPresenceName(nick.getContent()); AvatarCache.clear(account); UiUpdateHelper.updateConversationUi(); UiUpdateHelper.updateAccountUi(); } } else if (ConversationsPlusPreferences.omemoEnabled() && AxolotlService.PEP_DEVICE_LIST.equals(node)) { Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing..."); Element item = items.findChild("item"); Set deviceIds = IqPacketReceiver.getInstance().getLegacyIqParser().deviceIds(item); AxolotlService axolotlService = account.getAxolotlService(); axolotlService.registerDevices(from, deviceIds); UiUpdateHelper.updateAccountUi(); } } private boolean handleErrorMessage(Account account, MessagePacket packet) { if (packet.getType() == MessagePacket.TYPE_ERROR) { Jid from = packet.getFrom(); if (from != null) { Element error = packet.findChild("error"); String text = error == null ? null : error.findChildContent("text"); if (text != null) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + text); } else if (error != null) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error); } Message message = XmppConnectionServiceAccessor.xmppConnectionService.getMessage(account, from.toBareJid(), packet.getId()); if (null != message) { MessageUtil.setAndSaveMessageStatus(message, MessageStatus.FAILED); if (Message.ENCRYPTION_OTR == message.getEncryption()) { message.getConversation().endOtrIfNeeded(); } } } return true; } return false; } @Override public void onMessagePacketReceived(Account account, MessagePacket original) { if (handleErrorMessage(account, original)) { return; } MessagePacket packet = null; Long timestamp = null; boolean isCarbon = false; boolean isMAMCatchup = false; String serverMsgId = null; MessageDirection direction = null; final Element fin = original.findChild("fin", Mam.NAMESPACE); MessageArchiveService messageArchiveService = XmppConnectionServiceAccessor.xmppConnectionService.getMessageArchiveService(); if (fin != null) { // Todo: Check for query.getWith() -> MAM Catchup -> Handle?? messageArchiveService.processFin(fin,original.getFrom()); return; } final Element result = original.findChild("result", Mam.NAMESPACE); boolean isMAM = result != null; if (isMAM) { final MessageArchiveService.Query query = messageArchiveService.findQuery(result.getAttribute("queryid")); if (null != query && query.validFrom(original.getFrom())) { Pair forwardedMessagePacket = original.getForwardedMessagePacket("result", Mam.NAMESPACE); if (forwardedMessagePacket == null) { return; } isMAMCatchup = null == query.getWith(); timestamp = forwardedMessagePacket.second; packet = forwardedMessagePacket.first; serverMsgId = result.getAttribute("id"); query.incrementMessageCount(); } else { Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender"); return; } } else if (original.fromServer(account)) { // Todo: Check if carbons are not always from the own bare JID Pair forwardedMessagePacket = original.getForwardedMessagePacket("received", Carbons.NAMESPACE); if (null != forwardedMessagePacket) { direction = MessageDirection.IN; } else { forwardedMessagePacket = original.getForwardedMessagePacket("sent", Carbons.NAMESPACE); if (null != forwardedMessagePacket) { direction = MessageDirection.OUT; } } isCarbon = forwardedMessagePacket != null; if (isCarbon) { packet = forwardedMessagePacket.first; timestamp = forwardedMessagePacket.second; if (handleErrorMessage(account, packet)) { return; } } } if (null == packet) { packet = original; } if (null == timestamp) { timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis()); } final Jid from = packet.getFrom(); if (from == null) { Log.d(Config.LOGTAG,"no from in: "+packet.toString()); return; } final String body = packet.getBody(); final String pgpEncrypted = (Config.supportOpenPgp()) ? packet.findChildContent(OpenPgpXep.ENCRYPTED_ELEMENT, OpenPgpXep.ENCRYPTED_NAMESPACE) : null; final Element axolotlEncrypted = (Config.supportOmemo()) ? packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX) : null; final Jid counterpart; final Jid to = packet.getTo(); if (packet.fromAccount(account)) { direction = (null == direction) ? MessageDirection.OUT : direction; counterpart = to != null ? to : account.getJid(); } else { direction = (null == direction) ? MessageDirection.IN : direction; counterpart = from; } if (MucPacketParser.extractAndExecuteInvite(account, packet)) { return; } Conversation conversation = null; if ((body != null || pgpEncrypted != null || axolotlEncrypted != null)) { boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT; conversation = XmppConnectionServiceAccessor.xmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat); final String remoteMsgId = packet.getId(); Message message = null; if (Config.supportOtr() && OtrUtil.isOtrBody(body)) { if (OtrUtil.isValidOtrMessagePacket(packet, account)) { message = OtrMessageParser.parseOtrChat(body, counterpart, remoteMsgId, conversation); } else { Logging.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": ignoring OTR message from "+counterpart+", addressed to: " + to); } } else { message = this.createMessage(conversation, counterpart, body, pgpEncrypted, axolotlEncrypted); } if (null != message) { message.setDirection(direction); message.setMessageStatus(MessageStatus.TRANSMITTING); message.setCarbon(isCarbon); message.setMamReceived(isMAM); message.setConfirmation(MessageParserUtil.extractMessageConfirmation(packet)); if (isTypeGroupChat && this.fixMessageDirectionForGroupChatIfNecessary(message, counterpart)) { return; } this.handleMessagePacketWithBodyOrEncryptedContent(message, packet, account, conversation, counterpart, isTypeGroupChat, remoteMsgId, serverMsgId, timestamp); this.markMessagesUnreadIfNecessary(message, isMAM, isMAMCatchup); if (!isMAM) { UiUpdateHelper.updateConversationUi(); } if (message.trusted() && MessageStatus.TRANSMITTING == message.getMessageStatus() // Todo: Every message should be in transmitting state here && ConversationsPlusPreferences.autoAcceptFileSize() > 0 && (message.isHttpUploaded() || MessageUtil.hasDownloadableLink(message))) { // Can this be checked by MessageUtil.needsDownload(message) ?? HttpRetrieveHead hrh = new HttpRetrieveHead(message); hrh.setListener(new AutomaticFileDownload()); hrh.retrieveAndSetContentTypeAndLength(); } else { MessageUtil.setAndSaveMessageStatus(message, MessageStatus.TRANSMITTED); } } } else if (!packet.hasChild("body")){ //no body this.handleMessagePacketWithoutBody(packet, account); } this.handleChatState(conversation, account, packet, counterpart); this.handleMessageReceipts(packet, account, from, counterpart, timestamp); this.handleEvent(packet, account, from); this.handleNick(packet, account, from); } private void markMessagesUnreadIfNecessary(Message message, boolean isMAM, boolean isCatchup) { Conversation conversation = message.getConversation(); Account account = conversation.getAccount(); if (!isMAM || isCatchup) { //either no mam or catchup if (MessageUtil.isOutgoingMessage(message)) { Logging.d("markRead", "MessageParser.onMessagePacketReceived1 (" + conversation.getName() + ")"); XmppConnectionServiceAccessor.xmppConnectionService.markRead(conversation); if (!isMAM) { account.activateGracePeriod(); } } else { // only not mam messages should be marked as unread if (!isMAM) { message.markUnread(); } } } } private void handleChatState(Conversation conversation, Account account, MessagePacket packet, Jid counterpart) { if (null == conversation) { conversation = XmppConnectionServiceAccessor.xmppConnectionService.find(account, counterpart.toBareJid()); } if (extractChatState(conversation, packet)) { UiUpdateHelper.updateConversationUi(); } } private boolean fixMessageDirectionForGroupChatIfNecessary(Message message, Jid counterpart) { Conversation conversation = message.getConversation(); if (MessageUtil.isIncomingMessage(message) && counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { // Message is sent from me message.setDirection(MessageDirection.OUT); // Not sure, but assuming String remoteMsgId = message.getRemoteMsgId(); if (MessageUtil.setAndSaveMessageStatus(conversation, remoteMsgId, MessageStatus.RECEIVED)) { // TODO: Check if this status is correct return true; } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) { Message existingMessage = conversation.findSentMessageWithBody(message.getBody()); if (existingMessage != null) { MessageUtil.setAndSaveMessageStatus(existingMessage, MessageStatus.RECEIVED); return true; } } } return false; } private Message createMessage(Conversation conversation, Jid counterpart, String body, String pgpEncrypted, Element axolotlEncrypted) { Message message; if (pgpEncrypted != null) { message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP); } else if (axolotlEncrypted != null) { message = AxolotlMessageParser.parseAxolotlChat(axolotlEncrypted, counterpart, counterpart, conversation); if (message == null) { return null; } } else { message = new Message(conversation, body, Message.ENCRYPTION_NONE); } return message; } private Message handleMessagePacketWithBodyOrEncryptedContent(Message message, MessagePacket packet, Account account, Conversation conversation, Jid counterpart, boolean isTypeGroupChat, String remoteMsgId, String serverMsgId, long timestamp) { if (serverMsgId == null) { serverMsgId = extractStanzaId(packet, isTypeGroupChat ? conversation.getJid().toBareJid() : account.getServer()); } message.setHttpUploaded(packet.hasChild(HttpUploadHint.HTTP_UPLOAD_HINT)); message.setCounterpart(counterpart); message.setRemoteMsgId(remoteMsgId); message.setServerMsgId(serverMsgId); message.setTime(timestamp); if (conversation.getMode() == Conversation.MODE_MULTI) { Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart()); message.setTrueCounterpart(trueCounterpart); if (trueCounterpart != null) { updateLastseen(timestamp, account, trueCounterpart, false); } if (!isTypeGroupChat) { message.setType(Message.TYPE_PRIVATE); } } else { updateLastseen(timestamp, account, packet.getFrom(), true); } conversation.add(message); if (message.getEncryption() == Message.ENCRYPTION_PGP) { conversation.getAccount().getPgpDecryptionService().add(message); } if (MessageUtil.isIncomingMessage(message) && OtrUtil.isOtrSessionActive(conversation) && !OtrUtil.isCounterpartOfActiveOtrSession(conversation, message.getCounterpart())) { conversation.endOtrIfNeeded(); } MessageParserUtil.extractFileParamsFromBody(message); if (message.getEncryption() == Message.ENCRYPTION_NONE || !ConversationsPlusPreferences.dontSaveEncrypted()) { DatabaseBackend.getInstance().createMessage(message); } return message; } private void handleNick(MessagePacket packet, Account account, Jid from) { // TODO: Is this a nick change? String nick = packet.findChildContent("nick", "http://jabber.org/protocol/nick"); if (nick != null) { Contact contact = account.getRoster().getContact(from); contact.setPresenceName(nick); } } private void handleEvent(MessagePacket packet, Account account, Jid from) { Element event = packet.findChild("event", "http://jabber.org/protocol/pubsub#event"); if (event != null) { parseEvent(event, from, account); } } private void handleMessageReceipts(MessagePacket packet, Account account, Jid from, Jid counterpart, long timestamp) { Element received = packet.findChild(ChatMarkers.RECEIVED); if (received == null) { received = packet.findChild(MessageDeliveryReceipts.RECEIVED); } if (received != null && !packet.fromAccount(account)) { MessageUtil.markMessageAsReceived(account, from.toBareJid(), received.getAttribute("id")); } Element displayed = packet.findChild(ChatMarkers.DISPLAYED); if (displayed != null) { if (packet.fromAccount(account)) { Conversation conversation = mXmppConnectionService.find(account, counterpart.toBareJid()); if (conversation != null) { Logging.d("markRead", "MessageParser.onMessagePacketReceived2 (" + conversation.getName() + ")"); mXmppConnectionService.markRead(conversation); } } else { updateLastseen(timestamp, account, packet.getFrom(), true); Message message = XmppConnectionServiceAccessor.xmppConnectionService.getMessage(account, from.toBareJid(), displayed.getAttribute("id")); if (null != message) { ConversationUtil.markMessagesAsDisplayedUpToMessage(message); } } } } private void handleMessagePacketWithoutBody(MessagePacket packet, Account account) { if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) { Jid from = packet.getFrom(); final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); boolean isMucStatusMessage = from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status"); Conversation conversation = mXmppConnectionService.find(account, from.toBareJid()); if (packet.hasChild("subject")) { if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0); String subject = packet.findChildContent("subject"); conversation.getMucOptions().setSubject(subject); final Bookmark bookmark = conversation.getBookmark(); if (bookmark != null && bookmark.getBookmarkName() == null) { if (bookmark.setBookmarkName(subject)) { mXmppConnectionService.pushBookmarks(account); } } UiUpdateHelper.updateConversationUi(); return; } } if (conversation != null && isMucStatusMessage) { for (Element child : mucUserElement.getChildren()) { if (child.getName().equals("status") && MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED.equals(child.getAttribute("code"))) { mXmppConnectionService.fetchConferenceConfiguration(conversation); } } } } } }