root/trunk/twisted/words/protocols/irc.py

Revision 33787, 100.7 KB (checked in by cyli, 2 months ago)

Merge dccchatfactory-protocol-3179: Return protocol in t.w.protocols.irc.DccChatFactory.buildProtocol

Author: Nagyman
Reviewer: cyli
Fixes: #3179

Fix bug where the protocol was not returned from twisted.words.protocols.irc.DccChatFactory.buildProtocol

Line 
1# -*- test-case-name: twisted.words.test.test_irc -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Internet Relay Chat Protocol for client and server.
7
8Future Plans
9============
10
11The way the IRCClient class works here encourages people to implement
12IRC clients by subclassing the ephemeral protocol class, and it tends
13to end up with way more state than it should for an object which will
14be destroyed as soon as the TCP transport drops.  Someone oughta do
15something about that, ya know?
16
17The DCC support needs to have more hooks for the client for it to be
18able to ask the user things like "Do you want to accept this session?"
19and "Transfer #2 is 67% done." and otherwise manage the DCC sessions.
20
21Test coverage needs to be better.
22
23@var MAX_COMMAND_LENGTH: The maximum length of a command, as defined by RFC
24    2812 section 2.3.
25
26@author: Kevin Turner
27
28@see: RFC 1459: Internet Relay Chat Protocol
29@see: RFC 2812: Internet Relay Chat: Client Protocol
30@see: U{The Client-To-Client-Protocol
31<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
32"""
33
34import errno, os, random, re, stat, struct, sys, time, types, traceback
35import string, socket
36import warnings
37import textwrap
38from os import path
39
40from twisted.internet import reactor, protocol, task
41from twisted.persisted import styles
42from twisted.protocols import basic
43from twisted.python import log, reflect, text
44from twisted.python.compat import set
45
46NUL = chr(0)
47CR = chr(015)
48NL = chr(012)
49LF = NL
50SPC = chr(040)
51
52# This includes the CRLF terminator characters.
53MAX_COMMAND_LENGTH = 512
54
55CHANNEL_PREFIXES = '&#!+'
56
57class IRCBadMessage(Exception):
58    pass
59
60class IRCPasswordMismatch(Exception):
61    pass
62
63
64
65class IRCBadModes(ValueError):
66    """
67    A malformed mode was encountered while attempting to parse a mode string.
68    """
69
70
71
72def parsemsg(s):
73    """Breaks a message from an IRC server into its prefix, command, and arguments.
74    """
75    prefix = ''
76    trailing = []
77    if not s:
78        raise IRCBadMessage("Empty line.")
79    if s[0] == ':':
80        prefix, s = s[1:].split(' ', 1)
81    if s.find(' :') != -1:
82        s, trailing = s.split(' :', 1)
83        args = s.split()
84        args.append(trailing)
85    else:
86        args = s.split()
87    command = args.pop(0)
88    return prefix, command, args
89
90
91
92def split(str, length=80):
93    """
94    Split a string into multiple lines.
95
96    Whitespace near C{str[length]} will be preferred as a breaking point.
97    C{"\\n"} will also be used as a breaking point.
98
99    @param str: The string to split.
100    @type str: C{str}
101
102    @param length: The maximum length which will be allowed for any string in
103        the result.
104    @type length: C{int}
105
106    @return: C{list} of C{str}
107    """
108    return [chunk
109            for line in str.split('\n')
110            for chunk in textwrap.wrap(line, length)]
111
112
113def _intOrDefault(value, default=None):
114    """
115    Convert a value to an integer if possible.
116
117    @rtype: C{int} or type of L{default}
118    @return: An integer when C{value} can be converted to an integer,
119        otherwise return C{default}
120    """
121    if value:
122        try:
123            return int(value)
124        except (TypeError, ValueError):
125            pass
126    return default
127
128
129
130class UnhandledCommand(RuntimeError):
131    """
132    A command dispatcher could not locate an appropriate command handler.
133    """
134
135
136
137class _CommandDispatcherMixin(object):
138    """
139    Dispatch commands to handlers based on their name.
140
141    Command handler names should be of the form C{prefix_commandName},
142    where C{prefix} is the value specified by L{prefix}, and must
143    accept the parameters as given to L{dispatch}.
144
145    Attempting to mix this in more than once for a single class will cause
146    strange behaviour, due to L{prefix} being overwritten.
147
148    @type prefix: C{str}
149    @ivar prefix: Command handler prefix, used to locate handler attributes
150    """
151    prefix = None
152
153    def dispatch(self, commandName, *args):
154        """
155        Perform actual command dispatch.
156        """
157        def _getMethodName(command):
158            return '%s_%s' % (self.prefix, command)
159
160        def _getMethod(name):
161            return getattr(self, _getMethodName(name), None)
162
163        method = _getMethod(commandName)
164        if method is not None:
165            return method(*args)
166
167        method = _getMethod('unknown')
168        if method is None:
169            raise UnhandledCommand("No handler for %r could be found" % (_getMethodName(commandName),))
170        return method(commandName, *args)
171
172
173
174
175
176def parseModes(modes, params, paramModes=('', '')):
177    """
178    Parse an IRC mode string.
179
180    The mode string is parsed into two lists of mode changes (added and
181    removed), with each mode change represented as C{(mode, param)} where mode
182    is the mode character, and param is the parameter passed for that mode, or
183    C{None} if no parameter is required.
184
185    @type modes: C{str}
186    @param modes: Modes string to parse.
187
188    @type params: C{list}
189    @param params: Parameters specified along with L{modes}.
190
191    @type paramModes: C{(str, str)}
192    @param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take
193        parameters when added or removed.
194
195    @returns: Two lists of mode changes, one for modes added and the other for
196        modes removed respectively, mode changes in each list are represented as
197        C{(mode, param)}.
198    """
199    if len(modes) == 0:
200        raise IRCBadModes('Empty mode string')
201
202    if modes[0] not in '+-':
203        raise IRCBadModes('Malformed modes string: %r' % (modes,))
204
205    changes = ([], [])
206
207    direction = None
208    count = -1
209    for ch in modes:
210        if ch in '+-':
211            if count == 0:
212                raise IRCBadModes('Empty mode sequence: %r' % (modes,))
213            direction = '+-'.index(ch)
214            count = 0
215        else:
216            param = None
217            if ch in paramModes[direction]:
218                try:
219                    param = params.pop(0)
220                except IndexError:
221                    raise IRCBadModes('Not enough parameters: %r' % (ch,))
222            changes[direction].append((ch, param))
223            count += 1
224
225    if len(params) > 0:
226        raise IRCBadModes('Too many parameters: %r %r' % (modes, params))
227
228    if count == 0:
229        raise IRCBadModes('Empty mode sequence: %r' % (modes,))
230
231    return changes
232
233
234
235class IRC(protocol.Protocol):
236    """
237    Internet Relay Chat server protocol.
238    """
239
240    buffer = ""
241    hostname = None
242
243    encoding = None
244
245    def connectionMade(self):
246        self.channels = []
247        if self.hostname is None:
248            self.hostname = socket.getfqdn()
249
250
251    def sendLine(self, line):
252        if self.encoding is not None:
253            if isinstance(line, unicode):
254                line = line.encode(self.encoding)
255        self.transport.write("%s%s%s" % (line, CR, LF))
256
257
258    def sendMessage(self, command, *parameter_list, **prefix):
259        """
260        Send a line formatted as an IRC message.
261
262        First argument is the command, all subsequent arguments are parameters
263        to that command.  If a prefix is desired, it may be specified with the
264        keyword argument 'prefix'.
265        """
266
267        if not command:
268            raise ValueError, "IRC message requires a command."
269
270        if ' ' in command or command[0] == ':':
271            # Not the ONLY way to screw up, but provides a little
272            # sanity checking to catch likely dumb mistakes.
273            raise ValueError, "Somebody screwed up, 'cuz this doesn't" \
274                  " look like a command to me: %s" % command
275
276        line = string.join([command] + list(parameter_list))
277        if prefix.has_key('prefix'):
278            line = ":%s %s" % (prefix['prefix'], line)
279        self.sendLine(line)
280
281        if len(parameter_list) > 15:
282            log.msg("Message has %d parameters (RFC allows 15):\n%s" %
283                    (len(parameter_list), line))
284
285
286    def dataReceived(self, data):
287        """
288        This hack is to support mIRC, which sends LF only, even though the RFC
289        says CRLF.  (Also, the flexibility of LineReceiver to turn "line mode"
290        on and off was not required.)
291        """
292        lines = (self.buffer + data).split(LF)
293        # Put the (possibly empty) element after the last LF back in the
294        # buffer
295        self.buffer = lines.pop()
296
297        for line in lines:
298            if len(line) <= 2:
299                # This is a blank line, at best.
300                continue
301            if line[-1] == CR:
302                line = line[:-1]
303            prefix, command, params = parsemsg(line)
304            # mIRC is a big pile of doo-doo
305            command = command.upper()
306            # DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
307
308            self.handleCommand(command, prefix, params)
309
310
311    def handleCommand(self, command, prefix, params):
312        """
313        Determine the function to call for the given command and call it with
314        the given arguments.
315        """
316        method = getattr(self, "irc_%s" % command, None)
317        try:
318            if method is not None:
319                method(prefix, params)
320            else:
321                self.irc_unknown(prefix, command, params)
322        except:
323            log.deferr()
324
325
326    def irc_unknown(self, prefix, command, params):
327        """
328        Called by L{handleCommand} on a command that doesn't have a defined
329        handler. Subclasses should override this method.
330        """
331        raise NotImplementedError(command, prefix, params)
332
333
334    # Helper methods
335    def privmsg(self, sender, recip, message):
336        """
337        Send a message to a channel or user
338
339        @type sender: C{str} or C{unicode}
340        @param sender: Who is sending this message.  Should be of the form
341            username!ident@hostmask (unless you know better!).
342
343        @type recip: C{str} or C{unicode}
344        @param recip: The recipient of this message.  If a channel, it must
345            start with a channel prefix.
346
347        @type message: C{str} or C{unicode}
348        @param message: The message being sent.
349        """
350        self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message)))
351
352
353    def notice(self, sender, recip, message):
354        """
355        Send a "notice" to a channel or user.
356
357        Notices differ from privmsgs in that the RFC claims they are different.
358        Robots are supposed to send notices and not respond to them.  Clients
359        typically display notices differently from privmsgs.
360
361        @type sender: C{str} or C{unicode}
362        @param sender: Who is sending this message.  Should be of the form
363            username!ident@hostmask (unless you know better!).
364
365        @type recip: C{str} or C{unicode}
366        @param recip: The recipient of this message.  If a channel, it must
367            start with a channel prefix.
368
369        @type message: C{str} or C{unicode}
370        @param message: The message being sent.
371        """
372        self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message))
373
374
375    def action(self, sender, recip, message):
376        """
377        Send an action to a channel or user.
378
379        @type sender: C{str} or C{unicode}
380        @param sender: Who is sending this message.  Should be of the form
381            username!ident@hostmask (unless you know better!).
382
383        @type recip: C{str} or C{unicode}
384        @param recip: The recipient of this message.  If a channel, it must
385            start with a channel prefix.
386
387        @type message: C{str} or C{unicode}
388        @param message: The action being sent.
389        """
390        self.sendLine(":%s ACTION %s :%s" % (sender, recip, message))
391
392
393    def topic(self, user, channel, topic, author=None):
394        """
395        Send the topic to a user.
396
397        @type user: C{str} or C{unicode}
398        @param user: The user receiving the topic.  Only their nick name, not
399            the full hostmask.
400
401        @type channel: C{str} or C{unicode}
402        @param channel: The channel for which this is the topic.
403
404        @type topic: C{str} or C{unicode} or C{None}
405        @param topic: The topic string, unquoted, or None if there is no topic.
406
407        @type author: C{str} or C{unicode}
408        @param author: If the topic is being changed, the full username and
409            hostmask of the person changing it.
410        """
411        if author is None:
412            if topic is None:
413                self.sendLine(':%s %s %s %s :%s' % (
414                    self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.'))
415            else:
416                self.sendLine(":%s %s %s %s :%s" % (
417                    self.hostname, RPL_TOPIC, user, channel, lowQuote(topic)))
418        else:
419            self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic)))
420
421
422    def topicAuthor(self, user, channel, author, date):
423        """
424        Send the author of and time at which a topic was set for the given
425        channel.
426
427        This sends a 333 reply message, which is not part of the IRC RFC.
428
429        @type user: C{str} or C{unicode}
430        @param user: The user receiving the topic.  Only their nick name, not
431            the full hostmask.
432
433        @type channel: C{str} or C{unicode}
434        @param channel: The channel for which this information is relevant.
435
436        @type author: C{str} or C{unicode}
437        @param author: The nickname (without hostmask) of the user who last set
438            the topic.
439
440        @type date: C{int}
441        @param date: A POSIX timestamp (number of seconds since the epoch) at
442            which the topic was last set.
443        """
444        self.sendLine(':%s %d %s %s %s %d' % (
445            self.hostname, 333, user, channel, author, date))
446
447
448    def names(self, user, channel, names):
449        """
450        Send the names of a channel's participants to a user.
451
452        @type user: C{str} or C{unicode}
453        @param user: The user receiving the name list.  Only their nick name,
454            not the full hostmask.
455
456        @type channel: C{str} or C{unicode}
457        @param channel: The channel for which this is the namelist.
458
459        @type names: C{list} of C{str} or C{unicode}
460        @param names: The names to send.
461        """
462        # XXX If unicode is given, these limits are not quite correct
463        prefixLength = len(channel) + len(user) + 10
464        namesLength = 512 - prefixLength
465
466        L = []
467        count = 0
468        for n in names:
469            if count + len(n) + 1 > namesLength:
470                self.sendLine(":%s %s %s = %s :%s" % (
471                    self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
472                L = [n]
473                count = len(n)
474            else:
475                L.append(n)
476                count += len(n) + 1
477        if L:
478            self.sendLine(":%s %s %s = %s :%s" % (
479                self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
480        self.sendLine(":%s %s %s %s :End of /NAMES list" % (
481            self.hostname, RPL_ENDOFNAMES, user, channel))
482
483
484    def who(self, user, channel, memberInfo):
485        """
486        Send a list of users participating in a channel.
487
488        @type user: C{str} or C{unicode}
489        @param user: The user receiving this member information.  Only their
490            nick name, not the full hostmask.
491
492        @type channel: C{str} or C{unicode}
493        @param channel: The channel for which this is the member information.
494
495        @type memberInfo: C{list} of C{tuples}
496        @param memberInfo: For each member of the given channel, a 7-tuple
497            containing their username, their hostmask, the server to which they
498            are connected, their nickname, the letter "H" or "G" (standing for
499            "Here" or "Gone"), the hopcount from C{user} to this member, and
500            this member's real name.
501        """
502        for info in memberInfo:
503            (username, hostmask, server, nickname, flag, hops, realName) = info
504            assert flag in ("H", "G")
505            self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % (
506                self.hostname, RPL_WHOREPLY, user, channel,
507                username, hostmask, server, nickname, flag, hops, realName))
508
509        self.sendLine(":%s %s %s %s :End of /WHO list." % (
510            self.hostname, RPL_ENDOFWHO, user, channel))
511
512
513    def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels):
514        """
515        Send information about the state of a particular user.
516
517        @type user: C{str} or C{unicode}
518        @param user: The user receiving this information.  Only their nick name,
519            not the full hostmask.
520
521        @type nick: C{str} or C{unicode}
522        @param nick: The nickname of the user this information describes.
523
524        @type username: C{str} or C{unicode}
525        @param username: The user's username (eg, ident response)
526
527        @type hostname: C{str}
528        @param hostname: The user's hostmask
529
530        @type realName: C{str} or C{unicode}
531        @param realName: The user's real name
532
533        @type server: C{str} or C{unicode}
534        @param server: The name of the server to which the user is connected
535
536        @type serverInfo: C{str} or C{unicode}
537        @param serverInfo: A descriptive string about that server
538
539        @type oper: C{bool}
540        @param oper: Indicates whether the user is an IRC operator
541
542        @type idle: C{int}
543        @param idle: The number of seconds since the user last sent a message
544
545        @type signOn: C{int}
546        @param signOn: A POSIX timestamp (number of seconds since the epoch)
547            indicating the time the user signed on
548
549        @type channels: C{list} of C{str} or C{unicode}
550        @param channels: A list of the channels which the user is participating in
551        """
552        self.sendLine(":%s %s %s %s %s %s * :%s" % (
553            self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName))
554        self.sendLine(":%s %s %s %s %s :%s" % (
555            self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo))
556        if oper:
557            self.sendLine(":%s %s %s %s :is an IRC operator" % (
558                self.hostname, RPL_WHOISOPERATOR, user, nick))
559        self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % (
560            self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn))
561        self.sendLine(":%s %s %s %s :%s" % (
562            self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels)))
563        self.sendLine(":%s %s %s %s :End of WHOIS list." % (
564            self.hostname, RPL_ENDOFWHOIS, user, nick))
565
566
567    def join(self, who, where):
568        """
569        Send a join message.
570
571        @type who: C{str} or C{unicode}
572        @param who: The name of the user joining.  Should be of the form
573            username!ident@hostmask (unless you know better!).
574
575        @type where: C{str} or C{unicode}
576        @param where: The channel the user is joining.
577        """
578        self.sendLine(":%s JOIN %s" % (who, where))
579
580
581    def part(self, who, where, reason=None):
582        """
583        Send a part message.
584
585        @type who: C{str} or C{unicode}
586        @param who: The name of the user joining.  Should be of the form
587            username!ident@hostmask (unless you know better!).
588
589        @type where: C{str} or C{unicode}
590        @param where: The channel the user is joining.
591
592        @type reason: C{str} or C{unicode}
593        @param reason: A string describing the misery which caused this poor
594            soul to depart.
595        """
596        if reason:
597            self.sendLine(":%s PART %s :%s" % (who, where, reason))
598        else:
599            self.sendLine(":%s PART %s" % (who, where))
600
601
602    def channelMode(self, user, channel, mode, *args):
603        """
604        Send information about the mode of a channel.
605
606        @type user: C{str} or C{unicode}
607        @param user: The user receiving the name list.  Only their nick name,
608            not the full hostmask.
609
610        @type channel: C{str} or C{unicode}
611        @param channel: The channel for which this is the namelist.
612
613        @type mode: C{str}
614        @param mode: A string describing this channel's modes.
615
616        @param args: Any additional arguments required by the modes.
617        """
618        self.sendLine(":%s %s %s %s %s %s" % (
619            self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args)))
620
621
622
623class ServerSupportedFeatures(_CommandDispatcherMixin):
624    """
625    Handle ISUPPORT messages.
626
627    Feature names match those in the ISUPPORT RFC draft identically.
628
629    Information regarding the specifics of ISUPPORT was gleaned from
630    <http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>.
631    """
632    prefix = 'isupport'
633
634    def __init__(self):
635        self._features = {
636            'CHANNELLEN': 200,
637            'CHANTYPES': tuple('#&'),
638            'MODES': 3,
639            'NICKLEN': 9,
640            'PREFIX': self._parsePrefixParam('(ovh)@+%'),
641            # The ISUPPORT draft explicitly says that there is no default for
642            # CHANMODES, but we're defaulting it here to handle the case where
643            # the IRC server doesn't send us any ISUPPORT information, since
644            # IRCClient.getChannelModeParams relies on this value.
645            'CHANMODES': self._parseChanModesParam(['b', '', 'lk'])}
646
647
648    def _splitParamArgs(cls, params, valueProcessor=None):
649        """
650        Split ISUPPORT parameter arguments.
651
652        Values can optionally be processed by C{valueProcessor}.
653
654        For example::
655
656            >>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2'])
657            (('A', '1'), ('B', '2'))
658
659        @type params: C{iterable} of C{str}
660
661        @type valueProcessor: C{callable} taking {str}
662        @param valueProcessor: Callable to process argument values, or C{None}
663            to perform no processing
664
665        @rtype: C{list} of C{(str, object)}
666        @return: Sequence of C{(name, processedValue)}
667        """
668        if valueProcessor is None:
669            valueProcessor = lambda x: x
670
671        def _parse():
672            for param in params:
673                if ':' not in param:
674                    param += ':'
675                a, b = param.split(':', 1)
676                yield a, valueProcessor(b)
677        return list(_parse())
678    _splitParamArgs = classmethod(_splitParamArgs)
679
680
681    def _unescapeParamValue(cls, value):
682        """
683        Unescape an ISUPPORT parameter.
684
685        The only form of supported escape is C{\\xHH}, where HH must be a valid
686        2-digit hexadecimal number.
687
688        @rtype: C{str}
689        """
690        def _unescape():
691            parts = value.split('\\x')
692            # The first part can never be preceeded by the escape.
693            yield parts.pop(0)
694            for s in parts:
695                octet, rest = s[:2], s[2:]
696                try:
697                    octet = int(octet, 16)
698                except ValueError:
699                    raise ValueError('Invalid hex octet: %r' % (octet,))
700                yield chr(octet) + rest
701
702        if '\\x' not in value:
703            return value
704        return ''.join(_unescape())
705    _unescapeParamValue = classmethod(_unescapeParamValue)
706
707
708    def _splitParam(cls, param):
709        """
710        Split an ISUPPORT parameter.
711
712        @type param: C{str}
713
714        @rtype: C{(str, list)}
715        @return C{(key, arguments)}
716        """
717        if '=' not in param:
718            param += '='
719        key, value = param.split('=', 1)
720        return key, map(cls._unescapeParamValue, value.split(','))
721    _splitParam = classmethod(_splitParam)
722
723
724    def _parsePrefixParam(cls, prefix):
725        """
726        Parse the ISUPPORT "PREFIX" parameter.
727
728        The order in which the parameter arguments appear is significant, the
729        earlier a mode appears the more privileges it gives.
730
731        @rtype: C{dict} mapping C{str} to C{(str, int)}
732        @return: A dictionary mapping a mode character to a two-tuple of
733            C({symbol, priority)}, the lower a priority (the lowest being
734            C{0}) the more privileges it gives
735        """
736        if not prefix:
737            return None
738        if prefix[0] != '(' and ')' not in prefix:
739            raise ValueError('Malformed PREFIX parameter')
740        modes, symbols = prefix.split(')', 1)
741        symbols = zip(symbols, xrange(len(symbols)))
742        modes = modes[1:]
743        return dict(zip(modes, symbols))
744    _parsePrefixParam = classmethod(_parsePrefixParam)
745
746
747    def _parseChanModesParam(self, params):
748        """
749        Parse the ISUPPORT "CHANMODES" parameter.
750
751        See L{isupport_CHANMODES} for a detailed explanation of this parameter.
752        """
753        names = ('addressModes', 'param', 'setParam', 'noParam')
754        if len(params) > len(names):
755            raise ValueError(
756                'Expecting a maximum of %d channel mode parameters, got %d' % (
757                    len(names), len(params)))
758        items = map(lambda key, value: (key, value or ''), names, params)
759        return dict(items)
760    _parseChanModesParam = classmethod(_parseChanModesParam)
761
762
763    def getFeature(self, feature, default=None):
764        """
765        Get a server supported feature's value.
766
767        A feature with the value C{None} is equivalent to the feature being
768        unsupported.
769
770        @type feature: C{str}
771        @param feature: Feature name
772
773        @type default: C{object}
774        @param default: The value to default to, assuming that C{feature}
775            is not supported
776
777        @return: Feature value
778        """
779        return self._features.get(feature, default)
780
781
782    def hasFeature(self, feature):
783        """
784        Determine whether a feature is supported or not.
785
786        @rtype: C{bool}
787        """
788        return self.getFeature(feature) is not None
789
790
791    def parse(self, params):
792        """
793        Parse ISUPPORT parameters.
794
795        If an unknown parameter is encountered, it is simply added to the
796        dictionary, keyed by its name, as a tuple of the parameters provided.
797
798        @type params: C{iterable} of C{str}
799        @param params: Iterable of ISUPPORT parameters to parse
800        """
801        for param in params:
802            key, value = self._splitParam(param)
803            if key.startswith('-'):
804                self._features.pop(key[1:], None)
805            else:
806                self._features[key] = self.dispatch(key, value)
807
808
809    def isupport_unknown(self, command, params):
810        """
811        Unknown ISUPPORT parameter.
812        """
813        return tuple(params)
814
815
816    def isupport_CHANLIMIT(self, params):
817        """
818        The maximum number of each channel type a user may join.
819        """
820        return self._splitParamArgs(params, _intOrDefault)
821
822
823    def isupport_CHANMODES(self, params):
824        """
825        Available channel modes.
826
827        There are 4 categories of channel mode::
828
829            addressModes - Modes that add or remove an address to or from a
830            list, these modes always take a parameter.
831
832            param - Modes that change a setting on a channel, these modes
833            always take a parameter.
834
835            setParam - Modes that change a setting on a channel, these modes
836            only take a parameter when being set.
837
838            noParam - Modes that change a setting on a channel, these modes
839            never take a parameter.
840        """
841        try:
842            return self._parseChanModesParam(params)
843        except ValueError:
844            return self.getFeature('CHANMODES')
845
846
847    def isupport_CHANNELLEN(self, params):
848        """
849        Maximum length of a channel name a client may create.
850        """
851        return _intOrDefault(params[0], self.getFeature('CHANNELLEN'))
852
853
854    def isupport_CHANTYPES(self, params):
855        """
856        Valid channel prefixes.
857        """
858        return tuple(params[0])
859
860
861    def isupport_EXCEPTS(self, params):
862        """
863        Mode character for "ban exceptions".
864
865        The presence of this parameter indicates that the server supports
866        this functionality.
867        """
868        return params[0] or 'e'
869
870
871    def isupport_IDCHAN(self, params):
872        """
873        Safe channel identifiers.
874
875        The presence of this parameter indicates that the server supports
876        this functionality.
877        """
878        return self._splitParamArgs(params)
879
880
881    def isupport_INVEX(self, params):
882        """
883        Mode character for "invite exceptions".
884
885        The presence of this parameter indicates that the server supports
886        this functionality.
887        """
888        return params[0] or 'I'
889
890
891    def isupport_KICKLEN(self, params):
892        """
893        Maximum length of a kick message a client may provide.
894        """
895        return _intOrDefault(params[0])
896
897
898    def isupport_MAXLIST(self, params):
899        """
900        Maximum number of "list modes" a client may set on a channel at once.
901
902        List modes are identified by the "addressModes" key in CHANMODES.
903        """
904        return self._splitParamArgs(params, _intOrDefault)
905
906
907    def isupport_MODES(self, params):
908        """
909        Maximum number of modes accepting parameters that may be sent, by a
910        client, in a single MODE command.
911        """
912        return _intOrDefault(params[0])
913
914
915    def isupport_NETWORK(self, params):
916        """
917        IRC network name.
918        """
919        return params[0]
920
921
922    def isupport_NICKLEN(self, params):
923        """
924        Maximum length of a nickname the client may use.
925        """
926        return _intOrDefault(params[0], self.getFeature('NICKLEN'))
927
928
929    def isupport_PREFIX(self, params):
930        """
931        Mapping of channel modes that clients may have to status flags.
932        """
933        try:
934            return self._parsePrefixParam(params[0])
935        except ValueError:
936            return self.getFeature('PREFIX')
937
938
939    def isupport_SAFELIST(self, params):
940        """
941        Flag indicating that a client may request a LIST without being
942        disconnected due to the large amount of data generated.
943        """
944        return True
945
946
947    def isupport_STATUSMSG(self, params):
948        """
949        The server supports sending messages to only to clients on a channel
950        with a specific status.
951        """
952        return params[0]
953
954
955    def isupport_TARGMAX(self, params):
956        """
957        Maximum number of targets allowable for commands that accept multiple
958        targets.
959        """
960        return dict(self._splitParamArgs(params, _intOrDefault))
961
962
963    def isupport_TOPICLEN(self, params):
964        """
965        Maximum length of a topic that may be set.
966        """
967        return _intOrDefault(params[0])
968
969
970
971class IRCClient(basic.LineReceiver):
972    """
973    Internet Relay Chat client protocol, with sprinkles.
974
975    In addition to providing an interface for an IRC client protocol,
976    this class also contains reasonable implementations of many common
977    CTCP methods.
978
979    TODO
980    ====
981     - Limit the length of messages sent (because the IRC server probably
982       does).
983     - Add flood protection/rate limiting for my CTCP replies.
984     - NickServ cooperation.  (a mix-in?)
985
986    @ivar nickname: Nickname the client will use.
987    @ivar password: Password used to log on to the server.  May be C{None}.
988    @ivar realname: Supplied to the server during login as the "Real name"
989        or "ircname".  May be C{None}.
990    @ivar username: Supplied to the server during login as the "User name".
991        May be C{None}
992
993    @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query.  If C{None}, no
994        USERINFO reply will be sent.
995        "This is used to transmit a string which is settable by
996        the user (and never should be set by the client)."
997    @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query.  If C{None}, no
998        FINGER reply will be sent.
999    @type fingerReply: Callable or String
1000
1001    @ivar versionName: CTCP VERSION reply, client name.  If C{None}, no VERSION
1002        reply will be sent.
1003    @type versionName: C{str}, or None.
1004    @ivar versionNum: CTCP VERSION reply, client version.
1005    @type versionNum: C{str}, or None.
1006    @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
1007    @type versionEnv: C{str}, or None.
1008
1009    @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
1010        client may be found.  If C{None}, no SOURCE reply will be sent.
1011
1012    @ivar lineRate: Minimum delay between lines sent to the server.  If
1013        C{None}, no delay will be imposed.
1014    @type lineRate: Number of Seconds.
1015
1016    @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and
1017        I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content
1018        of an I{RPL_MOTD} message.
1019
1020    @ivar erroneousNickFallback: Default nickname assigned when an unregistered
1021        client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register
1022        with an illegal nickname.
1023    @type erroneousNickFallback: C{str}
1024
1025    @ivar _registered: Whether or not the user is registered. It becomes True
1026        once a welcome has been received from the server.
1027    @type _registered: C{bool}
1028
1029    @ivar _attemptedNick: The nickname that will try to get registered. It may
1030        change if it is illegal or already taken. L{nickname} becomes the
1031        L{_attemptedNick} that is successfully registered.
1032    @type _attemptedNick:  C{str}
1033
1034    @type supported: L{ServerSupportedFeatures}
1035    @ivar supported: Available ISUPPORT features on the server
1036
1037    @type hostname: C{str}
1038    @ivar hostname: Host name of the IRC server the client is connected to.
1039        Initially the host name is C{None} and later is set to the host name
1040        from which the I{RPL_WELCOME} message is received.
1041
1042    @type _heartbeat: L{task.LoopingCall}
1043    @ivar _heartbeat: Looping call to perform the keepalive by calling
1044        L{IRCClient._sendHeartbeat} every L{heartbeatInterval} seconds, or
1045        C{None} if there is no heartbeat.
1046
1047    @type heartbeatInterval: C{float}
1048    @ivar heartbeatInterval: Interval, in seconds, to send I{PING} messages to
1049        the server as a form of keepalive, defaults to 120 seconds. Use C{None}
1050        to disable the heartbeat.
1051    """
1052    hostname = None
1053    motd = None
1054    nickname = 'irc'
1055    password = None
1056    realname = None
1057    username = None
1058    ### Responses to various CTCP queries.
1059
1060    userinfo = None
1061    # fingerReply is a callable returning a string, or a str()able object.
1062    fingerReply = None
1063    versionName = None
1064    versionNum = None
1065    versionEnv = None
1066
1067    sourceURL = "http://twistedmatrix.com/downloads/"
1068
1069    dcc_destdir = '.'
1070    dcc_sessions = None
1071
1072    # If this is false, no attempt will be made to identify
1073    # ourself to the server.
1074    performLogin = 1
1075
1076    lineRate = None
1077    _queue = None
1078    _queueEmptying = None
1079
1080    delimiter = '\n' # '\r\n' will also work (see dataReceived)
1081
1082    __pychecker__ = 'unusednames=params,prefix,channel'
1083
1084    _registered = False
1085    _attemptedNick = ''
1086    erroneousNickFallback = 'defaultnick'
1087
1088    _heartbeat = None
1089    heartbeatInterval = 120
1090
1091
1092    def _reallySendLine(self, line):
1093        return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r')
1094
1095    def sendLine(self, line):
1096        if self.lineRate is None:
1097            self._reallySendLine(line)
1098        else:
1099            self._queue.append(line)
1100            if not self._queueEmptying:
1101                self._sendLine()
1102
1103    def _sendLine(self):
1104        if self._queue:
1105            self._reallySendLine(self._queue.pop(0))
1106            self._queueEmptying = reactor.callLater(self.lineRate,
1107                                                    self._sendLine)
1108        else:
1109            self._queueEmptying = None
1110
1111
1112    def connectionLost(self, reason):
1113        basic.LineReceiver.connectionLost(self, reason)
1114        self.stopHeartbeat()
1115
1116
1117    def _createHeartbeat(self):
1118        """
1119        Create the heartbeat L{LoopingCall}.
1120        """
1121        return task.LoopingCall(self._sendHeartbeat)
1122
1123
1124    def _sendHeartbeat(self):
1125        """
1126        Send a I{PING} message to the IRC server as a form of keepalive.
1127        """
1128        self.sendLine('PING ' + self.hostname)
1129
1130
1131    def stopHeartbeat(self):
1132        """
1133        Stop sending I{PING} messages to keep the connection to the server
1134        alive.
1135
1136        @since: 11.1
1137        """
1138        if self._heartbeat is not None:
1139            self._heartbeat.stop()
1140            self._heartbeat = None
1141
1142
1143    def startHeartbeat(self):
1144        """
1145        Start sending I{PING} messages every L{IRCClient.heartbeatInterval}
1146        seconds to keep the connection to the server alive during periods of no
1147        activity.
1148
1149        @since: 11.1
1150        """
1151        self.stopHeartbeat()
1152        if self.heartbeatInterval is None:
1153            return
1154        self._heartbeat = self._createHeartbeat()
1155        self._heartbeat.start(self.heartbeatInterval, now=False)
1156
1157
1158    ### Interface level client->user output methods
1159    ###
1160    ### You'll want to override these.
1161
1162    ### Methods relating to the server itself
1163
1164    def created(self, when):
1165        """
1166        Called with creation date information about the server, usually at logon.
1167
1168        @type when: C{str}
1169        @param when: A string describing when the server was created, probably.
1170        """
1171
1172    def yourHost(self, info):
1173        """
1174        Called with daemon information about the server, usually at logon.
1175
1176        @type info: C{str}
1177        @param when: A string describing what software the server is running, probably.
1178        """
1179
1180    def myInfo(self, servername, version, umodes, cmodes):
1181        """
1182        Called with information about the server, usually at logon.
1183
1184        @type servername: C{str}
1185        @param servername: The hostname of this server.
1186
1187        @type version: C{str}
1188        @param version: A description of what software this server runs.
1189
1190        @type umodes: C{str}
1191        @param umodes: All the available user modes.
1192
1193        @type cmodes: C{str}
1194        @param cmodes: All the available channel modes.
1195        """
1196
1197    def luserClient(self, info):
1198        """
1199        Called with information about the number of connections, usually at logon.
1200
1201        @type info: C{str}
1202        @param info: A description of the number of clients and servers
1203        connected to the network, probably.
1204        """
1205
1206    def bounce(self, info):
1207        """
1208        Called with information about where the client should reconnect.
1209
1210        @type info: C{str}
1211        @param info: A plaintext description of the address that should be
1212        connected to.
1213        """
1214
1215    def isupport(self, options):
1216        """
1217        Called with various information about what the server supports.
1218
1219        @type options: C{list} of C{str}
1220        @param options: Descriptions of features or limits of the server, possibly
1221        in the form "NAME=VALUE".
1222        """
1223
1224    def luserChannels(self, channels):
1225        """
1226        Called with the number of channels existant on the server.
1227
1228        @type channels: C{int}
1229        """
1230
1231    def luserOp(self, ops):
1232        """
1233        Called with the number of ops logged on to the server.
1234
1235        @type ops: C{int}
1236        """
1237
1238    def luserMe(self, info):
1239        """
1240        Called with information about the server connected to.
1241
1242        @type info: C{str}
1243        @param info: A plaintext string describing the number of users and servers
1244        connected to this server.
1245        """
1246
1247    ### Methods involving me directly
1248
1249    def privmsg(self, user, channel, message):
1250        """
1251        Called when I have a message from a user to me or a channel.
1252        """
1253        pass
1254
1255    def joined(self, channel):
1256        """
1257        Called when I finish joining a channel.
1258
1259        channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
1260        intact.
1261        """
1262
1263    def left(self, channel):
1264        """
1265        Called when I have left a channel.
1266
1267        channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
1268        intact.
1269        """
1270
1271
1272    def noticed(self, user, channel, message):
1273        """
1274        Called when I have a notice from a user to me or a channel.
1275
1276        If the client makes any automated replies, it must not do so in
1277        response to a NOTICE message, per the RFC::
1278
1279            The difference between NOTICE and PRIVMSG is that
1280            automatic replies MUST NEVER be sent in response to a
1281            NOTICE message. [...] The object of this rule is to avoid
1282            loops between clients automatically sending something in
1283            response to something it received.
1284        """
1285
1286
1287    def modeChanged(self, user, channel, set, modes, args):
1288        """
1289        Called when users or channel's modes are changed.
1290
1291        @type user: C{str}
1292        @param user: The user and hostmask which instigated this change.
1293
1294        @type channel: C{str}
1295        @param channel: The channel where the modes are changed. If args is
1296        empty the channel for which the modes are changing. If the changes are
1297        at server level it could be equal to C{user}.
1298
1299        @type set: C{bool} or C{int}
1300        @param set: True if the mode(s) is being added, False if it is being
1301        removed. If some modes are added and others removed at the same time
1302        this function will be called twice, the first time with all the added
1303        modes, the second with the removed ones. (To change this behaviour
1304        override the irc_MODE method)
1305
1306        @type modes: C{str}
1307        @param modes: The mode or modes which are being changed.
1308
1309        @type args: C{tuple}
1310        @param args: Any additional information required for the mode
1311        change.
1312        """
1313
1314    def pong(self, user, secs):
1315        """
1316        Called with the results of a CTCP PING query.
1317        """
1318        pass
1319
1320    def signedOn(self):
1321        """
1322        Called after sucessfully signing on to the server.
1323        """
1324        pass
1325
1326    def kickedFrom(self, channel, kicker, message):
1327        """
1328        Called when I am kicked from a channel.
1329        """
1330        pass
1331
1332    def nickChanged(self, nick):
1333        """
1334        Called when my nick has been changed.
1335        """
1336        self.nickname = nick
1337
1338
1339    ### Things I observe other people doing in a channel.
1340
1341    def userJoined(self, user, channel):
1342        """
1343        Called when I see another user joining a channel.
1344        """
1345        pass
1346
1347    def userLeft(self, user, channel):
1348        """
1349        Called when I see another user leaving a channel.
1350        """
1351        pass
1352
1353    def userQuit(self, user, quitMessage):
1354        """
1355        Called when I see another user disconnect from the network.
1356        """
1357        pass
1358
1359    def userKicked(self, kickee, channel, kicker, message):
1360        """
1361        Called when I observe someone else being kicked from a channel.
1362        """
1363        pass
1364
1365    def action(self, user, channel, data):
1366        """
1367        Called when I see a user perform an ACTION on a channel.
1368        """
1369        pass
1370
1371    def topicUpdated(self, user, channel, newTopic):
1372        """
1373        In channel, user changed the topic to newTopic.
1374
1375        Also called when first joining a channel.
1376        """
1377        pass
1378
1379    def userRenamed(self, oldname, newname):
1380        """
1381        A user changed their name from oldname to newname.
1382        """
1383        pass
1384
1385    ### Information from the server.
1386
1387    def receivedMOTD(self, motd):
1388        """
1389        I received a message-of-the-day banner from the server.
1390
1391        motd is a list of strings, where each string was sent as a seperate
1392        message from the server. To display, you might want to use::
1393
1394            '\\n'.join(motd)
1395
1396        to get a nicely formatted string.
1397        """
1398        pass
1399
1400    ### user input commands, client->server
1401    ### Your client will want to invoke these.
1402
1403    def join(self, channel, key=None):
1404        """
1405        Join a channel.
1406
1407        @type channel: C{str}
1408        @param channel: The name of the channel to join. If it has no prefix,
1409            C{'#'} will be prepended to it.
1410        @type key: C{str}
1411        @param key: If specified, the key used to join the channel.
1412        """
1413        if channel[0] not in CHANNEL_PREFIXES:
1414            channel = '#' + channel
1415        if key:
1416            self.sendLine("JOIN %s %s" % (channel, key))
1417        else:
1418            self.sendLine("JOIN %s" % (channel,))
1419
1420    def leave(self, channel, reason=None):
1421        """
1422        Leave a channel.
1423
1424        @type channel: C{str}
1425        @param channel: The name of the channel to leave. If it has no prefix,
1426            C{'#'} will be prepended to it.
1427        @type reason: C{str}
1428        @param reason: If given, the reason for leaving.
1429        """
1430        if channel[0] not in CHANNEL_PREFIXES:
1431            channel = '#' + channel
1432        if reason:
1433            self.sendLine("PART %s :%s" % (channel, reason))
1434        else:
1435            self.sendLine("PART %s" % (channel,))
1436
1437    def kick(self, channel, user, reason=None):
1438        """
1439        Attempt to kick a user from a channel.
1440
1441        @type channel: C{str}
1442        @param channel: The name of the channel to kick the user from. If it has
1443            no prefix, C{'#'} will be prepended to it.
1444        @type user: C{str}
1445        @param user: The nick of the user to kick.
1446        @type reason: C{str}
1447        @param reason: If given, the reason for kicking the user.
1448        """
1449        if channel[0] not in CHANNEL_PREFIXES:
1450            channel = '#' + channel
1451        if reason:
1452            self.sendLine("KICK %s %s :%s" % (channel, user, reason))
1453        else:
1454            self.sendLine("KICK %s %s" % (channel, user))
1455
1456    part = leave
1457
1458
1459    def invite(self, user, channel):
1460        """
1461        Attempt to invite user to channel
1462
1463        @type user: C{str}
1464        @param user: The user to invite
1465        @type channel: C{str}
1466        @param channel: The channel to invite the user too
1467
1468        @since: 11.0
1469        """
1470        if channel[0] not in CHANNEL_PREFIXES:
1471            channel = '#' + channel
1472        self.sendLine("INVITE %s %s" % (user, channel))
1473
1474
1475    def topic(self, channel, topic=None):
1476        """
1477        Attempt to set the topic of the given channel, or ask what it is.
1478
1479        If topic is None, then I sent a topic query instead of trying to set the
1480        topic. The server should respond with a TOPIC message containing the
1481        current topic of the given channel.
1482
1483        @type channel: C{str}
1484        @param channel: The name of the channel to change the topic on. If it
1485            has no prefix, C{'#'} will be prepended to it.
1486        @type topic: C{str}
1487        @param topic: If specified, what to set the topic to.
1488        """
1489        # << TOPIC #xtestx :fff
1490        if channel[0] not in CHANNEL_PREFIXES:
1491            channel = '#' + channel
1492        if topic != None:
1493            self.sendLine("TOPIC %s :%s" % (channel, topic))
1494        else:
1495            self.sendLine("TOPIC %s" % (channel,))
1496
1497
1498    def mode(self, chan, set, modes, limit = None, user = None, mask = None):
1499        """
1500        Change the modes on a user or channel.
1501
1502        The C{limit}, C{user}, and C{mask} parameters are mutually exclusive.
1503
1504        @type chan: C{str}
1505        @param chan: The name of the channel to operate on.
1506        @type set: C{bool}
1507        @param set: True to give the user or channel permissions and False to
1508            remove them.
1509        @type modes: C{str}
1510        @param modes: The mode flags to set on the user or channel.
1511        @type limit: C{int}
1512        @param limit: In conjuction with the C{'l'} mode flag, limits the
1513             number of users on the channel.
1514        @type user: C{str}
1515        @param user: The user to change the mode on.
1516        @type mask: C{str}
1517        @param mask: In conjuction with the C{'b'} mode flag, sets a mask of
1518            users to be banned from the channel.
1519        """
1520        if set:
1521            line = 'MODE %s +%s' % (chan, modes)
1522        else:
1523            line = 'MODE %s -%s' % (chan, modes)
1524        if limit is not None:
1525            line = '%s %d' % (line, limit)
1526        elif user is not None:
1527            line = '%s %s' % (line, user)
1528        elif mask is not None:
1529            line = '%s %s' % (line, mask)
1530        self.sendLine(line)
1531
1532
1533    def say(self, channel, message, length=None):
1534        """
1535        Send a message to a channel
1536
1537        @type channel: C{str}
1538        @param channel: The channel to say the message on. If it has no prefix,
1539            C{'#'} will be prepended to it.
1540        @type message: C{str}
1541        @param message: The message to say.
1542        @type length: C{int}
1543        @param length: The maximum number of octets to send at a time.  This has
1544            the effect of turning a single call to C{msg()} into multiple
1545            commands to the server.  This is useful when long messages may be
1546            sent that would otherwise cause the server to kick us off or
1547            silently truncate the text we are sending.  If None is passed, the
1548            entire message is always send in one command.
1549        """
1550        if channel[0] not in CHANNEL_PREFIXES:
1551            channel = '#' + channel
1552        self.msg(channel, message, length)
1553
1554
1555    def _safeMaximumLineLength(self, command):
1556        """
1557        Estimate a safe maximum line length for the given command.
1558
1559        This is done by assuming the maximum values for nickname length,
1560        realname and hostname combined with the command that needs to be sent
1561        and some guessing. A theoretical maximum value is used because it is
1562        possible that our nickname, username or hostname changes (on the server
1563        side) while the length is still being calculated.
1564        """
1565        # :nickname!realname@hostname COMMAND ...
1566        theoretical = ':%s!%s@%s %s' % (
1567            'a' * self.supported.getFeature('NICKLEN'),
1568            # This value is based on observation.
1569            'b' * 10,
1570            # See <http://tools.ietf.org/html/rfc2812#section-2.3.1>.
1571            'c' * 63,
1572            command)
1573        # Fingers crossed.
1574        fudge = 10
1575        return MAX_COMMAND_LENGTH - len(theoretical) - fudge
1576
1577
1578    def msg(self, user, message, length=None):
1579        """
1580        Send a message to a user or channel.
1581
1582        The message will be split into multiple commands to the server if:
1583         - The message contains any newline characters
1584         - Any span between newline characters is longer than the given
1585           line-length.
1586
1587        @param user: Username or channel name to which to direct the
1588            message.
1589        @type user: C{str}
1590
1591        @param message: Text to send.
1592        @type message: C{str}
1593
1594        @param length: Maximum number of octets to send in a single
1595            command, including the IRC protocol framing. If C{None} is given
1596            then L{IRCClient._safeMaximumLineLength} is used to determine a
1597            value.
1598        @type length: C{int}
1599        """
1600        fmt = 'PRIVMSG %s :' % (user,)
1601
1602        if length is None:
1603            length = self._safeMaximumLineLength(fmt)
1604
1605        # Account for the line terminator.
1606        minimumLength = len(fmt) + 2
1607        if length <= minimumLength:
1608            raise ValueError("Maximum length must exceed %d for message "
1609                             "to %s" % (minimumLength, user))
1610        for line in split(message, length - minimumLength):
1611            self.sendLine(fmt + line)
1612
1613
1614    def notice(self, user, message):
1615        """
1616        Send a notice to a user.
1617
1618        Notices are like normal message, but should never get automated
1619        replies.
1620
1621        @type user: C{str}
1622        @param user: The user to send a notice to.
1623        @type message: C{str}
1624        @param message: The contents of the notice to send.
1625        """
1626        self.sendLine("NOTICE %s :%s" % (user, message))
1627
1628
1629    def away(self, message=''):
1630        """
1631        Mark this client as away.
1632
1633        @type message: C{str}
1634        @param message: If specified, the away message.
1635        """
1636        self.sendLine("AWAY :%s" % message)
1637
1638
1639    def back(self):
1640        """
1641        Clear the away status.
1642        """
1643        # An empty away marks us as back
1644        self.away()
1645
1646
1647    def whois(self, nickname, server=None):
1648        """
1649        Retrieve user information about the given nick name.
1650
1651        @type nickname: C{str}
1652        @param nickname: The nick name about which to retrieve information.
1653
1654        @since: 8.2
1655        """
1656        if server is None:
1657            self.sendLine('WHOIS ' + nickname)
1658        else:
1659            self.sendLine('WHOIS %s %s' % (server, nickname))
1660
1661
1662    def register(self, nickname, hostname='foo', servername='bar'):
1663        """
1664        Login to the server.
1665
1666        @type nickname: C{str}
1667        @param nickname: The nickname to register.
1668        @type hostname: C{str}
1669        @param hostname: If specified, the hostname to logon as.
1670        @type servername: C{str}
1671        @param servername: If specified, the servername to logon as.
1672        """
1673        if self.password is not None:
1674            self.sendLine("PASS %s" % self.password)
1675        self.setNick(nickname)
1676        if self.username is None:
1677            self.username = nickname
1678        self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname))
1679
1680
1681    def setNick(self, nickname):
1682        """
1683        Set this client's nickname.
1684
1685        @type nickname: C{str}
1686        @param nickname: The nickname to change to.
1687        """
1688        self._attemptedNick = nickname
1689        self.sendLine("NICK %s" % nickname)
1690
1691
1692    def quit(self, message = ''):
1693        """
1694        Disconnect from the server
1695
1696        @type message: C{str}
1697
1698        @param message: If specified, the message to give when quitting the
1699            server.
1700        """
1701        self.sendLine("QUIT :%s" % message)
1702
1703    ### user input commands, client->client
1704
1705    def describe(self, channel, action):
1706        """
1707        Strike a pose.
1708
1709        @type channel: C{str}
1710        @param channel: The name of the channel to have an action on. If it
1711            has no prefix, it is sent to the user of that name.
1712        @type action: C{str}
1713        @param action: The action to preform.
1714        @since: 9.0
1715        """
1716        self.ctcpMakeQuery(channel, [('ACTION', action)])
1717
1718
1719    _pings = None
1720    _MAX_PINGRING = 12
1721
1722    def ping(self, user, text = None):
1723        """
1724        Measure round-trip delay to another IRC client.
1725        """
1726        if self._pings is None:
1727            self._pings = {}
1728
1729        if text is None:
1730            chars = string.letters + string.digits + string.punctuation
1731            key = ''.join([random.choice(chars) for i in range(12)])
1732        else:
1733            key = str(text)
1734        self._pings[(user, key)] = time.time()
1735        self.ctcpMakeQuery(user, [('PING', key)])
1736
1737        if len(self._pings) > self._MAX_PINGRING:
1738            # Remove some of the oldest entries.
1739            byValue = [(v, k) for (k, v) in self._pings.items()]
1740            byValue.sort()
1741            excess = self._MAX_PINGRING - len(self._pings)
1742            for i in xrange(excess):
1743                del self._pings[byValue[i][1]]
1744
1745
1746    def dccSend(self, user, file):
1747        if type(file) == types.StringType:
1748            file = open(file, 'r')
1749
1750        size = fileSize(file)
1751
1752        name = getattr(file, "name", "file@%s" % (id(file),))
1753
1754        factory = DccSendFactory(file)
1755        port = reactor.listenTCP(0, factory, 1)
1756
1757        raise NotImplementedError,(
1758            "XXX!!! Help!  I need to bind a socket, have it listen, and tell me its address.  "
1759            "(and stop accepting once we've made a single connection.)")
1760
1761        my_address = struct.pack("!I", my_address)
1762
1763        args = ['SEND', name, my_address, str(port)]
1764
1765        if not (size is None):
1766            args.append(size)
1767
1768        args = string.join(args, ' ')
1769
1770        self.ctcpMakeQuery(user, [('DCC', args)])
1771
1772
1773    def dccResume(self, user, fileName, port, resumePos):
1774        """
1775        Send a DCC RESUME request to another user.
1776        """
1777        self.ctcpMakeQuery(user, [
1778            ('DCC', ['RESUME', fileName, port, resumePos])])
1779
1780
1781    def dccAcceptResume(self, user, fileName, port, resumePos):
1782        """
1783        Send a DCC ACCEPT response to clients who have requested a resume.
1784        """
1785        self.ctcpMakeQuery(user, [
1786            ('DCC', ['ACCEPT', fileName, port, resumePos])])
1787
1788    ### server->client messages
1789    ### You might want to fiddle with these,
1790    ### but it is safe to leave them alone.
1791
1792    def irc_ERR_NICKNAMEINUSE(self, prefix, params):
1793        """
1794        Called when we try to register or change to a nickname that is already
1795        taken.
1796        """
1797        self._attemptedNick = self.alterCollidedNick(self._attemptedNick)
1798        self.setNick(self._attemptedNick)
1799
1800
1801    def alterCollidedNick(self, nickname):
1802        """
1803        Generate an altered version of a nickname that caused a collision in an
1804        effort to create an unused related name for subsequent registration.
1805
1806        @param nickname: The nickname a user is attempting to register.
1807        @type nickname: C{str}
1808
1809        @returns: A string that is in some way different from the nickname.
1810        @rtype: C{str}
1811        """
1812        return nickname + '_'
1813
1814
1815    def irc_ERR_ERRONEUSNICKNAME(self, prefix, params):
1816        """
1817        Called when we try to register or change to an illegal nickname.
1818
1819        The server should send this reply when the nickname contains any
1820        disallowed characters.  The bot will stall, waiting for RPL_WELCOME, if
1821        we don't handle this during sign-on.
1822
1823        @note: The method uses the spelling I{erroneus}, as it appears in
1824            the RFC, section 6.1.
1825        """
1826        if not self._registered:
1827            self.setNick(self.erroneousNickFallback)
1828
1829
1830    def irc_ERR_PASSWDMISMATCH(self, prefix, params):
1831        """
1832        Called when the login was incorrect.
1833        """
1834        raise IRCPasswordMismatch("Password Incorrect.")
1835
1836
1837    def irc_RPL_WELCOME(self, prefix, params):
1838        """
1839        Called when we have received the welcome from the server.
1840        """
1841        self.hostname = prefix
1842        self._registered = True
1843        self.nickname = self._attemptedNick
1844        self.signedOn()
1845        self.startHeartbeat()
1846
1847
1848    def irc_JOIN(self, prefix, params):
1849        """
1850        Called when a user joins a channel.
1851        """
1852        nick = string.split(prefix,'!')[0]
1853        channel = params[-1]
1854        if nick == self.nickname:
1855            self.joined(channel)
1856        else:
1857            self.userJoined(nick, channel)
1858
1859    def irc_PART(self, prefix, params):
1860        """
1861        Called when a user leaves a channel.
1862        """
1863        nick = string.split(prefix,'!')[0]
1864        channel = params[0]
1865        if nick == self.nickname:
1866            self.left(channel)
1867        else:
1868            self.userLeft(nick, channel)
1869
1870    def irc_QUIT(self, prefix, params):
1871        """
1872        Called when a user has quit.
1873        """
1874        nick = string.split(prefix,'!')[0]
1875        self.userQuit(nick, params[0])
1876
1877
1878    def irc_MODE(self, user, params):
1879        """
1880        Parse a server mode change message.
1881        """
1882        channel, modes, args = params[0], params[1], params[2:]
1883
1884        if modes[0] not in '-+':
1885            modes = '+' + modes
1886
1887        if channel == self.nickname:
1888            # This is a mode change to our individual user, not a channel mode
1889            # that involves us.
1890            paramModes = self.getUserModeParams()
1891        else:
1892            paramModes = self.getChannelModeParams()
1893
1894        try:
1895            added, removed = parseModes(modes, args, paramModes)
1896        except IRCBadModes:
1897            log.err(None, 'An error occured while parsing the following '
1898                          'MODE message: MODE %s' % (' '.join(params),))
1899        else:
1900            if added:
1901                modes, params = zip(*added)
1902                self.modeChanged(user, channel, True, ''.join(modes), params)
1903
1904            if removed:
1905                modes, params = zip(*removed)
1906                self.modeChanged(user, channel, False, ''.join(modes), params)
1907
1908
1909    def irc_PING(self, prefix, params):
1910        """
1911        Called when some has pinged us.
1912        """
1913        self.sendLine("PONG %s" % params[-1])
1914
1915    def irc_PRIVMSG(self, prefix, params):
1916        """
1917        Called when we get a message.
1918        """
1919        user = prefix
1920        channel = params[0]
1921        message = params[-1]
1922
1923        if not message:
1924            # Don't raise an exception if we get blank message.
1925            return
1926
1927        if message[0] == X_DELIM:
1928            m = ctcpExtract(message)
1929            if m['extended']:
1930                self.ctcpQuery(user, channel, m['extended'])
1931
1932            if not m['normal']:
1933                return
1934
1935            message = string.join(m['normal'], ' ')
1936
1937        self.privmsg(user, channel, message)
1938
1939    def irc_NOTICE(self, prefix, params):
1940        """
1941        Called when a user gets a notice.
1942        """
1943        user = prefix
1944        channel = params[0]
1945        message = params[-1]
1946
1947        if message[0]==X_DELIM:
1948            m = ctcpExtract(message)
1949            if m['extended']:
1950                self.ctcpReply(user, channel, m['extended'])
1951
1952            if not m['normal']:
1953                return
1954
1955            message = string.join(m['normal'], ' ')
1956
1957        self.noticed(user, channel, message)
1958
1959    def irc_NICK(self, prefix, params):
1960        """
1961        Called when a user changes their nickname.
1962        """
1963        nick = string.split(prefix,'!', 1)[0]
1964        if nick == self.nickname:
1965            self.nickChanged(params[0])
1966        else:
1967            self.userRenamed(nick, params[0])
1968
1969    def irc_KICK(self, prefix, params):
1970        """
1971        Called when a user is kicked from a channel.
1972        """
1973        kicker = string.split(prefix,'!')[0]
1974        channel = params[0]
1975        kicked = params[1]
1976        message = params[-1]
1977        if string.lower(kicked) == string.lower(self.nickname):
1978            # Yikes!
1979            self.kickedFrom(channel, kicker, message)
1980        else:
1981            self.userKicked(kicked, channel, kicker, message)
1982
1983    def irc_TOPIC(self, prefix, params):
1984        """
1985        Someone in the channel set the topic.
1986        """
1987        user = string.split(prefix, '!')[0]
1988        channel = params[0]
1989        newtopic = params[1]
1990        self.topicUpdated(user, channel, newtopic)
1991
1992    def irc_RPL_TOPIC(self, prefix, params):
1993        """
1994        Called when the topic for a channel is initially reported or when it
1995        subsequently changes.
1996        """
1997        user = string.split(prefix, '!')[0]
1998        channel = params[1]
1999        newtopic = params[2]
2000        self.topicUpdated(user, channel, newtopic)
2001
2002    def irc_RPL_NOTOPIC(self, prefix, params):
2003        user = string.split(prefix, '!')[0]
2004        channel = params[1]
2005        newtopic = ""
2006        self.topicUpdated(user, channel, newtopic)
2007
2008    def irc_RPL_MOTDSTART(self, prefix, params):
2009        if params[-1].startswith("- "):
2010            params[-1] = params[-1][2:]
2011        self.motd = [params[-1]]
2012
2013    def irc_RPL_MOTD(self, prefix, params):
2014        if params[-1].startswith("- "):
2015            params[-1] = params[-1][2:]
2016        if self.motd is None:
2017            self.motd = []
2018        self.motd.append(params[-1])
2019
2020
2021    def irc_RPL_ENDOFMOTD(self, prefix, params):
2022        """
2023        I{RPL_ENDOFMOTD} indicates the end of the message of the day
2024        messages.  Deliver the accumulated lines to C{receivedMOTD}.
2025        """
2026        motd = self.motd
2027        self.motd = None
2028        self.receivedMOTD(motd)
2029
2030
2031    def irc_RPL_CREATED(self, prefix, params):
2032        self.created(params[1])
2033
2034    def irc_RPL_YOURHOST(self, prefix, params):
2035        self.yourHost(params[1])
2036
2037    def irc_RPL_MYINFO(self, prefix, params):
2038        info = params[1].split(None, 3)
2039        while len(info) < 4:
2040            info.append(None)
2041        self.myInfo(*info)
2042
2043    def irc_RPL_BOUNCE(self, prefix, params):
2044        self.bounce(params[1])
2045
2046    def irc_RPL_ISUPPORT(self, prefix, params):
2047        args = params[1:-1]
2048        # Several ISUPPORT messages, in no particular order, may be sent
2049        # to the client at any given point in time (usually only on connect,
2050        # though.) For this reason, ServerSupportedFeatures.parse is intended
2051        # to mutate the supported feature list.
2052        self.supported.parse(args)
2053        self.isupport(args)
2054
2055    def irc_RPL_LUSERCLIENT(self, prefix, params):
2056        self.luserClient(params[1])
2057
2058    def irc_RPL_LUSEROP(self, prefix, params):
2059        try:
2060            self.luserOp(int(params[1]))
2061        except ValueError:
2062            pass
2063
2064    def irc_RPL_LUSERCHANNELS(self, prefix, params):
2065        try:
2066            self.luserChannels(int(params[1]))
2067        except ValueError:
2068            pass
2069
2070    def irc_RPL_LUSERME(self, prefix, params):
2071        self.luserMe(params[1])
2072
2073    def irc_unknown(self, prefix, command, params):
2074        pass
2075
2076    ### Receiving a CTCP query from another party
2077    ### It is safe to leave these alone.
2078
2079
2080    def ctcpQuery(self, user, channel, messages):
2081        """
2082        Dispatch method for any CTCP queries received.
2083
2084        Duplicated CTCP queries are ignored and no dispatch is
2085        made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}.
2086        """
2087        seen = set()
2088        for tag, data in messages:
2089            method = getattr(self, 'ctcpQuery_%s' % tag, None)
2090            if tag not in seen:
2091                if method is not None:
2092                    method(user, channel, data)
2093                else:
2094                    self.ctcpUnknownQuery(user, channel, tag, data)
2095            seen.add(tag)
2096
2097
2098    def ctcpUnknownQuery(self, user, channel, tag, data):
2099        """
2100        Fallback handler for unrecognized CTCP queries.
2101
2102        No CTCP I{ERRMSG} reply is made to remove a potential denial of service
2103        avenue.
2104        """
2105        log.msg('Unknown CTCP query from %r: %r %r' % (user, tag, data))
2106
2107
2108    def ctcpQuery_ACTION(self, user, channel, data):
2109        self.action(user, channel, data)
2110
2111    def ctcpQuery_PING(self, user, channel, data):
2112        nick = string.split(user,"!")[0]
2113        self.ctcpMakeReply(nick, [("PING", data)])
2114
2115    def ctcpQuery_FINGER(self, user, channel, data):
2116        if data is not None:
2117            self.quirkyMessage("Why did %s send '%s' with a FINGER query?"
2118                               % (user, data))
2119        if not self.fingerReply:
2120            return
2121
2122        if callable(self.fingerReply):
2123            reply = self.fingerReply()
2124        else:
2125            reply = str(self.fingerReply)
2126
2127        nick = string.split(user,"!")[0]
2128        self.ctcpMakeReply(nick, [('FINGER', reply)])
2129
2130    def ctcpQuery_VERSION(self, user, channel, data):
2131        if data is not None:
2132            self.quirkyMessage("Why did %s send '%s' with a VERSION query?"
2133                               % (user, data))
2134
2135        if self.versionName:
2136            nick = string.split(user,"!")[0]
2137            self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' %
2138                                       (self.versionName,
2139                                        self.versionNum or '',
2140                                        self.versionEnv or ''))])
2141
2142    def ctcpQuery_SOURCE(self, user, channel, data):
2143        if data is not None:
2144            self.quirkyMessage("Why did %s send '%s' with a SOURCE query?"
2145                               % (user, data))
2146        if self.sourceURL:
2147            nick = string.split(user,"!")[0]
2148            # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
2149            # replies should be responded to with the location of an anonymous
2150            # FTP server in host:directory:file format.  I'm taking the liberty
2151            # of bringing it into the 21st century by sending a URL instead.
2152            self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL),
2153                                      ('SOURCE', None)])
2154
2155    def ctcpQuery_USERINFO(self, user, channel, data):
2156        if data is not None:
2157            self.quirkyMessage("Why did %s send '%s' with a USERINFO query?"
2158                               % (user, data))
2159        if self.userinfo:
2160            nick = string.split(user,"!")[0]
2161            self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)])
2162
2163    def ctcpQuery_CLIENTINFO(self, user, channel, data):
2164        """
2165        A master index of what CTCP tags this client knows.
2166
2167        If no arguments are provided, respond with a list of known tags.
2168        If an argument is provided, provide human-readable help on
2169        the usage of that tag.
2170        """
2171
2172        nick = string.split(user,"!")[0]
2173        if not data:
2174            # XXX: prefixedMethodNames gets methods from my *class*,
2175            # but it's entirely possible that this *instance* has more
2176            # methods.
2177            names = reflect.prefixedMethodNames(self.__class__,
2178                                                'ctcpQuery_')
2179
2180            self.ctcpMakeReply(nick, [('CLIENTINFO',
2181                                       string.join(names, ' '))])
2182        else:
2183            args = string.split(data)
2184            method = getattr(self, 'ctcpQuery_%s' % (args[0],), None)
2185            if not method:
2186                self.ctcpMakeReply(nick, [('ERRMSG',
2187                                           "CLIENTINFO %s :"
2188                                           "Unknown query '%s'"
2189                                           % (data, args[0]))])
2190                return
2191            doc = getattr(method, '__doc__', '')
2192            self.ctcpMakeReply(nick, [('CLIENTINFO', doc)])
2193
2194
2195    def ctcpQuery_ERRMSG(self, user, channel, data):
2196        # Yeah, this seems strange, but that's what the spec says to do
2197        # when faced with an ERRMSG query (not a reply).
2198        nick = string.split(user,"!")[0]
2199        self.ctcpMakeReply(nick, [('ERRMSG',
2200                                   "%s :No error has occoured." % data)])
2201
2202    def ctcpQuery_TIME(self, user, channel, data):
2203        if data is not None:
2204            self.quirkyMessage("Why did %s send '%s' with a TIME query?"
2205                               % (user, data))
2206        nick = string.split(user,"!")[0]
2207        self.ctcpMakeReply(nick,
2208                           [('TIME', ':%s' %
2209                             time.asctime(time.localtime(time.time())))])
2210
2211    def ctcpQuery_DCC(self, user, channel, data):
2212        """Initiate a Direct Client Connection
2213        """
2214
2215        if not data: return
2216        dcctype = data.split(None, 1)[0].upper()
2217        handler = getattr(self, "dcc_" + dcctype, None)
2218        if handler:
2219            if self.dcc_sessions is None:
2220                self.dcc_sessions = []
2221            data = data[len(dcctype)+1:]
2222            handler(user, channel, data)
2223        else:
2224            nick = string.split(user,"!")[0]
2225            self.ctcpMakeReply(nick, [('ERRMSG',
2226                                       "DCC %s :Unknown DCC type '%s'"
2227                                       % (data, dcctype))])
2228            self.quirkyMessage("%s offered unknown DCC type %s"
2229                               % (user, dcctype))
2230
2231    def dcc_SEND(self, user, channel, data):
2232        # Use splitQuoted for those who send files with spaces in the names.
2233        data = text.splitQuoted(data)
2234        if len(data) < 3:
2235            raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,)
2236
2237        (filename, address, port) = data[:3]
2238
2239        address = dccParseAddress(address)
2240        try:
2241            port = int(port)
2242        except ValueError:
2243            raise IRCBadMessage, "Indecipherable port %r" % (port,)
2244
2245        size = -1
2246        if len(data) >= 4:
2247            try:
2248                size = int(data[3])
2249            except ValueError:
2250                pass
2251
2252        # XXX Should we bother passing this data?
2253        self.dccDoSend(user, address, port, filename, size, data)
2254
2255    def dcc_ACCEPT(self, user, channel, data):
2256        data = text.splitQuoted(data)
2257        if len(data) < 3:
2258            raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,)
2259        (filename, port, resumePos) = data[:3]
2260        try:
2261            port = int(port)
2262            resumePos = int(resumePos)
2263        except ValueError:
2264            return
2265
2266        self.dccDoAcceptResume(user, filename, port, resumePos)
2267
2268    def dcc_RESUME(self, user, channel, data):
2269        data = text.splitQuoted(data)
2270        if len(data) < 3:
2271            raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,)
2272        (filename, port, resumePos) = data[:3]
2273        try:
2274            port = int(port)
2275            resumePos = int(resumePos)
2276        except ValueError:
2277            return
2278        self.dccDoResume(user, filename, port, resumePos)
2279
2280    def dcc_CHAT(self, user, channel, data):
2281        data = text.splitQuoted(data)
2282        if len(data) < 3:
2283            raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,)
2284
2285        (filename, address, port) = data[:3]
2286
2287        address = dccParseAddress(address)
2288        try:
2289            port = int(port)
2290        except ValueError:
2291            raise IRCBadMessage, "Indecipherable port %r" % (port,)
2292
2293        self.dccDoChat(user, channel, address, port, data)
2294
2295    ### The dccDo methods are the slightly higher-level siblings of
2296    ### common dcc_ methods; the arguments have been parsed for them.
2297
2298    def dccDoSend(self, user, address, port, fileName, size, data):
2299        """Called when I receive a DCC SEND offer from a client.
2300
2301        By default, I do nothing here."""
2302        ## filename = path.basename(arg)
2303        ## protocol = DccFileReceive(filename, size,
2304        ##                           (user,channel,data),self.dcc_destdir)
2305        ## reactor.clientTCP(address, port, protocol)
2306        ## self.dcc_sessions.append(protocol)
2307        pass
2308
2309    def dccDoResume(self, user, file, port, resumePos):
2310        """Called when a client is trying to resume an offered file
2311        via DCC send.  It should be either replied to with a DCC
2312        ACCEPT or ignored (default)."""
2313        pass
2314
2315    def dccDoAcceptResume(self, user, file, port, resumePos):
2316        """Called when a client has verified and accepted a DCC resume
2317        request made by us.  By default it will do nothing."""
2318        pass
2319
2320    def dccDoChat(self, user, channel, address, port, data):
2321        pass
2322        #factory = DccChatFactory(self, queryData=(user, channel, data))
2323        #reactor.connectTCP(address, port, factory)
2324        #self.dcc_sessions.append(factory)
2325
2326    #def ctcpQuery_SED(self, user, data):
2327    #    """Simple Encryption Doodoo
2328    #
2329    #    Feel free to implement this, but no specification is available.
2330    #    """
2331    #    raise NotImplementedError
2332
2333
2334    def ctcpMakeReply(self, user, messages):
2335        """
2336        Send one or more C{extended messages} as a CTCP reply.
2337
2338        @type messages: a list of extended messages.  An extended
2339        message is a (tag, data) tuple, where 'data' may be C{None}.
2340        """
2341        self.notice(user, ctcpStringify(messages))
2342
2343    ### client CTCP query commands
2344
2345    def ctcpMakeQuery(self, user, messages):
2346        """
2347        Send one or more C{extended messages} as a CTCP query.
2348
2349        @type messages: a list of extended messages.  An extended
2350        message is a (tag, data) tuple, where 'data' may be C{None}.
2351        """
2352        self.msg(user, ctcpStringify(messages))
2353
2354    ### Receiving a response to a CTCP query (presumably to one we made)
2355    ### You may want to add methods here, or override UnknownReply.
2356
2357    def ctcpReply(self, user, channel, messages):
2358        """
2359        Dispatch method for any CTCP replies received.
2360        """
2361        for m in messages:
2362            method = getattr(self, "ctcpReply_%s" % m[0], None)
2363            if method:
2364                method(user, channel, m[1])
2365            else:
2366                self.ctcpUnknownReply(user, channel, m[0], m[1])
2367
2368    def ctcpReply_PING(self, user, channel, data):
2369        nick = user.split('!', 1)[0]
2370        if (not self._pings) or (not self._pings.has_key((nick, data))):
2371            raise IRCBadMessage,\
2372                  "Bogus PING response from %s: %s" % (user, data)
2373
2374        t0 = self._pings[(nick, data)]
2375        self.pong(user, time.time() - t0)
2376
2377    def ctcpUnknownReply(self, user, channel, tag, data):
2378        """Called when a fitting ctcpReply_ method is not found.
2379
2380        XXX: If the client makes arbitrary CTCP queries,
2381        this method should probably show the responses to
2382        them instead of treating them as anomolies.
2383        """
2384        log.msg("Unknown CTCP reply from %s: %s %s\n"
2385                 % (user, tag, data))
2386
2387    ### Error handlers
2388    ### You may override these with something more appropriate to your UI.
2389
2390    def badMessage(self, line, excType, excValue, tb):
2391        """When I get a message that's so broken I can't use it.
2392        """
2393        log.msg(line)
2394        log.msg(string.join(traceback.format_exception(excType,
2395                                                        excValue,
2396                                                        tb),''))
2397
2398    def quirkyMessage(self, s):
2399        """This is called when I receive a message which is peculiar,
2400        but not wholly indecipherable.
2401        """
2402        log.msg(s + '\n')
2403
2404    ### Protocool methods
2405
2406    def connectionMade(self):
2407        self.supported = ServerSupportedFeatures()
2408        self._queue = []
2409        if self.performLogin:
2410            self.register(self.nickname)
2411
2412    def dataReceived(self, data):
2413        basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
2414
2415    def lineReceived(self, line):
2416        line = lowDequote(line)
2417        try:
2418            prefix, command, params = parsemsg(line)
2419            if numeric_to_symbolic.has_key(command):
2420                command = numeric_to_symbolic[command]
2421            self.handleCommand(command, prefix, params)
2422        except IRCBadMessage:
2423            self.badMessage(line, *sys.exc_info())
2424
2425
2426    def getUserModeParams(self):
2427        """
2428        Get user modes that require parameters for correct parsing.
2429
2430        @rtype: C{[str, str]}
2431        @return C{[add, remove]}
2432        """
2433        return ['', '']
2434
2435
2436    def getChannelModeParams(self):
2437        """
2438        Get channel modes that require parameters for correct parsing.
2439
2440        @rtype: C{[str, str]}
2441        @return C{[add, remove]}
2442        """
2443        # PREFIX modes are treated as "type B" CHANMODES, they always take
2444        # parameter.
2445        params = ['', '']
2446        prefixes = self.supported.getFeature('PREFIX', {})
2447        params[0] = params[1] = ''.join(prefixes.iterkeys())
2448
2449        chanmodes = self.supported.getFeature('CHANMODES')
2450        if chanmodes is not None:
2451            params[0] += chanmodes.get('addressModes', '')
2452            params[0] += chanmodes.get('param', '')
2453            params[1] = params[0]
2454            params[0] += chanmodes.get('setParam', '')
2455        return params
2456
2457
2458    def handleCommand(self, command, prefix, params):
2459        """Determine the function to call for the given command and call
2460        it with the given arguments.
2461        """
2462        method = getattr(self, "irc_%s" % command, None)
2463        try:
2464            if method is not None:
2465                method(prefix, params)
2466            else:
2467                self.irc_unknown(prefix, command, params)
2468        except:
2469            log.deferr()
2470
2471
2472    def __getstate__(self):
2473        dct = self.__dict__.copy()
2474        dct['dcc_sessions'] = None
2475        dct['_pings'] = None
2476        return dct
2477
2478
2479def dccParseAddress(address):
2480    if '.' in address:
2481        pass
2482    else:
2483        try:
2484            address = long(address)
2485        except ValueError:
2486            raise IRCBadMessage,\
2487                  "Indecipherable address %r" % (address,)
2488        else:
2489            address = (
2490                (address >> 24) & 0xFF,
2491                (address >> 16) & 0xFF,
2492                (address >> 8) & 0xFF,
2493                address & 0xFF,
2494                )
2495            address = '.'.join(map(str,address))
2496    return address
2497
2498
2499class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
2500    """Bare protocol to receive a Direct Client Connection SEND stream.
2501
2502    This does enough to keep the other guy talking, but you'll want to
2503    extend my dataReceived method to *do* something with the data I get.
2504    """
2505
2506    bytesReceived = 0
2507
2508    def __init__(self, resumeOffset=0):
2509        self.bytesReceived = resumeOffset
2510        self.resume = (resumeOffset != 0)
2511
2512    def dataReceived(self, data):
2513        """Called when data is received.
2514
2515        Warning: This just acknowledges to the remote host that the
2516        data has been received; it doesn't *do* anything with the
2517        data, so you'll want to override this.
2518        """
2519        self.bytesReceived = self.bytesReceived + len(data)
2520        self.transport.write(struct.pack('!i', self.bytesReceived))
2521
2522
2523class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
2524    """Protocol for an outgoing Direct Client Connection SEND.
2525    """
2526
2527    blocksize = 1024
2528    file = None
2529    bytesSent = 0
2530    completed = 0
2531    connected = 0
2532
2533    def __init__(self, file):
2534        if type(file) is types.StringType:
2535            self.file = open(file, 'r')
2536
2537    def connectionMade(self):
2538        self.connected = 1
2539        self.sendBlock()
2540
2541    def dataReceived(self, data):
2542        # XXX: Do we need to check to see if len(data) != fmtsize?
2543
2544        bytesShesGot = struct.unpack("!I", data)
2545        if bytesShesGot < self.bytesSent:
2546            # Wait for her.
2547            # XXX? Add some checks to see if we've stalled out?
2548            return
2549        elif bytesShesGot > self.bytesSent:
2550            # self.transport.log("DCC SEND %s: She says she has %d bytes "
2551            #                    "but I've only sent %d.  I'm stopping "
2552            #                    "this screwy transfer."
2553            #                    % (self.file,
2554            #                       bytesShesGot, self.bytesSent))
2555            self.transport.loseConnection()
2556            return
2557
2558        self.sendBlock()
2559
2560    def sendBlock(self):
2561        block = self.file.read(self.blocksize)
2562        if block:
2563            self.transport.write(block)
2564            self.bytesSent = self.bytesSent + len(block)
2565        else:
2566            # Nothing more to send, transfer complete.
2567            self.transport.loseConnection()
2568            self.completed = 1
2569
2570    def connectionLost(self, reason):
2571        self.connected = 0
2572        if hasattr(self.file, "close"):
2573            self.file.close()
2574
2575
2576class DccSendFactory(protocol.Factory):
2577    protocol = DccSendProtocol
2578    def __init__(self, file):
2579        self.file = file
2580
2581    def buildProtocol(self, connection):
2582        p = self.protocol(self.file)
2583        p.factory = self
2584        return p
2585
2586
2587def fileSize(file):
2588    """I'll try my damndest to determine the size of this file object.
2589    """
2590    size = None
2591    if hasattr(file, "fileno"):
2592        fileno = file.fileno()
2593        try:
2594            stat_ = os.fstat(fileno)
2595            size = stat_[stat.ST_SIZE]
2596        except:
2597            pass
2598        else:
2599            return size
2600
2601    if hasattr(file, "name") and path.exists(file.name):
2602        try:
2603            size = path.getsize(file.name)
2604        except:
2605            pass
2606        else:
2607            return size
2608
2609    if hasattr(file, "seek") and hasattr(file, "tell"):
2610        try:
2611            try:
2612                file.seek(0, 2)
2613                size = file.tell()
2614            finally:
2615                file.seek(0, 0)
2616        except:
2617            pass
2618        else:
2619            return size
2620
2621    return size
2622
2623class DccChat(basic.LineReceiver, styles.Ephemeral):
2624    """Direct Client Connection protocol type CHAT.
2625
2626    DCC CHAT is really just your run o' the mill basic.LineReceiver
2627    protocol.  This class only varies from that slightly, accepting
2628    either LF or CR LF for a line delimeter for incoming messages
2629    while always using CR LF for outgoing.
2630
2631    The lineReceived method implemented here uses the DCC connection's
2632    'client' attribute (provided upon construction) to deliver incoming
2633    lines from the DCC chat via IRCClient's normal privmsg interface.
2634    That's something of a spoof, which you may well want to override.
2635    """
2636
2637    queryData = None
2638    delimiter = CR + NL
2639    client = None
2640    remoteParty = None
2641    buffer = ""
2642
2643    def __init__(self, client, queryData=None):
2644        """Initialize a new DCC CHAT session.
2645
2646        queryData is a 3-tuple of
2647        (fromUser, targetUserOrChannel, data)
2648        as received by the CTCP query.
2649
2650        (To be honest, fromUser is the only thing that's currently
2651        used here. targetUserOrChannel is potentially useful, while
2652        the 'data' argument is soley for informational purposes.)
2653        """
2654        self.client = client
2655        if queryData:
2656            self.queryData = queryData
2657            self.remoteParty = self.queryData[0]
2658
2659    def dataReceived(self, data):
2660        self.buffer = self.buffer + data
2661        lines = string.split(self.buffer, LF)
2662        # Put the (possibly empty) element after the last LF back in the
2663        # buffer
2664        self.buffer = lines.pop()
2665
2666        for line in lines:
2667            if line[-1] == CR:
2668                line = line[:-1]
2669            self.lineReceived(line)
2670
2671    def lineReceived(self, line):
2672        log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line))
2673        self.client.privmsg(self.remoteParty,
2674                            self.client.nickname, line)
2675
2676
2677class DccChatFactory(protocol.ClientFactory):
2678    protocol = DccChat
2679    noisy = 0
2680    def __init__(self, client, queryData):
2681        self.client = client
2682        self.queryData = queryData
2683
2684
2685    def buildProtocol(self, addr):
2686        p = self.protocol(client=self.client, queryData=self.queryData)
2687        p.factory = self
2688        return p
2689
2690
2691    def clientConnectionFailed(self, unused_connector, unused_reason):
2692        self.client.dcc_sessions.remove(self)
2693
2694    def clientConnectionLost(self, unused_connector, unused_reason):
2695        self.client.dcc_sessions.remove(self)
2696
2697
2698def dccDescribe(data):
2699    """Given the data chunk from a DCC query, return a descriptive string.
2700    """
2701
2702    orig_data = data
2703    data = string.split(data)
2704    if len(data) < 4:
2705        return orig_data
2706
2707    (dcctype, arg, address, port) = data[:4]
2708
2709    if '.' in address:
2710        pass
2711    else:
2712        try:
2713            address = long(address)
2714        except ValueError:
2715            pass
2716        else:
2717            address = (
2718                (address >> 24) & 0xFF,
2719                (address >> 16) & 0xFF,
2720                (address >> 8) & 0xFF,
2721                address & 0xFF,
2722                )
2723            # The mapping to 'int' is to get rid of those accursed
2724            # "L"s which python 1.5.2 puts on the end of longs.
2725            address = string.join(map(str,map(int,address)), ".")
2726
2727    if dcctype == 'SEND':
2728        filename = arg
2729
2730        size_txt = ''
2731        if len(data) >= 5:
2732            try:
2733                size = int(data[4])
2734                size_txt = ' of size %d bytes' % (size,)
2735            except ValueError:
2736                pass
2737
2738        dcc_text = ("SEND for file '%s'%s at host %s, port %s"
2739                    % (filename, size_txt, address, port))
2740    elif dcctype == 'CHAT':
2741        dcc_text = ("CHAT for host %s, port %s"
2742                    % (address, port))
2743    else:
2744        dcc_text = orig_data
2745
2746    return dcc_text
2747
2748
2749class DccFileReceive(DccFileReceiveBasic):
2750    """Higher-level coverage for getting a file from DCC SEND.
2751
2752    I allow you to change the file's name and destination directory.
2753    I won't overwrite an existing file unless I've been told it's okay
2754    to do so. If passed the resumeOffset keyword argument I will attempt to
2755    resume the file from that amount of bytes.
2756
2757    XXX: I need to let the client know when I am finished.
2758    XXX: I need to decide how to keep a progress indicator updated.
2759    XXX: Client needs a way to tell me "Do not finish until I say so."
2760    XXX: I need to make sure the client understands if the file cannot be written.
2761    """
2762
2763    filename = 'dcc'
2764    fileSize = -1
2765    destDir = '.'
2766    overwrite = 0
2767    fromUser = None
2768    queryData = None
2769
2770    def __init__(self, filename, fileSize=-1, queryData=None,
2771                 destDir='.', resumeOffset=0):
2772        DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
2773        self.filename = filename
2774        self.destDir = destDir
2775        self.fileSize = fileSize
2776
2777        if queryData:
2778            self.queryData = queryData
2779            self.fromUser = self.queryData[0]
2780
2781    def set_directory(self, directory):
2782        """Set the directory where the downloaded file will be placed.
2783
2784        May raise OSError if the supplied directory path is not suitable.
2785        """
2786        if not path.exists(directory):
2787            raise OSError(errno.ENOENT, "You see no directory there.",
2788                          directory)
2789        if not path.isdir(directory):
2790            raise OSError(errno.ENOTDIR, "You cannot put a file into "
2791                          "something which is not a directory.",
2792                          directory)
2793        if not os.access(directory, os.X_OK | os.W_OK):
2794            raise OSError(errno.EACCES,
2795                          "This directory is too hard to write in to.",
2796                          directory)
2797        self.destDir = directory
2798
2799    def set_filename(self, filename):
2800        """Change the name of the file being transferred.
2801
2802        This replaces the file name provided by the sender.
2803        """
2804        self.filename = filename
2805
2806    def set_overwrite(self, boolean):
2807        """May I overwrite existing files?
2808        """
2809        self.overwrite = boolean
2810
2811
2812    # Protocol-level methods.
2813
2814    def connectionMade(self):
2815        dst = path.abspath(path.join(self.destDir,self.filename))
2816        exists = path.exists(dst)
2817        if self.resume and exists:
2818            # I have been told I want to resume, and a file already
2819            # exists - Here we go
2820            self.file = open(dst, 'ab')
2821            log.msg("Attempting to resume %s - starting from %d bytes" %
2822                    (self.file, self.file.tell()))
2823        elif self.overwrite or not exists:
2824            self.file = open(dst, 'wb')
2825        else:
2826            raise OSError(errno.EEXIST,
2827                          "There's a file in the way.  "
2828                          "Perhaps that's why you cannot open it.",
2829                          dst)
2830
2831    def dataReceived(self, data):
2832        self.file.write(data)
2833        DccFileReceiveBasic.dataReceived(self, data)
2834
2835        # XXX: update a progress indicator here?
2836
2837    def connectionLost(self, reason):
2838        """When the connection is lost, I close the file.
2839        """
2840        self.connected = 0
2841        logmsg = ("%s closed." % (self,))
2842        if self.fileSize > 0:
2843            logmsg = ("%s  %d/%d bytes received"
2844                      % (logmsg, self.bytesReceived, self.fileSize))
2845            if self.bytesReceived == self.fileSize:
2846                pass # Hooray!
2847            elif self.bytesReceived < self.fileSize:
2848                logmsg = ("%s (Warning: %d bytes short)"
2849                          % (logmsg, self.fileSize - self.bytesReceived))
2850            else:
2851                logmsg = ("%s (file larger than expected)"
2852                          % (logmsg,))
2853        else:
2854            logmsg = ("%s  %d bytes received"
2855                      % (logmsg, self.bytesReceived))
2856
2857        if hasattr(self, 'file'):
2858            logmsg = "%s and written to %s.\n" % (logmsg, self.file.name)
2859            if hasattr(self.file, 'close'): self.file.close()
2860
2861        # self.transport.log(logmsg)
2862
2863    def __str__(self):
2864        if not self.connected:
2865            return "<Unconnected DccFileReceive object at %x>" % (id(self),)
2866        from_ = self.transport.getPeer()
2867        if self.fromUser:
2868            from_ = "%s (%s)" % (self.fromUser, from_)
2869
2870        s = ("DCC transfer of '%s' from %s" % (self.filename, from_))
2871        return s
2872
2873    def __repr__(self):
2874        s = ("<%s at %x: GET %s>"
2875             % (self.__class__, id(self), self.filename))
2876        return s
2877
2878
2879# CTCP constants and helper functions
2880
2881X_DELIM = chr(001)
2882
2883def ctcpExtract(message):
2884    """
2885    Extract CTCP data from a string.
2886
2887    @return: A C{dict} containing two keys:
2888       - C{'extended'}: A list of CTCP (tag, data) tuples.
2889       - C{'normal'}: A list of strings which were not inside a CTCP delimiter.
2890    """
2891    extended_messages = []
2892    normal_messages = []
2893    retval = {'extended': extended_messages,
2894              'normal': normal_messages }
2895
2896    messages = string.split(message, X_DELIM)
2897    odd = 0
2898
2899    # X1 extended data X2 nomal data X3 extended data X4 normal...
2900    while messages:
2901        if odd:
2902            extended_messages.append(messages.pop(0))
2903        else:
2904            normal_messages.append(messages.pop(0))
2905        odd = not odd
2906
2907    extended_messages[:] = filter(None, extended_messages)
2908    normal_messages[:] = filter(None, normal_messages)
2909
2910    extended_messages[:] = map(ctcpDequote, extended_messages)
2911    for i in xrange(len(extended_messages)):
2912        m = string.split(extended_messages[i], SPC, 1)
2913        tag = m[0]
2914        if len(m) > 1:
2915            data = m[1]
2916        else:
2917            data = None
2918
2919        extended_messages[i] = (tag, data)
2920
2921    return retval
2922
2923# CTCP escaping
2924
2925M_QUOTE= chr(020)
2926
2927mQuoteTable = {
2928    NUL: M_QUOTE + '0',
2929    NL: M_QUOTE + 'n',
2930    CR: M_QUOTE + 'r',
2931    M_QUOTE: M_QUOTE + M_QUOTE
2932    }
2933
2934mDequoteTable = {}
2935for k, v in mQuoteTable.items():
2936    mDequoteTable[v[-1]] = k
2937del k, v
2938
2939mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
2940
2941def lowQuote(s):
2942    for c in (M_QUOTE, NUL, NL, CR):
2943        s = string.replace(s, c, mQuoteTable[c])
2944    return s
2945
2946def lowDequote(s):
2947    def sub(matchobj, mDequoteTable=mDequoteTable):
2948        s = matchobj.group()[1]
2949        try:
2950            s = mDequoteTable[s]
2951        except KeyError:
2952            s = s
2953        return s
2954
2955    return mEscape_re.sub(sub, s)
2956
2957X_QUOTE = '\\'
2958
2959xQuoteTable = {
2960    X_DELIM: X_QUOTE + 'a',
2961    X_QUOTE: X_QUOTE + X_QUOTE
2962    }
2963
2964xDequoteTable = {}
2965
2966for k, v in xQuoteTable.items():
2967    xDequoteTable[v[-1]] = k
2968
2969xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
2970
2971def ctcpQuote(s):
2972    for c in (X_QUOTE, X_DELIM):
2973        s = string.replace(s, c, xQuoteTable[c])
2974    return s
2975
2976def ctcpDequote(s):
2977    def sub(matchobj, xDequoteTable=xDequoteTable):
2978        s = matchobj.group()[1]
2979        try:
2980            s = xDequoteTable[s]
2981        except KeyError:
2982            s = s
2983        return s
2984
2985    return xEscape_re.sub(sub, s)
2986
2987def ctcpStringify(messages):
2988    """
2989    @type messages: a list of extended messages.  An extended
2990    message is a (tag, data) tuple, where 'data' may be C{None}, a
2991    string, or a list of strings to be joined with whitespace.
2992
2993    @returns: String
2994    """
2995    coded_messages = []
2996    for (tag, data) in messages:
2997        if data:
2998            if not isinstance(data, types.StringType):
2999                try:
3000                    # data as list-of-strings
3001                    data = " ".join(map(str, data))
3002                except TypeError:
3003                    # No?  Then use it's %s representation.
3004                    pass
3005            m = "%s %s" % (tag, data)
3006        else:
3007            m = str(tag)
3008        m = ctcpQuote(m)
3009        m = "%s%s%s" % (X_DELIM, m, X_DELIM)
3010        coded_messages.append(m)
3011
3012    line = string.join(coded_messages, '')
3013    return line
3014
3015
3016# Constants (from RFC 2812)
3017RPL_WELCOME = '001'
3018RPL_YOURHOST = '002'
3019RPL_CREATED = '003'
3020RPL_MYINFO = '004'
3021RPL_ISUPPORT = '005'
3022RPL_BOUNCE = '010'
3023RPL_USERHOST = '302'
3024RPL_ISON = '303'
3025RPL_AWAY = '301'
3026RPL_UNAWAY = '305'
3027RPL_NOWAWAY = '306'
3028RPL_WHOISUSER = '311'
3029RPL_WHOISSERVER = '312'
3030RPL_WHOISOPERATOR = '313'
3031RPL_WHOISIDLE = '317'
3032RPL_ENDOFWHOIS = '318'
3033RPL_WHOISCHANNELS = '319'
3034RPL_WHOWASUSER = '314'
3035RPL_ENDOFWHOWAS = '369'
3036RPL_LISTSTART = '321'
3037RPL_LIST = '322'
3038RPL_LISTEND = '323'
3039RPL_UNIQOPIS = '325'
3040RPL_CHANNELMODEIS = '324'
3041RPL_NOTOPIC = '331'
3042RPL_TOPIC = '332'
3043RPL_INVITING = '341'
3044RPL_SUMMONING = '342'
3045RPL_INVITELIST = '346'
3046RPL_ENDOFINVITELIST = '347'
3047RPL_EXCEPTLIST = '348'
3048RPL_ENDOFEXCEPTLIST = '349'
3049RPL_VERSION = '351'
3050RPL_WHOREPLY = '352'
3051RPL_ENDOFWHO = '315'
3052RPL_NAMREPLY = '353'
3053RPL_ENDOFNAMES = '366'
3054RPL_LINKS = '364'
3055RPL_ENDOFLINKS = '365'
3056RPL_BANLIST = '367'
3057RPL_ENDOFBANLIST = '368'
3058RPL_INFO = '371'
3059RPL_ENDOFINFO = '374'
3060RPL_MOTDSTART = '375'
3061RPL_MOTD = '372'
3062RPL_ENDOFMOTD = '376'
3063RPL_YOUREOPER = '381'
3064RPL_REHASHING = '382'
3065RPL_YOURESERVICE = '383'
3066RPL_TIME = '391'
3067RPL_USERSSTART = '392'
3068RPL_USERS = '393'
3069RPL_ENDOFUSERS = '394'
3070RPL_NOUSERS = '395'
3071RPL_TRACELINK = '200'
3072RPL_TRACECONNECTING = '201'
3073RPL_TRACEHANDSHAKE = '202'
3074RPL_TRACEUNKNOWN = '203'
3075RPL_TRACEOPERATOR = '204'
3076RPL_TRACEUSER = '205'
3077RPL_TRACESERVER = '206'
3078RPL_TRACESERVICE = '207'
3079RPL_TRACENEWTYPE = '208'
3080RPL_TRACECLASS = '209'
3081RPL_TRACERECONNECT = '210'
3082RPL_TRACELOG = '261'
3083RPL_TRACEEND = '262'
3084RPL_STATSLINKINFO = '211'
3085RPL_STATSCOMMANDS = '212'
3086RPL_ENDOFSTATS = '219'
3087RPL_STATSUPTIME = '242'
3088RPL_STATSOLINE = '243'
3089RPL_UMODEIS = '221'
3090RPL_SERVLIST = '234'
3091RPL_SERVLISTEND = '235'
3092RPL_LUSERCLIENT = '251'
3093RPL_LUSEROP = '252'
3094RPL_LUSERUNKNOWN = '253'
3095RPL_LUSERCHANNELS = '254'
3096RPL_LUSERME = '255'
3097RPL_ADMINME = '256'
3098RPL_ADMINLOC = '257'
3099RPL_ADMINLOC = '258'
3100RPL_ADMINEMAIL = '259'
3101RPL_TRYAGAIN = '263'
3102ERR_NOSUCHNICK = '401'
3103ERR_NOSUCHSERVER = '402'
3104ERR_NOSUCHCHANNEL = '403'
3105ERR_CANNOTSENDTOCHAN = '404'
3106ERR_TOOMANYCHANNELS = '405'
3107ERR_WASNOSUCHNICK = '406'
3108ERR_TOOMANYTARGETS = '407'
3109ERR_NOSUCHSERVICE = '408'
3110ERR_NOORIGIN = '409'
3111ERR_NORECIPIENT = '411'
3112ERR_NOTEXTTOSEND = '412'
3113ERR_NOTOPLEVEL = '413'
3114ERR_WILDTOPLEVEL = '414'
3115ERR_BADMASK = '415'
3116ERR_UNKNOWNCOMMAND = '421'
3117ERR_NOMOTD = '422'
3118ERR_NOADMININFO = '423'
3119ERR_FILEERROR = '424'
3120ERR_NONICKNAMEGIVEN = '431'
3121ERR_ERRONEUSNICKNAME = '432'
3122ERR_NICKNAMEINUSE = '433'
3123ERR_NICKCOLLISION = '436'
3124ERR_UNAVAILRESOURCE = '437'
3125ERR_USERNOTINCHANNEL = '441'
3126ERR_NOTONCHANNEL = '442'
3127ERR_USERONCHANNEL = '443'
3128ERR_NOLOGIN = '444'
3129ERR_SUMMONDISABLED = '445'
3130ERR_USERSDISABLED = '446'
3131ERR_NOTREGISTERED = '451'
3132ERR_NEEDMOREPARAMS = '461'
3133ERR_ALREADYREGISTRED = '462'
3134ERR_NOPERMFORHOST = '463'
3135ERR_PASSWDMISMATCH = '464'
3136ERR_YOUREBANNEDCREEP = '465'
3137ERR_YOUWILLBEBANNED = '466'
3138ERR_KEYSET = '467'
3139ERR_CHANNELISFULL = '471'
3140ERR_UNKNOWNMODE = '472'
3141ERR_INVITEONLYCHAN = '473'
3142ERR_BANNEDFROMCHAN = '474'
3143ERR_BADCHANNELKEY = '475'
3144ERR_BADCHANMASK = '476'
3145ERR_NOCHANMODES = '477'
3146ERR_BANLISTFULL = '478'
3147ERR_NOPRIVILEGES = '481'
3148ERR_CHANOPRIVSNEEDED = '482'
3149ERR_CANTKILLSERVER = '483'
3150ERR_RESTRICTED = '484'
3151ERR_UNIQOPPRIVSNEEDED = '485'
3152ERR_NOOPERHOST = '491'
3153ERR_NOSERVICEHOST = '492'
3154ERR_UMODEUNKNOWNFLAG = '501'
3155ERR_USERSDONTMATCH = '502'
3156
3157# And hey, as long as the strings are already intern'd...
3158symbolic_to_numeric = {
3159    "RPL_WELCOME": '001',
3160    "RPL_YOURHOST": '002',
3161    "RPL_CREATED": '003',
3162    "RPL_MYINFO": '004',
3163    "RPL_ISUPPORT": '005',
3164    "RPL_BOUNCE": '010',
3165    "RPL_USERHOST": '302',
3166    "RPL_ISON": '303',
3167    "RPL_AWAY": '301',
3168    "RPL_UNAWAY": '305',
3169    "RPL_NOWAWAY": '306',
3170    "RPL_WHOISUSER": '311',
3171    "RPL_WHOISSERVER": '312',
3172    "RPL_WHOISOPERATOR": '313',
3173    "RPL_WHOISIDLE": '317',
3174    "RPL_ENDOFWHOIS": '318',
3175    "RPL_WHOISCHANNELS": '319',
3176    "RPL_WHOWASUSER": '314',
3177    "RPL_ENDOFWHOWAS": '369',
3178    "RPL_LISTSTART": '321',
3179    "RPL_LIST": '322',
3180    "RPL_LISTEND": '323',
3181    "RPL_UNIQOPIS": '325',
3182    "RPL_CHANNELMODEIS": '324',
3183    "RPL_NOTOPIC": '331',
3184    "RPL_TOPIC": '332',
3185    "RPL_INVITING": '341',
3186    "RPL_SUMMONING": '342',
3187    "RPL_INVITELIST": '346',
3188    "RPL_ENDOFINVITELIST": '347',
3189    "RPL_EXCEPTLIST": '348',
3190    "RPL_ENDOFEXCEPTLIST": '349',
3191    "RPL_VERSION": '351',
3192    "RPL_WHOREPLY": '352',
3193    "RPL_ENDOFWHO": '315',
3194    "RPL_NAMREPLY": '353',
3195    "RPL_ENDOFNAMES": '366',
3196    "RPL_LINKS": '364',
3197    "RPL_ENDOFLINKS": '365',
3198    "RPL_BANLIST": '367',
3199    "RPL_ENDOFBANLIST": '368',
3200    "RPL_INFO": '371',
3201    "RPL_ENDOFINFO": '374',
3202    "RPL_MOTDSTART": '375',
3203    "RPL_MOTD": '372',
3204    "RPL_ENDOFMOTD": '376',
3205    "RPL_YOUREOPER": '381',
3206    "RPL_REHASHING": '382',
3207    "RPL_YOURESERVICE": '383',
3208    "RPL_TIME": '391',
3209    "RPL_USERSSTART": '392',
3210    "RPL_USERS": '393',
3211    "RPL_ENDOFUSERS": '394',
3212    "RPL_NOUSERS": '395',
3213    "RPL_TRACELINK": '200',
3214    "RPL_TRACECONNECTING": '201',
3215    "RPL_TRACEHANDSHAKE": '202',
3216    "RPL_TRACEUNKNOWN": '203',
3217    "RPL_TRACEOPERATOR": '204',
3218    "RPL_TRACEUSER": '205',
3219    "RPL_TRACESERVER": '206',
3220    "RPL_TRACESERVICE": '207',
3221    "RPL_TRACENEWTYPE": '208',
3222    "RPL_TRACECLASS": '209',
3223    "RPL_TRACERECONNECT": '210',
3224    "RPL_TRACELOG": '261',
3225    "RPL_TRACEEND": '262',
3226    "RPL_STATSLINKINFO": '211',
3227    "RPL_STATSCOMMANDS": '212',
3228    "RPL_ENDOFSTATS": '219',
3229    "RPL_STATSUPTIME": '242',
3230    "RPL_STATSOLINE": '243',
3231    "RPL_UMODEIS": '221',
3232    "RPL_SERVLIST": '234',
3233    "RPL_SERVLISTEND": '235',
3234    "RPL_LUSERCLIENT": '251',
3235    "RPL_LUSEROP": '252',
3236    "RPL_LUSERUNKNOWN": '253',
3237    "RPL_LUSERCHANNELS": '254',
3238    "RPL_LUSERME": '255',
3239    "RPL_ADMINME": '256',
3240    "RPL_ADMINLOC": '257',
3241    "RPL_ADMINLOC": '258',
3242    "RPL_ADMINEMAIL": '259',
3243    "RPL_TRYAGAIN": '263',
3244    "ERR_NOSUCHNICK": '401',
3245    "ERR_NOSUCHSERVER": '402',
3246    "ERR_NOSUCHCHANNEL": '403',
3247    "ERR_CANNOTSENDTOCHAN": '404',
3248    "ERR_TOOMANYCHANNELS": '405',
3249    "ERR_WASNOSUCHNICK": '406',
3250    "ERR_TOOMANYTARGETS": '407',
3251    "ERR_NOSUCHSERVICE": '408',
3252    "ERR_NOORIGIN": '409',
3253    "ERR_NORECIPIENT": '411',
3254    "ERR_NOTEXTTOSEND": '412',
3255    "ERR_NOTOPLEVEL": '413',
3256    "ERR_WILDTOPLEVEL": '414',
3257    "ERR_BADMASK": '415',
3258    "ERR_UNKNOWNCOMMAND": '421',
3259    "ERR_NOMOTD": '422',
3260    "ERR_NOADMININFO": '423',
3261    "ERR_FILEERROR": '424',
3262    "ERR_NONICKNAMEGIVEN": '431',
3263    "ERR_ERRONEUSNICKNAME": '432',
3264    "ERR_NICKNAMEINUSE": '433',
3265    "ERR_NICKCOLLISION": '436',
3266    "ERR_UNAVAILRESOURCE": '437',
3267    "ERR_USERNOTINCHANNEL": '441',
3268    "ERR_NOTONCHANNEL": '442',
3269    "ERR_USERONCHANNEL": '443',
3270    "ERR_NOLOGIN": '444',
3271    "ERR_SUMMONDISABLED": '445',
3272    "ERR_USERSDISABLED": '446',
3273    "ERR_NOTREGISTERED": '451',
3274    "ERR_NEEDMOREPARAMS": '461',
3275    "ERR_ALREADYREGISTRED": '462',
3276    "ERR_NOPERMFORHOST": '463',
3277    "ERR_PASSWDMISMATCH": '464',
3278    "ERR_YOUREBANNEDCREEP": '465',
3279    "ERR_YOUWILLBEBANNED": '466',
3280    "ERR_KEYSET": '467',
3281    "ERR_CHANNELISFULL": '471',
3282    "ERR_UNKNOWNMODE": '472',
3283    "ERR_INVITEONLYCHAN": '473',
3284    "ERR_BANNEDFROMCHAN": '474',
3285    "ERR_BADCHANNELKEY": '475',
3286    "ERR_BADCHANMASK": '476',
3287    "ERR_NOCHANMODES": '477',
3288    "ERR_BANLISTFULL": '478',
3289    "ERR_NOPRIVILEGES": '481',
3290    "ERR_CHANOPRIVSNEEDED": '482',
3291    "ERR_CANTKILLSERVER": '483',
3292    "ERR_RESTRICTED": '484',
3293    "ERR_UNIQOPPRIVSNEEDED": '485',
3294    "ERR_NOOPERHOST": '491',
3295    "ERR_NOSERVICEHOST": '492',
3296    "ERR_UMODEUNKNOWNFLAG": '501',
3297    "ERR_USERSDONTMATCH": '502',
3298}
3299
3300numeric_to_symbolic = {}
3301for k, v in symbolic_to_numeric.items():
3302    numeric_to_symbolic[v] = k
Note: See TracBrowser for help on using the browser.