Anonymous avatar Anonymous committed 3bb4c2a

Version 0.3.0.

Comments (0)

Files changed (7)

+2000-11-26  Joel Rosdahl  <joel@rosdahl.net>
+
+	* Released version 0.3.0.
+
+	* Makefile (dist): Include ircbot.py again.
+
+	* README: Updated.
+
+	* irclib.py (ServerConnection.get_nickname): Renamed from
+	get_nick_name.
+	(ServerConnection._get_socket): Return None if not connected.
+
+2000-11-25  Joel Rosdahl  <joel@rosdahl.net>
+
+	* irclib.py (ServerConnection.process_data): all_raw_messages
+	instead of allrawmessages.
+	(IRC._handle_event): Added "all_events" event type.
+	(nm_to_n): Renamed from nick_from_nickmask.
+	(nm_to_uh): Renamed from userhost_from_nickmask.
+	(nm_to_h): Renamed from host_from_nickmask.
+	(nm_to_u): Renamed from user_from_nickmask.
+	(SimpleIRCClient): Created.
+
+2000-11-22  Joel Rosdahl  <joel@rosdahl.net>
+
+	* irclib.py (lower_irc_string): Use translation instead.
+	(ServerConnection.process_data): Split non-RFC-compliant lines a
+	bit more intelligently.
+	(ServerConnection.process_data): Removed unnecessary try/except
+	block.
+	(ServerConnection.get_server_name): Return empty server if
+	unknown.
+	(_rfc_1459_command_regexp): Tweaked a bit.
+
+	* ircbot.py: Rewritten.
+
+2000-11-21  Joel Rosdahl  <joel@rosdahl.net>
+
+	* irclib.py (IRC.process_forever): Default to processing a bit
+	more often.
+
 2000-10-29  Joel Rosdahl  <joel@rosdahl.net>
 
 	* Released version 0.2.4.
 doc:
 	rm -r doc
 	mkdir doc
-	PYTHONPATH=. pythondoc -d doc -f HTML4 -i frame=1 irclib
+	PYTHONPATH=. pythondoc -d doc -f HTML4 -i frame=1 irclib ircbot
 
 dist: doc
 	mkdir irclib-$(VERSION)
-	cp -r COPYING README ChangeLog Makefile irclib.py irccat servermap doc irclib-$(VERSION)
+	cp -r COPYING README ChangeLog Makefile irclib.py ircbot.py irccat irccat2 servermap testbot.py doc irclib-$(VERSION)
 	tar cvzf irclib-$(VERSION).tar.gz irclib-$(VERSION)
 	rm -r irclib-$(VERSION)
 
 This library is intended to encapsulate the IRC protocol at a quite
 low level.  It provides an event-driven IRC client framework.  It has
 a fairly thorough support for the basic IRC protocol and CTCP, but DCC
-connection support is not yet implemented.
+connection support is not yet implemented.  It actually does CTCP
+parsing exactly as the CTCP specifications describe it -- I've never
+seen another IRC client do that!
 
 In order to understand how to make an IRC client, I'm afraid you more
 or less must understand the IRC specifications.  They are available
 
     http://www.irchelp.org/irchelp/rfc/
 
+The main features of the IRC client framework are:
+
+  * Abstraction of the IRC protocol.
+  * Handles multiple simultaneous IRC server connections.
+  * Handles server PONGing transparently.
+  * Messages to the IRC server are done by calling methods on an IRC
+    connection object.
+  * Messages from an IRC server triggers events, which can be caught
+    by event handlers.
+  * Reading from and writing to IRC server sockets are normally done
+    by an internal select() loop, but the select()ing may be done by
+    an external main loop.
+  * Functions can be registered to execute at specified times by the
+    event-loop.
+  * Decodes CTCP tagging correctly (hopefully); I haven't seen any
+    other IRC client implementation that handles the CTCP
+    specification subtilties.
+  * A kind of simple, single-server, object-oriented IRC client class
+    that dispatches events to instance methods is included.
+
+Current limitations:
+
+  * The IRC protocol shines through the abstraction a bit too much.
+  * Data is not written asynchronously to the server, i.e. the write()
+    may block if the TCP buffers are stuffed.
+  * There are no support for DCC connections.
+  * The author haven't even read RFC 2810, 2811, 2812 and 2813.
+  * Like most projects, documentation is lacking...
+
 Unfortunately, this library isn't as well-documented as I would like
 it to be.  There is some documentation in HTML format in the doc
 subdirectory.  It is generated from the docstrings in irclib.py by the
     stdin and writes it to a specified user or channel on an IRC
     server.
 
+  * irccat2
+
+    The same as above, but using the SimpleIRCClient class.
+
   * servermap
 
     Another simple example.  servermap connects to an IRC server,
     finds out what other IRC servers there are in the net and prints
     a tree-like map of their interconnections.
 
+  * testbot.py
+
+    An example bot that uses the SingleServerIRCBot class from
+    ircbot.py.  The bot enters a channel and listens for commands in
+    private messages or channel traffic.
+
 Enjoy.
 
 Since I seldom use IRC anymore, I will probably not work much on the
-# THIS IS PROBABLY MOSTLY USELESS, ERRONEOUS, BUGGY AND UNTESTED CODE RIGHT NOW.
-#
-# Copyright (C) 1999 Joel Rosdahl
+# Copyright (C) 1999, 2000 Joel Rosdahl
 # 
 # This program 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.
-#        
+# 
 # This program 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
 #
 # $Id$
 
+"""ircbot -- Simple IRC bot library.
+
+This module contains a single-server IRC bot class that can be used to
+write simpler bots.
+"""
+
 import sys
+import string
+from UserDict import UserDict
 
-from irclib import *
+from irclib import SimpleIRCClient
+from irclib import nm_to_n, irc_lower, all_events
+from irclib import parse_channel_modes, is_channel, is_channel
+from irclib import ServerConnectionError
 
-# Every RECONNECT_INTERVAL seconds, check if the bot is connected and
-# reconnect it if it isn't.
-RECONNECT_INTERVAL = 60
+class SingleServerIRCBot(SimpleIRCClient):
+    """A single-server IRC bot class.
 
-class SingleServerIRCBot:
-    def __init__(self, server_list, nickname, realname, channels_file="bot.channels"):
-        self.channels = {}
-        self.irc = IRC()
+    The bot tries to reconnect if it is disconnected.
+
+    The bot keeps track of the channels it has joined, the other
+    clients that are present in the channels and which of those that
+    have operator or voice modes.
+    """
+    def __init__(self, server_list, nickname, realname, reconnection_interval=60):
+        """Constructor for SingleServerIRCBot objects.
+
+        Arguments:
+
+            server_list -- A list of tuples (server, port) that
+                           defines which servers the bot should try to
+                           connect to.
+
+            nickname -- The bot's nickname.
+
+            realname -- The bot's realname.
+
+            reconnection_interval -- How long the bot should wait
+                                     before trying to reconnect.
+        """
+
+        SimpleIRCClient.__init__(self)
+        self.channels = IRCDict()
         self.server_list = server_list
-        self.current_server = 0
-        self.channels_file = channels_file
+        if not reconnection_interval or reconnection_interval < 0:
+            reconnection_interval = 2**31
+        self.reconnection_interval = reconnection_interval
 
-        try:
-            f = open(channels_file, "r")
-        except:
-            f = open(channels_file, "w")
-            f.close()
-            f = open(channels_file, "r")
-        for ch in f.readlines():
-            self.channels[ch[:-1]] = Channel(ch[:-1])
-        f.close()
-
-        self.nickname = nickname
-        self.realname = realname
-        self.connection = self.irc.server()
-        for numeric in all_events:
-            self.connection.add_global_handler(numeric, self.event_dispatcher)
-        self.connection.execute_delayed(RECONNECT_INTERVAL, self.__connected_checker, ())
-        self.connect()
-
-    def __connected_checker(self):
-        self.connection.execute_delayed(RECONNECT_INTERVAL, self.__connected_checker, ())
+        self._nickname = nickname
+        self._realname = realname
+        for i in ["disconnect", "join", "kick", "mode",
+                  "namreply", "nick", "part", "quit"]:
+            self.connection.add_global_handler(i,
+                                               getattr(self, "_on_" + i),
+                                               -10)
+    def _connected_checker(self):
+        """[Internal]"""
         if not self.connection.is_connected():
+            self.connection.execute_delayed(self.reconnection_interval,
+                                            self._connected_checker)
             self.jump_server()
 
-    def connect(self):
+    def _connect(self):
+        """[Internal]"""
         password = None
-        if len(self.server_list[self.current_server]) > 2:
-            password = self.server_list[self.current_server][2]
+        if len(self.server_list[0]) > 2:
+            password = self.server_list[0][2]
         try:
-            self.connection.connect(self.server_list[self.current_server][0],
-                                    self.server_list[self.current_server][1],
-                                    self.nickname,
-                                    self.nickname,
-                                    self.realname,
-                                    password)
+            self.connect(self.server_list[0][0],
+                         self.server_list[0][1],
+                         self._nickname,
+                         password,
+                         ircname=self._realname)
         except ServerConnectionError:
             pass
 
-    def jump_server(self):
-        if self.connection.is_connected():
-            self.get_connection().quit("Jumping servers")
-        self.current_server = (self.current_server + 1) % len(self.server_list)
-        self.connect()
+    def _on_disconnect(self, c, e):
+        """[Internal]"""
+        self.channels = IRCDict()
+        self.connection.execute_delayed(self.reconnection_interval,
+                                        self._connected_checker)
 
-    def start(self):
-        self.irc.process_forever()
+    def _on_join(self, c, e):
+        """[Internal]"""
+        ch = e.target()
+        nick = nm_to_n(e.source())
+        if nick == self._nickname:
+            self.channels[ch] = Channel()
+        self.channels[ch].add_user(nick)
 
-    def get_connection(self):
-        return self.connection
+    def _on_kick(self, c, e):
+        """[Internal]"""
+        nick = e.arguments()[0]
+        channel = e.target()
 
-    def get_irc_object(self):
-        return self.ircobj
+        if nick == self._nickname:
+            del self.channels[channel]
+        else:
+            self.channels[channel].remove_user(nick)
 
-    def join_new_channel(self, channel):
-        self.channels[channel] = Channel(channel)
-        self.get_connection().join(channel)
-        f = open(self.channels_file, "w")
-        f.writelines(map(lambda x: x+"\n", self.channels.keys()))
-        f.close()
-
-    def part_old_channel(self, channel):
-        self.get_connection().part(channel)
-        del self.channels[channel]
-        f = open(self.channels_file, "w")
-        f.writelines(map(lambda x: x+"\n", self.channels.keys()))
-        f.close()
-
-    def die(self):
-        self.connection.exit("I'll be back!")
-
-    def get_version(self):
-        return "IRCBot by Joel Rosdahl <joel@rosdahl.net>"
-
-    def event_dispatcher(self, c, e):
-        try:
-            method = getattr(self, "on_" + e.eventtype())
-        except AttributeError:
-            # No such handler.
-            return
-        apply(method, (c, e))
-
-    def on_welcome(self, c, e):
-        for ch in self.channels.keys():
-            c.join(ch)
-
-    def on_ctcp(self, c, e):
-        if e.arguments()[0] == "VERSION":
-            c.ctcp_reply(nick_from_nickmask(e.source()), self.get_version())
-        elif e.arguments()[0] == "PING":
-            if len(e.arguments()) > 1:
-                c.ctcp_reply(nick_from_nickmask(e.source()), "PING " + e.arguments()[1])
-    
-    def on_error(self, c, e):
-        # XXX join channels here, etc.
-        pass
-        
-    def on_join(self, c, e):
-        self.channels[lower_irc_string(e.target())].add_nick(e.source())
-        if e.source() == c.get_nick_name():
-            self.channels[lower_irc_string(e.target())].set_joined()
-    
-    def on_kick(self, c, e):
-        if e.arguments()[0] == self.nickname:
-            self.channels[lower_irc_string(e.target())].clear_joined()
-            if self.channels[lower_irc_string(e.target())].auto_join():
-                def rejoin(c, channel):
-                    c.join(channel)
-                c.execute_delayed(10, rejoin, (c, e.target()))
-        else:
-            self.channels[lower_irc_string(e.target())].remove_nick(e.arguments()[0])
-    
-    def on_inviteonlychan(self, c, e):
-        # XXX
-        pass
-
-    def on_bannedfromchan(self, c, e):
-        # XXX
-        pass
-
-    def on_badchannelkey(self, c, e):
-        # XXX
-        pass
-
-    def on_namreply(self, c, e):
-        # e.arguments()[0] = "="     (why?)
-        # e.arguments()[1] = channel
-        # e.arguments()[2] = nick list
-
-        flags = ""
-        channel = lower_irc_string(e.arguments()[1])
-        for nick in string.split(e.arguments()[2]):
-            self.channels[channel].add_nick(nick)
-            if nick[0] == "@":
-                self.channels[channel].add_oper(nick[1:])
-            elif nick[0] == "+":
-                self.channels[channel].remove_oper(nick[1:])
-
-    def on_mode(self, c, e):
+    def _on_mode(self, c, e):
+        """[Internal]"""
         modes = parse_channel_modes(string.join(e.arguments()))
-        if is_channel(e.target()):
-            channel = lower_irc_string(e.target())
+        t = e.target()
+        if is_channel(t):
+            ch = self.channels[t]
             for mode in modes:
                 if mode[0] == "+":
-                    self.channels[channel].set_mode(mode[1], mode[2])
+                    f = ch.set_mode
                 else:
-                    self.channels[channel].clear_mode(mode[1])
+                    f = ch.clear_mode
+                f(mode[1], mode[2])
         else:
             # Mode on self... XXX
             pass
 
-    def on_nick(self, c, e):
-        for channel in self.channels.values():
-            if e.source() in channel.users():
-                channel.change_nick(e.source(), e.target())
-        if nick_from_nickmask(e.source()) == self.nickname:
-            self.nickname = e.target()
-    
-    def on_part(self, c, e):
-        if nick_from_nickmask(e.source()) != self.nickname:
-            self.channels[lower_irc_string(e.target())].remove_nick(e.source())
-    
-    def on_privmsg(self, c, e):
-        pass
-    
-    def on_pubmsg(self, c, e):
-        pass
-    
-    def on_quit(self, c, e):
-        for channel in self.channels.values():
-            if e.source() in channel.users():
-                channel.remove_nick(e.source())
+    def _on_namreply(self, c, e):
+        """[Internal]"""
+
+        # e.arguments()[0] == "="     (why?)
+        # e.arguments()[1] == channel
+        # e.arguments()[2] == nick list
+
+        ch = e.arguments()[1]
+        for nick in string.split(e.arguments()[2]):
+            if nick[0] == "@":
+                nick = nick[1:]
+                self.channels[ch].set_mode("o", nick)
+            elif nick[0] == "+":
+                nick = nick[1:]
+                self.channels[ch].set_mode("v", nick)
+            self.channels[ch].add_user(nick)
+
+    def _on_nick(self, c, e):
+        """[Internal]"""
+        before = nm_to_n(e.source())
+        after = e.target()
+        for ch in self.channels.values():
+            if ch.has_user(before):
+                ch.change_nick(before, after)
+        if nm_to_n(before) == self._nickname:
+            self._nickname = after
+
+    def _on_part(self, c, e):
+        """[Internal]"""
+        nick = nm_to_n(e.source())
+        channel = e.target()
+
+        if nick == self._nickname:
+            del self.channels[channel]
+        else:
+            self.channels[channel].remove_user(nick)
+
+    def _on_quit(self, c, e):
+        """[Internal]"""
+        nick = nm_to_n(e.source())
+        for ch in self.channels.values():
+            if ch.has_user(nick):
+                ch.remove_user(nick)
+
+    def die(self, msg="Bye, cruel world!"):
+        """Let the bot die.
+
+        Arguments:
+
+            msg -- Quit message.
+        """
+        self.connection.quit(msg)
+        sys.exit(0)
+
+    def disconnect(self, msg="I'll be back!"):
+        """Disconnect the bot.
+
+        The bot will try to reconnect after a while.
+
+        Arguments:
+
+            msg -- Quit message.
+        """
+        self.connection.quit(msg)
+
+    def get_version(self):
+        """Returns the bot version.
+
+        Used when answering a CTCP VERSION request.
+        """
+        return "ircbot.py by Joel Rosdahl <joel@rosdahl.net>"
+
+    def jump_server(self):
+        """Connect to a new server, possible disconnecting from the current.
+
+        The bot will skip to next server in the server_list each time
+        jump_server is called.
+        """
+        if self.connection.is_connected():
+            self.connection.quit("Jumping servers")
+        self.server_list.append(self.server_list.pop(0))
+        self._connect()
+
+    def on_ctcp(self, c, e):
+        """Default handler for ctcp events.
+
+        Replies to VERSION and PING requests.
+        """
+        if e.arguments()[0] == "VERSION":
+            c.ctcp_reply(nm_to_n(e.source()), self.get_version())
+        elif e.arguments()[0] == "PING":
+            if len(e.arguments()) > 1:
+                c.ctcp_reply(nm_to_n(e.source()),
+                             "PING " + e.arguments()[1])
+
+    def start(self):
+        """Start the bot."""
+        self._connect()
+        SimpleIRCClient.start(self)
+
+
+class IRCDict:
+    """A dictionary suitable for storing IRC-related things.
+
+    Dictionary keys a and b are considered equal if and only if
+    irc_lower(a) == irc_lower(b)
+
+    Otherwise, it should behave exactly as a normal dictionary.
+    """
+
+    def __init__(self, dict=None):
+        self.data = {}
+        self.canon_keys = {}  # Canonical keys
+        if dict is not None:
+            self.update(dict)
+    def __repr__(self):
+        return repr(self.data)
+    def __cmp__(self, dict):
+        if isinstance(dict, IRCDict):
+            return cmp(self.data, dict.data)
+        else:
+            return cmp(self.data, dict)
+    def __len__(self):
+        return len(self.data)
+    def __getitem__(self, key):
+        return self.data[self.canon_keys[irc_lower(key)]]
+    def __setitem__(self, key, item):
+        if self.has_key(key):
+            del self[key]
+        self.data[key] = item
+        self.canon_keys[irc_lower(key)] = key
+    def __delitem__(self, key):
+        ck = irc_lower(key)
+        del self.data[self.canon_keys[ck]]
+        del self.canon_keys[ck]
+    def clear(self):
+        self.data.clear()
+        self.canon_keys.clear()
+    def copy(self):
+        if self.__class__ is UserDict:
+            return UserDict(self.data)
+        import copy
+        return copy.copy(self)
+    def keys(self):
+        return self.data.keys()
+    def items(self):
+        return self.data.items()
+    def values(self):
+        return self.data.values()
+    def has_key(self, key):
+        return self.canon_keys.has_key(irc_lower(key))
+    def update(self, dict):
+        for k, v in dict.items():
+            self.data[k] = v
+    def get(self, key, failobj=None):
+        return self.data.get(key, failobj)
 
 
 class Channel:
-    def __init__(self, name, auto_join=1):
-        self.name = name
-        self.nicks = {}
-        self.opers = {}
-        self.voiced = {}
-        self._auto_join = auto_join
+    """A class for keeping information about an IRC channel.
+
+    This class can be improved a lot.
+    """
+
+    def __init__(self):
+        self.userdict = IRCDict()
+        self.operdict = IRCDict()
+        self.voiceddict = IRCDict()
         self.modes = {}
-        self.joined = 0
-
-    def get_name(self):
-        return self.name
-
-    def is_joined(self):
-        return self.joined
-
-    def set_joined(self):
-        self.joined = 1
-
-    def clear_joined(self):
-        self.joined = 0
-
-    def auto_join(self):
-        return self._auto_join
 
     def users(self):
-        return self.nicks.keys()
+        """Returns an unsorted list of the channel's users."""
+        return self.userdict.keys()
+
+    def opers(self):
+        """Returns an unsorted list of the channel's operators."""
+        return self.operdict.keys()
+
+    def voiced(self):
+        """Returns an unsorted list of the persons that have voice
+        mode set in the channel."""
+        return self.voiceddict.keys()
 
     def has_user(self, nick):
-        return self.nicks.has_keys(nick)
+        """Check whether the channel has a user."""
+        return self.userdict.has_key(nick)
 
     def is_oper(self, nick):
-        return self.opers.has_key(nick)
+        """Check whether a user has operator status in the channel."""
+        return self.operdict.has_key(nick)
 
     def is_voiced(self, nick):
-        return self.voiced.has_key(nick)
+        """Check whether a user has voice mode set in the channel."""
+        return self.voiceddict.has_key(nick)
 
-    def add_nick(self, nick):
-        self.nicks[nick] = 1
+    def add_user(self, nick):
+        self.userdict[nick] = 1
 
-    def remove_nick(self, nick):
-        try:
-            del self.nicks[nick]
-            return 1
-        except KeyError:
-            return 0
-    
-    def add_oper(self, nick):
-        self.opers[nick] = 1
+    def remove_user(self, nick):
+        for d in self.userdict, self.operdict, self.voiceddict:
+            if d.has_key(nick):
+                del d[nick]
 
-    def remove_oper(self, nick):
-        try:
-            del self.opers[nick]
-            return 1
-        except KeyError:
-            return 0
-    
-    def add_voice(self, nick):
-        self.voiced[nick] = 1
-
-    def remove_voice(self, nick):
-        try:
-            del self.voiced[nick]
-            return 1
-        except KeyError:
-            return 0
-    
     def change_nick(self, before, after):
-        self.nicks[after] = self.nicks[before]
-        del self.nicks[before]
+        self.userdict[after] = 1
+        del self.userdict[before]
+        if self.operdict.has_key(before):
+            self.operdict[after] = 1
+            del self.operdict[before]
+        if self.voiceddict.has_key(before):
+            self.voiceddict[after] = 1
+            del self.voiceddict[before]
 
     def set_mode(self, mode, value=None):
-        self.modes[mode] = value
+        """Set mode on the channel.
+
+        Arguments:
+
+            mode -- The mode (a single-character string).
+
+            value -- Value
+        """
+        if mode == "o":
+            self.operdict[value] = 1
+        elif mode == "v":
+            self.voiceddict[value] = 1
+        else:
+            self.modes[mode] = value
 
     def clear_mode(self, mode, value=None):
-        if self.modes.has_key(mode):
-            del self.modes[mode]
+        """Clear mode on the channel.
+
+        Arguments:
+
+            mode -- The mode (a single-character string).
+
+            value -- Value
+        """
+        try:
+            if mode == "o":
+                del self.operdict[value]
+            elif mode == "v":
+                del self.voiceddict[value]
+            else:
+                del self.modes[mode]
+        except KeyError:
+            pass
 
     def has_mode(self, mode):
         return mode in self.modes
+#! /usr/bin/env python
+#
+# Example program using irclib.py.
+#
+# Joel Rosdahl <joel@rosdahl.net>
+
+import irclib
+import string
+import sys
+
+class IRCCat(irclib.SimpleIRCClient):
+    def __init__(self, target):
+        irclib.SimpleIRCClient.__init__(self)
+        self.target = target
+
+    def on_welcome(self, event):
+        if irclib.is_channel(self.target):
+            connection.join(self.target)
+        else:
+            self.send_it()
+
+    def on_join(self, event):
+        self.send_it()
+
+    def on_disconnect(self, event):
+        sys.exit(0)
+
+    def send_it(self):
+        while 1:
+            line = sys.stdin.readline()
+            if not line:
+                break
+            self.connection.privmsg(self.target, line)
+        self.connection.quit("Using irclib.py")
+
+def main():
+    if len(sys.argv) != 4:
+        print "Usage: irccat2 <server[:port]> <nickname> <target>"
+        print "\ntarget is a nickname or a channel."
+        sys.exit(1)
+
+    s = string.split(sys.argv[1], ":", 1)
+    server = s[0]
+    if len(s) == 2:
+        try:
+            port = int(s[1])
+        except ValueError:
+            print "Error: Erroneous port."
+            sys.exit(1)
+    else:
+        port = 6667
+    nickname = sys.argv[2]
+    target = sys.argv[3]
+
+    c = IRCCat(target)
+    try:
+        c.connect(server, port, nickname)
+    except irclib.ServerConnectionError, x:
+        print x
+        sys.exit(1)
+    c.start()
+
+if __name__ == "__main__":
+    main()
-# irclib -- IRC protocol client library
-#
-# Copyright (C) 1999 Joel Rosdahl
+# Copyright (C) 1999, 2000 Joel Rosdahl
 # 
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
 or less must understand the IRC specifications.  They are available
 here: [IRC specifications].
 
+The main features of the IRC client framework are:
+
+  * Abstraction of the IRC protocol.
+  * Handles multiple simultaneous IRC server connections.
+  * Handles server PONGing transparently.
+  * Messages to the IRC server are done by calling methods on an IRC
+    connection object.
+  * Messages from an IRC server triggers events, which can be caught
+    by event handlers.
+  * Reading from and writing to IRC server sockets are normally done
+    by an internal select() loop, but the select()ing may be done by
+    an external main loop.
+  * Functions can be registered to execute at specified times by the
+    event-loop.
+  * Decodes CTCP tagging correctly (hopefully); I haven't seen any
+    other IRC client implementation that handles the CTCP
+    specification subtilties.
+  * A kind of simple, single-server, object-oriented IRC client class
+    that dispatches events to instance methods is included.
+
+Current limitations:
+
+  * The IRC protocol shines through the abstraction a bit too much.
+  * Data is not written asynchronously to the server, i.e. the write()
+    may block if the TCP buffers are stuffed.
+  * There are no support for DCC connections.
+  * The author haven't even read RFC 2810, 2811, 2812 and 2813.
+  * Like most projects, documentation is lacking...
+
 Since I seldom use IRC anymore, I will probably not work much on the
 library.  If you want to help or continue developing the library,
-please contact me.
-
-Joel Rosdahl <joel@rosdahl.net>
+please contact me (Joel Rosdahl <joel@rosdahl.net>).
 
 .. [IRC specifications] http://www.irchelp.org/irchelp/rfc/
 """
 import time
 import types
 
-VERSION = 0, 2, 4
+VERSION = 0, 3, 0
 DEBUG = 0
 
 # TODO
     """Represents an IRC exception."""
     pass
 
+
 class IRC:
     """Class that handles one or several IRC server connections.
 
-    When an IRC object has been instanciated, it can be used to create
+    When an IRC object has been instantiated, it can be used to create
     Connection objects that represent the IRC connections.  The
     responsibility of the IRC object is to provide an event-driven
     framework for the connections and to keep the connections alive.
         self.process_data(i)
         self.process_timeout()
 
-    def process_forever(self):
+    def process_forever(self, timeout=0.2):
         """Run an infinite loop, processing data from connections.
 
-        This method repeatedly calls process_once.  Timeouts will be
-        processed every second.
+        This method repeatedly calls process_once.
+
+        Arguments:
+
+            timeout -- Parameter to pass to process_once.
         """
         while 1:
-            self.process_once(1)
+            self.process_once(timeout)
 
     def disconnect_all(self, message=""):
         """Disconnects all connections."""
                 self.handlers[event].remove(h)
         return 1
 
-    def execute_at(self, at, function, arguments):
+    def execute_at(self, at, function, arguments=()):
         """Execute a function at a specified time.
 
         Arguments:
         """
         self.execute_delayed(at-time.time(), function, arguments)
 
-    def execute_delayed(self, delay, function, arguments):
+    def execute_delayed(self, delay, function, arguments=()):
         """Execute a function after a specified time.
 
         Arguments:
 
     def _handle_event(self, connection, event):
         """[Internal]"""
-        if self.handlers.has_key(event.eventtype()):
-            for handler in self.handlers[event.eventtype()]:
-                if handler[1](connection, event) == "NO MORE":
-                    return
+        h = self.handlers
+        for handler in h.get("all_events", []) + h.get(event.eventtype(), []):
+            if handler[1](connection, event) == "NO MORE":
+                return
 
     def _remove_connection(self, connection):
         """[Internal]"""
         self.connections.remove(connection)
         if self.fn_to_remove_socket:
-            self.fn_to_remove_socket(connection._get_socket()) 
+            self.fn_to_remove_socket(connection._get_socket())
 
-_rfc_1459_command_regexp = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( +(?P<argument>.+))?")
+_rfc_1459_command_regexp = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?")
+
 
 class Connection:
     """Base class for IRC connections.
     def _get_socket():
         raise IRCError, "Not overridden"
 
-    def execute_at(self, at, function, arguments):
+    ##############################
+    ### Convenience wrappers.
+
+    def execute_at(self, at, function, arguments=()):
         self.irclibobj.execute_at(at, function, arguments)
-        
-    def execute_delayed(self, delay, function, arguments):
+
+    def execute_delayed(self, delay, function, arguments=()):
         self.irclibobj.execute_delayed(delay, function, arguments)
 
+
 class ServerConnectionError(IRCError):
     pass
 
+
+# Huh!?  Crrrrazy EFNet doesn't follow the RFC: their ircd seems to
+# use \n as message separator!  :P
+_linesep_regexp = re.compile("\r?\n")
+
 class ServerConnection(Connection):
     """This class represents an IRC server connection.
 
-    ServerConnection objects are instanciated by calling the server
+    ServerConnection objects are instantiated by calling the server
     method on an IRC object.
     """
 
         Connection.__init__(self, irclibobj)
         self.connected = 0  # Not connected yet.
 
-    def connect(self, server, port, nickname, password=None, username=None, ircname=None):
+    def connect(self, server, port, nickname, password=None, username=None,
+                ircname=None):
         """Connect/reconnect to a server.
 
         Arguments:
 
         self.disconnect("Closing object")
         self.irclibobj._remove_connection(self)
-        
+
     def _get_socket(self):
         """[Internal]"""
-        return self.socket
+        if self.connected:
+            return self.socket
+        else:
+            return None
 
     def get_server_name(self):
         """Get the (real) server name.
         if self.real_server_name:
             return self.real_server_name
         else:
-            raise ServerConnectionError, "Not connected yet"
+            return ""
 
-    def get_nick_name(self):
+    def get_nickname(self):
         """Get the (real) nick name.
 
-        This method returns the (real) nick name.  The library keeps
+        This method returns the (real) nickname.  The library keeps
         track of nick changes, so it might not be the nick name that
-        was passed to the connect() method.
-        """
+        was passed to the connect() method.  """
 
         return self.real_nickname
 
             # Read nothing: connection must be down.
             self.disconnect("Connection reset by peer")
             return
-          
-        lines = string.split(self.previous_buffer + new_data, "\r\n")
 
-        # Huh!?  Crrrrazy EFNet doesn't follow the RFC: their ircd
-        # seems to use \n as message separator!  :P
-        efnet_kluge = string.split(self.previous_buffer + new_data, "\n")
-        if len(efnet_kluge) > len(lines):
-            lines = efnet_kluge
+        lines = _linesep_regexp.split(self.previous_buffer + new_data)
 
         # Save the last, unfinished line.
         self.previous_buffer = lines[-1]
         lines = lines[:-1]
 
         for line in lines:
+            if DEBUG:
+                print "FROM SERVER:", line
+
             prefix = None
             command = None
             arguments = None
-            try:
-                self._handle_event(Event("allrawmessages",
-                                         self.get_server_name(),
-                                         None,
-                                         [line]))
-            except ServerConnectionError:
-                pass
+            self._handle_event(Event("all_raw_messages",
+                                     self.get_server_name(),
+                                     None,
+                                     [line]))
 
             m = _rfc_1459_command_regexp.match(line)
             if m.group("prefix"):
                     arguments.append(a[1])
 
             if command == "nick":
-                if nick_from_nickmask(prefix) == self.real_nickname:
+                if nm_to_n(prefix) == self.real_nickname:
                     self.real_nickname = arguments[0]
 
             if command in ["privmsg", "notice"]:
                         self._handle_event(Event(command, prefix, target, [m]))
             else:
                 target = None
-  
+
                 if command == "quit":
                     arguments = [arguments[0]]
                 elif command == "ping":
                 # Translate numerics into more readable strings.
                 if numeric_events.has_key(command):
                     command = numeric_events[command]
-  
+
                 if DEBUG:
                     print "command: %s, source: %s, target: %s, arguments: %s" % (
                         command, prefix, target, arguments)
         try:
             self.socket.send(string + "\r\n")
             if DEBUG:
-                print "SENT TO SERVER:", string
+                print "TO SERVER:", string
         except socket.error, x:
             # Aouch!
             self.disconnect("Connection reset by peer.")
     def version(self, server=""):
         """Send a VERSION command."""
         self.send_raw("VERSION" + (server and (" " + server)))
-        
+
     def wallops(self, text):
         """Send a WALLOPS command."""
         self.send_raw("WALLOPS :" + text)
                                          max and (" " + max),
                                          server and (" " + server)))
 
+
 class DCCConnection(Connection):
     """Unimplemented."""
     def __init__(self):
         raise IRCError, "Unimplemented."
 
+
+class SimpleIRCClient:
+    """A simple single-server IRC client class.
+
+    This is an example of an object-oriented wrapper of the IRC
+    framework.  A real IRC client can be made by subclassing this
+    class and adding appropriate methods.
+
+    The method on_join will be called when a "join" event is created
+    (which is done when the server sends a JOIN messsage/command),
+    on_privmsg will be called for "privmsg" events, and so on.  The
+    handler methods get two arguments: the connection object (same as
+    self.connection) and the event object.
+
+    Instance attributes that can be used by sub classes:
+
+        ircobj -- The IRC instance.
+
+        connection -- The ServerConnection instance.
+    """
+    def __init__(self):
+        self.ircobj = IRC()
+        self.connection = self.ircobj.server()
+        self.ircobj.add_global_handler("all_events", self._dispatcher, -10)
+
+    def _dispatcher(self, c, e):
+        """[Internal]"""
+        m = "on_" + e.eventtype()
+        if hasattr(self, m):
+            getattr(self, m)(c, e)
+
+    def connect(self, server, port, nickname, password=None, username=None,
+                ircname=None):
+        """Connect/reconnect to a server.
+
+        Arguments:
+
+            server -- Server name.
+
+            port -- Port number.
+
+            nickname -- The nickname.
+
+            password -- Password (if any).
+
+            username -- The username.
+
+            ircname -- The IRC name.
+
+        This function can be called to reconnect a closed connection.
+        """
+        self.connection.connect(server, port, nickname,
+                                password, username, ircname)
+
+    def start(self):
+        """Start the IRC client."""
+        self.ircobj.process_forever()
+
+
 class Event:
     """Class representing an IRC event."""
     def __init__(self, eventtype, source, target, arguments=None):
         """Constructor of Event objects.
-        
+
         Arguments:
 
             eventtype -- A string describing the event.
 
     Returns true if the nick matches, otherwise false.
     """
-    nick = lower_irc_string(nick)
-    mask = lower_irc_string(mask)
+    nick = irc_lower(nick)
+    mask = irc_lower(mask)
     mask = string.replace(mask, "\\", "\\\\")
-    for ch in ".$|[](){}?+":
+    for ch in ".$|[](){}+":
         mask = string.replace(mask, ch, "\\" + ch)
     mask = string.replace(mask, "?", ".")
     mask = string.replace(mask, "*", ".*")
     r = re.compile(mask, re.IGNORECASE)
     return r.match(nick)
 
-def lower_irc_string(s):
+_alpha = "abcdefghijklmnopqrstuvxyz"
+_special = "-[]\\`^{}"
+nick_characters = _alpha + string.upper(_alpha) + string.digits + _special
+_ircstring_translation = string.maketrans(string.upper(_alpha) + "[]\\^",
+                                          _alpha + "{}|~")
+
+def irc_lower(s):
     """Returns a lowercased string.
 
     The definition of lowercased comes from the IRC specification (RFC
     1459).
     """
-    s = string.lower(s)
-    s = string.replace(s, "[", "{")
-    s = string.replace(s, "\\", "|")
-    s = string.replace(s, "]", "}")
-    s = string.replace(s, "^", "~")
-    return s
+    return string.translate(s, _ircstring_translation)
 
 def _ctcp_dequote(message):
     """[Internal] Dequote a message according to CTCP specifications.
             # normal message!  (This is according to the CTCP
             # specification.)
             messages.append(_CTCP_DELIMITER + chunks[-1])
-        
+
         return messages
 
 def is_channel(string):
     """
     return string and string[0] in "#&+!"
 
-def nick_from_nickmask(s):
+def nm_to_n(s):
     """Get the nick part of a nickmask.
 
     (The source of an Event is a nickmask.)
     """
     return string.split(s, "!")[0]
 
-def userhost_from_nickmask(s):
+def nm_to_uh(s):
     """Get the userhost part of a nickmask.
 
     (The source of an Event is a nickmask.)
     """
     return string.split(s, "!")[1]
 
-def host_from_nickmask(s):
+def nm_to_h(s):
     """Get the host part of a nickmask.
 
     (The source of an Event is a nickmask.)
     """
     return string.split(s, "@")[1]
 
-def user_from_nickmask(s):
+def nm_to_u(s):
     """Get the user part of a nickmask.
-    
+
     (The source of an Event is a nickmask.)
     """
     s = string.split(s, "!")[1]
     """[Internal]"""
     connection.pong(event.target())
 
-nick_characters = "]ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789\\[-`^{}"
-
 # Numeric table mostly stolen from the Perl IRC module (Net::IRC).
 numeric_events = {
     "001": "welcome",
+#! /usr/bin/env python
+#
+# Example program using ircbot.py.
+#
+# Joel Rosdahl <joel@rosdahl.net>
+
+"""A simple example bot.
+
+This is an example bot that uses the SingleServerIRCBot class from
+ircbot.py.  The bot enters a channel and listens for commands in
+private messages or channel traffic.  Commands in channel messages are
+given by prefixing the text by the bot name followed by a colon.
+
+The known commands are:
+
+    stats -- Prints some channel information.
+
+    disconnect -- Disconnect the bot.  The bot will try to reconnect
+                  after 60 seconds.
+
+    die -- Let the bot cease to exist.
+"""
+
+import string
+from ircbot import SingleServerIRCBot
+from irclib import nm_to_n, irc_lower
+
+class TestBot(SingleServerIRCBot):
+    def __init__(self, channel, nickname, server, port=6667):
+        SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname)
+        self.channel = channel
+        self.start()
+
+    def on_welcome(self, c, e):
+        c.join(self.channel)
+
+    def on_privmsg(self, c, e):
+        self.do_command(nm_to_n(e.source()), e.arguments()[0])
+
+    def on_pubmsg(self, c, e):
+        a = string.split(e.arguments()[0], ":", 1)
+        if len(a) > 1 and irc_lower(a[0]) == irc_lower(self.connection.get_nickname()):
+            self.do_command(nm_to_n(e.source()), string.strip(a[1]))
+        return
+
+    def do_command(self, nick, cmd):
+        c = self.connection
+
+        if cmd == "disconnect":
+            self.disconnect()
+        elif cmd == "die":
+            self.die()
+        elif cmd == "stats":
+            for chname, chobj in self.channels.items():
+                c.notice(nick, "--- Channel statistics ---")
+                c.notice(nick, "Channel: " + chname)
+                users = chobj.users()
+                users.sort()
+                c.notice(nick, "Users: " + string.join(users, ", "))
+                opers = chobj.opers()
+                opers.sort()
+                c.notice(nick, "Opers: " + string.join(opers, ", "))
+                voiced = chobj.voiced()
+                voiced.sort()
+                c.notice(nick, "Voiced: " + string.join(voiced, ", "))
+        else:
+            c.notice(nick, "Not understood: " + cmd)
+
+def main():
+    import sys
+    if len(sys.argv) != 4:
+        print "Usage: testbot <server[:port]> <channel> <nickname>"
+        sys.exit(1)
+
+    s = string.split(sys.argv[1], ":", 1)
+    server = s[0]
+    if len(s) == 2:
+        try:
+            port = int(s[1])
+        except ValueError:
+            print "Error: Erroneous port."
+            sys.exit(1)
+    else:
+        port = 6667
+    channel = sys.argv[2]
+    nickname = sys.argv[3]
+
+    bot = TestBot(channel, nickname, server, port)
+    bot.start()
+
+if __name__ == "__main__":
+    main()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.