chiark / gitweb /
Upstream version 0.3
[irc.git] / irclib.py
diff --git a/irclib.py b/irclib.py
new file mode 100644 (file)
index 0000000..5645ae5
--- /dev/null
+++ b/irclib.py
@@ -0,0 +1,1276 @@
+# Copyright (C) 1999, 2000 Joel Rosdahl\r
+# \r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+#        \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.\r
+#\r
+# Joel Rosdahl <joel@rosdahl.net>\r
+#\r
+# $Id$\r
+\r
+"""irclib -- Internet Relay Chat (IRC) protocol client library.\r
+\r
+This library is intended to encapsulate the IRC protocol at a quite\r
+low level.  It provides an event-driven IRC client framework.  It has\r
+a fairly thorough support for the basic IRC protocol and CTCP, but DCC\r
+connection support is not yet implemented.\r
+\r
+In order to understand how to make an IRC client, I'm afraid you more\r
+or less must understand the IRC specifications.  They are available\r
+here: [IRC specifications].\r
+\r
+The main features of the IRC client framework are:\r
+\r
+  * Abstraction of the IRC protocol.\r
+  * Handles multiple simultaneous IRC server connections.\r
+  * Handles server PONGing transparently.\r
+  * Messages to the IRC server are done by calling methods on an IRC\r
+    connection object.\r
+  * Messages from an IRC server triggers events, which can be caught\r
+    by event handlers.\r
+  * Reading from and writing to IRC server sockets are normally done\r
+    by an internal select() loop, but the select()ing may be done by\r
+    an external main loop.\r
+  * Functions can be registered to execute at specified times by the\r
+    event-loop.\r
+  * Decodes CTCP tagging correctly (hopefully); I haven't seen any\r
+    other IRC client implementation that handles the CTCP\r
+    specification subtilties.\r
+  * A kind of simple, single-server, object-oriented IRC client class\r
+    that dispatches events to instance methods is included.\r
+\r
+Current limitations:\r
+\r
+  * The IRC protocol shines through the abstraction a bit too much.\r
+  * Data is not written asynchronously to the server, i.e. the write()\r
+    may block if the TCP buffers are stuffed.\r
+  * There are no support for DCC connections.\r
+  * The author haven't even read RFC 2810, 2811, 2812 and 2813.\r
+  * Like most projects, documentation is lacking...\r
+\r
+Since I seldom use IRC anymore, I will probably not work much on the\r
+library.  If you want to help or continue developing the library,\r
+please contact me (Joel Rosdahl <joel@rosdahl.net>).\r
+\r
+.. [IRC specifications] http://www.irchelp.org/irchelp/rfc/\r
+"""\r
+\r
+import bisect\r
+import re\r
+import select\r
+import socket\r
+import string\r
+import sys\r
+import time\r
+import types\r
+\r
+VERSION = 0, 3, 1\r
+DEBUG = 0\r
+\r
+# TODO\r
+# ----\r
+# DCC\r
+# (maybe) thread safety\r
+# (maybe) color parser convenience functions\r
+# documentation (including all event types)\r
+# (maybe) add awareness of different types of ircds\r
+# send data asynchronously to the server\r
+\r
+# NOTES\r
+# -----\r
+# connection.quit() only sends QUIT to the server.\r
+# ERROR from the server triggers the error event and the disconnect event.\r
+# dropping of the connection triggers the disconnect event.\r
+\r
+class IRCError(Exception):\r
+    """Represents an IRC exception."""\r
+    pass\r
+\r
+\r
+class IRC:\r
+    """Class that handles one or several IRC server connections.\r
+\r
+    When an IRC object has been instantiated, it can be used to create\r
+    Connection objects that represent the IRC connections.  The\r
+    responsibility of the IRC object is to provide an event-driven\r
+    framework for the connections and to keep the connections alive.\r
+    It runs a select loop to poll each connection's TCP socket and\r
+    hands over the sockets with incoming data for processing by the\r
+    corresponding connection.\r
+\r
+    The methods of most interest for an IRC client writer are server,\r
+    add_global_handler, remove_global_handler, execute_at,\r
+    execute_delayed, process_once and process_forever.\r
+\r
+    Here is an example:\r
+\r
+        irc = irclib.IRC()\r
+        server = irc.server()\r
+        server.connect(\"irc.some.where\", 6667, \"my_nickname\")\r
+        server.privmsg(\"a_nickname\", \"Hi there!\")\r
+        server.process_forever()\r
+\r
+    This will connect to the IRC server irc.some.where on port 6667\r
+    using the nickname my_nickname and send the message \"Hi there!\"\r
+    to the nickname a_nickname.\r
+    """\r
+\r
+    def __init__(self, fn_to_add_socket=None,\r
+                 fn_to_remove_socket=None,\r
+                 fn_to_add_timeout=None):\r
+        """Constructor for IRC objects.\r
+\r
+        Optional arguments are fn_to_add_socket, fn_to_remove_socket\r
+        and fn_to_add_timeout.  The first two specify functions that\r
+        will be called with a socket object as argument when the IRC\r
+        object wants to be notified (or stop being notified) of data\r
+        coming on a new socket.  When new data arrives, the method\r
+        process_data should be called.  Similarly, fn_to_add_timeout\r
+        is called with a number of seconds (a floating point number)\r
+        as first argument when the IRC object wants to receive a\r
+        notification (by calling the process_timeout method).  So, if\r
+        e.g. the argument is 42.17, the object wants the\r
+        process_timeout method to be called after 42 seconds and 170\r
+        milliseconds.\r
+\r
+        The three arguments mainly exist to be able to use an external\r
+        main loop (for example Tkinter's or PyGTK's main app loop)\r
+        instead of calling the process_forever method.\r
+\r
+        An alternative is to just call ServerConnection.process_once()\r
+        once in a while.\r
+        """\r
+\r
+        if fn_to_add_socket and fn_to_remove_socket:\r
+            self.fn_to_add_socket = fn_to_add_socket\r
+            self.fn_to_remove_socket = fn_to_remove_socket\r
+        else:\r
+            self.fn_to_add_socket = None\r
+            self.fn_to_remove_socket = None\r
+\r
+        self.fn_to_add_timeout = fn_to_add_timeout\r
+        self.connections = []\r
+        self.handlers = {}\r
+        self.delayed_commands = [] # list of tuples in the format (time, function, arguments)\r
+\r
+        self.add_global_handler("ping", _ping_ponger, -42)\r
+\r
+    def server(self):\r
+        """Creates and returns a ServerConnection object."""\r
+\r
+        c = ServerConnection(self)\r
+        self.connections.append(c)\r
+        return c\r
+\r
+    def process_data(self, sockets):\r
+        """Called when there is more data to read on connection sockets.\r
+\r
+        Arguments:\r
+\r
+            sockets -- A list of socket objects.\r
+\r
+        See documentation for IRC.__init__.\r
+        """\r
+        for s in sockets:\r
+            for c in self.connections:\r
+                if s == c._get_socket():\r
+                    c.process_data()\r
+\r
+    def process_timeout(self):\r
+        """Called when a timeout notification is due.\r
+\r
+        See documentation for IRC.__init__.\r
+        """\r
+        t = time.time()\r
+        while self.delayed_commands:\r
+            if t >= self.delayed_commands[0][0]:\r
+                apply(self.delayed_commands[0][1], self.delayed_commands[0][2])\r
+                del self.delayed_commands[0]\r
+            else:\r
+                break\r
+\r
+    def process_once(self, timeout=0):\r
+        """Process data from connections once.\r
+\r
+        Arguments:\r
+\r
+            timeout -- How long the select() call should wait if no\r
+                       data is available.\r
+\r
+        This method should be called periodically to check and process\r
+        incoming data, if there are any.  If that seems boring, look\r
+        at the process_forever method.\r
+        """\r
+        sockets = map(lambda x: x._get_socket(), self.connections)\r
+        sockets = filter(lambda x: x != None, sockets)\r
+        if sockets:\r
+            (i, o, e) = select.select(sockets, [], [], timeout)\r
+            self.process_data(i)\r
+        else:\r
+            time.sleep(timeout)\r
+        self.process_timeout()\r
+\r
+    def process_forever(self, timeout=0.2):\r
+        """Run an infinite loop, processing data from connections.\r
+\r
+        This method repeatedly calls process_once.\r
+\r
+        Arguments:\r
+\r
+            timeout -- Parameter to pass to process_once.\r
+        """\r
+        while 1:\r
+            self.process_once(timeout)\r
+\r
+    def disconnect_all(self, message=""):\r
+        """Disconnects all connections."""\r
+        for c in self.connections:\r
+            c.quit(message)\r
+            c.disconnect(message)\r
+\r
+    def add_global_handler(self, event, handler, priority=0):\r
+        """Adds a global handler function for a specific event type.\r
+\r
+        Arguments:\r
+\r
+            event -- Event type (a string).  Check the values of the\r
+            numeric_events dictionary in irclib.py for possible event\r
+            types.\r
+\r
+            handler -- Callback function.\r
+\r
+            priority -- A number (the lower number, the higher priority).\r
+\r
+        The handler function is called whenever the specified event is\r
+        triggered in any of the connections.  See documentation for\r
+        the Event class.\r
+\r
+        The handler functions are called in priority order (lowest\r
+        number is highest priority).  If a handler function returns\r
+        \"NO MORE\", no more handlers will be called.\r
+        """\r
+\r
+        if not self.handlers.has_key(event):\r
+            self.handlers[event] = []\r
+        bisect.insort(self.handlers[event], ((priority, handler)))\r
+\r
+    def remove_global_handler(self, event, handler):\r
+        """Removes a global handler function.\r
+\r
+        Arguments:\r
+\r
+            event -- Event type (a string).\r
+\r
+            handler -- Callback function.\r
+\r
+        Returns 1 on success, otherwise 0.\r
+        """\r
+        if not self.handlers.has_key(event):\r
+            return 0\r
+        for h in self.handlers[event]:\r
+            if handler == h[1]:\r
+                self.handlers[event].remove(h)\r
+        return 1\r
+\r
+    def execute_at(self, at, function, arguments=()):\r
+        """Execute a function at a specified time.\r
+\r
+        Arguments:\r
+\r
+            at -- Execute at this time (standard \"time_t\" time).\r
+\r
+            function -- Function to call.\r
+\r
+            arguments -- Arguments to give the function.\r
+        """\r
+        self.execute_delayed(at-time.time(), function, arguments)\r
+\r
+    def execute_delayed(self, delay, function, arguments=()):\r
+        """Execute a function after a specified time.\r
+\r
+        Arguments:\r
+\r
+            delay -- How many seconds to wait.\r
+\r
+            function -- Function to call.\r
+\r
+            arguments -- Arguments to give the function.\r
+        """\r
+        bisect.insort(self.delayed_commands, (delay+time.time(), function, arguments))\r
+        if self.fn_to_add_timeout:\r
+            self.fn_to_add_timeout(delay)\r
+\r
+    def _handle_event(self, connection, event):\r
+        """[Internal]"""\r
+        h = self.handlers\r
+        for handler in h.get("all_events", []) + h.get(event.eventtype(), []):\r
+            if handler[1](connection, event) == "NO MORE":\r
+                return\r
+\r
+    def _remove_connection(self, connection):\r
+        """[Internal]"""\r
+        self.connections.remove(connection)\r
+        if self.fn_to_remove_socket:\r
+            self.fn_to_remove_socket(connection._get_socket())\r
+\r
+_rfc_1459_command_regexp = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?")\r
+\r
+\r
+class Connection:\r
+    """Base class for IRC connections.\r
+\r
+    Must be overridden.\r
+    """\r
+    def __init__(self, irclibobj):\r
+        self.irclibobj = irclibobj\r
+\r
+    def _get_socket():\r
+        raise IRCError, "Not overridden"\r
+\r
+    ##############################\r
+    ### Convenience wrappers.\r
+\r
+    def execute_at(self, at, function, arguments=()):\r
+        self.irclibobj.execute_at(at, function, arguments)\r
+\r
+    def execute_delayed(self, delay, function, arguments=()):\r
+        self.irclibobj.execute_delayed(delay, function, arguments)\r
+\r
+\r
+class ServerConnectionError(IRCError):\r
+    pass\r
+\r
+\r
+# Huh!?  Crrrrazy EFNet doesn't follow the RFC: their ircd seems to\r
+# use \n as message separator!  :P\r
+_linesep_regexp = re.compile("\r?\n")\r
+\r
+class ServerConnection(Connection):\r
+    """This class represents an IRC server connection.\r
+\r
+    ServerConnection objects are instantiated by calling the server\r
+    method on an IRC object.\r
+    """\r
+\r
+    def __init__(self, irclibobj):\r
+        Connection.__init__(self, irclibobj)\r
+        self.connected = 0  # Not connected yet.\r
+\r
+    def connect(self, server, port, nickname, password=None, username=None,\r
+                ircname=None):\r
+        """Connect/reconnect to a server.\r
+\r
+        Arguments:\r
+\r
+            server -- Server name.\r
+\r
+            port -- Port number.\r
+\r
+            nickname -- The nickname.\r
+\r
+            password -- Password (if any).\r
+\r
+            username -- The username.\r
+\r
+            ircname -- The IRC name.\r
+\r
+        This function can be called to reconnect a closed connection.\r
+\r
+        Returns the ServerConnection object.\r
+        """\r
+        if self.connected:\r
+            self.quit("Changing server")\r
+\r
+        self.socket = None\r
+        self.previous_buffer = ""\r
+        self.handlers = {}\r
+        self.real_server_name = ""\r
+        self.real_nickname = nickname\r
+        self.server = server\r
+        self.port = port\r
+        self.nickname = nickname\r
+        self.username = username or nickname\r
+        self.ircname = ircname or nickname\r
+        self.password = password\r
+        self.localhost = socket.gethostname()\r
+        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r
+        try:\r
+            self.socket.connect((self.server, self.port))\r
+        except socket.error, x:\r
+            raise ServerConnectionError, "Couldn't connect to socket: %s" % x\r
+        self.connected = 1\r
+        if self.irclibobj.fn_to_add_socket:\r
+            self.irclibobj.fn_to_add_socket(self.socket)\r
+\r
+        # Log on...\r
+        if self.password:\r
+            self.pass_(self.password)\r
+        self.nick(self.nickname)\r
+        self.user(self.username, self.localhost, self.server, self.ircname)\r
+        return self\r
+\r
+    def close(self):\r
+        """Close the connection.\r
+\r
+        This method closes the connection permanently; after it has\r
+        been called, the object is unusable.\r
+        """\r
+\r
+        self.disconnect("Closing object")\r
+        self.irclibobj._remove_connection(self)\r
+\r
+    def _get_socket(self):\r
+        """[Internal]"""\r
+        if self.connected:\r
+            return self.socket\r
+        else:\r
+            return None\r
+\r
+    def get_server_name(self):\r
+        """Get the (real) server name.\r
+\r
+        This method returns the (real) server name, or, more\r
+        specifically, what the server calls itself.\r
+        """\r
+\r
+        if self.real_server_name:\r
+            return self.real_server_name\r
+        else:\r
+            return ""\r
+\r
+    def get_nickname(self):\r
+        """Get the (real) nick name.\r
+\r
+        This method returns the (real) nickname.  The library keeps\r
+        track of nick changes, so it might not be the nick name that\r
+        was passed to the connect() method.  """\r
+\r
+        return self.real_nickname\r
+\r
+    def process_data(self):\r
+        """[Internal]"""\r
+\r
+        try:\r
+            new_data = self.socket.recv(2**14)\r
+        except socket.error, x:\r
+            # The server hung up.\r
+            self.disconnect("Connection reset by peer")\r
+            return\r
+        if not new_data:\r
+            # Read nothing: connection must be down.\r
+            self.disconnect("Connection reset by peer")\r
+            return\r
+\r
+        lines = _linesep_regexp.split(self.previous_buffer + new_data)\r
+\r
+        # Save the last, unfinished line.\r
+        self.previous_buffer = lines[-1]\r
+        lines = lines[:-1]\r
+\r
+        for line in lines:\r
+            if DEBUG:\r
+                print "FROM SERVER:", line\r
+\r
+            prefix = None\r
+            command = None\r
+            arguments = None\r
+            self._handle_event(Event("all_raw_messages",\r
+                                     self.get_server_name(),\r
+                                     None,\r
+                                     [line]))\r
+\r
+            m = _rfc_1459_command_regexp.match(line)\r
+            if m.group("prefix"):\r
+                prefix = m.group("prefix")\r
+                if not self.real_server_name:\r
+                    self.real_server_name = prefix\r
+\r
+            if m.group("command"):\r
+                command = string.lower(m.group("command"))\r
+\r
+            if m.group("argument"):\r
+                a = string.split(m.group("argument"), " :", 1)\r
+                arguments = string.split(a[0])\r
+                if len(a) == 2:\r
+                    arguments.append(a[1])\r
+\r
+            if command == "nick":\r
+                if nm_to_n(prefix) == self.real_nickname:\r
+                    self.real_nickname = arguments[0]\r
+\r
+            if command in ["privmsg", "notice"]:\r
+                target, message = arguments[0], arguments[1]\r
+                messages = _ctcp_dequote(message)\r
+\r
+                if command == "privmsg":\r
+                    if is_channel(target):\r
+                        command = "pubmsg"\r
+                else:\r
+                    if is_channel(target):\r
+                        command = "pubnotice"\r
+                    else:\r
+                        command = "privnotice"\r
+\r
+                for m in messages:\r
+                    if type(m) is types.TupleType:\r
+                        if command in ["privmsg", "pubmsg"]:\r
+                            command = "ctcp"\r
+                        else:\r
+                            command = "ctcpreply"\r
+\r
+                        m = list(m)\r
+                        if DEBUG:\r
+                            print "command: %s, source: %s, target: %s, arguments: %s" % (\r
+                                command, prefix, target, m)\r
+                        self._handle_event(Event(command, prefix, target, m))\r
+                    else:\r
+                        if DEBUG:\r
+                            print "command: %s, source: %s, target: %s, arguments: %s" % (\r
+                                command, prefix, target, [m])\r
+                        self._handle_event(Event(command, prefix, target, [m]))\r
+            else:\r
+                target = None\r
+\r
+                if command == "quit":\r
+                    arguments = [arguments[0]]\r
+                elif command == "ping":\r
+                    target = arguments[0]\r
+                else:\r
+                    target = arguments[0]\r
+                    arguments = arguments[1:]\r
+\r
+                if command == "mode":\r
+                    if not is_channel(target):\r
+                        command = "umode"\r
+\r
+                # Translate numerics into more readable strings.\r
+                if numeric_events.has_key(command):\r
+                    command = numeric_events[command]\r
+\r
+                if DEBUG:\r
+                    print "command: %s, source: %s, target: %s, arguments: %s" % (\r
+                        command, prefix, target, arguments)\r
+                self._handle_event(Event(command, prefix, target, arguments))\r
+\r
+    def _handle_event(self, event):\r
+        """[Internal]"""\r
+        self.irclibobj._handle_event(self, event)\r
+        if self.handlers.has_key(event.eventtype()):\r
+            for fn in self.handlers[event.eventtype()]:\r
+                fn(self, event)\r
+\r
+    def is_connected(self):\r
+        """Return connection status.\r
+\r
+        Returns true if connected, otherwise false.\r
+        """\r
+        return self.connected\r
+\r
+    def add_global_handler(self, *args):\r
+        """Add global handler.\r
+\r
+        See documentation for IRC.add_global_handler.\r
+        """\r
+        apply(self.irclibobj.add_global_handler, args)\r
+\r
+    def action(self, target, action):\r
+        """Send a CTCP ACTION command."""\r
+        self.ctcp("ACTION", target, action)\r
+\r
+    def admin(self, server=""):\r
+        """Send an ADMIN command."""\r
+        self.send_raw(string.strip(string.join(["ADMIN", server])))\r
+\r
+    def ctcp(self, ctcptype, target, parameter=""):\r
+        """Send a CTCP command."""\r
+        ctcptype = string.upper(ctcptype)\r
+        self.privmsg(target, "\001%s%s\001" % (ctcptype, parameter and (" " + parameter) or ""))\r
+\r
+    def ctcp_reply(self, target, parameter):\r
+        """Send a CTCP REPLY command."""\r
+        self.notice(target, "\001%s\001" % parameter)\r
+\r
+    def disconnect(self, message=""):\r
+        """Hang up the connection.\r
+\r
+        Arguments:\r
+\r
+            message -- Quit message.\r
+        """\r
+        if self.connected == 0:\r
+            return\r
+\r
+        self.connected = 0\r
+        try:\r
+            self.socket.close()\r
+        except socket.error, x:\r
+            pass\r
+        self.socket = None\r
+        self._handle_event(Event("disconnect", self.server, "", [message]))\r
+\r
+    def globops(self, text):\r
+        """Send a GLOBOPS command."""\r
+        self.send_raw("GLOBOPS :" + text)\r
+\r
+    def info(self, server=""):\r
+        """Send an INFO command."""\r
+        self.send_raw(string.strip(string.join(["INFO", server])))\r
+\r
+    def invite(self, nick, channel):\r
+        """Send an INVITE command."""\r
+        self.send_raw(string.strip(string.join(["INVITE", nick, channel])))\r
+\r
+    def ison(self, nicks):\r
+        """Send an ISON command.\r
+\r
+        Arguments:\r
+\r
+            nicks -- List of nicks.\r
+        """\r
+        self.send_raw("ISON " + string.join(nicks, ","))\r
+\r
+    def join(self, channel, key=""):\r
+        """Send a JOIN command."""\r
+        self.send_raw("JOIN %s%s" % (channel, (key and (" " + key))))\r
+\r
+    def kick(self, channel, nick, comment=""):\r
+        """Send a KICK command."""\r
+        self.send_raw("KICK %s %s%s" % (channel, nick, (comment and (" :" + comment))))\r
+\r
+    def links(self, remote_server="", server_mask=""):\r
+        """Send a LINKS command."""\r
+        command = "LINKS"\r
+        if remote_server:\r
+            command = command + " " + remote_server\r
+        if server_mask:\r
+            command = command + " " + server_mask\r
+        self.send_raw(command)\r
+\r
+    def list(self, channels=None, server=""):\r
+        """Send a LIST command."""\r
+        command = "LIST"\r
+        if channels:\r
+            command = command + " " + string.join(channels, ",")\r
+        if server:\r
+            command = command + " " + server\r
+        self.send_raw(command)\r
+\r
+    def lusers(self, server=""):\r
+        """Send a LUSERS command."""\r
+        self.send_raw("LUSERS" + (server and (" " + server)))\r
+\r
+    def mode(self, target, command):\r
+        """Send a MODE command."""\r
+        self.send_raw("MODE %s %s" % (target, command))\r
+\r
+    def motd(self, server=""):\r
+        """Send an MOTD command."""\r
+        self.send_raw("MOTD" + (server and (" " + server)))\r
+\r
+    def names(self, channels=None):\r
+        """Send a NAMES command."""\r
+        self.send_raw("NAMES" + (channels and (" " + string.join(channels, ",")) or ""))\r
+\r
+    def nick(self, newnick):\r
+        """Send a NICK command."""\r
+        self.send_raw("NICK " + newnick)\r
+\r
+    def notice(self, target, text):\r
+        """Send a NOTICE command."""\r
+        # Should limit len(text) here!\r
+        self.send_raw("NOTICE %s :%s" % (target, text))\r
+\r
+    def oper(self, nick, password):\r
+        """Send an OPER command."""\r
+        self.send_raw("OPER %s %s" % (nick, password))\r
+\r
+    def part(self, channels):\r
+        """Send a PART command."""\r
+        if type(channels) == types.StringType:\r
+            self.send_raw("PART " + channels)\r
+        else:\r
+            self.send_raw("PART " + string.join(channels, ","))\r
+\r
+    def pass_(self, password):\r
+        """Send a PASS command."""\r
+        self.send_raw("PASS " + password)\r
+\r
+    def ping(self, target, target2=""):\r
+        """Send a PING command."""\r
+        self.send_raw("PING %s%s" % (target, target2 and (" " + target2)))\r
+\r
+    def pong(self, target, target2=""):\r
+        """Send a PONG command."""\r
+        self.send_raw("PONG %s%s" % (target, target2 and (" " + target2)))\r
+\r
+    def privmsg(self, target, text):\r
+        """Send a PRIVMSG command."""\r
+        # Should limit len(text) here!\r
+        self.send_raw("PRIVMSG %s :%s" % (target, text))\r
+\r
+    def privmsg_many(self, targets, text):\r
+        """Send a PRIVMSG command to multiple targets."""\r
+        # Should limit len(text) here!\r
+        self.send_raw("PRIVMSG %s :%s" % (string.join(targets, ","), text))\r
+\r
+    def quit(self, message=""):\r
+        """Send a QUIT command."""\r
+        self.send_raw("QUIT" + (message and (" :" + message)))\r
+\r
+    def sconnect(self, target, port="", server=""):\r
+        """Send an SCONNECT command."""\r
+        self.send_raw("CONNECT %s%s%s" % (target,\r
+                                          port and (" " + port),\r
+                                          server and (" " + server)))\r
+\r
+    def send_raw(self, string):\r
+        """Send raw string to the server.\r
+\r
+        The string will be padded with appropriate CR LF.\r
+        """\r
+        try:\r
+            self.socket.send(string + "\r\n")\r
+            if DEBUG:\r
+                print "TO SERVER:", string\r
+        except socket.error, x:\r
+            # Aouch!\r
+            self.disconnect("Connection reset by peer.")\r
+\r
+    def squit(self, server, comment=""):\r
+        """Send an SQUIT command."""\r
+        self.send_raw("SQUIT %s%s" % (server, comment and (" :" + comment)))\r
+\r
+    def stats(self, statstype, server=""):\r
+        """Send a STATS command."""\r
+        self.send_raw("STATS %s%s" % (statstype, server and (" " + server)))\r
+\r
+    def time(self, server=""):\r
+        """Send a TIME command."""\r
+        self.send_raw("TIME" + (server and (" " + server)))\r
+\r
+    def topic(self, channel, new_topic=None):\r
+        """Send a TOPIC command."""\r
+        if new_topic == None:\r
+            self.send_raw("TOPIC " + channel)\r
+        else:\r
+            self.send_raw("TOPIC %s :%s" % (channel, new_topic))\r
+\r
+    def trace(self, target=""):\r
+        """Send a TRACE command."""\r
+        self.send_raw("TRACE" + (target and (" " + target)))\r
+\r
+    def user(self, username, localhost, server, ircname):\r
+        """Send a USER command."""\r
+        self.send_raw("USER %s %s %s :%s" % (username, localhost, server, ircname))\r
+\r
+    def userhost(self, nicks):\r
+        """Send a USERHOST command."""\r
+        self.send_raw("USERHOST " + string.join(nicks, ","))\r
+\r
+    def users(self, server=""):\r
+        """Send a USERS command."""\r
+        self.send_raw("USERS" + (server and (" " + server)))\r
+\r
+    def version(self, server=""):\r
+        """Send a VERSION command."""\r
+        self.send_raw("VERSION" + (server and (" " + server)))\r
+\r
+    def wallops(self, text):\r
+        """Send a WALLOPS command."""\r
+        self.send_raw("WALLOPS :" + text)\r
+\r
+    def who(self, target="", op=""):\r
+        """Send a WHO command."""\r
+        self.send_raw("WHO%s%s" % (target and (" " + target), op and (" o")))\r
+\r
+    def whois(self, targets):\r
+        """Send a WHOIS command."""\r
+        self.send_raw("WHOIS " + string.join(targets, ","))\r
+\r
+    def whowas(self, nick, max=None, server=""):\r
+        """Send a WHOWAS command."""\r
+        self.send_raw("WHOWAS %s%s%s" % (nick,\r
+                                         max and (" " + max),\r
+                                         server and (" " + server)))\r
+\r
+\r
+class DCCConnection(Connection):\r
+    """Unimplemented."""\r
+    def __init__(self):\r
+        raise IRCError, "Unimplemented."\r
+\r
+\r
+class SimpleIRCClient:\r
+    """A simple single-server IRC client class.\r
+\r
+    This is an example of an object-oriented wrapper of the IRC\r
+    framework.  A real IRC client can be made by subclassing this\r
+    class and adding appropriate methods.\r
+\r
+    The method on_join will be called when a "join" event is created\r
+    (which is done when the server sends a JOIN messsage/command),\r
+    on_privmsg will be called for "privmsg" events, and so on.  The\r
+    handler methods get two arguments: the connection object (same as\r
+    self.connection) and the event object.\r
+\r
+    Instance attributes that can be used by sub classes:\r
+\r
+        ircobj -- The IRC instance.\r
+\r
+        connection -- The ServerConnection instance.\r
+    """\r
+    def __init__(self):\r
+        self.ircobj = IRC()\r
+        self.connection = self.ircobj.server()\r
+        self.ircobj.add_global_handler("all_events", self._dispatcher, -10)\r
+\r
+    def _dispatcher(self, c, e):\r
+        """[Internal]"""\r
+        m = "on_" + e.eventtype()\r
+        if hasattr(self, m):\r
+            getattr(self, m)(c, e)\r
+\r
+    def connect(self, server, port, nickname, password=None, username=None,\r
+                ircname=None):\r
+        """Connect/reconnect to a server.\r
+\r
+        Arguments:\r
+\r
+            server -- Server name.\r
+\r
+            port -- Port number.\r
+\r
+            nickname -- The nickname.\r
+\r
+            password -- Password (if any).\r
+\r
+            username -- The username.\r
+\r
+            ircname -- The IRC name.\r
+\r
+        This function can be called to reconnect a closed connection.\r
+        """\r
+        self.connection.connect(server, port, nickname,\r
+                                password, username, ircname)\r
+\r
+    def start(self):\r
+        """Start the IRC client."""\r
+        self.ircobj.process_forever()\r
+\r
+\r
+class Event:\r
+    """Class representing an IRC event."""\r
+    def __init__(self, eventtype, source, target, arguments=None):\r
+        """Constructor of Event objects.\r
+\r
+        Arguments:\r
+\r
+            eventtype -- A string describing the event.\r
+\r
+            source -- The originator of the event (a nick mask or a server). XXX Correct?\r
+\r
+            target -- The target of the event (a nick or a channel). XXX Correct?\r
+\r
+            arguments -- Any event specific arguments.\r
+        """\r
+        self._eventtype = eventtype\r
+        self._source = source\r
+        self._target = target\r
+        if arguments:\r
+            self._arguments = arguments\r
+        else:\r
+            self._arguments = []\r
+\r
+    def eventtype(self):\r
+        """Get the event type."""\r
+        return self._eventtype\r
+\r
+    def source(self):\r
+        """Get the event source."""\r
+        return self._source\r
+\r
+    def target(self):\r
+        """Get the event target."""\r
+        return self._target\r
+\r
+    def arguments(self):\r
+        """Get the event arguments."""\r
+        return self._arguments\r
+\r
+_LOW_LEVEL_QUOTE = "\020"\r
+_CTCP_LEVEL_QUOTE = "\134"\r
+_CTCP_DELIMITER = "\001"\r
+\r
+_low_level_mapping = {\r
+    "0": "\000",\r
+    "n": "\n",\r
+    "r": "\r",\r
+    _LOW_LEVEL_QUOTE: _LOW_LEVEL_QUOTE\r
+}\r
+\r
+_low_level_regexp = re.compile(_LOW_LEVEL_QUOTE + "(.)")\r
+\r
+def mask_matches(nick, mask):\r
+    """Check if a nick matches a mask.\r
+\r
+    Returns true if the nick matches, otherwise false.\r
+    """\r
+    nick = irc_lower(nick)\r
+    mask = irc_lower(mask)\r
+    mask = string.replace(mask, "\\", "\\\\")\r
+    for ch in ".$|[](){}+":\r
+        mask = string.replace(mask, ch, "\\" + ch)\r
+    mask = string.replace(mask, "?", ".")\r
+    mask = string.replace(mask, "*", ".*")\r
+    r = re.compile(mask, re.IGNORECASE)\r
+    return r.match(nick)\r
+\r
+_alpha = "abcdefghijklmnopqrstuvxyz"\r
+_special = "-[]\\`^{}"\r
+nick_characters = _alpha + string.upper(_alpha) + string.digits + _special\r
+_ircstring_translation = string.maketrans(string.upper(_alpha) + "[]\\^",\r
+                                          _alpha + "{}|~")\r
+\r
+def irc_lower(s):\r
+    """Returns a lowercased string.\r
+\r
+    The definition of lowercased comes from the IRC specification (RFC\r
+    1459).\r
+    """\r
+    return string.translate(s, _ircstring_translation)\r
+\r
+def _ctcp_dequote(message):\r
+    """[Internal] Dequote a message according to CTCP specifications.\r
+\r
+    The function returns a list where each element can be either a\r
+    string (normal message) or a tuple of one or two strings (tagged\r
+    messages).  If a tuple has only one element (ie is a singleton),\r
+    that element is the tag; otherwise the tuple has two elements: the\r
+    tag and the data.\r
+\r
+    Arguments:\r
+\r
+        message -- The message to be decoded.\r
+    """\r
+\r
+    def _low_level_replace(match_obj):\r
+        ch = match_obj.group(1)\r
+\r
+        # If low_level_mapping doesn't have the character as key, we\r
+        # should just return the character.\r
+        return _low_level_mapping.get(ch, ch)\r
+\r
+    if _LOW_LEVEL_QUOTE in message:\r
+        # Yup, there was a quote.  Release the dequoter, man!\r
+        message = _low_level_regexp.sub(_low_level_replace, message)\r
+\r
+    if _CTCP_DELIMITER not in message:\r
+        return [message]\r
+    else:\r
+        # Split it into parts.  (Does any IRC client actually *use*\r
+        # CTCP stacking like this?)\r
+        chunks = string.split(message, _CTCP_DELIMITER)\r
+\r
+        messages = []\r
+        i = 0\r
+        while i < len(chunks)-1:\r
+            # Add message if it's non-empty.\r
+            if len(chunks[i]) > 0:\r
+                messages.append(chunks[i])\r
+\r
+            if i < len(chunks)-2:\r
+                # Aye!  CTCP tagged data ahead!\r
+                messages.append(tuple(string.split(chunks[i+1], " ", 1)))\r
+\r
+            i = i + 2\r
+\r
+        if len(chunks) % 2 == 0:\r
+            # Hey, a lonely _CTCP_DELIMITER at the end!  This means\r
+            # that the last chunk, including the delimiter, is a\r
+            # normal message!  (This is according to the CTCP\r
+            # specification.)\r
+            messages.append(_CTCP_DELIMITER + chunks[-1])\r
+\r
+        return messages\r
+\r
+def is_channel(string):\r
+    """Check if a string is a channel name.\r
+\r
+    Returns true if the argument is a channel name, otherwise false.\r
+    """\r
+    return string and string[0] in "#&+!"\r
+\r
+def nm_to_n(s):\r
+    """Get the nick part of a nickmask.\r
+\r
+    (The source of an Event is a nickmask.)\r
+    """\r
+    return string.split(s, "!")[0]\r
+\r
+def nm_to_uh(s):\r
+    """Get the userhost part of a nickmask.\r
+\r
+    (The source of an Event is a nickmask.)\r
+    """\r
+    return string.split(s, "!")[1]\r
+\r
+def nm_to_h(s):\r
+    """Get the host part of a nickmask.\r
+\r
+    (The source of an Event is a nickmask.)\r
+    """\r
+    return string.split(s, "@")[1]\r
+\r
+def nm_to_u(s):\r
+    """Get the user part of a nickmask.\r
+\r
+    (The source of an Event is a nickmask.)\r
+    """\r
+    s = string.split(s, "!")[1]\r
+    return string.split(s, "@")[0]\r
+\r
+def parse_nick_modes(mode_string):\r
+    """Parse a nick mode string.\r
+\r
+    The function returns a list of lists with three members: sign,\r
+    mode and argument.  The sign is \"+\" or \"-\".  The argument is\r
+    always None.\r
+\r
+    Example:\r
+\r
+    >>> irclib.parse_nick_modes(\"+ab-c\")\r
+    [['+', 'a', None], ['+', 'b', None], ['-', 'c', None]]\r
+    """\r
+\r
+    return _parse_modes(mode_string, "")\r
+\r
+def parse_channel_modes(mode_string):\r
+    """Parse a channel mode string.\r
+\r
+    The function returns a list of lists with three members: sign,\r
+    mode and argument.  The sign is \"+\" or \"-\".  The argument is\r
+    None if mode isn't one of \"b\", \"k\", \"l\", \"v\" or \"o\".\r
+\r
+    Example:\r
+\r
+    >>> irclib.parse_channel_modes(\"+ab-c foo\")\r
+    [['+', 'a', None], ['+', 'b', 'foo'], ['-', 'c', None]]\r
+    """\r
+\r
+    return _parse_modes(mode_string, "bklvo")\r
+\r
+def _parse_modes(mode_string, unary_modes=""):\r
+    """[Internal]"""\r
+    modes = []\r
+    arg_count = 0\r
+\r
+    # State variable.\r
+    sign = ""\r
+\r
+    a = string.split(mode_string)\r
+    if len(a) == 0:\r
+        return []\r
+    else:\r
+        mode_part, args = a[0], a[1:]\r
+\r
+    if mode_part[0] not in "+-":\r
+        return []\r
+    for ch in mode_part:\r
+        if ch in "+-":\r
+            sign = ch\r
+        elif ch == " ":\r
+            collecting_arguments = 1\r
+        elif ch in unary_modes:\r
+            modes.append([sign, ch, args[arg_count]])\r
+            arg_count = arg_count + 1\r
+        else:\r
+            modes.append([sign, ch, None])\r
+    return modes\r
+\r
+def _ping_ponger(connection, event):\r
+    """[Internal]"""\r
+    connection.pong(event.target())\r
+\r
+# Numeric table mostly stolen from the Perl IRC module (Net::IRC).\r
+numeric_events = {\r
+    "001": "welcome",\r
+    "002": "yourhost",\r
+    "003": "created",\r
+    "004": "myinfo",\r
+    "005": "featurelist",  # XXX\r
+    "200": "tracelink",\r
+    "201": "traceconnecting",\r
+    "202": "tracehandshake",\r
+    "203": "traceunknown",\r
+    "204": "traceoperator",\r
+    "205": "traceuser",\r
+    "206": "traceserver",\r
+    "208": "tracenewtype",\r
+    "209": "traceclass",\r
+    "211": "statslinkinfo",\r
+    "212": "statscommands",\r
+    "213": "statscline",\r
+    "214": "statsnline",\r
+    "215": "statsiline",\r
+    "216": "statskline",\r
+    "217": "statsqline",\r
+    "218": "statsyline",\r
+    "219": "endofstats",\r
+    "221": "umodeis",\r
+    "231": "serviceinfo",\r
+    "232": "endofservices",\r
+    "233": "service",\r
+    "234": "servlist",\r
+    "235": "servlistend",\r
+    "241": "statslline",\r
+    "242": "statsuptime",\r
+    "243": "statsoline",\r
+    "244": "statshline",\r
+    "250": "luserconns",\r
+    "251": "luserclient",\r
+    "252": "luserop",\r
+    "253": "luserunknown",\r
+    "254": "luserchannels",\r
+    "255": "luserme",\r
+    "256": "adminme",\r
+    "257": "adminloc1",\r
+    "258": "adminloc2",\r
+    "259": "adminemail",\r
+    "261": "tracelog",\r
+    "262": "endoftrace",\r
+    "265": "n_local",\r
+    "266": "n_global",\r
+    "300": "none",\r
+    "301": "away",\r
+    "302": "userhost",\r
+    "303": "ison",\r
+    "305": "unaway",\r
+    "306": "nowaway",\r
+    "311": "whoisuser",\r
+    "312": "whoisserver",\r
+    "313": "whoisoperator",\r
+    "314": "whowasuser",\r
+    "315": "endofwho",\r
+    "316": "whoischanop",\r
+    "317": "whoisidle",\r
+    "318": "endofwhois",\r
+    "319": "whoischannels",\r
+    "321": "liststart",\r
+    "322": "list",\r
+    "323": "listend",\r
+    "324": "channelmodeis",\r
+    "329": "channelcreate",\r
+    "331": "notopic",\r
+    "332": "topic",\r
+    "333": "topicinfo",\r
+    "341": "inviting",\r
+    "342": "summoning",\r
+    "351": "version",\r
+    "352": "whoreply",\r
+    "353": "namreply",\r
+    "361": "killdone",\r
+    "362": "closing",\r
+    "363": "closeend",\r
+    "364": "links",\r
+    "365": "endoflinks",\r
+    "366": "endofnames",\r
+    "367": "banlist",\r
+    "368": "endofbanlist",\r
+    "369": "endofwhowas",\r
+    "371": "info",\r
+    "372": "motd",\r
+    "373": "infostart",\r
+    "374": "endofinfo",\r
+    "375": "motdstart",\r
+    "376": "endofmotd",\r
+    "377": "motd2",        # 1997-10-16 -- tkil\r
+    "381": "youreoper",\r
+    "382": "rehashing",\r
+    "384": "myportis",\r
+    "391": "time",\r
+    "392": "usersstart",\r
+    "393": "users",\r
+    "394": "endofusers",\r
+    "395": "nousers",\r
+    "401": "nosuchnick",\r
+    "402": "nosuchserver",\r
+    "403": "nosuchchannel",\r
+    "404": "cannotsendtochan",\r
+    "405": "toomanychannels",\r
+    "406": "wasnosuchnick",\r
+    "407": "toomanytargets",\r
+    "409": "noorigin",\r
+    "411": "norecipient",\r
+    "412": "notexttosend",\r
+    "413": "notoplevel",\r
+    "414": "wildtoplevel",\r
+    "421": "unknowncommand",\r
+    "422": "nomotd",\r
+    "423": "noadmininfo",\r
+    "424": "fileerror",\r
+    "431": "nonicknamegiven",\r
+    "432": "erroneusnickname", # Thiss iz how its speld in thee RFC.\r
+    "433": "nicknameinuse",\r
+    "436": "nickcollision",\r
+    "441": "usernotinchannel",\r
+    "442": "notonchannel",\r
+    "443": "useronchannel",\r
+    "444": "nologin",\r
+    "445": "summondisabled",\r
+    "446": "usersdisabled",\r
+    "451": "notregistered",\r
+    "461": "needmoreparams",\r
+    "462": "alreadyregistered",\r
+    "463": "nopermforhost",\r
+    "464": "passwdmismatch",\r
+    "465": "yourebannedcreep", # I love this one...\r
+    "466": "youwillbebanned",\r
+    "467": "keyset",\r
+    "471": "channelisfull",\r
+    "472": "unknownmode",\r
+    "473": "inviteonlychan",\r
+    "474": "bannedfromchan",\r
+    "475": "badchannelkey",\r
+    "476": "badchanmask",\r
+    "481": "noprivileges",\r
+    "482": "chanoprivsneeded",\r
+    "483": "cantkillserver",\r
+    "491": "nooperhost",\r
+    "492": "noservicehost",\r
+    "501": "umodeunknownflag",\r
+    "502": "usersdontmatch",\r
+}\r
+\r
+generated_events = [\r
+    # Generated events\r
+    "disconnect",\r
+    "ctcp",\r
+    "ctcpreply"\r
+]\r
+\r
+protocol_events = [\r
+    # IRC protocol events\r
+    "error",\r
+    "join",\r
+    "kick",\r
+    "mode",\r
+    "part",\r
+    "ping",\r
+    "privmsg",\r
+    "privnotice",\r
+    "pubmsg",\r
+    "pubnotice",\r
+    "quit"\r
+]\r
+\r
+all_events = generated_events + protocol_events + numeric_events.values()\r