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