From d308329461d4c66bd9ef76e981f11428134ba6bb Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 16 Jul 2021 19:50:12 +0200 Subject: [PATCH] irclib: Fix support of power prefix chars in RPL_NAMREPLY nickFromHostmask now (legitimately) complains when it's getting @ or ! at the beginning of a hostmask; so we need to strip them before passing it to nickFromHostmask. Then re-add them before calling c.addUser, because it uses them to sort users in the right sets (ops/halfops/voices). Additionally, this commit replaces the hardcoded set of prefix chars (`@%+&~!`) with the one advertised in ISUPPORT when possible. --- src/irclib.py | 28 +++++++++++++++++++++------- test/test_irclib.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/irclib.py b/src/irclib.py index 34eba36ee..ccec3e420 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -389,15 +389,15 @@ class ChannelState(utils.python.Object): """Returns whether the given nick is an halfop, or an op.""" return nick in self.halfops or nick in self.ops - def addUser(self, user): + def addUser(self, user, prefix_chars='@%+&~!'): "Adds a given user to the ChannelState. Power prefixes are handled." - nick = user.lstrip('@%+&~!') + nick = user.lstrip(prefix_chars) if not nick: return # & is used to denote protected users in UnrealIRCd # ~ is used to denote channel owner in UnrealIRCd # ! is used to denote protected users in UltimateIRCd - while user and user[0] in '@%+&~!': + while user and user[0] in prefix_chars: (marker, user) = (user[0], user[1:]) assert user, 'Looks like my caller is passing chars, not nicks.' if marker in '@&~!': @@ -963,13 +963,27 @@ class IrcState(IrcCommandDispatcher, log.Firewalled): if channel not in self.channels: self.channels[channel] = ChannelState() c = self.channels[channel] + + # Set of prefixes servers may append before a NAMES reply when + # the user is op/halfop/voice/... + # https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.15 + prefix = self.supported.get('PREFIX') + if prefix is None: + prefix_chars = '@%+&~!' # see the comments in addUser + else: + prefix_chars = prefix.split(')', 1)[-1] + for item in items.split(): - if ircutils.isUserHostmask(item): - name = ircutils.nickFromHostmask(item) - self.nicksToHostmasks[name] = name + stripped_item = item.lstrip(prefix_chars) + item_prefix = item[0:-len(stripped_item)] + if ircutils.isUserHostmask(stripped_item): + nick = ircutils.nickFromHostmask(stripped_item) + self.nicksToHostmasks[nick] = nick + name = item_prefix + nick else: name = item - c.addUser(name) + c.addUser(name, prefix_chars=prefix_chars) + if type == '@': c.modes['s'] = None diff --git a/test/test_irclib.py b/test/test_irclib.py index 6de1f9308..0f1ef9b7a 100644 --- a/test/test_irclib.py +++ b/test/test_irclib.py @@ -529,6 +529,41 @@ class IrcStateTestCase(SupyTestCase): st = irclib.IrcState() self.assert_(st.addMsg(self.irc, ircmsgs.IrcMsg('MODE foo +i')) or 1) + def testNamreply(self): + """RPL_NAMREPLY / reply to NAMES""" + + # Just nicks (à la RFC 1459 + some common prefix chars) + st = irclib.IrcState() + st.addMsg(self.irc, ircmsgs.IrcMsg(command='353', + args=('*', '=', '#chan', 'nick1 @nick2 ~@nick3'))) + chan_st = st.channels['#chan'] + self.assertEqual(chan_st.users, ircutils.IrcSet(['nick1', 'nick2', 'nick3'])) + self.assertEqual(chan_st.ops, ircutils.IrcSet(['nick2', 'nick3'])) + self.assertEqual(st.nicksToHostmasks, ircutils.IrcDict({})) + + # with userhost-in-names + st = irclib.IrcState() + st.addMsg(self.irc, ircmsgs.IrcMsg(command='353', + args=('*', '=', '#chan', 'nick1!u1@h1 @nick2!u2@h2 ~@nick3!u3@h3'))) + chan_st = st.channels['#chan'] + self.assertEqual(chan_st.users, ircutils.IrcSet(['nick1', 'nick2', 'nick3'])) + self.assertEqual(chan_st.ops, ircutils.IrcSet(['nick2', 'nick3'])) + self.assertEqual(st.nicksToHostmasks['nick1'], 'nick1') + self.assertEqual(st.nicksToHostmasks['nick2'], 'nick2') + self.assertEqual(st.nicksToHostmasks['nick3'], 'nick3') + + # Prefixed with chars not in ISUPPORT PREFIX + st = irclib.IrcState() + st.supported['PREFIX'] = '(ov)@+' + st.addMsg(self.irc, ircmsgs.IrcMsg(command='353', + args=('*', '=', '#chan', 'nick1!u1@h1 @nick2!u2@h2 ~@nick3!u3@h3'))) + chan_st = st.channels['#chan'] + self.assertEqual(chan_st.users, ircutils.IrcSet(['nick1', 'nick2', '~@nick3'])) + self.assertEqual(chan_st.ops, ircutils.IrcSet(['nick2'])) + self.assertEqual(st.nicksToHostmasks['nick1'], 'nick1') + self.assertEqual(st.nicksToHostmasks['nick2'], 'nick2') + self.assertEqual(st.nicksToHostmasks['~@nick3'], '~@nick3') + class IrcCapsTestCase(SupyTestCase, CapNegMixin): def testReqLineLength(self):