~singpolyma/cheogram-android

ecd6e59cea1dda86406482c4774c821225474c4e — Stephen Paul Weber 5 months ago 77740d2
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 <nick> 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 <nick> 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 <nick> at all right now. May relax this in the
future).
Save their actual nick (whatever we're actually showing, so <nick> 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.
M src/main/java/eu/siacs/conversations/entities/MucOptions.java => src/main/java/eu/siacs/conversations/entities/MucOptions.java +50 -6
@@ 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<User> getFallbackUsersFromCryptoTargets() {
        List<User> 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() {

M src/main/java/eu/siacs/conversations/parser/AbstractParser.java => src/main/java/eu/siacs/conversations/parser/AbstractParser.java +9 -3
@@ 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);
		}

M src/main/java/eu/siacs/conversations/parser/PresenceParser.java => src/main/java/eu/siacs/conversations/parser/PresenceParser.java +3 -2
@@ 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<String> 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) {

M src/main/java/eu/siacs/conversations/services/NotificationService.java => src/main/java/eu/siacs/conversations/services/NotificationService.java +5 -2
@@ 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;
        }

M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +10 -8
@@ 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);
        }

M src/main/java/eu/siacs/conversations/ui/ConversationFragment.java => src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +9 -7
@@ 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<String> 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(),

M src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java => src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java +1 -1
@@ 68,7 68,7 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ
            final String needle = search.toLowerCase(Locale.getDefault());
            ArrayList<MucOptions.User> 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);

M src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java => src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +6 -0
@@ 582,6 582,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                    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);

M src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java => src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java +2 -2
@@ 71,7 71,7 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
        viewHolder.binding.getRoot().setOnClickListener(v -> {
            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<MucOptions.User, UserAdapter.ViewHo
            selectedUser = user;
            return false;
        });
        final String name = user.getName();
        final String name = user.getNick();
        final Contact contact = user.getContact();
        if (contact != null) {
            final String displayName = contact.getDisplayName();

M src/main/java/eu/siacs/conversations/ui/adapter/UserPreviewAdapter.java => src/main/java/eu/siacs/conversations/ui/adapter/UserPreviewAdapter.java +1 -1
@@ 38,7 38,7 @@ public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreview
        viewHolder.binding.getRoot().setOnClickListener(v -> {
            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);

M src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java => src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java +1 -1
@@ 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);

M src/main/java/eu/siacs/conversations/utils/UIHelper.java => src/main/java/eu/siacs/conversations/utils/UIHelper.java +6 -2
@@ 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();