diff --git a/src/Admin.py b/src/Admin.py index 0d75c6552..ae22edbf7 100755 --- a/src/Admin.py +++ b/src/Admin.py @@ -181,7 +181,7 @@ class Admin(privmsgs.CapabilityCheckingPrivmsg): irc.reply(utils.commaAndify(L)) else: irc.reply('I\'m not currently in any channels.') - channels = commands.wrap(channels, wrappers=['private'], noExtra=True) + channels = commands.wrap(channels, ['private'], noExtra=True) def do484(self, irc, msg): irc = self.pendingNickChanges.get(irc, None) @@ -231,7 +231,7 @@ class Admin(privmsgs.CapabilityCheckingPrivmsg): self.pendingNickChanges[irc.getRealIrc()] = irc else: irc.reply(irc.nick) - nick = commands.wrap(nick, optional=['nick']) + nick = commands.wrap(nick, ['?nick']) def part(self, irc, msg, args): """ [ ...] [] @@ -342,7 +342,7 @@ class Admin(privmsgs.CapabilityCheckingPrivmsg): expires += time.time() ircdb.ignores.add(hostmask, expires) irc.replySuccess() - ignore = commands.wrap(ignore, ['hostmask'], [('int', 0)]) + ignore = commands.wrap(ignore, ['hostmask', ('?int', 0)]) def unignore(self, irc, msg, args, hostmask): """ diff --git a/src/Channel.py b/src/Channel.py index 5edd4c7b4..aa1541146 100755 --- a/src/Channel.py +++ b/src/Channel.py @@ -62,50 +62,41 @@ conf.registerChannelValue(conf.supybot.plugins.Channel, 'alwaysRejoin', rejoin a channel whenever it's kicked from the channel.""")) class Channel(callbacks.Privmsg): - def haveOps(self, irc, channel, what): - try: - if irc.nick in irc.state.channels[channel].ops: - return True - else: - irc.error('How can I %s? I\'m not opped in %s.' % - (what, channel)) - return False - except KeyError: - irc.error('I don\'t seem to be in %s.' % channel) - def doKick(self, irc, msg): channel = msg.args[0] if msg.args[1] == irc.nick: if self.registryValue('alwaysRejoin', channel): irc.sendMsg(ircmsgs.join(channel)) # Fix for keys. - def mode(self, irc, msg, args, channel, mode): + def mode(self, irc, msg, args, channel): """[] [ ...] Sets the mode in to , sending the arguments given. is only necessary if the message isn't sent in the channel itself. """ - if self.haveOps(irc, channel, 'change the mode'): - irc.queueMsg(ircmsgs.mode(channel, mode)) - mode = commands.wrap(mode, [('channel', 'op'), 'something']) + irc.queueMsg(ircmsgs.mode(channel, args)) + mode = commands.wrap(mode, + ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'change the mode')], + requireExtra=True) def limit(self, irc, msg, args, channel, limit): - """[] + """[] [] - Sets the channel limit to . If is 0, removes the - channel limit. is only necessary if the message isn't sent - in the channel itself. + Sets the channel limit to . If is 0, or isn't given, + removes the channel limit. is only necessary if the message + isn't sent in the channel itself. """ - if limit < 0: - irc.error('%r is not a positive integer.' % limit, Raise=True) if limit: - if self.haveOps(irc, channel, 'set the limit'): - irc.queueMsg(ircmsgs.mode(channel, ['+l', limit])) + irc.queueMsg(ircmsgs.mode(channel, ['+l', limit])) else: - if self.haveOps(irc, channel, 'unset the limit'): - irc.queueMsg(ircmsgs.mode(channel, ['-l'])) - limit = commands.wrap(mode, [('channel', 'op'), 'int']) + irc.queueMsg(ircmsgs.mode(channel, ['-l'])) + limit = commands.wrap(mode, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'change the limit'), + ('?nonNegativeInt', 0)]) def moderate(self, irc, msg, args, channel): """[] @@ -114,9 +105,10 @@ class Channel(callbacks.Privmsg): send messages to the channel. is only necessary if the message isn't sent in the channel itself. """ - if self.haveOps(irc, channel, 'moderate the channel'): - irc.queueMsg(ircmsgs.mode(channel, ['+m'])) - moderate = commands.wrap(moderate, [('channel', 'op')]) + irc.queueMsg(ircmsgs.mode(channel, ['+m'])) + moderate = commands.wrap(moderate, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'moderate the channel')]) def unmoderate(self, irc, msg, args, channel): """[] @@ -125,9 +117,11 @@ class Channel(callbacks.Privmsg): send messages to the channel. is only necessary if the message isn't sent in the channel itself. """ - if self.haveOps(irc, channel, 'unmoderate the channel'): - irc.queueMsg(ircmsgs.mode(channel, ['-m'])) - unmoderate = commands.wrap(unmoderate, [('channel', 'op')]) + irc.queueMsg(ircmsgs.mode(channel, ['-m'])) + unmoderate = commands.wrap(unmoderate, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', + 'unmoderate the channel')]) def key(self, irc, msg, args, channel, key): """[] [] @@ -137,12 +131,13 @@ class Channel(callbacks.Privmsg): if the message isn't sent in the channel itself. """ if key: - if self.haveOps(irc, channel, 'set the keyword'): - irc.queueMsg(ircmsgs.mode(channel, ['+k', key])) + irc.queueMsg(ircmsgs.mode(channel, ['+k', key])) else: - if self.haveOps(irc, channel, 'unset the keyword'): - irc.queueMsg(ircmsgs.mode(channel, ['-k'])) - key = commands.wrap(key, [('channel', 'op')], ['something']) + irc.queueMsg(ircmsgs.mode(channel, ['-k'])) + key = commands.wrap(key, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'change the keyword'), + '?somethingWithoutSpaces']) def op(self, irc, msg, args, channel): """[] [ ...] @@ -154,9 +149,10 @@ class Channel(callbacks.Privmsg): """ if not args: args = [msg.nick] - if self.haveOps(irc, channel, 'op you'): - irc.queueMsg(ircmsgs.ops(channel, args)) - op = commands.wrap(op, [('channel', 'op')]) + irc.queueMsg(ircmsgs.ops(channel, args)) + op = commands.wrap(op, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'op someone')]) def halfop(self, irc, msg, args, channel): """[] @@ -168,9 +164,10 @@ class Channel(callbacks.Privmsg): """ if not args: args = [msg.nick] - if self.haveOps(irc, channel, 'halfop you'): - irc.queueMsg(ircmsgs.halfops(channel, args)) - halfop = commands.wrap(halfop, [('channel', 'halfop')]) + irc.queueMsg(ircmsgs.halfops(channel, args)) + halfop = commands.wrap(halfop, ['channel', + ('checkChannelCapability', 'halfop'), + ('haveOp', 'halfop someone')]) def voice(self, irc, msg, args, channel): """[] @@ -182,9 +179,10 @@ class Channel(callbacks.Privmsg): """ if not args: args = [msg.nick] - if self.haveOps(irc, channel, 'voice you'): - irc.queueMsg(ircmsgs.voices(channel, args)) - voice = commands.wrap(voice, [('channel', 'voice')]) + irc.queueMsg(ircmsgs.voices(channel, args)) + voice = commands.wrap(voice, ['channel', + ('checkChannelCapability', 'voice'), + ('haveOp', 'voice someone')]) def deop(self, irc, msg, args, channel): """[] [ ...] @@ -199,9 +197,11 @@ class Channel(callbacks.Privmsg): irc.error('I cowardly refuse to deop myself. If you really want ' 'me deopped, tell me to op you and then deop me ' 'yourself.') - elif self.haveOps(irc, channel, 'deop someone'): + else: irc.queueMsg(ircmsgs.deops(channel, args)) - deop = commands.wrap(deop, [('channel', 'op')]) + deop = commands.wrap(deop, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'deop someone')]) def dehalfop(self, irc, msg, args, channel): """[] [ ...] @@ -216,9 +216,11 @@ class Channel(callbacks.Privmsg): irc.error('I cowardly refuse to dehalfop myself. If you really ' 'want me dehalfopped, tell me to op you and then ' 'dehalfop me yourself.') - elif self.haveOps(irc, channel, 'dehalfop someone'): + else: irc.queueMsg(ircmsgs.dehalfops(channel, args)) - dehalfop = commands.wrap(dehalfop, [('channel', 'halfop')]) + dehalfop = commands.wrap(dehalfop, ['channel', + ('checkChannelCapability', 'halfop'), + ('haveOp', 'dehalfop someone')]) def devoice(self, irc, msg, args, channel): """[] [ ...] @@ -233,9 +235,11 @@ class Channel(callbacks.Privmsg): irc.error('I cowardly refuse to devoice myself. If you really ' 'want me devoiced, tell me to op you and then devoice ' 'me yourself.') - elif self.haveOps(irc, channel, 'devoice someone'): + else: irc.queueMsg(ircmsgs.devoices(channel, args)) - devoice = commands.wrap(devoice, [('channel', 'voice')]) + devoice = commands.wrap(devoice, ['channel', + ('checkChannelCapability', 'voice'), + ('haveOp', 'devoice someone')]) def cycle(self, irc, msg, args, channel, key): """[] [] @@ -249,7 +253,9 @@ class Channel(callbacks.Privmsg): key = None irc.queueMsg(ircmsgs.part(channel)) irc.queueMsg(ircmsgs.join(channel, key)) - cycle = commands.wrap(cycle, [('channel', 'op')], ['something']) + cycle = commands.wrap(cycle, ['channel', + ('checkChannelCapability', 'op'), + 'anything']) def kick(self, irc, msg, args, channel, nick, reason): """[] [] @@ -259,21 +265,25 @@ class Channel(callbacks.Privmsg): is only necessary if the message isn't sent in the channel itself. """ - if self.haveOps(irc, channel, 'kick someone'): - if nick not in irc.state.channels[channel].users: - irc.error('%s isn\'t in %s.' % (nick, channel)) - return - if not reason: - reason = msg.nick - kicklen = irc.state.supported.get('kicklen', sys.maxint) - if len(reason) > kicklen: - irc.error('The reason you gave is longer than the allowed ' - 'length for a KICK reason on this server.') - return - irc.queueMsg(ircmsgs.kick(channel, nick, reason)) - kick = commands.wrap(kick, [('channel', 'op'), 'something'], ['something']) + if nick not in irc.state.channels[channel].users: + irc.error('%s isn\'t in %s.' % (nick, channel)) + return + if not reason: + reason = msg.nick + kicklen = irc.state.supported.get('kicklen', sys.maxint) + if len(reason) > kicklen: + irc.error('The reason you gave is longer than the allowed ' + 'length for a KICK reason on this server.') + return + irc.queueMsg(ircmsgs.kick(channel, nick, reason)) + kick = commands.wrap(kick, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'kick someone'), + 'nick', + '?anything']) - def kban(self, irc, msg, args, channel, bannedNick, length, reason, *optlist): + def kban(self, irc, msg, args, + optlist, channel, bannedNick, length, reason): """[] [--{exact,nick,user,host}] [] [] If you have the #channel,op capability, this will kickban for @@ -287,7 +297,7 @@ class Channel(callbacks.Privmsg): itself. """ # Check that they're not trying to make us kickban ourself. - self.log.critical('In kban') + self.log.debug('In kban') if not ircutils.isNick(bannedNick): self.log.warning('%r tried to kban a non nick: %r', msg.prefix, bannedNick) @@ -358,29 +368,29 @@ class Channel(callbacks.Privmsg): irc.queueMsg(ircmsgs.unban(channel, banmask)) schedule.addEvent(f, time.time() + length) if bannedNick == msg.nick: - if self.haveOps(irc, channel, 'kick or ban someone'): - doBan() + doBan() elif ircdb.checkCapability(msg.prefix, capability): if ircdb.checkCapability(bannedHostmask, capability): - self.log.warning('%r tried to ban %r, but both have %s', + self.log.warning('%s tried to ban %r, but both have %s', msg.prefix, bannedHostmask, capability) irc.error('%s has %s too, you can\'t ban him/her/it.' % (bannedNick, capability)) - elif self.haveOps(irc, channel, 'kick or ban someone'): + else: doBan() else: self.log.warning('%r attempted kban without %s', msg.prefix, capability) irc.errorNoCapability(capability) exact,nick,user,host - kban = \ - commands.wrap(kban, ['channel', 'something'], - [('expiry', 0), 'something'], - getopts={'exact': None, - 'nick': None, - 'user': None, - 'host': None}) - + kban = commands.wrap(kban, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'kick or ban someone'), + 'nick', ('expiry?', 0), '?anything'], + getopts={'exact': None, + 'nick': None, + 'user': None, + 'host': None}) + def unban(self, irc, msg, args, channel, hostmask): """[] @@ -389,9 +399,11 @@ class Channel(callbacks.Privmsg): the channel. is only necessary if the message isn't sent in the channel itself. """ - if self.haveOps(irc, channel, 'unban someone'): - irc.queueMsg(ircmsgs.unban(channel, hostmask)) - unban = commands.wrap(unban, [('channel', 'op'), 'hostmask']) + irc.queueMsg(ircmsgs.unban(channel, hostmask)) + unban = commands.wrap(unban, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'unban someone'), + 'hostmask']) def invite(self, irc, msg, args, channel, nick): """[] @@ -400,9 +412,11 @@ class Channel(callbacks.Privmsg): to join . is only necessary if the message isn't sent in the channel itself. """ - if self.haveOps(irc, channel, 'invite someone'): - irc.queueMsg(ircmsgs.invite(nick, channel)) - invite = commands.wrap(invite, [('channel', 'op'), 'something']) + irc.queueMsg(ircmsgs.invite(nick, channel)) + invite = commands.wrap(invite, ['channel', + ('checkChannelCapability', 'op'), + ('haveOp', 'invite someone'), + 'nick']) def lobotomize(self, irc, msg, args, channel): """[] @@ -416,7 +430,8 @@ class Channel(callbacks.Privmsg): c.lobotomized = True ircdb.channels.setChannel(channel, c) irc.replySuccess() - lobotomize = commands.wrap(lobotomize, [('channel', 'op')]) + lobotomize = commands.wrap(lobotomize, ['channel', + ('checkChannelCapability', 'op')]) def unlobotomize(self, irc, msg, args, channel): """[] @@ -430,7 +445,9 @@ class Channel(callbacks.Privmsg): c.lobotomized = False ircdb.channels.setChannel(channel, c) irc.replySuccess() - unlobotomize = commands.wrap(unlobotomize, [('channel', 'op')]) + unlobotomize = commands.wrap(unlobotomize, + ['channel', + ('checkChannelCapability', 'op')]) def permban(self, irc, msg, args, channel, banmask, expires): """[] [] @@ -448,8 +465,10 @@ class Channel(callbacks.Privmsg): c.addBan(banmask, expires) ircdb.channels.setChannel(channel, c) irc.replySuccess() - permban = \ - commands.wrap(permban, [('channel', 'op'), 'banmask', ('expiry', 0)]) + permban = commands.wrap(permban, ['channel', + ('checkChannelCapability', 'op'), + 'hostmask', + ('?expiry', 0)]) def unpermban(self, irc, msg, args, channel, banmask): """[] @@ -458,13 +477,13 @@ class Channel(callbacks.Privmsg): ban on . is only necessary if the message isn't sent in the channel itself. """ - banmask = privmsgs.getArgs(args) c = ircdb.channels.getChannel(channel) c.removeBan(banmask) ircdb.channels.setChannel(channel, c) - #irc.queueMsg(ircmsgs.unban(channel, banmask)) irc.replySuccess() - unpermban = commands.wrap(unpermban, [('channel', 'op'), 'banmask']) + unpermban = commands.wrap(unpermban, ['channel', + ('checkChannelCapability', 'op'), + 'hostmask']) def permbans(self, irc, msg, args, channel): """[] @@ -478,7 +497,8 @@ class Channel(callbacks.Privmsg): irc.reply(utils.commaAndify(map(utils.dqrepr, c.bans))) else: irc.reply('There are currently no permanent bans on %s' % channel) - permbans = commands.wrap(permbans, [('channel', 'op')]) + permbans = commands.wrap(permbans, ['channel', + ('checkChannelCapability', 'op')]) def ignore(self, irc, msg, args, channel, banmask, expires): """[] [] @@ -494,8 +514,10 @@ class Channel(callbacks.Privmsg): c.addIgnore(banmask, expires) ircdb.channels.setChannel(channel, c) irc.replySuccess() - ignore = \ - commands.wrap(ignore,[('channel', 'op'), 'banmask', ('expiry', 0)]) + ignore = commands.wrap(ignore, ['channel', + ('checkChannelCapability', 'op'), + 'hostmask', + ('?expiry', 0)]) def unignore(self, irc, msg, args, channel, banmask): """[] @@ -508,7 +530,9 @@ class Channel(callbacks.Privmsg): c.removeIgnore(banmask) ircdb.channels.setChannel(channel, c) irc.replySuccess() - unignore = commands.wrap(unignore, [('channel', 'op'), 'something']) + unignore = commands.wrap(unignore, ['channel', + ('checkChannelCapability', 'op'), + 'hostmask']) def ignores(self, irc, msg, args, channel): """[] @@ -525,7 +549,8 @@ class Channel(callbacks.Privmsg): else: L = sorted(c.ignores) irc.reply(utils.commaAndify(imap(repr, L))) - ignores = commands.wrap(ignores, [('channel', 'op')]) + ignores = commands.wrap(ignores, ['channel', + ('checkChannelCapability', 'op')]) def addcapability(self, irc, msg, args, channel, hostmask, capabilities): """[] [ ...] @@ -545,9 +570,11 @@ class Channel(callbacks.Privmsg): user.addCapability(c) ircdb.users.setUser(id, user) irc.replySuccess() - addcapability = \ - commands.wrap(addcapability, - [('channel', 'op'), 'hostmask', 'something']) + addcapability = commands.wrap(addcapability, + ['channel', + ('checkChannelCapability', 'op'), + 'hostmask', + 'somethingWithoutSpaces']) def removecapability(self, irc, msg, args, channel, hostmask, capabilities): """[] [ ...] @@ -576,10 +603,14 @@ class Channel(callbacks.Privmsg): (utils.commaAndify(fail), utils.pluralize('capability', len(fail))), Raise=True) irc.replySuccess() - removecapability = \ - commands.wrap(removecapability, - [('channel', 'op'), 'hostmask', 'something']) + removecapability = commands.wrap(removecapability, + ['channel', + ('checkChannelCapability', 'op'), + 'hostmask', + 'somethingWithoutSpaces']) + # XXX This needs to be fix0red to be like Owner.defaultcapability. Or + # something else. This is a horrible interface. def setdefaultcapability(self, irc, msg, args, channel, v): """[] {True|False} @@ -595,8 +626,10 @@ class Channel(callbacks.Privmsg): c.setDefaultCapability(False) ircdb.channels.setChannel(channel, c) irc.replySuccess() - setdefaultcapability = \ - commands.wrap(setdefaultcapability, [('channel', 'op'), 'boolean']) + setdefaultcapability = commands.wrap(setdefaultcapability, + ['channel', + ('checkChannelCapability', 'op'), + 'boolean']) def setcapability(self, irc, msg, args, channel, capabilities): """[] [ ...] @@ -610,8 +643,10 @@ class Channel(callbacks.Privmsg): chan.addCapability(c) ircdb.channels.setChannel(channel, chan) irc.replySuccess() - setcapability = \ - commands.wrap(setcapability, [('channel', 'op'), 'something']) + setcapability = commands.wrap(setcapability, + ['channel', + ('checkChannelCapability', 'op'), + 'something']) def unsetcapability(self, irc, msg, args, channel, capabilities): """[] [ ...] @@ -634,8 +669,10 @@ class Channel(callbacks.Privmsg): (utils.commaAndify(fail), utils.pluralize('capability', len(fail))), Raise=True) irc.replySuccess() - unsetcapability = \ - commands.wrap(unsetcapability, [('channel', 'op'), 'something']) + unsetcapability = commands.wrap(unsetcapability, + ['channel', + ('checkChannelCapability', 'op'), + 'somethingWithoutSpaces']) def capabilities(self, irc, msg, args, channel): """[] @@ -645,7 +682,7 @@ class Channel(callbacks.Privmsg): """ c = ircdb.channels.getChannel(channel) L = sorted(c.capabilities) - irc.reply('[%s]' % '; '.join(L)) + irc.reply(' '.join(L)) capabilities = commands.wrap(capabilities, ['channel']) def lobotomies(self, irc, msg, args): @@ -687,15 +724,16 @@ class Channel(callbacks.Privmsg): if ircdb.checkCapability(hostmask, capability): irc.reply(s, to=nick, private=True) - def alert(self, irc, msg, args, channel): + def alert(self, irc, msg, args, channel, text): """[] Sends to all the users in who have the ,op capability. """ - text = privmsgs.getArgs(args) self.alertOps(irc, channel, text, frm=msg.nick) - alert = privmsgs.checkChannelCapability(alert, 'op') + alert = commands.wrap(alert, ['channel', + ('checkChannelCapability', 'op'), + 'something']) Class = Channel diff --git a/src/commands.py b/src/commands.py index 83d4a1dec..3b6595c61 100644 --- a/src/commands.py +++ b/src/commands.py @@ -43,6 +43,7 @@ import time import types import threading +import supybot.log as log import supybot.conf as conf import supybot.utils as utils import supybot.world as world @@ -69,31 +70,6 @@ def thread(f): 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 @@ -158,105 +134,141 @@ def urlSnarfer(f): newf = utils.changeFunctionName(newf, f.func_name, f.__doc__) return newf -wrappers = ircutils.IrcDict({ +decorators = ircutils.IrcDict({ 'thread': thread, - 'private': private, 'urlSnarfer': urlSnarfer, - 'checkCapability': checkCapability, }) ### -# Arg wrappers, wrappers that add arguments to the command. +# Arg wrappers, wrappers that add arguments to the command. They accept the +# irc, msg, and args, of course, as well as a State object which holds the args +# (and kwargs, though none currently take advantage of that) to be given to the +# command being decorated, as well as the name of the command, the plugin, the +# log, etc. ### -def getInt(irc, msg, args, default=None, type='integer'): - s = args.pop(0) + +# This is just so we can centralize this, since it may change. +def _int(s): + return int(float(s)) + +def getInt(irc, msg, args, state, default=None, type='integer', p=None): try: - return int(s) + i = _int(args[0]) + if p is not None: + if not p(i): + raise ValueError + state.args.append(_int(args[0])) + del args[0] except ValueError: if default is not None: - return default + state.args.append(default) else: - irc.errorInvalid(type, s, Raise=True) + irc.errorInvalid(type, args[0]) -def getId(irc, msg, args): - getInt(irc, msg, args, type='id') +def getPositiveInt(irc, msg, args, state, *L): + getInt(irc, msg, args, state, + p=lambda i: i<=0, type='positive integer', *L) -def getExpiry(irc, msg, args, default=None): - s = args.pop(0) +def getNonNegativeInt(irc, msg, args, state, *L): + getInt(irc, msg, args, state, + p=lambda i: i<0, type='non-negative integer', *L) + +def getId(irc, msg, args, state): + getInt(irc, msg, args, state, type='id') + +def getExpiry(irc, msg, args, state, default=None): + now = int(time.time()) try: - expires = int(float(s)) - expires += int(time.time()) + expires = _int(args[0]) + if expires: + expires += now + state.args.append(expires) + del args[0] except ValueError: if default is not None: - return default + if default: + default += now + state.args.append(default) else: - irc.errorInvalid('number of seconds', s, Raise=True) + irc.errorInvalid('number of seconds', args[0]) + # XXX This should be handled elsewhere; perhaps all optional args should + # consider their first extra arg to be a default. + except IndexError: + if default is not None: + if default: + default += now + state.args.append(default) + else: + raise -def getBoolean(irc, msg, args, default=None): - s = args.pop(0).strip().lower() - if s in ('true', 'on', 'enable', 'enabled'): - return True - elif s in ('false', 'off', 'disable', 'disabled'): - return False - elif default is not None: - return default - else: - irc.error("Value must be either True or False (or On or Off).", - Raise=True) +def getBoolean(irc, msg, args, state, default=None): + try: + state.args.append(utils.toBool(args[0])) + del args[0] + except ValueError: + if default is not None: + state.args.append(default) + else: + irc.errorInvalid('boolean', args[0]) -def getChannelDb(irc, msg, args, **kwargs): +def getChannelDb(irc, msg, args, state, **kwargs): if not conf.supybot.databases.plugins.channelSpecific(): - return None + state.args.append(None) + state.channel = None else: - return channel(irc, msg, args, **kwargs) + getChannel(irc, msg, args, state, **kwargs) -def validChannel(irc, msg, args): - s = args.pop(0) - if ircutils.isChannel(s): - return s +def getHaveOp(irc, msg, args, state, action='do that'): + if state.channel not in irc.state.channels: + irc.error('I\'m not even in %s.' % state.channel, Raise=True) + if irc.nick not in irc.state.channels[state.channel].ops: + irc.error('I need to be opped to %s.' % action, Raise=True) + +def validChannel(irc, msg, args, state): + if ircutils.isChannel(args[0]): + # XXX Check maxlength in irc.state.supported. + state.args.append(args.pop(0)) else: - irc.errorInvalid('channel', s, Raise=True) + irc.errorInvalid('channel', args[0]) -def getHostmask(irc, msg, args): +def getHostmask(irc, msg, args, state): if ircutils.isUserHostmask(args[0]): - return args.pop(0) + state.args.append(args.pop(0)) else: try: - s = args.pop(0) - return irc.state.nickToHostmask(s) + hostmask = irc.state.nickToHostmask(args[0]) + state.args.append(hostmask) + del args[0] except KeyError: - irc.errorInvalid('nick or hostmask', s, Raise=True) + irc.errorInvalid('nick or hostmask', args[0]) -def getBanmask(irc, msg, args): - if ircutils.isUserHostmask(args[0]): - return args.pop(0) - else: - try: - s = args.pop(0) - return ircutils.banmask(irc.state.nickToHostmask(s)) - except KeyError: - irc.errorInvalid('nick or hostmask', s, Raise=True) +def getBanmask(irc, msg, args, state): + getHostmask(irc, msg, args, state) + # XXX Channel-specific stuff. + state.args[-1] = ircutils.banmask(state.args[-1]) -def getUser(irc, msg, args): +def getUser(irc, msg, args, state): try: - return ircdb.users.getUser(msg.prefix) + state.args.append(ircdb.users.getUser(msg.prefix)) except KeyError: irc.errorNotRegistered(Raise=True) -def getOtherUser(irc, msg, args): - s = args.pop(0) +def getOtherUser(irc, msg, args, state): try: - return ircdb.users.getUser(s) + state.args.append(ircdb.users.getUser(args[0])) + del args[0] except KeyError: try: - hostmask = getHostmask(irc, msg, [s]) - return ircdb.users.getUser(hostmask) + getHostmask(irc, msg, args, state) + hostmask = state.args.pop() + state.args.append(ircdb.users.getUser(hostmask)) except (KeyError, IndexError, callbacks.Error): irc.errorNoUser(Raise=True) def _getRe(f): - def get(irc, msg, args): + def get(irc, msg, args, state): + original = args[:] s = args.pop(0) def isRe(s): try: @@ -264,170 +276,233 @@ def _getRe(f): return True except ValueError: return False - while not isRe(s): - s += ' ' + args.pop(0) - return f(s) + try: + while not isRe(s): + s += ' ' + args.pop(0) + state.args.append(f(s)) + except IndexError: + args[:] = original + raise return get getMatcher = _getRe(utils.perlReToPythonRe) getReplacer = _getRe(utils.perlReToReplacer) -def getNick(irc, msg, args): - s = args.pop(0) - if ircutils.isNick(s): +def getNick(irc, msg, args, state): + if ircutils.isNick(args[0]): if 'nicklen' in irc.state.supported: - if len(s) > irc.state.supported['nicklen']: + if len(args[0]) > irc.state.supported['nicklen']: irc.errorInvalid('nick', s, - 'That nick is too long for this server.', - Raise=True) - return s + 'That nick is too long for this server.') + state.args.append(args.pop(0)) else: - irc.errorInvalid('nick', s, Raise=True) + irc.errorInvalid('nick', s) -def getChannel(irc, msg, args, cap=None): - if ircutils.isChannel(args[0]): +def getChannel(irc, msg, args, state): + if args and ircutils.isChannel(args[0]): channel = args.pop(0) elif ircutils.isChannel(msg.args[0]): channel = msg.args[0] else: + state.log.debug('Raising ArgumentError because there is no channel.') 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 + state.channel = channel + state.args.append(channel) -def getLowered(irc, msg, args): - return ircutils.toLower(args.pop(0)) +def checkChannelCapability(irc, msg, args, state, cap): + assert state.channel, \ + 'You must use a channel arg before you use checkChannelCapability.' + cap = ircdb.canonicalCapability(cap) + cap = ircdb.makeChannelCapability(state.channel, cap) + if not ircdb.checkCapability(msg.prefix, cap): + irc.errorNoCapability(cap, Raise=True) + +def getLowered(irc, msg, args, state): + state.args.append(ircutils.toLower(args.pop(0))) -def getSomething(irc, msg, args): - s = args.pop(0) - if not s: - # XXX Better reply? How? - irc.error('You must not give the empty string as an argument.', - Raise=True) - return s +def getSomething(irc, msg, args, state, errorMsg=None, p=None): + if p is None: + p = lambda _: True + if not args[0] or not p(args[0]): + if errorMsg is None: + errorMsg = 'You must not give the empty string as an argument.' + irc.error(errorMsg, Raise=True) + else: + state.args.append(args.pop(0)) -def getPlugin(irc, msg, args, requirePresent=False): - s = args.pop(0) - cb = irc.getCallback(s) +def getSomethingNoSpaces(irc, msg, args, state, *L): + def p(s): + return len(s.split(None, 1)) == 1 + getSomething(irc, msg, args, state, p=p, *L) + +def getPlugin(irc, msg, args, state, requirePresent=False): + cb = irc.getCallback(args[0]) if requirePresent and cb is None: - irc.errorInvalid('plugin', s, Raise=True) - return cb + irc.errorInvalid('plugin', s) + state.args.append(cb) + del args[0] -argWrappers = ircutils.IrcDict({ +def private(irc, msg, args, state): + if ircutils.isChannel(msg.args[0]): + irc.errorRequiresPrivacy(Raise=True) + +def checkCapability(irc, msg, args, state, cap): + cap = ircdb.canonicalCapability(cap) + if not ircdb.checkCapability(msg.prefix, cap): + state.log.warning('%s tried %s without %s.', + msg.prefix, state.name, cap) + irc.errorNoCapability(cap, Raise=True) + +def anything(irc, msg, args, state): + state.args.append(args.pop(0)) + +wrappers = ircutils.IrcDict({ 'id': getId, 'int': getInt, + 'positiveInt': getPositiveInt, + 'nonNegativeInt': getNonNegativeInt, + 'haveOp': getHaveOp, 'expiry': getExpiry, 'nick': getNick, 'channel': getChannel, 'plugin': getPlugin, 'boolean': getBoolean, 'lowered': getLowered, + 'anything': anything, 'something': getSomething, + 'somethingWithoutSpaces': getSomethingNoSpaces, 'channelDb': getChannelDb, 'hostmask': getHostmask, 'banmask': getBanmask, 'user': getUser, + 'private': private, 'otherUser': getOtherUser, 'regexpMatcher': getMatcher, 'validChannel': validChannel, 'regexpReplacer': getReplacer, + 'checkCapability': checkCapability, + 'checkChannelCapability': checkChannelCapability, }) -def args(irc,msg,args, required=[], optional=[], getopts=None, noExtra=False): - starArgs = [] - req = required[:] - opt = optional[:] +class State(object): + def __init__(self, name=None, logger=None): + if logger is None: + logger = log + self.args = [] + self.kwargs = {} + self.name = name + self.log = logger + self.getopts = [] + self.channel = None + +def args(irc,msg,args, types=[], state=None, + getopts=None, noExtra=False, requireExtra=False, combineRest=True): + orig = args[:] + if state is None: + state = State(name='unknown', logger=log) + if requireExtra: + combineRest = False # Implied by requireExtra. + types = types[:] # We're going to destroy this. if getopts is not None: getoptL = [] for (key, value) in getopts.iteritems(): if value != '': # value can be None, remember. key += '=' getoptL.append(key) - def getArgWrapper(x): - if isinstance(x, tuple): - assert x - name = x[0] - args = x[1:] + def callWrapper(spec): + if isinstance(spec, tuple): + assert spec, 'tuple specification cannot be empty.' + name = spec[0] + specArgs = spec[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 getConversion(name): - (converter, convertArgs) = getArgWrapper(name) - v = converter(irc, msg, args, *convertArgs) - return v - def callConverter(name): - v = getConversion(name) - starArgs.append(v) + assert isinstance(spec, basestring) or spec is None + name = spec + specArgs = () + if name is None: + name = 'anything' + enforce = True + optional = False + if name.startswith('?'): + optional = True + name = name[1:] + elif name.endswith('?'): + optional = True + enforce = False + name = name[:-1] + wrapper = wrappers[name] + try: + wrapper(irc, msg, args, state, *specArgs) + except (callbacks.Error, ValueError, callbacks.ArgumentError), e: + state.log.debug('%r when calling wrapper.', utils.exnToString(e)) + if not enforce: + state.args.append('') + else: + state.log.debug('Re-raising %s because of enforce.', e) + raise + except IndexError, e: + state.log.debug('%r when calling wrapper.', utils.exnToString(e)) + if optional: + state.args.append('') + else: + state.log.debug('Raising ArgumentError because of ' + 'non-optional args.') + raise callbacks.ArgumentError # First, we getopt stuff. if getopts is not None: - L = [] (optlist, args) = getopt.getopt(args, '', getoptL) for (opt, arg) in optlist: opt = opt[2:] # Strip -- assert opt in getopts if arg is not None: assert getopts[opt] != '' - L.append((opt, getConversion(getopts[opt]))) + state.getopts.append((opt, callWrapper(getopts[opt]))) else: assert getopts[opt] == '' - L.append((opt, True)) - starArgs.append(L) + state.getopts.append((opt, True)) - # Second, we get out everything but the last argument. - try: - while len(req) + len(opt) > 1: - if req: - callConverter(req.pop(0)) - else: - assert opt - callConverter(opt.pop(0)) - # Third, 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: + # Second, we get out everything but the last argument (or, if combineRest + # is False, we'll clear out all the types). + while len(types) > 1 or (types and not combineRest): + callWrapper(types.pop(0)) + # Third, 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 types: + assert len(types) == 1 + if args: rest = ' '.join(args) args = [rest] - 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('') + callWrapper(types.pop(0)) if noExtra and args: + log.debug('noExtra and args: %r (originally %r)', args, orig) raise callbacks.ArgumentError - return starArgs + if requireExtra and not args: + log.debug('requireExtra and not args: %r (originally %r)', args, orig) + log.debug('args: %r' % args) + log.debug('State.args: %r' % state.args) + log.debug('State.getopts: %r' % state.getopts) + return state # These are used below, but we need to rename them so their names aren't # shadowed by our locals. _args = args -_wrappers = wrappers -def wrap(f, required=[], optional=[], - wrappers=None, getopts=None, noExtra=False): +_decorators = decorators +def wrap(f, *argsArgs, **argsKwargs): def newf(self, irc, msg, args, **kwargs): - starArgs = _args(irc, msg, args, - getopts=getopts, noExtra=noExtra, - required=required, optional=optional) - f(self, irc, msg, args, *starArgs, **kwargs) + state = State('%s.%s' % (self.name(), f.func_name), self.log) + state.cb = self # This should probably be in State.__init__. + _args(irc,msg,args, state=state, *argsArgs, **argsKwargs) + if state.getopts: + f(self, irc, msg, args, state.getopts, *state.args, **state.kwargs) + else: + f(self, irc, msg, args, *state.args, **state.kwargs) - if wrappers is not None: - wrappers = map(_wrappers.__getitem__, wrappers) - for wrapper in wrappers: - newf = wrapper(newf) + decorators = argsKwargs.pop('decorators', None) + if decorators is not None: + decorators = map(_decorators.__getitem__, decorators) + for decorator in decorators: + newf = decorator(newf) return utils.changeFunctionName(newf, f.func_name, f.__doc__)