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