diff --git a/classes.py b/classes.py index 767ccd4..8df369a 100644 --- a/classes.py +++ b/classes.py @@ -18,6 +18,7 @@ import ipaddress import queue import functools import collections +import string try: import ircmatch @@ -1074,6 +1075,31 @@ class PyLinkNetworkCoreWithUtils(PyLinkNetworkCore): if self.match_host(banmask, uid) and uid in self.users: yield uid + def make_channel_ban(self, uid, ban_type='ban'): + """Creates a hostmask-based ban for the given user. + + Ban exceptions, invite exceptions quiets, and extbans are also supported by setting ban_type + to the appropriate PyLink named mode (e.g. "ban", "banexception", "invex", "quiet", "ban_nonick").""" + assert uid in self.users, "Unknown user %s" % uid + + # FIXME: verify that this is a valid mask. + # XXX: support slicing hosts so things like *!ident@*.isp.net are possible. This is actually + # more annoying to do than it appears because of vHosts using /, IPv6 addresses + # (cloaked and uncloaked), etc. + ban_style = self.serverdata.get('ban_style') or conf.conf['pylink'].get('ban_style') or \ + '*!*@$host' + + template = string.Template(ban_style) + banhost = template.safe_substitute(ban_style, **self.users[uid].__dict__) + assert utils.isHostmask(banhost), "Ban mask %r is not a valid hostmask!" % banhost + + if ban_type in self.cmodes: + return ('+%s' % self.cmodes[ban_type], banhost) + elif ban_type in self.extbans_acting: # Handle extbans, which are generally "+b prefix:banmask" + return ('+%s' % self.cmodes['ban'], self.extbans_acting[ban_type]+banhost) + else: + raise ValueError("ban_type %r is not available on IRCd %r" % (ban_type, self.protoname)) + def updateTS(self, sender, channel, their_ts, modes=None): """ Merges modes of a channel given the remote TS and a list of modes. diff --git a/plugins/opercmds.py b/plugins/opercmds.py index a64893c..e19ce6d 100644 --- a/plugins/opercmds.py +++ b/plugins/opercmds.py @@ -67,6 +67,62 @@ def checkban(irc, source, args): else: irc.reply('No, \x02%s\x02 does not match \x02%s\x02.' % (args.target, args.banmask)) +massban_parser = utils.IRCParser() +massban_parser.add_argument('channel') +massban_parser.add_argument('banmask') +# Regarding default ban reason: it's a good idea not to leave in the caller to prevent retaliation... +massban_parser.add_argument('reason', nargs='*', default="Banned") +massban_parser.add_argument('--quiet', '-q', action='store_true') + +def massban(irc, source, args): + """ [] [--quiet/-q] + + Applies (i.e. kicks affected users) the given PyLink banmask on the specified channel. + + The --quiet option can also be given to mass-mute the given user on networks where this is supported + (currently ts6, unreal, and inspircd). No kicks will be sent in this case.""" + permissions.check_permissions(irc, source, ['opercmds.massban']) + + args = massban_parser.parse_args(args) + + if args.channel not in irc.channels: + irc.error("Unknown channel %r" % args.channel) + return + + results = 0 + + for uid in irc.match_all(args.banmask, channel=args.channel): + # Remove the target's access before banning them. + bans = [('-%s' % irc.cmodes[prefix], uid) for prefix in irc.channels[args.channel].get_prefix_modes(uid) if prefix in irc.cmodes] + + # Then, add the actual ban. + bans += [irc.make_channel_ban(uid, ban_type='quiet' if args.quiet else 'ban')] + irc.mode(irc.pseudoclient.uid, args.channel, bans) + + try: + irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSBAN', + {'target': args.channel, 'modes': bans, 'parse_as': 'MODE'}]) + except: + log.exception('(%s) Failed to send process massban hook; some bans may have not ' + 'been sent to plugins / relay networks!', irc.name) + + if not args.quiet: + irc.kick(irc.pseudoclient.uid, args.channel, uid, args.reason) + + # XXX: this better not be blocking... + try: + irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MASSKICK', + {'channel': args.channel, 'target': uid, 'text': args.reason, 'parse_as': 'KICK'}]) + + except: + log.exception('(%s) Failed to send process massban hook; some kicks may have not ' + 'been sent to plugins / relay networks!', irc.name) + + results += 1 + else: + irc.reply('Banned %s users on %r.' % (results, args.channel)) +utils.add_cmd(massban, aliases=('mban',)) + @utils.add_cmd def jupe(irc, source, args): """ []