chiark / gitweb /
Found in rapun:/home/matthew/programming/irc; only cvs id changes
[irc.git] / irclib.py
1 # Copyright (C) 1999, 2000 Joel Rosdahl\r
2\r
3 # This program is free software; you can redistribute it and/or\r
4 # modify it under the terms of the GNU General Public License\r
5 # as published by the Free Software Foundation; either version 2\r
6 # of the License, or (at your option) any later version.\r
7 #        \r
8 # This program is distributed in the hope that it will be useful,\r
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of\r
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
11 # GNU General Public License for more details.\r
12\r
13 # You should have received a copy of the GNU General Public License\r
14 # along with this program; if not, write to the Free Software\r
15 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.\r
16 #\r
17 # Joel Rosdahl <joel@rosdahl.net>\r
18 #\r
19 # $Id: irclib.py,v 1.1 2002/02/07 09:15:32 matthew Exp $\r
20 \r
21 """irclib -- Internet Relay Chat (IRC) protocol client library.\r
22 \r
23 This library is intended to encapsulate the IRC protocol at a quite\r
24 low level.  It provides an event-driven IRC client framework.  It has\r
25 a fairly thorough support for the basic IRC protocol and CTCP, but DCC\r
26 connection support is not yet implemented.\r
27 \r
28 In order to understand how to make an IRC client, I'm afraid you more\r
29 or less must understand the IRC specifications.  They are available\r
30 here: [IRC specifications].\r
31 \r
32 The main features of the IRC client framework are:\r
33 \r
34   * Abstraction of the IRC protocol.\r
35   * Handles multiple simultaneous IRC server connections.\r
36   * Handles server PONGing transparently.\r
37   * Messages to the IRC server are done by calling methods on an IRC\r
38     connection object.\r
39   * Messages from an IRC server triggers events, which can be caught\r
40     by event handlers.\r
41   * Reading from and writing to IRC server sockets are normally done\r
42     by an internal select() loop, but the select()ing may be done by\r
43     an external main loop.\r
44   * Functions can be registered to execute at specified times by the\r
45     event-loop.\r
46   * Decodes CTCP tagging correctly (hopefully); I haven't seen any\r
47     other IRC client implementation that handles the CTCP\r
48     specification subtilties.\r
49   * A kind of simple, single-server, object-oriented IRC client class\r
50     that dispatches events to instance methods is included.\r
51 \r
52 Current limitations:\r
53 \r
54   * The IRC protocol shines through the abstraction a bit too much.\r
55   * Data is not written asynchronously to the server, i.e. the write()\r
56     may block if the TCP buffers are stuffed.\r
57   * There are no support for DCC connections.\r
58   * The author haven't even read RFC 2810, 2811, 2812 and 2813.\r
59   * Like most projects, documentation is lacking...\r
60 \r
61 Since I seldom use IRC anymore, I will probably not work much on the\r
62 library.  If you want to help or continue developing the library,\r
63 please contact me (Joel Rosdahl <joel@rosdahl.net>).\r
64 \r
65 .. [IRC specifications] http://www.irchelp.org/irchelp/rfc/\r
66 """\r
67 \r
68 import bisect\r
69 import re\r
70 import select\r
71 import socket\r
72 import string\r
73 import sys\r
74 import time\r
75 import types\r
76 \r
77 VERSION = 0, 3, 1\r
78 DEBUG = 0\r
79 \r
80 # TODO\r
81 # ----\r
82 # DCC\r
83 # (maybe) thread safety\r
84 # (maybe) color parser convenience functions\r
85 # documentation (including all event types)\r
86 # (maybe) add awareness of different types of ircds\r
87 # send data asynchronously to the server\r
88 \r
89 # NOTES\r
90 # -----\r
91 # connection.quit() only sends QUIT to the server.\r
92 # ERROR from the server triggers the error event and the disconnect event.\r
93 # dropping of the connection triggers the disconnect event.\r
94 \r
95 class IRCError(Exception):\r
96     """Represents an IRC exception."""\r
97     pass\r
98 \r
99 \r
100 class IRC:\r
101     """Class that handles one or several IRC server connections.\r
102 \r
103     When an IRC object has been instantiated, it can be used to create\r
104     Connection objects that represent the IRC connections.  The\r
105     responsibility of the IRC object is to provide an event-driven\r
106     framework for the connections and to keep the connections alive.\r
107     It runs a select loop to poll each connection's TCP socket and\r
108     hands over the sockets with incoming data for processing by the\r
109     corresponding connection.\r
110 \r
111     The methods of most interest for an IRC client writer are server,\r
112     add_global_handler, remove_global_handler, execute_at,\r
113     execute_delayed, process_once and process_forever.\r
114 \r
115     Here is an example:\r
116 \r
117         irc = irclib.IRC()\r
118         server = irc.server()\r
119         server.connect(\"irc.some.where\", 6667, \"my_nickname\")\r
120         server.privmsg(\"a_nickname\", \"Hi there!\")\r
121         server.process_forever()\r
122 \r
123     This will connect to the IRC server irc.some.where on port 6667\r
124     using the nickname my_nickname and send the message \"Hi there!\"\r
125     to the nickname a_nickname.\r
126     """\r
127 \r
128     def __init__(self, fn_to_add_socket=None,\r
129                  fn_to_remove_socket=None,\r
130                  fn_to_add_timeout=None):\r
131         """Constructor for IRC objects.\r
132 \r
133         Optional arguments are fn_to_add_socket, fn_to_remove_socket\r
134         and fn_to_add_timeout.  The first two specify functions that\r
135         will be called with a socket object as argument when the IRC\r
136         object wants to be notified (or stop being notified) of data\r
137         coming on a new socket.  When new data arrives, the method\r
138         process_data should be called.  Similarly, fn_to_add_timeout\r
139         is called with a number of seconds (a floating point number)\r
140         as first argument when the IRC object wants to receive a\r
141         notification (by calling the process_timeout method).  So, if\r
142         e.g. the argument is 42.17, the object wants the\r
143         process_timeout method to be called after 42 seconds and 170\r
144         milliseconds.\r
145 \r
146         The three arguments mainly exist to be able to use an external\r
147         main loop (for example Tkinter's or PyGTK's main app loop)\r
148         instead of calling the process_forever method.\r
149 \r
150         An alternative is to just call ServerConnection.process_once()\r
151         once in a while.\r
152         """\r
153 \r
154         if fn_to_add_socket and fn_to_remove_socket:\r
155             self.fn_to_add_socket = fn_to_add_socket\r
156             self.fn_to_remove_socket = fn_to_remove_socket\r
157         else:\r
158             self.fn_to_add_socket = None\r
159             self.fn_to_remove_socket = None\r
160 \r
161         self.fn_to_add_timeout = fn_to_add_timeout\r
162         self.connections = []\r
163         self.handlers = {}\r
164         self.delayed_commands = [] # list of tuples in the format (time, function, arguments)\r
165 \r
166         self.add_global_handler("ping", _ping_ponger, -42)\r
167 \r
168     def server(self):\r
169         """Creates and returns a ServerConnection object."""\r
170 \r
171         c = ServerConnection(self)\r
172         self.connections.append(c)\r
173         return c\r
174 \r
175     def process_data(self, sockets):\r
176         """Called when there is more data to read on connection sockets.\r
177 \r
178         Arguments:\r
179 \r
180             sockets -- A list of socket objects.\r
181 \r
182         See documentation for IRC.__init__.\r
183         """\r
184         for s in sockets:\r
185             for c in self.connections:\r
186                 if s == c._get_socket():\r
187                     c.process_data()\r
188 \r
189     def process_timeout(self):\r
190         """Called when a timeout notification is due.\r
191 \r
192         See documentation for IRC.__init__.\r
193         """\r
194         t = time.time()\r
195         while self.delayed_commands:\r
196             if t >= self.delayed_commands[0][0]:\r
197                 apply(self.delayed_commands[0][1], self.delayed_commands[0][2])\r
198                 del self.delayed_commands[0]\r
199             else:\r
200                 break\r
201 \r
202     def process_once(self, timeout=0):\r
203         """Process data from connections once.\r
204 \r
205         Arguments:\r
206 \r
207             timeout -- How long the select() call should wait if no\r
208                        data is available.\r
209 \r
210         This method should be called periodically to check and process\r
211         incoming data, if there are any.  If that seems boring, look\r
212         at the process_forever method.\r
213         """\r
214         sockets = map(lambda x: x._get_socket(), self.connections)\r
215         sockets = filter(lambda x: x != None, sockets)\r
216         if sockets:\r
217             (i, o, e) = select.select(sockets, [], [], timeout)\r
218             self.process_data(i)\r
219         else:\r
220             time.sleep(timeout)\r
221         self.process_timeout()\r
222 \r
223     def process_forever(self, timeout=0.2):\r
224         """Run an infinite loop, processing data from connections.\r
225 \r
226         This method repeatedly calls process_once.\r
227 \r
228         Arguments:\r
229 \r
230             timeout -- Parameter to pass to process_once.\r
231         """\r
232         while 1:\r
233             self.process_once(timeout)\r
234 \r
235     def disconnect_all(self, message=""):\r
236         """Disconnects all connections."""\r
237         for c in self.connections:\r
238             c.quit(message)\r
239             c.disconnect(message)\r
240 \r
241     def add_global_handler(self, event, handler, priority=0):\r
242         """Adds a global handler function for a specific event type.\r
243 \r
244         Arguments:\r
245 \r
246             event -- Event type (a string).  Check the values of the\r
247             numeric_events dictionary in irclib.py for possible event\r
248             types.\r
249 \r
250             handler -- Callback function.\r
251 \r
252             priority -- A number (the lower number, the higher priority).\r
253 \r
254         The handler function is called whenever the specified event is\r
255         triggered in any of the connections.  See documentation for\r
256         the Event class.\r
257 \r
258         The handler functions are called in priority order (lowest\r
259         number is highest priority).  If a handler function returns\r
260         \"NO MORE\", no more handlers will be called.\r
261         """\r
262 \r
263         if not self.handlers.has_key(event):\r
264             self.handlers[event] = []\r
265         bisect.insort(self.handlers[event], ((priority, handler)))\r
266 \r
267     def remove_global_handler(self, event, handler):\r
268         """Removes a global handler function.\r
269 \r
270         Arguments:\r
271 \r
272             event -- Event type (a string).\r
273 \r
274             handler -- Callback function.\r
275 \r
276         Returns 1 on success, otherwise 0.\r
277         """\r
278         if not self.handlers.has_key(event):\r
279             return 0\r
280         for h in self.handlers[event]:\r
281             if handler == h[1]:\r
282                 self.handlers[event].remove(h)\r
283         return 1\r
284 \r
285     def execute_at(self, at, function, arguments=()):\r
286         """Execute a function at a specified time.\r
287 \r
288         Arguments:\r
289 \r
290             at -- Execute at this time (standard \"time_t\" time).\r
291 \r
292             function -- Function to call.\r
293 \r
294             arguments -- Arguments to give the function.\r
295         """\r
296         self.execute_delayed(at-time.time(), function, arguments)\r
297 \r
298     def execute_delayed(self, delay, function, arguments=()):\r
299         """Execute a function after a specified time.\r
300 \r
301         Arguments:\r
302 \r
303             delay -- How many seconds to wait.\r
304 \r
305             function -- Function to call.\r
306 \r
307             arguments -- Arguments to give the function.\r
308         """\r
309         bisect.insort(self.delayed_commands, (delay+time.time(), function, arguments))\r
310         if self.fn_to_add_timeout:\r
311             self.fn_to_add_timeout(delay)\r
312 \r
313     def _handle_event(self, connection, event):\r
314         """[Internal]"""\r
315         h = self.handlers\r
316         for handler in h.get("all_events", []) + h.get(event.eventtype(), []):\r
317             if handler[1](connection, event) == "NO MORE":\r
318                 return\r
319 \r
320     def _remove_connection(self, connection):\r
321         """[Internal]"""\r
322         self.connections.remove(connection)\r
323         if self.fn_to_remove_socket:\r
324             self.fn_to_remove_socket(connection._get_socket())\r
325 \r
326 _rfc_1459_command_regexp = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?")\r
327 \r
328 \r
329 class Connection:\r
330     """Base class for IRC connections.\r
331 \r
332     Must be overridden.\r
333     """\r
334     def __init__(self, irclibobj):\r
335         self.irclibobj = irclibobj\r
336 \r
337     def _get_socket():\r
338         raise IRCError, "Not overridden"\r
339 \r
340     ##############################\r
341     ### Convenience wrappers.\r
342 \r
343     def execute_at(self, at, function, arguments=()):\r
344         self.irclibobj.execute_at(at, function, arguments)\r
345 \r
346     def execute_delayed(self, delay, function, arguments=()):\r
347         self.irclibobj.execute_delayed(delay, function, arguments)\r
348 \r
349 \r
350 class ServerConnectionError(IRCError):\r
351     pass\r
352 \r
353 \r
354 # Huh!?  Crrrrazy EFNet doesn't follow the RFC: their ircd seems to\r
355 # use \n as message separator!  :P\r
356 _linesep_regexp = re.compile("\r?\n")\r
357 \r
358 class ServerConnection(Connection):\r
359     """This class represents an IRC server connection.\r
360 \r
361     ServerConnection objects are instantiated by calling the server\r
362     method on an IRC object.\r
363     """\r
364 \r
365     def __init__(self, irclibobj):\r
366         Connection.__init__(self, irclibobj)\r
367         self.connected = 0  # Not connected yet.\r
368 \r
369     def connect(self, server, port, nickname, password=None, username=None,\r
370                 ircname=None):\r
371         """Connect/reconnect to a server.\r
372 \r
373         Arguments:\r
374 \r
375             server -- Server name.\r
376 \r
377             port -- Port number.\r
378 \r
379             nickname -- The nickname.\r
380 \r
381             password -- Password (if any).\r
382 \r
383             username -- The username.\r
384 \r
385             ircname -- The IRC name.\r
386 \r
387         This function can be called to reconnect a closed connection.\r
388 \r
389         Returns the ServerConnection object.\r
390         """\r
391         if self.connected:\r
392             self.quit("Changing server")\r
393 \r
394         self.socket = None\r
395         self.previous_buffer = ""\r
396         self.handlers = {}\r
397         self.real_server_name = ""\r
398         self.real_nickname = nickname\r
399         self.server = server\r
400         self.port = port\r
401         self.nickname = nickname\r
402         self.username = username or nickname\r
403         self.ircname = ircname or nickname\r
404         self.password = password\r
405         self.localhost = socket.gethostname()\r
406         self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r
407         try:\r
408             self.socket.connect((self.server, self.port))\r
409         except socket.error, x:\r
410             raise ServerConnectionError, "Couldn't connect to socket: %s" % x\r
411         self.connected = 1\r
412         if self.irclibobj.fn_to_add_socket:\r
413             self.irclibobj.fn_to_add_socket(self.socket)\r
414 \r
415         # Log on...\r
416         if self.password:\r
417             self.pass_(self.password)\r
418         self.nick(self.nickname)\r
419         self.user(self.username, self.localhost, self.server, self.ircname)\r
420         return self\r
421 \r
422     def close(self):\r
423         """Close the connection.\r
424 \r
425         This method closes the connection permanently; after it has\r
426         been called, the object is unusable.\r
427         """\r
428 \r
429         self.disconnect("Closing object")\r
430         self.irclibobj._remove_connection(self)\r
431 \r
432     def _get_socket(self):\r
433         """[Internal]"""\r
434         if self.connected:\r
435             return self.socket\r
436         else:\r
437             return None\r
438 \r
439     def get_server_name(self):\r
440         """Get the (real) server name.\r
441 \r
442         This method returns the (real) server name, or, more\r
443         specifically, what the server calls itself.\r
444         """\r
445 \r
446         if self.real_server_name:\r
447             return self.real_server_name\r
448         else:\r
449             return ""\r
450 \r
451     def get_nickname(self):\r
452         """Get the (real) nick name.\r
453 \r
454         This method returns the (real) nickname.  The library keeps\r
455         track of nick changes, so it might not be the nick name that\r
456         was passed to the connect() method.  """\r
457 \r
458         return self.real_nickname\r
459 \r
460     def process_data(self):\r
461         """[Internal]"""\r
462 \r
463         try:\r
464             new_data = self.socket.recv(2**14)\r
465         except socket.error, x:\r
466             # The server hung up.\r
467             self.disconnect("Connection reset by peer")\r
468             return\r
469         if not new_data:\r
470             # Read nothing: connection must be down.\r
471             self.disconnect("Connection reset by peer")\r
472             return\r
473 \r
474         lines = _linesep_regexp.split(self.previous_buffer + new_data)\r
475 \r
476         # Save the last, unfinished line.\r
477         self.previous_buffer = lines[-1]\r
478         lines = lines[:-1]\r
479 \r
480         for line in lines:\r
481             if DEBUG:\r
482                 print "FROM SERVER:", line\r
483 \r
484             prefix = None\r
485             command = None\r
486             arguments = None\r
487             self._handle_event(Event("all_raw_messages",\r
488                                      self.get_server_name(),\r
489                                      None,\r
490                                      [line]))\r
491 \r
492             m = _rfc_1459_command_regexp.match(line)\r
493             if m.group("prefix"):\r
494                 prefix = m.group("prefix")\r
495                 if not self.real_server_name:\r
496                     self.real_server_name = prefix\r
497 \r
498             if m.group("command"):\r
499                 command = string.lower(m.group("command"))\r
500 \r
501             if m.group("argument"):\r
502                 a = string.split(m.group("argument"), " :", 1)\r
503                 arguments = string.split(a[0])\r
504                 if len(a) == 2:\r
505                     arguments.append(a[1])\r
506 \r
507             if command == "nick":\r
508                 if nm_to_n(prefix) == self.real_nickname:\r
509                     self.real_nickname = arguments[0]\r
510 \r
511             if command in ["privmsg", "notice"]:\r
512                 target, message = arguments[0], arguments[1]\r
513                 messages = _ctcp_dequote(message)\r
514 \r
515                 if command == "privmsg":\r
516                     if is_channel(target):\r
517                         command = "pubmsg"\r
518                 else:\r
519                     if is_channel(target):\r
520                         command = "pubnotice"\r
521                     else:\r
522                         command = "privnotice"\r
523 \r
524                 for m in messages:\r
525                     if type(m) is types.TupleType:\r
526                         if command in ["privmsg", "pubmsg"]:\r
527                             command = "ctcp"\r
528                         else:\r
529                             command = "ctcpreply"\r
530 \r
531                         m = list(m)\r
532                         if DEBUG:\r
533                             print "command: %s, source: %s, target: %s, arguments: %s" % (\r
534                                 command, prefix, target, m)\r
535                         self._handle_event(Event(command, prefix, target, m))\r
536                     else:\r
537                         if DEBUG:\r
538                             print "command: %s, source: %s, target: %s, arguments: %s" % (\r
539                                 command, prefix, target, [m])\r
540                         self._handle_event(Event(command, prefix, target, [m]))\r
541             else:\r
542                 target = None\r
543 \r
544                 if command == "quit":\r
545                     arguments = [arguments[0]]\r
546                 elif command == "ping":\r
547                     target = arguments[0]\r
548                 else:\r
549                     target = arguments[0]\r
550                     arguments = arguments[1:]\r
551 \r
552                 if command == "mode":\r
553                     if not is_channel(target):\r
554                         command = "umode"\r
555 \r
556                 # Translate numerics into more readable strings.\r
557                 if numeric_events.has_key(command):\r
558                     command = numeric_events[command]\r
559 \r
560                 if DEBUG:\r
561                     print "command: %s, source: %s, target: %s, arguments: %s" % (\r
562                         command, prefix, target, arguments)\r
563                 self._handle_event(Event(command, prefix, target, arguments))\r
564 \r
565     def _handle_event(self, event):\r
566         """[Internal]"""\r
567         self.irclibobj._handle_event(self, event)\r
568         if self.handlers.has_key(event.eventtype()):\r
569             for fn in self.handlers[event.eventtype()]:\r
570                 fn(self, event)\r
571 \r
572     def is_connected(self):\r
573         """Return connection status.\r
574 \r
575         Returns true if connected, otherwise false.\r
576         """\r
577         return self.connected\r
578 \r
579     def add_global_handler(self, *args):\r
580         """Add global handler.\r
581 \r
582         See documentation for IRC.add_global_handler.\r
583         """\r
584         apply(self.irclibobj.add_global_handler, args)\r
585 \r
586     def action(self, target, action):\r
587         """Send a CTCP ACTION command."""\r
588         self.ctcp("ACTION", target, action)\r
589 \r
590     def admin(self, server=""):\r
591         """Send an ADMIN command."""\r
592         self.send_raw(string.strip(string.join(["ADMIN", server])))\r
593 \r
594     def ctcp(self, ctcptype, target, parameter=""):\r
595         """Send a CTCP command."""\r
596         ctcptype = string.upper(ctcptype)\r
597         self.privmsg(target, "\001%s%s\001" % (ctcptype, parameter and (" " + parameter) or ""))\r
598 \r
599     def ctcp_reply(self, target, parameter):\r
600         """Send a CTCP REPLY command."""\r
601         self.notice(target, "\001%s\001" % parameter)\r
602 \r
603     def disconnect(self, message=""):\r
604         """Hang up the connection.\r
605 \r
606         Arguments:\r
607 \r
608             message -- Quit message.\r
609         """\r
610         if self.connected == 0:\r
611             return\r
612 \r
613         self.connected = 0\r
614         try:\r
615             self.socket.close()\r
616         except socket.error, x:\r
617             pass\r
618         self.socket = None\r
619         self._handle_event(Event("disconnect", self.server, "", [message]))\r
620 \r
621     def globops(self, text):\r
622         """Send a GLOBOPS command."""\r
623         self.send_raw("GLOBOPS :" + text)\r
624 \r
625     def info(self, server=""):\r
626         """Send an INFO command."""\r
627         self.send_raw(string.strip(string.join(["INFO", server])))\r
628 \r
629     def invite(self, nick, channel):\r
630         """Send an INVITE command."""\r
631         self.send_raw(string.strip(string.join(["INVITE", nick, channel])))\r
632 \r
633     def ison(self, nicks):\r
634         """Send an ISON command.\r
635 \r
636         Arguments:\r
637 \r
638             nicks -- List of nicks.\r
639         """\r
640         self.send_raw("ISON " + string.join(nicks, ","))\r
641 \r
642     def join(self, channel, key=""):\r
643         """Send a JOIN command."""\r
644         self.send_raw("JOIN %s%s" % (channel, (key and (" " + key))))\r
645 \r
646     def kick(self, channel, nick, comment=""):\r
647         """Send a KICK command."""\r
648         self.send_raw("KICK %s %s%s" % (channel, nick, (comment and (" :" + comment))))\r
649 \r
650     def links(self, remote_server="", server_mask=""):\r
651         """Send a LINKS command."""\r
652         command = "LINKS"\r
653         if remote_server:\r
654             command = command + " " + remote_server\r
655         if server_mask:\r
656             command = command + " " + server_mask\r
657         self.send_raw(command)\r
658 \r
659     def list(self, channels=None, server=""):\r
660         """Send a LIST command."""\r
661         command = "LIST"\r
662         if channels:\r
663             command = command + " " + string.join(channels, ",")\r
664         if server:\r
665             command = command + " " + server\r
666         self.send_raw(command)\r
667 \r
668     def lusers(self, server=""):\r
669         """Send a LUSERS command."""\r
670         self.send_raw("LUSERS" + (server and (" " + server)))\r
671 \r
672     def mode(self, target, command):\r
673         """Send a MODE command."""\r
674         self.send_raw("MODE %s %s" % (target, command))\r
675 \r
676     def motd(self, server=""):\r
677         """Send an MOTD command."""\r
678         self.send_raw("MOTD" + (server and (" " + server)))\r
679 \r
680     def names(self, channels=None):\r
681         """Send a NAMES command."""\r
682         self.send_raw("NAMES" + (channels and (" " + string.join(channels, ",")) or ""))\r
683 \r
684     def nick(self, newnick):\r
685         """Send a NICK command."""\r
686         self.send_raw("NICK " + newnick)\r
687 \r
688     def notice(self, target, text):\r
689         """Send a NOTICE command."""\r
690         # Should limit len(text) here!\r
691         self.send_raw("NOTICE %s :%s" % (target, text))\r
692 \r
693     def oper(self, nick, password):\r
694         """Send an OPER command."""\r
695         self.send_raw("OPER %s %s" % (nick, password))\r
696 \r
697     def part(self, channels):\r
698         """Send a PART command."""\r
699         if type(channels) == types.StringType:\r
700             self.send_raw("PART " + channels)\r
701         else:\r
702             self.send_raw("PART " + string.join(channels, ","))\r
703 \r
704     def pass_(self, password):\r
705         """Send a PASS command."""\r
706         self.send_raw("PASS " + password)\r
707 \r
708     def ping(self, target, target2=""):\r
709         """Send a PING command."""\r
710         self.send_raw("PING %s%s" % (target, target2 and (" " + target2)))\r
711 \r
712     def pong(self, target, target2=""):\r
713         """Send a PONG command."""\r
714         self.send_raw("PONG %s%s" % (target, target2 and (" " + target2)))\r
715 \r
716     def privmsg(self, target, text):\r
717         """Send a PRIVMSG command."""\r
718         # Should limit len(text) here!\r
719         self.send_raw("PRIVMSG %s :%s" % (target, text))\r
720 \r
721     def privmsg_many(self, targets, text):\r
722         """Send a PRIVMSG command to multiple targets."""\r
723         # Should limit len(text) here!\r
724         self.send_raw("PRIVMSG %s :%s" % (string.join(targets, ","), text))\r
725 \r
726     def quit(self, message=""):\r
727         """Send a QUIT command."""\r
728         self.send_raw("QUIT" + (message and (" :" + message)))\r
729 \r
730     def sconnect(self, target, port="", server=""):\r
731         """Send an SCONNECT command."""\r
732         self.send_raw("CONNECT %s%s%s" % (target,\r
733                                           port and (" " + port),\r
734                                           server and (" " + server)))\r
735 \r
736     def send_raw(self, string):\r
737         """Send raw string to the server.\r
738 \r
739         The string will be padded with appropriate CR LF.\r
740         """\r
741         try:\r
742             self.socket.send(string + "\r\n")\r
743             if DEBUG:\r
744                 print "TO SERVER:", string\r
745         except socket.error, x:\r
746             # Aouch!\r
747             self.disconnect("Connection reset by peer.")\r
748 \r
749     def squit(self, server, comment=""):\r
750         """Send an SQUIT command."""\r
751         self.send_raw("SQUIT %s%s" % (server, comment and (" :" + comment)))\r
752 \r
753     def stats(self, statstype, server=""):\r
754         """Send a STATS command."""\r
755         self.send_raw("STATS %s%s" % (statstype, server and (" " + server)))\r
756 \r
757     def time(self, server=""):\r
758         """Send a TIME command."""\r
759         self.send_raw("TIME" + (server and (" " + server)))\r
760 \r
761     def topic(self, channel, new_topic=None):\r
762         """Send a TOPIC command."""\r
763         if new_topic == None:\r
764             self.send_raw("TOPIC " + channel)\r
765         else:\r
766             self.send_raw("TOPIC %s :%s" % (channel, new_topic))\r
767 \r
768     def trace(self, target=""):\r
769         """Send a TRACE command."""\r
770         self.send_raw("TRACE" + (target and (" " + target)))\r
771 \r
772     def user(self, username, localhost, server, ircname):\r
773         """Send a USER command."""\r
774         self.send_raw("USER %s %s %s :%s" % (username, localhost, server, ircname))\r
775 \r
776     def userhost(self, nicks):\r
777         """Send a USERHOST command."""\r
778         self.send_raw("USERHOST " + string.join(nicks, ","))\r
779 \r
780     def users(self, server=""):\r
781         """Send a USERS command."""\r
782         self.send_raw("USERS" + (server and (" " + server)))\r
783 \r
784     def version(self, server=""):\r
785         """Send a VERSION command."""\r
786         self.send_raw("VERSION" + (server and (" " + server)))\r
787 \r
788     def wallops(self, text):\r
789         """Send a WALLOPS command."""\r
790         self.send_raw("WALLOPS :" + text)\r
791 \r
792     def who(self, target="", op=""):\r
793         """Send a WHO command."""\r
794         self.send_raw("WHO%s%s" % (target and (" " + target), op and (" o")))\r
795 \r
796     def whois(self, targets):\r
797         """Send a WHOIS command."""\r
798         self.send_raw("WHOIS " + string.join(targets, ","))\r
799 \r
800     def whowas(self, nick, max=None, server=""):\r
801         """Send a WHOWAS command."""\r
802         self.send_raw("WHOWAS %s%s%s" % (nick,\r
803                                          max and (" " + max),\r
804                                          server and (" " + server)))\r
805 \r
806 \r
807 class DCCConnection(Connection):\r
808     """Unimplemented."""\r
809     def __init__(self):\r
810         raise IRCError, "Unimplemented."\r
811 \r
812 \r
813 class SimpleIRCClient:\r
814     """A simple single-server IRC client class.\r
815 \r
816     This is an example of an object-oriented wrapper of the IRC\r
817     framework.  A real IRC client can be made by subclassing this\r
818     class and adding appropriate methods.\r
819 \r
820     The method on_join will be called when a "join" event is created\r
821     (which is done when the server sends a JOIN messsage/command),\r
822     on_privmsg will be called for "privmsg" events, and so on.  The\r
823     handler methods get two arguments: the connection object (same as\r
824     self.connection) and the event object.\r
825 \r
826     Instance attributes that can be used by sub classes:\r
827 \r
828         ircobj -- The IRC instance.\r
829 \r
830         connection -- The ServerConnection instance.\r
831     """\r
832     def __init__(self):\r
833         self.ircobj = IRC()\r
834         self.connection = self.ircobj.server()\r
835         self.ircobj.add_global_handler("all_events", self._dispatcher, -10)\r
836 \r
837     def _dispatcher(self, c, e):\r
838         """[Internal]"""\r
839         m = "on_" + e.eventtype()\r
840         if hasattr(self, m):\r
841             getattr(self, m)(c, e)\r
842 \r
843     def connect(self, server, port, nickname, password=None, username=None,\r
844                 ircname=None):\r
845         """Connect/reconnect to a server.\r
846 \r
847         Arguments:\r
848 \r
849             server -- Server name.\r
850 \r
851             port -- Port number.\r
852 \r
853             nickname -- The nickname.\r
854 \r
855             password -- Password (if any).\r
856 \r
857             username -- The username.\r
858 \r
859             ircname -- The IRC name.\r
860 \r
861         This function can be called to reconnect a closed connection.\r
862         """\r
863         self.connection.connect(server, port, nickname,\r
864                                 password, username, ircname)\r
865 \r
866     def start(self):\r
867         """Start the IRC client."""\r
868         self.ircobj.process_forever()\r
869 \r
870 \r
871 class Event:\r
872     """Class representing an IRC event."""\r
873     def __init__(self, eventtype, source, target, arguments=None):\r
874         """Constructor of Event objects.\r
875 \r
876         Arguments:\r
877 \r
878             eventtype -- A string describing the event.\r
879 \r
880             source -- The originator of the event (a nick mask or a server). XXX Correct?\r
881 \r
882             target -- The target of the event (a nick or a channel). XXX Correct?\r
883 \r
884             arguments -- Any event specific arguments.\r
885         """\r
886         self._eventtype = eventtype\r
887         self._source = source\r
888         self._target = target\r
889         if arguments:\r
890             self._arguments = arguments\r
891         else:\r
892             self._arguments = []\r
893 \r
894     def eventtype(self):\r
895         """Get the event type."""\r
896         return self._eventtype\r
897 \r
898     def source(self):\r
899         """Get the event source."""\r
900         return self._source\r
901 \r
902     def target(self):\r
903         """Get the event target."""\r
904         return self._target\r
905 \r
906     def arguments(self):\r
907         """Get the event arguments."""\r
908         return self._arguments\r
909 \r
910 _LOW_LEVEL_QUOTE = "\020"\r
911 _CTCP_LEVEL_QUOTE = "\134"\r
912 _CTCP_DELIMITER = "\001"\r
913 \r
914 _low_level_mapping = {\r
915     "0": "\000",\r
916     "n": "\n",\r
917     "r": "\r",\r
918     _LOW_LEVEL_QUOTE: _LOW_LEVEL_QUOTE\r
919 }\r
920 \r
921 _low_level_regexp = re.compile(_LOW_LEVEL_QUOTE + "(.)")\r
922 \r
923 def mask_matches(nick, mask):\r
924     """Check if a nick matches a mask.\r
925 \r
926     Returns true if the nick matches, otherwise false.\r
927     """\r
928     nick = irc_lower(nick)\r
929     mask = irc_lower(mask)\r
930     mask = string.replace(mask, "\\", "\\\\")\r
931     for ch in ".$|[](){}+":\r
932         mask = string.replace(mask, ch, "\\" + ch)\r
933     mask = string.replace(mask, "?", ".")\r
934     mask = string.replace(mask, "*", ".*")\r
935     r = re.compile(mask, re.IGNORECASE)\r
936     return r.match(nick)\r
937 \r
938 _alpha = "abcdefghijklmnopqrstuvxyz"\r
939 _special = "-[]\\`^{}"\r
940 nick_characters = _alpha + string.upper(_alpha) + string.digits + _special\r
941 _ircstring_translation = string.maketrans(string.upper(_alpha) + "[]\\^",\r
942                                           _alpha + "{}|~")\r
943 \r
944 def irc_lower(s):\r
945     """Returns a lowercased string.\r
946 \r
947     The definition of lowercased comes from the IRC specification (RFC\r
948     1459).\r
949     """\r
950     return string.translate(s, _ircstring_translation)\r
951 \r
952 def _ctcp_dequote(message):\r
953     """[Internal] Dequote a message according to CTCP specifications.\r
954 \r
955     The function returns a list where each element can be either a\r
956     string (normal message) or a tuple of one or two strings (tagged\r
957     messages).  If a tuple has only one element (ie is a singleton),\r
958     that element is the tag; otherwise the tuple has two elements: the\r
959     tag and the data.\r
960 \r
961     Arguments:\r
962 \r
963         message -- The message to be decoded.\r
964     """\r
965 \r
966     def _low_level_replace(match_obj):\r
967         ch = match_obj.group(1)\r
968 \r
969         # If low_level_mapping doesn't have the character as key, we\r
970         # should just return the character.\r
971         return _low_level_mapping.get(ch, ch)\r
972 \r
973     if _LOW_LEVEL_QUOTE in message:\r
974         # Yup, there was a quote.  Release the dequoter, man!\r
975         message = _low_level_regexp.sub(_low_level_replace, message)\r
976 \r
977     if _CTCP_DELIMITER not in message:\r
978         return [message]\r
979     else:\r
980         # Split it into parts.  (Does any IRC client actually *use*\r
981         # CTCP stacking like this?)\r
982         chunks = string.split(message, _CTCP_DELIMITER)\r
983 \r
984         messages = []\r
985         i = 0\r
986         while i < len(chunks)-1:\r
987             # Add message if it's non-empty.\r
988             if len(chunks[i]) > 0:\r
989                 messages.append(chunks[i])\r
990 \r
991             if i < len(chunks)-2:\r
992                 # Aye!  CTCP tagged data ahead!\r
993                 messages.append(tuple(string.split(chunks[i+1], " ", 1)))\r
994 \r
995             i = i + 2\r
996 \r
997         if len(chunks) % 2 == 0:\r
998             # Hey, a lonely _CTCP_DELIMITER at the end!  This means\r
999             # that the last chunk, including the delimiter, is a\r
1000             # normal message!  (This is according to the CTCP\r
1001             # specification.)\r
1002             messages.append(_CTCP_DELIMITER + chunks[-1])\r
1003 \r
1004         return messages\r
1005 \r
1006 def is_channel(string):\r
1007     """Check if a string is a channel name.\r
1008 \r
1009     Returns true if the argument is a channel name, otherwise false.\r
1010     """\r
1011     return string and string[0] in "#&+!"\r
1012 \r
1013 def nm_to_n(s):\r
1014     """Get the nick part of a nickmask.\r
1015 \r
1016     (The source of an Event is a nickmask.)\r
1017     """\r
1018     return string.split(s, "!")[0]\r
1019 \r
1020 def nm_to_uh(s):\r
1021     """Get the userhost part of a nickmask.\r
1022 \r
1023     (The source of an Event is a nickmask.)\r
1024     """\r
1025     return string.split(s, "!")[1]\r
1026 \r
1027 def nm_to_h(s):\r
1028     """Get the host part of a nickmask.\r
1029 \r
1030     (The source of an Event is a nickmask.)\r
1031     """\r
1032     return string.split(s, "@")[1]\r
1033 \r
1034 def nm_to_u(s):\r
1035     """Get the user part of a nickmask.\r
1036 \r
1037     (The source of an Event is a nickmask.)\r
1038     """\r
1039     s = string.split(s, "!")[1]\r
1040     return string.split(s, "@")[0]\r
1041 \r
1042 def parse_nick_modes(mode_string):\r
1043     """Parse a nick mode string.\r
1044 \r
1045     The function returns a list of lists with three members: sign,\r
1046     mode and argument.  The sign is \"+\" or \"-\".  The argument is\r
1047     always None.\r
1048 \r
1049     Example:\r
1050 \r
1051     >>> irclib.parse_nick_modes(\"+ab-c\")\r
1052     [['+', 'a', None], ['+', 'b', None], ['-', 'c', None]]\r
1053     """\r
1054 \r
1055     return _parse_modes(mode_string, "")\r
1056 \r
1057 def parse_channel_modes(mode_string):\r
1058     """Parse a channel mode string.\r
1059 \r
1060     The function returns a list of lists with three members: sign,\r
1061     mode and argument.  The sign is \"+\" or \"-\".  The argument is\r
1062     None if mode isn't one of \"b\", \"k\", \"l\", \"v\" or \"o\".\r
1063 \r
1064     Example:\r
1065 \r
1066     >>> irclib.parse_channel_modes(\"+ab-c foo\")\r
1067     [['+', 'a', None], ['+', 'b', 'foo'], ['-', 'c', None]]\r
1068     """\r
1069 \r
1070     return _parse_modes(mode_string, "bklvo")\r
1071 \r
1072 def _parse_modes(mode_string, unary_modes=""):\r
1073     """[Internal]"""\r
1074     modes = []\r
1075     arg_count = 0\r
1076 \r
1077     # State variable.\r
1078     sign = ""\r
1079 \r
1080     a = string.split(mode_string)\r
1081     if len(a) == 0:\r
1082         return []\r
1083     else:\r
1084         mode_part, args = a[0], a[1:]\r
1085 \r
1086     if mode_part[0] not in "+-":\r
1087         return []\r
1088     for ch in mode_part:\r
1089         if ch in "+-":\r
1090             sign = ch\r
1091         elif ch == " ":\r
1092             collecting_arguments = 1\r
1093         elif ch in unary_modes:\r
1094             modes.append([sign, ch, args[arg_count]])\r
1095             arg_count = arg_count + 1\r
1096         else:\r
1097             modes.append([sign, ch, None])\r
1098     return modes\r
1099 \r
1100 def _ping_ponger(connection, event):\r
1101     """[Internal]"""\r
1102     connection.pong(event.target())\r
1103 \r
1104 # Numeric table mostly stolen from the Perl IRC module (Net::IRC).\r
1105 numeric_events = {\r
1106     "001": "welcome",\r
1107     "002": "yourhost",\r
1108     "003": "created",\r
1109     "004": "myinfo",\r
1110     "005": "featurelist",  # XXX\r
1111     "200": "tracelink",\r
1112     "201": "traceconnecting",\r
1113     "202": "tracehandshake",\r
1114     "203": "traceunknown",\r
1115     "204": "traceoperator",\r
1116     "205": "traceuser",\r
1117     "206": "traceserver",\r
1118     "208": "tracenewtype",\r
1119     "209": "traceclass",\r
1120     "211": "statslinkinfo",\r
1121     "212": "statscommands",\r
1122     "213": "statscline",\r
1123     "214": "statsnline",\r
1124     "215": "statsiline",\r
1125     "216": "statskline",\r
1126     "217": "statsqline",\r
1127     "218": "statsyline",\r
1128     "219": "endofstats",\r
1129     "221": "umodeis",\r
1130     "231": "serviceinfo",\r
1131     "232": "endofservices",\r
1132     "233": "service",\r
1133     "234": "servlist",\r
1134     "235": "servlistend",\r
1135     "241": "statslline",\r
1136     "242": "statsuptime",\r
1137     "243": "statsoline",\r
1138     "244": "statshline",\r
1139     "250": "luserconns",\r
1140     "251": "luserclient",\r
1141     "252": "luserop",\r
1142     "253": "luserunknown",\r
1143     "254": "luserchannels",\r
1144     "255": "luserme",\r
1145     "256": "adminme",\r
1146     "257": "adminloc1",\r
1147     "258": "adminloc2",\r
1148     "259": "adminemail",\r
1149     "261": "tracelog",\r
1150     "262": "endoftrace",\r
1151     "265": "n_local",\r
1152     "266": "n_global",\r
1153     "300": "none",\r
1154     "301": "away",\r
1155     "302": "userhost",\r
1156     "303": "ison",\r
1157     "305": "unaway",\r
1158     "306": "nowaway",\r
1159     "311": "whoisuser",\r
1160     "312": "whoisserver",\r
1161     "313": "whoisoperator",\r
1162     "314": "whowasuser",\r
1163     "315": "endofwho",\r
1164     "316": "whoischanop",\r
1165     "317": "whoisidle",\r
1166     "318": "endofwhois",\r
1167     "319": "whoischannels",\r
1168     "321": "liststart",\r
1169     "322": "list",\r
1170     "323": "listend",\r
1171     "324": "channelmodeis",\r
1172     "329": "channelcreate",\r
1173     "331": "notopic",\r
1174     "332": "topic",\r
1175     "333": "topicinfo",\r
1176     "341": "inviting",\r
1177     "342": "summoning",\r
1178     "351": "version",\r
1179     "352": "whoreply",\r
1180     "353": "namreply",\r
1181     "361": "killdone",\r
1182     "362": "closing",\r
1183     "363": "closeend",\r
1184     "364": "links",\r
1185     "365": "endoflinks",\r
1186     "366": "endofnames",\r
1187     "367": "banlist",\r
1188     "368": "endofbanlist",\r
1189     "369": "endofwhowas",\r
1190     "371": "info",\r
1191     "372": "motd",\r
1192     "373": "infostart",\r
1193     "374": "endofinfo",\r
1194     "375": "motdstart",\r
1195     "376": "endofmotd",\r
1196     "377": "motd2",        # 1997-10-16 -- tkil\r
1197     "381": "youreoper",\r
1198     "382": "rehashing",\r
1199     "384": "myportis",\r
1200     "391": "time",\r
1201     "392": "usersstart",\r
1202     "393": "users",\r
1203     "394": "endofusers",\r
1204     "395": "nousers",\r
1205     "401": "nosuchnick",\r
1206     "402": "nosuchserver",\r
1207     "403": "nosuchchannel",\r
1208     "404": "cannotsendtochan",\r
1209     "405": "toomanychannels",\r
1210     "406": "wasnosuchnick",\r
1211     "407": "toomanytargets",\r
1212     "409": "noorigin",\r
1213     "411": "norecipient",\r
1214     "412": "notexttosend",\r
1215     "413": "notoplevel",\r
1216     "414": "wildtoplevel",\r
1217     "421": "unknowncommand",\r
1218     "422": "nomotd",\r
1219     "423": "noadmininfo",\r
1220     "424": "fileerror",\r
1221     "431": "nonicknamegiven",\r
1222     "432": "erroneusnickname", # Thiss iz how its speld in thee RFC.\r
1223     "433": "nicknameinuse",\r
1224     "436": "nickcollision",\r
1225     "441": "usernotinchannel",\r
1226     "442": "notonchannel",\r
1227     "443": "useronchannel",\r
1228     "444": "nologin",\r
1229     "445": "summondisabled",\r
1230     "446": "usersdisabled",\r
1231     "451": "notregistered",\r
1232     "461": "needmoreparams",\r
1233     "462": "alreadyregistered",\r
1234     "463": "nopermforhost",\r
1235     "464": "passwdmismatch",\r
1236     "465": "yourebannedcreep", # I love this one...\r
1237     "466": "youwillbebanned",\r
1238     "467": "keyset",\r
1239     "471": "channelisfull",\r
1240     "472": "unknownmode",\r
1241     "473": "inviteonlychan",\r
1242     "474": "bannedfromchan",\r
1243     "475": "badchannelkey",\r
1244     "476": "badchanmask",\r
1245     "481": "noprivileges",\r
1246     "482": "chanoprivsneeded",\r
1247     "483": "cantkillserver",\r
1248     "491": "nooperhost",\r
1249     "492": "noservicehost",\r
1250     "501": "umodeunknownflag",\r
1251     "502": "usersdontmatch",\r
1252 }\r
1253 \r
1254 generated_events = [\r
1255     # Generated events\r
1256     "disconnect",\r
1257     "ctcp",\r
1258     "ctcpreply"\r
1259 ]\r
1260 \r
1261 protocol_events = [\r
1262     # IRC protocol events\r
1263     "error",\r
1264     "join",\r
1265     "kick",\r
1266     "mode",\r
1267     "part",\r
1268     "ping",\r
1269     "privmsg",\r
1270     "privnotice",\r
1271     "pubmsg",\r
1272     "pubnotice",\r
1273     "quit"\r
1274 ]\r
1275 \r
1276 all_events = generated_events + protocol_events + numeric_events.values()\r