chiark / gitweb /
Upstream version 0.3
authormatthew <matthew>
Thu, 7 Feb 2002 09:15:32 +0000 (09:15 +0000)
committermatthew <matthew>
Thu, 7 Feb 2002 09:15:32 +0000 (09:15 +0000)
acrobat.py [new file with mode: 0755]
commands.py [new file with mode: 0644]
config.py [new file with mode: 0644]
ircbot.py [new file with mode: 0644]
irclib.py [new file with mode: 0644]

diff --git a/acrobat.py b/acrobat.py
new file mode 100755 (executable)
index 0000000..0305f94
--- /dev/null
@@ -0,0 +1,156 @@
+#!/usr/bin/env python2
+#
+# Bot logic:
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+#
+# IRC framework:
+# Joel Rosdahl <joel@rosdahl.net>
+#
+# Contributors:
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# 
+# This file is part of Acrobat.
+#
+# Acrobat is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; either version 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+"""
+Acrobat - an extensible, minmalist irc bot.
+"""
+
+import string, urllib, sys, cPickle, os, random
+from ircbot import SingleServerIRCBot
+from irclib import nm_to_n, irc_lower
+import config
+
+#splitting out the configuration to a separate (source, but this is incidental-
+#it's just the nearest free parser) file.
+
+import config
+
+class Karma:
+    def __init__(self):
+        self.dict = {}
+
+class Acrobat(SingleServerIRCBot):
+    def __init__(self, channel, nickname, server, owner, port=6667):
+        SingleServerIRCBot.__init__(self,
+                                    [(server, port)], nickname, nickname)
+        self.channel = channel
+        self.owner = owner
+        self.revision = "$Revision$" # global version number
+        self.trouts = config.trouts
+        self.karmafilename = config.karmafilename
+        self.config = config
+        
+        # load the karma db
+        try:
+            f = open(self.karmafilename, "r")
+            self.karma = cPickle.load(f)
+            f.close()
+        except IOError:
+            self.karma = Karma()
+        
+    ## EVENT HANDLERS
+            
+    def on_welcome(self, conn, evt):
+        conn.join(self.channel)
+
+    def on_privmsg(self, conn, evt):
+        self.do_command(nm_to_n(evt.source()), evt.arguments()[0])
+        
+    def on_pubmsg(self, conn, evt):
+        payload = evt.arguments()[0]
+        nc = string.split(payload, " ", 1)
+        if len(nc) > 1 and (irc_lower(nc[0]).startswith(
+            irc_lower(self.connection.get_nickname()))):
+            self.do_command(self.channel, nc[1].strip(), public = 1)
+        elif payload[0] in config.triggers and len(payload)>1:
+            self.do_command(self.channel, payload[1:].strip(), public=1)
+        elif payload.find("++") != -1 or payload.find("--") != -1:
+            self.do_command(self.channel, payload.strip(), public=1)
+    # General query handler
+    def do_command(self, nick, cmd, public=0):
+        conn = self.connection
+        command = cmd.split()[0]
+        sys.stderr.write(command)
+        args = (self, cmd, nick, conn, public)
+
+        # regrettably, these (and anything else with special triggers)
+        # must be special-cased, which is aesthetically unsatisfying.
+        
+        # karma: up
+        if command.endswith("++"):
+            self.karmaup(cmd)
+        # karma: down
+        if command.endswith("--"):
+            self.karmadown(cmd)
+
+        # and in the general case (this is slightly magical)
+        if command.lower() in config.commands.keys():
+            config.commands[command](*args)
+        # else do nothing.
+
+        # What this _means_ is: you write a
+        # function(bot, cmd, nick, conn, public), where bot is the bot class
+        # (ie self here), and drop it in commands.py; then add a trigger command
+        # to config.py for it, in the dictionary "commands", and it will
+        # just start working on bot restart or config reload.
+
+        # This is, IMO, quite nifty. :)
+
+    # And now the karma commands, as these pretty much have to be here :(
+    # increment karma
+    def karmaup(self, cmd):
+        if self.karma.dict.has_key(cmd.split()[0][:-2]):
+            self.karma.dict[cmd.split()[0][:-2]] += 1
+        else:
+            self.karma.dict[cmd.split()[0][:-2]] = 1
+    #decrement karma
+    def karmadown(self, cmd):
+        if self.karma.dict.has_key(cmd.split()[0][:-2]):
+            self.karma.dict[cmd.split()[0][:-2]] -= 1
+        else:
+            self.karma.dict[cmd.split()[0][:-2]] = -1
+                
+
+def main():
+    # initialize the bot
+    bot = Acrobat(config.channel, config.nickname, config.server,
+                  config.owner, config.port)
+    sys.stderr.write("Trying to connect...\n")
+    # and the event loop
+    bot.start()
+
+#    if len(sys.argv) != 5: # insufficient arguments
+#        print "Usage: acrobat <server[:port]> <channel> <nickname> owner"
+#        sys.exit(1)
+#    sv_port = string.split(sys.argv[1], ":", 1) # tuple; (server, port)
+#    server = sv_port[0]
+#    if len(sv_port) == 2:
+#        try:
+#            port = int(sv_port[1])
+#        except ValueError:
+#            print "Error: Erroneous port."
+#            sys.exit(1)
+#    else:
+#        port = 6667 # default irc port
+#    channel = sys.argv[2]
+#    nickname = sys.argv[3]
+#    owner = sys.argv[4]
+
+if __name__ == "__main__":
+    main()
diff --git a/commands.py b/commands.py
new file mode 100644 (file)
index 0000000..6f6d1f7
--- /dev/null
@@ -0,0 +1,139 @@
+# Part of Acrobat.
+import string, cPickle, random, urllib, sys
+from irclib import irc_lower, nm_to_n
+
+# query karma
+def karmaq(bot, cmd, nick, conn, public):
+    # in public
+    if public == 1:
+        try:
+            if bot.karma.dict.has_key(cmd.split()[1]):
+                conn.privmsg(bot.channel, "%s has karma %s."
+                             %(cmd.split()[1],
+                                   bot.karma.dict[cmd.split()[1]]))
+            else:
+                conn.privmsg(bot.channel, "%s has no karma set." %
+                             cmd.split()[1])
+        except IndexError:
+            conn.privmsg(bot.channel, "I have karma on %s items." %
+                         len(bot.karma.dict.keys()))
+    # in private
+    else:
+        try:
+            if bot.karma.dict.has_key(cmd.split()[1]):
+                conn.notice(nick, "%s has karma %s." %
+                            (cmd.split()[1],
+                             bot.karma.dict[cmd.split()[1]]))
+            else:
+                conn.notice(nick, "I have karma on %s items." %
+                            len(bot.karma.dict.keys()))
+        except IndexError:
+            conn.notice(nick, "I have karma on %s items." %
+                        len(bot.karma.dict.keys()))
+# query bot status
+def infoq(bot, cmd, nick, conn, public):
+    if public == 1:
+        conn.privmsg(bot.channel,
+                     "I am Acrobat %s, on %s, as nick %s." %
+                    (bot.revision.split()[1], bot.channel, conn.get_nickname()))
+        conn.privmsg(bot.channel,
+                     "My owner is %s; I have karma on %s items." %
+                     (bot.owner, len(bot.karma.dict.keys())))
+    else:
+        conn.notice(nick, "I am Acrobat %s, on %s, as nick %s." %
+                    (bot.revision.split()[1], bot.channel, conn.get_nickname()))
+        conn.notice(nick, "My owner is %s; I have karma on %s items." %
+                    (bot.owner, len(bot.karma.dict.keys())))
+
+# trout someone
+def troutq(bot, cmd, nick, conn, public):
+    try:
+        target = string.join(cmd.split()[1:])
+        me = bot.connection.get_nickname()
+        trout_msg = random.choice(bot.trouts)
+        # The bot won't trout itself;
+        if irc_lower(me) == irc_lower(target):
+            target = nick
+        conn.action(bot.channel, trout_msg % target)
+        if public == 0:
+            if random.random() <= bot.config.selftroutrisk:
+                conn.action(bot.channel,
+                 "notes %s is conducting a whispering campaign." % nick)
+    except IndexError:
+        conn.notice(nick, "Who do you wish me to trout?")
+
+# rehash bot config
+def reloadq(bot, cmd, nick, conn, public):
+    if irc_lower(nick) == irc_lower(bot.owner):
+        try:
+            reload(bot.config)
+            bot.trouts = bot.config.trouts
+            conn.privmsg(nick, "Config reloaded.")
+        except ImportError:
+            conn.notice(nick, "Config reloading failed!")
+    else:
+        conn.notice(nick, "This command can only be invoked by my owner.")
+
+# quit irc
+def quitq(bot, cmd, nick, conn, public):
+    if irc_lower(nick) == irc_lower(bot.owner):
+        try:
+            f = open(bot.karmafilename, "w")
+            cPickle.dump(bot.karma, f)
+            f.close()
+        except IOError:
+            sys.stderr.write("Problems dumping karma: probably lost :(")
+        bot.die(msg = "I have been chosen!")
+    elif public == 1:
+        conn.privmsg(nick, "Such aggression in public!")
+    else:
+        conn.notice(nick, "You're not my owner.")
+
+# google for something
+def googleq(bot, cmd, nick, conn, public):
+    cmdrest = string.join(cmd.split()[1:])
+    #sys.stderr.write(conn)
+    # "I'm Feeling Lucky" rather than try and parse the html
+    targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
+            % urllib.quote_plus(cmdrest))
+    try:
+        # get redirected and grab the resulting url for returning
+        gsearch = urllib.urlopen(targ).geturl()
+        if gsearch != targ: # we've found something
+            if public == 0:
+                conn.notice(nick, str(gsearch))
+            else: # we haven't found anything.
+                conn.privmsg(nick, str(gsearch))
+        else:
+            if public == 0:
+                conn.notice(nick, "No pages found.")
+            else:
+                conn.privmsg(nick, "No pages found.")
+    except IOError: # if the connection times out. This blocks. :(
+        if public == 0:
+            conn.notice(nick, "The web's broken. Waah!")
+        else:
+            conn.privmsg(nick, "The web's broken. Waah!")
+
+### say to msg/channel            
+def sayq(bot, cmd, nick, conn, public):
+    if irc_lower(nick) == irc_lower(bot.owner):
+        conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
+    else:
+        conn.privmsg(nick, "You're not my owner!")
+
+### action to msg/channel
+def doq(bot, cmd, nick, conn, public):
+    sys.stderr.write(irc_lower(bot.owner))
+    sys.stderr.write(irc_lower(nick))
+    if public == 0:
+        if irc_lower(nick) == irc_lower(bot.owner):
+            conn.action(bot.channel, string.join(cmd.split()[1:]))
+        else:
+            conn.privmsg(nick, "You're not my owner!")
+
+###disconnect
+def disconnq(bot, cmd, nick, conn, public):
+    if cmd == "disconnect": # hop off for 60s
+        bot.disconnect(msg="Be right back.")
+
diff --git a/config.py b/config.py
new file mode 100644 (file)
index 0000000..df074e0
--- /dev/null
+++ b/config.py
@@ -0,0 +1,59 @@
+# This file is part of Acrobat.
+#
+# Acrobat is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; either version 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+
+# Acrobat configuration file
+# this is just python source, so has to be formatted as such
+from commands import * 
+
+karmafilename = "karmadump"
+
+trouts = ("questions %s's parentage.",
+          "slaps %s about a bit with a wet trout",
+          "thumbs its nose at %s.")
+
+selftroutrisk = 0.1 # some number between 0 and 1...
+                    # the risk of a trout rebounding on the trouter!
+                    
+server = "irc.barrysworld.com"
+
+port = 6667
+
+nickname = "Acrobat"
+
+channel = "#acrotest"
+
+owner = "Acronym"
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+# these are "command": (function in commands.py) pairs.
+
+commands = {"karma": karmaq,
+            "info": infoq,
+            "trout": troutq,
+            "reload": reloadq,
+            "quit": quitq,
+            "google": googleq,
+            "say": sayq,
+            "do": doq,
+            "disconnect": disconnq,
+            "hop": disconnq }
diff --git a/ircbot.py b/ircbot.py
new file mode 100644 (file)
index 0000000..f83c822
--- /dev/null
+++ b/ircbot.py
@@ -0,0 +1,424 @@
+# 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
+"""ircbot -- Simple IRC bot library.\r
+\r
+This module contains a single-server IRC bot class that can be used to\r
+write simpler bots.\r
+"""\r
+\r
+import sys\r
+import string\r
+from UserDict import UserDict\r
+\r
+from irclib import SimpleIRCClient\r
+from irclib import nm_to_n, irc_lower, all_events\r
+from irclib import parse_channel_modes, is_channel, is_channel\r
+from irclib import ServerConnectionError\r
+\r
+class SingleServerIRCBot(SimpleIRCClient):\r
+    """A single-server IRC bot class.\r
+\r
+    The bot tries to reconnect if it is disconnected.\r
+\r
+    The bot keeps track of the channels it has joined, the other\r
+    clients that are present in the channels and which of those that\r
+    have operator or voice modes.  The "database" is kept in the\r
+    self.channels attribute, which is an IRCDict of Channels.\r
+    """\r
+    def __init__(self, server_list, nickname, realname, reconnection_interval=60):\r
+        """Constructor for SingleServerIRCBot objects.\r
+\r
+        Arguments:\r
+\r
+            server_list -- A list of tuples (server, port) that\r
+                           defines which servers the bot should try to\r
+                           connect to.\r
+\r
+            nickname -- The bot's nickname.\r
+\r
+            realname -- The bot's realname.\r
+\r
+            reconnection_interval -- How long the bot should wait\r
+                                     before trying to reconnect.\r
+        """\r
+\r
+        SimpleIRCClient.__init__(self)\r
+        self.channels = IRCDict()\r
+        self.server_list = server_list\r
+        if not reconnection_interval or reconnection_interval < 0:\r
+            reconnection_interval = 2**31\r
+        self.reconnection_interval = reconnection_interval\r
+\r
+        self._nickname = nickname\r
+        self._realname = realname\r
+        for i in ["disconnect", "join", "kick", "mode",\r
+                  "namreply", "nick", "part", "quit"]:\r
+            self.connection.add_global_handler(i,\r
+                                               getattr(self, "_on_" + i),\r
+                                               -10)\r
+    def _connected_checker(self):\r
+        """[Internal]"""\r
+        if not self.connection.is_connected():\r
+            self.connection.execute_delayed(self.reconnection_interval,\r
+                                            self._connected_checker)\r
+            self.jump_server()\r
+\r
+    def _connect(self):\r
+        """[Internal]"""\r
+        password = None\r
+        if len(self.server_list[0]) > 2:\r
+            password = self.server_list[0][2]\r
+        try:\r
+            self.connect(self.server_list[0][0],\r
+                         self.server_list[0][1],\r
+                         self._nickname,\r
+                         password,\r
+                         ircname=self._realname)\r
+        except ServerConnectionError:\r
+            pass\r
+\r
+    def _on_disconnect(self, c, e):\r
+        """[Internal]"""\r
+        self.channels = IRCDict()\r
+        self.connection.execute_delayed(self.reconnection_interval,\r
+                                        self._connected_checker)\r
+\r
+    def _on_join(self, c, e):\r
+        """[Internal]"""\r
+        ch = e.target()\r
+        nick = nm_to_n(e.source())\r
+        if nick == self._nickname:\r
+            self.channels[ch] = Channel()\r
+        self.channels[ch].add_user(nick)\r
+\r
+    def _on_kick(self, c, e):\r
+        """[Internal]"""\r
+        nick = e.arguments()[0]\r
+        channel = e.target()\r
+\r
+        if nick == self._nickname:\r
+            del self.channels[channel]\r
+        else:\r
+            self.channels[channel].remove_user(nick)\r
+\r
+    def _on_mode(self, c, e):\r
+        """[Internal]"""\r
+        modes = parse_channel_modes(string.join(e.arguments()))\r
+        t = e.target()\r
+        if is_channel(t):\r
+            ch = self.channels[t]\r
+            for mode in modes:\r
+                if mode[0] == "+":\r
+                    f = ch.set_mode\r
+                else:\r
+                    f = ch.clear_mode\r
+                f(mode[1], mode[2])\r
+        else:\r
+            # Mode on self... XXX\r
+            pass\r
+\r
+    def _on_namreply(self, c, e):\r
+        """[Internal]"""\r
+\r
+        # e.arguments()[0] == "="     (why?)\r
+        # e.arguments()[1] == channel\r
+        # e.arguments()[2] == nick list\r
+\r
+        ch = e.arguments()[1]\r
+        for nick in string.split(e.arguments()[2]):\r
+            if nick[0] == "@":\r
+                nick = nick[1:]\r
+                self.channels[ch].set_mode("o", nick)\r
+            elif nick[0] == "+":\r
+                nick = nick[1:]\r
+                self.channels[ch].set_mode("v", nick)\r
+            self.channels[ch].add_user(nick)\r
+\r
+    def _on_nick(self, c, e):\r
+        """[Internal]"""\r
+        before = nm_to_n(e.source())\r
+        after = e.target()\r
+        for ch in self.channels.values():\r
+            if ch.has_user(before):\r
+                ch.change_nick(before, after)\r
+        if nm_to_n(before) == self._nickname:\r
+            self._nickname = after\r
+\r
+    def _on_part(self, c, e):\r
+        """[Internal]"""\r
+        nick = nm_to_n(e.source())\r
+        channel = e.target()\r
+\r
+        if nick == self._nickname:\r
+            del self.channels[channel]\r
+        else:\r
+            self.channels[channel].remove_user(nick)\r
+\r
+    def _on_quit(self, c, e):\r
+        """[Internal]"""\r
+        nick = nm_to_n(e.source())\r
+        for ch in self.channels.values():\r
+            if ch.has_user(nick):\r
+                ch.remove_user(nick)\r
+\r
+    def die(self, msg="Bye, cruel world!"):\r
+        """Let the bot die.\r
+\r
+        Arguments:\r
+\r
+            msg -- Quit message.\r
+        """\r
+        self.connection.quit(msg)\r
+        sys.exit(0)\r
+\r
+    def disconnect(self, msg="I'll be back!"):\r
+        """Disconnect the bot.\r
+\r
+        The bot will try to reconnect after a while.\r
+\r
+        Arguments:\r
+\r
+            msg -- Quit message.\r
+        """\r
+        self.connection.quit(msg)\r
+\r
+    def get_version(self):\r
+        """Returns the bot version.\r
+\r
+        Used when answering a CTCP VERSION request.\r
+        """\r
+        return "ircbot.py by Joel Rosdahl <joel@rosdahl.net>"\r
+\r
+    def jump_server(self):\r
+        """Connect to a new server, possible disconnecting from the current.\r
+\r
+        The bot will skip to next server in the server_list each time\r
+        jump_server is called.\r
+        """\r
+        if self.connection.is_connected():\r
+            self.connection.quit("Jumping servers")\r
+        self.server_list.append(self.server_list.pop(0))\r
+        self._connect()\r
+\r
+    def on_ctcp(self, c, e):\r
+        """Default handler for ctcp events.\r
+\r
+        Replies to VERSION and PING requests.\r
+        """\r
+        if e.arguments()[0] == "VERSION":\r
+            c.ctcp_reply(nm_to_n(e.source()), self.get_version())\r
+        elif e.arguments()[0] == "PING":\r
+            if len(e.arguments()) > 1:\r
+                c.ctcp_reply(nm_to_n(e.source()),\r
+                             "PING " + e.arguments()[1])\r
+\r
+    def start(self):\r
+        """Start the bot."""\r
+        self._connect()\r
+        SimpleIRCClient.start(self)\r
+\r
+\r
+class IRCDict:\r
+    """A dictionary suitable for storing IRC-related things.\r
+\r
+    Dictionary keys a and b are considered equal if and only if\r
+    irc_lower(a) == irc_lower(b)\r
+\r
+    Otherwise, it should behave exactly as a normal dictionary.\r
+    """\r
+\r
+    def __init__(self, dict=None):\r
+        self.data = {}\r
+        self.canon_keys = {}  # Canonical keys\r
+        if dict is not None:\r
+            self.update(dict)\r
+    def __repr__(self):\r
+        return repr(self.data)\r
+    def __cmp__(self, dict):\r
+        if isinstance(dict, IRCDict):\r
+            return cmp(self.data, dict.data)\r
+        else:\r
+            return cmp(self.data, dict)\r
+    def __len__(self):\r
+        return len(self.data)\r
+    def __getitem__(self, key):\r
+        return self.data[self.canon_keys[irc_lower(key)]]\r
+    def __setitem__(self, key, item):\r
+        if self.has_key(key):\r
+            del self[key]\r
+        self.data[key] = item\r
+        self.canon_keys[irc_lower(key)] = key\r
+    def __delitem__(self, key):\r
+        ck = irc_lower(key)\r
+        del self.data[self.canon_keys[ck]]\r
+        del self.canon_keys[ck]\r
+    def clear(self):\r
+        self.data.clear()\r
+        self.canon_keys.clear()\r
+    def copy(self):\r
+        if self.__class__ is UserDict:\r
+            return UserDict(self.data)\r
+        import copy\r
+        return copy.copy(self)\r
+    def keys(self):\r
+        return self.data.keys()\r
+    def items(self):\r
+        return self.data.items()\r
+    def values(self):\r
+        return self.data.values()\r
+    def has_key(self, key):\r
+        return self.canon_keys.has_key(irc_lower(key))\r
+    def update(self, dict):\r
+        for k, v in dict.items():\r
+            self.data[k] = v\r
+    def get(self, key, failobj=None):\r
+        return self.data.get(key, failobj)\r
+\r
+\r
+class Channel:\r
+    """A class for keeping information about an IRC channel.\r
+\r
+    This class can be improved a lot.\r
+    """\r
+\r
+    def __init__(self):\r
+        self.userdict = IRCDict()\r
+        self.operdict = IRCDict()\r
+        self.voiceddict = IRCDict()\r
+        self.modes = {}\r
+\r
+    def users(self):\r
+        """Returns an unsorted list of the channel's users."""\r
+        return self.userdict.keys()\r
+\r
+    def opers(self):\r
+        """Returns an unsorted list of the channel's operators."""\r
+        return self.operdict.keys()\r
+\r
+    def voiced(self):\r
+        """Returns an unsorted list of the persons that have voice\r
+        mode set in the channel."""\r
+        return self.voiceddict.keys()\r
+\r
+    def has_user(self, nick):\r
+        """Check whether the channel has a user."""\r
+        return self.userdict.has_key(nick)\r
+\r
+    def is_oper(self, nick):\r
+        """Check whether a user has operator status in the channel."""\r
+        return self.operdict.has_key(nick)\r
+\r
+    def is_voiced(self, nick):\r
+        """Check whether a user has voice mode set in the channel."""\r
+        return self.voiceddict.has_key(nick)\r
+\r
+    def add_user(self, nick):\r
+        self.userdict[nick] = 1\r
+\r
+    def remove_user(self, nick):\r
+        for d in self.userdict, self.operdict, self.voiceddict:\r
+            if d.has_key(nick):\r
+                del d[nick]\r
+\r
+    def change_nick(self, before, after):\r
+        self.userdict[after] = 1\r
+        del self.userdict[before]\r
+        if self.operdict.has_key(before):\r
+            self.operdict[after] = 1\r
+            del self.operdict[before]\r
+        if self.voiceddict.has_key(before):\r
+            self.voiceddict[after] = 1\r
+            del self.voiceddict[before]\r
+\r
+    def set_mode(self, mode, value=None):\r
+        """Set mode on the channel.\r
+\r
+        Arguments:\r
+\r
+            mode -- The mode (a single-character string).\r
+\r
+            value -- Value\r
+        """\r
+        if mode == "o":\r
+            self.operdict[value] = 1\r
+        elif mode == "v":\r
+            self.voiceddict[value] = 1\r
+        else:\r
+            self.modes[mode] = value\r
+\r
+    def clear_mode(self, mode, value=None):\r
+        """Clear mode on the channel.\r
+\r
+        Arguments:\r
+\r
+            mode -- The mode (a single-character string).\r
+\r
+            value -- Value\r
+        """\r
+        try:\r
+            if mode == "o":\r
+                del self.operdict[value]\r
+            elif mode == "v":\r
+                del self.voiceddict[value]\r
+            else:\r
+                del self.modes[mode]\r
+        except KeyError:\r
+            pass\r
+\r
+    def has_mode(self, mode):\r
+        return mode in self.modes\r
+\r
+    def is_moderated(self):\r
+        return self.has_mode("m")\r
+\r
+    def is_secret(self):\r
+        return self.has_mode("s")\r
+\r
+    def is_protected(self):\r
+        return self.has_mode("p")\r
+\r
+    def has_topic_lock(self):\r
+        return self.has_mode("t")\r
+\r
+    def is_invite_only(self):\r
+        return self.has_mode("i")\r
+\r
+    def has_message_from_outside_protection(self):\r
+        # Eh... What should it be called, really?\r
+        return self.has_mode("n")\r
+\r
+    def has_limit(self):\r
+        return self.has_mode("l")\r
+\r
+    def limit(self):\r
+        if self.has_limit():\r
+            return self.modes[l]\r
+        else:\r
+            return None\r
+\r
+    def has_key(self):\r
+        return self.has_mode("k")\r
+\r
+    def key(self):\r
+        if self.has_key():\r
+            return self.modes["k"]\r
+        else:\r
+            return None\r
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