Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for account-based channel bans #1546

Merged
merged 19 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions plugins/AutoMode/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,9 @@ def unban():
# 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:
Expand Down
68 changes: 50 additions & 18 deletions plugins/Channel/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,16 @@ def kban(self, irc, msg, args,
--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),
Expand All @@ -343,13 +346,16 @@ def iban(self, irc, msg, args,
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)])
Expand All @@ -362,18 +368,22 @@ def _ban(self, irc, msg, args,
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)
Expand All @@ -389,30 +399,47 @@ def _ban(self, irc, msg, args,
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()
Expand Down Expand Up @@ -583,7 +610,7 @@ def hostmask(self, irc, msg, args, channel, banmask):
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
Expand All @@ -597,10 +624,15 @@ def add(self, irc, msg, args, channel, banmask, expires):
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):
Expand Down
137 changes: 131 additions & 6 deletions plugins/Channel/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,13 @@ def testVoice(self):
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):
Expand All @@ -185,6 +189,30 @@ def testIban(self):
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='[email protected]'))
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 = '[email protected]'
self.irc.nick = 'something'
Expand Down Expand Up @@ -219,11 +247,108 @@ def join():

self.assertRegexp('kban adlkfajsdlfkjsd', 'adlkfajsdlfkjsd is not in')

def testAccountKbanNoAccount(self):
self.irc.prefix = '[email protected]'
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='[email protected]'))
join()
self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick))
self.assertKban('kban --account --exact foobar',
'[email protected]')
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',
'*[email protected]')
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 = '[email protected]'
self.irc.nick = 'something'
self.irc.state.supported['ACCOUNTEXTBAN'] = 'a,account'
self.irc.state.supported['EXTBAN'] = '~,abc'
self.irc.feedMsg(ircmsgs.IrcMsg(
prefix='[email protected]',
command='ACCOUNT', args=['*']))
def join():
self.irc.feedMsg(ircmsgs.join(
self.channel, prefix='[email protected]'))
join()
self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick))
self.assertKban('kban --account --exact foobar',
'[email protected]')
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',
'*[email protected]')
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 = '[email protected]'
self.irc.nick = 'something'
self.irc.state.supported['ACCOUNTEXTBAN'] = 'a,account'
self.irc.state.supported['EXTBAN'] = '~,abc'
self.irc.feedMsg(ircmsgs.IrcMsg(
prefix='[email protected]',
command='ACCOUNT', args=['account1']))
def join():
self.irc.feedMsg(ircmsgs.join(
self.channel, prefix='[email protected]'))
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', '[email protected]')
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', '[email protected]')
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
Expand All @@ -249,16 +374,16 @@ def testBanList(self):
'"foobar!*@baz" (never expires)')

def testIgnore(self):
orig = conf.supybot.protocols.irc.banmask()
def ignore(given, expect=None):
if expect is None:
expect = given
self.assertNotError('channel ignore add %s' % given)
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!*@*')
Expand Down
10 changes: 9 additions & 1 deletion src/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -806,6 +813,7 @@ def getText(irc, msg, args, state):
'commandName': getCommandName,
'email': getEmail,
'expiry': getExpiry,
'extbanmasks': getExtBanmasks,
'filename': getSomething, # XXX Check for validity.
'float': getFloat,
'glob': getGlob,
Expand Down
Loading
Loading