1### 2# Copyright (c) 2002-2005, Jeremiah Fincher 3# Copyright (c) 2009,2011,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""" 32Provides a great number of useful utility functions for IRC. Things to muck 33around with hostmasks, set bold or color on strings, IRC-case-insensitive 34dicts, a nick class to handle nicks (so comparisons and hashing and whatnot 35work in an IRC-case-insensitive fashion), and numerous other things. 36""" 37 38from __future__ import division 39from __future__ import print_function 40 41import re 42import sys 43import time 44import base64 45import random 46import string 47import textwrap 48import functools 49 50from . import utils 51from .utils import minisix 52from .version import version 53 54from .i18n import PluginInternationalization 55_ = PluginInternationalization() 56 57def debug(s, *args): 58 """Prints a debug string. Most likely replaced by our logging debug.""" 59 print('***', s % args) 60 61userHostmaskRe = re.compile(r'^\S+!\S+@\S+$') 62def isUserHostmask(s): 63 """Returns whether or not the string s is a valid User hostmask.""" 64 return userHostmaskRe.match(s) is not None 65 66def isServerHostmask(s): 67 """s => bool 68 Returns True if s is a valid server hostmask.""" 69 return not isUserHostmask(s) 70 71def nickFromHostmask(hostmask): 72 """hostmask => nick 73 Returns the nick from a user hostmask.""" 74 assert isUserHostmask(hostmask) 75 return splitHostmask(hostmask)[0] 76 77def userFromHostmask(hostmask): 78 """hostmask => user 79 Returns the user from a user hostmask.""" 80 assert isUserHostmask(hostmask) 81 return splitHostmask(hostmask)[1] 82 83def hostFromHostmask(hostmask): 84 """hostmask => host 85 Returns the host from a user hostmask.""" 86 assert isUserHostmask(hostmask) 87 return splitHostmask(hostmask)[2] 88 89def splitHostmask(hostmask): 90 """hostmask => (nick, user, host) 91 Returns the nick, user, host of a user hostmask.""" 92 assert isUserHostmask(hostmask) 93 nick, rest = hostmask.rsplit('!', 1) 94 user, host = rest.rsplit('@', 1) 95 return (minisix.intern(nick), minisix.intern(user), minisix.intern(host)) 96 97def joinHostmask(nick, ident, host): 98 """nick, user, host => hostmask 99 Joins the nick, ident, host into a user hostmask.""" 100 assert nick and ident and host 101 return minisix.intern('%s!%s@%s' % (nick, ident, host)) 102 103_rfc1459trans = utils.str.MultipleReplacer(dict(list(zip( 104 string.ascii_uppercase + r'\[]~', 105 string.ascii_lowercase + r'|{}^')))) 106def toLower(s, casemapping=None): 107 """s => s 108 Returns the string s lowered according to IRC case rules.""" 109 if casemapping is None or casemapping == 'rfc1459': 110 return _rfc1459trans(s) 111 elif casemapping == 'ascii': # freenode 112 return s.lower() 113 else: 114 raise ValueError('Invalid casemapping: %r' % casemapping) 115 116def strEqual(nick1, nick2): 117 """s1, s2 => bool 118 Returns True if nick1 == nick2 according to IRC case rules.""" 119 assert isinstance(nick1, minisix.string_types) 120 assert isinstance(nick2, minisix.string_types) 121 return toLower(nick1) == toLower(nick2) 122 123nickEqual = strEqual 124 125_nickchars = r'[]\`_^{|}' 126nickRe = re.compile(r'^[A-Za-z%s][-0-9A-Za-z%s]*$' 127 % (re.escape(_nickchars), re.escape(_nickchars))) 128def isNick(s, strictRfc=True, nicklen=None): 129 """s => bool 130 Returns True if s is a valid IRC nick.""" 131 if strictRfc: 132 ret = bool(nickRe.match(s)) 133 if ret and nicklen is not None: 134 ret = len(s) <= nicklen 135 return ret 136 else: 137 return not isChannel(s) and \ 138 not isUserHostmask(s) and \ 139 not ' ' in s and not '!' in s 140 141def areNicks(s, strictRfc=True, nicklen=None): 142 """Like 'isNick(x)' but for comma-separated list.""" 143 nick = functools.partial(isNick, strictRfc=strictRfc, nicklen=nicklen) 144 return all(map(nick, s.split(','))) 145 146def isChannel(s, chantypes='#&!', channellen=50): 147 """s => bool 148 Returns True if s is a valid IRC channel name.""" 149 return s and \ 150 ',' not in s and \ 151 '\x07' not in s and \ 152 s[0] in chantypes and \ 153 len(s) <= channellen and \ 154 len(s.split(None, 1)) == 1 155 156def areChannels(s, chantypes='#&!', channellen=50): 157 """Like 'isChannel(x)' but for comma-separated list.""" 158 chan = functools.partial(isChannel, chantypes=chantypes, 159 channellen=channellen) 160 return all(map(chan, s.split(','))) 161 162def areReceivers(s, strictRfc=True, nicklen=None, chantypes='#&!', 163 channellen=50): 164 """Like 'isNick(x) or isChannel(x)' but for comma-separated list.""" 165 nick = functools.partial(isNick, strictRfc=strictRfc, nicklen=nicklen) 166 chan = functools.partial(isChannel, chantypes=chantypes, 167 channellen=channellen) 168 return all([nick(x) or chan(x) for x in s.split(',')]) 169 170_patternCache = utils.structures.CacheDict(1000) 171def _hostmaskPatternEqual(pattern, hostmask): 172 try: 173 return _patternCache[pattern](hostmask) is not None 174 except KeyError: 175 # We make our own regexps, rather than use fnmatch, because fnmatch's 176 # case-insensitivity is not IRC's case-insensitity. 177 fd = minisix.io.StringIO() 178 for c in pattern: 179 if c == '*': 180 fd.write('.*') 181 elif c == '?': 182 fd.write('.') 183 elif c in '[{': 184 fd.write(r'[\[{]') 185 elif c in '}]': 186 fd.write(r'[}\]]') 187 elif c in '|\\': 188 fd.write(r'[|\\]') 189 elif c in '^~': 190 fd.write('[~^]') 191 else: 192 fd.write(re.escape(c)) 193 fd.write('$') 194 f = re.compile(fd.getvalue(), re.I).match 195 _patternCache[pattern] = f 196 return f(hostmask) is not None 197 198_hostmaskPatternEqualCache = utils.structures.CacheDict(1000) 199def hostmaskPatternEqual(pattern, hostmask): 200 """pattern, hostmask => bool 201 Returns True if hostmask matches the hostmask pattern pattern.""" 202 try: 203 return _hostmaskPatternEqualCache[(pattern, hostmask)] 204 except KeyError: 205 b = _hostmaskPatternEqual(pattern, hostmask) 206 _hostmaskPatternEqualCache[(pattern, hostmask)] = b 207 return b 208 209def banmask(hostmask): 210 """Returns a properly generic banning hostmask for a hostmask. 211 212 >>> banmask('nick!user@host.domain.tld') 213 '*!*@*.domain.tld' 214 215 >>> banmask('nick!user@10.0.0.1') 216 '*!*@10.0.0.*' 217 """ 218 assert isUserHostmask(hostmask) 219 host = hostFromHostmask(hostmask) 220 if utils.net.isIPV4(host): 221 L = host.split('.') 222 L[-1] = '*' 223 return '*!*@' + '.'.join(L) 224 elif utils.net.isIPV6(host): 225 L = host.split(':') 226 L[-1] = '*' 227 return '*!*@' + ':'.join(L) 228 else: 229 if len(host.split('.')) > 2: # If it is a subdomain 230 return '*!*@*%s' % host[host.find('.'):] 231 else: 232 return '*!*@' + host 233 234_plusRequireArguments = 'ovhblkqeI' 235_minusRequireArguments = 'ovhbkqeI' 236def separateModes(args): 237 """Separates modelines into single mode change tuples. Basically, you 238 should give it the .args of a MODE IrcMsg. 239 240 Examples: 241 242 >>> separateModes(['+ooo', 'jemfinch', 'StoneTable', 'philmes']) 243 [('+o', 'jemfinch'), ('+o', 'StoneTable'), ('+o', 'philmes')] 244 245 >>> separateModes(['+o-o', 'jemfinch', 'PeterB']) 246 [('+o', 'jemfinch'), ('-o', 'PeterB')] 247 248 >>> separateModes(['+s-o', 'test']) 249 [('+s', None), ('-o', 'test')] 250 251 >>> separateModes(['+sntl', '100']) 252 [('+s', None), ('+n', None), ('+t', None), ('+l', 100)] 253 """ 254 if not args: 255 return [] 256 modes = args[0] 257 args = list(args[1:]) 258 ret = [] 259 last = '+' 260 for c in modes: 261 if c in '+-': 262 last = c 263 else: 264 if last == '+': 265 requireArguments = _plusRequireArguments 266 else: 267 requireArguments = _minusRequireArguments 268 if c in requireArguments: 269 if not args: 270 # It happens, for example with "MODE #channel +b", which 271 # is used for getting the list of all bans. 272 continue 273 arg = args.pop(0) 274 try: 275 arg = int(arg) 276 except ValueError: 277 pass 278 ret.append((last + c, arg)) 279 else: 280 ret.append((last + c, None)) 281 return ret 282 283def joinModes(modes): 284 """[(mode, targetOrNone), ...] => args 285 Joins modes of the same form as returned by separateModes.""" 286 args = [] 287 modeChars = [] 288 currentMode = '\x00' 289 for (mode, arg) in modes: 290 if arg is not None: 291 args.append(arg) 292 if not mode.startswith(currentMode): 293 currentMode = mode[0] 294 modeChars.append(mode[0]) 295 modeChars.append(mode[1]) 296 args.insert(0, ''.join(modeChars)) 297 return args 298 299def bold(s): 300 """Returns the string s, bolded.""" 301 return '\x02%s\x02' % s 302 303def italic(s): 304 """Returns the string s, italicised.""" 305 return '\x1D%s\x1D' % s 306 307def reverse(s): 308 """Returns the string s, reverse-videoed.""" 309 return '\x16%s\x16' % s 310 311def underline(s): 312 """Returns the string s, underlined.""" 313 return '\x1F%s\x1F' % s 314 315# Definition of mircColors dictionary moved below because it became an IrcDict. 316def mircColor(s, fg=None, bg=None): 317 """Returns s with the appropriate mIRC color codes applied.""" 318 if fg is None and bg is None: 319 return s 320 elif bg is None: 321 if str(fg) in mircColors: 322 fg = mircColors[str(fg)] 323 elif len(str(fg)) > 1: 324 fg = mircColors[str(fg)[:-1]] 325 else: 326 # Should not happen 327 pass 328 return '\x03%s%s\x03' % (fg.zfill(2), s) 329 elif fg is None: 330 bg = mircColors[str(bg)] 331 # According to the mirc color doc, a fg color MUST be specified if a 332 # background color is specified. So, we'll specify 00 (white) if the 333 # user doesn't specify one. 334 return '\x0300,%s%s\x03' % (bg.zfill(2), s) 335 else: 336 fg = mircColors[str(fg)] 337 bg = mircColors[str(bg)] 338 # No need to zfill fg because the comma delimits. 339 return '\x03%s,%s%s\x03' % (fg, bg.zfill(2), s) 340 341def canonicalColor(s, bg=False, shift=0): 342 """Assigns an (fg, bg) canonical color pair to a string based on its hash 343 value. This means it might change between Python versions. This pair can 344 be used as a *parameter to mircColor. The shift parameter is how much to 345 right-shift the hash value initially. 346 """ 347 h = hash(s) >> shift 348 fg = h % 14 + 2 # The + 2 is to rule out black and white. 349 if bg: 350 bg = (h >> 4) & 3 # The 5th, 6th, and 7th least significant bits. 351 if fg < 8: 352 bg += 8 353 else: 354 bg += 2 355 return (fg, bg) 356 else: 357 return (fg, None) 358 359def stripBold(s): 360 """Returns the string s, with bold removed.""" 361 return s.replace('\x02', '') 362 363def stripItalic(s): 364 """Returns the string s, with italics removed.""" 365 return s.replace('\x1d', '') 366 367_stripColorRe = re.compile(r'\x03(?:\d{1,2},\d{1,2}|\d{1,2}|,\d{1,2}|)') 368def stripColor(s): 369 """Returns the string s, with color removed.""" 370 return _stripColorRe.sub('', s) 371 372def stripReverse(s): 373 """Returns the string s, with reverse-video removed.""" 374 return s.replace('\x16', '') 375 376def stripUnderline(s): 377 """Returns the string s, with underlining removed.""" 378 return s.replace('\x1f', '') 379 380def stripFormatting(s): 381 """Returns the string s, with all formatting removed.""" 382 # stripColor has to go first because of some strings, check the tests. 383 s = stripColor(s) 384 s = stripBold(s) 385 s = stripReverse(s) 386 s = stripUnderline(s) 387 s = stripItalic(s) 388 return s.replace('\x0f', '') 389 390_containsFormattingRe = re.compile(r'[\x02\x03\x16\x1f]') 391def formatWhois(irc, replies, caller='', channel='', command='whois'): 392 """Returns a string describing the target of a WHOIS command. 393 394 Arguments are: 395 * irc: the irclib.Irc object on which the replies was received 396 397 * replies: a dict mapping the reply codes ('311', '312', etc.) to their 398 corresponding ircmsg.IrcMsg 399 400 * caller: an optional nick specifying who requested the whois information 401 402 * channel: an optional channel specifying where the reply will be sent 403 404 If provided, caller and channel will be used to avoid leaking information 405 that the caller/channel shouldn't be privy to. 406 """ 407 hostmask = '@'.join(replies['311'].args[2:4]) 408 nick = replies['318'].args[1] 409 user = replies['311'].args[-1] 410 START_CODE = '311' if command == 'whois' else '314' 411 hostmask = '@'.join(replies[START_CODE].args[2:4]) 412 user = replies[START_CODE].args[-1] 413 if _containsFormattingRe.search(user) and user[-1] != '\x0f': 414 # For good measure, disable any formatting 415 user = '%s\x0f' % user 416 if '319' in replies: 417 channels = [] 418 for msg in replies['319']: 419 channels.extend(msg.args[-1].split()) 420 ops = [] 421 voices = [] 422 normal = [] 423 halfops = [] 424 for chan in channels: 425 origchan = chan 426 chan = chan.lstrip('@%+~!') 427 # UnrealIRCd uses & for user modes and disallows it as a 428 # channel-prefix, flying in the face of the RFC. Have to 429 # handle this specially when processing WHOIS response. 430 testchan = chan.lstrip('&') 431 if testchan != chan and irc.isChannel(testchan): 432 chan = testchan 433 diff = len(chan) - len(origchan) 434 modes = origchan[:diff] 435 chanState = irc.state.channels.get(chan) 436 # The user is in a channel the bot is in, so the ircd may have 437 # responded with otherwise private data. 438 if chanState: 439 # Skip channels the caller isn't in. This prevents 440 # us from leaking information when the channel is +s or the 441 # target is +i. 442 if caller not in chanState.users: 443 continue 444 # Skip +s/+p channels the target is in only if the reply isn't 445 # being sent to that channel. 446 if set(('p', 's')) & set(chanState.modes.keys()) and \ 447 not strEqual(channel or '', chan): 448 continue 449 if not modes: 450 normal.append(chan) 451 elif utils.iter.any(lambda c: c in modes,('@', '&', '~', '!')): 452 ops.append(chan) 453 elif utils.iter.any(lambda c: c in modes, ('%',)): 454 halfops.append(chan) 455 elif utils.iter.any(lambda c: c in modes, ('+',)): 456 voices.append(chan) 457 L = [] 458 if ops: 459 L.append(format(_('is an op on %L'), ops)) 460 if halfops: 461 L.append(format(_('is a halfop on %L'), halfops)) 462 if voices: 463 L.append(format(_('is voiced on %L'), voices)) 464 if normal: 465 if L: 466 L.append(format(_('is also on %L'), normal)) 467 else: 468 L.append(format(_('is on %L'), normal)) 469 else: 470 if command == 'whois': 471 L = [_('isn\'t on any publicly visible channels')] 472 else: 473 L = [] 474 channels = format('%L', L) 475 if '317' in replies: 476 idle = utils.timeElapsed(replies['317'].args[2]) 477 signon = utils.str.timestamp(float(replies['317'].args[3])) 478 else: 479 idle = '<unknown>' 480 signon = '<unknown>' 481 if '312' in replies: 482 server = replies['312'].args[2] 483 if len(replies['312']) > 3: 484 signoff = replies['312'].args[3] 485 else: 486 server = '<unknown>' 487 if '301' in replies: 488 away = ' %s is away: %s.' % (nick, replies['301'].args[2]) 489 else: 490 away = '' 491 if '320' in replies: 492 if replies['320'].args[2]: 493 identify = ' identified' 494 else: 495 identify = '' 496 else: 497 identify = '' 498 if command == 'whois': 499 s = _('%s (%s) has been%s on server %s since %s (idle for %s). %s ' 500 '%s.%s') % (user, hostmask, identify, server, 501 signon, idle, nick, channels, away) 502 else: 503 s = _('%s (%s) has been%s on server %s and disconnected on %s.') % \ 504 (user, hostmask, identify, server, signoff) 505 return s 506 507class FormatContext(object): 508 def __init__(self): 509 self.reset() 510 511 def reset(self): 512 self.fg = None 513 self.bg = None 514 self.bold = False 515 self.reverse = False 516 self.underline = False 517 518 def start(self, s): 519 """Given a string, starts all the formatters in this context.""" 520 if self.bold: 521 s = '\x02' + s 522 if self.reverse: 523 s = '\x16' + s 524 if self.underline: 525 s = '\x1f' + s 526 if self.fg is not None or self.bg is not None: 527 s = mircColor(s, fg=self.fg, bg=self.bg)[:-1] # Remove \x03. 528 return s 529 530 def end(self, s): 531 """Given a string, ends all the formatters in this context.""" 532 if self.bold or self.reverse or \ 533 self.fg or self.bg or self.underline: 534 # Should we individually end formatters? 535 s += '\x0f' 536 return s 537 538 def size(self): 539 """Returns the number of bytes needed to reproduce this context in an 540 IRC string.""" 541 prefix_size = self.bold + self.reverse + self.underline + \ 542 bool(self.fg) + bool(self.bg) 543 if self.fg and self.bg: 544 prefix_size += 6 # '\x03xx,yy%s' 545 elif self.fg or self.bg: 546 prefix_size += 3 # '\x03xx%s' 547 if prefix_size: 548 return prefix_size + 1 # '\x0f' 549 else: 550 return 0 551 552class FormatParser(object): 553 def __init__(self, s): 554 self.fd = minisix.io.StringIO(s) 555 self.last = None 556 self.max_context_size = 0 557 558 def getChar(self): 559 if self.last is not None: 560 c = self.last 561 self.last = None 562 return c 563 else: 564 return self.fd.read(1) 565 566 def ungetChar(self, c): 567 self.last = c 568 569 def parse(self): 570 context = FormatContext() 571 c = self.getChar() 572 while c: 573 if c == '\x02': 574 context.bold = not context.bold 575 self.max_context_size = max( 576 self.max_context_size, context.size()) 577 elif c == '\x16': 578 context.reverse = not context.reverse 579 self.max_context_size = max( 580 self.max_context_size, context.size()) 581 elif c == '\x1f': 582 context.underline = not context.underline 583 self.max_context_size = max( 584 self.max_context_size, context.size()) 585 elif c == '\x0f': 586 context.reset() 587 elif c == '\x03': 588 self.getColor(context) 589 self.max_context_size = max( 590 self.max_context_size, context.size()) 591 c = self.getChar() 592 return context 593 594 def getInt(self): 595 i = 0 596 setI = False 597 c = self.getChar() 598 while c.isdigit(): 599 j = i * 10 600 j += int(c) 601 if j >= 16: 602 self.ungetChar(c) 603 break 604 else: 605 setI = True 606 i = j 607 c = self.getChar() 608 self.ungetChar(c) 609 if setI: 610 return i 611 else: 612 return None 613 614 def getColor(self, context): 615 context.fg = self.getInt() 616 c = self.getChar() 617 if c == ',': 618 context.bg = self.getInt() 619 else: 620 self.ungetChar(c) 621 622def wrap(s, length, break_on_hyphens = False): 623 # Get the maximum number of bytes needed to format a chunk of the string 624 # at any point. 625 # This is an overapproximation of what each chunk will need, but it's 626 # either that or make the code of byteTextWrap aware of contexts, and its 627 # code is complicated enough as it is already. 628 parser = FormatParser(s) 629 parser.parse() 630 format_overhead = parser.max_context_size 631 632 processed = [] 633 chunks = utils.str.byteTextWrap(s, length - format_overhead) 634 context = None 635 for chunk in chunks: 636 if context is not None: 637 chunk = context.start(chunk) 638 context = FormatParser(chunk).parse() 639 processed.append(context.end(chunk)) 640 return processed 641 642def isValidArgument(s): 643 """Returns whether s is strictly a valid argument for an IRC message.""" 644 645 return '\r' not in s and '\n' not in s and '\x00' not in s 646 647def safeArgument(s): 648 """If s is unsafe for IRC, returns a safe version.""" 649 if minisix.PY2 and isinstance(s, unicode): 650 s = s.encode('utf-8') 651 elif (minisix.PY2 and not isinstance(s, minisix.string_types)) or \ 652 (minisix.PY3 and not isinstance(s, str)): 653 debug('Got a non-string in safeArgument: %r', s) 654 s = str(s) 655 if isValidArgument(s): 656 return s 657 else: 658 return repr(s) 659 660def replyTo(msg): 661 """Returns the appropriate target to send responses to msg.""" 662 if isChannel(msg.args[0]): 663 return msg.args[0] 664 else: 665 return msg.nick 666 667def dccIP(ip): 668 """Converts an IP string to the DCC integer form.""" 669 assert utils.net.isIPV4(ip), \ 670 'argument must be a string ip in xxx.yyy.zzz.www format.' 671 i = 0 672 x = 256**3 673 for quad in ip.split('.'): 674 i += int(quad)*x 675 x //= 256 676 return i 677 678def unDccIP(i): 679 """Takes an integer DCC IP and return a normal string IP.""" 680 assert isinstance(i, minisix.integer_types), '%r is not an number.' % i 681 L = [] 682 while len(L) < 4: 683 L.append(i % 256) 684 i //= 256 685 L.reverse() 686 return '.'.join(map(str, L)) 687 688class IrcString(str): 689 """This class does case-insensitive comparison and hashing of nicks.""" 690 def __new__(cls, s=''): 691 x = super(IrcString, cls).__new__(cls, s) 692 x.lowered = str(toLower(x)) 693 return x 694 695 def __eq__(self, s): 696 try: 697 return toLower(s) == self.lowered 698 except: 699 return False 700 701 def __ne__(self, s): 702 return not (self == s) 703 704 def __hash__(self): 705 return hash(self.lowered) 706 707 708class IrcDict(utils.InsensitivePreservingDict): 709 """Subclass of dict to make key comparison IRC-case insensitive.""" 710 def key(self, s): 711 if s is not None: 712 s = toLower(s) 713 return s 714 715class CallableValueIrcDict(IrcDict): 716 def __getitem__(self, k): 717 v = super(IrcDict, self).__getitem__(k) 718 if callable(v): 719 v = v() 720 return v 721 722class IrcSet(utils.NormalizingSet): 723 """A sets.Set using IrcStrings instead of regular strings.""" 724 def normalize(self, s): 725 return IrcString(s) 726 727 def __reduce__(self): 728 return (self.__class__, (list(self),)) 729 730 731class FloodQueue(object): 732 timeout = 0 733 def __init__(self, timeout=None, queues=None): 734 if timeout is not None: 735 self.timeout = timeout 736 if queues is None: 737 queues = IrcDict() 738 self.queues = queues 739 740 def __repr__(self): 741 return 'FloodQueue(timeout=%r, queues=%s)' % (self.timeout, 742 repr(self.queues)) 743 744 def key(self, msg): 745 # This really ought to be configurable without subclassing, but for 746 # now, it works. 747 # used to be msg.user + '@' + msg.host but that was too easily abused. 748 return msg.host 749 750 def getTimeout(self): 751 if callable(self.timeout): 752 return self.timeout() 753 else: 754 return self.timeout 755 756 def _getQueue(self, msg, insert=True): 757 key = self.key(msg) 758 try: 759 return self.queues[key] 760 except KeyError: 761 if insert: 762 # python-- 763 # instancemethod.__repr__ calls the instance.__repr__, which 764 # means that our __repr__ calls self.queues.__repr__, which 765 # calls structures.TimeoutQueue.__repr__, which calls 766 # getTimeout.__repr__, which calls our __repr__, which calls... 767 getTimeout = lambda : self.getTimeout() 768 q = utils.structures.TimeoutQueue(getTimeout) 769 self.queues[key] = q 770 return q 771 else: 772 return None 773 774 def enqueue(self, msg, what=None): 775 if what is None: 776 what = msg 777 q = self._getQueue(msg) 778 q.enqueue(what) 779 780 def len(self, msg): 781 q = self._getQueue(msg, insert=False) 782 if q is not None: 783 return len(q) 784 else: 785 return 0 786 787 def has(self, msg, what=None): 788 q = self._getQueue(msg, insert=False) 789 if q is not None: 790 if what is None: 791 what = msg 792 for elt in q: 793 if elt == what: 794 return True 795 return False 796 797 798mircColors = IrcDict({ 799 'white': '0', 800 'black': '1', 801 'blue': '2', 802 'green': '3', 803 'red': '4', 804 'brown': '5', 805 'purple': '6', 806 'orange': '7', 807 'yellow': '8', 808 'light green': '9', 809 'teal': '10', 810 'light blue': '11', 811 'dark blue': '12', 812 'pink': '13', 813 'dark grey': '14', 814 'light grey': '15', 815 'dark gray': '14', 816 'light gray': '15', 817}) 818 819# We'll map integers to their string form so mircColor is simpler. 820for (k, v) in list(mircColors.items()): 821 if k is not None: # Ignore empty string for None. 822 sv = str(v) 823 mircColors[sv] = sv 824 mircColors[sv.zfill(2)] = sv 825 826def standardSubstitute(irc, msg, text, env=None): 827 """Do the standard set of substitutions on text, and return it""" 828 def randInt(): 829 return str(random.randint(-1000, 1000)) 830 def randDate(): 831 t = pow(2,30)*random.random()+time.time()/4.0 832 return time.ctime(t) 833 ctime = time.strftime("%a %b %d %H:%M:%S %Y") 834 localtime = time.localtime() 835 gmtime = time.strftime("%a %b %d %H:%M:%S %Y", time.gmtime()) 836 vars = CallableValueIrcDict({ 837 'now': ctime, 'ctime': ctime, 838 'utc': gmtime, 'gmt': gmtime, 839 'randdate': randDate, 'randomdate': randDate, 840 'rand': randInt, 'randint': randInt, 'randomint': randInt, 841 'today': time.strftime('%d %b %Y', localtime), 842 'year': localtime[0], 843 'month': localtime[1], 844 'monthname': time.strftime('%b', localtime), 845 'date': localtime[2], 846 'day': time.strftime('%A', localtime), 847 'h': localtime[3], 'hr': localtime[3], 'hour': localtime[3], 848 'm': localtime[4], 'min': localtime[4], 'minute': localtime[4], 849 's': localtime[5], 'sec': localtime[5], 'second': localtime[5], 850 'tz': time.strftime('%Z', localtime), 851 'version': version, 852 }) 853 if irc: 854 vars.update({ 855 'botnick': irc.nick, 856 'network': irc.network, 857 }) 858 859 if msg: 860 vars.update({ 861 'who': msg.nick, 862 'nick': msg.nick, 863 'user': msg.user, 864 'host': msg.host, 865 }) 866 if msg.reply_env: 867 vars.update(msg.reply_env) 868 869 if irc and msg: 870 if isChannel(msg.args[0]): 871 channel = msg.args[0] 872 else: 873 channel = 'somewhere' 874 def randNick(): 875 if channel != 'somewhere': 876 L = list(irc.state.channels[channel].users) 877 if len(L) > 1: 878 n = msg.nick 879 while n == msg.nick: 880 n = utils.iter.choice(L) 881 return n 882 else: 883 return msg.nick 884 else: 885 return 'someone' 886 vars.update({ 887 'randnick': randNick, 'randomnick': randNick, 888 'channel': channel, 889 }) 890 else: 891 vars.update({ 892 'channel': 'somewhere', 893 'randnick': 'someone', 'randomnick': 'someone', 894 }) 895 896 if env is not None: 897 vars.update(env) 898 t = string.Template(text) 899 t.idpattern = '[a-zA-Z][a-zA-Z0-9]*' 900 return t.safe_substitute(vars) 901 902 903 904AUTHENTICATE_CHUNK_SIZE = 400 905def authenticate_generator(authstring, base64ify=True): 906 if base64ify: 907 authstring = base64.b64encode(authstring) 908 if minisix.PY3: 909 authstring = authstring.decode() 910 # +1 so we get an empty string at the end if len(authstring) is a multiple 911 # of AUTHENTICATE_CHUNK_SIZE (including 0) 912 for n in range(0, len(authstring)+1, AUTHENTICATE_CHUNK_SIZE): 913 chunk = authstring[n:n+AUTHENTICATE_CHUNK_SIZE] or '+' 914 yield chunk 915 916class AuthenticateDecoder(object): 917 def __init__(self): 918 self.chunks = [] 919 self.ready = False 920 def feed(self, msg): 921 assert msg.command == 'AUTHENTICATE' 922 chunk = msg.args[0] 923 if chunk == '+' or len(chunk) != AUTHENTICATE_CHUNK_SIZE: 924 self.ready = True 925 if chunk != '+': 926 if minisix.PY3: 927 chunk = chunk.encode() 928 self.chunks.append(chunk) 929 def get(self): 930 assert self.ready 931 return base64.b64decode(b''.join(self.chunks)) 932 933 934numerics = { 935 # <= 2.10 936 # Reply 937 '001': 'RPL_WELCOME', 938 '002': 'RPL_YOURHOST', 939 '003': 'RPL_CREATED', 940 '004': 'RPL_MYINFO', 941 '005': 'RPL_BOUNCE', 942 '302': 'RPL_USERHOST', 943 '303': 'RPL_ISON', 944 '301': 'RPL_AWAY', 945 '305': 'RPL_UNAWAY', 946 '306': 'RPL_NOWAWAY', 947 '311': 'RPL_WHOISUSER', 948 '312': 'RPL_WHOISSERVER', 949 '313': 'RPL_WHOISOPERATOR', 950 '317': 'RPL_WHOISIDLE', 951 '318': 'RPL_ENDOFWHOIS', 952 '319': 'RPL_WHOISCHANNELS', 953 '314': 'RPL_WHOWASUSER', 954 '369': 'RPL_ENDOFWHOWAS', 955 '321': 'RPL_LISTSTART', 956 '322': 'RPL_LIST', 957 '323': 'RPL_LISTEND', 958 '325': 'RPL_UNIQOPIS', 959 '324': 'RPL_CHANNELMODEIS', 960 '331': 'RPL_NOTOPIC', 961 '332': 'RPL_TOPIC', 962 '341': 'RPL_INVITING', 963 '342': 'RPL_SUMMONING', 964 '346': 'RPL_INVITELIST', 965 '347': 'RPL_ENDOFINVITELIST', 966 '348': 'RPL_EXCEPTLIST', 967 '349': 'RPL_ENDOFEXCEPTLIST', 968 '351': 'RPL_VERSION', 969 '352': 'RPL_WHOREPLY', 970 '352': 'RPL_WHOREPLY', 971 '353': 'RPL_NAMREPLY', 972 '366': 'RPL_ENDOFNAMES', 973 '364': 'RPL_LINKS', 974 '365': 'RPL_ENDOFLINKS', 975 '367': 'RPL_BANLIST', 976 '368': 'RPL_ENDOFBANLIST', 977 '371': 'RPL_INFO', 978 '374': 'RPL_ENDOFINFO', 979 '372': 'RPL_MOTD', 980 '376': 'RPL_ENDOFMOTD', 981 '381': 'RPL_YOUREOPER', 982 '382': 'RPL_REHASHING', 983 '383': 'RPL_YOURESERVICE', 984 '391': 'RPL_TIME', 985 '392': 'RPL_USERSSTART', 986 '393': 'RPL_USERS', 987 '394': 'RPL_ENDOFUSERS', 988 '395': 'RPL_NOUSERS', 989 '200': 'RPL_TRACELINK', 990 '201': 'RPL_TRACECONNECTING', 991 '202': 'RPL_TRACEHANDSHAKE', 992 '203': 'RPL_TRACEUNKNOWN', 993 '204': 'RPL_TRACEOPERATOR', 994 '205': 'RPL_TRACEUSER', 995 '206': 'RPL_TRACESERVER', 996 '207': 'RPL_TRACESERVICE', 997 '208': 'RPL_TRACENEWTYPE', 998 '209': 'RPL_TRACECLASS', 999 '210': 'RPL_TRACERECONNECT', 1000 '261': 'RPL_TRACELOG', 1001 '262': 'RPL_TRACEEND', 1002 '211': 'RPL_STATSLINKINFO', 1003 '212': 'RPL_STATSCOMMANDS', 1004 '219': 'RPL_ENDOFSTATS', 1005 '242': 'RPL_STATSUPTIME', 1006 '243': 'RPL_STATSOLINE', 1007 '221': 'RPL_UMODEIS', 1008 '234': 'RPL_SERVLIST', 1009 '235': 'RPL_SERVLISTEND', 1010 '251': 'RPL_LUSERCLIENT', 1011 '252': 'RPL_LUSEROP', 1012 '253': 'RPL_LUSERUNKNOWN', 1013 '254': 'RPL_LUSERCHANNELS', 1014 '255': 'RPL_LUSERME', 1015 '256': 'RPL_ADMINME', 1016 '257': 'RPL_ADMINLOC1', 1017 '258': 'RPL_ADMINLOC2', 1018 '259': 'RPL_ADMINEMAIL', 1019 '263': 'RPL_TRYAGAIN', 1020 1021 # Error 1022 '401': 'ERR_NOSUCHNICK', 1023 '402': 'ERR_NOSUCHSERVER', 1024 '403': 'ERR_NOSUCHCHANNEL', 1025 '404': 'ERR_CANNOTSENDTOCHAN', 1026 '405': 'ERR_TOOMANYCHANNELS', 1027 '406': 'ERR_WASNOSUCHNICK', 1028 '407': 'ERR_TOOMANYTARGETS', 1029 '408': 'ERR_NOSUCHSERVICE', 1030 '409': 'ERR_NOORIGIN', 1031 '411': 'ERR_NORECIPIENT', 1032 '412': 'ERR_NOTEXTTOSEND', 1033 '413': 'ERR_NOTOPLEVEL', 1034 '414': 'ERR_WILDTOPLEVEL', 1035 '415': 'ERR_BADMASK', 1036 '421': 'ERR_UNKNOWNCOMMAND', 1037 '422': 'ERR_NOMOTD', 1038 '423': 'ERR_NOADMININFO', 1039 '424': 'ERR_FILEERROR', 1040 '431': 'ERR_NONICKNAMEGIVEN', 1041 '432': 'ERR_ERRONEUSNICKNAME', 1042 '433': 'ERR_NICKNAMEINUSE', 1043 '436': 'ERR_NICKCOLLISION', 1044 '437': 'ERR_UNAVAILRESOURCE', 1045 '441': 'ERR_USERNOTINCHANNEL', 1046 '442': 'ERR_NOTONCHANNEL', 1047 '443': 'ERR_USERONCHANNEL', 1048 '444': 'ERR_NOLOGIN', 1049 '445': 'ERR_SUMMONDISABLED', 1050 '446': 'ERR_USERSDISABLED', 1051 '451': 'ERR_NOTREGISTERED', 1052 '461': 'ERR_NEEDMOREPARAMS', 1053 '462': 'ERR_ALREADYREGISTRED', 1054 '463': 'ERR_NOPERMFORHOST', 1055 '464': 'ERR_PASSWDMISMATCH', 1056 '465': 'ERR_YOUREBANNEDCREEP', 1057 '466': 'ERR_YOUWILLBEBANNED', 1058 '467': 'ERR_KEYSET', 1059 '471': 'ERR_CHANNELISFULL', 1060 '472': 'ERR_UNKNOWNMODE', 1061 '473': 'ERR_INVITEONLYCHAN', 1062 '474': 'ERR_BANNEDFROMCHAN', 1063 '475': 'ERR_BADCHANNELKEY', 1064 '476': 'ERR_BADCHANMASK', 1065 '477': 'ERR_NOCHANMODES', 1066 '478': 'ERR_BANLISTFULL', 1067 '481': 'ERR_NOPRIVILEGES', 1068 '482': 'ERR_CHANOPRIVSNEEDED', 1069 '483': 'ERR_CANTKILLSERVER', 1070 '484': 'ERR_RESTRICTED', 1071 '485': 'ERR_UNIQOPPRIVSNEEDED', 1072 '491': 'ERR_NOOPERHOST', 1073 '501': 'ERR_UMODEUNKNOWNFLAG', 1074 '502': 'ERR_USERSDONTMATCH', 1075 1076 # Reserved 1077 '231': 'RPL_SERVICEINFO', 1078 '232': 'RPL_ENDOFSERVICES', 1079 '233': 'RPL_SERVICE', 1080 '300': 'RPL_NONE', 1081 '316': 'RPL_WHOISCHANOP', 1082 '361': 'RPL_KILLDONE', 1083 '362': 'RPL_CLOSING', 1084 '363': 'RPL_CLOSEEND', 1085 '373': 'RPL_INFOSTART', 1086 '384': 'RPL_MYPORTIS', 1087 '213': 'RPL_STATSCLINE', 1088 '214': 'RPL_STATSNLINE', 1089 '215': 'RPL_STATSILINE', 1090 '216': 'RPL_STATSKLINE', 1091 '217': 'RPL_STATSQLINE', 1092 '218': 'RPL_STATSYLINE', 1093 '240': 'RPL_STATSVLINE', 1094 '241': 'RPL_STATSLLINE', 1095 '244': 'RPL_STATSHLINE', 1096 '244': 'RPL_STATSSLINE', 1097 '246': 'RPL_STATSPING', 1098 '247': 'RPL_STATSBLINE', 1099 '250': 'RPL_STATSDLINE', 1100 '492': 'ERR_NOSERVICEHOST', 1101 1102 # IRC v3.1 1103 # SASL 1104 '900': 'RPL_LOGGEDIN', 1105 '901': 'RPL_LOGGEDOUT', 1106 '902': 'ERR_NICKLOCKED', 1107 '903': 'RPL_SASLSUCCESS', 1108 '904': 'ERR_SASLFAIL', 1109 '905': 'ERR_SASLTOOLONG', 1110 '906': 'ERR_SASLABORTED', 1111 '907': 'ERR_SASLALREADY', 1112 '908': 'RPL_SASLMECHS', 1113 1114 # IRC v3.2 1115 # Metadata 1116 '760': 'RPL_WHOISKEYVALUE', 1117 '761': 'RPL_KEYVALUE', 1118 '762': 'RPL_METADATAEND', 1119 '764': 'ERR_METADATALIMIT', 1120 '765': 'ERR_TARGETINVALID', 1121 '766': 'ERR_NOMATCHINGKEY', 1122 '767': 'ERR_KEYINVALID', 1123 '768': 'ERR_KEYNOTSET', 1124 '769': 'ERR_KEYNOPERMISSION', 1125 1126 # Monitor 1127 '730': 'RPL_MONONLINE', 1128 '731': 'RPL_MONOFFLINE', 1129 '732': 'RPL_MONLIST', 1130 '733': 'RPL_ENDOFMONLIST', 1131 '734': 'ERR_MONLISTFULL', 1132} 1133 1134if __name__ == '__main__': 1135 import doctest 1136 doctest.testmod(sys.modules['__main__']) 1137# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 1138