Merge pull request #1546 from progval/account-ban

Add support for account-based channel bans
This commit is contained in:
Val Lorentz 2024-07-24 21:12:01 +02:00 committed by GitHub
commit 10a341c70c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 512 additions and 38 deletions

View File

@ -163,8 +163,9 @@ class AutoMode(callbacks.Plugin):
# We're not in the channel anymore.
pass
schedule.addEvent(unban, time.time()+period)
banmask =conf.supybot.protocols.irc.banmask.makeBanmask(msg.prefix)
irc.queueMsg(ircmsgs.ban(channel, banmask))
banmasks = conf.supybot.protocols.irc.banmask.makeExtBanmasks(
msg.prefix, channel=channel, network=irc.network)
irc.queueMsg(ircmsgs.bans(channel, banmasks))
irc.queueMsg(ircmsgs.kick(channel, msg.nick))
try:

View File

@ -321,13 +321,16 @@ class Channel(callbacks.Plugin):
--exact bans only the exact hostmask; --nick bans just the nick;
--user bans just the user, and --host bans just the host
You can combine the --nick, --user, and --host options as you choose.
If --account is provided and the user is logged in and the network
supports account bans, this will ban the user's account instead.
<channel> is only necessary if the message isn't sent in the channel itself.
"""
self._ban(irc, msg, args,
channel, optlist, bannedNick, expiry, reason, True)
kban = wrap(kban,
['op',
getopts({'exact':'', 'nick':'', 'user':'', 'host':''}),
getopts({'exact':'', 'nick':'', 'user':'', 'host':'',
'account': ''}),
('haveHalfop+', _('kick or ban someone')),
'nickInChannel',
optional('expiry', 0),
@ -343,13 +346,16 @@ class Channel(callbacks.Plugin):
don't specify a number of seconds) it will ban the person indefinitely.
--exact can be used to specify an exact hostmask.
You can combine the --nick, --user, and --host options as you choose.
If --account is provided and the user is logged in and the network
supports account bans, this will ban the user's account instead.
<channel> is only necessary if the message isn't sent in the channel itself.
"""
self._ban(irc, msg, args,
channel, optlist, bannedNick, expiry, None, False)
iban = wrap(iban,
['op',
getopts({'exact':'', 'nick':'', 'user':'', 'host':''}),
getopts({'exact':'', 'nick':'', 'user':'', 'host':'',
'account': ''}),
('haveHalfop+', _('ban someone')),
first('nick', 'hostmask'),
optional('expiry', 0)])
@ -362,18 +368,22 @@ class Channel(callbacks.Plugin):
try:
bannedHostmask = irc.state.nickToHostmask(target)
banmaskstyle = conf.supybot.protocols.irc.banmask
banmask = banmaskstyle.makeBanmask(bannedHostmask, [o[0] for o in optlist])
banmasks = banmaskstyle.makeExtBanmasks(
bannedHostmask, [o[0] for o in optlist],
channel=channel, network=irc.network)
except KeyError:
if not conf.supybot.protocols.irc.strictRfc() and \
target.startswith('$'):
# Select the last part, or the whole target:
bannedNick = target.split(':')[-1]
banmask = bannedHostmask = target
bannedHostmask = target
banmasks = [bannedHostmask]
else:
irc.error(format(_('I haven\'t seen %s.'), bannedNick), Raise=True)
else:
bannedNick = ircutils.nickFromHostmask(target)
banmask = bannedHostmask = target
bannedHostmask = target
banmasks = [bannedHostmask]
if not irc.isNick(bannedNick):
self.log.warning('%q tried to kban a non nick: %q',
msg.prefix, bannedNick)
@ -389,30 +399,47 @@ class Channel(callbacks.Plugin):
if not reason:
reason = msg.nick
capability = ircdb.makeChannelCapability(channel, 'op')
# Check (again) that they're not trying to make us kickban ourself.
if ircutils.hostmaskPatternEqual(banmask, irc.prefix):
if ircutils.hostmaskPatternEqual(bannedHostmask, irc.prefix):
self_account_extban = ircutils.accountExtban(irc, irc.nick)
for banmask in banmasks:
if ircutils.hostmaskPatternEqual(banmask, irc.prefix):
if ircutils.hostmaskPatternEqual(bannedHostmask, irc.prefix):
self.log.warning('%q tried to make me kban myself.',msg.prefix)
irc.error(_('I cowardly refuse to ban myself.'))
return
else:
self.log.warning('Using exact hostmask since banmask would '
'ban myself.')
banmasks = [bannedHostmask]
elif self_account_extban is not None \
and banmask.lower() == self_account_extban.lower():
self.log.warning('%q tried to make me kban myself.',msg.prefix)
irc.error(_('I cowardly refuse to ban myself.'))
return
else:
self.log.warning('Using exact hostmask since banmask would '
'ban myself.')
banmask = bannedHostmask
# Now, let's actually get to it. Check to make sure they have
# #channel,op and the bannee doesn't have #channel,op; or that the
# bannee and the banner are both the same person.
def doBan():
if irc.state.channels[channel].isOp(bannedNick):
irc.queueMsg(ircmsgs.deop(channel, bannedNick))
irc.queueMsg(ircmsgs.ban(channel, banmask))
irc.queueMsg(ircmsgs.bans(channel, banmasks))
if kick:
irc.queueMsg(ircmsgs.kick(channel, bannedNick, reason))
if expiry > 0:
def f():
if channel in irc.state.channels and \
banmask in irc.state.channels[channel].bans:
irc.queueMsg(ircmsgs.unban(channel, banmask))
if channel not in irc.state.channels:
return
remaining_banmasks = [
banmask
for banmask in banmasks
if banmask in irc.state.channels[channel].bans
]
if remaining_banmasks:
irc.queueMsg(ircmsgs.unbans(
channel, remaining_banmasks))
schedule.addEvent(f, expiry)
if bannedNick == msg.nick:
doBan()
@ -583,7 +610,7 @@ class Channel(callbacks.Plugin):
hostmask = wrap(hostmask, ['op', ('haveHalfop+', _('ban someone')), 'text'])
@internationalizeDocstring
def add(self, irc, msg, args, channel, banmask, expires):
def add(self, irc, msg, args, channel, banmasks, expires):
"""[<channel>] <nick|hostmask> [<expires>]
If you have the #channel,op capability, this will effect a
@ -597,10 +624,15 @@ class Channel(callbacks.Plugin):
channel itself.
"""
c = ircdb.channels.getChannel(channel)
c.addBan(banmask, expires)
if isinstance(banmasks, str):
banmasks = [banmasks]
for banmask in banmasks:
c.addBan(banmask, expires)
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
add = wrap(add, ['op', first('hostmask', 'banmask'), additional('expiry', 0)])
add = wrap(add, ['op',
first('hostmask', 'extbanmasks'),
additional('expiry', 0)])
@internationalizeDocstring
def remove(self, irc, msg, args, channel, banmask):

View File

@ -161,9 +161,13 @@ class ChannelTestCase(ChannelPluginTestCase):
self.assertTrue(m.command == 'MODE' and
m.args == (self.channel, '+v', 'bar'))
def assertKban(self, query, hostmask, **kwargs):
def assertKban(self, query, *hostmasks, **kwargs):
m = self.getMsg(query, **kwargs)
self.assertEqual(m, ircmsgs.ban(self.channel, hostmask))
self.assertEqual(m.command, "MODE", m)
self.assertEqual(m.args[0], self.channel, m)
self.assertEqual(m.args[1], "+" + "b" * len(hostmasks), m)
self.assertCountEqual(m.args[2:], hostmasks, m)
m = self.getMsg(' ')
self.assertEqual(m.command, 'KICK')
def assertBan(self, query, hostmask, **kwargs):
@ -185,6 +189,30 @@ class ChannelTestCase(ChannelPluginTestCase):
self.assertBan('iban $a:nyuszika7h', '$a:nyuszika7h')
self.assertNotError('unban $a:nyuszika7h')
def testWontIbanItself(self):
self.irc.state.supported['ACCOUNTEXTBAN'] = 'a,account'
self.irc.state.supported['EXTBAN'] = '~,abc'
self.irc.feedMsg(ircmsgs.join(self.channel,
prefix='foobar!user@host.domain.tld'))
self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick))
# not authenticated, falls back to hostname and notices the match
self.assertError('iban --account ' + self.nick)
self.irc.feedMsg(ircmsgs.IrcMsg(prefix=self.prefix, command='ACCOUNT',
args=['botaccount']))
# notices the matching account
self.assertError('iban --account ' + self.nick)
self.irc.feedMsg(ircmsgs.IrcMsg(prefix='othernick!otheruser@otherhost',
command='ACCOUNT',
args=['botaccount']))
# ditto
self.assertError('iban --account othernick')
def testKban(self):
self.irc.prefix = 'something!else@somehwere.else'
self.irc.nick = 'something'
@ -219,11 +247,108 @@ class ChannelTestCase(ChannelPluginTestCase):
self.assertRegexp('kban adlkfajsdlfkjsd', 'adlkfajsdlfkjsd is not in')
def testAccountKbanNoAccount(self):
self.irc.prefix = 'something!else@somehwere.else'
self.irc.nick = 'something'
self.irc.state.supported['ACCOUNTEXTBAN'] = 'a,account'
self.irc.state.supported['EXTBAN'] = '~,abc'
def join():
self.irc.feedMsg(ircmsgs.join(
self.channel, prefix='foobar!user@host.domain.tld'))
join()
self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick))
self.assertKban('kban --account --exact foobar',
'foobar!user@host.domain.tld')
join()
self.assertKban('kban --account foobar',
'*!*@host.domain.tld')
join()
with conf.supybot.protocols.irc.banmask.context(['user', 'host']):
# falls back from --account to config
self.assertKban('kban --account foobar',
'*!user@host.domain.tld')
join()
with conf.supybot.protocols.irc.banmask.context(['account']):
# falls back from --account to config, then to only the host
self.assertKban('kban --account foobar',
'*!*@host.domain.tld')
join()
self.assertKban('kban --account --host foobar',
'*!*@host.domain.tld')
def testAccountKbanLoggedOut(self):
self.irc.prefix = 'something!else@somehwere.else'
self.irc.nick = 'something'
self.irc.state.supported['ACCOUNTEXTBAN'] = 'a,account'
self.irc.state.supported['EXTBAN'] = '~,abc'
self.irc.feedMsg(ircmsgs.IrcMsg(
prefix='foobar!user@host.domain.tld',
command='ACCOUNT', args=['*']))
def join():
self.irc.feedMsg(ircmsgs.join(
self.channel, prefix='foobar!user@host.domain.tld'))
join()
self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick))
self.assertKban('kban --account --exact foobar',
'foobar!user@host.domain.tld')
join()
self.assertKban('kban --account foobar',
'*!*@host.domain.tld')
join()
with conf.supybot.protocols.irc.banmask.context(['user', 'host']):
# falls back from --account to config
self.assertKban('kban --account foobar',
'*!user@host.domain.tld')
join()
with conf.supybot.protocols.irc.banmask.context(['account']):
# falls back from --account to config, then to only the host
self.assertKban('kban --account foobar',
'*!*@host.domain.tld')
join()
self.assertKban('kban --account --host foobar',
'*!*@host.domain.tld')
def testAccountKbanLoggedIn(self):
self.irc.prefix = 'something!else@somehwere.else'
self.irc.nick = 'something'
self.irc.state.supported['ACCOUNTEXTBAN'] = 'a,account'
self.irc.state.supported['EXTBAN'] = '~,abc'
self.irc.feedMsg(ircmsgs.IrcMsg(
prefix='foobar!user@host.domain.tld',
command='ACCOUNT', args=['account1']))
def join():
self.irc.feedMsg(ircmsgs.join(
self.channel, prefix='foobar!user@host.domain.tld'))
join()
self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick))
for style in (['exact'], ['account', 'exact']):
with conf.supybot.protocols.irc.banmask.context(style):
self.assertKban('kban --account --exact foobar',
'~a:account1', 'foobar!user@host.domain.tld')
join()
self.assertKban('kban --account foobar',
'~a:account1')
join()
self.assertKban('kban --account --host foobar',
'~a:account1', '*!*@host.domain.tld')
join()
with conf.supybot.protocols.irc.banmask.context(['account', 'exact']):
self.assertKban('kban foobar',
'~a:account1', 'foobar!user@host.domain.tld')
join()
with conf.supybot.protocols.irc.banmask.context(['account', 'host']):
self.assertKban('kban foobar',
'~a:account1', '*!*@host.domain.tld')
join()
def testBan(self):
with conf.supybot.protocols.irc.banmask.context(['exact']):
self.assertNotError('ban add foo!bar@baz')
self.assertNotError('ban remove foo!bar@baz')
orig = conf.supybot.protocols.irc.strictRfc()
with conf.supybot.protocols.irc.strictRfc.context(True):
# something wonky is going on here. irc.error (src/Channel.py|449)
# is being called but the assert is failing
@ -249,7 +374,6 @@ class ChannelTestCase(ChannelPluginTestCase):
'"foobar!*@baz" (never expires)')
def testIgnore(self):
orig = conf.supybot.protocols.irc.banmask()
def ignore(given, expect=None):
if expect is None:
expect = given
@ -257,8 +381,9 @@ class ChannelTestCase(ChannelPluginTestCase):
self.assertResponse('channel ignore list', "'%s'" % expect)
self.assertNotError('channel ignore remove %s' % expect)
self.assertRegexp('channel ignore list', 'not currently')
ignore('foo!bar@baz', '*!*@baz')
ignore('foo!*@*')
with conf.supybot.protocols.irc.banmask.context(['host']):
ignore('foo!bar@baz', '*!*@baz')
ignore('foo!*@*')
with conf.supybot.protocols.irc.banmask.context(['exact']):
ignore('foo!bar@baz')
ignore('foo!*@*')

View File

@ -435,7 +435,14 @@ def getBanmask(irc, msg, args, state):
getChannel(irc, msg, args, state)
banmaskstyle = conf.supybot.protocols.irc.banmask
state.args[-1] = banmaskstyle.makeBanmask(state.args[-1],
channel=state.channel)
channel=state.channel, network=irc.network)
def getExtBanmasks(irc, msg, args, state):
getHostmask(irc, msg, args, state)
getChannel(irc, msg, args, state)
banmaskstyle = conf.supybot.protocols.irc.extbanmask
state.args[-1] = banmaskstyle.makeExtBanmasks(state.args[-1],
channel=state.channel, network=irc.network)
def getUser(irc, msg, args, state):
try:
@ -806,6 +813,7 @@ wrappers = ircutils.IrcDict({
'commandName': getCommandName,
'email': getEmail,
'expiry': getExpiry,
'extbanmasks': getExtBanmasks,
'filename': getSomething, # XXX Check for validity.
'float': getFloat,
'glob': getGlob,

View File

@ -1191,7 +1191,7 @@ registerGroup(supybot.protocols, 'irc')
class Banmask(registry.SpaceSeparatedSetOfStrings):
__slots__ = ('__parent', '__dict__') # __dict__ is needed to set __doc__
validStrings = ('exact', 'nick', 'user', 'host')
validStrings = ('exact', 'nick', 'user', 'host', 'account')
def __init__(self, *args, **kwargs):
assert self.validStrings, 'There must be some valid strings. ' \
'This is a bug.'
@ -1225,13 +1225,43 @@ class Banmask(registry.SpaceSeparatedSetOfStrings):
isn't specified via options, the value of
conf.supybot.protocols.irc.banmask is used.
Unlike :meth:`makeExtBanmasks`, this is guaranteed to return an
RFC1459-like mask, suitable for ircdb's ignore lists.
options - A list specifying which parts of the hostmask should
explicitly be matched: nick, user, host. If 'exact' is given, then
only the exact hostmask will be used."""
if not channel:
channel = dynamic.channel
only the exact hostmask will be used.
"""
if not network:
network = dynamic.irc.network
if not options:
options = supybot.protocols.irc.banmask.getSpecific(
network, channel)()
options = [option for option in options if option != 'account']
masks = self.makeExtBanmasks(
hostmask, options, channel, network=network)
assert len(masks) == 1, 'Unexpected number of banmasks: %r' % masks
return masks[0]
def makeExtBanmasks(self, hostmask, options=None, channel=None, *, network):
"""Create banmasks from the given hostmask. If a style of banmask
isn't specified via options, the value of
conf.supybot.protocols.irc.banmask is used.
Depending on the options and configuration, this may return a mask
in the format of an extban (eg. "~account:foobar" on UnrealIRCd).
If this is unwanted (eg. to pass to ircdb's ignore lists), use
:meth:`makeBanmask` instead.
options - A list specifying which parts of the hostmask should
explicitly be matched: nick, user, host. If 'exact' is given, then
only the exact hostmask will be used.
If 'account' is given (and not after 'exact') and the user is
logged in and the server supports account extbans, then an account
extban is returned instead.
"""
if not channel:
channel = dynamic.channel
(nick, user, host) = ircutils.splitHostmask(hostmask)
bnick = '*'
buser = '*'
@ -1239,19 +1269,62 @@ class Banmask(registry.SpaceSeparatedSetOfStrings):
if not options:
options = supybot.protocols.irc.banmask.getSpecific(
network, channel)()
add_star_mask = False
masks = []
for option in options:
if option == 'nick':
bnick = nick
add_star_mask = True
elif option == 'user':
buser = user
add_star_mask = True
elif option == 'host':
bhost = host
add_star_mask = True
elif option == 'exact':
return hostmask
masks.append(hostmask)
elif option == 'account':
import supybot.world as world
irc = world.getIrc(network)
if irc is None:
continue
extban = ircutils.accountExtban(irc, nick)
if extban is not None:
masks.append(extban)
else:
from . import log
log.warning(
"Unknown mask option passed to makeExtBanmasks: %r",
option)
if add_star_mask and (bnick, buser, bhost) != ('*', '*', '*'):
masks.append(ircutils.joinHostmask(bnick, buser, bhost))
if (bnick, buser, bhost) == ('*', '*', '*') and \
ircutils.isUserHostmask(hostmask):
return hostmask
return ircutils.joinHostmask(bnick, buser, bhost)
options == ['account'] and \
not masks:
# found no ban mask to set (because options == ['account'] and user
# is logged out?), try again with the default ban mask
options = supybot.protocols.irc.banmask.getSpecific(
network, channel)()
options = [option for option in options if option != 'account']
return self.makeExtBanmasks(
hostmask, options=options, channel=channel, network=network)
if (bnick, buser, bhost) == ('*', '*', '*') and \
ircutils.isUserHostmask(hostmask) and \
not masks:
# still no ban mask found, fallback to the host, if any
if host != '*':
masks.append(ircutils.joinHostmask('*', '*', host))
else:
# if no host, fall back to the exact mask provided
masks.append(hostmask)
return masks
registerChannelValue(supybot.protocols.irc, 'banmask',
Banmask(['host'], _("""Determines what will be used as the

View File

@ -681,6 +681,13 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
Stores the last hostmask of a seen nick.
:type: ircutils.IrcDict[str, str]
.. attribute:: nicksToAccounts
Stores the current services account name of a seen nick (or
:const:`None` for un-identified nicks)
:type: ircutils.IrcDict[str, Optional[str]]
"""
__firewalled__ = {'addMsg': None}
@ -689,7 +696,8 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
nicksToHostmasks=None, channels=None,
capabilities_req=None,
capabilities_ack=None, capabilities_nak=None,
capabilities_ls=None):
capabilities_ls=None,
nicksToAccounts=None):
self.fsm = IrcStateFsm()
if history is None:
history = RingBuffer(conf.supybot.protocols.irc.maxHistoryLength())
@ -697,6 +705,8 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
supported = utils.InsensitivePreservingDict()
if nicksToHostmasks is None:
nicksToHostmasks = ircutils.IrcDict()
if nicksToAccounts is None:
nicksToAccounts = ircutils.IrcDict()
if channels is None:
channels = ircutils.IrcDict()
self.capabilities_req = capabilities_req or set()
@ -708,6 +718,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
self.history = history
self.channels = channels
self.nicksToHostmasks = nicksToHostmasks
self.nicksToAccounts = nicksToAccounts
# Batches usually finish and are way shorter than 3600s, but
# we need to:
@ -725,6 +736,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
self.channels.clear()
self.supported.clear()
self.nicksToHostmasks.clear()
self.nicksToAccounts.clear()
self.capabilities_req = set()
self.capabilities_ack = set()
self.capabilities_nak = set()
@ -745,13 +757,16 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
def __reduce__(self):
return (self.__class__, (self.history, self.supported,
self.nicksToHostmasks, self.channels))
self.nicksToHostmasks,
self.nicksToAccounts,
self.channels))
def __eq__(self, other):
return self.history == other.history and \
self.channels == other.channels and \
self.supported == other.supported and \
self.nicksToHostmasks == other.nicksToHostmasks and \
self.nicksToAccounts == other.nicksToAccounts and \
self.batches == other.batches
def __ne__(self, other):
@ -761,6 +776,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
ret = self.__class__()
ret.history = copy.deepcopy(self.history)
ret.nicksToHostmasks = copy.deepcopy(self.nicksToHostmasks)
ret.nicksToAccounts = copy.deepcopy(self.nicksToAccounts)
ret.channels = copy.deepcopy(self.channels)
ret.batches = copy.deepcopy(self.batches)
return ret
@ -770,6 +786,8 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
self.history.append(msg)
if ircutils.isUserHostmask(msg.prefix) and not msg.command == 'NICK':
self.nicksToHostmasks[msg.nick] = msg.prefix
if 'account' in msg.server_tags:
self.nicksToAccounts[msg.nick] = msg.server_tags['account']
if 'batch' in msg.server_tags:
batch_name = msg.server_tags['batch']
assert batch_name in self.batches, \
@ -788,6 +806,12 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
"""Returns the hostmask for a given nick."""
return self.nicksToHostmasks[nick]
def nickToAccount(self, nick):
"""Returns the account for a given nick, or None if the nick is logged
out. Raises :exc:`KeyError` if the nick was not seen or its account is
not known yet."""
return self.nicksToAccounts[nick]
def getParentBatches(self, msg):
"""Given an IrcMsg, returns a list of all batches that contain it,
innermost first.
@ -957,6 +981,11 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
(n, t, user, ip, host, nick, status, account, gecos) = msg.args
hostmask = '%s!%s@%s' % (nick, user, host)
self.nicksToHostmasks[nick] = hostmask
if account == '0':
# logged out
self.nicksToAccounts[nick] = None
else:
self.nicksToAccounts[nick] = account
def do353(self, irc, msg):
# NAMES reply.
@ -978,6 +1007,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
stripped_item = item.lstrip(prefix_chars)
item_prefix = item[0:-len(stripped_item)]
if ircutils.isUserHostmask(stripped_item):
# https://ircv3.net/specs/extensions/userhost-in-names
nick = ircutils.nickFromHostmask(stripped_item)
self.nicksToHostmasks[nick] = stripped_item
name = item_prefix + nick
@ -989,11 +1019,20 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
c.modes['s'] = None
def doChghost(self, irc, msg):
# https://ircv3.net/specs/extensions/chghost
(user, host) = msg.args
nick = msg.nick
hostmask = '%s!%s@%s' % (nick, user, host)
self.nicksToHostmasks[nick] = hostmask
def doAccount(self, irc, msg):
# https://ircv3.net/specs/extensions/account-notify
account = msg.args[0]
if account == '*':
self.nicksToAccounts[msg.nick] = None
else:
self.nicksToAccounts[msg.nick] = account
def doJoin(self, irc, msg):
for channel in msg.args[0].split(','):
if channel in self.channels:
@ -1004,6 +1043,12 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
self.channels[channel] = chan
# I don't know why this assert was here.
#assert msg.nick == irc.nick, msg
if 'extended-join' in self.capabilities_ack:
account = msg.args[1]
if account == '*':
self.nicksToAccounts[msg.nick] = None
else:
self.nicksToAccounts[msg.nick] = account
def do367(self, irc, msg):
# Example:
@ -1083,6 +1128,8 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
if msg.nick in self.nicksToHostmasks:
# If we're quitting, it may not be.
del self.nicksToHostmasks[msg.nick]
if msg.nick in self.nicksToAccounts:
del self.nicksToAccounts[msg.nick]
def doTopic(self, irc, msg):
if len(msg.args) == 1:
@ -1100,6 +1147,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
def doNick(self, irc, msg):
newNick = msg.args[0]
oldNick = msg.nick
try:
if msg.user and msg.host:
# Nick messages being handed out from the bot itself won't
@ -1109,6 +1157,13 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
del self.nicksToHostmasks[oldNick]
except KeyError:
pass
try:
self.nicksToAccounts[newNick] = self.nicksToAccounts[oldNick]
del self.nicksToAccounts[oldNick]
except KeyError:
pass
channel_names = ircutils.IrcSet()
for (name, channel) in self.channels.items():
if msg.nick in channel.users:

View File

@ -345,6 +345,25 @@ def banmask(hostmask):
else:
return '*!*@' + host
def accountExtban(irc, nick):
"""If 'nick' is logged in and the network supports account extbans,
returns a ban mask for it. If not, returns None."""
if 'ACCOUNTEXTBAN' not in irc.state.supported:
return None
if 'EXTBAN' not in irc.state.supported:
return None
try:
account = irc.state.nickToAccount(nick)
except KeyError:
account = None
if account is None:
return None
account_extban = irc.state.supported['ACCOUNTEXTBAN'].split(',')[0]
extban_prefix = irc.state.supported['EXTBAN'].split(',', 1)[0]
return '%s%s:%s'% (extban_prefix, account_extban, account)
_plusRequireArguments = 'ovhblkqeI'
_minusRequireArguments = 'ovhbkqeI'
def separateModes(args):

View File

@ -318,7 +318,8 @@ class PluginTestCase(SupyTestCase):
raise TimeoutError(query)
if lastGetHelp not in m.args[1]:
self.assertTrue(m.args[1].startswith('Error:'),
'%r did not error: %s' % (query, m.args[1]))
'%r did not error: %s' %
(query, ' '.join(m.args[1:])))
return m
def assertSnarfError(self, query, **kwargs):

View File

@ -579,6 +579,112 @@ class IrcStateTestCase(SupyTestCase):
command='CHGHOST', args=['bar2', 'baz2']))
self.assertEqual(st.nickToHostmask('foo'), 'foo!bar2@baz2')
def testNickToAccountBaseJoin(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz'))
st.addMsg(self.irc, ircmsgs.join('#foo', prefix='bar!baz@qux'))
with self.assertRaises(KeyError):
st.nickToAccount('foo')
with self.assertRaises(KeyError):
st.nickToAccount('bar')
def testNickToAccountExtendedJoin(self):
st = irclib.IrcState()
st.capabilities_ack.add('extended-join')
st.addMsg(self.irc, ircmsgs.IrcMsg(
command='JOIN', prefix='foo!bar@baz',
args=['#foo', 'account1', 'real name1']))
st.addMsg(self.irc, ircmsgs.IrcMsg(
command='JOIN', prefix='bar!baz@qux',
args=['#foo', 'account2', 'real name2']))
st.addMsg(self.irc, ircmsgs.IrcMsg(
command='JOIN', prefix='baz!qux@quux',
args=['#foo', '*', 'real name3']))
self.assertEqual(st.nickToAccount('foo'), 'account1')
self.assertEqual(st.nickToAccount('bar'), 'account2')
self.assertIsNone(st.nickToAccount('baz'))
with self.assertRaises(KeyError):
st.nickToAccount('qux')
# QUIT erases the entry
with self.subTest("QUIT"):
st2 = st.copy()
st2.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz'))
with self.assertRaises(KeyError):
st2.nickToAccount('foo')
self.assertEqual(st2.nickToAccount('bar'), 'account2')
# check st isn't affected by changes to st2
self.assertEqual(st.nickToAccount('foo'), 'account1')
self.assertEqual(st.nickToAccount('bar'), 'account2')
# NICK moves the entry
with self.subTest("NICK"):
st2 = st.copy()
st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='NICK', args=['foo2']))
with self.assertRaises(KeyError):
st2.nickToAccount('foo')
self.assertEqual(st2.nickToAccount('foo2'), 'account1')
self.assertEqual(st2.nickToAccount('bar'), 'account2')
# check st isn't affected by changes to st2
self.assertEqual(st.nickToAccount('foo'), 'account1')
self.assertEqual(st.nickToAccount('bar'), 'account2')
# NICK moves the entry (and overwrites if needed)
with self.subTest("NICK with overwrite"):
st2 = st.copy()
st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='NICK', args=['bar']))
with self.assertRaises(KeyError):
st2.nickToAccount('foo')
self.assertEqual(st2.nickToAccount('bar'), 'account1')
# check st isn't affected by changes to st2
self.assertEqual(st.nickToAccount('foo'), 'account1')
self.assertEqual(st.nickToAccount('bar'), 'account2')
def testNickToAccountWho(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.IrcMsg(command='352', # RPL_WHOREPLY
args=[self.irc.nick, '#chan', 'bar', 'baz', 'server.example',
'foo', 'H', '0 real name']))
with self.assertRaises(KeyError):
st.nickToAccount('foo')
def testNickToAccountWhox(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.IrcMsg(command='354', # RPL_WHOSPCRPL
args=[self.irc.nick, '1', 'bar', '127.0.0.1', 'baz',
'foo', 'H', 'account1', 'real name']))
self.assertEqual(st.nickToAccount('foo'), 'account1')
st.addMsg(self.irc, ircmsgs.IrcMsg(command='354', # RPL_WHOSPCRPL
args=[self.irc.nick, '1', 'bar', '127.0.0.1', 'baz',
'foo', 'H', '0', 'real name']))
self.assertIsNone(st.nickToAccount('foo'))
def testAccountNotify(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='ACCOUNT', args=['account1']))
self.assertEqual(st.nickToAccount('foo'), 'account1')
st.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='ACCOUNT', args=['account2']))
self.assertEqual(st.nickToAccount('foo'), 'account2')
st.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='ACCOUNT', args=['*']))
self.assertIsNone(st.nickToAccount('foo'))
def testEq(self):
state1 = irclib.IrcState()
state2 = irclib.IrcState()

View File

@ -367,6 +367,60 @@ class FunctionsTestCase(SupyTestCase):
'*!*@*.host.tld')
self.assertEqual(ircutils.banmask('foo!bar@2001::'), '*!*@2001::*')
def testAccountExtban(self):
irc = getTestIrc()
irc.state.addMsg(irc, ircmsgs.IrcMsg(
prefix='foo!bar@baz', command='ACCOUNT', args=['account1']))
irc.state.addMsg(irc, ircmsgs.IrcMsg(
prefix='bar!baz@qux', command='ACCOUNT', args=['*']))
with self.subTest('spec example'):
irc.state.supported['ACCOUNTEXTBAN'] = 'a,account'
irc.state.supported['EXTBAN'] = '~,abc'
self.assertEqual(ircutils.accountExtban(irc, 'foo'),
'~a:account1')
self.assertIsNone(ircutils.accountExtban(irc, 'bar'))
self.assertIsNone(ircutils.accountExtban(irc, 'baz'))
with self.subTest('InspIRCd'):
irc.state.supported['ACCOUNTEXTBAN'] = 'account,R'
irc.state.supported['EXTBAN'] = ',abcR'
self.assertEqual(ircutils.accountExtban(irc, 'foo'),
'account:account1')
self.assertIsNone(ircutils.accountExtban(irc, 'bar'))
self.assertIsNone(ircutils.accountExtban(irc, 'baz'))
with self.subTest('Solanum'):
irc.state.supported['ACCOUNTEXTBAN'] = 'a'
irc.state.supported['EXTBAN'] = '$,abc'
self.assertEqual(ircutils.accountExtban(irc, 'foo'),
'$a:account1')
self.assertIsNone(ircutils.accountExtban(irc, 'bar'))
self.assertIsNone(ircutils.accountExtban(irc, 'baz'))
with self.subTest('UnrealIRCd'):
irc.state.supported['ACCOUNTEXTBAN'] = 'account,a'
irc.state.supported['EXTBAN'] = '~,abc'
self.assertEqual(ircutils.accountExtban(irc, 'foo'),
'~account:account1')
self.assertIsNone(ircutils.accountExtban(irc, 'bar'))
self.assertIsNone(ircutils.accountExtban(irc, 'baz'))
with self.subTest('no ACCOUNTEXTBAN'):
irc.state.supported.pop('ACCOUNTEXTBAN')
irc.state.supported['EXTBAN'] = '~,abc'
self.assertIsNone(ircutils.accountExtban(irc, 'foo'))
self.assertIsNone(ircutils.accountExtban(irc, 'bar'))
self.assertIsNone(ircutils.accountExtban(irc, 'baz'))
with self.subTest('no EXTBAN'):
irc.state.supported['ACCOUNTEXTBAN'] = 'account,a'
irc.state.supported.pop('EXTBAN')
self.assertIsNone(ircutils.accountExtban(irc, 'foo'))
self.assertIsNone(ircutils.accountExtban(irc, 'bar'))
self.assertIsNone(ircutils.accountExtban(irc, 'baz'))
def testSeparateModes(self):
self.assertEqual(ircutils.separateModes(['+ooo', 'x', 'y', 'z']),
[('+o', 'x'), ('+o', 'y'), ('+o', 'z')])