1###
2# Copyright (c) 2002-2005 Jeremiah Fincher
3# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are met:
7#
8#   * Redistributions of source code must retain the above copyright notice,
9#     this list of conditions, and the following disclaimer.
10#   * Redistributions in binary form must reproduce the above copyright notice,
11#     this list of conditions, and the following disclaimer in the
12#     documentation and/or other materials provided with the distribution.
13#   * Neither the name of the author of this software nor the name of
14#     contributors to this software may be used to endorse or promote products
15#     derived from this software without specific prior written consent.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27# POSSIBILITY OF SUCH DAMAGE.
28###
29
30import re
31import copy
32import time
33import random
34import base64
35import collections
36
37try:
38    import ecdsa
39except ImportError:
40    ecdsa = None
41
42try:
43    import pyxmpp2_scram as scram
44except ImportError:
45    scram = None
46
47from . import conf, ircdb, ircmsgs, ircutils, log, utils, world
48from .utils.str import rsplit
49from .utils.iter import chain
50from .utils.structures import smallqueue, RingBuffer
51
52###
53# The base class for a callback to be registered with an Irc object.  Shows
54# the required interface for callbacks -- name(),
55# inFilter(irc, msg), outFilter(irc, msg), and __call__(irc, msg) [used so as
56# to make functions used as callbacks conceivable, and so if refactoring ever
57# changes the nature of the callbacks from classes to functions, syntactical
58# changes elsewhere won't be required.]
59###
60
61class IrcCommandDispatcher(object):
62    """Base class for classes that must dispatch on a command."""
63    def dispatchCommand(self, command):
64        """Given a string 'command', dispatches to doCommand."""
65        return getattr(self, 'do' + command.capitalize(), None)
66
67
68class IrcCallback(IrcCommandDispatcher, log.Firewalled):
69    """Base class for standard callbacks.
70
71    Callbacks derived from this class should have methods of the form
72    "doCommand" -- doPrivmsg, doNick, do433, etc.  These will be called
73    on matching messages.
74    """
75    callAfter = ()
76    callBefore = ()
77    __firewalled__ = {'die': None,
78                      'reset': None,
79                      '__call__': None,
80                      'inFilter': lambda self, irc, msg: msg,
81                      'outFilter': lambda self, irc, msg: msg,
82                      'name': lambda self: self.__class__.__name__,
83                      'callPrecedence': lambda self, irc: ([], []),
84                      }
85
86    def __init__(self, *args, **kwargs):
87        #object doesn't take any args, so the buck stops here.
88        #super(IrcCallback, self).__init__(*args, **kwargs)
89        pass
90
91    def __repr__(self):
92        return '<%s %s %s>' % \
93               (self.__class__.__name__, self.name(), object.__repr__(self))
94
95    def name(self):
96        """Returns the name of the callback."""
97        return self.__class__.__name__
98
99    def callPrecedence(self, irc):
100        """Returns a pair of (callbacks to call before me,
101                              callbacks to call after me)"""
102        after = []
103        before = []
104        for name in self.callBefore:
105            cb = irc.getCallback(name)
106            if cb is not None:
107                after.append(cb)
108        for name in self.callAfter:
109            cb = irc.getCallback(name)
110            if cb is not None:
111                before.append(cb)
112        assert self not in after, '%s was in its own after.' % self.name()
113        assert self not in before, '%s was in its own before.' % self.name()
114        return (before, after)
115
116    def inFilter(self, irc, msg):
117        """Used for filtering/modifying messages as they're entering.
118
119        ircmsgs.IrcMsg objects are immutable, so this method is expected to
120        return another ircmsgs.IrcMsg object.  Obviously the same IrcMsg
121        can be returned.
122        """
123        return msg
124
125    def outFilter(self, irc, msg):
126        """Used for filtering/modifying messages as they're leaving.
127
128        As with inFilter, an IrcMsg is returned.
129        """
130        return msg
131
132    def __call__(self, irc, msg):
133        """Used for handling each message."""
134        method = self.dispatchCommand(msg.command)
135        if method is not None:
136            method(irc, msg)
137
138    def reset(self):
139        """Resets the callback.  Called when reconnecting to the server."""
140        pass
141
142    def die(self):
143        """Makes the callback die.  Called when the parent Irc object dies."""
144        pass
145
146###
147# Basic queue for IRC messages.  It doesn't presently (but should at some
148# later point) reorder messages based on priority or penalty calculations.
149###
150_high = frozenset(['MODE', 'KICK', 'PONG', 'NICK', 'PASS', 'CAPAB', 'REMOVE'])
151_low = frozenset(['PRIVMSG', 'PING', 'WHO', 'NOTICE', 'JOIN'])
152class IrcMsgQueue(object):
153    """Class for a queue of IrcMsgs.  Eventually, it should be smart.
154
155    Probably smarter than it is now, though it's gotten quite a bit smarter
156    than it originally was.  A method to "score" methods, and a heapq to
157    maintain a priority queue of the messages would be the ideal way to do
158    intelligent queuing.
159
160    As it stands, however, we simply keep track of 'high priority' messages,
161    'low priority' messages, and normal messages, and just make sure to return
162    the 'high priority' ones before the normal ones before the 'low priority'
163    ones.
164    """
165    __slots__ = ('msgs', 'highpriority', 'normal', 'lowpriority', 'lastJoin')
166    def __init__(self, iterable=()):
167        self.reset()
168        for msg in iterable:
169            self.enqueue(msg)
170
171    def reset(self):
172        """Clears the queue."""
173        self.lastJoin = 0
174        self.highpriority = smallqueue()
175        self.normal = smallqueue()
176        self.lowpriority = smallqueue()
177
178    def enqueue(self, msg):
179        """Enqueues a given message."""
180        if msg in self and \
181           conf.supybot.protocols.irc.queuing.duplicates():
182            s = str(msg).strip()
183            log.info('Not adding message %q to queue, already added.', s)
184            return False
185        else:
186            if msg.command in _high:
187                self.highpriority.enqueue(msg)
188            elif msg.command in _low:
189                self.lowpriority.enqueue(msg)
190            else:
191                self.normal.enqueue(msg)
192            return True
193
194    def dequeue(self):
195        """Dequeues a given message."""
196        msg = None
197        if self.highpriority:
198            msg = self.highpriority.dequeue()
199        elif self.normal:
200            msg = self.normal.dequeue()
201        elif self.lowpriority:
202            msg = self.lowpriority.dequeue()
203            if msg.command == 'JOIN':
204                limit = conf.supybot.protocols.irc.queuing.rateLimit.join()
205                now = time.time()
206                if self.lastJoin + limit <= now:
207                    self.lastJoin = now
208                else:
209                    self.lowpriority.enqueue(msg)
210                    msg = None
211        return msg
212
213    def __contains__(self, msg):
214        return msg in self.normal or \
215               msg in self.lowpriority or \
216               msg in self.highpriority
217
218    def __bool__(self):
219        return bool(self.highpriority or self.normal or self.lowpriority)
220    __nonzero__ = __bool__
221
222    def __len__(self):
223        return len(self.highpriority)+len(self.lowpriority)+len(self.normal)
224
225    def __repr__(self):
226        name = self.__class__.__name__
227        return '%s(%r)' % (name, list(chain(self.highpriority,
228                                            self.normal,
229                                            self.lowpriority)))
230    __str__ = __repr__
231
232
233###
234# Maintains the state of IRC connection -- the most recent messages, the
235# status of various modes (especially ops/halfops/voices) in channels, etc.
236###
237class ChannelState(utils.python.Object):
238    __slots__ = ('users', 'ops', 'halfops', 'bans',
239                 'voices', 'topic', 'modes', 'created')
240    def __init__(self):
241        self.topic = ''
242        self.created = 0
243        self.ops = ircutils.IrcSet()
244        self.bans = ircutils.IrcSet()
245        self.users = ircutils.IrcSet()
246        self.voices = ircutils.IrcSet()
247        self.halfops = ircutils.IrcSet()
248        self.modes = {}
249
250    def isOp(self, nick):
251        return nick in self.ops
252    def isOpPlus(self, nick):
253        return nick in self.ops
254    def isVoice(self, nick):
255        return nick in self.voices
256    def isVoicePlus(self, nick):
257        return nick in self.voices or nick in self.halfops or nick in self.ops
258    def isHalfop(self, nick):
259        return nick in self.halfops
260    def isHalfopPlus(self, nick):
261        return nick in self.halfops or nick in self.ops
262
263    def addUser(self, user):
264        "Adds a given user to the ChannelState.  Power prefixes are handled."
265        nick = user.lstrip('@%+&~!')
266        if not nick:
267            return
268        # & is used to denote protected users in UnrealIRCd
269        # ~ is used to denote channel owner in UnrealIRCd
270        # ! is used to denote protected users in UltimateIRCd
271        while user and user[0] in '@%+&~!':
272            (marker, user) = (user[0], user[1:])
273            assert user, 'Looks like my caller is passing chars, not nicks.'
274            if marker in '@&~!':
275                self.ops.add(nick)
276            elif marker == '%':
277                self.halfops.add(nick)
278            elif marker == '+':
279                self.voices.add(nick)
280        self.users.add(nick)
281
282    def replaceUser(self, oldNick, newNick):
283        """Changes the user oldNick to newNick; used for NICK changes."""
284        # Note that this doesn't have to have the sigil (@%+) that users
285        # have to have for addUser; it just changes the name of the user
286        # without changing any of their categories.
287        for s in (self.users, self.ops, self.halfops, self.voices):
288            if oldNick in s:
289                s.remove(oldNick)
290                s.add(newNick)
291
292    def removeUser(self, user):
293        """Removes a given user from the channel."""
294        self.users.discard(user)
295        self.ops.discard(user)
296        self.halfops.discard(user)
297        self.voices.discard(user)
298
299    def setMode(self, mode, value=None):
300        assert mode not in 'ovhbeq'
301        self.modes[mode] = value
302
303    def unsetMode(self, mode):
304        assert mode not in 'ovhbeq'
305        if mode in self.modes:
306            del self.modes[mode]
307
308    def doMode(self, msg):
309        def getSet(c):
310            if c == 'o':
311                Set = self.ops
312            elif c == 'v':
313                Set = self.voices
314            elif c == 'h':
315                Set = self.halfops
316            elif c == 'b':
317                Set = self.bans
318            else: # We don't care yet, so we'll just return an empty set.
319                Set = set()
320            return Set
321        for (mode, value) in ircutils.separateModes(msg.args[1:]):
322            (action, modeChar) = mode
323            if modeChar in 'ovhbeq': # We don't handle e or q yet.
324                Set = getSet(modeChar)
325                if action == '-':
326                    Set.discard(value)
327                elif action == '+':
328                    Set.add(value)
329            else:
330                if action == '+':
331                    self.setMode(modeChar, value)
332                else:
333                    assert action == '-'
334                    self.unsetMode(modeChar)
335
336    def __getstate__(self):
337        return [getattr(self, name) for name in self.__slots__]
338
339    def __setstate__(self, t):
340        for (name, value) in zip(self.__slots__, t):
341            setattr(self, name, value)
342
343    def __eq__(self, other):
344        ret = True
345        for name in self.__slots__:
346            ret = ret and getattr(self, name) == getattr(other, name)
347        return ret
348
349Batch = collections.namedtuple('Batch', 'type arguments messages')
350
351class IrcState(IrcCommandDispatcher, log.Firewalled):
352    """Maintains state of the Irc connection.  Should also become smarter.
353    """
354    __firewalled__ = {'addMsg': None}
355    def __init__(self, history=None, supported=None,
356                 nicksToHostmasks=None, channels=None,
357                 capabilities_ack=None, capabilities_nak=None,
358                 capabilities_ls=None):
359        if history is None:
360            history = RingBuffer(conf.supybot.protocols.irc.maxHistoryLength())
361        if supported is None:
362            supported = utils.InsensitivePreservingDict()
363        if nicksToHostmasks is None:
364            nicksToHostmasks = ircutils.IrcDict()
365        if channels is None:
366            channels = ircutils.IrcDict()
367        self.capabilities_ack = capabilities_ack or set()
368        self.capabilities_nak = capabilities_nak or set()
369        self.capabilities_ls = capabilities_ls or {}
370        self.ircd = None
371        self.supported = supported
372        self.history = history
373        self.channels = channels
374        self.nicksToHostmasks = nicksToHostmasks
375        self.batches = {}
376
377    def reset(self):
378        """Resets the state to normal, unconnected state."""
379        self.history.reset()
380        self.channels.clear()
381        self.supported.clear()
382        self.nicksToHostmasks.clear()
383        self.history.resize(conf.supybot.protocols.irc.maxHistoryLength())
384        self.batches = {}
385
386    def __reduce__(self):
387        return (self.__class__, (self.history, self.supported,
388                                 self.nicksToHostmasks, self.channels))
389
390    def __eq__(self, other):
391        return self.history == other.history and \
392               self.channels == other.channels and \
393               self.supported == other.supported and \
394               self.nicksToHostmasks == other.nicksToHostmasks and \
395               self.batches == other.batches
396
397    def __ne__(self, other):
398        return not self == other
399
400    def copy(self):
401        ret = self.__class__()
402        ret.history = copy.deepcopy(self.history)
403        ret.nicksToHostmasks = copy.deepcopy(self.nicksToHostmasks)
404        ret.channels = copy.deepcopy(self.channels)
405        ret.batches = copy.deepcopy(self.batches)
406        return ret
407
408    def addMsg(self, irc, msg):
409        """Updates the state based on the irc object and the message."""
410        self.history.append(msg)
411        if ircutils.isUserHostmask(msg.prefix) and not msg.command == 'NICK':
412            self.nicksToHostmasks[msg.nick] = msg.prefix
413        if 'batch' in msg.server_tags:
414            batch = msg.server_tags['batch']
415            assert batch in self.batches, \
416                    'Server references undeclared batch %s' % batch
417            self.batches[batch].messages.append(msg)
418        method = self.dispatchCommand(msg.command)
419        if method is not None:
420            method(irc, msg)
421
422    def getTopic(self, channel):
423        """Returns the topic for a given channel."""
424        return self.channels[channel].topic
425
426    def nickToHostmask(self, nick):
427        """Returns the hostmask for a given nick."""
428        return self.nicksToHostmasks[nick]
429
430    def do004(self, irc, msg):
431        """Handles parsing the 004 reply
432
433        Supported user and channel modes are cached"""
434        # msg.args = [nick, server, ircd-version, umodes, modes,
435        #             modes that require arguments? (non-standard)]
436        self.ircd = msg.args[2] if len(msg.args) >= 3 else msg.args[1]
437        self.supported['umodes'] = frozenset(msg.args[3])
438        self.supported['chanmodes'] = frozenset(msg.args[4])
439
440    _005converters = utils.InsensitivePreservingDict({
441        'modes': int,
442        'keylen': int,
443        'nicklen': int,
444        'userlen': int,
445        'hostlen': int,
446        'kicklen': int,
447        'awaylen': int,
448        'silence': int,
449        'topiclen': int,
450        'channellen': int,
451        'maxtargets': int,
452        'maxnicklen': int,
453        'maxchannels': int,
454        'watch': int, # DynastyNet, EnterTheGame
455        })
456    def _prefixParser(s):
457        if ')' in s:
458            (left, right) = s.split(')')
459            assert left[0] == '(', 'Odd PREFIX in 005: %s' % s
460            left = left[1:]
461            assert len(left) == len(right), 'Odd PREFIX in 005: %s' % s
462            return dict(list(zip(left, right)))
463        else:
464            return dict(list(zip('ovh', s)))
465    _005converters['prefix'] = _prefixParser
466    del _prefixParser
467    def _maxlistParser(s):
468        modes = ''
469        limits = []
470        pairs = s.split(',')
471        for pair in pairs:
472            (mode, limit) = pair.split(':', 1)
473            modes += mode
474            limits += (int(limit),) * len(mode)
475        return dict(list(zip(modes, limits)))
476    _005converters['maxlist'] = _maxlistParser
477    del _maxlistParser
478    def _maxbansParser(s):
479        # IRCd using a MAXLIST style string (IRCNet)
480        if ':' in s:
481            modes = ''
482            limits = []
483            pairs = s.split(',')
484            for pair in pairs:
485                (mode, limit) = pair.split(':', 1)
486                modes += mode
487                limits += (int(limit),) * len(mode)
488            d = dict(list(zip(modes, limits)))
489            assert 'b' in d
490            return d['b']
491        else:
492            return int(s)
493    _005converters['maxbans'] = _maxbansParser
494    del _maxbansParser
495    def do005(self, irc, msg):
496        for arg in msg.args[1:-1]: # 0 is nick, -1 is "are supported"
497            if '=' in arg:
498                (name, value) = arg.split('=', 1)
499                converter = self._005converters.get(name, lambda x: x)
500                try:
501                    self.supported[name] = converter(value)
502                except Exception:
503                    log.exception('Uncaught exception in 005 converter:')
504                    log.error('Name: %s, Converter: %s', name, converter)
505            else:
506                self.supported[arg] = None
507
508    def do352(self, irc, msg):
509        # WHO reply.
510
511        (nick, user, host) = (msg.args[5], msg.args[2], msg.args[3])
512        hostmask = '%s!%s@%s' % (nick, user, host)
513        self.nicksToHostmasks[nick] = hostmask
514
515    def do354(self, irc, msg):
516        # WHOX reply.
517
518        if len(msg.args) != 9 or msg.args[1] != '1':
519            return
520        # irc.nick 1 user ip host nick status account gecos
521        (n, t, user, ip, host, nick, status, account, gecos) = msg.args
522        hostmask = '%s!%s@%s' % (nick, user, host)
523        self.nicksToHostmasks[nick] = hostmask
524
525    def do353(self, irc, msg):
526        # NAMES reply.
527        (__, type, channel, items) = msg.args
528        if channel not in self.channels:
529            self.channels[channel] = ChannelState()
530        c = self.channels[channel]
531        for item in items.split():
532            if ircutils.isUserHostmask(item):
533                name = ircutils.nickFromHostmask(item)
534                self.nicksToHostmasks[name] = name
535            else:
536                name = item
537            c.addUser(name)
538        if type == '@':
539            c.modes['s'] = None
540
541    def doChghost(self, irc, msg):
542        (user, host) = msg.args
543        nick = msg.nick
544        hostmask = '%s!%s@%s' % (nick, user, host)
545        self.nicksToHostmasks[nick] = hostmask
546
547    def doJoin(self, irc, msg):
548        for channel in msg.args[0].split(','):
549            if channel in self.channels:
550                self.channels[channel].addUser(msg.nick)
551            elif msg.nick: # It must be us.
552                chan = ChannelState()
553                chan.addUser(msg.nick)
554                self.channels[channel] = chan
555                # I don't know why this assert was here.
556                #assert msg.nick == irc.nick, msg
557
558    def do367(self, irc, msg):
559        # Example:
560        # :server 367 user #chan some!random@user evil!channel@op 1356276459
561        try:
562            state = self.channels[msg.args[1]]
563        except KeyError:
564            # We have been kicked of the channel before the server replied to
565            # the MODE +b command.
566            pass
567        else:
568            state.bans.add(msg.args[2])
569
570    def doMode(self, irc, msg):
571        channel = msg.args[0]
572        if irc.isChannel(channel): # There can be user modes, as well.
573            try:
574                chan = self.channels[channel]
575            except KeyError:
576                chan = ChannelState()
577                self.channels[channel] = chan
578            chan.doMode(msg)
579
580    def do324(self, irc, msg):
581        channel = msg.args[1]
582        try:
583            chan = self.channels[channel]
584        except KeyError:
585            chan = ChannelState()
586            self.channels[channel] = chan
587        for (mode, value) in ircutils.separateModes(msg.args[2:]):
588            modeChar = mode[1]
589            if mode[0] == '+' and mode[1] not in 'ovh':
590                chan.setMode(modeChar, value)
591            elif mode[0] == '-' and mode[1] not in 'ovh':
592                chan.unsetMode(modeChar)
593
594    def do329(self, irc, msg):
595        # This is the last part of an empty mode.
596        channel = msg.args[1]
597        try:
598            chan = self.channels[channel]
599        except KeyError:
600            chan = ChannelState()
601            self.channels[channel] = chan
602        chan.created = int(msg.args[2])
603
604    def doPart(self, irc, msg):
605        for channel in msg.args[0].split(','):
606            try:
607                chan = self.channels[channel]
608            except KeyError:
609                continue
610            if ircutils.strEqual(msg.nick, irc.nick):
611                del self.channels[channel]
612            else:
613                chan.removeUser(msg.nick)
614
615    def doKick(self, irc, msg):
616        (channel, users) = msg.args[:2]
617        chan = self.channels[channel]
618        for user in users.split(','):
619            if ircutils.strEqual(user, irc.nick):
620                del self.channels[channel]
621                return
622            else:
623                chan.removeUser(user)
624
625    def doQuit(self, irc, msg):
626        channel_names = ircutils.IrcSet()
627        for (name, channel) in self.channels.items():
628            if msg.nick in channel.users:
629                channel_names.add(name)
630                channel.removeUser(msg.nick)
631        # Remember which channels the user was on
632        msg.tag('channels', channel_names)
633        if msg.nick in self.nicksToHostmasks:
634            # If we're quitting, it may not be.
635            del self.nicksToHostmasks[msg.nick]
636
637    def doTopic(self, irc, msg):
638        if len(msg.args) == 1:
639            return # Empty TOPIC for information.  Does not affect state.
640        try:
641            chan = self.channels[msg.args[0]]
642            chan.topic = msg.args[1]
643        except KeyError:
644            pass # We don't have to be in a channel to send a TOPIC.
645
646    def do332(self, irc, msg):
647        chan = self.channels[msg.args[1]]
648        chan.topic = msg.args[2]
649
650    def doNick(self, irc, msg):
651        newNick = msg.args[0]
652        oldNick = msg.nick
653        try:
654            if msg.user and msg.host:
655                # Nick messages being handed out from the bot itself won't
656                # have the necessary prefix to make a hostmask.
657                newHostmask = ircutils.joinHostmask(newNick,msg.user,msg.host)
658                self.nicksToHostmasks[newNick] = newHostmask
659            del self.nicksToHostmasks[oldNick]
660        except KeyError:
661            pass
662        channel_names = ircutils.IrcSet()
663        for (name, channel) in self.channels.items():
664            if msg.nick in channel.users:
665                channel_names.add(name)
666            channel.replaceUser(oldNick, newNick)
667        msg.tag('channels', channel_names)
668
669    def doBatch(self, irc, msg):
670        batch_name = msg.args[0][1:]
671        if msg.args[0].startswith('+'):
672            batch_type = msg.args[1]
673            batch_arguments = tuple(msg.args[2:])
674            self.batches[batch_name] = Batch(type=batch_type,
675                    arguments=batch_arguments, messages=[])
676        elif msg.args[0].startswith('-'):
677            batch = self.batches.pop(batch_name)
678            msg.tag('batch', batch)
679        else:
680            assert False, msg.args[0]
681
682    def doAway(self, irc, msg):
683        channel_names = ircutils.IrcSet()
684        for (name, channel) in self.channels.items():
685            if msg.nick in channel.users:
686                channel_names.add(name)
687        msg.tag('channels', channel_names)
688
689
690###
691# The basic class for handling a connection to an IRC server.  Accepts
692# callbacks of the IrcCallback interface.  Public attributes include 'driver',
693# 'queue', and 'state', in addition to the standard nick/user/ident attributes.
694###
695_callbacks = []
696class Irc(IrcCommandDispatcher, log.Firewalled):
697    """The base class for an IRC connection.
698
699    Handles PING commands already.
700    """
701    __firewalled__ = {'die': None,
702                      'feedMsg': None,
703                      'takeMsg': None,}
704    _nickSetters = set(['001', '002', '003', '004', '250', '251', '252',
705                        '254', '255', '265', '266', '372', '375', '376',
706                        '333', '353', '332', '366', '005'])
707    # We specifically want these callbacks to be common between all Ircs,
708    # that's why we don't do the normal None default with a check.
709    def __init__(self, network, callbacks=_callbacks):
710        self.zombie = False
711        world.ircs.append(self)
712        self.network = network
713        self.startedAt = time.time()
714        self.callbacks = callbacks
715        self.state = IrcState()
716        self.queue = IrcMsgQueue()
717        self.fastqueue = smallqueue()
718        self.driver = None # The driver should set this later.
719        self._setNonResettingVariables()
720        self._queueConnectMessages()
721        self.startedSync = ircutils.IrcDict()
722        self.monitoring = ircutils.IrcDict()
723
724    def isChannel(self, s):
725        """Helper function to check whether a given string is a channel on
726        the network this Irc object is connected to."""
727        kw = {}
728        if 'chantypes' in self.state.supported:
729            kw['chantypes'] = self.state.supported['chantypes']
730        if 'channellen' in self.state.supported:
731            kw['channellen'] = self.state.supported['channellen']
732        return ircutils.isChannel(s, **kw)
733
734    def isNick(self, s):
735        kw = {}
736        if 'nicklen' in self.state.supported:
737            kw['nicklen'] = self.state.supported['nicklen']
738        return ircutils.isNick(s, **kw)
739
740    # This *isn't* threadsafe!
741    def addCallback(self, callback):
742        """Adds a callback to the callbacks list.
743
744        :param callback: A callback object
745        :type callback: supybot.irclib.IrcCallback
746        """
747        assert not self.getCallback(callback.name())
748        self.callbacks.append(callback)
749        # This is the new list we're building, which will be tsorted.
750        cbs = []
751        # The vertices are self.callbacks itself.  Now we make the edges.
752        edges = set()
753        for cb in self.callbacks:
754            (before, after) = cb.callPrecedence(self)
755            assert cb not in after, 'cb was in its own after.'
756            assert cb not in before, 'cb was in its own before.'
757            for otherCb in before:
758                edges.add((otherCb, cb))
759            for otherCb in after:
760                edges.add((cb, otherCb))
761        def getFirsts():
762            firsts = set(self.callbacks) - set(cbs)
763            for (before, after) in edges:
764                firsts.discard(after)
765            return firsts
766        firsts = getFirsts()
767        while firsts:
768            # Then we add these to our list of cbs, and remove all edges that
769            # originate with these cbs.
770            for cb in firsts:
771                cbs.append(cb)
772                edgesToRemove = []
773                for edge in edges:
774                    if edge[0] is cb:
775                        edgesToRemove.append(edge)
776                for edge in edgesToRemove:
777                    edges.remove(edge)
778            firsts = getFirsts()
779        assert len(cbs) == len(self.callbacks), \
780               'cbs: %s, self.callbacks: %s' % (cbs, self.callbacks)
781        self.callbacks[:] = cbs
782
783    def getCallback(self, name):
784        """Gets a given callback by name."""
785        name = name.lower()
786        for callback in self.callbacks:
787            if callback.name().lower() == name:
788                return callback
789        else:
790            return None
791
792    def removeCallback(self, name):
793        """Removes a callback from the callback list."""
794        name = name.lower()
795        def nameMatches(cb):
796            return cb.name().lower() == name
797        (bad, good) = utils.iter.partition(nameMatches, self.callbacks)
798        self.callbacks[:] = good
799        return bad
800
801    def queueMsg(self, msg):
802        """Queues a message to be sent to the server."""
803        if not self.zombie:
804            return self.queue.enqueue(msg)
805        else:
806            log.warning('Refusing to queue %r; %s is a zombie.', msg, self)
807            return False
808
809    def sendMsg(self, msg):
810        """Queues a message to be sent to the server *immediately*"""
811        if not self.zombie:
812            self.fastqueue.enqueue(msg)
813        else:
814            log.warning('Refusing to send %r; %s is a zombie.', msg, self)
815
816    def takeMsg(self):
817        """Called by the IrcDriver; takes a message to be sent."""
818        if not self.callbacks:
819            log.critical('No callbacks in %s.', self)
820        now = time.time()
821        msg = None
822        if self.fastqueue:
823            msg = self.fastqueue.dequeue()
824        elif self.queue:
825            if now-self.lastTake <= conf.supybot.protocols.irc.throttleTime():
826                log.debug('Irc.takeMsg throttling.')
827            else:
828                self.lastTake = now
829                msg = self.queue.dequeue()
830        elif self.afterConnect and \
831             conf.supybot.protocols.irc.ping() and \
832             now > self.lastping + conf.supybot.protocols.irc.ping.interval():
833            if self.outstandingPing:
834                s = 'Ping sent at %s not replied to.' % \
835                    log.timestamp(self.lastping)
836                log.warning(s)
837                self.feedMsg(ircmsgs.error(s))
838                self.driver.reconnect()
839            elif not self.zombie:
840                self.lastping = now
841                now = str(int(now))
842                self.outstandingPing = True
843                self.queueMsg(ircmsgs.ping(now))
844        if msg:
845            for callback in reversed(self.callbacks):
846                msg = callback.outFilter(self, msg)
847                if msg is None:
848                    log.debug('%s.outFilter returned None.', callback.name())
849                    return self.takeMsg()
850                world.debugFlush()
851            if len(str(msg)) > 512:
852                # Yes, this violates the contract, but at this point it doesn't
853                # matter.  That's why we gotta go munging in private attributes
854                #
855                # I'm changing this to a log.debug to fix a possible loop in
856                # the LogToIrc plugin.  Since users can't do anything about
857                # this issue, there's no fundamental reason to make it a
858                # warning.
859                log.debug('Truncating %r, message is too long.', msg)
860                msg._str = msg._str[:500] + '\r\n'
861                msg._len = len(str(msg))
862            # I don't think we should do this.  Why should it matter?  If it's
863            # something important, then the server will send it back to us,
864            # and if it's just a privmsg/notice/etc., we don't care.
865            # On second thought, we need this for testing.
866            if world.testing:
867                self.state.addMsg(self, msg)
868            log.debug('Outgoing message (%s): %s', self.network, str(msg).rstrip('\r\n'))
869            return msg
870        elif self.zombie:
871            # We kill the driver here so it doesn't continue to try to
872            # take messages from us.
873            self.driver.die()
874            self._reallyDie()
875        else:
876            return None
877
878    _numericErrorCommandRe = re.compile(r'^[45][0-9][0-9]$')
879    def feedMsg(self, msg):
880        """Called by the IrcDriver; feeds a message received."""
881        msg.tag('receivedBy', self)
882        msg.tag('receivedOn', self.network)
883        msg.tag('receivedAt', time.time())
884        if msg.args and self.isChannel(msg.args[0]):
885            channel = msg.args[0]
886        else:
887            channel = None
888        preInFilter = str(msg).rstrip('\r\n')
889        log.debug('Incoming message (%s): %s', self.network, preInFilter)
890
891        # Yeah, so this is odd.  Some networks (oftc) seem to give us certain
892        # messages with our nick instead of our prefix.  We'll fix that here.
893        if msg.prefix == self.nick:
894            log.debug('Got one of those odd nick-instead-of-prefix msgs.')
895            msg = ircmsgs.IrcMsg(prefix=self.prefix, msg=msg)
896
897        # This catches cases where we know our own nick (from sending it to the
898        # server) but we don't yet know our prefix.
899        if msg.nick == self.nick and self.prefix != msg.prefix:
900            self.prefix = msg.prefix
901
902        # This keeps our nick and server attributes updated.
903        if msg.command in self._nickSetters:
904            if msg.args[0] != self.nick:
905                self.nick = msg.args[0]
906                log.debug('Updating nick attribute to %s.', self.nick)
907            if msg.prefix != self.server:
908                self.server = msg.prefix
909                log.debug('Updating server attribute to %s.', self.server)
910
911        # Dispatch to specific handlers for commands.
912        method = self.dispatchCommand(msg.command)
913        if method is not None:
914            method(msg)
915        elif self._numericErrorCommandRe.search(msg.command):
916            log.error('Unhandled error message from server: %r' % msg)
917
918        # Now update the IrcState object.
919        try:
920            self.state.addMsg(self, msg)
921        except:
922            log.exception('Exception in update of IrcState object:')
923
924        # Now call the callbacks.
925        world.debugFlush()
926        for callback in self.callbacks:
927            try:
928                m = callback.inFilter(self, msg)
929                if not m:
930                    log.debug('%s.inFilter returned None', callback.name())
931                    return
932                msg = m
933            except:
934                log.exception('Uncaught exception in inFilter:')
935            world.debugFlush()
936        postInFilter = str(msg).rstrip('\r\n')
937        if postInFilter != preInFilter:
938            log.debug('Incoming message (post-inFilter): %s', postInFilter)
939        for callback in self.callbacks:
940            try:
941                if callback is not None:
942                    callback(self, msg)
943            except:
944                log.exception('Uncaught exception in callback:')
945            world.debugFlush()
946
947    def die(self):
948        """Makes the Irc object *promise* to die -- but it won't die (of its
949        own volition) until all its queues are clear.  Isn't that cool?"""
950        self.zombie = True
951        if not self.afterConnect:
952            self._reallyDie()
953
954    # This is useless because it's in world.ircs, so it won't be deleted until
955    # the program exits.  Just figured you might want to know.
956    #def __del__(self):
957    #    self._reallyDie()
958
959    def reset(self):
960        """Resets the Irc object.  Called when the driver reconnects."""
961        self._setNonResettingVariables()
962        self.state.reset()
963        self.queue.reset()
964        self.fastqueue.reset()
965        self.startedSync.clear()
966        for callback in self.callbacks:
967            callback.reset()
968        self._queueConnectMessages()
969
970    def _setNonResettingVariables(self):
971        # Configuration stuff.
972        network_config = conf.supybot.networks.get(self.network)
973        def get_value(name):
974            return getattr(network_config, name)() or \
975                getattr(conf.supybot, name)()
976        self.nick = get_value('nick')
977        # Expand variables like $version in realname.
978        self.user = ircutils.standardSubstitute(self, None, get_value('user'))
979        self.ident = get_value('ident')
980        self.alternateNicks = conf.supybot.nick.alternates()[:]
981        self.triedNicks = ircutils.IrcSet()
982        self.password = network_config.password()
983        self.prefix = '%s!%s@%s' % (self.nick, self.ident, 'unset.domain')
984        # The rest.
985        self.lastTake = 0
986        self.server = 'unset'
987        self.afterConnect = False
988        self.startedAt = time.time()
989        self.lastping = time.time()
990        self.outstandingPing = False
991        self.capNegociationEnded = False
992        self.requireStarttls = not network_config.ssl() and \
993                network_config.requireStarttls()
994        if self.requireStarttls:
995            log.error(('STARTTLS is no longer supported. Set '
996                'supybot.networks.%s.requireStarttls to False '
997                'to disable it, and use supybot.networks.%s.ssl '
998                'instead.') % (self.network, self.network))
999            self.driver.die()
1000            self._reallyDie()
1001            return
1002        self.resetSasl()
1003
1004    def resetSasl(self):
1005        network_config = conf.supybot.networks.get(self.network)
1006        self.sasl_authenticated = False
1007        self.sasl_username = network_config.sasl.username()
1008        self.sasl_password = network_config.sasl.password()
1009        self.sasl_ecdsa_key = network_config.sasl.ecdsa_key()
1010        self.sasl_scram_state = {'step': 'uninitialized'}
1011        self.authenticate_decoder = None
1012        self.sasl_next_mechanisms = []
1013        self.sasl_current_mechanism = None
1014
1015        for mechanism in network_config.sasl.mechanisms():
1016            if mechanism == 'ecdsa-nist256p-challenge' and \
1017                    ecdsa and self.sasl_username and self.sasl_ecdsa_key:
1018                self.sasl_next_mechanisms.append(mechanism)
1019            elif mechanism == 'external' and (
1020                    network_config.certfile() or
1021                    conf.supybot.protocols.irc.certfile()):
1022                self.sasl_next_mechanisms.append(mechanism)
1023            elif mechanism.startswith('scram-') and scram and \
1024                    self.sasl_username and self.sasl_password:
1025                self.sasl_next_mechanisms.append(mechanism)
1026            elif mechanism == 'plain' and \
1027                    self.sasl_username and self.sasl_password:
1028                self.sasl_next_mechanisms.append(mechanism)
1029
1030        if self.sasl_next_mechanisms:
1031            self.REQUEST_CAPABILITIES.add('sasl')
1032
1033
1034    REQUEST_CAPABILITIES = set(['account-notify', 'extended-join',
1035        'multi-prefix', 'metadata-notify', 'account-tag',
1036        'userhost-in-names', 'invite-notify', 'server-time',
1037        'chghost', 'batch', 'away-notify', 'message-tags'])
1038
1039    def _queueConnectMessages(self):
1040        if self.zombie:
1041            self.driver.die()
1042            self._reallyDie()
1043
1044            return
1045
1046        self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('LS', '302')))
1047
1048        self.sendAuthenticationMessages()
1049
1050    def sendAuthenticationMessages(self):
1051        # Notes:
1052        # * using sendMsg instead of queueMsg because these messages cannot
1053        #   be throttled.
1054
1055        if self.password:
1056            log.info('%s: Queuing PASS command, not logging the password.',
1057                     self.network)
1058            self.sendMsg(ircmsgs.password(self.password))
1059
1060        log.debug('%s: Sending NICK command, nick is %s.',
1061                  self.network, self.nick)
1062
1063        self.sendMsg(ircmsgs.nick(self.nick))
1064
1065        log.debug('%s: Sending USER command, ident is %s, user is %s.',
1066                  self.network, self.ident, self.user)
1067
1068        self.sendMsg(ircmsgs.user(self.ident, self.user))
1069
1070    def endCapabilityNegociation(self):
1071        if not self.capNegociationEnded:
1072            self.capNegociationEnded = True
1073            self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('END',)))
1074
1075    def sendSaslString(self, string):
1076        for chunk in ircutils.authenticate_generator(string):
1077            self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
1078                args=(chunk,)))
1079
1080    def tryNextSaslMechanism(self):
1081        if self.sasl_next_mechanisms:
1082            self.sasl_current_mechanism = self.sasl_next_mechanisms.pop(0)
1083            self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
1084                args=(self.sasl_current_mechanism.upper(),)))
1085        elif conf.supybot.networks.get(self.network).sasl.required():
1086            log.error('None of the configured SASL mechanisms succeeded, '
1087                    'aborting connection.')
1088        else:
1089            self.sasl_current_mechanism = None
1090            self.endCapabilityNegociation()
1091
1092    def filterSaslMechanisms(self, available):
1093        available = set(map(str.lower, available))
1094        self.sasl_next_mechanisms = [
1095                x for x in self.sasl_next_mechanisms
1096                if x.lower() in available]
1097
1098    def doAuthenticate(self, msg):
1099        if not self.authenticate_decoder:
1100            self.authenticate_decoder = ircutils.AuthenticateDecoder()
1101        self.authenticate_decoder.feed(msg)
1102        if not self.authenticate_decoder.ready:
1103            return # Waiting for other messages
1104        string = self.authenticate_decoder.get()
1105        self.authenticate_decoder = None
1106
1107        mechanism = self.sasl_current_mechanism
1108        if mechanism == 'ecdsa-nist256p-challenge':
1109            self.doAuthenticateEcdsa(string)
1110        elif mechanism == 'external':
1111            self.sendSaslString(b'')
1112        elif mechanism.startswith('scram-'):
1113            step = self.sasl_scram_state['step']
1114            try:
1115                if step == 'uninitialized':
1116                    log.debug('%s: starting SCRAM.',
1117                            self.network)
1118                    self.doAuthenticateScramFirst(mechanism)
1119                elif step == 'first-sent':
1120                    log.debug('%s: received SCRAM challenge.',
1121                            self.network)
1122                    self.doAuthenticateScramChallenge(string)
1123                elif step == 'final-sent':
1124                    log.debug('%s: finishing SCRAM.',
1125                            self.network)
1126                    self.doAuthenticateScramFinish(string)
1127                else:
1128                    assert False
1129            except scram.ScramException:
1130                self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
1131                    args=('*',)))
1132                self.tryNextSaslMechanism()
1133        elif mechanism == 'plain':
1134            authstring = b'\0'.join([
1135                self.sasl_username.encode('utf-8'),
1136                self.sasl_username.encode('utf-8'),
1137                self.sasl_password.encode('utf-8'),
1138            ])
1139            self.sendSaslString(authstring)
1140
1141    def doAuthenticateEcdsa(self, string):
1142        if string == b'':
1143            self.sendSaslString(self.sasl_username.encode('utf-8'))
1144            return
1145        try:
1146            with open(self.sasl_ecdsa_key) as fd:
1147                private_key = ecdsa.SigningKey.from_pem(fd.read())
1148            authstring = private_key.sign(string)
1149            self.sendSaslString(authstring)
1150        except (ecdsa.BadDigestError, OSError, ValueError):
1151            self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
1152                args=('*',)))
1153            self.tryNextSaslMechanism()
1154
1155    def doAuthenticateScramFirst(self, mechanism):
1156        """Handle sending the client-first message of SCRAM auth."""
1157        hash_name = mechanism[len('scram-'):]
1158        if hash_name.endswith('-plus'):
1159            hash_name = hash_name[:-len('-plus')]
1160        hash_name = hash_name.upper()
1161        if hash_name not in scram.HASH_FACTORIES:
1162            log.debug('%s: SCRAM hash %r not supported, aborting.',
1163                    self.network, hash_name)
1164            self.tryNextSaslMechanism()
1165            return
1166        authenticator = scram.SCRAMClientAuthenticator(hash_name,
1167                channel_binding=False)
1168        self.sasl_scram_state['authenticator'] = authenticator
1169        client_first = authenticator.start({
1170            'username': self.sasl_username,
1171            'password': self.sasl_password,
1172            })
1173        self.sendSaslString(client_first)
1174        self.sasl_scram_state['step'] = 'first-sent'
1175
1176    def doAuthenticateScramChallenge(self, challenge):
1177        client_final = self.sasl_scram_state['authenticator'] \
1178                .challenge(challenge)
1179        self.sendSaslString(client_final)
1180        self.sasl_scram_state['step'] = 'final-sent'
1181
1182    def doAuthenticateScramFinish(self, data):
1183        try:
1184            res = self.sasl_scram_state['authenticator'] \
1185                    .finish(data)
1186        except scram.BadSuccessException as e:
1187            log.warning('%s: SASL authentication failed with SCRAM error: %e',
1188                    self.network, e)
1189            self.tryNextSaslMechanism()
1190        else:
1191            self.sendSaslString(b'')
1192            self.sasl_scram_state['step'] = 'authenticated'
1193
1194    def do903(self, msg):
1195        log.info('%s: SASL authentication successful', self.network)
1196        self.sasl_authenticated = True
1197        self.endCapabilityNegociation()
1198
1199    def do904(self, msg):
1200        log.warning('%s: SASL authentication failed', self.network)
1201        self.tryNextSaslMechanism()
1202
1203    def do905(self, msg):
1204        log.warning('%s: SASL authentication failed because the username or '
1205                    'password is too long.', self.network)
1206        self.tryNextSaslMechanism()
1207
1208    def do906(self, msg):
1209        log.warning('%s: SASL authentication aborted', self.network)
1210        self.tryNextSaslMechanism()
1211
1212    def do907(self, msg):
1213        log.warning('%s: Attempted SASL authentication when we were already '
1214                    'authenticated.', self.network)
1215        self.tryNextSaslMechanism()
1216
1217    def do908(self, msg):
1218        log.info('%s: Supported SASL mechanisms: %s',
1219                 self.network, msg.args[1])
1220        self.filterSaslMechanisms(set(msg.args[1].split(',')))
1221
1222    def doCap(self, msg):
1223        subcommand = msg.args[1]
1224        if subcommand == 'ACK':
1225            self.doCapAck(msg)
1226        elif subcommand == 'NAK':
1227            self.doCapNak(msg)
1228        elif subcommand == 'LS':
1229            self.doCapLs(msg)
1230        elif subcommand == 'DEL':
1231            self.doCapDel(msg)
1232        elif subcommand == 'NEW':
1233            self.doCapNew(msg)
1234    def doCapAck(self, msg):
1235        if len(msg.args) != 3:
1236            log.warning('Bad CAP ACK from server: %r', msg)
1237            return
1238        caps = msg.args[2].split()
1239        assert caps, 'Empty list of capabilities'
1240        log.debug('%s: Server acknowledged capabilities: %L',
1241                 self.network, caps)
1242        self.state.capabilities_ack.update(caps)
1243
1244        if 'sasl' in caps:
1245            self.tryNextSaslMechanism()
1246        else:
1247            self.endCapabilityNegociation()
1248    def doCapNak(self, msg):
1249        if len(msg.args) != 3:
1250            log.warning('Bad CAP NAK from server: %r', msg)
1251            return
1252        caps = msg.args[2].split()
1253        assert caps, 'Empty list of capabilities'
1254        self.state.capabilities_nak.update(caps)
1255        log.warning('%s: Server refused capabilities: %L',
1256                    self.network, caps)
1257        self.endCapabilityNegociation()
1258    def _addCapabilities(self, capstring):
1259        for item in capstring.split():
1260            while item.startswith(('=', '~')):
1261                item = item[1:]
1262            if '=' in item:
1263                (cap, value) = item.split('=', 1)
1264                self.state.capabilities_ls[cap] = value
1265            else:
1266                self.state.capabilities_ls[item] = None
1267    def doCapLs(self, msg):
1268        if len(msg.args) == 4:
1269            # Multi-line LS
1270            if msg.args[2] != '*':
1271                log.warning('Bad CAP LS from server: %r', msg)
1272                return
1273            self._addCapabilities(msg.args[3])
1274        elif len(msg.args) == 3: # End of LS
1275            self._addCapabilities(msg.args[2])
1276            common_supported_capabilities = set(self.state.capabilities_ls) & \
1277                    self.REQUEST_CAPABILITIES
1278            if 'sasl' in self.state.capabilities_ls:
1279                s = self.state.capabilities_ls['sasl']
1280                if s is not None:
1281                    self.filterSaslMechanisms(set(s.split(',')))
1282            # NOTE: Capabilities are requested in alphabetic order, because
1283            # sets are unordered, and their "order" is nondeterministic.
1284            # This is needed for the tests.
1285            if common_supported_capabilities:
1286                caps = ' '.join(sorted(common_supported_capabilities))
1287                self.sendMsg(ircmsgs.IrcMsg(command='CAP',
1288                    args=('REQ', caps)))
1289            else:
1290                self.endCapabilityNegociation()
1291        else:
1292            log.warning('Bad CAP LS from server: %r', msg)
1293            return
1294    def doCapDel(self, msg):
1295        if len(msg.args) != 3:
1296            log.warning('Bad CAP DEL from server: %r', msg)
1297            return
1298        caps = msg.args[2].split()
1299        assert caps, 'Empty list of capabilities'
1300        for cap in caps:
1301            # The spec says "If capability negotiation 3.2 was used, extensions
1302            # listed MAY contain values." for CAP NEW and CAP DEL
1303            cap = cap.split('=')[0]
1304            try:
1305                del self.state.capabilities_ls[cap]
1306            except KeyError:
1307                pass
1308            try:
1309                self.state.capabilities_ack.remove(cap)
1310            except KeyError:
1311                pass
1312    def doCapNew(self, msg):
1313        if len(msg.args) != 3:
1314            log.warning('Bad CAP NEW from server: %r', msg)
1315            return
1316        caps = msg.args[2].split()
1317        assert caps, 'Empty list of capabilities'
1318        self._addCapabilities(msg.args[2])
1319        if not self.sasl_authenticated and 'sasl' in self.state.capabilities_ls:
1320            self.resetSasl()
1321            s = self.state.capabilities_ls['sasl']
1322            if s is not None:
1323                self.filterSaslMechanisms(set(s.split(',')))
1324        common_supported_unrequested_capabilities = (
1325                set(self.state.capabilities_ls) &
1326                self.REQUEST_CAPABILITIES -
1327                self.state.capabilities_ack)
1328        if common_supported_unrequested_capabilities:
1329            caps = ' '.join(sorted(common_supported_unrequested_capabilities))
1330            self.sendMsg(ircmsgs.IrcMsg(command='CAP',
1331                args=('REQ', caps)))
1332
1333    def monitor(self, targets):
1334        """Increment a counter of how many callbacks monitor each target;
1335        and send a MONITOR + to the server if the target is not yet
1336        monitored."""
1337        if isinstance(targets, str):
1338            targets = [targets]
1339        not_yet_monitored = set()
1340        for target in targets:
1341            if target in self.monitoring:
1342                self.monitoring[target] += 1
1343            else:
1344                not_yet_monitored.add(target)
1345                self.monitoring[target] = 1
1346        if not_yet_monitored:
1347            self.queueMsg(ircmsgs.monitor('+', not_yet_monitored))
1348        return not_yet_monitored
1349
1350    def unmonitor(self, targets):
1351        """Decrements a counter of how many callbacks monitor each target;
1352        and send a MONITOR - to the server if the counter drops to 0."""
1353        if isinstance(targets, str):
1354            targets = [targets]
1355        should_be_unmonitored = set()
1356        for target in targets:
1357            self.monitoring[target] -= 1
1358            if self.monitoring[target] == 0:
1359                del self.monitoring[target]
1360                should_be_unmonitored.add(target)
1361        if should_be_unmonitored:
1362            self.queueMsg(ircmsgs.monitor('-', should_be_unmonitored))
1363        return should_be_unmonitored
1364
1365    def _getNextNick(self):
1366        if self.alternateNicks:
1367            nick = self.alternateNicks.pop(0)
1368            if '%s' in nick:
1369                network_nick = conf.supybot.networks.get(self.network).nick()
1370                if network_nick == '':
1371                    nick %= conf.supybot.nick()
1372                else:
1373                    nick %= network_nick
1374            if nick not in self.triedNicks:
1375                self.triedNicks.add(nick)
1376                return nick
1377
1378        nick = conf.supybot.nick()
1379        network_nick = conf.supybot.networks.get(self.network).nick()
1380        if network_nick != '':
1381            nick = network_nick
1382        ret = nick
1383        L = list(nick)
1384        while len(L) <= 3:
1385            L.append('`')
1386        while ret in self.triedNicks:
1387            L[random.randrange(len(L))] = utils.iter.choice('0123456789')
1388            ret = ''.join(L)
1389        self.triedNicks.add(ret)
1390        return ret
1391
1392    def do002(self, msg):
1393        """Logs the ircd version."""
1394        (beginning, version) = rsplit(msg.args[-1], maxsplit=1)
1395        log.info('Server %s has version %s', self.server, version)
1396
1397    def doPing(self, msg):
1398        """Handles PING messages."""
1399        self.sendMsg(ircmsgs.pong(msg.args[0]))
1400
1401    def doPong(self, msg):
1402        """Handles PONG messages."""
1403        self.outstandingPing = False
1404
1405    def do376(self, msg):
1406        log.info('Got end of MOTD from %s', self.server)
1407        self.afterConnect = True
1408        # Let's reset nicks in case we had to use a weird one.
1409        self.alternateNicks = conf.supybot.nick.alternates()[:]
1410        umodes = conf.supybot.networks.get(self.network).umodes()
1411        if umodes == '':
1412            umodes = conf.supybot.protocols.irc.umodes()
1413        supported = self.state.supported.get('umodes')
1414        if supported:
1415            acceptedchars = supported.union('+-')
1416            umodes = ''.join([m for m in umodes if m in acceptedchars])
1417        if umodes:
1418            log.info('Sending user modes to %s: %s', self.network, umodes)
1419            self.sendMsg(ircmsgs.mode(self.nick, umodes))
1420    do377 = do422 = do376
1421
1422    def do43x(self, msg, problem):
1423        if not self.afterConnect:
1424            newNick = self._getNextNick()
1425            assert newNick != self.nick
1426            log.info('Got %s: %s %s.  Trying %s.',
1427                     msg.command, self.nick, problem, newNick)
1428            self.sendMsg(ircmsgs.nick(newNick))
1429    def do437(self, msg):
1430        self.do43x(msg, 'is temporarily unavailable')
1431    def do433(self, msg):
1432        self.do43x(msg, 'is in use')
1433    def do432(self, msg):
1434        self.do43x(msg, 'is not a valid nickname')
1435
1436    def doJoin(self, msg):
1437        if msg.nick == self.nick:
1438            channel = msg.args[0]
1439            self.queueMsg(ircmsgs.who(channel, args=('%tuhnairf,1',))) # Ends with 315.
1440            self.queueMsg(ircmsgs.mode(channel)) # Ends with 329.
1441            for channel in msg.args[0].split(','):
1442                self.queueMsg(ircmsgs.mode(channel, '+b'))
1443            self.startedSync[channel] = time.time()
1444
1445    def do315(self, msg):
1446        channel = msg.args[1]
1447        if channel in self.startedSync:
1448            now = time.time()
1449            started = self.startedSync.pop(channel)
1450            elapsed = now - started
1451            log.info('Join to %s on %s synced in %.2f seconds.',
1452                     channel, self.network, elapsed)
1453
1454    def doError(self, msg):
1455        """Handles ERROR messages."""
1456        log.warning('Error message from %s: %s', self.network, msg.args[0])
1457        if not self.zombie:
1458           if msg.args[0].lower().startswith('closing link'):
1459              self.driver.reconnect()
1460           elif 'too fast' in msg.args[0]: # Connecting too fast.
1461              self.driver.reconnect(wait=True)
1462
1463    def doNick(self, msg):
1464        """Handles NICK messages."""
1465        if msg.nick == self.nick:
1466            newNick = msg.args[0]
1467            self.nick = newNick
1468            (nick, user, domain) = ircutils.splitHostmask(msg.prefix)
1469            self.prefix = ircutils.joinHostmask(self.nick, user, domain)
1470        elif conf.supybot.followIdentificationThroughNickChanges():
1471            # We use elif here because this means it's someone else's nick
1472            # change, not our own.
1473            try:
1474                id = ircdb.users.getUserId(msg.prefix)
1475                u = ircdb.users.getUser(id)
1476            except KeyError:
1477                return
1478            if u.auth:
1479                (_, user, host) = ircutils.splitHostmask(msg.prefix)
1480                newhostmask = ircutils.joinHostmask(msg.args[0], user, host)
1481                for (i, (when, authmask)) in enumerate(u.auth[:]):
1482                    if ircutils.strEqual(msg.prefix, authmask):
1483                        log.info('Following identification for %s: %s -> %s',
1484                                 u.name, authmask, newhostmask)
1485                        u.auth[i] = (u.auth[i][0], newhostmask)
1486                        ircdb.users.setUser(u)
1487
1488    def _reallyDie(self):
1489        """Makes the Irc object die.  Dead."""
1490        log.info('Irc object for %s dying.', self.network)
1491        # XXX This hasattr should be removed, I'm just putting it here because
1492        #     we're so close to a release.  After 0.80.0 we should remove this
1493        #     and fix whatever AttributeErrors arise in the drivers themselves.
1494        if self.driver is not None and hasattr(self.driver, 'die'):
1495            self.driver.die()
1496        if self in world.ircs:
1497            world.ircs.remove(self)
1498            # Only kill the callbacks if we're the last Irc.
1499            if not world.ircs:
1500                for cb in self.callbacks:
1501                    cb.die()
1502                # If we shared our list of callbacks, this ensures that
1503                # cb.die() is only called once for each callback.  It's
1504                # not really necessary since we already check to make sure
1505                # we're the only Irc object, but a little robustitude never
1506                # hurt anybody.
1507                log.debug('Last Irc, clearing callbacks.')
1508                self.callbacks[:] = []
1509        else:
1510            log.warning('Irc object killed twice: %s', utils.stackTrace())
1511
1512    def __hash__(self):
1513        return id(self)
1514
1515    def __eq__(self, other):
1516        # We check isinstance here, so that if some proxy object (like those
1517        # defined in callbacks.py) has overridden __eq__, it takes precedence.
1518        if isinstance(other, self.__class__):
1519            return id(self) == id(other)
1520        else:
1521            return other.__eq__(self)
1522
1523    def __ne__(self, other):
1524        return not (self == other)
1525
1526    def __str__(self):
1527        return 'Irc object for %s' % self.network
1528
1529    def __repr__(self):
1530        return '<irclib.Irc object for %s>' % self.network
1531
1532
1533# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
1534