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