diff --git a/plugins/Channel/plugin.py b/plugins/Channel/plugin.py index 26ad62501..e6d24fcae 100644 --- a/plugins/Channel/plugin.py +++ b/plugins/Channel/plugin.py @@ -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. 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. 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)]) diff --git a/plugins/Channel/test.py b/plugins/Channel/test.py index 4295ef081..f091cae88 100644 --- a/plugins/Channel/test.py +++ b/plugins/Channel/test.py @@ -219,6 +219,69 @@ 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', + 'foobar!user@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', + 'foobar!user@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)) + self.assertKban('kban --account --exact foobar', + '~a:account1') + join() + self.assertKban('kban --account foobar', + '~a:account1') + join() + self.assertKban('kban --account --host foobar', + '~a:account1') + def testBan(self): with conf.supybot.protocols.irc.banmask.context(['exact']): self.assertNotError('ban add foo!bar@baz') diff --git a/src/conf.py b/src/conf.py index 16c554451..2848d817d 100644 --- a/src/conf.py +++ b/src/conf.py @@ -1217,7 +1217,11 @@ class Banmask(registry.SpaceSeparatedSetOfStrings): 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.""" + 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 if not network: @@ -1238,6 +1242,14 @@ class Banmask(registry.SpaceSeparatedSetOfStrings): bhost = host elif option == 'exact': return hostmask + elif option == 'account': + import supybot.world as world + irc = world.getIrc(network) + if irc is None: + continue + extban = ircutils.accountExtban(nick, irc) + if extban is not None: + return extban if (bnick, buser, bhost) == ('*', '*', '*') and \ ircutils.isUserHostmask(hostmask): return hostmask diff --git a/src/ircutils.py b/src/ircutils.py index da7397810..ca7c26f56 100644 --- a/src/ircutils.py +++ b/src/ircutils.py @@ -345,6 +345,25 @@ def banmask(hostmask): else: return '*!*@' + host + +def accountExtban(nick, irc): + """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): diff --git a/test/test_ircutils.py b/test/test_ircutils.py index cb62358c8..a9c40a6f6 100644 --- a/test/test_ircutils.py +++ b/test/test_ircutils.py @@ -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('foo', irc), + '~a:account1') + self.assertIsNone(ircutils.accountExtban('bar', irc)) + self.assertIsNone(ircutils.accountExtban('baz', irc)) + + with self.subTest('InspIRCd'): + irc.state.supported['ACCOUNTEXTBAN'] = 'account,R' + irc.state.supported['EXTBAN'] = ',abcR' + self.assertEqual(ircutils.accountExtban('foo', irc), + 'account:account1') + self.assertIsNone(ircutils.accountExtban('bar', irc)) + self.assertIsNone(ircutils.accountExtban('baz', irc)) + + with self.subTest('Solanum'): + irc.state.supported['ACCOUNTEXTBAN'] = 'a' + irc.state.supported['EXTBAN'] = '$,abc' + self.assertEqual(ircutils.accountExtban('foo', irc), + '$a:account1') + self.assertIsNone(ircutils.accountExtban('bar', irc)) + self.assertIsNone(ircutils.accountExtban('baz', irc)) + + with self.subTest('UnrealIRCd'): + irc.state.supported['ACCOUNTEXTBAN'] = 'account,a' + irc.state.supported['EXTBAN'] = '~,abc' + self.assertEqual(ircutils.accountExtban('foo', irc), + '~account:account1') + self.assertIsNone(ircutils.accountExtban('bar', irc)) + self.assertIsNone(ircutils.accountExtban('baz', irc)) + + with self.subTest('no ACCOUNTEXTBAN'): + irc.state.supported.pop('ACCOUNTEXTBAN') + irc.state.supported['EXTBAN'] = '~,abc' + self.assertIsNone(ircutils.accountExtban('foo', irc)) + self.assertIsNone(ircutils.accountExtban('bar', irc)) + self.assertIsNone(ircutils.accountExtban('baz', irc)) + + with self.subTest('no EXTBAN'): + irc.state.supported['ACCOUNTEXTBAN'] = 'account,a' + irc.state.supported.pop('EXTBAN') + self.assertIsNone(ircutils.accountExtban('foo', irc)) + self.assertIsNone(ircutils.accountExtban('bar', irc)) + self.assertIsNone(ircutils.accountExtban('baz', irc)) + + def testSeparateModes(self): self.assertEqual(ircutils.separateModes(['+ooo', 'x', 'y', 'z']), [('+o', 'x'), ('+o', 'y'), ('+o', 'z')])