From cd46067681c23551f3a7ba3eb18409c291d790ba Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Jan 2025 10:34:23 +0100 Subject: [PATCH] ensure that message is parsed into bubble OR receipt, marker, reaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit ensures that Conversations always parses a message into a bubble or as meta data (receipt, display marker, reaction) never both and with a preference for bubble. Conversations goes through great length to ensure that all participants of a group chat have a similiar (or ideally the same) view of the chat history. For example Conversations doesn’t use xhtml, shows a language tag when the content is available in multiple languages, etc. So a message that has both a body and a reaction for example shouldn't show differently in clients that support reactions and clients that don't. Take the following example: ```xml Conversations users are stupid! 💩 ``` This message should render as what it is: A message and the body shouldn't be silently discarded for clients that support reactions. (The same goes for display markers and receipts) Fallback messages for something that is not supposed to render as a bubble should be discouraged. --- .../conversations/parser/MessageParser.java | 1021 +++++++++++------ .../android/xmpp/model/Extension.java | 11 +- 2 files changed, 704 insertions(+), 328 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 3ddb981240cf68030eb37bfbd656b71459b1282e..80430e38477b44959216b84e0aec0559d04dc2d0 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -2,22 +2,8 @@ package eu.siacs.conversations.parser; import android.util.Log; import android.util.Pair; - import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.function.Consumer; - import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -47,22 +33,36 @@ import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnMessagePacketReceived; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.axolotl.Encrypted; import im.conversations.android.xmpp.model.carbons.Received; import im.conversations.android.xmpp.model.carbons.Sent; import im.conversations.android.xmpp.model.correction.Replace; import im.conversations.android.xmpp.model.forward.Forwarded; +import im.conversations.android.xmpp.model.markers.Displayed; import im.conversations.android.xmpp.model.occupant.OccupantId; import im.conversations.android.xmpp.model.reactions.Reactions; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; -public class MessageParser extends AbstractParser implements Consumer { +public class MessageParser extends AbstractParser + implements Consumer { - private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); + private static final SimpleDateFormat TIME_FORMAT = + new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); private static final List JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish"); @@ -71,7 +71,8 @@ public class MessageParser extends AbstractParser implements Consumer deviceIds = IqParser.deviceIds(item); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... "); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received PEP device list " + + deviceIds + + " update from " + + from + + ", processing... "); final AxolotlService axolotlService = account.getAxolotlService(); axolotlService.registerDevices(from, deviceIds); } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) { @@ -260,7 +303,8 @@ public class MessageParser extends AbstractParser implements Consumer f; f = getForwardedMessagePacket(original, Received.class); f = f == null ? getForwardedMessagePacket(original, Sent.class) : f; @@ -451,19 +539,21 @@ public class MessageParser extends AbstractParser implements Consumer fallbacksBySourceId = Collections.emptySet(); if (conversationMultiMode) { - final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); + final Jid fallback = + conversation.getMucOptions().getTrueCounterpart(counterpart); origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback); if (origin == null) { try { - fallbacksBySourceId = account.getAxolotlService().findCounterpartsBySourceId(XmppAxolotlMessage.parseSourceId(axolotlEncrypted)); + fallbacksBySourceId = + account.getAxolotlService() + .findCounterpartsBySourceId( + XmppAxolotlMessage.parseSourceId( + axolotlEncrypted)); } catch (IllegalArgumentException e) { - //ignoring + // ignoring } } - if (origin == null && fallbacksBySourceId.size() == 0) { - Log.d(Config.LOGTAG, "axolotl message in anonymous conference received and no possible fallbacks"); + if (origin == null && fallbacksBySourceId.isEmpty()) { + Log.d( + Config.LOGTAG, + "axolotl message in anonymous conference received and no possible" + + " fallbacks"); return; } } else { @@ -615,17 +757,40 @@ public class MessageParser extends AbstractParser implements Consumer 0); - final LocalizedContent subject = packet.findInternationalizedChildContentInDefaultNamespace("subject"); - if (subject != null && conversation.getMucOptions().setSubject(subject.content)) { + final LocalizedContent subject = + packet.findInternationalizedChildContentInDefaultNamespace( + "subject"); + if (subject != null + && conversation.getMucOptions().setSubject(subject.content)) { mXmppConnectionService.updateConversation(conversation); } mXmppConnectionService.updateConversationUi(); @@ -900,7 +1159,10 @@ public class MessageParser extends AbstractParser implements Consumer cryptoTargets = conversation.getAcceptedCryptoTargets(); if (cryptoTargets.remove(user.getRealJid())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": removed " + + jid + + " from crypto targets of " + + conversation.getName()); conversation.setAcceptedCryptoTargets(cryptoTargets); mXmppConnectionService.updateConversation(conversation); } @@ -935,7 +1209,8 @@ public class MessageParser extends AbstractParser implements Consumer getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, Class clazz) { + private static Pair + getForwardedMessagePacket( + final im.conversations.android.xmpp.model.stanza.Message original, + Class clazz) { final var extension = original.getExtension(clazz); final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class); if (forwarded == null) { @@ -1267,36 +1612,51 @@ public class MessageParser extends AbstractParser implements Consumer(forwardedMessage,timestamp); + return new Pair<>(forwardedMessage, timestamp); } - private static Pair getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, final String name, final String namespace) { + private static Pair + getForwardedMessagePacket( + final im.conversations.android.xmpp.model.stanza.Message original, + final String name, + final String namespace) { final Element wrapper = original.findChild(name, namespace); - final var forwardedElement = wrapper == null ? null : wrapper.findChild("forwarded",Namespace.FORWARD); + final var forwardedElement = + wrapper == null ? null : wrapper.findChild("forwarded", Namespace.FORWARD); if (forwardedElement instanceof Forwarded forwarded) { final Long timestamp = AbstractParser.parseTimestamp(forwarded, null); final var forwardedMessage = forwarded.getMessage(); if (forwardedMessage == null) { return null; } - return new Pair<>(forwardedMessage,timestamp); + return new Pair<>(forwardedMessage, timestamp); } return null; } - private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) { - final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); + private void dismissNotification( + Account account, Jid counterpart, MessageArchiveService.Query query, final String id) { + final Conversation conversation = + mXmppConnectionService.find(account, counterpart.asBareJid()); if (conversation != null && (query == null || query.isCatchup())) { final String displayableId = conversation.findMostRecentRemoteDisplayableId(); if (displayableId != null && displayableId.equals(id)) { mXmppConnectionService.markRead(conversation); } else { - Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": received dismissing display marker that did not match our last id in that conversation"); + Log.w( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received dismissing display marker that did not match our last" + + " id in that conversation"); } } } - private void processMessageReceipts(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet, final String remoteMsgId, MessageArchiveService.Query query) { + private void processMessageReceipts( + final Account account, + final im.conversations.android.xmpp.model.stanza.Message packet, + final String remoteMsgId, + MessageArchiveService.Query query) { final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); final boolean request = packet.hasChild("request", "urn:xmpp:receipts"); if (query == null) { @@ -1308,11 +1668,15 @@ public class MessageParser extends AbstractParser implements Consumer 0) { - final var receipt = mXmppConnectionService.getMessageGenerator().received(account, - packet.getFrom(), - remoteMsgId, - receiptsNamespaces, - packet.getType()); + final var receipt = + mXmppConnectionService + .getMessageGenerator() + .received( + account, + packet.getFrom(), + remoteMsgId, + receiptsNamespaces, + packet.getType()); mXmppConnectionService.sendMessagePacket(account, receipt); } } else if (query.isCatchup()) { @@ -1323,8 +1687,15 @@ public class MessageParser extends AbstractParser implements Consumer E getOnlyExtension(final Class clazz) { + final var extensions = getExtensions(clazz); + if (extensions.size() == 1) { + return Iterables.getOnlyElement(extensions); + } + return null; + } + public Collection getExtensions(final Class clazz) { return Collections2.transform( Collections2.filter(this.children, clazz::isInstance), clazz::cast);