1 # Copyright (C) 1999, 2000 Joel Rosdahl
\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
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
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
17 # Joel Rosdahl <joel@rosdahl.net>
\r
19 # $Id: irclib.py,v 1.1 2002/02/07 09:15:32 matthew Exp $
\r
21 """irclib -- Internet Relay Chat (IRC) protocol client library.
\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
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
32 The main features of the IRC client framework are:
\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
39 * Messages from an IRC server triggers events, which can be caught
\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
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
52 Current limitations:
\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
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
65 .. [IRC specifications] http://www.irchelp.org/irchelp/rfc/
\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
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
95 class IRCError(Exception):
\r
96 """Represents an IRC exception."""
\r
101 """Class that handles one or several IRC server connections.
\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
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
115 Here is an example:
\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
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
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
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
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
150 An alternative is to just call ServerConnection.process_once()
\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
158 self.fn_to_add_socket = None
\r
159 self.fn_to_remove_socket = None
\r
161 self.fn_to_add_timeout = fn_to_add_timeout
\r
162 self.connections = []
\r
164 self.delayed_commands = [] # list of tuples in the format (time, function, arguments)
\r
166 self.add_global_handler("ping", _ping_ponger, -42)
\r
169 """Creates and returns a ServerConnection object."""
\r
171 c = ServerConnection(self)
\r
172 self.connections.append(c)
\r
175 def process_data(self, sockets):
\r
176 """Called when there is more data to read on connection sockets.
\r
180 sockets -- A list of socket objects.
\r
182 See documentation for IRC.__init__.
\r
185 for c in self.connections:
\r
186 if s == c._get_socket():
\r
189 def process_timeout(self):
\r
190 """Called when a timeout notification is due.
\r
192 See documentation for IRC.__init__.
\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
202 def process_once(self, timeout=0):
\r
203 """Process data from connections once.
\r
207 timeout -- How long the select() call should wait if no
\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
214 sockets = map(lambda x: x._get_socket(), self.connections)
\r
215 sockets = filter(lambda x: x != None, sockets)
\r
217 (i, o, e) = select.select(sockets, [], [], timeout)
\r
218 self.process_data(i)
\r
220 time.sleep(timeout)
\r
221 self.process_timeout()
\r
223 def process_forever(self, timeout=0.2):
\r
224 """Run an infinite loop, processing data from connections.
\r
226 This method repeatedly calls process_once.
\r
230 timeout -- Parameter to pass to process_once.
\r
233 self.process_once(timeout)
\r
235 def disconnect_all(self, message=""):
\r
236 """Disconnects all connections."""
\r
237 for c in self.connections:
\r
239 c.disconnect(message)
\r
241 def add_global_handler(self, event, handler, priority=0):
\r
242 """Adds a global handler function for a specific event type.
\r
246 event -- Event type (a string). Check the values of the
\r
247 numeric_events dictionary in irclib.py for possible event
\r
250 handler -- Callback function.
\r
252 priority -- A number (the lower number, the higher priority).
\r
254 The handler function is called whenever the specified event is
\r
255 triggered in any of the connections. See documentation for
\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
263 if not self.handlers.has_key(event):
\r
264 self.handlers[event] = []
\r
265 bisect.insort(self.handlers[event], ((priority, handler)))
\r
267 def remove_global_handler(self, event, handler):
\r
268 """Removes a global handler function.
\r
272 event -- Event type (a string).
\r
274 handler -- Callback function.
\r
276 Returns 1 on success, otherwise 0.
\r
278 if not self.handlers.has_key(event):
\r
280 for h in self.handlers[event]:
\r
281 if handler == h[1]:
\r
282 self.handlers[event].remove(h)
\r
285 def execute_at(self, at, function, arguments=()):
\r
286 """Execute a function at a specified time.
\r
290 at -- Execute at this time (standard \"time_t\" time).
\r
292 function -- Function to call.
\r
294 arguments -- Arguments to give the function.
\r
296 self.execute_delayed(at-time.time(), function, arguments)
\r
298 def execute_delayed(self, delay, function, arguments=()):
\r
299 """Execute a function after a specified time.
\r
303 delay -- How many seconds to wait.
\r
305 function -- Function to call.
\r
307 arguments -- Arguments to give the function.
\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
313 def _handle_event(self, connection, event):
\r
316 for handler in h.get("all_events", []) + h.get(event.eventtype(), []):
\r
317 if handler[1](connection, event) == "NO MORE":
\r
320 def _remove_connection(self, connection):
\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
326 _rfc_1459_command_regexp = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?")
\r
330 """Base class for IRC connections.
\r
332 Must be overridden.
\r
334 def __init__(self, irclibobj):
\r
335 self.irclibobj = irclibobj
\r
338 raise IRCError, "Not overridden"
\r
340 ##############################
\r
341 ### Convenience wrappers.
\r
343 def execute_at(self, at, function, arguments=()):
\r
344 self.irclibobj.execute_at(at, function, arguments)
\r
346 def execute_delayed(self, delay, function, arguments=()):
\r
347 self.irclibobj.execute_delayed(delay, function, arguments)
\r
350 class ServerConnectionError(IRCError):
\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
358 class ServerConnection(Connection):
\r
359 """This class represents an IRC server connection.
\r
361 ServerConnection objects are instantiated by calling the server
\r
362 method on an IRC object.
\r
365 def __init__(self, irclibobj):
\r
366 Connection.__init__(self, irclibobj)
\r
367 self.connected = 0 # Not connected yet.
\r
369 def connect(self, server, port, nickname, password=None, username=None,
\r
371 """Connect/reconnect to a server.
\r
375 server -- Server name.
\r
377 port -- Port number.
\r
379 nickname -- The nickname.
\r
381 password -- Password (if any).
\r
383 username -- The username.
\r
385 ircname -- The IRC name.
\r
387 This function can be called to reconnect a closed connection.
\r
389 Returns the ServerConnection object.
\r
392 self.quit("Changing server")
\r
395 self.previous_buffer = ""
\r
397 self.real_server_name = ""
\r
398 self.real_nickname = nickname
\r
399 self.server = server
\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
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
412 if self.irclibobj.fn_to_add_socket:
\r
413 self.irclibobj.fn_to_add_socket(self.socket)
\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
423 """Close the connection.
\r
425 This method closes the connection permanently; after it has
\r
426 been called, the object is unusable.
\r
429 self.disconnect("Closing object")
\r
430 self.irclibobj._remove_connection(self)
\r
432 def _get_socket(self):
\r
439 def get_server_name(self):
\r
440 """Get the (real) server name.
\r
442 This method returns the (real) server name, or, more
\r
443 specifically, what the server calls itself.
\r
446 if self.real_server_name:
\r
447 return self.real_server_name
\r
451 def get_nickname(self):
\r
452 """Get the (real) nick name.
\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
458 return self.real_nickname
\r
460 def process_data(self):
\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
470 # Read nothing: connection must be down.
\r
471 self.disconnect("Connection reset by peer")
\r
474 lines = _linesep_regexp.split(self.previous_buffer + new_data)
\r
476 # Save the last, unfinished line.
\r
477 self.previous_buffer = lines[-1]
\r
482 print "FROM SERVER:", line
\r
487 self._handle_event(Event("all_raw_messages",
\r
488 self.get_server_name(),
\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
498 if m.group("command"):
\r
499 command = string.lower(m.group("command"))
\r
501 if m.group("argument"):
\r
502 a = string.split(m.group("argument"), " :", 1)
\r
503 arguments = string.split(a[0])
\r
505 arguments.append(a[1])
\r
507 if command == "nick":
\r
508 if nm_to_n(prefix) == self.real_nickname:
\r
509 self.real_nickname = arguments[0]
\r
511 if command in ["privmsg", "notice"]:
\r
512 target, message = arguments[0], arguments[1]
\r
513 messages = _ctcp_dequote(message)
\r
515 if command == "privmsg":
\r
516 if is_channel(target):
\r
519 if is_channel(target):
\r
520 command = "pubnotice"
\r
522 command = "privnotice"
\r
525 if type(m) is types.TupleType:
\r
526 if command in ["privmsg", "pubmsg"]:
\r
529 command = "ctcpreply"
\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
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
544 if command == "quit":
\r
545 arguments = [arguments[0]]
\r
546 elif command == "ping":
\r
547 target = arguments[0]
\r
549 target = arguments[0]
\r
550 arguments = arguments[1:]
\r
552 if command == "mode":
\r
553 if not is_channel(target):
\r
556 # Translate numerics into more readable strings.
\r
557 if numeric_events.has_key(command):
\r
558 command = numeric_events[command]
\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
565 def _handle_event(self, event):
\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
572 def is_connected(self):
\r
573 """Return connection status.
\r
575 Returns true if connected, otherwise false.
\r
577 return self.connected
\r
579 def add_global_handler(self, *args):
\r
580 """Add global handler.
\r
582 See documentation for IRC.add_global_handler.
\r
584 apply(self.irclibobj.add_global_handler, args)
\r
586 def action(self, target, action):
\r
587 """Send a CTCP ACTION command."""
\r
588 self.ctcp("ACTION", target, action)
\r
590 def admin(self, server=""):
\r
591 """Send an ADMIN command."""
\r
592 self.send_raw(string.strip(string.join(["ADMIN", server])))
\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
599 def ctcp_reply(self, target, parameter):
\r
600 """Send a CTCP REPLY command."""
\r
601 self.notice(target, "\001%s\001" % parameter)
\r
603 def disconnect(self, message=""):
\r
604 """Hang up the connection.
\r
608 message -- Quit message.
\r
610 if self.connected == 0:
\r
615 self.socket.close()
\r
616 except socket.error, x:
\r
619 self._handle_event(Event("disconnect", self.server, "", [message]))
\r
621 def globops(self, text):
\r
622 """Send a GLOBOPS command."""
\r
623 self.send_raw("GLOBOPS :" + text)
\r
625 def info(self, server=""):
\r
626 """Send an INFO command."""
\r
627 self.send_raw(string.strip(string.join(["INFO", server])))
\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
633 def ison(self, nicks):
\r
634 """Send an ISON command.
\r
638 nicks -- List of nicks.
\r
640 self.send_raw("ISON " + string.join(nicks, ","))
\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
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
650 def links(self, remote_server="", server_mask=""):
\r
651 """Send a LINKS command."""
\r
654 command = command + " " + remote_server
\r
656 command = command + " " + server_mask
\r
657 self.send_raw(command)
\r
659 def list(self, channels=None, server=""):
\r
660 """Send a LIST command."""
\r
663 command = command + " " + string.join(channels, ",")
\r
665 command = command + " " + server
\r
666 self.send_raw(command)
\r
668 def lusers(self, server=""):
\r
669 """Send a LUSERS command."""
\r
670 self.send_raw("LUSERS" + (server and (" " + server)))
\r
672 def mode(self, target, command):
\r
673 """Send a MODE command."""
\r
674 self.send_raw("MODE %s %s" % (target, command))
\r
676 def motd(self, server=""):
\r
677 """Send an MOTD command."""
\r
678 self.send_raw("MOTD" + (server and (" " + server)))
\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
684 def nick(self, newnick):
\r
685 """Send a NICK command."""
\r
686 self.send_raw("NICK " + newnick)
\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
693 def oper(self, nick, password):
\r
694 """Send an OPER command."""
\r
695 self.send_raw("OPER %s %s" % (nick, password))
\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
702 self.send_raw("PART " + string.join(channels, ","))
\r
704 def pass_(self, password):
\r
705 """Send a PASS command."""
\r
706 self.send_raw("PASS " + password)
\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
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
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
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
726 def quit(self, message=""):
\r
727 """Send a QUIT command."""
\r
728 self.send_raw("QUIT" + (message and (" :" + message)))
\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
736 def send_raw(self, string):
\r
737 """Send raw string to the server.
\r
739 The string will be padded with appropriate CR LF.
\r
742 self.socket.send(string + "\r\n")
\r
744 print "TO SERVER:", string
\r
745 except socket.error, x:
\r
747 self.disconnect("Connection reset by peer.")
\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
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
757 def time(self, server=""):
\r
758 """Send a TIME command."""
\r
759 self.send_raw("TIME" + (server and (" " + server)))
\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
766 self.send_raw("TOPIC %s :%s" % (channel, new_topic))
\r
768 def trace(self, target=""):
\r
769 """Send a TRACE command."""
\r
770 self.send_raw("TRACE" + (target and (" " + target)))
\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
776 def userhost(self, nicks):
\r
777 """Send a USERHOST command."""
\r
778 self.send_raw("USERHOST " + string.join(nicks, ","))
\r
780 def users(self, server=""):
\r
781 """Send a USERS command."""
\r
782 self.send_raw("USERS" + (server and (" " + server)))
\r
784 def version(self, server=""):
\r
785 """Send a VERSION command."""
\r
786 self.send_raw("VERSION" + (server and (" " + server)))
\r
788 def wallops(self, text):
\r
789 """Send a WALLOPS command."""
\r
790 self.send_raw("WALLOPS :" + text)
\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
796 def whois(self, targets):
\r
797 """Send a WHOIS command."""
\r
798 self.send_raw("WHOIS " + string.join(targets, ","))
\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
807 class DCCConnection(Connection):
\r
808 """Unimplemented."""
\r
809 def __init__(self):
\r
810 raise IRCError, "Unimplemented."
\r
813 class SimpleIRCClient:
\r
814 """A simple single-server IRC client class.
\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
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
826 Instance attributes that can be used by sub classes:
\r
828 ircobj -- The IRC instance.
\r
830 connection -- The ServerConnection instance.
\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
837 def _dispatcher(self, c, e):
\r
839 m = "on_" + e.eventtype()
\r
840 if hasattr(self, m):
\r
841 getattr(self, m)(c, e)
\r
843 def connect(self, server, port, nickname, password=None, username=None,
\r
845 """Connect/reconnect to a server.
\r
849 server -- Server name.
\r
851 port -- Port number.
\r
853 nickname -- The nickname.
\r
855 password -- Password (if any).
\r
857 username -- The username.
\r
859 ircname -- The IRC name.
\r
861 This function can be called to reconnect a closed connection.
\r
863 self.connection.connect(server, port, nickname,
\r
864 password, username, ircname)
\r
867 """Start the IRC client."""
\r
868 self.ircobj.process_forever()
\r
872 """Class representing an IRC event."""
\r
873 def __init__(self, eventtype, source, target, arguments=None):
\r
874 """Constructor of Event objects.
\r
878 eventtype -- A string describing the event.
\r
880 source -- The originator of the event (a nick mask or a server). XXX Correct?
\r
882 target -- The target of the event (a nick or a channel). XXX Correct?
\r
884 arguments -- Any event specific arguments.
\r
886 self._eventtype = eventtype
\r
887 self._source = source
\r
888 self._target = target
\r
890 self._arguments = arguments
\r
892 self._arguments = []
\r
894 def eventtype(self):
\r
895 """Get the event type."""
\r
896 return self._eventtype
\r
899 """Get the event source."""
\r
900 return self._source
\r
903 """Get the event target."""
\r
904 return self._target
\r
906 def arguments(self):
\r
907 """Get the event arguments."""
\r
908 return self._arguments
\r
910 _LOW_LEVEL_QUOTE = "\020"
\r
911 _CTCP_LEVEL_QUOTE = "\134"
\r
912 _CTCP_DELIMITER = "\001"
\r
914 _low_level_mapping = {
\r
918 _LOW_LEVEL_QUOTE: _LOW_LEVEL_QUOTE
\r
921 _low_level_regexp = re.compile(_LOW_LEVEL_QUOTE + "(.)")
\r
923 def mask_matches(nick, mask):
\r
924 """Check if a nick matches a mask.
\r
926 Returns true if the nick matches, otherwise false.
\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
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
945 """Returns a lowercased string.
\r
947 The definition of lowercased comes from the IRC specification (RFC
\r
950 return string.translate(s, _ircstring_translation)
\r
952 def _ctcp_dequote(message):
\r
953 """[Internal] Dequote a message according to CTCP specifications.
\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
963 message -- The message to be decoded.
\r
966 def _low_level_replace(match_obj):
\r
967 ch = match_obj.group(1)
\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
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
977 if _CTCP_DELIMITER not in message:
\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
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
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
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
1002 messages.append(_CTCP_DELIMITER + chunks[-1])
\r
1006 def is_channel(string):
\r
1007 """Check if a string is a channel name.
\r
1009 Returns true if the argument is a channel name, otherwise false.
\r
1011 return string and string[0] in "#&+!"
\r
1014 """Get the nick part of a nickmask.
\r
1016 (The source of an Event is a nickmask.)
\r
1018 return string.split(s, "!")[0]
\r
1021 """Get the userhost part of a nickmask.
\r
1023 (The source of an Event is a nickmask.)
\r
1025 return string.split(s, "!")[1]
\r
1028 """Get the host part of a nickmask.
\r
1030 (The source of an Event is a nickmask.)
\r
1032 return string.split(s, "@")[1]
\r
1035 """Get the user part of a nickmask.
\r
1037 (The source of an Event is a nickmask.)
\r
1039 s = string.split(s, "!")[1]
\r
1040 return string.split(s, "@")[0]
\r
1042 def parse_nick_modes(mode_string):
\r
1043 """Parse a nick mode string.
\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
1051 >>> irclib.parse_nick_modes(\"+ab-c\")
\r
1052 [['+', 'a', None], ['+', 'b', None], ['-', 'c', None]]
\r
1055 return _parse_modes(mode_string, "")
\r
1057 def parse_channel_modes(mode_string):
\r
1058 """Parse a channel mode string.
\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
1066 >>> irclib.parse_channel_modes(\"+ab-c foo\")
\r
1067 [['+', 'a', None], ['+', 'b', 'foo'], ['-', 'c', None]]
\r
1070 return _parse_modes(mode_string, "bklvo")
\r
1072 def _parse_modes(mode_string, unary_modes=""):
\r
1080 a = string.split(mode_string)
\r
1084 mode_part, args = a[0], a[1:]
\r
1086 if mode_part[0] not in "+-":
\r
1088 for ch in mode_part:
\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
1097 modes.append([sign, ch, None])
\r
1100 def _ping_ponger(connection, event):
\r
1102 connection.pong(event.target())
\r
1104 # Numeric table mostly stolen from the Perl IRC module (Net::IRC).
\r
1105 numeric_events = {
\r
1107 "002": "yourhost",
\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
1130 "231": "serviceinfo",
\r
1131 "232": "endofservices",
\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
1142 "253": "luserunknown",
\r
1143 "254": "luserchannels",
\r
1146 "257": "adminloc1",
\r
1147 "258": "adminloc2",
\r
1148 "259": "adminemail",
\r
1149 "261": "tracelog",
\r
1150 "262": "endoftrace",
\r
1152 "266": "n_global",
\r
1155 "302": "userhost",
\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
1171 "324": "channelmodeis",
\r
1172 "329": "channelcreate",
\r
1175 "333": "topicinfo",
\r
1176 "341": "inviting",
\r
1177 "342": "summoning",
\r
1179 "352": "whoreply",
\r
1180 "353": "namreply",
\r
1181 "361": "killdone",
\r
1183 "363": "closeend",
\r
1185 "365": "endoflinks",
\r
1186 "366": "endofnames",
\r
1188 "368": "endofbanlist",
\r
1189 "369": "endofwhowas",
\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
1201 "392": "usersstart",
\r
1203 "394": "endofusers",
\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
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
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
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
1254 generated_events = [
\r
1255 # Generated events
\r
1261 protocol_events = [
\r
1262 # IRC protocol events
\r
1276 all_events = generated_events + protocol_events + numeric_events.values()
\r