Imported Upstream version 12.1.0
[contrib/python-twisted.git] / twisted / words / protocols / irc.py
1 # -*- test-case-name: twisted.words.test.test_irc -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 Internet Relay Chat Protocol for client and server.
7
8 Future Plans
9 ============
10
11 The way the IRCClient class works here encourages people to implement
12 IRC clients by subclassing the ephemeral protocol class, and it tends
13 to end up with way more state than it should for an object which will
14 be destroyed as soon as the TCP transport drops.  Someone oughta do
15 something about that, ya know?
16
17 The DCC support needs to have more hooks for the client for it to be
18 able to ask the user things like "Do you want to accept this session?"
19 and "Transfer #2 is 67% done." and otherwise manage the DCC sessions.
20
21 Test 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
34 import errno, os, random, re, stat, struct, sys, time, types, traceback
35 import string, socket
36 import warnings
37 import textwrap
38 from os import path
39
40 from twisted.internet import reactor, protocol, task
41 from twisted.persisted import styles
42 from twisted.protocols import basic
43 from twisted.python import log, reflect, text
44 from twisted.python.compat import set
45
46 NUL = chr(0)
47 CR = chr(015)
48 NL = chr(012)
49 LF = NL
50 SPC = chr(040)
51
52 # This includes the CRLF terminator characters.
53 MAX_COMMAND_LENGTH = 512
54
55 CHANNEL_PREFIXES = '&#!+'
56
57 class IRCBadMessage(Exception):
58     pass
59
60 class IRCPasswordMismatch(Exception):
61     pass
62
63
64
65 class IRCBadModes(ValueError):
66     """
67     A malformed mode was encountered while attempting to parse a mode string.
68     """
69
70
71
72 def 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
92 def 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
113 def _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
130 class UnhandledCommand(RuntimeError):
131     """
132     A command dispatcher could not locate an appropriate command handler.
133     """
134
135
136
137 class _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
176 def 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
235 class 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
623 class 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
971 class 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
2479 def 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
2499 class 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
2523 class 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
2576 class 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
2587 def 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
2623 class 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
2677 class 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
2698 def 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
2749 class 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
2881 X_DELIM = chr(001)
2882
2883 def 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
2925 M_QUOTE= chr(020)
2926
2927 mQuoteTable = {
2928     NUL: M_QUOTE + '0',
2929     NL: M_QUOTE + 'n',
2930     CR: M_QUOTE + 'r',
2931     M_QUOTE: M_QUOTE + M_QUOTE
2932     }
2933
2934 mDequoteTable = {}
2935 for k, v in mQuoteTable.items():
2936     mDequoteTable[v[-1]] = k
2937 del k, v
2938
2939 mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
2940
2941 def lowQuote(s):
2942     for c in (M_QUOTE, NUL, NL, CR):
2943         s = string.replace(s, c, mQuoteTable[c])
2944     return s
2945
2946 def 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
2957 X_QUOTE = '\\'
2958
2959 xQuoteTable = {
2960     X_DELIM: X_QUOTE + 'a',
2961     X_QUOTE: X_QUOTE + X_QUOTE
2962     }
2963
2964 xDequoteTable = {}
2965
2966 for k, v in xQuoteTable.items():
2967     xDequoteTable[v[-1]] = k
2968
2969 xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
2970
2971 def ctcpQuote(s):
2972     for c in (X_QUOTE, X_DELIM):
2973         s = string.replace(s, c, xQuoteTable[c])
2974     return s
2975
2976 def 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
2987 def 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)
3017 RPL_WELCOME = '001'
3018 RPL_YOURHOST = '002'
3019 RPL_CREATED = '003'
3020 RPL_MYINFO = '004'
3021 RPL_ISUPPORT = '005'
3022 RPL_BOUNCE = '010'
3023 RPL_USERHOST = '302'
3024 RPL_ISON = '303'
3025 RPL_AWAY = '301'
3026 RPL_UNAWAY = '305'
3027 RPL_NOWAWAY = '306'
3028 RPL_WHOISUSER = '311'
3029 RPL_WHOISSERVER = '312'
3030 RPL_WHOISOPERATOR = '313'
3031 RPL_WHOISIDLE = '317'
3032 RPL_ENDOFWHOIS = '318'
3033 RPL_WHOISCHANNELS = '319'
3034 RPL_WHOWASUSER = '314'
3035 RPL_ENDOFWHOWAS = '369'
3036 RPL_LISTSTART = '321'
3037 RPL_LIST = '322'
3038 RPL_LISTEND = '323'
3039 RPL_UNIQOPIS = '325'
3040 RPL_CHANNELMODEIS = '324'
3041 RPL_NOTOPIC = '331'
3042 RPL_TOPIC = '332'
3043 RPL_INVITING = '341'
3044 RPL_SUMMONING = '342'
3045 RPL_INVITELIST = '346'
3046 RPL_ENDOFINVITELIST = '347'
3047 RPL_EXCEPTLIST = '348'
3048 RPL_ENDOFEXCEPTLIST = '349'
3049 RPL_VERSION = '351'
3050 RPL_WHOREPLY = '352'
3051 RPL_ENDOFWHO = '315'
3052 RPL_NAMREPLY = '353'
3053 RPL_ENDOFNAMES = '366'
3054 RPL_LINKS = '364'
3055 RPL_ENDOFLINKS = '365'
3056 RPL_BANLIST = '367'
3057 RPL_ENDOFBANLIST = '368'
3058 RPL_INFO = '371'
3059 RPL_ENDOFINFO = '374'
3060 RPL_MOTDSTART = '375'
3061 RPL_MOTD = '372'
3062 RPL_ENDOFMOTD = '376'
3063 RPL_YOUREOPER = '381'
3064 RPL_REHASHING = '382'
3065 RPL_YOURESERVICE = '383'
3066 RPL_TIME = '391'
3067 RPL_USERSSTART = '392'
3068 RPL_USERS = '393'
3069 RPL_ENDOFUSERS = '394'
3070 RPL_NOUSERS = '395'
3071 RPL_TRACELINK = '200'
3072 RPL_TRACECONNECTING = '201'
3073 RPL_TRACEHANDSHAKE = '202'
3074 RPL_TRACEUNKNOWN = '203'
3075 RPL_TRACEOPERATOR = '204'
3076 RPL_TRACEUSER = '205'
3077 RPL_TRACESERVER = '206'
3078 RPL_TRACESERVICE = '207'
3079 RPL_TRACENEWTYPE = '208'
3080 RPL_TRACECLASS = '209'
3081 RPL_TRACERECONNECT = '210'
3082 RPL_TRACELOG = '261'
3083 RPL_TRACEEND = '262'
3084 RPL_STATSLINKINFO = '211'
3085 RPL_STATSCOMMANDS = '212'
3086 RPL_ENDOFSTATS = '219'
3087 RPL_STATSUPTIME = '242'
3088 RPL_STATSOLINE = '243'
3089 RPL_UMODEIS = '221'
3090 RPL_SERVLIST = '234'
3091 RPL_SERVLISTEND = '235'
3092 RPL_LUSERCLIENT = '251'
3093 RPL_LUSEROP = '252'
3094 RPL_LUSERUNKNOWN = '253'
3095 RPL_LUSERCHANNELS = '254'
3096 RPL_LUSERME = '255'
3097 RPL_ADMINME = '256'
3098 RPL_ADMINLOC = '257'
3099 RPL_ADMINLOC = '258'
3100 RPL_ADMINEMAIL = '259'
3101 RPL_TRYAGAIN = '263'
3102 ERR_NOSUCHNICK = '401'
3103 ERR_NOSUCHSERVER = '402'
3104 ERR_NOSUCHCHANNEL = '403'
3105 ERR_CANNOTSENDTOCHAN = '404'
3106 ERR_TOOMANYCHANNELS = '405'
3107 ERR_WASNOSUCHNICK = '406'
3108 ERR_TOOMANYTARGETS = '407'
3109 ERR_NOSUCHSERVICE = '408'
3110 ERR_NOORIGIN = '409'
3111 ERR_NORECIPIENT = '411'
3112 ERR_NOTEXTTOSEND = '412'
3113 ERR_NOTOPLEVEL = '413'
3114 ERR_WILDTOPLEVEL = '414'
3115 ERR_BADMASK = '415'
3116 ERR_UNKNOWNCOMMAND = '421'
3117 ERR_NOMOTD = '422'
3118 ERR_NOADMININFO = '423'
3119 ERR_FILEERROR = '424'
3120 ERR_NONICKNAMEGIVEN = '431'
3121 ERR_ERRONEUSNICKNAME = '432'
3122 ERR_NICKNAMEINUSE = '433'
3123 ERR_NICKCOLLISION = '436'
3124 ERR_UNAVAILRESOURCE = '437'
3125 ERR_USERNOTINCHANNEL = '441'
3126 ERR_NOTONCHANNEL = '442'
3127 ERR_USERONCHANNEL = '443'
3128 ERR_NOLOGIN = '444'
3129 ERR_SUMMONDISABLED = '445'
3130 ERR_USERSDISABLED = '446'
3131 ERR_NOTREGISTERED = '451'
3132 ERR_NEEDMOREPARAMS = '461'
3133 ERR_ALREADYREGISTRED = '462'
3134 ERR_NOPERMFORHOST = '463'
3135 ERR_PASSWDMISMATCH = '464'
3136 ERR_YOUREBANNEDCREEP = '465'
3137 ERR_YOUWILLBEBANNED = '466'
3138 ERR_KEYSET = '467'
3139 ERR_CHANNELISFULL = '471'
3140 ERR_UNKNOWNMODE = '472'
3141 ERR_INVITEONLYCHAN = '473'
3142 ERR_BANNEDFROMCHAN = '474'
3143 ERR_BADCHANNELKEY = '475'
3144 ERR_BADCHANMASK = '476'
3145 ERR_NOCHANMODES = '477'
3146 ERR_BANLISTFULL = '478'
3147 ERR_NOPRIVILEGES = '481'
3148 ERR_CHANOPRIVSNEEDED = '482'
3149 ERR_CANTKILLSERVER = '483'
3150 ERR_RESTRICTED = '484'
3151 ERR_UNIQOPPRIVSNEEDED = '485'
3152 ERR_NOOPERHOST = '491'
3153 ERR_NOSERVICEHOST = '492'
3154 ERR_UMODEUNKNOWNFLAG = '501'
3155 ERR_USERSDONTMATCH = '502'
3156
3157 # And hey, as long as the strings are already intern'd...
3158 symbolic_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
3300 numeric_to_symbolic = {}
3301 for k, v in symbolic_to_numeric.items():
3302     numeric_to_symbolic[v] = k