diff --git a/src/Admin.py b/src/Admin.py index ce0013d57..93f66d80e 100755 --- a/src/Admin.py +++ b/src/Admin.py @@ -50,6 +50,7 @@ import supybot.ircdb as ircdb import supybot.utils as utils import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils +import supybot.commands as commands import supybot.privmsgs as privmsgs import supybot.schedule as schedule import supybot.callbacks as callbacks @@ -173,15 +174,13 @@ class Admin(privmsgs.CapabilityCheckingPrivmsg): Returns the channels the bot is on. Must be given in private, in order to protect the secrecy of secret channels. """ - if ircutils.isChannel(msg.args[0]): - irc.errorRequiresPrivacy() - return L = irc.state.channels.keys() if L: utils.sortBy(ircutils.toLower, L) irc.reply(utils.commaAndify(L)) else: irc.reply('I\'m not currently in any channels.') + channels = commands.wrap(channels, wrappers=['private'], noExtra=True) def do484(self, irc, msg): irc = self.pendingNickChanges.get(irc, None) @@ -219,32 +218,25 @@ class Admin(privmsgs.CapabilityCheckingPrivmsg): except KeyError: self.log.debug('Got NICK without Admin.nick being called.') - def nick(self, irc, msg, args): + def nick(self, irc, msg, args, nick): """[] Changes the bot's nick to . If no nick is given, returns the bot's current nick. """ - nick = privmsgs.getArgs(args, required=0, optional=1) if nick: - if ircutils.isNick(nick): - if 'nicklen' in irc.state.supported: - if len(nick) > irc.state.supported['nicklen']: - irc.error('That nick is too long for this server.') - return - conf.supybot.nick.setValue(nick) - irc.queueMsg(ircmsgs.nick(nick)) - self.pendingNickChanges[irc.getRealIrc()] = irc - else: - irc.errorInvalid('nick', nick) + conf.supybot.nick.setValue(nick) + irc.queueMsg(ircmsgs.nick(nick)) + self.pendingNickChanges[irc.getRealIrc()] = irc else: irc.reply(irc.nick) + nick = commands.wrap(nick, optional=['nick']) def part(self, irc, msg, args): """ [ ...] [] - Tells the bot to part the whitespace-separated list of channels - you give it. If is specified, use it as the part message. + Tells the bot to part the list of channels you give it. If + is specified, use it as the part message. """ if not args: args = [msg.args[0]] @@ -283,7 +275,7 @@ class Admin(privmsgs.CapabilityCheckingPrivmsg): if not inAtLeastOneChannel: irc.replySuccess() - def addcapability(self, irc, msg, args): + def addcapability(self, irc, msg, args, user, capability): """ Gives the user specified by (or the user to whom @@ -302,54 +294,40 @@ class Admin(privmsgs.CapabilityCheckingPrivmsg): # can only give out capabilities they have themselves (which will # depend on supybot.capabilities and its child default) but generally # means they can't mess with channel capabilities. - (name, capability) = privmsgs.getArgs(args, required=2) - if capability == 'owner': + if ircutils.strEqual(capability, 'owner'): irc.error('The "owner" capability can\'t be added in the bot. ' 'Use the supybot-adduser program (or edit the ' 'users.conf file yourself) to add an owner capability.') return - if ircdb.checkCapability(msg.prefix, capability) or \ - '-' in capability: - try: - id = ircdb.users.getUserId(name) - except KeyError: - irc.errorNoUser() - return - user = ircdb.users.getUser(id) + if ircdb.isAntiCapability(capability) or \ + ircdb.checkCapability(msg.prefix, capability): user.addCapability(capability) ircdb.users.setUser(id, user) irc.replySuccess() else: - s = 'You can\'t add capabilities you don\'t have.' - irc.error(s) + irc.error('You can\'t add capabilities you don\'t have.') + addcapability = commands.wrap(addcapability, ['otherUser', 'lowered']) - def removecapability(self, irc, msg, args): + def removecapability(self, irc, msg, args, user, capability): """ Takes from the user specified by (or the user to whom currently maps) the specified capability """ - (name, capability) = privmsgs.getArgs(args, 2) if ircdb.checkCapability(msg.prefix, capability) or \ ircdb.isAntiCapability(capability): - try: - id = ircdb.users.getUserId(name) - user = ircdb.users.getUser(id) - except KeyError: - irc.errorNoUser() - return try: user.removeCapability(capability) - ircdb.users.setUser(id, user) + ircdb.users.setUser(user.id, user) irc.replySuccess() except KeyError: irc.error('That user doesn\'t have that capability.') - return else: s = 'You can\'t remove capabilities you don\'t have.' irc.error(s) + removecapability = commands.wrap(removecapability, ['otherUser','lowered']) - def ignore(self, irc, msg, args): + def ignore(self, irc, msg, args, hostmask, expires): """ [] Ignores or, if a nick is given, ignores whatever hostmask @@ -359,46 +337,24 @@ class Admin(privmsgs.CapabilityCheckingPrivmsg): 3600. If no is given, the ignore will never automatically expire. """ - (nickOrHostmask, expires) = privmsgs.getArgs(args, optional=1) - if ircutils.isUserHostmask(nickOrHostmask): - hostmask = nickOrHostmask - else: - try: - hostmask = irc.state.nickToHostmask(nickOrHostmask) - except KeyError: - irc.error('I can\'t find a hostmask for %s.' % nickOrHostmask) - return if expires: - try: - expires = int(float(expires)) - expires += int(time.time()) - except ValueError: - irc.errorInvalid('number of seconds', expires, Raise=True) - else: - expires = 0 + expires += time.time() ircdb.ignores.add(hostmask, expires) irc.replySuccess() + ignore = commands.wrap(ignore, ['hostmask'], [('int', 0)]) - def unignore(self, irc, msg, args): + def unignore(self, irc, msg, args, hostmask): """ Ignores or, if a nick is given, ignores whatever hostmask that nick is currently using. """ - arg = privmsgs.getArgs(args) - if ircutils.isUserHostmask(arg): - hostmask = arg - else: - try: - hostmask = irc.state.nickToHostmask(arg) - except KeyError: - irc.error('I can\'t find a hostmask for %s' % arg) - return try: ircdb.ignores.remove(hostmask) irc.replySuccess() except KeyError: irc.error('%s wasn\'t in the ignores database.' % hostmask) + unignore = commands.wrap(unignore, ['hostmask']) def ignores(self, irc, msg, args): """takes no arguments @@ -410,6 +366,7 @@ class Admin(privmsgs.CapabilityCheckingPrivmsg): irc.reply(utils.commaAndify(imap(repr, ircdb.ignores.hostmasks))) else: irc.reply('I\'m not currently globally ignoring anyone.') + ignores = commands.wrap(ignores, noExtra=True) Class = Admin diff --git a/src/commands.py b/src/commands.py new file mode 100644 index 000000000..17cee7f18 --- /dev/null +++ b/src/commands.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python + +### +# Copyright (c) 2002-2004, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +Includes wrappers for commands. +""" + +__revision__ = "$Id$" + +import supybot.fix as fix + +import time +import types +import threading + +import supybot.conf as conf +import supybot.utils as utils +import supybot.world as world +import supybot.ircdb as ircdb +import supybot.ircmsgs as ircmsgs +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks +import supybot.structures as structures + + +### +# Non-arg wrappers -- these just change the behavior of a command without +# changing the arguments given to it. +### +def thread(f): + """Makes sure a command spawns a thread when called.""" + def newf(self, irc, msg, args, *L, **kwargs): + if threading.currentThread() is world.mainThread: + t = callbacks.CommandThread(target=irc._callCommand, + args=(f.func_name, self), + kwargs=kwargs) + t.start() + else: + f(self, irc, msg, args, *L, **kwargs) + return utils.changeFunctionName(newf, f.func_name, f.__doc__) + +def private(f): + """Makes sure a command is given in private.""" + def newf(self, irc, msg, args, *L, **kwargs): + if ircutils.isChannel(msg.args[0]): + irc.errorRequiresPrivacy() + else: + f(self, irc, msg, args, *L, **kwargs) + return utils.changeFunctionName(newf, f.func_name, f.__doc__) + +def checkCapability(f, capability): + """Makes sure a user has a certain capability before a command will run. + capability can be either a string or a callable object which will be called + in order to produce a string for ircdb.checkCapability.""" + def newf(self, irc, msg, args): + cap = capability + if callable(cap): + cap = cap() + if ircdb.checkCapability(msg.prefix, cap): + f(self, irc, msg, args) + else: + self.log.info('%s attempted %s without %s.', + msg.prefix, f.func_name, cap) + irc.errorNoCapability(cap) + return utils.changeFunctionName(newf, f.func_name, f.__doc__) + +class UrlSnarfThread(threading.Thread): + def __init__(self, *args, **kwargs): + assert 'url' in kwargs + kwargs['name'] = 'Thread #%s (for snarfing %s)' % \ + (world.threadsSpawned, kwargs.pop('url')) + world.threadsSpawned += 1 + threading.Thread.__init__(self, *args, **kwargs) + self.setDaemon(True) + +class SnarfQueue(ircutils.FloodQueue): + timeout = conf.supybot.snarfThrottle + def key(self, channel): + return channel + +_snarfed = SnarfQueue() + +class SnarfIrc(object): + def __init__(self, irc, channel, url): + self.irc = irc + self.url = url + self.channel = channel + + def __getattr__(self, attr): + return getattr(self.irc, attr) + + def reply(self, *args, **kwargs): + _snarfed.enqueue(self.channel, self.url) + self.irc.reply(*args, **kwargs) + +# This lock is used to serialize the calls to snarfers, so +# earlier snarfers are guaranteed to beat out later snarfers. +_snarfLock = threading.Lock() +def urlSnarfer(f): + """Protects the snarfer from loops (with other bots) and whatnot.""" + def newf(self, irc, msg, match, *L, **kwargs): + url = match.group(0) + channel = msg.args[0] + if not ircutils.isChannel(channel): + return + if ircdb.channels.getChannel(channel).lobotomized: + self.log.info('Not snarfing in %s: lobotomized.', channel) + return + if _snarfed.has(channel, url): + self.log.info('Throttling snarf of %s in %s.', url, channel) + return + irc = SnarfIrc(irc, channel, url) + def doSnarf(): + _snarfLock.acquire() + try: + if msg.repliedTo: + self.log.debug('Not snarfing, msg is already repliedTo.') + return + f(self, irc, msg, match, *L, **kwargs) + finally: + _snarfLock.release() + if threading.currentThread() is not world.mainThread: + doSnarf() + else: + L = list(L) + t = UrlSnarfThread(target=doSnarf, url=url) + t.start() + newf = utils.changeFunctionName(newf, f.func_name, f.__doc__) + return newf + +wrappers = ircutils.IrcDict({ + 'thread': thread, + 'private': private, + 'urlSnarfer': urlSnarfer, + 'checkCapability': checkCapability, +}) + + +### +# Arg wrappers, wrappers that add arguments to the command. +### +def getInt(irc, msg, args, default=None, type='integer'): + s = args.pop(0) + try: + return int(s) + except ValueError: + if default is not None: + return default + else: + irc.errorInvalid(type, s, Raise=True) + +def getId(irc, msg, args): + getInt(irc, msg, args, type='id') + +def channelDb(irc, msg, args, **kwargs): + if not conf.supybot.databases.plugins.channelSpecific(): + return None + else: + return channel(irc, msg, args, **kwargs) + +def getHostmask(irc, msg, args): + if ircutils.isUserHostmask(args[0]): + return args.pop(0) + else: + try: + s = args.pop(0) + return irc.state.nickToHostmask(s) + except KeyError: + irc.errorInvalid('nick or hostmask', s, Raise=True) + +def getUser(irc, msg, args): + try: + return ircdb.users.getUser(msg.prefix) + except KeyError: + irc.errorNotRegistered(Raise=True) + +def getOtherUser(irc, msg, args): + s = args.pop(0) + try: + return ircdb.users.getUser(s) + except KeyError: + try: + hostmask = getHostmask(irc, msg, [s]) + return ircdb.users.getUser(hostmask) + except (KeyError, IndexError, callbacks.Error): + irc.errorNoUser(Raise=True) + +def _getRe(f): + def get(irc, msg, args): + s = args.pop(0) + def isRe(s): + try: + _ = f(s) + return True + except ValueError: + return False + while not isRe(s): + s += ' ' + args.pop(0) + return f(s) + return get + +getMatcher = _getRe(utils.perlReToPythonRe) +getReplacer = _getRe(utils.perlReToReplacer) + +def getNick(irc, msg, args): + s = args.pop(0) + if ircutils.isNick(s): + if 'nicklen' in irc.state.supported: + if len(s) > irc.state.supported['nicklen']: + irc.errorInvalid('nick', s, + 'That nick is too long for this server.', + Raise=True) + return s + else: + irc.errorInvalid('nick', s, Raise=True) + +def channel(irc, msg, args, cap=None): + if ircutils.isChannel(args[0]): + channel = args.pop(0) + elif ircutils.isChannel(msg.args[0]): + channel = msg.args[0] + else: + raise callbacks.ArgumentError + if cap is not None: + if callable(cap): + cap = cap() + cap = ircdb.makeChannelCapability(channel, cap) + if not ircdb.checkCapability(msg.prefix, cap): + irc.errorNoCapability(cap, Raise=True) + return channel + +def getLowered(irc, msg, args): + return ircutils.toLower(args.pop(0)) + +argWrappers = ircutils.IrcDict({ + 'id': getId, + 'int': getInt, + 'nick': getNick, + 'channel': channel, + 'lowered': getLowered, + 'channelDb': channelDb, + 'hostmask': getHostmask, + 'user': getUser, + 'otherUser': getOtherUser, + 'regexpMatcher': getMatcher, + 'regexpReplacer': getReplacer, +}) + + +_wrappers = wrappers # Used below so we can use a keyword argument "wrappers". +def wrap(f, required=[], optional=[], wrappers=None, noExtra=False): + def getArgWrapper(x): + if isinstance(x, tuple): + assert x + name = x[0] + args = x[1:] + else: + assert isinstance(x, basestring) or x is None + name = x + args = () + if name is not None: + return argWrappers[name], args + else: + return lambda irc, msg, args: args.pop(0), args + + def newf(self, irc, msg, args, **kwargs): + starArgs = [] + req = (required or [])[:] + opt = (optional or [])[:] + def callConverter(name): + (converter, convertArgs) = getArgWrapper(name) + v = converter(irc, msg, args, *convertArgs) + starArgs.append(v) + + try: + # First, we get out everything but the last argument. + while len(req) + len(opt) > 1: + if req: + callConverter(req.pop(0)) + else: + assert opt + callConverter(opt.pop(0)) + # Second, if there is a remaining required or optional argument + # (there's a possibility that there were no required or optional + # arguments) then we join the remaining args and work convert that. + if req or opt: + rest = ' '.join(args) + if required: + converterName = req.pop(0) + else: + converterName = opt.pop(0) + callConverter(converterName) + except IndexError: + if req: + raise callbacks.ArgumentError + while opt: + del opt[-1] + starArgs.append('') + if noExtra and args: + raise callbacks.ArgumentError + f(self, irc, msg, args, *starArgs, **kwargs) + + if wrappers is not None: + wrappers = map(_wrappers.__getitem__, wrappers) + for wrapper in wrappers: + newf = wrapper(newf) + return utils.changeFunctionName(newf, f.func_name, f.__doc__) + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: