dreamIRC initial check-in
[enigma2-plugins.git] / dreamirc / src / protocols / irc.py
1 # -*- test-case-name: twisted.words.test.test_irc -*-
2 # Copyright (c) 2001-2005 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 @author: U{Kevin Turner<mailto:acapnotic@twistedmatrix.com>}
24
25 @see: RFC 1459: Internet Relay Chat Protocol
26 @see: RFC 2812: Internet Relay Chat: Client Protocol
27 @see: U{The Client-To-Client-Protocol
28 <http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
29 """
30
31 __version__ = '$Revision$'[11:-2]
32
33 from twisted.internet import reactor, protocol
34 from twisted.persisted import styles
35 from twisted.protocols import basic
36 from twisted.python import log, reflect, text
37
38 # System Imports
39
40 import errno
41 import os
42 import random
43 import re
44 import stat
45 import string
46 import struct
47 import sys
48 import time
49 import types
50 import traceback
51 import socket
52
53 from os import path
54
55 NUL = chr(0)
56 CR = chr(015)
57 NL = chr(012)
58 LF = NL
59 SPC = chr(040)
60
61 CHANNEL_PREFIXES = '&#!+'
62
63 class IRCBadMessage(Exception):
64     pass
65
66 class IRCPasswordMismatch(Exception):
67     pass
68
69 def parsemsg(s):
70     """Breaks a message from an IRC server into its prefix, command, and arguments.
71     """
72     prefix = ''
73     trailing = []
74     if not s:
75         raise IRCBadMessage("Empty line.")
76     if s[0] == ':':
77         prefix, s = s[1:].split(' ', 1)
78     if s.find(' :') != -1:
79         s, trailing = s.split(' :', 1)
80         args = s.split()
81         args.append(trailing)
82     else:
83         args = s.split()
84     command = args.pop(0)
85     return prefix, command, args
86
87
88 def split(str, length = 80):
89     """I break a message into multiple lines.
90
91     I prefer to break at whitespace near str[length].  I also break at \\n.
92
93     @returns: list of strings
94     """
95     if length <= 0:
96         raise ValueError("Length must be a number greater than zero")
97     r = []
98     while len(str) > length:
99         w, n = str[:length].rfind(' '), str[:length].find('\n')
100         if w == -1 and n == -1:
101             line, str = str[:length], str[length:]
102         else:
103             i = n == -1 and w or n
104             line, str = str[:i], str[i+1:]
105         r.append(line)
106     if len(str):
107         r.extend(str.split('\n'))
108     return r
109
110 class IRC(protocol.Protocol):
111     """Internet Relay Chat server protocol.
112     """
113
114     buffer = ""
115     hostname = None
116
117     encoding = None
118
119     def connectionMade(self):
120         self.channels = []
121         if self.hostname is None:
122             self.hostname = socket.getfqdn()
123
124
125     def sendLine(self, line):
126         if self.encoding is not None:
127             if isinstance(line, unicode):
128                 line = line.encode(self.encoding)
129         self.transport.write("%s%s%s" % (line, CR, LF))
130
131
132     def sendMessage(self, command, *parameter_list, **prefix):
133         """Send a line formatted as an IRC message.
134
135         First argument is the command, all subsequent arguments
136         are parameters to that command.  If a prefix is desired,
137         it may be specified with the keyword argument 'prefix'.
138         """
139
140         if not command:
141             raise ValueError, "IRC message requires a command."
142
143         if ' ' in command or command[0] == ':':
144             # Not the ONLY way to screw up, but provides a little
145             # sanity checking to catch likely dumb mistakes.
146             raise ValueError, "Somebody screwed up, 'cuz this doesn't" \
147                   " look like a command to me: %s" % command
148
149         line = string.join([command] + list(parameter_list))
150         if prefix.has_key('prefix'):
151             line = ":%s %s" % (prefix['prefix'], line)
152         self.sendLine(line)
153
154         if len(parameter_list) > 15:
155             log.msg("Message has %d parameters (RFC allows 15):\n%s" %
156                     (len(parameter_list), line))
157
158
159     def dataReceived(self, data):
160         """This hack is to support mIRC, which sends LF only,
161         even though the RFC says CRLF.  (Also, the flexibility
162         of LineReceiver to turn "line mode" on and off was not
163         required.)
164         """
165         lines = (self.buffer + data).split(LF)
166         # Put the (possibly empty) element after the last LF back in the
167         # buffer
168         self.buffer = lines.pop()
169
170         for line in lines:
171             if len(line) <= 2:
172                 # This is a blank line, at best.
173                 continue
174             if line[-1] == CR:
175                 line = line[:-1]
176             prefix, command, params = parsemsg(line)
177             # mIRC is a big pile of doo-doo
178             command = command.upper()
179             # DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
180
181             self.handleCommand(command, prefix, params)
182
183
184     def handleCommand(self, command, prefix, params):
185         """Determine the function to call for the given command and call
186         it with the given arguments.
187         """
188         method = getattr(self, "irc_%s" % command, None)
189         try:
190             if method is not None:
191                 method(prefix, params)
192             else:
193                 self.irc_unknown(prefix, command, params)
194         except:
195             log.deferr()
196
197
198     def irc_unknown(self, prefix, command, params):
199         """Implement me!"""
200         raise NotImplementedError(command, prefix, params)
201
202
203     # Helper methods
204     def privmsg(self, sender, recip, message):
205         """Send a message to a channel or user
206
207         @type sender: C{str} or C{unicode}
208         @param sender: Who is sending this message.  Should be of the form
209         username!ident@hostmask (unless you know better!).
210
211         @type recip: C{str} or C{unicode}
212         @param recip: The recipient of this message.  If a channel, it
213         must start with a channel prefix.
214
215         @type message: C{str} or C{unicode}
216         @param message: The message being sent.
217         """
218         self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message)))
219
220
221     def notice(self, sender, recip, message):
222         """Send a \"notice\" to a channel or user.
223
224         Notices differ from privmsgs in that the RFC claims they are different.
225         Robots are supposed to send notices and not respond to them.  Clients
226         typically display notices differently from privmsgs.
227
228         @type sender: C{str} or C{unicode}
229         @param sender: Who is sending this message.  Should be of the form
230         username!ident@hostmask (unless you know better!).
231
232         @type recip: C{str} or C{unicode}
233         @param recip: The recipient of this message.  If a channel, it
234         must start with a channel prefix.
235
236         @type message: C{str} or C{unicode}
237         @param message: The message being sent.
238         """
239         self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message))
240
241
242     def action(self, sender, recip, message):
243         """Send an action to a channel or user.
244
245         @type sender: C{str} or C{unicode}
246         @param sender: Who is sending this message.  Should be of the form
247         username!ident@hostmask (unless you know better!).
248
249         @type recip: C{str} or C{unicode}
250         @param recip: The recipient of this message.  If a channel, it
251         must start with a channel prefix.
252
253         @type message: C{str} or C{unicode}
254         @param message: The action being sent.
255         """
256         self.sendLine(":%s ACTION %s :%s" % (sender, recip, message))
257
258
259     def topic(self, user, channel, topic, author=None):
260         """Send the topic to a user.
261
262         @type user: C{str} or C{unicode}
263         @param user: The user receiving the topic.  Only their nick name, not
264         the full hostmask.
265
266         @type channel: C{str} or C{unicode}
267         @param channel: The channel for which this is the topic.
268
269         @type topic: C{str} or C{unicode} or C{None}
270         @param topic: The topic string, unquoted, or None if there is
271         no topic.
272
273         @type author: C{str} or C{unicode}
274         @param author: If the topic is being changed, the full username and hostmask
275         of the person changing it.
276         """
277         if author is None:
278             if topic is None:
279                 self.sendLine(':%s %s %s %s :%s' % (
280                     self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.'))
281             else:
282                 self.sendLine(":%s %s %s %s :%s" % (
283                     self.hostname, RPL_TOPIC, user, channel, lowQuote(topic)))
284         else:
285             self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic)))
286
287
288     def topicAuthor(self, user, channel, author, date):
289         """
290         Send the author of and time at which a topic was set for the given
291         channel.
292
293         This sends a 333 reply message, which is not part of the IRC RFC.
294
295         @type user: C{str} or C{unicode}
296         @param user: The user receiving the topic.  Only their nick name, not
297         the full hostmask.
298
299         @type channel: C{str} or C{unicode}
300         @param channel: The channel for which this information is relevant.
301
302         @type author: C{str} or C{unicode}
303         @param author: The nickname (without hostmask) of the user who last
304         set the topic.
305
306         @type date: C{int}
307         @param date: A POSIX timestamp (number of seconds since the epoch)
308         at which the topic was last set.
309         """
310         self.sendLine(':%s %d %s %s %s %d' % (
311             self.hostname, 333, user, channel, author, date))
312
313
314     def names(self, user, channel, names):
315         """Send the names of a channel's participants to a user.
316
317         @type user: C{str} or C{unicode}
318         @param user: The user receiving the name list.  Only their nick
319         name, not the full hostmask.
320
321         @type channel: C{str} or C{unicode}
322         @param channel: The channel for which this is the namelist.
323
324         @type names: C{list} of C{str} or C{unicode}
325         @param names: The names to send.
326         """
327         # XXX If unicode is given, these limits are not quite correct
328         prefixLength = len(channel) + len(user) + 10
329         namesLength = 512 - prefixLength
330
331         L = []
332         count = 0
333         for n in names:
334             if count + len(n) + 1 > namesLength:
335                 self.sendLine(":%s %s %s = %s :%s" % (
336                     self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
337                 L = [n]
338                 count = len(n)
339             else:
340                 L.append(n)
341                 count += len(n) + 1
342         if L:
343             self.sendLine(":%s %s %s = %s :%s" % (
344                 self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
345         self.sendLine(":%s %s %s %s :End of /NAMES list" % (
346             self.hostname, RPL_ENDOFNAMES, user, channel))
347
348
349     def who(self, user, channel, memberInfo):
350         """
351         Send a list of users participating in a channel.
352
353         @type user: C{str} or C{unicode}
354         @param user: The user receiving this member information.  Only their
355         nick name, not the full hostmask.
356
357         @type channel: C{str} or C{unicode}
358         @param channel: The channel for which this is the member
359         information.
360
361         @type memberInfo: C{list} of C{tuples}
362         @param memberInfo: For each member of the given channel, a 7-tuple
363         containing their username, their hostmask, the server to which they
364         are connected, their nickname, the letter "H" or "G" (wtf do these
365         mean?), the hopcount from C{user} to this member, and this member's
366         real name.
367         """
368         for info in memberInfo:
369             (username, hostmask, server, nickname, flag, hops, realName) = info
370             assert flag in ("H", "G")
371             self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % (
372                 self.hostname, RPL_WHOREPLY, user, channel,
373                 username, hostmask, server, nickname, flag, hops, realName))
374
375         self.sendLine(":%s %s %s %s :End of /WHO list." % (
376             self.hostname, RPL_ENDOFWHO, user, channel))
377
378
379     def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels):
380         """
381         Send information about the state of a particular user.
382
383         @type user: C{str} or C{unicode}
384         @param user: The user receiving this information.  Only their nick
385         name, not the full hostmask.
386
387         @type nick: C{str} or C{unicode}
388         @param nick: The nickname of the user this information describes.
389
390         @type username: C{str} or C{unicode}
391         @param username: The user's username (eg, ident response)
392
393         @type hostname: C{str}
394         @param hostname: The user's hostmask
395
396         @type realName: C{str} or C{unicode}
397         @param realName: The user's real name
398
399         @type server: C{str} or C{unicode}
400         @param server: The name of the server to which the user is connected
401
402         @type serverInfo: C{str} or C{unicode}
403         @param serverInfo: A descriptive string about that server
404
405         @type oper: C{bool}
406         @param oper: Indicates whether the user is an IRC operator
407
408         @type idle: C{int}
409         @param idle: The number of seconds since the user last sent a message
410
411         @type signOn: C{int}
412         @param signOn: A POSIX timestamp (number of seconds since the epoch)
413         indicating the time the user signed on
414
415         @type channels: C{list} of C{str} or C{unicode}
416         @param channels: A list of the channels which the user is participating in
417         """
418         self.sendLine(":%s %s %s %s %s %s * :%s" % (
419             self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName))
420         self.sendLine(":%s %s %s %s %s :%s" % (
421             self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo))
422         if oper:
423             self.sendLine(":%s %s %s %s :is an IRC operator" % (
424                 self.hostname, RPL_WHOISOPERATOR, user, nick))
425         self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % (
426             self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn))
427         self.sendLine(":%s %s %s %s :%s" % (
428             self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels)))
429         self.sendLine(":%s %s %s %s :End of WHOIS list." % (
430             self.hostname, RPL_ENDOFWHOIS, user, nick))
431
432
433     def join(self, who, where):
434         """Send a join message.
435
436         @type who: C{str} or C{unicode}
437         @param who: The name of the user joining.  Should be of the form
438         username!ident@hostmask (unless you know better!).
439
440         @type where: C{str} or C{unicode}
441         @param where: The channel the user is joining.
442         """
443         self.sendLine(":%s JOIN %s" % (who, where))
444
445
446     def part(self, who, where, reason=None):
447         """Send a part message.
448
449         @type who: C{str} or C{unicode}
450         @param who: The name of the user joining.  Should be of the form
451         username!ident@hostmask (unless you know better!).
452
453         @type where: C{str} or C{unicode}
454         @param where: The channel the user is joining.
455
456         @type reason: C{str} or C{unicode}
457         @param reason: A string describing the misery which caused
458         this poor soul to depart.
459         """
460         if reason:
461             self.sendLine(":%s PART %s :%s" % (who, where, reason))
462         else:
463             self.sendLine(":%s PART %s" % (who, where))
464
465
466     def channelMode(self, user, channel, mode, *args):
467         """
468         Send information about the mode of a channel.
469
470         @type user: C{str} or C{unicode}
471         @param user: The user receiving the name list.  Only their nick
472         name, not the full hostmask.
473
474         @type channel: C{str} or C{unicode}
475         @param channel: The channel for which this is the namelist.
476
477         @type mode: C{str}
478         @param mode: A string describing this channel's modes.
479
480         @param args: Any additional arguments required by the modes.
481         """
482         self.sendLine(":%s %s %s %s %s %s" % (
483             self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args)))
484
485
486 class IRCClient(basic.LineReceiver):
487     """Internet Relay Chat client protocol, with sprinkles.
488
489     In addition to providing an interface for an IRC client protocol,
490     this class also contains reasonable implementations of many common
491     CTCP methods.
492
493     TODO
494     ====
495      - Limit the length of messages sent (because the IRC server probably
496        does).
497      - Add flood protection/rate limiting for my CTCP replies.
498      - NickServ cooperation.  (a mix-in?)
499      - Heartbeat.  The transport may die in such a way that it does not realize
500        it is dead until it is written to.  Sending something (like \"PING
501        this.irc-host.net\") during idle peroids would alleviate that.  If
502        you're concerned with the stability of the host as well as that of the
503        transport, you might care to watch for the corresponding PONG.
504
505     @ivar nickname: Nickname the client will use.
506     @ivar password: Password used to log on to the server.  May be C{None}.
507     @ivar realname: Supplied to the server during login as the \"Real name\"
508         or \"ircname\".  May be C{None}.
509     @ivar username: Supplied to the server during login as the \"User name\".
510         May be C{None}
511
512     @ivar userinfo: Sent in reply to a X{USERINFO} CTCP query.  If C{None}, no
513         USERINFO reply will be sent.
514         \"This is used to transmit a string which is settable by
515         the user (and never should be set by the client).\"
516     @ivar fingerReply: Sent in reply to a X{FINGER} CTCP query.  If C{None}, no
517         FINGER reply will be sent.
518     @type fingerReply: Callable or String
519
520     @ivar versionName: CTCP VERSION reply, client name.  If C{None}, no VERSION
521         reply will be sent.
522     @ivar versionNum: CTCP VERSION reply, client version,
523     @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
524
525     @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
526         client may be found.  If C{None}, no SOURCE reply will be sent.
527
528     @ivar lineRate: Minimum delay between lines sent to the server.  If
529         C{None}, no delay will be imposed.
530     @type lineRate: Number of Seconds.
531     """
532
533     motd = ""
534     nickname = 'irc'
535     password = None
536     realname = None
537     username = None
538     ### Responses to various CTCP queries.
539
540     userinfo = None
541     # fingerReply is a callable returning a string, or a str()able object.
542     fingerReply = None
543     versionName = None
544     versionNum = None
545     versionEnv = None
546
547     sourceURL = "http://twistedmatrix.com/downloads/"
548
549     dcc_destdir = '.'
550     dcc_sessions = None
551
552     # If this is false, no attempt will be made to identify
553     # ourself to the server.
554     performLogin = 1
555
556     lineRate = None
557     _queue = None
558     _queueEmptying = None
559
560     delimiter = '\n' # '\r\n' will also work (see dataReceived)
561
562     __pychecker__ = 'unusednames=params,prefix,channel'
563
564
565     def _reallySendLine(self, line):
566         return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r')
567
568     def sendLine(self, line):
569         if self.lineRate is None:
570             self._reallySendLine(line)
571         else:
572             self._queue.append(line)
573             if not self._queueEmptying:
574                 self._sendLine()
575
576     def _sendLine(self):
577         if self._queue:
578             self._reallySendLine(self._queue.pop(0))
579             self._queueEmptying = reactor.callLater(self.lineRate,
580                                                     self._sendLine)
581         else:
582             self._queueEmptying = None
583
584
585     ### Interface level client->user output methods
586     ###
587     ### You'll want to override these.
588
589     ### Methods relating to the server itself
590
591     def created(self, when):
592         """Called with creation date information about the server, usually at logon.
593
594         @type when: C{str}
595         @param when: A string describing when the server was created, probably.
596         """
597
598     def yourHost(self, info):
599         """Called with daemon information about the server, usually at logon.
600
601         @type info: C{str}
602         @param when: A string describing what software the server is running, probably.
603         """
604
605     def myInfo(self, servername, version, umodes, cmodes):
606         """Called with information about the server, usually at logon.
607
608         @type servername: C{str}
609         @param servername: The hostname of this server.
610
611         @type version: C{str}
612         @param version: A description of what software this server runs.
613
614         @type umodes: C{str}
615         @param umodes: All the available user modes.
616
617         @type cmodes: C{str}
618         @param cmodes: All the available channel modes.
619         """
620
621     def luserClient(self, info):
622         """Called with information about the number of connections, usually at logon.
623
624         @type info: C{str}
625         @param info: A description of the number of clients and servers
626         connected to the network, probably.
627         """
628
629     def bounce(self, info):
630         """Called with information about where the client should reconnect.
631
632         @type info: C{str}
633         @param info: A plaintext description of the address that should be
634         connected to.
635         """
636
637     def isupport(self, options):
638         """Called with various information about what the server supports.
639
640         @type options: C{list} of C{str}
641         @param options: Descriptions of features or limits of the server, possibly
642         in the form "NAME=VALUE".
643         """
644
645     def luserChannels(self, channels):
646         """Called with the number of channels existant on the server.
647
648         @type channels: C{int}
649         """
650
651     def luserOp(self, ops):
652         """Called with the number of ops logged on to the server.
653
654         @type ops: C{int}
655         """
656
657     def luserMe(self, info):
658         """Called with information about the server connected to.
659
660         @type info: C{str}
661         @param info: A plaintext string describing the number of users and servers
662         connected to this server.
663         """
664
665     ### Methods involving me directly
666
667     def privmsg(self, user, channel, message):
668         """Called when I have a message from a user to me or a channel.
669         """
670         pass
671
672     def joined(self, channel):
673         """Called when I finish joining a channel.
674
675         channel has the starting character (# or &) intact.
676         """
677         pass
678
679     def left(self, channel):
680         """Called when I have left a channel.
681
682         channel has the starting character (# or &) intact.
683         """
684         pass
685
686     def noticed(self, user, channel, message):
687         """Called when I have a notice from a user to me or a channel.
688
689         By default, this is equivalent to IRCClient.privmsg, but if your
690         client makes any automated replies, you must override this!
691         From the RFC::
692
693             The difference between NOTICE and PRIVMSG is that
694             automatic replies MUST NEVER be sent in response to a
695             NOTICE message. [...] The object of this rule is to avoid
696             loops between clients automatically sending something in
697             response to something it received.
698         """
699         self.privmsg(user, channel, message)
700
701     def modeChanged(self, user, channel, set, modes, args):
702         """Called when a channel's modes are changed
703
704         @type user: C{str}
705         @param user: The user and hostmask which instigated this change.
706
707         @type channel: C{str}
708         @param channel: The channel for which the modes are changing.
709
710         @type set: C{bool} or C{int}
711         @param set: true if the mode is being added, false if it is being
712         removed.
713
714         @type modes: C{str}
715         @param modes: The mode or modes which are being changed.
716
717         @type args: C{tuple}
718         @param args: Any additional information required for the mode
719         change.
720         """
721
722     def pong(self, user, secs):
723         """Called with the results of a CTCP PING query.
724         """
725         pass
726
727     def signedOn(self):
728         """Called after sucessfully signing on to the server.
729         """
730         pass
731
732     def kickedFrom(self, channel, kicker, message):
733         """Called when I am kicked from a channel.
734         """
735         pass
736
737     def nickChanged(self, nick):
738         """Called when my nick has been changed.
739         """
740         self.nickname = nick
741
742
743     ### Things I observe other people doing in a channel.
744
745     def userJoined(self, user, channel):
746         """Called when I see another user joining a channel.
747         """
748         pass
749
750     def userLeft(self, user, channel):
751         """Called when I see another user leaving a channel.
752         """
753         pass
754
755     def userQuit(self, user, quitMessage):
756         """Called when I see another user disconnect from the network.
757         """
758         pass
759
760     def userKicked(self, kickee, channel, kicker, message):
761         """Called when I observe someone else being kicked from a channel.
762         """
763         pass
764
765     def action(self, user, channel, data):
766         """Called when I see a user perform an ACTION on a channel.
767         """
768         pass
769
770     def topicUpdated(self, user, channel, newTopic):
771         """In channel, user changed the topic to newTopic.
772
773         Also called when first joining a channel.
774         """
775         pass
776
777     def userRenamed(self, oldname, newname):
778         """A user changed their name from oldname to newname.
779         """
780         pass
781
782     ### Information from the server.
783
784     def receivedMOTD(self, motd):
785         """I received a message-of-the-day banner from the server.
786
787         motd is a list of strings, where each string was sent as a seperate
788         message from the server. To display, you might want to use::
789
790             string.join(motd, '\\n')
791
792         to get a nicely formatted string.
793         """
794         pass
795
796     ### user input commands, client->server
797     ### Your client will want to invoke these.
798
799     def join(self, channel, key=None):
800         if channel[0] not in '&#!+': channel = '#' + channel
801         if key:
802             self.sendLine("JOIN %s %s" % (channel, key))
803         else:
804             self.sendLine("JOIN %s" % (channel,))
805
806     def leave(self, channel, reason=None):
807         if channel[0] not in '&#!+': channel = '#' + channel
808         if reason:
809             self.sendLine("PART %s :%s" % (channel, reason))
810         else:
811             self.sendLine("PART %s" % (channel,))
812
813     def kick(self, channel, user, reason=None):
814         if channel[0] not in '&#!+': channel = '#' + channel
815         if reason:
816             self.sendLine("KICK %s %s :%s" % (channel, user, reason))
817         else:
818             self.sendLine("KICK %s %s" % (channel, user))
819
820     part = leave
821
822     def topic(self, channel, topic=None):
823         """Attempt to set the topic of the given channel, or ask what it is.
824
825         If topic is None, then I sent a topic query instead of trying to set
826         the topic. The server should respond with a TOPIC message containing
827         the current topic of the given channel.
828         """
829         # << TOPIC #xtestx :fff
830         if channel[0] not in '&#!+': channel = '#' + channel
831         if topic != None:
832             self.sendLine("TOPIC %s :%s" % (channel, topic))
833         else:
834             self.sendLine("TOPIC %s" % (channel,))
835
836     def mode(self, chan, set, modes, limit = None, user = None, mask = None):
837         """Change the modes on a user or channel."""
838         if set:
839             line = 'MODE %s +%s' % (chan, modes)
840         else:
841             line = 'MODE %s -%s' % (chan, modes)
842         if limit is not None:
843             line = '%s %d' % (line, limit)
844         elif user is not None:
845             line = '%s %s' % (line, user)
846         elif mask is not None:
847             line = '%s %s' % (line, mask)
848         self.sendLine(line)
849
850
851     def say(self, channel, message, length = None):
852         if channel[0] not in '&#!+': channel = '#' + channel
853         self.msg(channel, message, length)
854
855     def msg(self, user, message, length = None):
856         """Send a message to a user or channel.
857
858         @type user: C{str}
859         @param user: The username or channel name to which to direct the
860         message.
861
862         @type message: C{str}
863         @param message: The text to send
864
865         @type length: C{int}
866         @param length: The maximum number of octets to send at a time.  This
867         has the effect of turning a single call to msg() into multiple
868         commands to the server.  This is useful when long messages may be
869         sent that would otherwise cause the server to kick us off or silently
870         truncate the text we are sending.  If None is passed, the entire
871         message is always send in one command.
872         """
873
874         fmt = "PRIVMSG %s :%%s" % (user,)
875
876         if length is None:
877             self.sendLine(fmt % (message,))
878         else:
879             # NOTE: minimumLength really equals len(fmt) - 2 (for '%s') + n
880             # where n is how many bytes sendLine sends to end the line.
881             # n was magic numbered to 2, I think incorrectly
882             minimumLength = len(fmt)
883             if length <= minimumLength:
884                 raise ValueError("Maximum length must exceed %d for message "
885                                  "to %s" % (minimumLength, user))
886             lines = split(message, length - minimumLength)
887             map(lambda line, self=self, fmt=fmt: self.sendLine(fmt % line),
888                 lines)
889
890     def notice(self, user, message):
891         self.sendLine("NOTICE %s :%s" % (user, message))
892
893     def away(self, message=''):
894         self.sendLine("AWAY :%s" % message)
895
896     def register(self, nickname, hostname='foo', servername='bar'):
897         if self.password is not None:
898             self.sendLine("PASS %s" % self.password)
899         self.setNick(nickname)
900         if self.username is None:
901             self.username = nickname
902         self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname))
903
904     def setNick(self, nickname):
905         self.nickname = nickname
906         self.sendLine("NICK %s" % nickname)
907
908     def quit(self, message = ''):
909         self.sendLine("QUIT :%s" % message)
910
911     ### user input commands, client->client
912
913     def me(self, channel, action):
914         """Strike a pose.
915         """
916         if channel[0] not in '&#!+': channel = '#' + channel
917         self.ctcpMakeQuery(channel, [('ACTION', action)])
918
919     _pings = None
920     _MAX_PINGRING = 12
921
922     def ping(self, user, text = None):
923         """Measure round-trip delay to another IRC client.
924         """
925         if self._pings is None:
926             self._pings = {}
927
928         if text is None:
929             chars = string.letters + string.digits + string.punctuation
930             key = ''.join([random.choice(chars) for i in range(12)])
931         else:
932             key = str(text)
933         self._pings[(user, key)] = time.time()
934         self.ctcpMakeQuery(user, [('PING', key)])
935
936         if len(self._pings) > self._MAX_PINGRING:
937             # Remove some of the oldest entries.
938             byValue = [(v, k) for (k, v) in self._pings.items()]
939             byValue.sort()
940             excess = self._MAX_PINGRING - len(self._pings)
941             for i in xrange(excess):
942                 del self._pings[byValue[i][1]]
943
944     def dccSend(self, user, file):
945         if type(file) == types.StringType:
946             file = open(file, 'r')
947
948         size = fileSize(file)
949
950         name = getattr(file, "name", "file@%s" % (id(file),))
951
952         factory = DccSendFactory(file)
953         port = reactor.listenTCP(0, factory, 1)
954
955         raise NotImplementedError,(
956             "XXX!!! Help!  I need to bind a socket, have it listen, and tell me its address.  "
957             "(and stop accepting once we've made a single connection.)")
958
959         my_address = struct.pack("!I", my_address)
960
961         args = ['SEND', name, my_address, str(port)]
962
963         if not (size is None):
964             args.append(size)
965
966         args = string.join(args, ' ')
967
968         self.ctcpMakeQuery(user, [('DCC', args)])
969
970     def dccResume(self, user, fileName, port, resumePos):
971         """Send a DCC RESUME request to another user."""
972         self.ctcpMakeQuery(user, [
973             ('DCC', ['RESUME', fileName, port, resumePos])])
974
975     def dccAcceptResume(self, user, fileName, port, resumePos):
976         """Send a DCC ACCEPT response to clients who have requested a resume.
977         """
978         self.ctcpMakeQuery(user, [
979             ('DCC', ['ACCEPT', fileName, port, resumePos])])
980
981     ### server->client messages
982     ### You might want to fiddle with these,
983     ### but it is safe to leave them alone.
984
985     def irc_ERR_NICKNAMEINUSE(self, prefix, params):
986         self.register(self.nickname+'_')
987
988     def irc_ERR_PASSWDMISMATCH(self, prefix, params):
989         raise IRCPasswordMismatch("Password Incorrect.")
990
991     def irc_RPL_WELCOME(self, prefix, params):
992         self.signedOn()
993
994     def irc_JOIN(self, prefix, params):
995         nick = string.split(prefix,'!')[0]
996         channel = params[-1]
997         if nick == self.nickname:
998             self.joined(channel)
999         else:
1000             self.userJoined(nick, channel)
1001
1002     def irc_PART(self, prefix, params):
1003         nick = string.split(prefix,'!')[0]
1004         channel = params[0]
1005         if nick == self.nickname:
1006             self.left(channel)
1007         else:
1008             self.userLeft(nick, channel)
1009
1010     def irc_QUIT(self, prefix, params):
1011         nick = string.split(prefix,'!')[0]
1012         self.userQuit(nick, params[0])
1013
1014     def irc_MODE(self, prefix, params):
1015         channel, rest = params[0], params[1:]
1016         set = rest[0][0] == '+'
1017         modes = rest[0][1:]
1018         args = rest[1:]
1019         self.modeChanged(prefix, channel, set, modes, tuple(args))
1020
1021     def irc_PING(self, prefix, params):
1022         self.sendLine("PONG %s" % params[-1])
1023
1024     def irc_PRIVMSG(self, prefix, params):
1025         user = prefix
1026         channel = params[0]
1027         message = params[-1]
1028
1029         if not message: return # don't raise an exception if some idiot sends us a blank message
1030
1031         if message[0]==X_DELIM:
1032             m = ctcpExtract(message)
1033             if m['extended']:
1034                 self.ctcpQuery(user, channel, m['extended'])
1035
1036             if not m['normal']:
1037                 return
1038
1039             message = string.join(m['normal'], ' ')
1040
1041         self.privmsg(user, channel, message)
1042
1043     def irc_NOTICE(self, prefix, params):
1044         user = prefix
1045         channel = params[0]
1046         message = params[-1]
1047
1048         if message[0]==X_DELIM:
1049             m = ctcpExtract(message)
1050             if m['extended']:
1051                 self.ctcpReply(user, channel, m['extended'])
1052
1053             if not m['normal']:
1054                 return
1055
1056             message = string.join(m['normal'], ' ')
1057
1058         self.noticed(user, channel, message)
1059
1060     def irc_NICK(self, prefix, params):
1061         nick = string.split(prefix,'!', 1)[0]
1062         if nick == self.nickname:
1063             self.nickChanged(params[0])
1064         else:
1065             self.userRenamed(nick, params[0])
1066
1067     def irc_KICK(self, prefix, params):
1068         """Kicked?  Who?  Not me, I hope.
1069         """
1070         kicker = string.split(prefix,'!')[0]
1071         channel = params[0]
1072         kicked = params[1]
1073         message = params[-1]
1074         if string.lower(kicked) == string.lower(self.nickname):
1075             # Yikes!
1076             self.kickedFrom(channel, kicker, message)
1077         else:
1078             self.userKicked(kicked, channel, kicker, message)
1079
1080     def irc_TOPIC(self, prefix, params):
1081         """Someone in the channel set the topic.
1082         """
1083         user = string.split(prefix, '!')[0]
1084         channel = params[0]
1085         newtopic = params[1]
1086         self.topicUpdated(user, channel, newtopic)
1087
1088     def irc_RPL_TOPIC(self, prefix, params):
1089         """I just joined the channel, and the server is telling me the current topic.
1090         """
1091         user = string.split(prefix, '!')[0]
1092         channel = params[1]
1093         newtopic = params[2]
1094         self.topicUpdated(user, channel, newtopic)
1095
1096     def irc_RPL_NOTOPIC(self, prefix, params):
1097         user = string.split(prefix, '!')[0]
1098         channel = params[1]
1099         newtopic = ""
1100         self.topicUpdated(user, channel, newtopic)
1101
1102     def irc_RPL_MOTDSTART(self, prefix, params):
1103         if params[-1].startswith("- "):
1104             params[-1] = params[-1][2:]
1105         self.motd = [params[-1]]
1106
1107     def irc_RPL_MOTD(self, prefix, params):
1108         if params[-1].startswith("- "):
1109             params[-1] = params[-1][2:]
1110         self.motd.append(params[-1])
1111
1112     def irc_RPL_ENDOFMOTD(self, prefix, params):
1113         self.receivedMOTD(self.motd)
1114
1115     def irc_RPL_CREATED(self, prefix, params):
1116         self.created(params[1])
1117
1118     def irc_RPL_YOURHOST(self, prefix, params):
1119         self.yourHost(params[1])
1120
1121     def irc_RPL_MYINFO(self, prefix, params):
1122         info = params[1].split(None, 3)
1123         while len(info) < 4:
1124             info.append(None)
1125         self.myInfo(*info)
1126
1127     def irc_RPL_BOUNCE(self, prefix, params):
1128         # 005 is doubly assigned.  Piece of crap dirty trash protocol.
1129         if params[-1] == "are available on this server":
1130             self.isupport(params[1:-1])
1131         else:
1132             self.bounce(params[1])
1133
1134     def irc_RPL_LUSERCLIENT(self, prefix, params):
1135         self.luserClient(params[1])
1136
1137     def irc_RPL_LUSEROP(self, prefix, params):
1138         try:
1139             self.luserOp(int(params[1]))
1140         except ValueError:
1141             pass
1142
1143     def irc_RPL_LUSERCHANNELS(self, prefix, params):
1144         try:
1145             self.luserChannels(int(params[1]))
1146         except ValueError:
1147             pass
1148
1149     def irc_RPL_LUSERME(self, prefix, params):
1150         self.luserMe(params[1])
1151
1152     def irc_unknown(self, prefix, command, params):
1153         pass
1154
1155     ### Receiving a CTCP query from another party
1156     ### It is safe to leave these alone.
1157
1158     def ctcpQuery(self, user, channel, messages):
1159         """Dispatch method for any CTCP queries received.
1160         """
1161         for m in messages:
1162             method = getattr(self, "ctcpQuery_%s" % m[0], None)
1163             if method:
1164                 method(user, channel, m[1])
1165             else:
1166                 self.ctcpUnknownQuery(user, channel, m[0], m[1])
1167
1168     def ctcpQuery_ACTION(self, user, channel, data):
1169         self.action(user, channel, data)
1170
1171     def ctcpQuery_PING(self, user, channel, data):
1172         nick = string.split(user,"!")[0]
1173         self.ctcpMakeReply(nick, [("PING", data)])
1174
1175     def ctcpQuery_FINGER(self, user, channel, data):
1176         if data is not None:
1177             self.quirkyMessage("Why did %s send '%s' with a FINGER query?"
1178                                % (user, data))
1179         if not self.fingerReply:
1180             return
1181
1182         if callable(self.fingerReply):
1183             reply = self.fingerReply()
1184         else:
1185             reply = str(self.fingerReply)
1186
1187         nick = string.split(user,"!")[0]
1188         self.ctcpMakeReply(nick, [('FINGER', reply)])
1189
1190     def ctcpQuery_VERSION(self, user, channel, data):
1191         if data is not None:
1192             self.quirkyMessage("Why did %s send '%s' with a VERSION query?"
1193                                % (user, data))
1194
1195         if self.versionName:
1196             nick = string.split(user,"!")[0]
1197             self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' %
1198                                        (self.versionName,
1199                                         self.versionNum,
1200                                         self.versionEnv))])
1201
1202     def ctcpQuery_SOURCE(self, user, channel, data):
1203         if data is not None:
1204             self.quirkyMessage("Why did %s send '%s' with a SOURCE query?"
1205                                % (user, data))
1206         if self.sourceURL:
1207             nick = string.split(user,"!")[0]
1208             # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
1209             # replies should be responded to with the location of an anonymous
1210             # FTP server in host:directory:file format.  I'm taking the liberty
1211             # of bringing it into the 21st century by sending a URL instead.
1212             self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL),
1213                                       ('SOURCE', None)])
1214
1215     def ctcpQuery_USERINFO(self, user, channel, data):
1216         if data is not None:
1217             self.quirkyMessage("Why did %s send '%s' with a USERINFO query?"
1218                                % (user, data))
1219         if self.userinfo:
1220             nick = string.split(user,"!")[0]
1221             self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)])
1222
1223     def ctcpQuery_CLIENTINFO(self, user, channel, data):
1224         """A master index of what CTCP tags this client knows.
1225
1226         If no arguments are provided, respond with a list of known tags.
1227         If an argument is provided, provide human-readable help on
1228         the usage of that tag.
1229         """
1230
1231         nick = string.split(user,"!")[0]
1232         if not data:
1233             # XXX: prefixedMethodNames gets methods from my *class*,
1234             # but it's entirely possible that this *instance* has more
1235             # methods.
1236             names = reflect.prefixedMethodNames(self.__class__,
1237                                                 'ctcpQuery_')
1238
1239             self.ctcpMakeReply(nick, [('CLIENTINFO',
1240                                        string.join(names, ' '))])
1241         else:
1242             args = string.split(data)
1243             method = getattr(self, 'ctcpQuery_%s' % (args[0],), None)
1244             if not method:
1245                 self.ctcpMakeReply(nick, [('ERRMSG',
1246                                            "CLIENTINFO %s :"
1247                                            "Unknown query '%s'"
1248                                            % (data, args[0]))])
1249                 return
1250             doc = getattr(method, '__doc__', '')
1251             self.ctcpMakeReply(nick, [('CLIENTINFO', doc)])
1252
1253
1254     def ctcpQuery_ERRMSG(self, user, channel, data):
1255         # Yeah, this seems strange, but that's what the spec says to do
1256         # when faced with an ERRMSG query (not a reply).
1257         nick = string.split(user,"!")[0]
1258         self.ctcpMakeReply(nick, [('ERRMSG',
1259                                    "%s :No error has occoured." % data)])
1260
1261     def ctcpQuery_TIME(self, user, channel, data):
1262         if data is not None:
1263             self.quirkyMessage("Why did %s send '%s' with a TIME query?"
1264                                % (user, data))
1265         nick = string.split(user,"!")[0]
1266         self.ctcpMakeReply(nick,
1267                            [('TIME', ':%s' %
1268                              time.asctime(time.localtime(time.time())))])
1269
1270     def ctcpQuery_DCC(self, user, channel, data):
1271         """Initiate a Direct Client Connection
1272         """
1273
1274         if not data: return
1275         dcctype = data.split(None, 1)[0].upper()
1276         handler = getattr(self, "dcc_" + dcctype, None)
1277         if handler:
1278             if self.dcc_sessions is None:
1279                 self.dcc_sessions = []
1280             data = data[len(dcctype)+1:]
1281             handler(user, channel, data)
1282         else:
1283             nick = string.split(user,"!")[0]
1284             self.ctcpMakeReply(nick, [('ERRMSG',
1285                                        "DCC %s :Unknown DCC type '%s'"
1286                                        % (data, dcctype))])
1287             self.quirkyMessage("%s offered unknown DCC type %s"
1288                                % (user, dcctype))
1289
1290     def dcc_SEND(self, user, channel, data):
1291         # Use splitQuoted for those who send files with spaces in the names.
1292         data = text.splitQuoted(data)
1293         if len(data) < 3:
1294             raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,)
1295
1296         (filename, address, port) = data[:3]
1297
1298         address = dccParseAddress(address)
1299         try:
1300             port = int(port)
1301         except ValueError:
1302             raise IRCBadMessage, "Indecipherable port %r" % (port,)
1303
1304         size = -1
1305         if len(data) >= 4:
1306             try:
1307                 size = int(data[3])
1308             except ValueError:
1309                 pass
1310
1311         # XXX Should we bother passing this data?
1312         self.dccDoSend(user, address, port, filename, size, data)
1313
1314     def dcc_ACCEPT(self, user, channel, data):
1315         data = text.splitQuoted(data)
1316         if len(data) < 3:
1317             raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,)
1318         (filename, port, resumePos) = data[:3]
1319         try:
1320             port = int(port)
1321             resumePos = int(resumePos)
1322         except ValueError:
1323             return
1324
1325         self.dccDoAcceptResume(user, filename, port, resumePos)
1326
1327     def dcc_RESUME(self, user, channel, data):
1328         data = text.splitQuoted(data)
1329         if len(data) < 3:
1330             raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,)
1331         (filename, port, resumePos) = data[:3]
1332         try:
1333             port = int(port)
1334             resumePos = int(resumePos)
1335         except ValueError:
1336             return
1337         self.dccDoResume(user, filename, port, resumePos)
1338
1339     def dcc_CHAT(self, user, channel, data):
1340         data = text.splitQuoted(data)
1341         if len(data) < 3:
1342             raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,)
1343
1344         (filename, address, port) = data[:3]
1345
1346         address = dccParseAddress(address)
1347         try:
1348             port = int(port)
1349         except ValueError:
1350             raise IRCBadMessage, "Indecipherable port %r" % (port,)
1351
1352         self.dccDoChat(user, channel, address, port, data)
1353
1354     ### The dccDo methods are the slightly higher-level siblings of
1355     ### common dcc_ methods; the arguments have been parsed for them.
1356
1357     def dccDoSend(self, user, address, port, fileName, size, data):
1358         """Called when I receive a DCC SEND offer from a client.
1359
1360         By default, I do nothing here."""
1361         ## filename = path.basename(arg)
1362         ## protocol = DccFileReceive(filename, size,
1363         ##                           (user,channel,data),self.dcc_destdir)
1364         ## reactor.clientTCP(address, port, protocol)
1365         ## self.dcc_sessions.append(protocol)
1366         pass
1367
1368     def dccDoResume(self, user, file, port, resumePos):
1369         """Called when a client is trying to resume an offered file
1370         via DCC send.  It should be either replied to with a DCC
1371         ACCEPT or ignored (default)."""
1372         pass
1373
1374     def dccDoAcceptResume(self, user, file, port, resumePos):
1375         """Called when a client has verified and accepted a DCC resume
1376         request made by us.  By default it will do nothing."""
1377         pass
1378
1379     def dccDoChat(self, user, channel, address, port, data):
1380         pass
1381         #factory = DccChatFactory(self, queryData=(user, channel, data))
1382         #reactor.connectTCP(address, port, factory)
1383         #self.dcc_sessions.append(factory)
1384
1385     #def ctcpQuery_SED(self, user, data):
1386     #    """Simple Encryption Doodoo
1387     #
1388     #    Feel free to implement this, but no specification is available.
1389     #    """
1390     #    raise NotImplementedError
1391
1392     def ctcpUnknownQuery(self, user, channel, tag, data):
1393         nick = string.split(user,"!")[0]
1394         self.ctcpMakeReply(nick, [('ERRMSG',
1395                                    "%s %s: Unknown query '%s'"
1396                                    % (tag, data, tag))])
1397
1398         log.msg("Unknown CTCP query from %s: %s %s\n"
1399                  % (user, tag, data))
1400
1401     def ctcpMakeReply(self, user, messages):
1402         """Send one or more X{extended messages} as a CTCP reply.
1403
1404         @type messages: a list of extended messages.  An extended
1405         message is a (tag, data) tuple, where 'data' may be C{None}.
1406         """
1407         self.notice(user, ctcpStringify(messages))
1408
1409     ### client CTCP query commands
1410
1411     def ctcpMakeQuery(self, user, messages):
1412         """Send one or more X{extended messages} as a CTCP query.
1413
1414         @type messages: a list of extended messages.  An extended
1415         message is a (tag, data) tuple, where 'data' may be C{None}.
1416         """
1417         self.msg(user, ctcpStringify(messages))
1418
1419     ### Receiving a response to a CTCP query (presumably to one we made)
1420     ### You may want to add methods here, or override UnknownReply.
1421
1422     def ctcpReply(self, user, channel, messages):
1423         """Dispatch method for any CTCP replies received.
1424         """
1425         for m in messages:
1426             method = getattr(self, "ctcpReply_%s" % m[0], None)
1427             if method:
1428                 method(user, channel, m[1])
1429             else:
1430                 self.ctcpUnknownReply(user, channel, m[0], m[1])
1431
1432     def ctcpReply_PING(self, user, channel, data):
1433         nick = user.split('!', 1)[0]
1434         if (not self._pings) or (not self._pings.has_key((nick, data))):
1435             raise IRCBadMessage,\
1436                   "Bogus PING response from %s: %s" % (user, data)
1437
1438         t0 = self._pings[(nick, data)]
1439         self.pong(user, time.time() - t0)
1440
1441     def ctcpUnknownReply(self, user, channel, tag, data):
1442         """Called when a fitting ctcpReply_ method is not found.
1443
1444         XXX: If the client makes arbitrary CTCP queries,
1445         this method should probably show the responses to
1446         them instead of treating them as anomolies.
1447         """
1448         log.msg("Unknown CTCP reply from %s: %s %s\n"
1449                  % (user, tag, data))
1450
1451     ### Error handlers
1452     ### You may override these with something more appropriate to your UI.
1453
1454     def badMessage(self, line, excType, excValue, tb):
1455         """When I get a message that's so broken I can't use it.
1456         """
1457         log.msg(line)
1458         log.msg(string.join(traceback.format_exception(excType,
1459                                                         excValue,
1460                                                         tb),''))
1461
1462     def quirkyMessage(self, s):
1463         """This is called when I receive a message which is peculiar,
1464         but not wholly indecipherable.
1465         """
1466         log.msg(s + '\n')
1467
1468     ### Protocool methods
1469
1470     def connectionMade(self):
1471         self._queue = []
1472         if self.performLogin:
1473             self.register(self.nickname)
1474
1475     def dataReceived(self, data):
1476         basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
1477
1478     def lineReceived(self, line):
1479         line = lowDequote(line)
1480         try:
1481             prefix, command, params = parsemsg(line)
1482             if numeric_to_symbolic.has_key(command):
1483                 command = numeric_to_symbolic[command]
1484             self.handleCommand(command, prefix, params)
1485         except IRCBadMessage:
1486             self.badMessage(line, *sys.exc_info())
1487
1488
1489     def handleCommand(self, command, prefix, params):
1490         """Determine the function to call for the given command and call
1491         it with the given arguments.
1492         """
1493         method = getattr(self, "irc_%s" % command, None)
1494         try:
1495             if method is not None:
1496                 method(prefix, params)
1497             else:
1498                 self.irc_unknown(prefix, command, params)
1499         except:
1500             log.deferr()
1501
1502
1503     def __getstate__(self):
1504         dct = self.__dict__.copy()
1505         dct['dcc_sessions'] = None
1506         dct['_pings'] = None
1507         return dct
1508
1509
1510 def dccParseAddress(address):
1511     if '.' in address:
1512         pass
1513     else:
1514         try:
1515             address = long(address)
1516         except ValueError:
1517             raise IRCBadMessage,\
1518                   "Indecipherable address %r" % (address,)
1519         else:
1520             address = (
1521                 (address >> 24) & 0xFF,
1522                 (address >> 16) & 0xFF,
1523                 (address >> 8) & 0xFF,
1524                 address & 0xFF,
1525                 )
1526             address = '.'.join(map(str,address))
1527     return address
1528
1529
1530 class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
1531     """Bare protocol to receive a Direct Client Connection SEND stream.
1532
1533     This does enough to keep the other guy talking, but you'll want to
1534     extend my dataReceived method to *do* something with the data I get.
1535     """
1536
1537     bytesReceived = 0
1538
1539     def __init__(self, resumeOffset=0):
1540         self.bytesReceived = resumeOffset
1541         self.resume = (resumeOffset != 0)
1542
1543     def dataReceived(self, data):
1544         """Called when data is received.
1545
1546         Warning: This just acknowledges to the remote host that the
1547         data has been received; it doesn't *do* anything with the
1548         data, so you'll want to override this.
1549         """
1550         self.bytesReceived = self.bytesReceived + len(data)
1551         self.transport.write(struct.pack('!i', self.bytesReceived))
1552
1553
1554 class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
1555     """Protocol for an outgoing Direct Client Connection SEND.
1556     """
1557
1558     blocksize = 1024
1559     file = None
1560     bytesSent = 0
1561     completed = 0
1562     connected = 0
1563
1564     def __init__(self, file):
1565         if type(file) is types.StringType:
1566             self.file = open(file, 'r')
1567
1568     def connectionMade(self):
1569         self.connected = 1
1570         self.sendBlock()
1571
1572     def dataReceived(self, data):
1573         # XXX: Do we need to check to see if len(data) != fmtsize?
1574
1575         bytesShesGot = struct.unpack("!I", data)
1576         if bytesShesGot < self.bytesSent:
1577             # Wait for her.
1578             # XXX? Add some checks to see if we've stalled out?
1579             return
1580         elif bytesShesGot > self.bytesSent:
1581             # self.transport.log("DCC SEND %s: She says she has %d bytes "
1582             #                    "but I've only sent %d.  I'm stopping "
1583             #                    "this screwy transfer."
1584             #                    % (self.file,
1585             #                       bytesShesGot, self.bytesSent))
1586             self.transport.loseConnection()
1587             return
1588
1589         self.sendBlock()
1590
1591     def sendBlock(self):
1592         block = self.file.read(self.blocksize)
1593         if block:
1594             self.transport.write(block)
1595             self.bytesSent = self.bytesSent + len(block)
1596         else:
1597             # Nothing more to send, transfer complete.
1598             self.transport.loseConnection()
1599             self.completed = 1
1600
1601     def connectionLost(self, reason):
1602         self.connected = 0
1603         if hasattr(self.file, "close"):
1604             self.file.close()
1605
1606
1607 class DccSendFactory(protocol.Factory):
1608     protocol = DccSendProtocol
1609     def __init__(self, file):
1610         self.file = file
1611
1612     def buildProtocol(self, connection):
1613         p = self.protocol(self.file)
1614         p.factory = self
1615         return p
1616
1617
1618 def fileSize(file):
1619     """I'll try my damndest to determine the size of this file object.
1620     """
1621     size = None
1622     if hasattr(file, "fileno"):
1623         fileno = file.fileno()
1624         try:
1625             stat_ = os.fstat(fileno)
1626             size = stat_[stat.ST_SIZE]
1627         except:
1628             pass
1629         else:
1630             return size
1631
1632     if hasattr(file, "name") and path.exists(file.name):
1633         try:
1634             size = path.getsize(file.name)
1635         except:
1636             pass
1637         else:
1638             return size
1639
1640     if hasattr(file, "seek") and hasattr(file, "tell"):
1641         try:
1642             try:
1643                 file.seek(0, 2)
1644                 size = file.tell()
1645             finally:
1646                 file.seek(0, 0)
1647         except:
1648             pass
1649         else:
1650             return size
1651
1652     return size
1653
1654 class DccChat(basic.LineReceiver, styles.Ephemeral):
1655     """Direct Client Connection protocol type CHAT.
1656
1657     DCC CHAT is really just your run o' the mill basic.LineReceiver
1658     protocol.  This class only varies from that slightly, accepting
1659     either LF or CR LF for a line delimeter for incoming messages
1660     while always using CR LF for outgoing.
1661
1662     The lineReceived method implemented here uses the DCC connection's
1663     'client' attribute (provided upon construction) to deliver incoming
1664     lines from the DCC chat via IRCClient's normal privmsg interface.
1665     That's something of a spoof, which you may well want to override.
1666     """
1667
1668     queryData = None
1669     delimiter = CR + NL
1670     client = None
1671     remoteParty = None
1672     buffer = ""
1673
1674     def __init__(self, client, queryData=None):
1675         """Initialize a new DCC CHAT session.
1676
1677         queryData is a 3-tuple of
1678         (fromUser, targetUserOrChannel, data)
1679         as received by the CTCP query.
1680
1681         (To be honest, fromUser is the only thing that's currently
1682         used here. targetUserOrChannel is potentially useful, while
1683         the 'data' argument is soley for informational purposes.)
1684         """
1685         self.client = client
1686         if queryData:
1687             self.queryData = queryData
1688             self.remoteParty = self.queryData[0]
1689
1690     def dataReceived(self, data):
1691         self.buffer = self.buffer + data
1692         lines = string.split(self.buffer, LF)
1693         # Put the (possibly empty) element after the last LF back in the
1694         # buffer
1695         self.buffer = lines.pop()
1696
1697         for line in lines:
1698             if line[-1] == CR:
1699                 line = line[:-1]
1700             self.lineReceived(line)
1701
1702     def lineReceived(self, line):
1703         log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line))
1704         self.client.privmsg(self.remoteParty,
1705                             self.client.nickname, line)
1706
1707
1708 class DccChatFactory(protocol.ClientFactory):
1709     protocol = DccChat
1710     noisy = 0
1711     def __init__(self, client, queryData):
1712         self.client = client
1713         self.queryData = queryData
1714
1715     def buildProtocol(self, addr):
1716         p = self.protocol(client=self.client, queryData=self.queryData)
1717         p.factory = self
1718
1719     def clientConnectionFailed(self, unused_connector, unused_reason):
1720         self.client.dcc_sessions.remove(self)
1721
1722     def clientConnectionLost(self, unused_connector, unused_reason):
1723         self.client.dcc_sessions.remove(self)
1724
1725
1726 def dccDescribe(data):
1727     """Given the data chunk from a DCC query, return a descriptive string.
1728     """
1729
1730     orig_data = data
1731     data = string.split(data)
1732     if len(data) < 4:
1733         return orig_data
1734
1735     (dcctype, arg, address, port) = data[:4]
1736
1737     if '.' in address:
1738         pass
1739     else:
1740         try:
1741             address = long(address)
1742         except ValueError:
1743             pass
1744         else:
1745             address = (
1746                 (address >> 24) & 0xFF,
1747                 (address >> 16) & 0xFF,
1748                 (address >> 8) & 0xFF,
1749                 address & 0xFF,
1750                 )
1751             # The mapping to 'int' is to get rid of those accursed
1752             # "L"s which python 1.5.2 puts on the end of longs.
1753             address = string.join(map(str,map(int,address)), ".")
1754
1755     if dcctype == 'SEND':
1756         filename = arg
1757
1758         size_txt = ''
1759         if len(data) >= 5:
1760             try:
1761                 size = int(data[4])
1762                 size_txt = ' of size %d bytes' % (size,)
1763             except ValueError:
1764                 pass
1765
1766         dcc_text = ("SEND for file '%s'%s at host %s, port %s"
1767                     % (filename, size_txt, address, port))
1768     elif dcctype == 'CHAT':
1769         dcc_text = ("CHAT for host %s, port %s"
1770                     % (address, port))
1771     else:
1772         dcc_text = orig_data
1773
1774     return dcc_text
1775
1776
1777 class DccFileReceive(DccFileReceiveBasic):
1778     """Higher-level coverage for getting a file from DCC SEND.
1779
1780     I allow you to change the file's name and destination directory.
1781     I won't overwrite an existing file unless I've been told it's okay
1782     to do so. If passed the resumeOffset keyword argument I will attempt to
1783     resume the file from that amount of bytes.
1784
1785     XXX: I need to let the client know when I am finished.
1786     XXX: I need to decide how to keep a progress indicator updated.
1787     XXX: Client needs a way to tell me \"Do not finish until I say so.\"
1788     XXX: I need to make sure the client understands if the file cannot be written.
1789     """
1790
1791     filename = 'dcc'
1792     fileSize = -1
1793     destDir = '.'
1794     overwrite = 0
1795     fromUser = None
1796     queryData = None
1797
1798     def __init__(self, filename, fileSize=-1, queryData=None,
1799                  destDir='.', resumeOffset=0):
1800         DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
1801         self.filename = filename
1802         self.destDir = destDir
1803         self.fileSize = fileSize
1804
1805         if queryData:
1806             self.queryData = queryData
1807             self.fromUser = self.queryData[0]
1808
1809     def set_directory(self, directory):
1810         """Set the directory where the downloaded file will be placed.
1811
1812         May raise OSError if the supplied directory path is not suitable.
1813         """
1814         if not path.exists(directory):
1815             raise OSError(errno.ENOENT, "You see no directory there.",
1816                           directory)
1817         if not path.isdir(directory):
1818             raise OSError(errno.ENOTDIR, "You cannot put a file into "
1819                           "something which is not a directory.",
1820                           directory)
1821         if not os.access(directory, os.X_OK | os.W_OK):
1822             raise OSError(errno.EACCES,
1823                           "This directory is too hard to write in to.",
1824                           directory)
1825         self.destDir = directory
1826
1827     def set_filename(self, filename):
1828         """Change the name of the file being transferred.
1829
1830         This replaces the file name provided by the sender.
1831         """
1832         self.filename = filename
1833
1834     def set_overwrite(self, boolean):
1835         """May I overwrite existing files?
1836         """
1837         self.overwrite = boolean
1838
1839
1840     # Protocol-level methods.
1841
1842     def connectionMade(self):
1843         dst = path.abspath(path.join(self.destDir,self.filename))
1844         exists = path.exists(dst)
1845         if self.resume and exists:
1846             # I have been told I want to resume, and a file already
1847             # exists - Here we go
1848             self.file = open(dst, 'ab')
1849             log.msg("Attempting to resume %s - starting from %d bytes" %
1850                     (self.file, self.file.tell()))
1851         elif self.overwrite or not exists:
1852             self.file = open(dst, 'wb')
1853         else:
1854             raise OSError(errno.EEXIST,
1855                           "There's a file in the way.  "
1856                           "Perhaps that's why you cannot open it.",
1857                           dst)
1858
1859     def dataReceived(self, data):
1860         self.file.write(data)
1861         DccFileReceiveBasic.dataReceived(self, data)
1862
1863         # XXX: update a progress indicator here?
1864
1865     def connectionLost(self, reason):
1866         """When the connection is lost, I close the file.
1867         """
1868         self.connected = 0
1869         logmsg = ("%s closed." % (self,))
1870         if self.fileSize > 0:
1871             logmsg = ("%s  %d/%d bytes received"
1872                       % (logmsg, self.bytesReceived, self.fileSize))
1873             if self.bytesReceived == self.fileSize:
1874                 pass # Hooray!
1875             elif self.bytesReceived < self.fileSize:
1876                 logmsg = ("%s (Warning: %d bytes short)"
1877                           % (logmsg, self.fileSize - self.bytesReceived))
1878             else:
1879                 logmsg = ("%s (file larger than expected)"
1880                           % (logmsg,))
1881         else:
1882             logmsg = ("%s  %d bytes received"
1883                       % (logmsg, self.bytesReceived))
1884
1885         if hasattr(self, 'file'):
1886             logmsg = "%s and written to %s.\n" % (logmsg, self.file.name)
1887             if hasattr(self.file, 'close'): self.file.close()
1888
1889         # self.transport.log(logmsg)
1890
1891     def __str__(self):
1892         if not self.connected:
1893             return "<Unconnected DccFileReceive object at %x>" % (id(self),)
1894         from_ = self.transport.getPeer()
1895         if self.fromUser:
1896             from_ = "%s (%s)" % (self.fromUser, from_)
1897
1898         s = ("DCC transfer of '%s' from %s" % (self.filename, from_))
1899         return s
1900
1901     def __repr__(self):
1902         s = ("<%s at %x: GET %s>"
1903              % (self.__class__, id(self), self.filename))
1904         return s
1905
1906
1907 # CTCP constants and helper functions
1908
1909 X_DELIM = chr(001)
1910
1911 def ctcpExtract(message):
1912     """Extract CTCP data from a string.
1913
1914     Returns a dictionary with two items:
1915
1916        - C{'extended'}: a list of CTCP (tag, data) tuples
1917        - C{'normal'}: a list of strings which were not inside a CTCP delimeter
1918     """
1919
1920     extended_messages = []
1921     normal_messages = []
1922     retval = {'extended': extended_messages,
1923               'normal': normal_messages }
1924
1925     messages = string.split(message, X_DELIM)
1926     odd = 0
1927
1928     # X1 extended data X2 nomal data X3 extended data X4 normal...
1929     while messages:
1930         if odd:
1931             extended_messages.append(messages.pop(0))
1932         else:
1933             normal_messages.append(messages.pop(0))
1934         odd = not odd
1935
1936     extended_messages[:] = filter(None, extended_messages)
1937     normal_messages[:] = filter(None, normal_messages)
1938
1939     extended_messages[:] = map(ctcpDequote, extended_messages)
1940     for i in xrange(len(extended_messages)):
1941         m = string.split(extended_messages[i], SPC, 1)
1942         tag = m[0]
1943         if len(m) > 1:
1944             data = m[1]
1945         else:
1946             data = None
1947
1948         extended_messages[i] = (tag, data)
1949
1950     return retval
1951
1952 # CTCP escaping
1953
1954 M_QUOTE= chr(020)
1955
1956 mQuoteTable = {
1957     NUL: M_QUOTE + '0',
1958     NL: M_QUOTE + 'n',
1959     CR: M_QUOTE + 'r',
1960     M_QUOTE: M_QUOTE + M_QUOTE
1961     }
1962
1963 mDequoteTable = {}
1964 for k, v in mQuoteTable.items():
1965     mDequoteTable[v[-1]] = k
1966 del k, v
1967
1968 mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
1969
1970 def lowQuote(s):
1971     for c in (M_QUOTE, NUL, NL, CR):
1972         s = string.replace(s, c, mQuoteTable[c])
1973     return s
1974
1975 def lowDequote(s):
1976     def sub(matchobj, mDequoteTable=mDequoteTable):
1977         s = matchobj.group()[1]
1978         try:
1979             s = mDequoteTable[s]
1980         except KeyError:
1981             s = s
1982         return s
1983
1984     return mEscape_re.sub(sub, s)
1985
1986 X_QUOTE = '\\'
1987
1988 xQuoteTable = {
1989     X_DELIM: X_QUOTE + 'a',
1990     X_QUOTE: X_QUOTE + X_QUOTE
1991     }
1992
1993 xDequoteTable = {}
1994
1995 for k, v in xQuoteTable.items():
1996     xDequoteTable[v[-1]] = k
1997
1998 xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
1999
2000 def ctcpQuote(s):
2001     for c in (X_QUOTE, X_DELIM):
2002         s = string.replace(s, c, xQuoteTable[c])
2003     return s
2004
2005 def ctcpDequote(s):
2006     def sub(matchobj, xDequoteTable=xDequoteTable):
2007         s = matchobj.group()[1]
2008         try:
2009             s = xDequoteTable[s]
2010         except KeyError:
2011             s = s
2012         return s
2013
2014     return xEscape_re.sub(sub, s)
2015
2016 def ctcpStringify(messages):
2017     """
2018     @type messages: a list of extended messages.  An extended
2019     message is a (tag, data) tuple, where 'data' may be C{None}, a
2020     string, or a list of strings to be joined with whitespace.
2021
2022     @returns: String
2023     """
2024     coded_messages = []
2025     for (tag, data) in messages:
2026         if data:
2027             if not isinstance(data, types.StringType):
2028                 try:
2029                     # data as list-of-strings
2030                     data = " ".join(map(str, data))
2031                 except TypeError:
2032                     # No?  Then use it's %s representation.
2033                     pass
2034             m = "%s %s" % (tag, data)
2035         else:
2036             m = str(tag)
2037         m = ctcpQuote(m)
2038         m = "%s%s%s" % (X_DELIM, m, X_DELIM)
2039         coded_messages.append(m)
2040
2041     line = string.join(coded_messages, '')
2042     return line
2043
2044
2045 # Constants (from RFC 2812)
2046 RPL_WELCOME = '001'
2047 RPL_YOURHOST = '002'
2048 RPL_CREATED = '003'
2049 RPL_MYINFO = '004'
2050 RPL_BOUNCE = '005'
2051 RPL_USERHOST = '302'
2052 RPL_ISON = '303'
2053 RPL_AWAY = '301'
2054 RPL_UNAWAY = '305'
2055 RPL_NOWAWAY = '306'
2056 RPL_WHOISUSER = '311'
2057 RPL_WHOISSERVER = '312'
2058 RPL_WHOISOPERATOR = '313'
2059 RPL_WHOISIDLE = '317'
2060 RPL_ENDOFWHOIS = '318'
2061 RPL_WHOISCHANNELS = '319'
2062 RPL_WHOWASUSER = '314'
2063 RPL_ENDOFWHOWAS = '369'
2064 RPL_LISTSTART = '321'
2065 RPL_LIST = '322'
2066 RPL_LISTEND = '323'
2067 RPL_UNIQOPIS = '325'
2068 RPL_CHANNELMODEIS = '324'
2069 RPL_NOTOPIC = '331'
2070 RPL_TOPIC = '332'
2071 RPL_INVITING = '341'
2072 RPL_SUMMONING = '342'
2073 RPL_INVITELIST = '346'
2074 RPL_ENDOFINVITELIST = '347'
2075 RPL_EXCEPTLIST = '348'
2076 RPL_ENDOFEXCEPTLIST = '349'
2077 RPL_VERSION = '351'
2078 RPL_WHOREPLY = '352'
2079 RPL_ENDOFWHO = '315'
2080 RPL_NAMREPLY = '353'
2081 RPL_ENDOFNAMES = '366'
2082 RPL_LINKS = '364'
2083 RPL_ENDOFLINKS = '365'
2084 RPL_BANLIST = '367'
2085 RPL_ENDOFBANLIST = '368'
2086 RPL_INFO = '371'
2087 RPL_ENDOFINFO = '374'
2088 RPL_MOTDSTART = '375'
2089 RPL_MOTD = '372'
2090 RPL_ENDOFMOTD = '376'
2091 RPL_YOUREOPER = '381'
2092 RPL_REHASHING = '382'
2093 RPL_YOURESERVICE = '383'
2094 RPL_TIME = '391'
2095 RPL_USERSSTART = '392'
2096 RPL_USERS = '393'
2097 RPL_ENDOFUSERS = '394'
2098 RPL_NOUSERS = '395'
2099 RPL_TRACELINK = '200'
2100 RPL_TRACECONNECTING = '201'
2101 RPL_TRACEHANDSHAKE = '202'
2102 RPL_TRACEUNKNOWN = '203'
2103 RPL_TRACEOPERATOR = '204'
2104 RPL_TRACEUSER = '205'
2105 RPL_TRACESERVER = '206'
2106 RPL_TRACESERVICE = '207'
2107 RPL_TRACENEWTYPE = '208'
2108 RPL_TRACECLASS = '209'
2109 RPL_TRACERECONNECT = '210'
2110 RPL_TRACELOG = '261'
2111 RPL_TRACEEND = '262'
2112 RPL_STATSLINKINFO = '211'
2113 RPL_STATSCOMMANDS = '212'
2114 RPL_ENDOFSTATS = '219'
2115 RPL_STATSUPTIME = '242'
2116 RPL_STATSOLINE = '243'
2117 RPL_UMODEIS = '221'
2118 RPL_SERVLIST = '234'
2119 RPL_SERVLISTEND = '235'
2120 RPL_LUSERCLIENT = '251'
2121 RPL_LUSEROP = '252'
2122 RPL_LUSERUNKNOWN = '253'
2123 RPL_LUSERCHANNELS = '254'
2124 RPL_LUSERME = '255'
2125 RPL_ADMINME = '256'
2126 RPL_ADMINLOC = '257'
2127 RPL_ADMINLOC = '258'
2128 RPL_ADMINEMAIL = '259'
2129 RPL_TRYAGAIN = '263'
2130 ERR_NOSUCHNICK = '401'
2131 ERR_NOSUCHSERVER = '402'
2132 ERR_NOSUCHCHANNEL = '403'
2133 ERR_CANNOTSENDTOCHAN = '404'
2134 ERR_TOOMANYCHANNELS = '405'
2135 ERR_WASNOSUCHNICK = '406'
2136 ERR_TOOMANYTARGETS = '407'
2137 ERR_NOSUCHSERVICE = '408'
2138 ERR_NOORIGIN = '409'
2139 ERR_NORECIPIENT = '411'
2140 ERR_NOTEXTTOSEND = '412'
2141 ERR_NOTOPLEVEL = '413'
2142 ERR_WILDTOPLEVEL = '414'
2143 ERR_BADMASK = '415'
2144 ERR_UNKNOWNCOMMAND = '421'
2145 ERR_NOMOTD = '422'
2146 ERR_NOADMININFO = '423'
2147 ERR_FILEERROR = '424'
2148 ERR_NONICKNAMEGIVEN = '431'
2149 ERR_ERRONEUSNICKNAME = '432'
2150 ERR_NICKNAMEINUSE = '433'
2151 ERR_NICKCOLLISION = '436'
2152 ERR_UNAVAILRESOURCE = '437'
2153 ERR_USERNOTINCHANNEL = '441'
2154 ERR_NOTONCHANNEL = '442'
2155 ERR_USERONCHANNEL = '443'
2156 ERR_NOLOGIN = '444'
2157 ERR_SUMMONDISABLED = '445'
2158 ERR_USERSDISABLED = '446'
2159 ERR_NOTREGISTERED = '451'
2160 ERR_NEEDMOREPARAMS = '461'
2161 ERR_ALREADYREGISTRED = '462'
2162 ERR_NOPERMFORHOST = '463'
2163 ERR_PASSWDMISMATCH = '464'
2164 ERR_YOUREBANNEDCREEP = '465'
2165 ERR_YOUWILLBEBANNED = '466'
2166 ERR_KEYSET = '467'
2167 ERR_CHANNELISFULL = '471'
2168 ERR_UNKNOWNMODE = '472'
2169 ERR_INVITEONLYCHAN = '473'
2170 ERR_BANNEDFROMCHAN = '474'
2171 ERR_BADCHANNELKEY = '475'
2172 ERR_BADCHANMASK = '476'
2173 ERR_NOCHANMODES = '477'
2174 ERR_BANLISTFULL = '478'
2175 ERR_NOPRIVILEGES = '481'
2176 ERR_CHANOPRIVSNEEDED = '482'
2177 ERR_CANTKILLSERVER = '483'
2178 ERR_RESTRICTED = '484'
2179 ERR_UNIQOPPRIVSNEEDED = '485'
2180 ERR_NOOPERHOST = '491'
2181 ERR_NOSERVICEHOST = '492'
2182 ERR_UMODEUNKNOWNFLAG = '501'
2183 ERR_USERSDONTMATCH = '502'
2184
2185 # And hey, as long as the strings are already intern'd...
2186 symbolic_to_numeric = {
2187     "RPL_WELCOME": '001',
2188     "RPL_YOURHOST": '002',
2189     "RPL_CREATED": '003',
2190     "RPL_MYINFO": '004',
2191     "RPL_BOUNCE": '005',
2192     "RPL_USERHOST": '302',
2193     "RPL_ISON": '303',
2194     "RPL_AWAY": '301',
2195     "RPL_UNAWAY": '305',
2196     "RPL_NOWAWAY": '306',
2197     "RPL_WHOISUSER": '311',
2198     "RPL_WHOISSERVER": '312',
2199     "RPL_WHOISOPERATOR": '313',
2200     "RPL_WHOISIDLE": '317',
2201     "RPL_ENDOFWHOIS": '318',
2202     "RPL_WHOISCHANNELS": '319',
2203     "RPL_WHOWASUSER": '314',
2204     "RPL_ENDOFWHOWAS": '369',
2205     "RPL_LISTSTART": '321',
2206     "RPL_LIST": '322',
2207     "RPL_LISTEND": '323',
2208     "RPL_UNIQOPIS": '325',
2209     "RPL_CHANNELMODEIS": '324',
2210     "RPL_NOTOPIC": '331',
2211     "RPL_TOPIC": '332',
2212     "RPL_INVITING": '341',
2213     "RPL_SUMMONING": '342',
2214     "RPL_INVITELIST": '346',
2215     "RPL_ENDOFINVITELIST": '347',
2216     "RPL_EXCEPTLIST": '348',
2217     "RPL_ENDOFEXCEPTLIST": '349',
2218     "RPL_VERSION": '351',
2219     "RPL_WHOREPLY": '352',
2220     "RPL_ENDOFWHO": '315',
2221     "RPL_NAMREPLY": '353',
2222     "RPL_ENDOFNAMES": '366',
2223     "RPL_LINKS": '364',
2224     "RPL_ENDOFLINKS": '365',
2225     "RPL_BANLIST": '367',
2226     "RPL_ENDOFBANLIST": '368',
2227     "RPL_INFO": '371',
2228     "RPL_ENDOFINFO": '374',
2229     "RPL_MOTDSTART": '375',
2230     "RPL_MOTD": '372',
2231     "RPL_ENDOFMOTD": '376',
2232     "RPL_YOUREOPER": '381',
2233     "RPL_REHASHING": '382',
2234     "RPL_YOURESERVICE": '383',
2235     "RPL_TIME": '391',
2236     "RPL_USERSSTART": '392',
2237     "RPL_USERS": '393',
2238     "RPL_ENDOFUSERS": '394',
2239     "RPL_NOUSERS": '395',
2240     "RPL_TRACELINK": '200',
2241     "RPL_TRACECONNECTING": '201',
2242     "RPL_TRACEHANDSHAKE": '202',
2243     "RPL_TRACEUNKNOWN": '203',
2244     "RPL_TRACEOPERATOR": '204',
2245     "RPL_TRACEUSER": '205',
2246     "RPL_TRACESERVER": '206',
2247     "RPL_TRACESERVICE": '207',
2248     "RPL_TRACENEWTYPE": '208',
2249     "RPL_TRACECLASS": '209',
2250     "RPL_TRACERECONNECT": '210',
2251     "RPL_TRACELOG": '261',
2252     "RPL_TRACEEND": '262',
2253     "RPL_STATSLINKINFO": '211',
2254     "RPL_STATSCOMMANDS": '212',
2255     "RPL_ENDOFSTATS": '219',
2256     "RPL_STATSUPTIME": '242',
2257     "RPL_STATSOLINE": '243',
2258     "RPL_UMODEIS": '221',
2259     "RPL_SERVLIST": '234',
2260     "RPL_SERVLISTEND": '235',
2261     "RPL_LUSERCLIENT": '251',
2262     "RPL_LUSEROP": '252',
2263     "RPL_LUSERUNKNOWN": '253',
2264     "RPL_LUSERCHANNELS": '254',
2265     "RPL_LUSERME": '255',
2266     "RPL_ADMINME": '256',
2267     "RPL_ADMINLOC": '257',
2268     "RPL_ADMINLOC": '258',
2269     "RPL_ADMINEMAIL": '259',
2270     "RPL_TRYAGAIN": '263',
2271     "ERR_NOSUCHNICK": '401',
2272     "ERR_NOSUCHSERVER": '402',
2273     "ERR_NOSUCHCHANNEL": '403',
2274     "ERR_CANNOTSENDTOCHAN": '404',
2275     "ERR_TOOMANYCHANNELS": '405',
2276     "ERR_WASNOSUCHNICK": '406',
2277     "ERR_TOOMANYTARGETS": '407',
2278     "ERR_NOSUCHSERVICE": '408',
2279     "ERR_NOORIGIN": '409',
2280     "ERR_NORECIPIENT": '411',
2281     "ERR_NOTEXTTOSEND": '412',
2282     "ERR_NOTOPLEVEL": '413',
2283     "ERR_WILDTOPLEVEL": '414',
2284     "ERR_BADMASK": '415',
2285     "ERR_UNKNOWNCOMMAND": '421',
2286     "ERR_NOMOTD": '422',
2287     "ERR_NOADMININFO": '423',
2288     "ERR_FILEERROR": '424',
2289     "ERR_NONICKNAMEGIVEN": '431',
2290     "ERR_ERRONEUSNICKNAME": '432',
2291     "ERR_NICKNAMEINUSE": '433',
2292     "ERR_NICKCOLLISION": '436',
2293     "ERR_UNAVAILRESOURCE": '437',
2294     "ERR_USERNOTINCHANNEL": '441',
2295     "ERR_NOTONCHANNEL": '442',
2296     "ERR_USERONCHANNEL": '443',
2297     "ERR_NOLOGIN": '444',
2298     "ERR_SUMMONDISABLED": '445',
2299     "ERR_USERSDISABLED": '446',
2300     "ERR_NOTREGISTERED": '451',
2301     "ERR_NEEDMOREPARAMS": '461',
2302     "ERR_ALREADYREGISTRED": '462',
2303     "ERR_NOPERMFORHOST": '463',
2304     "ERR_PASSWDMISMATCH": '464',
2305     "ERR_YOUREBANNEDCREEP": '465',
2306     "ERR_YOUWILLBEBANNED": '466',
2307     "ERR_KEYSET": '467',
2308     "ERR_CHANNELISFULL": '471',
2309     "ERR_UNKNOWNMODE": '472',
2310     "ERR_INVITEONLYCHAN": '473',
2311     "ERR_BANNEDFROMCHAN": '474',
2312     "ERR_BADCHANNELKEY": '475',
2313     "ERR_BADCHANMASK": '476',
2314     "ERR_NOCHANMODES": '477',
2315     "ERR_BANLISTFULL": '478',
2316     "ERR_NOPRIVILEGES": '481',
2317     "ERR_CHANOPRIVSNEEDED": '482',
2318     "ERR_CANTKILLSERVER": '483',
2319     "ERR_RESTRICTED": '484',
2320     "ERR_UNIQOPPRIVSNEEDED": '485',
2321     "ERR_NOOPERHOST": '491',
2322     "ERR_NOSERVICEHOST": '492',
2323     "ERR_UMODEUNKNOWNFLAG": '501',
2324     "ERR_USERSDONTMATCH": '502',
2325 }
2326
2327 numeric_to_symbolic = {}
2328 for k, v in symbolic_to_numeric.items():
2329     numeric_to_symbolic[v] = k