mirror of
https://github.com/progval/Limnoria.git
synced 2025-04-25 12:31:04 -05:00
Merge pull request #1546 from progval/account-ban
Add support for account-based channel bans
This commit is contained in:
commit
10a341c70c
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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!*@*')
|
||||
|
@ -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,
|
||||
|
89
src/conf.py
89
src/conf.py
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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')])
|
||||
|
Loading…
x
Reference in New Issue
Block a user