From ecd6e59cea1dda86406482c4774c821225474c4e Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 25 Jun 2023 21:52:51 -0500 Subject: [PATCH] Allow any characters in MUC nicknames Some things are banned by resourceprep that a user might wish to use. If this happens, then punycode the resource so they can at least join the room. Always send their chosen nick (not a mangled version) as in presence. Always save their chosen nick (not a mangled version) in bookmarks (risky, since if they try to use this bookmark from a client not supporting this it may not let them join without changing nick). If we get a in presence, then we respect it if it matches the punycode decode of the resource (this is to prevent spoofing or double-naming since MUC services won't be validating the at all right now. May relax this in the future). Save their actual nick (whatever we're actually showing, so if we used that) into conversation attributes instead of assuming it will match the resource. Use resource for technical stuff (like PM addressing) and nick for display. Hilight on both nick and resource so that mentions from legacy clients work. --- .../conversations/entities/MucOptions.java | 56 +++++++++++++++++-- .../conversations/parser/AbstractParser.java | 12 +++- .../conversations/parser/PresenceParser.java | 5 +- .../services/NotificationService.java | 7 ++- .../services/XmppConnectionService.java | 18 +++--- .../ui/ConversationFragment.java | 16 +++--- .../conversations/ui/MucUsersActivity.java | 2 +- .../ui/adapter/MessageAdapter.java | 6 ++ .../conversations/ui/adapter/UserAdapter.java | 4 +- .../ui/adapter/UserPreviewAdapter.java | 2 +- .../ui/util/MucDetailsContextMenuHelper.java | 2 +- .../siacs/conversations/utils/UIHelper.java | 8 ++- 12 files changed, 103 insertions(+), 35 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index ef1952551f2a52c21b1e946c91f9bba0ec6dbeae..954d65dca6aecde0b179c9648d8b6dd267c274a8 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -53,7 +53,8 @@ public class MucOptions { public MucOptions(Conversation conversation) { this.account = conversation.getAccount(); this.conversation = conversation; - this.self = new User(this, createJoinJid(getProposedNick())); + final String nick = getProposedNick(conversation.getAttribute("mucNick")); + this.self = new User(this, createJoinJid(nick), nick); this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation")); this.self.role = Role.of(conversation.getAttribute("role")); } @@ -66,6 +67,7 @@ public class MucOptions { this.self = user; final boolean roleChanged = this.conversation.setAttribute("role", user.role.toString()); final boolean affiliationChanged = this.conversation.setAttribute("affiliation", user.affiliation.toString()); + this.conversation.setAttribute("mucNick", user.getNick()); return roleChanged || affiliationChanged; } @@ -291,6 +293,20 @@ public class MucOptions { return false; } + public User findUserByName(final String name) { + if (name == null) { + return null; + } + synchronized (users) { + for (User user : users) { + if (name.equals(user.getName())) { + return user; + } + } + } + return null; + } + public User findUserByFullJid(Jid jid) { if (jid == null) { return null; @@ -322,7 +338,7 @@ public class MucOptions { public User findOrCreateUserByRealJid(Jid jid, Jid fullJid) { User user = findUserByRealJid(jid); if (user == null) { - user = new User(this, fullJid); + user = new User(this, fullJid, null); user.setRealJid(jid); } return user; @@ -422,11 +438,17 @@ public class MucOptions { } public String getProposedNick() { + return getProposedNick(null); + } + + public String getProposedNick(final String mucNick) { final Bookmark bookmark = this.conversation.getBookmark(); final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick()); if (bookmarkedNick != null) { this.tookProposedNickFromBookmark = true; return bookmarkedNick; + } else if (mucNick != null) { + return mucNick; } else if (!conversation.getJid().isBareJid()) { return conversation.getJid().getResource(); } else { @@ -456,6 +478,14 @@ public class MucOptions { } public String getActualNick() { + if (this.self.getNick() != null) { + return this.self.getNick(); + } else { + return this.getProposedNick(); + } + } + + public String getActualName() { if (this.self.getName() != null) { return this.self.getName(); } else { @@ -507,7 +537,7 @@ public class MucOptions { private List getFallbackUsersFromCryptoTargets() { List users = new ArrayList<>(); for (Jid jid : conversation.getAcceptedCryptoTargets()) { - User user = new User(this, null); + User user = new User(this, null, null); user.setRealJid(jid); users.add(user); } @@ -581,10 +611,18 @@ public class MucOptions { } public Jid createJoinJid(String nick) { + return createJoinJid(nick, true); + } + + private Jid createJoinJid(String nick, boolean tryFix) { try { return conversation.getJid().withResource(nick); } catch (final IllegalArgumentException e) { - return null; + try { + return tryFix ? createJoinJid(gnu.inet.encoding.Punycode.encode(nick), false) : null; + } catch (final gnu.inet.encoding.PunycodeException e2) { + return null; + } } } @@ -748,20 +786,26 @@ public class MucOptions { private Affiliation affiliation = Affiliation.NONE; private Jid realJid; private Jid fullJid; + private String nick; private long pgpKeyId = 0; private Avatar avatar; private final MucOptions options; private ChatState chatState = Config.DEFAULT_CHAT_STATE; - public User(MucOptions options, Jid fullJid) { + public User(MucOptions options, Jid fullJid, final String nick) { this.options = options; this.fullJid = fullJid; + this.nick = nick == null ? getName() : nick; } public String getName() { return fullJid == null ? null : fullJid.getResource(); } + public String getNick() { + return nick; + } + public Role getRole() { return this.role; } @@ -869,7 +913,7 @@ public class MucOptions { @Override public String toString() { - return "[fulljid:" + fullJid + ",realjid:" + realJid + ",affiliation" + affiliation.toString() + "]"; + return "[fulljid:" + fullJid + ",realjid:" + realJid + ",nick:" + nick + ",affiliation" + affiliation.toString() + "]"; } public boolean realJidMatchesAccount() { diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 5de637399fb571bfaff80675cee50a2275cc8d60..75244edb5d5ff2cfe7366ec3ee87c7d9298a4605 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -132,10 +132,10 @@ public abstract class AbstractParser { } public static MucOptions.User parseItem(Conversation conference, Element item) { - return parseItem(conference,item, null); + return parseItem(conference,item,null,null); } - public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid) { + public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid, final String nickname) { final String local = conference.getJid().getLocal(); final String domain = conference.getJid().getDomain().toEscapedString(); String affiliation = item.getAttribute("affiliation"); @@ -149,7 +149,13 @@ public abstract class AbstractParser { } } Jid realJid = item.getAttributeAsJid("jid"); - MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid); + if (fullJid != null) nick = fullJid.getResource(); + try { + if (nickname != null && nick != null && !nick.equals(nickname) && gnu.inet.encoding.Punycode.decode(nick).equals(nickname)) { + nick = nickname; + } + } catch (final gnu.inet.encoding.PunycodeException e) { } + MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid, nick); if (InvalidJid.isValid(realJid)) { user.setRealJid(realJid); } diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 8ad582b17679165be3e6dc903d08492dd1d11a03..89772984a2ede6225665f3c7796d332ddc5652b5 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -63,6 +63,7 @@ public class PresenceParser extends AbstractParser implements if (!from.isBareJid()) { final String type = packet.getAttribute("type"); final Element x = packet.findChild("x", Namespace.MUC_USER); + final Element nick = packet.findChild("nick", Namespace.NICK); Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); final List codes = getStatusCodes(x); if (type == null) { @@ -70,7 +71,7 @@ public class PresenceParser extends AbstractParser implements Element item = x.findChild("item"); if (item != null && !from.isBareJid()) { mucOptions.setError(MucOptions.Error.NONE); - MucOptions.User user = parseItem(conversation, item, from); + MucOptions.User user = parseItem(conversation, item, from, nick == null ? null : nick.getContent()); if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && jid.equals(InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"))))) { if (mucOptions.setOnline()) { mXmppConnectionService.getAvatarService().clear(mucOptions); @@ -174,7 +175,7 @@ public class PresenceParser extends AbstractParser implements } else if (!from.isBareJid()){ Element item = x.findChild("item"); if (item != null) { - mucOptions.updateUser(parseItem(conversation, item, from)); + mucOptions.updateUser(parseItem(conversation, item, from, nick == null ? null : nick.getContent())); } MucOptions.User user = mucOptions.deleteUser(from); if (user != null) { diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index c6e6f6637c8c2fe73fd8e3854ad2e3d61ab84106..07b2f48ed47a17c0e59a549be0f85ea3b0bff67e 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -1817,11 +1817,14 @@ public class NotificationService { final String nick = conversation.getMucOptions().getActualNick(); final Pattern highlight = generateNickHighlightPattern(nick); - if (message.getBody() == null || nick == null) { + final String name = conversation.getMucOptions().getActualName(); + final Pattern highlightName = generateNickHighlightPattern(name); + if (message.getBody() == null || (nick == null && name == null)) { return false; } final Matcher m = highlight.matcher(message.getBody()); - return (m.find() || message.isPrivateMessage()); + final Matcher m2 = highlightName.matcher(message.getBody()); + return (m.find() || m2.find() || message.isPrivateMessage()); } else { return false; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 8096aefb2c636fa864860801e3cc0f0134f5f211..e9acea44b2d8a93cefc281ec6852e89340bf66ba 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -3037,7 +3037,7 @@ public class XmppConnectionService extends Service { final Jid joinJid = mucOptions.getSelf().getFullJid(); Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": joining conversation " + joinJid.toString()); - PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null); + PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null, mucOptions.getSelf().getNick()); packet.setTo(joinJid); Element x = packet.addChild("x", "http://jabber.org/protocol/muc"); if (conversation.getMucOptions().getPassword() != null) { @@ -3303,17 +3303,18 @@ public class XmppConnectionService extends Service { databaseBackend.updateConversation(conversation); } + final String nick = self.getNick(); final Bookmark bookmark = conversation.getBookmark(); final String bookmarkedNick = bookmark == null ? null : bookmark.getNick(); - if (bookmark != null && (tookProposedNickFromBookmark || TextUtils.isEmpty(bookmarkedNick)) && !full.getResource().equals(bookmarkedNick)) { + if (bookmark != null && (tookProposedNickFromBookmark || TextUtils.isEmpty(bookmarkedNick)) && !nick.equals(bookmarkedNick)) { final Account account = conversation.getAccount(); final String defaultNick = MucOptions.defaultNick(account); - if (TextUtils.isEmpty(bookmarkedNick) && full.getResource().equals(defaultNick)) { + if (TextUtils.isEmpty(bookmarkedNick) && nick.equals(defaultNick)) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not overwrite empty bookmark nick with default nick for " + conversation.getJid().asBareJid()); return; } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persist nick '" + full.getResource() + "' into bookmark for " + conversation.getJid().asBareJid()); - bookmark.setNick(full.getResource()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persist nick '" + nick + "' into bookmark for " + conversation.getJid().asBareJid()); + bookmark.setNick(nick); createBookmark(bookmark.getAccount(), bookmark); } } @@ -3339,7 +3340,7 @@ public class XmppConnectionService extends Service { } }); - final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous()); + final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); packet.setTo(joinJid); sendPresencePacket(account, packet); } else { @@ -4183,7 +4184,7 @@ public class XmppConnectionService extends Service { if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) { final MucOptions mucOptions = conversation.getMucOptions(); if (mucOptions.online()) { - PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous()); + PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous(), mucOptions.getSelf().getNick()); packet.setTo(mucOptions.getSelf().getFullJid()); connection.sendPresencePacket(packet); } @@ -5107,7 +5108,8 @@ public class XmppConnectionService extends Service { public void saveConversationAsBookmark(Conversation conversation, String name) { final Account account = conversation.getAccount(); final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid()); - final String nick = conversation.getJid().getResource(); + String nick = conversation.getMucOptions().getActualNick(); + if (nick == null) nick = conversation.getJid().getResource(); if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) { bookmark.setNick(nick); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 85620ef647414e846efd03117430f2ae668d5957..50f001711c3a7d71e6896e61715af08cfba57fd1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -991,10 +991,13 @@ public class ConversationFragment extends XmppFragment } else if (multi && conversation.getNextCounterpart() != null) { this.binding.textinput.setHint(R.string.send_message); this.binding.textInputHint.setVisibility(View.VISIBLE); + final MucOptions.User user = conversation.getMucOptions().findUserByName(conversation.getNextCounterpart().getResource()); + String nick = user == null ? null : user.getNick(); + if (nick == null) nick = conversation.getNextCounterpart().getResource(); this.binding.textInputHint.setText( getString( R.string.send_private_message_to, - conversation.getNextCounterpart().getResource())); + nick)); binding.conversationViewPager.setCurrentItem(0); } else if (multi && !conversation.getMucOptions().participating()) { this.binding.textInputHint.setVisibility(View.GONE); @@ -3889,7 +3892,7 @@ public class ConversationFragment extends XmppFragment } List completions = new ArrayList<>(); for (MucOptions.User user : conversation.getMucOptions().getUsers()) { - String name = user.getName(); + String name = user.getNick(); if (name != null && name.startsWith(incomplete)) { completions.add(name + (firstWord ? ": " : " ")); } @@ -4093,10 +4096,9 @@ public class ConversationFragment extends XmppFragment if (mucOptions.participating() || ((Conversation) message.getConversation()).getNextCounterpart() != null) { - if (!mucOptions.isUserInRoom(user) - && mucOptions.findUserByRealJid( - tcp == null ? null : tcp.asBareJid()) - == null) { + MucOptions.User mucUser = mucOptions.findUserByFullJid(user); + MucOptions.User tcpMucUser = mucOptions.findUserByRealJid(tcp == null ? null : tcp.asBareJid()); + if (mucUser == null && tcpMucUser == null) { Toast.makeText( getActivity(), activity.getString( @@ -4105,7 +4107,7 @@ public class ConversationFragment extends XmppFragment Toast.LENGTH_SHORT) .show(); } - highlightInConference(user.getResource()); + highlightInConference(mucUser == null ? (tcpMucUser == null ? user.getResource() : tcpMucUser.getNick()) : mucUser.getNick()); } else { Toast.makeText( getActivity(), diff --git a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java index 2f05be90f5fd712214c461dd1088a2003b997a52..dfa5e4292b67682d0b7a470b250fca08787c2311 100644 --- a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java @@ -68,7 +68,7 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ final String needle = search.toLowerCase(Locale.getDefault()); ArrayList filtered = new ArrayList<>(); for(MucOptions.User user : allUsers) { - final String name = user.getName(); + final String name = user.getNick(); final Contact contact = user.getContact(); if (name != null && name.toLowerCase(Locale.getDefault()).contains(needle) || contact != null && contact.getDisplayName().toLowerCase(Locale.getDefault()).contains(needle)) { filtered.add(user); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index dd8f47a62df58ffebd0dc191c191b06d7cbf7f73..5eae1588399a6f2db712f43bf48b7c5a5d19318c 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -582,6 +582,12 @@ public class MessageAdapter extends ArrayAdapter { while (matcher.find()) { body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } + + pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName()); + matcher = pattern.matcher(body); + while (matcher.find()) { + body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } } } Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java index 41bfb24a1b8110768e199ff4fb8c82aa0d7ee87c..5d917496e9c7a152fecbce5c71377a97a7510a56 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java @@ -71,7 +71,7 @@ public class UserAdapter extends ListAdapter { final XmppActivity activity = XmppActivity.find(v); if (activity != null) { - activity.highlightInMuc(user.getConversation(), user.getName()); + activity.highlightInMuc(user.getConversation(), user.getNick()); } }); viewHolder.binding.getRoot().setTag(user); @@ -80,7 +80,7 @@ public class UserAdapter extends ListAdapter { final XmppActivity activity = XmppActivity.find(v); if (activity != null) { - activity.highlightInMuc(user.getConversation(), user.getName()); + activity.highlightInMuc(user.getConversation(), user.getNick()); } }); viewHolder.binding.getRoot().setOnCreateContextMenuListener(this); diff --git a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java index 5bd90cdd77971ab7e58bd102532d4627b44406a2..d887f7d6032895af5bb56e63dae8b4a4fceb6542 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java @@ -43,7 +43,7 @@ public final class MucDetailsContextMenuHelper { } else if (user.getRealJid() != null) { name = user.getRealJid().asBareJid().toString(); } else { - name = user.getName(); + name = user.getNick(); } menu.setHeaderTitle(name); MucDetailsContextMenuHelper.configureMucDetailsContextMenu(activity, menu, user.getConversation(), user); diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index d98db6df87c167c5de0c88a2e65e811228bbaa79..17d40326b805222413341c2e7934c3a9bf69e701 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -462,7 +462,7 @@ public class UIHelper { if (contact != null) { return contact.getDisplayName(); } else { - final String name = user.getName(); + final String name = user.getNick(); if (name != null) { return name; } @@ -540,6 +540,10 @@ public class UIHelper { if (contact != null) { return contact.getDisplayName(); } else { + if (conversation instanceof Conversation) { + final MucOptions.User user = ((Conversation) conversation).getMucOptions().findUserByFullJid(message.getCounterpart()); + if (user != null) return getDisplayName(user); + } return getDisplayedMucCounterpart(message.getCounterpart()); } } else { @@ -547,7 +551,7 @@ public class UIHelper { } } else { if (conversation instanceof Conversation && conversation.getMode() == Conversation.MODE_MULTI) { - return ((Conversation) conversation).getMucOptions().getSelf().getName(); + return ((Conversation) conversation).getMucOptions().getSelf().getNick(); } else { final Account account = conversation.getAccount(); final Jid jid = account.getJid();