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.
This commit is contained in:
Valentin Lorentz 2021-07-16 19:50:12 +02:00
parent aa6bd7257d
commit d308329461
2 changed files with 56 additions and 7 deletions

View File

@ -389,15 +389,15 @@ class ChannelState(utils.python.Object):
"""Returns whether the given nick is an halfop, or an op.""" """Returns whether the given nick is an halfop, or an op."""
return nick in self.halfops or nick in self.ops 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." "Adds a given user to the ChannelState. Power prefixes are handled."
nick = user.lstrip('@%+&~!') nick = user.lstrip(prefix_chars)
if not nick: if not nick:
return return
# & is used to denote protected users in UnrealIRCd # & is used to denote protected users in UnrealIRCd
# ~ is used to denote channel owner in UnrealIRCd # ~ is used to denote channel owner in UnrealIRCd
# ! is used to denote protected users in UltimateIRCd # ! 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:]) (marker, user) = (user[0], user[1:])
assert user, 'Looks like my caller is passing chars, not nicks.' assert user, 'Looks like my caller is passing chars, not nicks.'
if marker in '@&~!': if marker in '@&~!':
@ -963,13 +963,27 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
if channel not in self.channels: if channel not in self.channels:
self.channels[channel] = ChannelState() self.channels[channel] = ChannelState()
c = self.channels[channel] 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(): for item in items.split():
if ircutils.isUserHostmask(item): stripped_item = item.lstrip(prefix_chars)
name = ircutils.nickFromHostmask(item) item_prefix = item[0:-len(stripped_item)]
self.nicksToHostmasks[name] = name if ircutils.isUserHostmask(stripped_item):
nick = ircutils.nickFromHostmask(stripped_item)
self.nicksToHostmasks[nick] = nick
name = item_prefix + nick
else: else:
name = item name = item
c.addUser(name) c.addUser(name, prefix_chars=prefix_chars)
if type == '@': if type == '@':
c.modes['s'] = None c.modes['s'] = None

View File

@ -529,6 +529,41 @@ class IrcStateTestCase(SupyTestCase):
st = irclib.IrcState() st = irclib.IrcState()
self.assert_(st.addMsg(self.irc, ircmsgs.IrcMsg('MODE foo +i')) or 1) 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): class IrcCapsTestCase(SupyTestCase, CapNegMixin):
def testReqLineLength(self): def testReqLineLength(self):