1###
2# Copyright (c) 2002-2005, Jeremiah Fincher
3# Copyright (c) 2009-2010,2015, James McCoy
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8#
9#   * Redistributions of source code must retain the above copyright notice,
10#     this list of conditions, and the following disclaimer.
11#   * Redistributions in binary form must reproduce the above copyright notice,
12#     this list of conditions, and the following disclaimer in the
13#     documentation and/or other materials provided with the distribution.
14#   * Neither the name of the author of this software nor the name of
15#     contributors to this software may be used to endorse or promote products
16#     derived from this software without specific prior written consent.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28# POSSIBILITY OF SUCH DAMAGE.
29###
30
31"""
32Includes wrappers for commands.
33"""
34
35import time
36import getopt
37import inspect
38import threading
39import multiprocessing #python2.6 or later!
40
41try:
42    import resource
43except ImportError: # Windows!
44    resource = None
45
46from . import callbacks, conf, ircdb, ircmsgs, ircutils, log, \
47        utils, world
48from .utils import minisix
49from .i18n import PluginInternationalization, internationalizeDocstring
50_ = PluginInternationalization()
51
52###
53# Non-arg wrappers -- these just change the behavior of a command without
54# changing the arguments given to it.
55###
56
57# Thread has to be a non-arg wrapper because by the time we're parsing and
58# validating arguments, we're inside the function we'd want to thread.
59def thread(f):
60    """Makes sure a command spawns a thread when called."""
61    def newf(self, irc, msg, args, *L, **kwargs):
62        if world.isMainThread():
63            targetArgs = (self.callingCommand, irc, msg, args) + tuple(L)
64            t = callbacks.CommandThread(target=self._callCommand,
65                                        args=targetArgs, kwargs=kwargs)
66            t.start()
67        else:
68            f(self, irc, msg, args, *L, **kwargs)
69    return utils.python.changeFunctionName(newf, f.__name__, f.__doc__)
70
71class ProcessTimeoutError(Exception):
72    """Gets raised when a process is killed due to timeout."""
73    pass
74
75def _rlimit_min(a, b):
76    if a == resource.RLIM_INFINITY:
77        return b
78    elif b == resource.RLIM_INFINITY:
79        return a
80    else:
81        return min(soft, heap_size)
82
83def process(f, *args, **kwargs):
84    """Runs a function <f> in a subprocess.
85
86    Several extra keyword arguments can be supplied.
87    <pn>, the pluginname, and <cn>, the command name, are strings used to
88    create the process name, for identification purposes.
89    <timeout>, if supplied, limits the length of execution of target
90    function to <timeout> seconds.
91    <heap_size>, if supplied, limits the memory used by the target
92    function."""
93    timeout = kwargs.pop('timeout', None)
94    heap_size = kwargs.pop('heap_size', None)
95    if resource and heap_size is None:
96        heap_size = resource.RLIM_INFINITY
97
98    if world.disableMultiprocessing:
99        pn = kwargs.pop('pn', 'Unknown')
100        cn = kwargs.pop('cn', 'unknown')
101        try:
102            return f(*args, **kwargs)
103        except Exception as e:
104            raise e
105
106    try:
107        q = multiprocessing.Queue()
108    except OSError:
109        log.error('Using multiprocessing.Queue raised an OSError.\n'
110                'This is probably caused by your system denying semaphore\n'
111                'usage. You should run these two commands:\n'
112                '\tsudo rmdir /dev/shm\n'
113                '\tsudo ln -Tsf /{run,dev}/shm\n'
114                '(See https://github.com/travis-ci/travis-core/issues/187\n'
115                'for more information about this bug.)\n')
116        raise
117    def newf(f, q, *args, **kwargs):
118        if resource:
119            rsrc = resource.RLIMIT_DATA
120            (soft, hard) = resource.getrlimit(rsrc)
121            soft = _rlimit_min(soft, heap_size)
122            hard = _rlimit_min(hard, heap_size)
123            resource.setrlimit(rsrc, (soft, hard))
124        try:
125            r = f(*args, **kwargs)
126            q.put(r)
127        except Exception as e:
128            q.put(e)
129    targetArgs = (f, q,) + args
130    p = callbacks.CommandProcess(target=newf,
131                                args=targetArgs, kwargs=kwargs)
132    p.start()
133    p.join(timeout)
134    if p.is_alive():
135        p.terminate()
136        q.close()
137        raise ProcessTimeoutError("%s aborted due to timeout." % (p.name,))
138    try:
139        v = q.get(block=False)
140    except minisix.queue.Empty:
141        return None
142    finally:
143        q.close()
144    if isinstance(v, Exception):
145        raise v
146    else:
147        return v
148
149def regexp_wrapper(s, reobj, timeout, plugin_name, fcn_name):
150    '''A convenient wrapper to stuff regexp search queries through a subprocess.
151
152    This is used because specially-crafted regexps can use exponential time
153    and hang the bot.'''
154    def re_bool(s, reobj):
155        """Since we can't enqueue match objects into the multiprocessing queue,
156        we'll just wrap the function to return bools."""
157        if reobj.search(s) is not None:
158            return True
159        else:
160            return False
161    try:
162        v = process(re_bool, s, reobj, timeout=timeout, pn=plugin_name, cn=fcn_name)
163        return v
164    except ProcessTimeoutError:
165        return False
166
167class UrlSnarfThread(world.SupyThread):
168    def __init__(self, *args, **kwargs):
169        assert 'url' in kwargs
170        kwargs['name'] = 'Thread #%s (for snarfing %s)' % \
171                         (world.threadsSpawned, kwargs.pop('url'))
172        super(UrlSnarfThread, self).__init__(*args, **kwargs)
173        self.setDaemon(True)
174
175    def run(self):
176        try:
177            super(UrlSnarfThread, self).run()
178        except utils.web.Error as e:
179            log.debug('Exception in urlSnarfer: %s', utils.exnToString(e))
180
181class SnarfQueue(ircutils.FloodQueue):
182    timeout = conf.supybot.snarfThrottle
183    def key(self, channel):
184        return channel
185
186_snarfed = SnarfQueue()
187
188class SnarfIrc(object):
189    def __init__(self, irc, channel, url):
190        self.irc = irc
191        self.url = url
192        self.channel = channel
193
194    def __getattr__(self, attr):
195        return getattr(self.irc, attr)
196
197    def reply(self, *args, **kwargs):
198        _snarfed.enqueue(self.channel, self.url)
199        return self.irc.reply(*args, **kwargs)
200
201# This lock is used to serialize the calls to snarfers, so
202# earlier snarfers are guaranteed to beat out later snarfers.
203_snarfLock = threading.Lock()
204def urlSnarfer(f):
205    """Protects the snarfer from loops (with other bots) and whatnot."""
206    def newf(self, irc, msg, match, *L, **kwargs):
207        url = match.group(0)
208        channel = msg.args[0]
209        if not irc.isChannel(channel) or (ircmsgs.isCtcp(msg) and not
210                                          ircmsgs.isAction(msg)):
211            return
212        if ircdb.channels.getChannel(channel).lobotomized:
213            self.log.debug('Not snarfing in %s: lobotomized.', channel)
214            return
215        if _snarfed.has(channel, url):
216            self.log.info('Throttling snarf of %s in %s.', url, channel)
217            return
218        irc = SnarfIrc(irc, channel, url)
219        def doSnarf():
220            _snarfLock.acquire()
221            try:
222                # This has to be *after* we've acquired the lock so we can be
223                # sure that all previous urlSnarfers have already run to
224                # completion.
225                if msg.repliedTo:
226                    self.log.debug('Not snarfing, msg is already repliedTo.')
227                    return
228                f(self, irc, msg, match, *L, **kwargs)
229            finally:
230                _snarfLock.release()
231        if threading.currentThread() is not world.mainThread:
232            doSnarf()
233        else:
234            L = list(L)
235            t = UrlSnarfThread(target=doSnarf, url=url)
236            t.start()
237    newf = utils.python.changeFunctionName(newf, f.__name__, f.__doc__)
238    return newf
239
240
241###
242# Converters, which take irc, msg, args, and a state object, and build up the
243# validated and converted args for the method in state.args.
244###
245
246# This is just so we can centralize this, since it may change.
247def _int(s):
248    base = 10
249    if s.startswith('0x'):
250        base = 16
251        s = s[2:]
252    elif s.startswith('0b'):
253        base = 2
254        s = s[2:]
255    elif s.startswith('0') and len(s) > 1:
256        base = 8
257        s = s[1:]
258    try:
259        return int(s, base)
260    except ValueError:
261        if base == 10 and '.' not in s:
262            try:
263                return int(float(s))
264            except OverflowError:
265                raise ValueError('I don\'t understand numbers that large.')
266        else:
267            raise
268
269def getInt(irc, msg, args, state, type=_('integer'), p=None):
270    try:
271        i = _int(args[0])
272        if p is not None:
273            if not p(i):
274                state.errorInvalid(type, args[0])
275        state.args.append(i)
276        del args[0]
277    except ValueError:
278        state.errorInvalid(type, args[0])
279
280def getNonInt(irc, msg, args, state, type=_('non-integer value')):
281    try:
282        _int(args[0])
283        state.errorInvalid(type, args[0])
284    except ValueError:
285        state.args.append(args.pop(0))
286
287def getLong(irc, msg, args, state, type='long'):
288    getInt(irc, msg, args, state, type)
289    state.args[-1] = minisix.long(state.args[-1])
290
291def getFloat(irc, msg, args, state, type=_('floating point number')):
292    try:
293        state.args.append(float(args[0]))
294        del args[0]
295    except ValueError:
296        state.errorInvalid(type, args[0])
297
298def getPositiveInt(irc, msg, args, state, *L):
299    getInt(irc, msg, args, state,
300           p=lambda i: i>0, type=_('positive integer'), *L)
301
302def getNonNegativeInt(irc, msg, args, state, *L):
303    getInt(irc, msg, args, state,
304            p=lambda i: i>=0, type=_('non-negative integer'), *L)
305
306def getIndex(irc, msg, args, state):
307    getInt(irc, msg, args, state, type=_('index'))
308    if state.args[-1] > 0:
309        state.args[-1] -= 1
310
311def getId(irc, msg, args, state, kind=None):
312    type = 'id'
313    if kind is not None and not kind.endswith('id'):
314        type = kind + ' id'
315    original = args[0]
316    try:
317        args[0] = args[0].lstrip('#')
318        getInt(irc, msg, args, state, type=type)
319    except Exception:
320        args[0] = original
321        raise
322
323def getExpiry(irc, msg, args, state):
324    now = int(time.time())
325    try:
326        expires = _int(args[0])
327        if expires:
328            expires += now
329        state.args.append(expires)
330        del args[0]
331    except ValueError:
332        state.errorInvalid(_('number of seconds'), args[0])
333
334def getBoolean(irc, msg, args, state):
335    try:
336        state.args.append(utils.str.toBool(args[0]))
337        del args[0]
338    except ValueError:
339        state.errorInvalid(_('boolean'), args[0])
340
341def getNetworkIrc(irc, msg, args, state, errorIfNoMatch=False):
342    if args:
343        for otherIrc in world.ircs:
344            if otherIrc.network.lower() == args[0].lower():
345                state.args.append(otherIrc)
346                del args[0]
347                return
348    if errorIfNoMatch:
349        raise callbacks.ArgumentError
350    else:
351        state.args.append(irc)
352
353def getHaveVoice(irc, msg, args, state, action=_('do that')):
354    getChannel(irc, msg, args, state)
355    if state.channel not in irc.state.channels:
356        state.error(_('I\'m not even in %s.') % state.channel, Raise=True)
357    if not irc.state.channels[state.channel].isVoice(irc.nick):
358        state.error(_('I need to be voiced to %s.') % action, Raise=True)
359
360def getHaveVoicePlus(irc, msg, args, state, action=_('do that')):
361    getChannel(irc, msg, args, state)
362    if state.channel not in irc.state.channels:
363        state.error(_('I\'m not even in %s.') % state.channel, Raise=True)
364    if not irc.state.channels[state.channel].isVoicePlus(irc.nick):
365        # isOp includes owners and protected users
366        state.error(_('I need to be at least voiced to %s.') % action,
367                Raise=True)
368
369def getHaveHalfop(irc, msg, args, state, action=_('do that')):
370    getChannel(irc, msg, args, state)
371    if state.channel not in irc.state.channels:
372        state.error(_('I\'m not even in %s.') % state.channel, Raise=True)
373    if not irc.state.channels[state.channel].isHalfop(irc.nick):
374        state.error(_('I need to be halfopped to %s.') % action, Raise=True)
375
376def getHaveHalfopPlus(irc, msg, args, state, action=_('do that')):
377    getChannel(irc, msg, args, state)
378    if state.channel not in irc.state.channels:
379        state.error(_('I\'m not even in %s.') % state.channel, Raise=True)
380    if not irc.state.channels[state.channel].isHalfopPlus(irc.nick):
381        # isOp includes owners and protected users
382        state.error(_('I need to be at least halfopped to %s.') % action,
383                Raise=True)
384
385def getHaveOp(irc, msg, args, state, action=_('do that')):
386    getChannel(irc, msg, args, state)
387    if state.channel not in irc.state.channels:
388        state.error(_('I\'m not even in %s.') % state.channel, Raise=True)
389    if not irc.state.channels[state.channel].isOp(irc.nick):
390        state.error(_('I need to be opped to %s.') % action, Raise=True)
391
392def validChannel(irc, msg, args, state):
393    if irc.isChannel(args[0]):
394        state.args.append(args.pop(0))
395    else:
396        state.errorInvalid(_('channel'), args[0])
397
398def getHostmask(irc, msg, args, state):
399    if ircutils.isUserHostmask(args[0]) or \
400            (not conf.supybot.protocols.irc.strictRfc() and
401                    args[0].startswith('$')):
402        state.args.append(args.pop(0))
403    else:
404        try:
405            hostmask = irc.state.nickToHostmask(args[0])
406            state.args.append(hostmask)
407            del args[0]
408        except KeyError:
409            state.errorInvalid(_('nick or hostmask'), args[0])
410
411def getBanmask(irc, msg, args, state):
412    getHostmask(irc, msg, args, state)
413    getChannel(irc, msg, args, state)
414    banmaskstyle = conf.supybot.protocols.irc.banmask
415    state.args[-1] = banmaskstyle.makeBanmask(state.args[-1],
416            channel=state.channel)
417
418def getUser(irc, msg, args, state):
419    try:
420        state.args.append(ircdb.users.getUser(msg.prefix))
421    except KeyError:
422        state.errorNotRegistered(Raise=True)
423
424def getOtherUser(irc, msg, args, state):
425    # Although ircdb.users.getUser could accept a hostmask, we're explicitly
426    # excluding that from our interface with this check
427    if ircutils.isUserHostmask(args[0]):
428        state.errorNoUser(args[0])
429    try:
430        state.args.append(ircdb.users.getUser(args[0]))
431        del args[0]
432    except KeyError:
433        try:
434            getHostmask(irc, msg, [args[0]], state)
435            hostmask = state.args.pop()
436            state.args.append(ircdb.users.getUser(hostmask))
437            del args[0]
438        except (KeyError, callbacks.Error):
439            state.errorNoUser(name=args[0])
440
441def _getRe(f):
442    def get(irc, msg, args, state, convert=True):
443        original = args[:]
444        s = args.pop(0)
445        def isRe(s):
446            try:
447                f(s)
448                return True
449            except ValueError:
450                return False
451        try:
452            while len(s) < 512 and not isRe(s):
453                s += ' ' + args.pop(0)
454            if len(s) < 512:
455                if convert:
456                    state.args.append(f(s))
457                else:
458                    state.args.append(s)
459            else:
460                raise ValueError
461        except (ValueError, IndexError):
462            args[:] = original
463            state.errorInvalid(_('regular expression'), s)
464    return get
465
466getMatcher = _getRe(utils.str.perlReToPythonRe)
467getMatcherMany = _getRe(utils.str.perlReToFindall)
468getReplacer = _getRe(utils.str.perlReToReplacer)
469
470def getNick(irc, msg, args, state):
471    if ircutils.isNick(args[0], conf.supybot.protocols.irc.strictRfc()):
472        if 'nicklen' in irc.state.supported:
473            if len(args[0]) > irc.state.supported['nicklen']:
474                state.errorInvalid(_('nick'), args[0],
475                                 _('That nick is too long for this server.'))
476        state.args.append(args.pop(0))
477    else:
478        state.errorInvalid(_('nick'), args[0])
479
480def getSeenNick(irc, msg, args, state, errmsg=None):
481    try:
482        irc.state.nickToHostmask(args[0])
483        state.args.append(args.pop(0))
484    except KeyError:
485        if errmsg is None:
486            errmsg = _('I haven\'t seen %s.') % args[0]
487        state.error(errmsg, Raise=True)
488
489def getChannel(irc, msg, args, state):
490    if state.channel:
491        return
492    if args and irc.isChannel(args[0]):
493        channel = args.pop(0)
494    elif irc.isChannel(msg.args[0]):
495        channel = msg.args[0]
496    else:
497        state.log.debug('Raising ArgumentError because there is no channel.')
498        raise callbacks.ArgumentError
499    state.channel = channel
500    state.args.append(channel)
501
502def getChannels(irc, msg, args, state):
503    if args and all(map(irc.isChannel, args[0].split(','))):
504        channels = args.pop(0).split(',')
505    elif irc.isChannel(msg.args[0]):
506        channels = [msg.args[0]]
507    else:
508        state.log.debug('Raising ArgumentError because there is no channel.')
509        raise callbacks.ArgumentError
510    state.args.append(channels)
511
512def getChannelDb(irc, msg, args, state, **kwargs):
513    channelSpecific = conf.supybot.databases.plugins.channelSpecific
514    try:
515        getChannel(irc, msg, args, state, **kwargs)
516        channel = channelSpecific.getChannelLink(state.channel)
517        state.channel = channel
518        state.args[-1] = channel
519    except (callbacks.ArgumentError, IndexError):
520        if channelSpecific():
521            raise
522        channel = channelSpecific.link()
523        if not conf.get(channelSpecific.link.allow, channel):
524            log.warning('channelSpecific.link is globally set to %s, but '
525                        '%s disallowed linking to its db.', channel, channel)
526            raise
527        else:
528            channel = channelSpecific.getChannelLink(channel)
529            state.channel = channel
530            state.args.append(channel)
531
532def inChannel(irc, msg, args, state):
533    getChannel(irc, msg, args, state)
534    if state.channel not in irc.state.channels:
535        state.error(_('I\'m not in %s.') % state.channel, Raise=True)
536
537def onlyInChannel(irc, msg, args, state):
538    if not (irc.isChannel(msg.args[0]) and msg.args[0] in irc.state.channels):
539        state.error(_('This command may only be given in a channel that I am '
540                    'in.'), Raise=True)
541    else:
542        state.channel = msg.args[0]
543        state.args.append(state.channel)
544
545def callerInGivenChannel(irc, msg, args, state):
546    channel = args[0]
547    if irc.isChannel(channel):
548        if channel in irc.state.channels:
549            if msg.nick in irc.state.channels[channel].users:
550                state.args.append(args.pop(0))
551            else:
552                state.error(_('You must be in %s.') % channel, Raise=True)
553        else:
554            state.error(_('I\'m not in %s.') % channel, Raise=True)
555    else:
556        state.errorInvalid(_('channel'), args[0])
557
558def nickInChannel(irc, msg, args, state):
559    originalArgs = state.args[:]
560    inChannel(irc, msg, args, state)
561    state.args = originalArgs
562    if args[0] not in irc.state.channels[state.channel].users:
563        state.error(_('%s is not in %s.') % (args[0], state.channel), Raise=True)
564    state.args.append(args.pop(0))
565
566def getChannelOrNone(irc, msg, args, state):
567    try:
568        getChannel(irc, msg, args, state)
569    except callbacks.ArgumentError:
570        state.args.append(None)
571
572def getChannelOrGlobal(irc, msg, args, state):
573    if args and args[0] == 'global':
574        channel = args.pop(0)
575        channel = 'global'
576    elif args and irc.isChannel(args[0]):
577        channel = args.pop(0)
578        state.channel = channel
579    elif irc.isChannel(msg.args[0]):
580        channel = msg.args[0]
581        state.channel = channel
582    else:
583        state.log.debug('Raising ArgumentError because there is no channel.')
584        raise callbacks.ArgumentError
585    state.args.append(channel)
586
587def checkChannelCapability(irc, msg, args, state, cap):
588    getChannel(irc, msg, args, state)
589    cap = ircdb.canonicalCapability(cap)
590    cap = ircdb.makeChannelCapability(state.channel, cap)
591    if not ircdb.checkCapability(msg.prefix, cap):
592        state.errorNoCapability(cap, Raise=True)
593
594def getOp(irc, msg, args, state):
595    checkChannelCapability(irc, msg, args, state, 'op')
596
597def getHalfop(irc, msg, args, state):
598    checkChannelCapability(irc, msg, args, state, 'halfop')
599
600def getVoice(irc, msg, args, state):
601    checkChannelCapability(irc, msg, args, state, 'voice')
602
603def getLowered(irc, msg, args, state):
604    state.args.append(ircutils.toLower(args.pop(0)))
605
606def getSomething(irc, msg, args, state, errorMsg=None, p=None):
607    if p is None:
608        p = lambda _: True
609    if not args[0] or not p(args[0]):
610        if errorMsg is None:
611            errorMsg = _('You must not give the empty string as an argument.')
612        state.error(errorMsg, Raise=True)
613    else:
614        state.args.append(args.pop(0))
615
616def getSomethingNoSpaces(irc, msg, args, state, *L):
617    def p(s):
618        return len(s.split(None, 1)) == 1
619    L = L or [_('You must not give a string containing spaces as an argument.')]
620    getSomething(irc, msg, args, state, p=p, *L)
621
622def private(irc, msg, args, state):
623    if irc.isChannel(msg.args[0]):
624        state.errorRequiresPrivacy(Raise=True)
625
626def public(irc, msg, args, state, errmsg=None):
627    if not irc.isChannel(msg.args[0]):
628        if errmsg is None:
629            errmsg = _('This message must be sent in a channel.')
630        state.error(errmsg, Raise=True)
631
632def checkCapability(irc, msg, args, state, cap):
633    cap = ircdb.canonicalCapability(cap)
634    if not ircdb.checkCapability(msg.prefix, cap):
635        state.errorNoCapability(cap, Raise=True)
636
637def checkCapabilityButIgnoreOwner(irc, msg, args, state, cap):
638    cap = ircdb.canonicalCapability(cap)
639    if not ircdb.checkCapability(msg.prefix, cap, ignoreOwner=True):
640        state.errorNoCapability(cap, Raise=True)
641
642def owner(irc, msg, args, state):
643    checkCapability(irc, msg, args, state, 'owner')
644
645def admin(irc, msg, args, state):
646    checkCapability(irc, msg, args, state, 'admin')
647
648def anything(irc, msg, args, state):
649    state.args.append(args.pop(0))
650
651def getGlob(irc, msg, args, state):
652    glob = args.pop(0)
653    if '*' not in glob and '?' not in glob:
654        glob = '*%s*' % glob
655    state.args.append(glob)
656
657def getUrl(irc, msg, args, state):
658    if utils.web.urlRe.match(args[0]):
659        state.args.append(args.pop(0))
660    else:
661        state.errorInvalid(_('url'), args[0])
662
663def getEmail(irc, msg, args, state):
664    if utils.net.emailRe.match(args[0]):
665        state.args.append(args.pop(0))
666    else:
667        state.errorInvalid(_('email'), args[0])
668
669def getHttpUrl(irc, msg, args, state):
670    if utils.web.httpUrlRe.match(args[0]):
671        state.args.append(args.pop(0))
672    elif utils.web.httpUrlRe.match('http://' + args[0]):
673        state.args.append('http://' + args.pop(0))
674    else:
675        state.errorInvalid(_('http url'), args[0])
676
677def getNow(irc, msg, args, state):
678    state.args.append(int(time.time()))
679
680def getCommandName(irc, msg, args, state):
681    if ' ' in args[0]:
682        state.errorInvalid(_('command name'), args[0])
683    else:
684        state.args.append(callbacks.canonicalName(args.pop(0)))
685
686def getIp(irc, msg, args, state):
687    if utils.net.isIP(args[0]):
688        state.args.append(args.pop(0))
689    else:
690        state.errorInvalid(_('ip'), args[0])
691
692def getLetter(irc, msg, args, state):
693    if len(args[0]) == 1:
694        state.args.append(args.pop(0))
695    else:
696        state.errorInvalid(_('letter'), args[0])
697
698def getMatch(irc, msg, args, state, regexp, errmsg):
699    m = regexp.search(args[0])
700    if m is not None:
701        state.args.append(m)
702        del args[0]
703    else:
704        state.error(errmsg, Raise=True)
705
706def getLiteral(irc, msg, args, state, literals, errmsg=None):
707    # ??? Should we allow abbreviations?
708    if isinstance(literals, minisix.string_types):
709        literals = (literals,)
710    abbrevs = utils.abbrev(literals)
711    if args[0] in abbrevs:
712        state.args.append(abbrevs[args.pop(0)])
713    elif errmsg is not None:
714        state.error(errmsg, Raise=True)
715    else:
716        raise callbacks.ArgumentError
717
718def getTo(irc, msg, args, state):
719    if args[0].lower() == 'to':
720        args.pop(0)
721
722def getPlugin(irc, msg, args, state, require=True):
723    cb = irc.getCallback(args[0])
724    if cb is not None:
725        state.args.append(cb)
726        del args[0]
727    elif require:
728        state.errorInvalid(_('plugin'), args[0])
729    else:
730        state.args.append(None)
731
732def getIrcColor(irc, msg, args, state):
733    if args[0] in ircutils.mircColors:
734        state.args.append(ircutils.mircColors[args.pop(0)])
735    else:
736        state.errorInvalid(_('irc color'))
737
738def getText(irc, msg, args, state):
739    if args:
740        state.args.append(' '.join(args))
741        args[:] = []
742    else:
743        raise IndexError
744
745wrappers = ircutils.IrcDict({
746    'admin': admin,
747    'anything': anything,
748    'banmask': getBanmask,
749    'boolean': getBoolean,
750    'callerInGivenChannel': callerInGivenChannel,
751    'isGranted': getHaveHalfopPlus, # Backward compatibility
752    'capability': getSomethingNoSpaces,
753    'channel': getChannel,
754    'channels': getChannels,
755    'channelOrGlobal': getChannelOrGlobal,
756    'channelDb': getChannelDb,
757    'checkCapability': checkCapability,
758    'checkCapabilityButIgnoreOwner': checkCapabilityButIgnoreOwner,
759    'checkChannelCapability': checkChannelCapability,
760    'color': getIrcColor,
761    'commandName': getCommandName,
762    'email': getEmail,
763    'expiry': getExpiry,
764    'filename': getSomething, # XXX Check for validity.
765    'float': getFloat,
766    'glob': getGlob,
767    'halfop': getHalfop,
768    'haveHalfop': getHaveHalfop,
769    'haveHalfop+': getHaveHalfopPlus,
770    'haveOp': getHaveOp,
771    'haveOp+': getHaveOp, # We don't handle modes greater than op.
772    'haveVoice': getHaveVoice,
773    'haveVoice+': getHaveVoicePlus,
774    'hostmask': getHostmask,
775    'httpUrl': getHttpUrl,
776    'id': getId,
777    'inChannel': inChannel,
778    'index': getIndex,
779    'int': getInt,
780    'ip': getIp,
781    'letter': getLetter,
782    'literal': getLiteral,
783    'long': getLong,
784    'lowered': getLowered,
785    'matches': getMatch,
786    'networkIrc': getNetworkIrc,
787    'nick': getNick,
788    'nickInChannel': nickInChannel,
789    'nonInt': getNonInt,
790    'nonNegativeInt': getNonNegativeInt,
791    'now': getNow,
792    'onlyInChannel': onlyInChannel,
793    'op': getOp,
794    'otherUser': getOtherUser,
795    'owner': owner,
796    'plugin': getPlugin,
797    'positiveInt': getPositiveInt,
798    'private': private,
799    'public': public,
800    'regexpMatcher': getMatcher,
801    'regexpMatcherMany': getMatcherMany,
802    'regexpReplacer': getReplacer,
803    'seenNick': getSeenNick,
804    'something': getSomething,
805    'somethingWithoutSpaces': getSomethingNoSpaces,
806    'text': getText,
807    'to': getTo,
808    'url': getUrl,
809    'user': getUser,
810    'validChannel': validChannel,
811    'voice': getVoice,
812})
813
814def addConverter(name, wrapper):
815    wrappers[name] = wrapper
816
817class UnknownConverter(KeyError):
818    pass
819
820def getConverter(name):
821    try:
822        return wrappers[name]
823    except KeyError as e:
824        raise UnknownConverter(str(e))
825
826def callConverter(name, irc, msg, args, state, *L):
827    getConverter(name)(irc, msg, args, state, *L)
828
829###
830# Contexts.  These determine what the nature of conversions is; whether they're
831# defaulted, or many of them are allowed, etc.  Contexts should be reusable;
832# i.e., they should not maintain state between calls.
833###
834def contextify(spec):
835    if not isinstance(spec, context):
836        spec = context(spec)
837    return spec
838
839def setDefault(state, default):
840    if callable(default):
841        state.args.append(default())
842    else:
843        state.args.append(default)
844
845class context(object):
846    def __init__(self, spec):
847        self.args = ()
848        self.spec = spec # for repr
849        if isinstance(spec, tuple):
850            assert spec, 'tuple spec must not be empty.'
851            self.args = spec[1:]
852            self.converter = getConverter(spec[0])
853        elif spec is None:
854            self.converter = getConverter('anything')
855        elif isinstance(spec, minisix.string_types):
856            self.args = ()
857            self.converter = getConverter(spec)
858        else:
859            assert isinstance(spec, context)
860            self.converter = spec
861
862    def __call__(self, irc, msg, args, state):
863        log.debug('args before %r: %r', self, args)
864        self.converter(irc, msg, args, state, *self.args)
865        log.debug('args after %r: %r', self, args)
866
867    def __repr__(self):
868        return '<%s for %s>' % (self.__class__.__name__, self.spec)
869
870class rest(context):
871    def __call__(self, irc, msg, args, state):
872        if args:
873            original = args[:]
874            args[:] = [' '.join(args)]
875            try:
876                super(rest, self).__call__(irc, msg, args, state)
877            except Exception:
878                args[:] = original
879        else:
880            raise IndexError
881
882# additional means:  Look for this (and make sure it's of this type).  If
883# there are no arguments for us to check, then use our default.
884class additional(context):
885    def __init__(self, spec, default=None):
886        self.__parent = super(additional, self)
887        self.__parent.__init__(spec)
888        self.default = default
889
890    def __call__(self, irc, msg, args, state):
891        try:
892            self.__parent.__call__(irc, msg, args, state)
893        except IndexError:
894            log.debug('Got IndexError, returning default.')
895            setDefault(state, self.default)
896
897# optional means: Look for this, but if it's not the type I'm expecting or
898# there are no arguments for us to check, then use the default value.
899class optional(additional):
900    def __call__(self, irc, msg, args, state):
901        try:
902            super(optional, self).__call__(irc, msg, args, state)
903        except (callbacks.ArgumentError, callbacks.Error) as e:
904            log.debug('Got %s, returning default.', utils.exnToString(e))
905            state.errored = False
906            setDefault(state, self.default)
907
908class any(context):
909    def __init__(self, spec, continueOnError=False):
910        self.__parent = super(any, self)
911        self.__parent.__init__(spec)
912        self.continueOnError = continueOnError
913
914    def __call__(self, irc, msg, args, state):
915        st = state.essence()
916        try:
917            while args:
918                self.__parent.__call__(irc, msg, args, st)
919        except IndexError:
920            pass
921        except (callbacks.ArgumentError, callbacks.Error) as e:
922            if not self.continueOnError:
923                raise
924            else:
925                log.debug('Got %s, returning default.', utils.exnToString(e))
926                pass
927        state.args.append(st.args)
928
929class many(any):
930    def __call__(self, irc, msg, args, state):
931        super(many, self).__call__(irc, msg, args, state)
932        if not state.args[-1]:
933            state.args.pop()
934            raise callbacks.ArgumentError
935
936class first(context):
937    def __init__(self, *specs, **kw):
938        if 'default' in kw:
939            self.default = kw.pop('default')
940            assert not kw, 'Bad kwargs for first.__init__'
941        self.spec = specs # for __repr__
942        self.specs = list(map(contextify, specs))
943
944    def __call__(self, irc, msg, args, state):
945        errored = False
946        for spec in self.specs:
947            try:
948                spec(irc, msg, args, state)
949                return
950            except Exception as e:
951                e2 = e # 'e' is local.
952                errored = state.errored
953                state.errored = False
954                continue
955        if hasattr(self, 'default'):
956            state.args.append(self.default)
957        else:
958            state.errored = errored
959            raise e2
960
961class reverse(context):
962    def __call__(self, irc, msg, args, state):
963        args[:] = args[::-1]
964        super(reverse, self).__call__(irc, msg, args, state)
965        args[:] = args[::-1]
966
967class commalist(context):
968    def __call__(self, irc, msg, args, state):
969        original = args[:]
970        st = state.essence()
971        trailingComma = True
972        try:
973            while trailingComma:
974                arg = args.pop(0)
975                if not arg.endswith(','):
976                    trailingComma = False
977                for part in arg.split(','):
978                    if part: # trailing commas
979                        super(commalist, self).__call__(irc, msg, [part], st)
980            state.args.append(st.args)
981        except Exception:
982            args[:] = original
983            raise
984
985class getopts(context):
986    """The empty string indicates that no argument is taken; None indicates
987    that there is no converter for the argument."""
988    def __init__(self, getopts):
989        self.spec = getopts # for repr
990        self.getopts = {}
991        self.getoptL = []
992        self.getoptLs = ''
993        for (name, spec) in getopts.items():
994            if spec == '':
995                if len(name) == 1:
996                    self.getoptLs += name
997                    self.getopts[name] = None
998                self.getoptL.append(name)
999                self.getopts[name] = None
1000            else:
1001                if len(name) == 1:
1002                    self.getoptLs += name + ':'
1003                    self.getopts[name] = contextify(spec)
1004                self.getoptL.append(name + '=')
1005                self.getopts[name] = contextify(spec)
1006        log.debug('getopts: %r', self.getopts)
1007        log.debug('getoptL: %r', self.getoptL)
1008
1009    def __call__(self, irc, msg, args, state):
1010        log.debug('args before %r: %r', self, args)
1011        (optlist, rest) = getopt.getopt(args, self.getoptLs, self.getoptL)
1012        getopts = []
1013        for (opt, arg) in optlist:
1014            if opt.startswith('--'):
1015                opt = opt[2:] # Strip --
1016            else:
1017                opt = opt[1:]
1018            log.debug('opt: %r, arg: %r', opt, arg)
1019            context = self.getopts[opt]
1020            if context is not None:
1021                st = state.essence()
1022                context(irc, msg, [arg], st)
1023                assert len(st.args) == 1
1024                getopts.append((opt, st.args[0]))
1025            else:
1026                getopts.append((opt, True))
1027        state.args.append(getopts)
1028        args[:] = rest
1029        log.debug('args after %r: %r', self, args)
1030
1031###
1032# This is our state object, passed to converters along with irc, msg, and args.
1033###
1034
1035class State(object):
1036    log = log
1037    def __init__(self, types):
1038        self.args = []
1039        self.kwargs = {}
1040        self.types = types
1041        self.channel = None
1042        self.errored = False
1043
1044    def __getattr__(self, attr):
1045        if attr.startswith('error'):
1046            self.errored = True
1047            return getattr(dynamic.irc, attr)
1048        else:
1049            raise AttributeError(attr)
1050
1051    def essence(self):
1052        st = State(self.types)
1053        for (attr, value) in self.__dict__.items():
1054            if attr not in ('args', 'kwargs'):
1055                setattr(st, attr, value)
1056        return st
1057
1058    def __repr__(self):
1059        return '%s(args=%r, kwargs=%r, channel=%r)' % (self.__class__.__name__,
1060                                                       self.args, self.kwargs,
1061                                                       self.channel)
1062
1063
1064###
1065# This is a compiled Spec object.
1066###
1067class Spec(object):
1068    def _state(self, types, attrs={}):
1069        st = State(types)
1070        st.__dict__.update(attrs)
1071        st.allowExtra = self.allowExtra
1072        return st
1073
1074    def __init__(self, types, allowExtra=False):
1075        self.types = types
1076        self.allowExtra = allowExtra
1077        utils.seq.mapinto(contextify, self.types)
1078
1079    def __call__(self, irc, msg, args, stateAttrs={}):
1080        state = self._state(self.types[:], stateAttrs)
1081        while state.types:
1082            context = state.types.pop(0)
1083            try:
1084                context(irc, msg, args, state)
1085            except IndexError:
1086                raise callbacks.ArgumentError
1087        if args and not state.allowExtra:
1088            log.debug('args and not self.allowExtra: %r', args)
1089            raise callbacks.ArgumentError
1090        return state
1091
1092def _wrap(f, specList=[], name=None, checkDoc=True, **kw):
1093    name = name or f.__name__
1094    assert (not checkDoc) or (hasattr(f, '__doc__') and f.__doc__), \
1095                'Command %r has no docstring.' % name
1096    spec = Spec(specList, **kw)
1097    def newf(self, irc, msg, args, **kwargs):
1098        state = spec(irc, msg, args, stateAttrs={'cb': self, 'log': self.log})
1099        self.log.debug('State before call: %s', state)
1100        if state.errored:
1101            self.log.debug('Refusing to call %s due to state.errored.', f)
1102        else:
1103            try:
1104                f(self, irc, msg, args, *state.args, **state.kwargs)
1105            except TypeError:
1106                self.log.error('Spec: %s', specList)
1107                self.log.error('Received args: %s', args)
1108                code = f.__code__
1109                funcArgs = inspect.getargs(code)[0][len(self.commandArgs):]
1110                self.log.error('Extra args: %s', funcArgs)
1111                self.log.debug('Make sure you did not wrap a wrapped '
1112                               'function ;)')
1113                raise
1114    newf2 = utils.python.changeFunctionName(newf, name, f.__doc__)
1115    newf2.__module__ = f.__module__
1116    return internationalizeDocstring(newf2)
1117
1118def wrap(f, *args, **kwargs):
1119    if callable(f):
1120        # Old-style call OR decorator syntax with no converter.
1121        # f is the command.
1122        return _wrap(f, *args, **kwargs)
1123    else:
1124        # Call with the Python decorator syntax
1125        assert isinstance(f, list) or isinstance(f, tuple)
1126        specList = f
1127        def decorator(f):
1128            return _wrap(f, specList, *args, **kwargs)
1129        return decorator
1130wrap.__doc__ = """Useful wrapper for plugin commands.
1131
1132Valid converters are: %s.
1133
1134:param f: A command, taking (self, irc, msg, args, ...) as arguments
1135:param specList: A list of converters and contexts""" % \
1136        ', '.join(sorted(wrappers.keys()))
1137
1138__all__ = [
1139    # Contexts.
1140    'any', 'many',
1141    'optional', 'additional',
1142    'rest', 'getopts',
1143    'first', 'reverse',
1144    'commalist',
1145    # Converter helpers.
1146    'getConverter', 'addConverter', 'callConverter',
1147    # Decorators.
1148    'urlSnarfer', 'thread',
1149    # Functions.
1150    'wrap', 'process', 'regexp_wrapper',
1151    # Stuff for testing.
1152    'Spec',
1153]
1154
1155# This doesn't work.  Suck.
1156## if world.testing:
1157##     __all__.append('Spec')
1158
1159# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
1160