1# -*- coding: utf8 -*-
2###
3# Copyright (c) 2002-2005, Jeremiah Fincher
4# Copyright (c) 2014, James McCoy
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions are met:
9#
10#   * Redistributions of source code must retain the above copyright notice,
11#     this list of conditions, and the following disclaimer.
12#   * Redistributions in binary form must reproduce the above copyright notice,
13#     this list of conditions, and the following disclaimer in the
14#     documentation and/or other materials provided with the distribution.
15#   * Neither the name of the author of this software nor the name of
16#     contributors to this software may be used to endorse or promote products
17#     derived from this software without specific prior written consent.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29# POSSIBILITY OF SUCH DAMAGE.
30###
31
32"""
33This module contains the basic callbacks for handling PRIVMSGs.
34"""
35
36import re
37import copy
38import time
39from . import shlex
40import codecs
41import getopt
42import inspect
43
44from . import (conf, ircdb, irclib, ircmsgs, ircutils, log, registry,
45        utils, world)
46from .utils import minisix
47from .utils.iter import any, all
48from .i18n import PluginInternationalization
49_ = PluginInternationalization()
50
51def _addressed(nick, msg, prefixChars=None, nicks=None,
52              prefixStrings=None, whenAddressedByNick=None,
53              whenAddressedByNickAtEnd=None):
54    def get(group):
55        if ircutils.isChannel(target):
56            group = group.get(target)
57        return group()
58    def stripPrefixStrings(payload):
59        for prefixString in prefixStrings:
60            if payload.startswith(prefixString):
61                payload = payload[len(prefixString):].lstrip()
62        return payload
63
64    assert msg.command == 'PRIVMSG'
65    (target, payload) = msg.args
66    if not payload:
67        return ''
68    if prefixChars is None:
69        prefixChars = get(conf.supybot.reply.whenAddressedBy.chars)
70    if whenAddressedByNick is None:
71        whenAddressedByNick = get(conf.supybot.reply.whenAddressedBy.nick)
72    if whenAddressedByNickAtEnd is None:
73        r = conf.supybot.reply.whenAddressedBy.nick.atEnd
74        whenAddressedByNickAtEnd = get(r)
75    if prefixStrings is None:
76        prefixStrings = get(conf.supybot.reply.whenAddressedBy.strings)
77    # We have to check this before nicks -- try "@google supybot" with supybot
78    # and whenAddressedBy.nick.atEnd on to see why.
79    if any(payload.startswith, prefixStrings):
80        return stripPrefixStrings(payload)
81    elif payload[0] in prefixChars:
82        return payload[1:].strip()
83    if nicks is None:
84        nicks = get(conf.supybot.reply.whenAddressedBy.nicks)
85        nicks = list(map(ircutils.toLower, nicks))
86    else:
87        nicks = list(nicks) # Just in case.
88    nicks.insert(0, ircutils.toLower(nick))
89    # Ok, let's see if it's a private message.
90    if ircutils.nickEqual(target, nick):
91        payload = stripPrefixStrings(payload)
92        while payload and payload[0] in prefixChars:
93            payload = payload[1:].lstrip()
94        return payload
95    # Ok, not private.  Does it start with our nick?
96    elif whenAddressedByNick:
97        for nick in nicks:
98            lowered = ircutils.toLower(payload)
99            if lowered.startswith(nick):
100                try:
101                    (maybeNick, rest) = payload.split(None, 1)
102                    toContinue = False
103                    while not ircutils.isNick(maybeNick, strictRfc=True):
104                        if maybeNick[-1].isalnum():
105                            toContinue = True
106                            break
107                        maybeNick = maybeNick[:-1]
108                    if toContinue:
109                        continue
110                    if ircutils.nickEqual(maybeNick, nick):
111                        return rest
112                    else:
113                        continue
114                except ValueError: # split didn't work.
115                    continue
116            elif whenAddressedByNickAtEnd and lowered.endswith(nick):
117                rest = payload[:-len(nick)]
118                possiblePayload = rest.rstrip(' \t,;')
119                if possiblePayload != rest:
120                    # There should be some separator between the nick and the
121                    # previous alphanumeric character.
122                    return possiblePayload
123    if get(conf.supybot.reply.whenNotAddressed):
124        return payload
125    else:
126        return ''
127
128def addressed(nick, msg, **kwargs):
129    """If msg is addressed to 'name', returns the portion after the address.
130    Otherwise returns the empty string.
131    """
132    payload = msg.addressed
133    if payload is not None:
134        return payload
135    else:
136        payload = _addressed(nick, msg, **kwargs)
137        msg.tag('addressed', payload)
138        return payload
139
140def canonicalName(command, preserve_spaces=False):
141    """Turn a command into its canonical form.
142
143    Currently, this makes everything lowercase and removes all dashes and
144    underscores.
145    """
146    if minisix.PY2 and isinstance(command, unicode):
147        command = command.encode('utf-8')
148    elif minisix.PY3 and isinstance(command, bytes):
149        command = command.decode()
150    special = '\t-_'
151    if not preserve_spaces:
152        special += ' '
153    reAppend = ''
154    while command and command[-1] in special:
155        reAppend = command[-1] + reAppend
156        command = command[:-1]
157    return ''.join([x for x in command if x not in special]).lower() + reAppend
158
159def reply(msg, s, prefixNick=None, private=None,
160          notice=None, to=None, action=None, error=False,
161          stripCtcp=True):
162    msg.tag('repliedTo')
163    # Ok, let's make the target:
164    # XXX This isn't entirely right.  Consider to=#foo, private=True.
165    target = ircutils.replyTo(msg)
166    if ircutils.isChannel(to):
167        target = to
168    if ircutils.isChannel(target):
169        channel = target
170    else:
171        channel = None
172    if notice is None:
173        notice = conf.get(conf.supybot.reply.withNotice, channel)
174    if private is None:
175        private = conf.get(conf.supybot.reply.inPrivate, channel)
176    if prefixNick is None:
177        prefixNick = conf.get(conf.supybot.reply.withNickPrefix, channel)
178    if error:
179        notice =conf.get(conf.supybot.reply.error.withNotice, channel) or notice
180        private=conf.get(conf.supybot.reply.error.inPrivate, channel) or private
181        s = _('Error: ') + s
182    if private:
183        prefixNick = False
184        if to is None:
185            target = msg.nick
186        else:
187            target = to
188    if action:
189        prefixNick = False
190    if to is None:
191        to = msg.nick
192    if stripCtcp:
193        s = s.strip('\x01')
194    # Ok, now let's make the payload:
195    s = ircutils.safeArgument(s)
196    if not s and not action:
197        s = _('Error: I tried to send you an empty message.')
198    if prefixNick and ircutils.isChannel(target):
199        # Let's may sure we don't do, "#channel: foo.".
200        if not ircutils.isChannel(to):
201            s = '%s: %s' % (to, s)
202    if not ircutils.isChannel(target):
203        if conf.supybot.reply.withNoticeWhenPrivate():
204            notice = True
205    # And now, let's decide whether it's a PRIVMSG or a NOTICE.
206    msgmaker = ircmsgs.privmsg
207    if notice:
208        msgmaker = ircmsgs.notice
209    # We don't use elif here because actions can't be sent as NOTICEs.
210    if action:
211        msgmaker = ircmsgs.action
212    # Finally, we'll return the actual message.
213    ret = msgmaker(target, s)
214    ret.tag('inReplyTo', msg)
215    return ret
216
217def error(msg, s, **kwargs):
218    """Makes an error reply to msg with the appropriate error payload."""
219    kwargs['error'] = True
220    msg.tag('isError')
221    return reply(msg, s, **kwargs)
222
223def getHelp(method, name=None, doc=None):
224    if name is None:
225        name = method.__name__
226    if doc is None:
227        if method.__doc__ is None:
228            doclines = ['This command has no help.  Complain to the author.']
229        else:
230            doclines = method.__doc__.splitlines()
231    else:
232        doclines = doc.splitlines()
233    s = '%s %s' % (name, doclines.pop(0))
234    if doclines:
235        help = ' '.join(doclines)
236        s = '(%s) -- %s' % (ircutils.bold(s), help)
237    return utils.str.normalizeWhitespace(s)
238
239def getSyntax(method, name=None, doc=None):
240    if name is None:
241        name = method.__name__
242    if doc is None:
243        doclines = method.__doc__.splitlines()
244    else:
245        doclines = doc.splitlines()
246    return '%s %s' % (name, doclines[0])
247
248class Error(Exception):
249    """Generic class for errors in Privmsg callbacks."""
250    pass
251
252class ArgumentError(Error):
253    """The bot replies with a help message when this is raised."""
254    pass
255
256class SilentError(Error):
257    """An error that we should not notify the user."""
258    pass
259
260class Tokenizer(object):
261    # This will be used as a global environment to evaluate strings in.
262    # Evaluation is, of course, necessary in order to allow escaped
263    # characters to be properly handled.
264    #
265    # These are the characters valid in a token.  Everything printable except
266    # double-quote, left-bracket, and right-bracket.
267    separators = '\x00\r\n \t'
268    def __init__(self, brackets='', pipe=False, quotes='"'):
269        if brackets:
270            self.separators += brackets
271            self.left = brackets[0]
272            self.right = brackets[1]
273        else:
274            self.left = ''
275            self.right = ''
276        self.pipe = pipe
277        if self.pipe:
278            self.separators += '|'
279        self.quotes = quotes
280        self.separators += quotes
281
282
283    def _handleToken(self, token):
284        if token[0] == token[-1] and token[0] in self.quotes:
285            token = token[1:-1]
286            # FIXME: No need to tell you this is a hack.
287            # It has to handle both IRC commands and serialized configuration.
288            #
289            # Whoever you are, if you make a single modification to this
290            # code, TEST the code with Python 2 & 3, both with the unit
291            # tests and on IRC with this: @echo "好"
292            if minisix.PY2:
293                try:
294                    token = token.encode('utf8').decode('string_escape')
295                    token = token.decode('utf8')
296                except:
297                    token = token.decode('string_escape')
298            else:
299                token = codecs.getencoder('utf8')(token)[0]
300                token = codecs.getdecoder('unicode_escape')(token)[0]
301                try:
302                    token = token.encode('iso-8859-1').decode()
303                except: # Prevent issue with tokens like '"\\x80"'.
304                    pass
305        return token
306
307    def _insideBrackets(self, lexer):
308        ret = []
309        while True:
310            token = lexer.get_token()
311            if not token:
312                raise SyntaxError(_('Missing "%s".  You may want to '
313                                   'quote your arguments with double '
314                                   'quotes in order to prevent extra '
315                                   'brackets from being evaluated '
316                                   'as nested commands.') % self.right)
317            elif token == self.right:
318                return ret
319            elif token == self.left:
320                ret.append(self._insideBrackets(lexer))
321            else:
322                ret.append(self._handleToken(token))
323        return ret
324
325    def tokenize(self, s):
326        lexer = shlex.shlex(minisix.io.StringIO(s))
327        lexer.commenters = ''
328        lexer.quotes = self.quotes
329        lexer.separators = self.separators
330        args = []
331        ends = []
332        while True:
333            token = lexer.get_token()
334            if not token:
335                break
336            elif token == '|' and self.pipe:
337                # The "and self.pipe" might seem redundant here, but it's there
338                # for strings like 'foo | bar', where a pipe stands alone as a
339                # token, but shouldn't be treated specially.
340                if not args:
341                    raise SyntaxError(_('"|" with nothing preceding.  I '
342                                       'obviously can\'t do a pipe with '
343                                       'nothing before the |.'))
344                ends.append(args)
345                args = []
346            elif token == self.left:
347                args.append(self._insideBrackets(lexer))
348            elif token == self.right:
349                raise SyntaxError(_('Spurious "%s".  You may want to '
350                                   'quote your arguments with double '
351                                   'quotes in order to prevent extra '
352                                   'brackets from being evaluated '
353                                   'as nested commands.') % self.right)
354            else:
355                args.append(self._handleToken(token))
356        if ends:
357            if not args:
358                raise SyntaxError(_('"|" with nothing following.  I '
359                                   'obviously can\'t do a pipe with '
360                                   'nothing after the |.'))
361            args.append(ends.pop())
362            while ends:
363                args[-1].append(ends.pop())
364        return args
365
366def tokenize(s, channel=None):
367    """A utility function to create a Tokenizer and tokenize a string."""
368    pipe = False
369    brackets = ''
370    nested = conf.supybot.commands.nested
371    if nested():
372        brackets = conf.get(nested.brackets, channel)
373        if conf.get(nested.pipeSyntax, channel): # No nesting, no pipe.
374            pipe = True
375    quotes = conf.get(conf.supybot.commands.quotes, channel)
376    try:
377        ret = Tokenizer(brackets=brackets,pipe=pipe,quotes=quotes).tokenize(s)
378        return ret
379    except ValueError as e:
380        raise SyntaxError(str(e))
381
382def formatCommand(command):
383    return ' '.join(command)
384
385def checkCommandCapability(msg, cb, commandName):
386    plugin = cb.name().lower()
387    if not isinstance(commandName, minisix.string_types):
388        assert commandName[0] == plugin, ('checkCommandCapability no longer '
389                'accepts command names that do not start with the callback\'s '
390                'name (%s): %s') % (plugin, commandName)
391        commandName = '.'.join(commandName)
392    def checkCapability(capability):
393        assert ircdb.isAntiCapability(capability)
394        if ircdb.checkCapability(msg.prefix, capability):
395            log.info('Preventing %s from calling %s because of %s.',
396                     msg.prefix, commandName, capability)
397            raise RuntimeError(capability)
398    try:
399        antiCommand = ircdb.makeAntiCapability(commandName)
400        checkCapability(antiCommand)
401        checkAtEnd = [commandName]
402        default = conf.supybot.capabilities.default()
403        if ircutils.isChannel(msg.args[0]):
404            channel = msg.args[0]
405            checkCapability(ircdb.makeChannelCapability(channel, antiCommand))
406            chanCommand = ircdb.makeChannelCapability(channel, commandName)
407            checkAtEnd += [chanCommand]
408            default &= ircdb.channels.getChannel(channel).defaultAllow
409        return not (default or \
410                    any(lambda x: ircdb.checkCapability(msg.prefix, x),
411                        checkAtEnd))
412    except RuntimeError as e:
413        s = ircdb.unAntiCapability(str(e))
414        return s
415
416
417class RichReplyMethods(object):
418    """This is a mixin so these replies need only be defined once.  It operates
419    under several assumptions, including the fact that 'self' is an Irc object
420    of some sort and there is a self.msg that is an IrcMsg."""
421    def __makeReply(self, prefix, s):
422        if s:
423            s = '%s  %s' % (prefix, s)
424        else:
425            s = prefix
426        return ircutils.standardSubstitute(self, self.msg, s)
427
428    def _getConfig(self, wrapper):
429        return conf.get(wrapper, self.msg.args[0])
430
431    def replySuccess(self, s='', **kwargs):
432        v = self._getConfig(conf.supybot.replies.success)
433        if v:
434            s = self.__makeReply(v, s)
435            return self.reply(s, **kwargs)
436        else:
437            self.noReply()
438
439    def replyError(self, s='', **kwargs):
440        v = self._getConfig(conf.supybot.replies.error)
441        if 'msg' in kwargs:
442            msg = kwargs['msg']
443            if ircdb.checkCapability(msg.prefix, 'owner'):
444                v = self._getConfig(conf.supybot.replies.errorOwner)
445        s = self.__makeReply(v, s)
446        return self.reply(s, **kwargs)
447
448    def _getTarget(self, to=None):
449        """Compute the target according to self.to, the provided to,
450        and self.private, and return it. Mainly used by reply methods."""
451        # FIXME: Don't set self.to.
452        # I still set it to be sure I don't introduce a regression,
453        # but it does not make sense for .reply() and .replies() to
454        # change the state of this Irc object.
455        if to is not None:
456            self.to = self.to or to
457        target = self.private and self.to or self.msg.args[0]
458        return target
459
460    def replies(self, L, prefixer=None, joiner=None,
461                onlyPrefixFirst=False,
462                oneToOne=None, **kwargs):
463        if prefixer is None:
464            prefixer = ''
465        if joiner is None:
466            joiner = utils.str.commaAndify
467        if isinstance(prefixer, minisix.string_types):
468            prefixer = prefixer.__add__
469        if isinstance(joiner, minisix.string_types):
470            joiner = joiner.join
471        to = self._getTarget(kwargs.get('to'))
472        if oneToOne is None: # Can be True, False, or None
473            if self.irc.isChannel(to):
474                oneToOne = conf.get(conf.supybot.reply.oneToOne, to)
475            else:
476                oneToOne = conf.supybot.reply.oneToOne()
477        if oneToOne:
478            return self.reply(prefixer(joiner(L)), **kwargs)
479        else:
480            msg = None
481            first = True
482            for s in L:
483                if onlyPrefixFirst:
484                    if first:
485                        first = False
486                        msg = self.reply(prefixer(s), **kwargs)
487                    else:
488                        msg = self.reply(s, **kwargs)
489                else:
490                    msg = self.reply(prefixer(s), **kwargs)
491            return msg
492
493    def noReply(self, msg=None):
494        self.repliedTo = True
495
496    def _error(self, s, Raise=False, **kwargs):
497        if Raise:
498            raise Error(s)
499        else:
500            return self.error(s, **kwargs)
501
502    def errorNoCapability(self, capability, s='', **kwargs):
503        if 'Raise' not in kwargs:
504            kwargs['Raise'] = True
505        log.warning('Denying %s for lacking %q capability.',
506                    self.msg.prefix, capability)
507        # noCapability means "don't send a specific capability error
508        # message" not "don't send a capability error message at all", like
509        # one would think
510        if self._getConfig(conf.supybot.reply.error.noCapability) or \
511            capability in conf.supybot.capabilities.private():
512            v = self._getConfig(conf.supybot.replies.genericNoCapability)
513        else:
514            v = self._getConfig(conf.supybot.replies.noCapability)
515            try:
516                v %= capability
517            except TypeError: # No %s in string
518                pass
519        s = self.__makeReply(v, s)
520        if s:
521            return self._error(s, **kwargs)
522        elif kwargs['Raise']:
523            raise Error()
524
525    def errorPossibleBug(self, s='', **kwargs):
526        v = self._getConfig(conf.supybot.replies.possibleBug)
527        if s:
528            s += '  (%s)' % v
529        else:
530            s = v
531        return self._error(s, **kwargs)
532
533    def errorNotRegistered(self, s='', **kwargs):
534        v = self._getConfig(conf.supybot.replies.notRegistered)
535        return self._error(self.__makeReply(v, s), **kwargs)
536
537    def errorNoUser(self, s='', name='that user', **kwargs):
538        if 'Raise' not in kwargs:
539            kwargs['Raise'] = True
540        v = self._getConfig(conf.supybot.replies.noUser)
541        try:
542            v = v % name
543        except TypeError:
544            log.warning('supybot.replies.noUser should have one "%s" in it.')
545        return self._error(self.__makeReply(v, s), **kwargs)
546
547    def errorRequiresPrivacy(self, s='', **kwargs):
548        v = self._getConfig(conf.supybot.replies.requiresPrivacy)
549        return self._error(self.__makeReply(v, s), **kwargs)
550
551    def errorInvalid(self, what, given=None, s='', repr=True, **kwargs):
552        if given is not None:
553            if repr:
554                given = _repr(given)
555            else:
556                given = '"%s"' % given
557            v = _('%s is not a valid %s.') % (given, what)
558        else:
559            v = _('That\'s not a valid %s.') % what
560        if 'Raise' not in kwargs:
561            kwargs['Raise'] = True
562        if s:
563            v += ' ' + s
564        return self._error(v, **kwargs)
565
566_repr = repr
567
568class ReplyIrcProxy(RichReplyMethods):
569    """This class is a thin wrapper around an irclib.Irc object that gives it
570    the reply() and error() methods (as well as everything in RichReplyMethods,
571    based on those two)."""
572    def __init__(self, irc, msg):
573        self.irc = irc
574        self.msg = msg
575
576    def getRealIrc(self):
577        """Returns the real irclib.Irc object underlying this proxy chain."""
578        if isinstance(self.irc, irclib.Irc):
579            return self.irc
580        else:
581            return self.irc.getRealIrc()
582
583    # This should make us be considered equal to our irclib.Irc object for
584    # hashing; an important thing (no more "too many open files" exceptions :))
585    def __hash__(self):
586        return hash(self.getRealIrc())
587    def __eq__(self, other):
588        return self.getRealIrc() == other
589    __req__ = __eq__
590    def __ne__(self, other):
591        return not (self == other)
592    __rne__ = __ne__
593
594    def error(self, s, msg=None, **kwargs):
595        if 'Raise' in kwargs and kwargs['Raise']:
596            if s:
597                raise Error(s)
598            else:
599                raise ArgumentError
600        if msg is None:
601            msg = self.msg
602        m = error(msg, s, **kwargs)
603        self.irc.queueMsg(m)
604        return m
605
606    def reply(self, s, msg=None, **kwargs):
607        if msg is None:
608            msg = self.msg
609        assert not isinstance(s, ircmsgs.IrcMsg), \
610               'Old code alert: there is no longer a "msg" argument to reply.'
611        kwargs.pop('noLengthCheck', None)
612        m = reply(msg, s, **kwargs)
613        self.irc.queueMsg(m)
614        return m
615
616    def __getattr__(self, attr):
617        return getattr(self.irc, attr)
618
619SimpleProxy = ReplyIrcProxy # Backwards-compatibility
620
621class NestedCommandsIrcProxy(ReplyIrcProxy):
622    "A proxy object to allow proper nesting of commands (even threaded ones)."
623    _mores = ircutils.IrcDict()
624    def __init__(self, irc, msg, args, nested=0):
625        assert isinstance(args, list), 'Args should be a list, not a string.'
626        self.irc = irc
627        self.msg = msg
628        self.nested = nested
629        self.repliedTo = False
630        if not self.nested and isinstance(irc, self.__class__):
631            # This means we were given an NestedCommandsIrcProxy instead of an
632            # irclib.Irc, and so we're obviously nested.  But nested wasn't
633            # set!  So we take our given Irc's nested value.
634            self.nested += irc.nested
635        maxNesting = conf.supybot.commands.nested.maximum()
636        if maxNesting and self.nested > maxNesting:
637            log.warning('%s attempted more than %s levels of nesting.',
638                        self.msg.prefix, maxNesting)
639            self.error(_('You\'ve attempted more nesting than is '
640                              'currently allowed on this bot.'))
641            return
642        # The deepcopy here is necessary for Scheduler; it re-runs already
643        # tokenized commands.  There's a possibility a simple copy[:] would
644        # work, but we're being careful.
645        self.args = copy.deepcopy(args)
646        self.counter = 0
647        self._resetReplyAttributes()
648        if not args:
649            self.finalEvaled = True
650            self._callInvalidCommands()
651        else:
652            self.finalEvaled = False
653            world.commandsProcessed += 1
654            self.evalArgs()
655
656    def __eq__(self, other):
657        return other == self.getRealIrc()
658
659    def __hash__(self):
660        return hash(self.getRealIrc())
661
662    def _resetReplyAttributes(self):
663        self.to = None
664        self.action = None
665        self.notice = None
666        self.private = None
667        self.noLengthCheck = None
668        if self.irc.isChannel(self.msg.args[0]):
669            self.prefixNick = conf.get(conf.supybot.reply.withNickPrefix,
670                                       self.msg.args[0])
671        else:
672            self.prefixNick = conf.supybot.reply.withNickPrefix()
673
674    def evalArgs(self, withClass=None):
675        while self.counter < len(self.args):
676            self.repliedTo = False
677            if isinstance(self.args[self.counter], minisix.string_types):
678                # If it's a string, just go to the next arg.  There is no
679                # evaluation to be done for strings.  If, at some point,
680                # we decided to, say, convert every string using
681                # ircutils.standardSubstitute, this would be where we would
682                # probably put it.
683                self.counter += 1
684            else:
685                assert isinstance(self.args[self.counter], list)
686                # It's a list.  So we spawn another NestedCommandsIrcProxy
687                # to evaluate its args.  When that class has finished
688                # evaluating its args, it will call our reply method, which
689                # will subsequently call this function again, and we'll
690                # pick up where we left off via self.counter.
691                cls = withClass or self.__class__
692                cls(self, self.msg, self.args[self.counter],
693                        nested=self.nested+1)
694                # We have to return here because the new NestedCommandsIrcProxy
695                # might not have called our reply method instantly, since
696                # its command might be threaded.  So (obviously) we can't
697                # just fall through to self.finalEval.
698                return
699        # Once all the list args are evaluated, we then evaluate our own
700        # list of args, since we're assured that they're all strings now.
701        assert all(lambda x: isinstance(x, minisix.string_types), self.args)
702        self.finalEval()
703
704    def _callInvalidCommands(self):
705        log.debug('Calling invalidCommands.')
706        threaded = False
707        cbs = []
708        for cb in self.irc.callbacks:
709            if hasattr(cb, 'invalidCommand'):
710                cbs.append(cb)
711                threaded = threaded or cb.threaded
712        def callInvalidCommands():
713            self.repliedTo = False
714            for cb in cbs:
715                log.debug('Calling %s.invalidCommand.', cb.name())
716                try:
717                    cb.invalidCommand(self, self.msg, self.args)
718                except Error as e:
719                    self.error(str(e))
720                except Exception as e:
721                    log.exception('Uncaught exception in %s.invalidCommand.',
722                                  cb.name())
723                log.debug('Finished calling %s.invalidCommand.', cb.name())
724                if self.repliedTo:
725                    log.debug('Done calling invalidCommands: %s.',cb.name())
726                    return
727        if threaded:
728            name = 'Thread #%s (for invalidCommands)' % world.threadsSpawned
729            t = world.SupyThread(target=callInvalidCommands, name=name)
730            t.setDaemon(True)
731            t.start()
732        else:
733            callInvalidCommands()
734
735    def findCallbacksForArgs(self, args):
736        """Returns a two-tuple of (command, plugins) that has the command
737        (a list of strings) and the plugins for which it was a command."""
738        assert isinstance(args, list)
739        args = list(map(canonicalName, args))
740        cbs = []
741        maxL = []
742        for cb in self.irc.callbacks:
743            if not hasattr(cb, 'getCommand'):
744                continue
745            L = cb.getCommand(args)
746            #log.debug('%s.getCommand(%r) returned %r', cb.name(), args, L)
747            if L and L >= maxL:
748                maxL = L
749                cbs.append((cb, L))
750                assert isinstance(L, list), \
751                       'getCommand now returns a list, not a method.'
752                assert utils.iter.startswith(L, args), \
753                       'getCommand must return a prefix of the args given.  ' \
754                       '(args given: %r, returned: %r)' % (args, L)
755        log.debug('findCallbacksForArgs: %r', cbs)
756        cbs = [cb for (cb, L) in cbs if L == maxL]
757        if len(maxL) == 1:
758            # Special case: one arg determines the callback.  In this case, we
759            # have to check, in order:
760            # 1. Whether the arg is the same as the name of a callback.  This
761            #    callback would then win.
762            for cb in cbs:
763                if cb.canonicalName() == maxL[0]:
764                    return (maxL, [cb])
765
766            # 2. Whether a defaultplugin is defined.
767            defaultPlugins = conf.supybot.commands.defaultPlugins
768            try:
769                defaultPlugin = defaultPlugins.get(maxL[0])()
770                log.debug('defaultPlugin: %r', defaultPlugin)
771                if defaultPlugin:
772                    cb = self.irc.getCallback(defaultPlugin)
773                    if cb in cbs:
774                        # This is just a sanity check, but there's a small
775                        # possibility that a default plugin for a command
776                        # is configured to point to a plugin that doesn't
777                        # actually have that command.
778                        return (maxL, [cb])
779            except registry.NonExistentRegistryEntry:
780                pass
781
782            # 3. Whether an importantPlugin is one of the responses.
783            important = defaultPlugins.importantPlugins()
784            important = list(map(canonicalName, important))
785            importants = []
786            for cb in cbs:
787                if cb.canonicalName() in important:
788                    importants.append(cb)
789            if len(importants) == 1:
790                return (maxL, importants)
791        return (maxL, cbs)
792
793    def finalEval(self):
794        # Now that we've already iterated through our args and made sure
795        # that any list of args was evaluated (by spawning another
796        # NestedCommandsIrcProxy to evaluated it into a string), we can finally
797        # evaluated our own list of arguments.
798        assert not self.finalEvaled, 'finalEval called twice.'
799        self.finalEvaled = True
800        # Now, the way we call a command is we iterate over the loaded pluings,
801        # asking each one if the list of args we have interests it.  The
802        # way we do that is by calling getCommand on the plugin.
803        # The plugin will return a list of args which it considers to be
804        # "interesting."  We will then give our args to the plugin which
805        # has the *longest* list.  The reason we pick the longest list is
806        # that it seems reasonable that the longest the list, the more
807        # specific the command is.  That is, given a list of length X, a list
808        # of length X+1 would be even more specific (assuming that both lists
809        # used the same prefix. Of course, if two plugins return a list of the
810        # same length, we'll just error out with a message about ambiguity.
811        (command, cbs) = self.findCallbacksForArgs(self.args)
812        if not cbs:
813            # We used to handle addressedRegexps here, but I think we'll let
814            # them handle themselves in getCommand.  They can always just
815            # return the full list of args as their "command".
816            self._callInvalidCommands()
817        elif len(cbs) > 1:
818            names = sorted([cb.name() for cb in cbs])
819            command = formatCommand(command)
820            self.error(format(_('The command %q is available in the %L '
821                              'plugins.  Please specify the plugin '
822                              'whose command you wish to call by using '
823                              'its name as a command before %q.'),
824                              command, names, command))
825        else:
826            cb = cbs[0]
827            args = self.args[len(command):]
828            if world.isMainThread() and \
829               (cb.threaded or conf.supybot.debug.threadAllCommands()):
830                t = CommandThread(target=cb._callCommand,
831                                  args=(command, self, self.msg, args))
832                t.start()
833            else:
834                cb._callCommand(command, self, self.msg, args)
835
836    def reply(self, s, noLengthCheck=False, prefixNick=None, action=None,
837              private=None, notice=None, to=None, msg=None,
838              sendImmediately=False, stripCtcp=True):
839        """
840        Keyword arguments:
841
842        * `noLengthCheck=False`:   True if the length shouldn't be checked
843                                   (used for 'more' handling)
844        * `prefixNick=True`:       False if the nick shouldn't be prefixed to the
845                                   reply.
846        * `action=False`:          True if the reply should be an action.
847        * `private=False`:         True if the reply should be in private.
848        * `notice=False`:          True if the reply should be noticed when the
849                                   bot is configured to do so.
850        * `to=<nick|channel>`:     The nick or channel the reply should go to.
851                                   Defaults to msg.args[0] (or msg.nick if private)
852        * `sendImmediately=False`: True if the reply should use sendMsg() which
853                                   bypasses conf.supybot.protocols.irc.throttleTime
854                                   and gets sent before any queued messages
855        """
856        # These use and or or based on whether or not they default to True or
857        # False.  Those that default to True use and; those that default to
858        # False use or.
859        assert not isinstance(s, ircmsgs.IrcMsg), \
860               'Old code alert: there is no longer a "msg" argument to reply.'
861        self.repliedTo = True
862        if sendImmediately:
863            sendMsg = self.irc.sendMsg
864        else:
865            sendMsg = self.irc.queueMsg
866        if msg is None:
867            msg = self.msg
868        if prefixNick is not None:
869            self.prefixNick = prefixNick
870        if action is not None:
871            self.action = self.action or action
872            if action:
873                self.prefixNick = False
874        if notice is not None:
875            self.notice = self.notice or notice
876        if private is not None:
877            self.private = self.private or private
878        target = self._getTarget(to)
879        # action=True implies noLengthCheck=True and prefixNick=False
880        self.noLengthCheck=noLengthCheck or self.noLengthCheck or self.action
881        if not isinstance(s, minisix.string_types): # avoid trying to str() unicode
882            s = str(s) # Allow non-string esses.
883        if self.finalEvaled:
884            try:
885                if isinstance(self.irc, self.__class__):
886                    s = s[:conf.supybot.reply.maximumLength()]
887                    return self.irc.reply(s, to=self.to,
888                                          notice=self.notice,
889                                          action=self.action,
890                                          private=self.private,
891                                          prefixNick=self.prefixNick,
892                                          noLengthCheck=self.noLengthCheck,
893                                          stripCtcp=stripCtcp)
894                elif self.noLengthCheck:
895                    # noLengthCheck only matters to NestedCommandsIrcProxy, so
896                    # it's not used here.  Just in case you were wondering.
897                    m = reply(msg, s, to=self.to,
898                              notice=self.notice,
899                              action=self.action,
900                              private=self.private,
901                              prefixNick=self.prefixNick,
902                              stripCtcp=stripCtcp)
903                    sendMsg(m)
904                    return m
905                else:
906                    s = ircutils.safeArgument(s)
907                    allowedLength = conf.get(conf.supybot.reply.mores.length,
908                                             target)
909                    if not allowedLength: # 0 indicates this.
910                        allowedLength = (512
911                                - len(':') - len(self.irc.prefix)
912                                - len(' PRIVMSG ')
913                                - len(target)
914                                - len(' :')
915                                - len('\r\n')
916                                )
917                        if self.prefixNick:
918                            allowedLength -= len(msg.nick) + len(': ')
919                    maximumMores = conf.get(conf.supybot.reply.mores.maximum,
920                                            target)
921                    maximumLength = allowedLength * maximumMores
922                    if len(s) > maximumLength:
923                        log.warning('Truncating to %s bytes from %s bytes.',
924                                    maximumLength, len(s))
925                        s = s[:maximumLength]
926                    s_size = len(s.encode()) if minisix.PY3 else len(s)
927                    if s_size <= allowedLength or \
928                       not conf.get(conf.supybot.reply.mores, target):
929                        # There's no need for action=self.action here because
930                        # action implies noLengthCheck, which has already been
931                        # handled.  Let's stick an assert in here just in case.
932                        assert not self.action
933                        m = reply(msg, s, to=self.to,
934                                  notice=self.notice,
935                                  private=self.private,
936                                  prefixNick=self.prefixNick,
937                                  stripCtcp=stripCtcp)
938                        sendMsg(m)
939                        return m
940                    # The '(XX more messages)' may have not the same
941                    # length in the current locale
942                    allowedLength -= len(_('(XX more messages)')) + 1 # bold
943                    msgs = ircutils.wrap(s, allowedLength)
944                    msgs.reverse()
945                    instant = conf.get(conf.supybot.reply.mores.instant,target)
946                    while instant > 1 and msgs:
947                        instant -= 1
948                        response = msgs.pop()
949                        m = reply(msg, response, to=self.to,
950                                  notice=self.notice,
951                                  private=self.private,
952                                  prefixNick=self.prefixNick,
953                                  stripCtcp=stripCtcp)
954                        sendMsg(m)
955                        # XXX We should somehow allow these to be returned, but
956                        #     until someone complains, we'll be fine :)  We
957                        #     can't return from here, though, for obvious
958                        #     reasons.
959                        # return m
960                    if not msgs:
961                        return
962                    response = msgs.pop()
963                    if msgs:
964                        if len(msgs) == 1:
965                            more = _('more message')
966                        else:
967                            more = _('more messages')
968                        n = ircutils.bold('(%i %s)' % (len(msgs), more))
969                        response = '%s %s' % (response, n)
970                    prefix = msg.prefix
971                    if self.to and ircutils.isNick(self.to):
972                        try:
973                            state = self.getRealIrc().state
974                            prefix = state.nickToHostmask(self.to)
975                        except KeyError:
976                            pass # We'll leave it as it is.
977                    mask = prefix.split('!', 1)[1]
978                    self._mores[mask] = msgs
979                    public = self.irc.isChannel(msg.args[0])
980                    private = self.private or not public
981                    self._mores[msg.nick] = (private, msgs)
982                    m = reply(msg, response, to=self.to,
983                                            action=self.action,
984                                            notice=self.notice,
985                                            private=self.private,
986                                            prefixNick=self.prefixNick,
987                                            stripCtcp=stripCtcp)
988                    sendMsg(m)
989                    return m
990            finally:
991                self._resetReplyAttributes()
992        else:
993            if msg.ignored:
994                # Since the final reply string is constructed via
995                # ' '.join(self.args), the args index for ignored commands
996                # needs to be popped to avoid extra spaces in the final reply.
997                self.args.pop(self.counter)
998                msg.tag('ignored', False)
999            else:
1000                self.args[self.counter] = s
1001            self.evalArgs()
1002
1003    def noReply(self, msg=None):
1004        if msg is None:
1005            msg = self.msg
1006        super(NestedCommandsIrcProxy, self).noReply(msg=msg)
1007        if self.finalEvaled:
1008            if isinstance(self.irc, NestedCommandsIrcProxy):
1009                self.irc.noReply(msg=msg)
1010            else:
1011                msg.tag('ignored', True)
1012        else:
1013            self.args.pop(self.counter)
1014            msg.tag('ignored', False)
1015            self.evalArgs()
1016
1017    def replies(self, L, prefixer=None, joiner=None,
1018                onlyPrefixFirst=False, to=None,
1019                oneToOne=None, **kwargs):
1020        if not self.finalEvaled and oneToOne is None:
1021            oneToOne = True
1022        return super(NestedCommandsIrcProxy, self).replies(L,
1023                prefixer=prefixer, joiner=joiner,
1024                onlyPrefixFirst=onlyPrefixFirst, to=to,
1025                oneToOne=oneToOne, **kwargs)
1026
1027    def error(self, s='', Raise=False, **kwargs):
1028        self.repliedTo = True
1029        if Raise:
1030            if s:
1031                raise Error(s)
1032            else:
1033                raise ArgumentError
1034        if s:
1035            if not isinstance(self.irc, irclib.Irc):
1036                return self.irc.error(s, **kwargs)
1037            else:
1038                m = error(self.msg, s, **kwargs)
1039                self.irc.queueMsg(m)
1040                return m
1041        else:
1042            raise ArgumentError
1043
1044    def __getattr__(self, attr):
1045        return getattr(self.irc, attr)
1046
1047IrcObjectProxy = NestedCommandsIrcProxy
1048
1049class CommandThread(world.SupyThread):
1050    """Just does some extra logging and error-recovery for commands that need
1051    to run in threads.
1052    """
1053    def __init__(self, target=None, args=(), kwargs={}):
1054        self.command = args[0]
1055        self.cb = target.__self__
1056        threadName = 'Thread #%s (for %s.%s)' % (world.threadsSpawned,
1057                                                 self.cb.name(),
1058                                                 self.command)
1059        log.debug('Spawning thread %s (args: %r)', threadName, args)
1060        self.__parent = super(CommandThread, self)
1061        self.__parent.__init__(target=target, name=threadName,
1062                               args=args, kwargs=kwargs)
1063        self.setDaemon(True)
1064        self.originalThreaded = self.cb.threaded
1065        self.cb.threaded = True
1066
1067    def run(self):
1068        try:
1069            self.__parent.run()
1070        finally:
1071            self.cb.threaded = self.originalThreaded
1072
1073class CommandProcess(world.SupyProcess):
1074    """Just does some extra logging and error-recovery for commands that need
1075    to run in processes.
1076    """
1077    def __init__(self, target=None, args=(), kwargs={}):
1078        pn = kwargs.pop('pn', 'Unknown')
1079        cn = kwargs.pop('cn', 'unknown')
1080        procName = 'Process #%s (for %s.%s)' % (world.processesSpawned,
1081                                                 pn,
1082                                                 cn)
1083        log.debug('Spawning process %s (args: %r)', procName, args)
1084        self.__parent = super(CommandProcess, self)
1085        self.__parent.__init__(target=target, name=procName,
1086                               args=args, kwargs=kwargs)
1087
1088    def run(self):
1089        self.__parent.run()
1090
1091class CanonicalString(registry.NormalizedString):
1092    def normalize(self, s):
1093        return canonicalName(s)
1094
1095class CanonicalNameSet(utils.NormalizingSet):
1096    def normalize(self, s):
1097        return canonicalName(s)
1098
1099class CanonicalNameDict(utils.InsensitivePreservingDict):
1100    def key(self, s):
1101        return canonicalName(s)
1102
1103class Disabled(registry.SpaceSeparatedListOf):
1104    sorted = True
1105    Value = CanonicalString
1106    List = CanonicalNameSet
1107
1108conf.registerGlobalValue(conf.supybot.commands, 'disabled',
1109    Disabled([], _("""Determines what commands are currently disabled.  Such
1110    commands will not appear in command lists, etc.  They will appear not even
1111    to exist.""")))
1112
1113class DisabledCommands(object):
1114    def __init__(self):
1115        self.d = CanonicalNameDict()
1116        for name in conf.supybot.commands.disabled():
1117            if '.' in name:
1118                (plugin, command) = name.split('.', 1)
1119                if command in self.d:
1120                    if self.d[command] is not None:
1121                        self.d[command].add(plugin)
1122                else:
1123                    self.d[command] = CanonicalNameSet([plugin])
1124            else:
1125                self.d[name] = None
1126
1127    def disabled(self, command, plugin=None):
1128        if command in self.d:
1129            if self.d[command] is None:
1130                return True
1131            elif plugin in self.d[command]:
1132                return True
1133        return False
1134
1135    def add(self, command, plugin=None):
1136        if plugin is None:
1137            self.d[command] = None
1138        else:
1139            if command in self.d:
1140                if self.d[command] is not None:
1141                    self.d[command].add(plugin)
1142            else:
1143                self.d[command] = CanonicalNameSet([plugin])
1144
1145    def remove(self, command, plugin=None):
1146        if plugin is None:
1147            del self.d[command]
1148        else:
1149            if self.d[command] is not None:
1150                self.d[command].remove(plugin)
1151
1152class BasePlugin(object):
1153    def __init__(self, *args, **kwargs):
1154        self.cbs = []
1155        for attr in dir(self):
1156            if attr != canonicalName(attr):
1157                continue
1158            obj = getattr(self, attr)
1159            if isinstance(obj, type) and issubclass(obj, BasePlugin):
1160                cb = obj(*args, **kwargs)
1161                setattr(self, attr, cb)
1162                self.cbs.append(cb)
1163                cb.log = log.getPluginLogger('%s.%s' % (self.name(),cb.name()))
1164        super(BasePlugin, self).__init__()
1165
1166class MetaSynchronizedAndFirewalled(log.MetaFirewall, utils.python.MetaSynchronized):
1167    pass
1168SynchronizedAndFirewalled = MetaSynchronizedAndFirewalled(
1169        'SynchronizedAndFirewalled', (), {})
1170
1171class Commands(BasePlugin, SynchronizedAndFirewalled):
1172    __synchronized__ = (
1173        '__call__',
1174        'callCommand',
1175        'invalidCommand',
1176        )
1177    # For a while, a comment stood here to say, "Eventually callCommand."  But
1178    # that's wrong, because we can't do generic error handling in this
1179    # callCommand -- plugins need to be able to override callCommand and do
1180    # error handling there (see the Web plugin for an example).
1181    __firewalled__ = {'isCommand': None,
1182                      '_callCommand': None}
1183    commandArgs = ['self', 'irc', 'msg', 'args']
1184    # These must be class-scope, so all plugins use the same one.
1185    _disabled = DisabledCommands()
1186    pre_command_callbacks = []
1187    def name(self):
1188        return self.__class__.__name__
1189
1190    def canonicalName(self):
1191        return canonicalName(self.name())
1192
1193    def isDisabled(self, command):
1194        return self._disabled.disabled(command, self.name())
1195
1196    def isCommandMethod(self, name):
1197        """Returns whether a given method name is a command in this plugin."""
1198        # This function is ugly, but I don't want users to call methods like
1199        # doPrivmsg or __init__ or whatever, and this is good to stop them.
1200
1201        # Don't normalize this name: consider outFilter(self, irc, msg).
1202        # name = canonicalName(name)
1203        if self.isDisabled(name):
1204            return False
1205        if name != canonicalName(name):
1206            return False
1207        if hasattr(self, name):
1208            method = getattr(self, name)
1209            if inspect.ismethod(method):
1210                code = method.__func__.__code__
1211                return inspect.getargs(code)[0] == self.commandArgs
1212            else:
1213                return False
1214        else:
1215            return False
1216
1217    def isCommand(self, command):
1218        """Convenience, backwards-compatibility, semi-deprecated."""
1219        if isinstance(command, minisix.string_types):
1220            return self.isCommandMethod(command)
1221        else:
1222            # Since we're doing a little type dispatching here, let's not be
1223            # too liberal.
1224            assert isinstance(command, list)
1225            return self.getCommand(command) == command
1226
1227    def getCommand(self, args, stripOwnName=True):
1228        assert args == list(map(canonicalName, args))
1229        first = args[0]
1230        for cb in self.cbs:
1231            if first == cb.canonicalName():
1232                return cb.getCommand(args)
1233        if first == self.canonicalName() and len(args) > 1 and \
1234                stripOwnName:
1235            ret = self.getCommand(args[1:], stripOwnName=False)
1236            if ret:
1237                return [first] + ret
1238        if self.isCommandMethod(first):
1239            return [first]
1240        return []
1241
1242    def getCommandMethod(self, command):
1243        """Gets the given command from this plugin."""
1244        #print '*** %s.getCommandMethod(%r)' % (self.name(), command)
1245        assert not isinstance(command, minisix.string_types)
1246        assert command == list(map(canonicalName, command))
1247        assert self.getCommand(command) == command
1248        for cb in self.cbs:
1249            if command[0] == cb.canonicalName():
1250                return cb.getCommandMethod(command)
1251        if len(command) > 1:
1252            assert command[0] == self.canonicalName()
1253            return self.getCommandMethod(command[1:])
1254        else:
1255            method = getattr(self, command[0])
1256            if inspect.ismethod(method):
1257                code = method.__func__.__code__
1258                if inspect.getargs(code)[0] == self.commandArgs:
1259                    return method
1260                else:
1261                    raise AttributeError
1262
1263    def listCommands(self, pluginCommands=[]):
1264        commands = set(pluginCommands)
1265        for s in dir(self):
1266            if self.isCommandMethod(s):
1267                commands.add(s)
1268        for cb in self.cbs:
1269            name = cb.canonicalName()
1270            for command in cb.listCommands():
1271                if command == name:
1272                    commands.add(command)
1273                else:
1274                    commands.add(' '.join([name, command]))
1275        L = list(commands)
1276        L.sort()
1277        return L
1278
1279    def callCommand(self, command, irc, msg, *args, **kwargs):
1280        # We run all callbacks before checking if one of them returned True
1281        if any(bool, list(cb(self, command, irc, msg, *args, **kwargs)
1282                    for cb in self.pre_command_callbacks)):
1283            return
1284        method = self.getCommandMethod(command)
1285        method(irc, msg, *args, **kwargs)
1286
1287    def _callCommand(self, command, irc, msg, *args, **kwargs):
1288        if irc.nick == msg.args[0]:
1289            self.log.info('%s called in private by %q.', formatCommand(command),
1290                    msg.prefix)
1291        else:
1292            self.log.info('%s called on %s by %q.', formatCommand(command),
1293                    msg.args[0], msg.prefix)
1294        try:
1295            if len(command) == 1 or command[0] != self.canonicalName():
1296                fullCommandName = [self.canonicalName()] + command
1297            else:
1298                fullCommandName = command
1299            # Let "P" be the plugin and "X Y" the command name. The
1300            # fullCommandName is "P X Y"
1301
1302            # check "Y"
1303            cap = checkCommandCapability(msg, self, command[-1])
1304            if cap:
1305                irc.errorNoCapability(cap)
1306                return
1307
1308            # check "P", "P.X", and "P.X.Y"
1309            prefix = []
1310            for name in fullCommandName:
1311                prefix.append(name)
1312                cap = checkCommandCapability(msg, self, prefix)
1313                if cap:
1314                    irc.errorNoCapability(cap)
1315                    return
1316
1317            try:
1318                self.callingCommand = command
1319                self.callCommand(command, irc, msg, *args, **kwargs)
1320            finally:
1321                self.callingCommand = None
1322        except SilentError:
1323            pass
1324        except (getopt.GetoptError, ArgumentError) as e:
1325            self.log.debug('Got %s, giving argument error.',
1326                           utils.exnToString(e))
1327            help = self.getCommandHelp(command)
1328            if 'command has no help.' in help:
1329                # Note: this case will never happen, unless 'checkDoc' is set
1330                # to False.
1331                irc.error(_('Invalid arguments for %s.') % formatCommand(command))
1332            else:
1333                irc.reply(help)
1334        except (SyntaxError, Error) as e:
1335            self.log.debug('Error return: %s', utils.exnToString(e))
1336            irc.error(str(e))
1337        except Exception as e:
1338            self.log.exception('Uncaught exception in %s.', command)
1339            if conf.supybot.reply.error.detailed():
1340                irc.error(utils.exnToString(e))
1341            else:
1342                irc.replyError(msg=msg)
1343
1344    def getCommandHelp(self, command, simpleSyntax=None):
1345        method = self.getCommandMethod(command)
1346        help = getHelp
1347        chan = None
1348        if dynamic.msg is not None:
1349            chan = dynamic.msg.args[0]
1350        if simpleSyntax is None:
1351            simpleSyntax = conf.get(conf.supybot.reply.showSimpleSyntax, chan)
1352        if simpleSyntax:
1353            help = getSyntax
1354        if hasattr(method, '__doc__'):
1355            return help(method, name=formatCommand(command))
1356        else:
1357            return format(_('The %q command has no help.'),
1358                          formatCommand(command))
1359
1360class PluginMixin(BasePlugin, irclib.IrcCallback):
1361    public = True
1362    alwaysCall = ()
1363    threaded = False
1364    noIgnore = False
1365    classModule = None
1366    Proxy = NestedCommandsIrcProxy
1367    def __init__(self, irc):
1368        myName = self.name()
1369        self.log = log.getPluginLogger(myName)
1370        self.__parent = super(PluginMixin, self)
1371        self.__parent.__init__(irc)
1372        # We can't do this because of the specialness that Owner and Misc do.
1373        # I guess plugin authors will have to get the capitalization right.
1374        # self.callAfter = map(str.lower, self.callAfter)
1375        # self.callBefore = map(str.lower, self.callBefore)
1376
1377    def canonicalName(self):
1378        return canonicalName(self.name())
1379
1380    def __call__(self, irc, msg):
1381        irc = SimpleProxy(irc, msg)
1382        if msg.command == 'PRIVMSG':
1383            if hasattr(self.noIgnore, '__call__'):
1384                noIgnore = self.noIgnore(irc, msg)
1385            else:
1386                noIgnore = self.noIgnore
1387            if noIgnore or \
1388               not ircdb.checkIgnored(msg.prefix, msg.args[0]) or \
1389               not ircutils.isUserHostmask(msg.prefix):  # Some services impl.
1390                self.__parent.__call__(irc, msg)
1391        else:
1392            self.__parent.__call__(irc, msg)
1393
1394    def registryValue(self, name, channel=None, value=True):
1395        plugin = self.name()
1396        group = conf.supybot.plugins.get(plugin)
1397        names = registry.split(name)
1398        for name in names:
1399            group = group.get(name)
1400        if channel is not None:
1401            if ircutils.isChannel(channel):
1402                group = group.get(channel)
1403            else:
1404                self.log.debug('%s: registryValue got channel=%r', plugin,
1405                               channel)
1406        if value:
1407            return group()
1408        else:
1409            return group
1410
1411    def setRegistryValue(self, name, value, channel=None):
1412        plugin = self.name()
1413        group = conf.supybot.plugins.get(plugin)
1414        names = registry.split(name)
1415        for name in names:
1416            group = group.get(name)
1417        if channel is None:
1418            group.setValue(value)
1419        else:
1420            group.get(channel).setValue(value)
1421
1422    def userValue(self, name, prefixOrName, default=None):
1423        try:
1424            id = str(ircdb.users.getUserId(prefixOrName))
1425        except KeyError:
1426            return None
1427        plugin = self.name()
1428        group = conf.users.plugins.get(plugin)
1429        names = registry.split(name)
1430        for name in names:
1431            group = group.get(name)
1432        return group.get(id)()
1433
1434    def setUserValue(self, name, prefixOrName, value,
1435                     ignoreNoUser=True, setValue=True):
1436        try:
1437            id = str(ircdb.users.getUserId(prefixOrName))
1438        except KeyError:
1439            if ignoreNoUser:
1440                return
1441            else:
1442                raise
1443        plugin = self.name()
1444        group = conf.users.plugins.get(plugin)
1445        names = registry.split(name)
1446        for name in names:
1447            group = group.get(name)
1448        group = group.get(id)
1449        if setValue:
1450            group.setValue(value)
1451        else:
1452            group.set(value)
1453
1454    def getPluginHelp(self):
1455        if hasattr(self, '__doc__'):
1456            return self.__doc__
1457        else:
1458            return None
1459
1460class Plugin(PluginMixin, Commands):
1461    pass
1462Privmsg = Plugin # Backwards compatibility.
1463
1464
1465class PluginRegexp(Plugin):
1466    """Same as Plugin, except allows the user to also include regexp-based
1467    callbacks.  All regexp-based callbacks must be specified in the set (or
1468    list) attribute "regexps", "addressedRegexps", or "unaddressedRegexps"
1469    depending on whether they should always be triggered, triggered only when
1470    the bot is addressed, or triggered only when the bot isn't addressed.
1471    """
1472    flags = re.I
1473    regexps = ()
1474    """'regexps' methods are called whether the message is addressed or not."""
1475    addressedRegexps = ()
1476    """'addressedRegexps' methods are called only when the message is addressed,
1477    and then, only with the payload (i.e., what is returned from the
1478    'addressed' function."""
1479    unaddressedRegexps = ()
1480    """'unaddressedRegexps' methods are called only when the message is *not*
1481    addressed."""
1482    Proxy = SimpleProxy
1483    def __init__(self, irc):
1484        self.__parent = super(PluginRegexp, self)
1485        self.__parent.__init__(irc)
1486        self.res = []
1487        self.addressedRes = []
1488        self.unaddressedRes = []
1489        for name in self.regexps:
1490            method = getattr(self, name)
1491            r = re.compile(method.__doc__, self.flags)
1492            self.res.append((r, name))
1493        for name in self.addressedRegexps:
1494            method = getattr(self, name)
1495            r = re.compile(method.__doc__, self.flags)
1496            self.addressedRes.append((r, name))
1497        for name in self.unaddressedRegexps:
1498            method = getattr(self, name)
1499            r = re.compile(method.__doc__, self.flags)
1500            self.unaddressedRes.append((r, name))
1501
1502    def _callRegexp(self, name, irc, msg, m):
1503        method = getattr(self, name)
1504        try:
1505            method(irc, msg, m)
1506        except Error as e:
1507            irc.error(str(e))
1508        except Exception as e:
1509            self.log.exception('Uncaught exception in _callRegexp:')
1510
1511    def invalidCommand(self, irc, msg, tokens):
1512        s = ' '.join(tokens)
1513        for (r, name) in self.addressedRes:
1514            for m in r.finditer(s):
1515                self._callRegexp(name, irc, msg, m)
1516
1517    def doPrivmsg(self, irc, msg):
1518        if msg.isError:
1519            return
1520        proxy = self.Proxy(irc, msg)
1521        if not msg.addressed:
1522            for (r, name) in self.unaddressedRes:
1523                for m in r.finditer(msg.args[1]):
1524                    self._callRegexp(name, proxy, msg, m)
1525        for (r, name) in self.res:
1526            for m in r.finditer(msg.args[1]):
1527                self._callRegexp(name, proxy, msg, m)
1528PrivmsgCommandAndRegexp = PluginRegexp
1529
1530
1531# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
1532