--- /dev/null
+# 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