changeset 0:4c842d23d4ce

Initial commit, version 0.1 Signed-off-by: Charly COSTE <changaco@changaco.net>
author Charly COSTE <changaco@changaco.net>
date Sun, 16 Aug 2009 01:47:03 +0200
parents
children 7d1745ca1dc2
files README bot.py bridge.py encoding.py example_config.xml irclib.py muc.py participant.py start_bots_from_xml_config.py
diffstat 9 files changed, 2488 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,12 @@
+This is the first release of xib so some things are not implemented yet, if you want/need an error-safe bot, don't use this one !
+
+Here is the list of the important things that still need to be implemented:
+- handle kicks
+- handle multiple bridges without risk of resource conflict
+
+and a list of features that should be implemented but are not critical for usual use case:
+- split logs into multiple files
+- handle password-protected rooms
+- handle room deletion and muc server reboot
+
+xib includes its own version of the old irclib.py <http://python-irclib.sourceforge.net/> because this library doesn't correctly handle the different character encodings, I'm sorry about it but I don't have time to contribute to the upstream version right now, I don't know if it still has developers anyway
new file mode 100644
--- /dev/null
+++ b/bot.py
@@ -0,0 +1,306 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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 3 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
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+# *** CONTRIBUTORS ***
+# Contributor: Changaco <changaco@changaco.net>
+
+
+# *** Changelog ***
+# 0.1: First release
+
+
+# *** Versioning ***
+# Major will pass to 1 when xib will be considered fault-tolerant
+# After that major will only be changed if the new version is not retro-compatible (e.g. requires changes in config file)
+
+version = 0, 1
+
+
+import irclib
+import xmppony as xmpp
+from threading import Thread
+from bridge import *
+from time import sleep
+import re
+import sys
+
+
+class bot(Thread):
+	
+	def __init__(self, jid, password, nickname, error_fd=sys.stderr, debug=False):
+		Thread.__init__(self)
+		self.jid = xmpp.protocol.JID(jid=jid)
+		self.nickname = nickname
+		self.password = password
+		self.error_fd = error_fd
+		self.debug = debug
+		self.bridges = []
+		self.irc = irclib.IRC()
+		self.irc.add_global_handler('all_events', self._irc_event_handler)
+		self.irc_thread = Thread(target=self.irc.process_forever)
+		self.irc_thread.start()
+		# Open connection with XMPP server
+		try:
+			self.xmpp_c = xmpp.client.Client(self.jid.getDomain(), debug=[])
+			self.xmpp_c.connect()
+			if self.jid.getResource() == '':
+				self.jid.setResource('xib-bot')
+			self.xmpp_c.auth(self.jid.getNode(), password, resource=self.jid.getResource())
+			self.xmpp_c.RegisterHandler('presence', self._xmpp_presence_handler)
+			self.xmpp_c.RegisterHandler('iq', self._xmpp_iq_handler)
+			self.xmpp_c.RegisterHandler('message', self._xmpp_message_handler)
+			self.xmpp_c.sendInitPresence()
+		except:
+			self.error('Error: XMPP Connection failed')
+			raise
+		self.xmpp_thread = Thread(target=self._xmpp_loop)
+		self.xmpp_thread.start()
+	
+	
+	def error(self, s, debug=False):
+		if not debug or debug and self.debug:
+			try:
+				self.error_fd.write(auto_encode(s)+"\n")
+			except EncodingException:
+				self.error_fd.write('Error message cannot be transcoded.\n')
+	
+	
+	def _xmpp_loop(self):
+		while True:
+			self.xmpp_c.Process(5)
+	
+	
+	def _xmpp_presence_handler(self, xmpp_c, presence):
+		"""[Internal] Manage XMPP presence."""
+		self.error('==> Debug: Received XMPP presence.', debug=True)
+		self.error(presence.__str__(fancy=1), debug=True)
+		
+		if presence.getTo() != self.jid:
+			#self.error('=> Debug: Skipping XMPP presence not received on bot connection.', debug=True)
+			return
+		from_ = xmpp.protocol.JID(presence.getFrom())
+		bare_jid = unicode(from_.getNode()+'@'+from_.getDomain())
+		for bridge in self.bridges:
+			if bare_jid == bridge.xmpp_room.room_jid:
+				# presence comes from a muc
+				resource = unicode(from_.getResource())
+				if resource == '':
+					# presence comes from the muc itself
+					# TODO: handle room deletion and muc server reboot
+					pass
+				else:
+					# presence comes from a participant of the muc
+					try:
+						p = bridge.getParticipant(resource)
+						if p.protocol == 'xmpp':
+							if presence.getType() == 'unavailable':
+								x = presence.getTag('x', namespace='http://jabber.org/protocol/muc#user')
+								if x and x.getTag('status', attrs={'code': '303'}):
+									# participant changed its nickname
+									item = x.getTag('item')
+									if not item:
+										self.error('Debug: bad stanza, no item element', debug=True)
+										return
+									new_nick = item.getAttr('nick')
+									if not new_nick:
+										self.error('Debug: bad stanza, new nick is not given', debug=True)
+										return
+									p.changeNickname(new_nick, 'irc')
+									return
+								# participant left
+								bridge.removeParticipant('xmpp', resource, presence.getStatus())
+					except NoSuchParticipantException:
+						try:
+							bridge.addParticipant('xmpp', resource)
+						except Exception:
+							pass
+				return
+	
+	
+	def _xmpp_iq_handler(self, xmpp_c, iq):
+		"""[Internal] Manage XMPP IQs."""
+		self.error('=> Debug: Received XMPP iq.', debug=True)
+		self.error(iq.__str__(fancy=1), debug=True)
+	
+	
+	def _xmpp_message_handler(self, xmpp_c, message):
+		"""[Internal] Manage XMPP messages."""
+		if message.getType() == 'chat':
+			self.error('==> Debug: Received XMPP message.', debug=True)
+			self.error(message.__str__(fancy=1), debug=True)
+			if message.getTo() == self.jid:
+				xmpp_c.send(xmpp.protocol.Message(to=message.getFrom(), body=u'Sorry I am a bot I don\'t speak …', typ='chat'))
+			else:
+				from_bare_jid = unicode(message.getFrom().getNode()+'@'+message.getFrom().getDomain())
+				for bridge in self.bridges:
+					if from_bare_jid == bridge.xmpp_room.room_jid:
+						# message comes from a room participant
+						try:
+							to_ = bridge.getParticipant(message.getTo().getResource())
+							from_ = bridge.getParticipant(message.getFrom().getResource())
+						except NoSuchParticipantException:
+							self.error('==> Debug: XMPP chat message not relayed, from_bare_jid='+from_bare_jid+'  to='+str(message.getTo().getResource())+'  from='+message.getFrom().getResource(), debug=True)
+							return
+						if from_.protocol in ['xmpp', 'both']:
+							from_.sayOnIRCTo(to_.nickname, message.getBody())
+						else:
+							self.error('==> Debug: received XMPP chat message from a non-XMPP participant, WTF ?', debug=True)
+		
+		elif message.getType() == 'groupchat':
+			# message comes from a room
+			if message.getTo() != self.jid:
+				self.error('=> Debug: Skipping XMPP MUC message not received on bot connection.', debug=True)
+				return
+			for child in message.getChildren():
+				if child.getName() == 'delay':
+					self.error('=> Debug: Skipping XMPP MUC delayed message.', debug=True)
+					return
+			self.error('==> Debug: Received XMPP message.', debug=True)
+			self.error(message.__str__(fancy=1), debug=True)
+			from_ = xmpp.protocol.JID(message.getFrom())
+			room_jid = unicode(from_.getNode()+'@'+from_.getDomain())
+			for bridge in self.bridges:
+				if room_jid == bridge.xmpp_room.room_jid:
+					resource = unicode(from_.getResource())
+					if resource == '':
+						# message comes from the room itself
+						pass
+					else:
+						# message comes from a participant of the room
+						try:
+							participant_ = bridge.getParticipant(resource)
+						except NoSuchParticipantException:
+							return
+						if participant_.protocol == 'xmpp':
+							participant_.sayOnIRC(message.getBody())
+						elif participant_.protocol == 'both':
+							bridge.irc_connection.privmsg(bridge.irc_room, '<'+participant_.nickname+'> '+message.getBody())
+		else:
+			self.error('==> Debug: Received XMPP message.', debug=True)
+			self.error(message.__str__(fancy=1), debug=True)
+	
+	
+	def _irc_event_handler(self, connection, event):
+		"""[internal] Manage IRC events"""
+		if not connection.bridge in self.bridges:
+			# Not for us, ignore
+			return
+		if 'all' in event.eventtype():
+			return
+		if 'motd' in event.eventtype():
+			self.error('=> Debug: ignoring event containing "motd" in the eventtype ('+event.eventtype()+')', debug=True)
+			return
+		if event.eventtype() in ['pong', 'welcome', 'yourhost', 'created', 'myinfo', 'featurelist', 'luserclient', 'luserop', 'luserchannels', 'luserme', 'n_local', 'n_global', 'endofnames', 'luserunknown']:
+			self.error('=> Debug: ignoring '+event.eventtype(), debug=True)
+			return
+		if event.eventtype() == 'pubmsg' and connection.get_nickname() != connection.bridge.irc_connection.get_nickname():
+			self.error('=> Debug: ignoring IRC pubmsg not received on bridge connection', debug=True)
+			return
+		if event.eventtype() == 'ping':
+			connection.pong(connection.get_server_name())
+			return
+		self.error('==> Debug: Received IRC event.', debug=True)
+		self.error('server='+connection.get_server_name(), debug=True)
+		self.error('eventtype='+event.eventtype(), debug=True)
+		self.error('source='+str(event.source()), debug=True)
+		self.error('target='+str(event.target()), debug=True)
+		self.error('arguments='+str(event.arguments()), debug=True)
+		if event.eventtype() == 'disconnect':
+			if connection.get_nickname() == connection.bridge.irc_connection.get_nickname():
+				# Lost bridge IRC connection, we must reconnect if we want the bridge to work
+				self.recreate_bridge(connection.bridge)
+				return
+			if connection.bridge.mode == 'normal' and event.arguments()[0] != 'Closing object':
+				connection.bridge.switchToLimitedMode()
+		elif event.eventtype() == 'nicknameinuse':
+			if connection.nick_callback:
+				connection.nick_callback('nicknameinuse')
+			else:
+				self.error('=> Debug: no nick callback for "'+str(event.target())+'"', debug=True)
+			return
+		elif event.eventtype() == 'erroneusnickname':
+			if connection.nick_callback:
+				connection.nick_callback('erroneusnickname')
+			else:
+				self.error('=> Debug: no nick callback for "'+str(event.target())+'"', debug=True)
+			return
+		elif event.eventtype() == 'umode':
+			if connection.nick_callback:
+				connection.nick_callback(None)
+			else:
+				self.error('=> Debug: no nick callback for "'+str(event.target())+'"', debug=True)
+				self.error('connection.nick_callback='+str(connection.nick_callback), debug=True)
+			return
+		elif event.eventtype() == 'namreply':
+			for nickname in re.split(' [@]?', event.arguments()[2].strip()):
+				try:
+					connection.bridge.addParticipant('irc', nickname)
+				except:
+					pass
+			return
+		elif event.eventtype() == 'join':
+			nickname = event.source().split('!')[0]
+			if nickname == self.nickname:
+				pass
+			else:
+				try:
+					connection.bridge.getParticipant(nickname)
+				except NoSuchParticipantException:
+					connection.bridge.addParticipant('irc', nickname)
+		try:
+			from_ = connection.bridge.getParticipant(event.source().split('!')[0])
+			if event.eventtype() == 'quit':
+				if from_.protocol == 'irc':
+					connection.bridge.removeParticipant('irc', from_.nickname, event.arguments()[0])
+				return
+		except NoSuchParticipantException:
+			return
+		except AttributeError:
+			pass
+		if event.eventtype() == 'pubmsg':
+			if from_.protocol == 'irc' or from_.protocol == 'both':
+				from_.sayOnXMPP(event.arguments()[0])
+		elif event.eventtype() == 'privmsg':
+			if event.target() == None:
+				return
+			try:
+				to_ = connection.bridge.getParticipant(event.target().split('!')[0])
+			except NoSuchParticipantException:
+				if event.target().split('!')[0] == self.nickname:
+					connection.privmsg(from_.nickname, u'Sorry I am a bot I don\'t speak …')
+				return
+			if to_.protocol == 'xmpp':
+				from_.sayOnXMPPTo(to_.nickname, event.arguments()[0])
+	
+	
+	def new_bridge(self, xmpp_room, irc_room, irc_server, irc_port=6667):
+		"""Create a bridge between xmpp_room and irc_room at irc_server."""
+		b = bridge(self, xmpp_room, irc_room, irc_server, irc_port=irc_port)
+		self.bridges.append(b)
+		return b
+	
+	
+	def recreate_bridge(self, bridge):
+		"""Disconnect and reconnect."""
+		self.new_bridge(bridge.xmpp_room.room_jid, bridge.irc_room, bridge.irc_server)
+		self.bridges.remove(bridge)
+		del bridge
+	
+	
+	def __del__(self):
+		for bridge in bridges:
+			del bridge
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/bridge.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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 3 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
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import muc
+xmpp = muc.xmpp
+del muc
+from participant import *
+from encoding import *
+
+
+class NoSuchParticipantException(Exception): pass
+
+
+class bridge:
+	def __init__(self, owner_bot, xmpp_room_jid, irc_room, irc_server, irc_port=6667, mode='normal'):
+		self.bot = owner_bot
+		self.irc_server = irc_server
+		self.irc_port = irc_port
+		self.irc_room = irc_room
+		self.participants = []
+		self.mode = mode
+		
+		# Join IRC room
+		self.irc_connection = self.bot.irc.server()
+		self.irc_connection.nick_callback = self._irc_nick_callback
+		self.irc_connection.bridge = self
+		try:
+			self.irc_connection.connect(irc_server, irc_port, self.bot.nickname)
+		except:
+			self.bot.error('Error: joining IRC room failed')
+			raise
+		
+		# Join XMPP room
+		try:
+			self.xmpp_room = xmpp.muc(xmpp_room_jid)
+			self.xmpp_room.join(self.bot.xmpp_c, self.bot.nickname)
+		except:
+			self.bot.error('Error: joining XMPP room failed')
+			raise
+	
+	
+	def _irc_nick_callback(self, error):
+		if error == None:
+			self.irc_connection.join(self.irc_room)
+			self.irc_connection.nick_callback = None
+			self.bot.error('===> Debug: successfully connected on IRC side of bridge "'+str(self)+'"', debug=True)
+		elif self.protocol != 'both':
+			if error == 'nicknameinuse':
+				self.bot.error('Error: "'+self.bot.nickname+'" is already used in the IRC chan of bridge "'+str(self)+'"')
+				raise Exception('Error: "'+self.bot.nickname+'" is already used in the IRC chan of bridge "'+str(self)+'"')
+			elif error == 'erroneusnickname':
+				self.bot.error('Error: "'+self.bot.nickname+'" got "erroneusnickname" on bridge "'+str(self)+'"')
+				raise Exception('Error: "'+self.bot.nickname+'" got "erroneusnickname" on bridge "'+str(self)+'"')
+	
+	
+	def addParticipant(self, protocol, nickname):
+		"""Add a participant to the bridge."""
+		if (protocol == 'irc' and nickname == self.irc_connection.get_nickname()) or (protocol == 'xmpp' and nickname == self.xmpp_room.nickname):
+			raise Exception('cannot add self')
+		try:
+			p = self.getParticipant(nickname)
+			if p.protocol != protocol:
+				if protocol == 'irc':
+					p.createDuplicateOnXMPP()
+				elif protocol == 'xmpp':
+					p.createDuplicateOnIRC()
+				else:
+					raise Exception('Internal Error: bad protocol')
+			return
+		except NoSuchParticipantException:
+			pass
+		self.bot.error('===> Debug: adding participant "'+nickname+'" from "'+protocol+'" to bridge "'+str(self)+'"', debug=True)
+		p = participant(self, protocol, nickname)
+		self.participants.append(p)
+		return p
+	
+	
+	def getParticipant(self, nickname):
+		"""Returns a participant object if there is a participant using nickname in the bridge. Raises a NoSuchParticipantException otherwise."""
+		for participant_ in self.participants:
+			if participant_.nickname == nickname:
+				return participant_
+		raise NoSuchParticipantException('there is no participant using the nickname "'+nickname+'" in this bridge')
+	
+	
+	def removeParticipant(self, protocol, nickname, leave_message):
+		"""Remove the participant using nickname from the bridge. Raises a NoSuchParticipantException if nickname is not used in the bridge."""
+		p = self.getParticipant(nickname)
+		if p.protocol == 'both':
+			self.bot.error('===> Debug: "'+nickname+'" was on both sides of bridge "'+str(self)+'" but left '+protocol, debug=True)
+			if protocol == 'xmpp':
+				p.createDuplicateOnIRC()
+			elif protocol == 'irc':
+				p.createDuplicateOnXMPP()
+			else:
+				raise Exception('Internal Error: bad protocol')
+		else:
+			self.bot.error('===> Debug: removing participant "'+nickname+'" from bridge "'+str(self)+'"', debug=True)
+			self.participants.remove(p)
+			p.leave(leave_message)
+			i = 0
+			for p in self.participants:
+				if p.protocol == 'irc':
+					i += 1
+			if protocol == 'xmpp' and self.irc_connections_limit >= i:
+				self.switchToNormalMode()
+			del p
+	
+	
+	def say(self, message):
+		self.xmpp_room.say(message)
+		self.irc_connection.privmsg(self.irc_room, auto_encode(message))
+	
+	
+	def switchToNormalMode(self):
+		if self.mode == 'normal':
+			return
+		prev_mode = self.mode
+		self.mode = 'normal'
+		for p in self.participants:
+			if p.protocol == 'xmpp':
+				p.createDuplicateOnIRC()
+			elif p.protocol == 'irc' and prev_mode == 'minimal':
+				p.createDuplicateOnXMPP()
+		self.bot.error('===> Bridge is switching to normal mode.')
+		self.say('[Notice] Bridge is switching to normal mode.')
+	
+	
+	def switchToLimitedMode(self):
+		if self.mode == 'limited':
+			return
+		self.mode = 'limited'
+		i = 0
+		for p in self.participants:
+			if p.protocol == 'xmpp':
+				i += 1
+				if p.irc_connection:
+					p.irc_connection.disconnect('Bridge is switching to limited mode')
+					p.irc_connection.close()
+					p.irc_connection = None
+		self.irc_connections_limit = i
+		self.bot.error('===> Bridge is switching to limited mode.')
+		self.say('[Warning] Bridge is switching to limited mode, it means that it will be transparent for XMPP users but not for IRC users, this is due to the IRC servers\' per-IP-address connections\' limit number.')
+	
+	
+	def __str__(self):
+		return self.irc_room+'@'+self.irc_server+' <-> '+self.xmpp_room.room_jid
+	
+	
+	def __del__(self):
+		# Delete participants objects
+		for p in self.participants:
+			p.leave('Removing bridge')
+			del p
+		# Leave IRC room
+		self.irc_connection.quit('Removing bridge')
+		# Close IRC connection
+		self.irc_connection.close()
+		del self.irc_connection
+		# Leave XMPP room
+		self.xmpp_room.leave('Removing bridge')
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/encoding.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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 3 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
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+class EncodingException(Exception): pass
+
+
+def auto_encode(s):
+	for codec in ['utf-8', 'iso8859_15']:
+		try:
+			return s.encode(codec)
+		except (UnicodeEncodeError, UnicodeDecodeError) as e:
+			print e
+			pass
+	raise EncodingException('no suitable codec found')
+
+
+def auto_decode(s):
+	for codec in ['utf-8', 'iso8859_15']:
+		try:
+			return s.decode(codec)
+		except (UnicodeEncodeError, UnicodeDecodeError) as e:
+			print e
+			pass
+	raise EncodingException('no suitable codec found')
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/example_config.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" ?>
+<config>
+	<bot jid='some_bot@example.net' password='do not forget to escape xml entities like &amp;' nickname='xib-bot-nickname' debug='true'>
+		<bridge>
+			<xmpp-room jid='dream-world@chat.example.com'/>
+			<irc chan='#dream-world' server='irc.example.org'/>
+		</bridge>
+		<!-- WARNING: multiple bridges is not currently well-implemented, start multiple bots instead -->
+	</bot>
+	<!-- WARNING: do NOT start two bots with the same JID -->
+</config>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/irclib.py
@@ -0,0 +1,1561 @@
+# Copyright (C) 1999--2002  Joel Rosdahl
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+# keltus <keltus@users.sourceforge.net>
+#
+# $Id: irclib.py,v 1.47 2008/09/25 22:00:59 keltus Exp $
+
+"""irclib -- Internet Relay Chat (IRC) protocol client library.
+
+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, CTCP, DCC chat,
+but DCC file transfers is not yet supported.
+
+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
+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 file transfers.
+  * The author haven't even read RFC 2810, 2811, 2812 and 2813.
+  * Like most projects, documentation is lacking...
+
+.. [IRC specifications] http://www.irchelp.org/irchelp/rfc/
+"""
+
+import bisect
+import re
+import select
+import socket
+import string
+import sys
+import time
+import types
+
+VERSION = 0, 4, 8
+DEBUG = 0
+
+# TODO
+# ----
+# (maybe) thread safety
+# (maybe) color parser convenience functions
+# documentation (including all event types)
+# (maybe) add awareness of different types of ircds
+# send data asynchronously to the server (and DCC connections)
+# (maybe) automatically close unused, passive DCC connections after a while
+
+# NOTES
+# -----
+# connection.quit() only sends QUIT to the server.
+# ERROR from the server triggers the error event and the disconnect event.
+# dropping of the connection triggers the disconnect event.
+
+class IRCError(Exception):
+    """Represents an IRC exception."""
+    pass
+
+
+class IRC:
+    """Class that handles one or several IRC server connections.
+
+    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.
+    It runs a select loop to poll each connection's TCP socket and
+    hands over the sockets with incoming data for processing by the
+    corresponding connection.
+
+    The methods of most interest for an IRC client writer are server,
+    add_global_handler, remove_global_handler, execute_at,
+    execute_delayed, process_once and process_forever.
+
+    Here is an example:
+
+        irc = irclib.IRC()
+        server = irc.server()
+        server.connect(\"irc.some.where\", 6667, \"my_nickname\")
+        server.privmsg(\"a_nickname\", \"Hi there!\")
+        irc.process_forever()
+
+    This will connect to the IRC server irc.some.where on port 6667
+    using the nickname my_nickname and send the message \"Hi there!\"
+    to the nickname a_nickname.
+    """
+
+    def __init__(self, fn_to_add_socket=None,
+                 fn_to_remove_socket=None,
+                 fn_to_add_timeout=None):
+        """Constructor for IRC objects.
+
+        Optional arguments are fn_to_add_socket, fn_to_remove_socket
+        and fn_to_add_timeout.  The first two specify functions that
+        will be called with a socket object as argument when the IRC
+        object wants to be notified (or stop being notified) of data
+        coming on a new socket.  When new data arrives, the method
+        process_data should be called.  Similarly, fn_to_add_timeout
+        is called with a number of seconds (a floating point number)
+        as first argument when the IRC object wants to receive a
+        notification (by calling the process_timeout method).  So, if
+        e.g. the argument is 42.17, the object wants the
+        process_timeout method to be called after 42 seconds and 170
+        milliseconds.
+
+        The three arguments mainly exist to be able to use an external
+        main loop (for example Tkinter's or PyGTK's main app loop)
+        instead of calling the process_forever method.
+
+        An alternative is to just call ServerConnection.process_once()
+        once in a while.
+        """
+
+        if fn_to_add_socket and fn_to_remove_socket:
+            self.fn_to_add_socket = fn_to_add_socket
+            self.fn_to_remove_socket = fn_to_remove_socket
+        else:
+            self.fn_to_add_socket = None
+            self.fn_to_remove_socket = None
+
+        self.fn_to_add_timeout = fn_to_add_timeout
+        self.connections = []
+        self.handlers = {}
+        self.delayed_commands = [] # list of tuples in the format (time, function, arguments)
+
+        self.add_global_handler("ping", _ping_ponger, -42)
+
+    def server(self):
+        """Creates and returns a ServerConnection object."""
+
+        c = ServerConnection(self)
+        self.connections.append(c)
+        return c
+
+    def process_data(self, sockets):
+        """Called when there is more data to read on connection sockets.
+
+        Arguments:
+
+            sockets -- A list of socket objects.
+
+        See documentation for IRC.__init__.
+        """
+        for s in sockets:
+            for c in self.connections:
+                if s == c._get_socket():
+                    c.process_data()
+
+    def process_timeout(self):
+        """Called when a timeout notification is due.
+
+        See documentation for IRC.__init__.
+        """
+        t = time.time()
+        while self.delayed_commands:
+            if t >= self.delayed_commands[0][0]:
+                self.delayed_commands[0][1](*self.delayed_commands[0][2])
+                del self.delayed_commands[0]
+            else:
+                break
+
+    def process_once(self, timeout=0):
+        """Process data from connections once.
+
+        Arguments:
+
+            timeout -- How long the select() call should wait if no
+                       data is available.
+
+        This method should be called periodically to check and process
+        incoming data, if there are any.  If that seems boring, look
+        at the process_forever method.
+        """
+        sockets = map(lambda x: x._get_socket(), self.connections)
+        sockets = filter(lambda x: x != None, sockets)
+        if sockets:
+            (i, o, e) = select.select(sockets, [], [], timeout)
+            self.process_data(i)
+        else:
+            time.sleep(timeout)
+        self.process_timeout()
+
+    def process_forever(self, timeout=0.2):
+        """Run an infinite loop, processing data from connections.
+
+        This method repeatedly calls process_once.
+
+        Arguments:
+
+            timeout -- Parameter to pass to process_once.
+        """
+        while 1:
+            self.process_once(timeout)
+
+    def disconnect_all(self, message=""):
+        """Disconnects all connections."""
+        for c in self.connections:
+            c.disconnect(message)
+
+    def add_global_handler(self, event, handler, priority=0):
+        """Adds a global handler function for a specific event type.
+
+        Arguments:
+
+            event -- Event type (a string).  Check the values of the
+            numeric_events dictionary in irclib.py for possible event
+            types.
+
+            handler -- Callback function.
+
+            priority -- A number (the lower number, the higher priority).
+
+        The handler function is called whenever the specified event is
+        triggered in any of the connections.  See documentation for
+        the Event class.
+
+        The handler functions are called in priority order (lowest
+        number is highest priority).  If a handler function returns
+        \"NO MORE\", no more handlers will be called.
+        """
+        if not event in self.handlers:
+            self.handlers[event] = []
+        bisect.insort(self.handlers[event], ((priority, handler)))
+
+    def remove_global_handler(self, event, handler):
+        """Removes a global handler function.
+
+        Arguments:
+
+            event -- Event type (a string).
+
+            handler -- Callback function.
+
+        Returns 1 on success, otherwise 0.
+        """
+        if not event in self.handlers:
+            return 0
+        for h in self.handlers[event]:
+            if handler == h[1]:
+                self.handlers[event].remove(h)
+        return 1
+
+    def execute_at(self, at, function, arguments=()):
+        """Execute a function at a specified time.
+
+        Arguments:
+
+            at -- Execute at this time (standard \"time_t\" time).
+
+            function -- Function to call.
+
+            arguments -- Arguments to give the function.
+        """
+        self.execute_delayed(at-time.time(), function, arguments)
+
+    def execute_delayed(self, delay, function, arguments=()):
+        """Execute a function after a specified time.
+
+        Arguments:
+
+            delay -- How many seconds to wait.
+
+            function -- Function to call.
+
+            arguments -- Arguments to give the function.
+        """
+        bisect.insort(self.delayed_commands, (delay+time.time(), function, arguments))
+        if self.fn_to_add_timeout:
+            self.fn_to_add_timeout(delay)
+
+    def dcc(self, dcctype="chat"):
+        """Creates and returns a DCCConnection object.
+
+        Arguments:
+
+            dcctype -- "chat" for DCC CHAT connections or "raw" for
+                       DCC SEND (or other DCC types). If "chat",
+                       incoming data will be split in newline-separated
+                       chunks. If "raw", incoming data is not touched.
+        """
+        c = DCCConnection(self, dcctype)
+        self.connections.append(c)
+        return c
+
+    def _handle_event(self, connection, event):
+        """[Internal]"""
+        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())
+
+_rfc_1459_command_regexp = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?")
+
+class Connection:
+    """Base class for IRC connections.
+
+    Must be overridden.
+    """
+    def __init__(self, irclibobj):
+        self.irclibobj = irclibobj
+
+    def _get_socket():
+        raise IRCError, "Not overridden"
+
+    ##############################
+    ### Convenience wrappers.
+
+    def execute_at(self, at, function, arguments=()):
+        self.irclibobj.execute_at(at, function, arguments)
+
+    def execute_delayed(self, delay, function, arguments=()):
+        self.irclibobj.execute_delayed(delay, function, arguments)
+
+
+class ServerConnectionError(IRCError):
+    pass
+
+class ServerNotConnectedError(ServerConnectionError):
+    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 instantiated by calling the server
+    method on an IRC object.
+    """
+
+    def __init__(self, irclibobj):
+        Connection.__init__(self, irclibobj)
+        self.connected = 0  # Not connected yet.
+        self.socket = None
+        self.ssl = None
+
+    def connect(self, server, port, nickname, password=None, username=None,
+                ircname=None, localaddress="", localport=0, ssl=False, ipv6=False):
+        """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 ("realname").
+
+            localaddress -- Bind the connection to a specific local IP address.
+
+            localport -- Bind the connection to a specific local port.
+
+            ssl -- Enable support for ssl.
+
+            ipv6 -- Enable support for ipv6.
+
+        This function can be called to reconnect a closed connection.
+
+        Returns the ServerConnection object.
+        """
+        if self.connected:
+            self.disconnect("Changing servers")
+
+        self.previous_buffer = ""
+        self.handlers = {}
+        self.real_server_name = ""
+        self.real_nickname = nickname
+        self.server = server
+        self.port = port
+        self.nickname = nickname
+        self.username = username or nickname
+        self.ircname = ircname or nickname
+        self.password = password
+        self.localaddress = localaddress
+        self.localport = localport
+        self.localhost = socket.gethostname()
+        if ipv6:
+            self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+        else:
+            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            self.socket.bind((self.localaddress, self.localport))
+            self.socket.connect((self.server, self.port))
+            if ssl:
+                self.ssl = socket.ssl(self.socket)
+        except socket.error, x:
+            self.socket.close()
+            self.socket = None
+            raise ServerConnectionError, "Couldn't connect to socket: %s" % x
+        self.connected = 1
+        if self.irclibobj.fn_to_add_socket:
+            self.irclibobj.fn_to_add_socket(self.socket)
+
+        # Log on...
+        if self.password:
+            self.pass_(self.password)
+        self.nick(self.nickname)
+        self.user(self.username, self.ircname)
+        return self
+
+    def close(self):
+        """Close the connection.
+
+        This method closes the connection permanently; after it has
+        been called, the object is unusable.
+        """
+
+        self.disconnect("Closing object")
+        self.irclibobj._remove_connection(self)
+
+    def _get_socket(self):
+        """[Internal]"""
+        return self.socket
+
+    def get_server_name(self):
+        """Get the (real) server name.
+
+        This method returns the (real) server name, or, more
+        specifically, what the server calls itself.
+        """
+
+        if self.real_server_name:
+            return self.real_server_name
+        else:
+            return ""
+
+    def get_nickname(self):
+        """Get the (real) nick name.
+
+        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.  """
+
+        return self.real_nickname
+
+    def process_data(self):
+        """[Internal]"""
+
+        try:
+            if self.ssl:
+                new_data = self.ssl.read(2**14)
+            else:
+                new_data = self.socket.recv(2**14)
+        except socket.error, x:
+            # The server hung up.
+            self.disconnect("Connection reset by peer")
+            return
+        if not new_data:
+            # Read nothing: connection must be down.
+            self.disconnect("Connection reset by peer")
+            return
+
+        lines = _linesep_regexp.split(self.previous_buffer + new_data)
+
+        # Save the last, unfinished line.
+        self.previous_buffer = lines.pop()
+
+        for line in lines:
+            if DEBUG:
+                print "FROM SERVER:", line
+
+            if not line:
+                continue
+
+            prefix = None
+            command = None
+            arguments = None
+            self._handle_event(Event("all_raw_messages",
+                                     self.get_server_name(),
+                                     None,
+                                     [line]))
+
+            m = _rfc_1459_command_regexp.match(line)
+            if m.group("prefix"):
+                prefix = m.group("prefix")
+                if not self.real_server_name:
+                    self.real_server_name = prefix
+
+            if m.group("command"):
+                command = m.group("command").lower()
+
+            if m.group("argument"):
+                a = m.group("argument").split(" :", 1)
+                arguments = a[0].split()
+                if len(a) == 2:
+                    arguments.append(a[1])
+
+            # Translate numerics into more readable strings.
+            if command in numeric_events:
+                command = numeric_events[command]
+
+            if command == "nick":
+                if nm_to_n(prefix) == self.real_nickname:
+                    self.real_nickname = arguments[0]
+            elif command == "welcome":
+                # Record the nickname in case the client changed nick
+                # in a nicknameinuse callback.
+                self.real_nickname = arguments[0]
+
+            if command in ["privmsg", "notice"]:
+                target, message = arguments[0], arguments[1]
+                messages = _ctcp_dequote(message)
+
+                if command == "privmsg":
+                    if is_channel(target):
+                        command = "pubmsg"
+                else:
+                    if is_channel(target):
+                        command = "pubnotice"
+                    else:
+                        command = "privnotice"
+
+                for m in messages:
+                    if type(m) is types.TupleType:
+                        if command in ["privmsg", "pubmsg"]:
+                            command = "ctcp"
+                        else:
+                            command = "ctcpreply"
+
+                        m = list(m)
+                        if DEBUG:
+                            print "command: %s, source: %s, target: %s, arguments: %s" % (
+                                command, prefix, target, m)
+                        self._handle_event(Event(command, prefix, target, m))
+                        if command == "ctcp" and m[0] == "ACTION":
+                            self._handle_event(Event("action", prefix, target, m[1:]))
+                    else:
+                        if DEBUG:
+                            print "command: %s, source: %s, target: %s, arguments: %s" % (
+                                command, prefix, target, [m])
+                        self._handle_event(Event(command, prefix, target, [m]))
+            else:
+                target = None
+
+                if command == "quit":
+                    arguments = [arguments[0]]
+                elif command == "ping":
+                    target = arguments[0]
+                else:
+                    target = arguments[0]
+                    arguments = arguments[1:]
+
+                if command == "mode":
+                    if not is_channel(target):
+                        command = "umode"
+
+                if DEBUG:
+                    print "command: %s, source: %s, target: %s, arguments: %s" % (
+                        command, prefix, target, arguments)
+                self._handle_event(Event(command, prefix, target, arguments))
+
+    def _handle_event(self, event):
+        """[Internal]"""
+        self.irclibobj._handle_event(self, event)
+        if event.eventtype() in self.handlers:
+            for fn in self.handlers[event.eventtype()]:
+                fn(self, event)
+
+    def is_connected(self):
+        """Return connection status.
+
+        Returns true if connected, otherwise false.
+        """
+        return self.connected
+
+    def add_global_handler(self, *args):
+        """Add global handler.
+
+        See documentation for IRC.add_global_handler.
+        """
+        self.irclibobj.add_global_handler(*args)
+
+    def remove_global_handler(self, *args):
+        """Remove global handler.
+
+        See documentation for IRC.remove_global_handler.
+        """
+        self.irclibobj.remove_global_handler(*args)
+
+    def action(self, target, action):
+        """Send a CTCP ACTION command."""
+        self.ctcp("ACTION", target, action)
+
+    def admin(self, server=""):
+        """Send an ADMIN command."""
+        self.send_raw(" ".join(["ADMIN", server]).strip())
+
+    def ctcp(self, ctcptype, target, parameter=""):
+        """Send a CTCP command."""
+        ctcptype = ctcptype.upper()
+        self.privmsg(target, "\001%s%s\001" % (ctcptype, parameter and (" " + parameter) or ""))
+
+    def ctcp_reply(self, target, parameter):
+        """Send a CTCP REPLY command."""
+        self.notice(target, "\001%s\001" % parameter)
+
+    def disconnect(self, message=""):
+        """Hang up the connection.
+
+        Arguments:
+
+            message -- Quit message.
+        """
+        if not self.connected:
+            return
+
+        self.connected = 0
+
+        self.quit(message)
+
+        try:
+            self.socket.close()
+        except socket.error, x:
+            pass
+        self.socket = None
+        self._handle_event(Event("disconnect", self.server, "", [message]))
+
+    def globops(self, text):
+        """Send a GLOBOPS command."""
+        self.send_raw("GLOBOPS :" + text)
+
+    def info(self, server=""):
+        """Send an INFO command."""
+        self.send_raw(" ".join(["INFO", server]).strip())
+
+    def invite(self, nick, channel):
+        """Send an INVITE command."""
+        self.send_raw(" ".join(["INVITE", nick, channel]).strip())
+
+    def ison(self, nicks):
+        """Send an ISON command.
+
+        Arguments:
+
+            nicks -- List of nicks.
+        """
+        self.send_raw("ISON " + " ".join(nicks))
+
+    def join(self, channel, key=""):
+        """Send a JOIN command."""
+        self.send_raw("JOIN %s%s" % (channel, (key and (" " + key))))
+
+    def kick(self, channel, nick, comment=""):
+        """Send a KICK command."""
+        self.send_raw("KICK %s %s%s" % (channel, nick, (comment and (" :" + comment))))
+
+    def links(self, remote_server="", server_mask=""):
+        """Send a LINKS command."""
+        command = "LINKS"
+        if remote_server:
+            command = command + " " + remote_server
+        if server_mask:
+            command = command + " " + server_mask
+        self.send_raw(command)
+
+    def list(self, channels=None, server=""):
+        """Send a LIST command."""
+        command = "LIST"
+        if channels:
+            command = command + " " + ",".join(channels)
+        if server:
+            command = command + " " + server
+        self.send_raw(command)
+
+    def lusers(self, server=""):
+        """Send a LUSERS command."""
+        self.send_raw("LUSERS" + (server and (" " + server)))
+
+    def mode(self, target, command):
+        """Send a MODE command."""
+        self.send_raw("MODE %s %s" % (target, command))
+
+    def motd(self, server=""):
+        """Send an MOTD command."""
+        self.send_raw("MOTD" + (server and (" " + server)))
+
+    def names(self, channels=None):
+        """Send a NAMES command."""
+        self.send_raw("NAMES" + (channels and (" " + ",".join(channels)) or ""))
+
+    def nick(self, newnick):
+        """Send a NICK command."""
+        self.send_raw("NICK " + newnick)
+
+    def notice(self, target, text):
+        """Send a NOTICE command."""
+        # Should limit len(text) here!
+        self.send_raw("NOTICE %s :%s" % (target, text))
+
+    def oper(self, nick, password):
+        """Send an OPER command."""
+        self.send_raw("OPER %s %s" % (nick, password))
+
+    def part(self, channels, message=""):
+        """Send a PART command."""
+        if type(channels) == types.StringType:
+            self.send_raw("PART " + channels + (message and (" " + message)))
+        else:
+            self.send_raw("PART " + ",".join(channels) + (message and (" " + message)))
+
+    def pass_(self, password):
+        """Send a PASS command."""
+        self.send_raw("PASS " + password)
+
+    def ping(self, target, target2=""):
+        """Send a PING command."""
+        self.send_raw("PING %s%s" % (target, target2 and (" " + target2)))
+
+    def pong(self, target, target2=""):
+        """Send a PONG command."""
+        self.send_raw("PONG %s%s" % (target, target2 and (" " + target2)))
+
+    def privmsg(self, target, text):
+        """Send a PRIVMSG command."""
+        # Should limit len(text) here!
+        self.send_raw("PRIVMSG %s :%s" % (target, text))
+
+    def privmsg_many(self, targets, text):
+        """Send a PRIVMSG command to multiple targets."""
+        # Should limit len(text) here!
+        self.send_raw("PRIVMSG %s :%s" % (",".join(targets), text))
+
+    def quit(self, message=""):
+        """Send a QUIT command."""
+        # Note that many IRC servers don't use your QUIT message
+        # unless you've been connected for at least 5 minutes!
+        self.send_raw("QUIT" + (message and (" :" + message)))
+
+    def send_raw(self, string):
+        """Send raw string to the server.
+
+        The string will be padded with appropriate CR LF.
+        """
+        from encoding import *
+        if self.socket is None:
+            raise ServerNotConnectedError, "Not connected."
+        try:
+            if self.ssl:
+                self.ssl.write(auto_encode(string) + "\r\n")
+            else:
+                self.socket.send(auto_encode(string) + "\r\n")
+            if DEBUG:
+                print "TO SERVER:", string
+        except socket.error, x:
+            # Ouch!
+            self.disconnect("Connection reset by peer.")
+
+    def squit(self, server, comment=""):
+        """Send an SQUIT command."""
+        self.send_raw("SQUIT %s%s" % (server, comment and (" :" + comment)))
+
+    def stats(self, statstype, server=""):
+        """Send a STATS command."""
+        self.send_raw("STATS %s%s" % (statstype, server and (" " + server)))
+
+    def time(self, server=""):
+        """Send a TIME command."""
+        self.send_raw("TIME" + (server and (" " + server)))
+
+    def topic(self, channel, new_topic=None):
+        """Send a TOPIC command."""
+        if new_topic is None:
+            self.send_raw("TOPIC " + channel)
+        else:
+            self.send_raw("TOPIC %s :%s" % (channel, new_topic))
+
+    def trace(self, target=""):
+        """Send a TRACE command."""
+        self.send_raw("TRACE" + (target and (" " + target)))
+
+    def user(self, username, realname):
+        """Send a USER command."""
+        self.send_raw("USER %s 0 * :%s" % (username, realname))
+
+    def userhost(self, nicks):
+        """Send a USERHOST command."""
+        self.send_raw("USERHOST " + ",".join(nicks))
+
+    def users(self, server=""):
+        """Send a USERS command."""
+        self.send_raw("USERS" + (server and (" " + server)))
+
+    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)
+
+    def who(self, target="", op=""):
+        """Send a WHO command."""
+        self.send_raw("WHO%s%s" % (target and (" " + target), op and (" o")))
+
+    def whois(self, targets):
+        """Send a WHOIS command."""
+        self.send_raw("WHOIS " + ",".join(targets))
+
+    def whowas(self, nick, max="", server=""):
+        """Send a WHOWAS command."""
+        self.send_raw("WHOWAS %s%s%s" % (nick,
+                                         max and (" " + max),
+                                         server and (" " + server)))
+
+class DCCConnectionError(IRCError):
+    pass
+
+
+class DCCConnection(Connection):
+    """This class represents a DCC connection.
+
+    DCCConnection objects are instantiated by calling the dcc
+    method on an IRC object.
+    """
+    def __init__(self, irclibobj, dcctype):
+        Connection.__init__(self, irclibobj)
+        self.connected = 0
+        self.passive = 0
+        self.dcctype = dcctype
+        self.peeraddress = None
+        self.peerport = None
+
+    def connect(self, address, port):
+        """Connect/reconnect to a DCC peer.
+
+        Arguments:
+            address -- Host/IP address of the peer.
+
+            port -- The port number to connect to.
+
+        Returns the DCCConnection object.
+        """
+        self.peeraddress = socket.gethostbyname(address)
+        self.peerport = port
+        self.socket = None
+        self.previous_buffer = ""
+        self.handlers = {}
+        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.passive = 0
+        try:
+            self.socket.connect((self.peeraddress, self.peerport))
+        except socket.error, x:
+            raise DCCConnectionError, "Couldn't connect to socket: %s" % x
+        self.connected = 1
+        if self.irclibobj.fn_to_add_socket:
+            self.irclibobj.fn_to_add_socket(self.socket)
+        return self
+
+    def listen(self):
+        """Wait for a connection/reconnection from a DCC peer.
+
+        Returns the DCCConnection object.
+
+        The local IP address and port are available as
+        self.localaddress and self.localport.  After connection from a
+        peer, the peer address and port are available as
+        self.peeraddress and self.peerport.
+        """
+        self.previous_buffer = ""
+        self.handlers = {}
+        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.passive = 1
+        try:
+            self.socket.bind((socket.gethostbyname(socket.gethostname()), 0))
+            self.localaddress, self.localport = self.socket.getsockname()
+            self.socket.listen(10)
+        except socket.error, x:
+            raise DCCConnectionError, "Couldn't bind socket: %s" % x
+        return self
+
+    def disconnect(self, message=""):
+        """Hang up the connection and close the object.
+
+        Arguments:
+
+            message -- Quit message.
+        """
+        if not self.connected:
+            return
+
+        self.connected = 0
+        try:
+            self.socket.close()
+        except socket.error, x:
+            pass
+        self.socket = None
+        self.irclibobj._handle_event(
+            self,
+            Event("dcc_disconnect", self.peeraddress, "", [message]))
+        self.irclibobj._remove_connection(self)
+
+    def process_data(self):
+        """[Internal]"""
+
+        if self.passive and not self.connected:
+            conn, (self.peeraddress, self.peerport) = self.socket.accept()
+            self.socket.close()
+            self.socket = conn
+            self.connected = 1
+            if DEBUG:
+                print "DCC connection from %s:%d" % (
+                    self.peeraddress, self.peerport)
+            self.irclibobj._handle_event(
+                self,
+                Event("dcc_connect", self.peeraddress, None, None))
+            return
+
+        try:
+            new_data = self.socket.recv(2**14)
+        except socket.error, x:
+            # The server hung up.
+            self.disconnect("Connection reset by peer")
+            return
+        if not new_data:
+            # Read nothing: connection must be down.
+            self.disconnect("Connection reset by peer")
+            return
+
+        if self.dcctype == "chat":
+            # The specification says lines are terminated with LF, but
+            # it seems safer to handle CR LF terminations too.
+            chunks = _linesep_regexp.split(self.previous_buffer + new_data)
+
+            # Save the last, unfinished line.
+            self.previous_buffer = chunks[-1]
+            if len(self.previous_buffer) > 2**14:
+                # Bad peer! Naughty peer!
+                self.disconnect()
+                return
+            chunks = chunks[:-1]
+        else:
+            chunks = [new_data]
+
+        command = "dccmsg"
+        prefix = self.peeraddress
+        target = None
+        for chunk in chunks:
+            if DEBUG:
+                print "FROM PEER:", chunk
+            arguments = [chunk]
+            if DEBUG:
+                print "command: %s, source: %s, target: %s, arguments: %s" % (
+                    command, prefix, target, arguments)
+            self.irclibobj._handle_event(
+                self,
+                Event(command, prefix, target, arguments))
+
+    def _get_socket(self):
+        """[Internal]"""
+        return self.socket
+
+    def privmsg(self, string):
+        """Send data to DCC peer.
+
+        The string will be padded with appropriate LF if it's a DCC
+        CHAT session.
+        """
+        try:
+            self.socket.send(string)
+            if self.dcctype == "chat":
+                self.socket.send("\n")
+            if DEBUG:
+                print "TO PEER: %s\n" % string
+        except socket.error, x:
+            # Ouch!
+            self.disconnect("Connection reset by peer.")
+
+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.
+
+        dcc_connections -- A list of DCCConnection instances.
+    """
+    def __init__(self):
+        self.ircobj = IRC()
+        self.connection = self.ircobj.server()
+        self.dcc_connections = []
+        self.ircobj.add_global_handler("all_events", self._dispatcher, -10)
+        self.ircobj.add_global_handler("dcc_disconnect", self._dcc_disconnect, -10)
+
+    def _dispatcher(self, c, e):
+        """[Internal]"""
+        m = "on_" + e.eventtype()
+        if hasattr(self, m):
+            getattr(self, m)(c, e)
+
+    def _dcc_disconnect(self, c, e):
+        self.dcc_connections.remove(c)
+
+    def connect(self, server, port, nickname, password=None, username=None,
+                ircname=None, localaddress="", localport=0, ssl=False, ipv6=False):
+        """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.
+
+            localaddress -- Bind the connection to a specific local IP address.
+
+            localport -- Bind the connection to a specific local port.
+
+            ssl -- Enable support for ssl.
+
+            ipv6 -- Enable support for ipv6.
+
+        This function can be called to reconnect a closed connection.
+        """
+        self.connection.connect(server, port, nickname,
+                                password, username, ircname,
+                                localaddress, localport, ssl, ipv6)
+
+    def dcc_connect(self, address, port, dcctype="chat"):
+        """Connect to a DCC peer.
+
+        Arguments:
+
+            address -- IP address of the peer.
+
+            port -- Port to connect to.
+
+        Returns a DCCConnection instance.
+        """
+        dcc = self.ircobj.dcc(dcctype)
+        self.dcc_connections.append(dcc)
+        dcc.connect(address, port)
+        return dcc
+
+    def dcc_listen(self, dcctype="chat"):
+        """Listen for connections from a DCC peer.
+
+        Returns a DCCConnection instance.
+        """
+        dcc = self.ircobj.dcc(dcctype)
+        self.dcc_connections.append(dcc)
+        dcc.listen()
+        return dcc
+
+    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.
+
+            source -- The originator of the event (a nick mask or a server).
+
+            target -- The target of the event (a nick or a channel).
+
+            arguments -- Any event specific arguments.
+        """
+        self._eventtype = eventtype
+        self._source = source
+        self._target = target
+        if arguments:
+            self._arguments = arguments
+        else:
+            self._arguments = []
+
+    def eventtype(self):
+        """Get the event type."""
+        return self._eventtype
+
+    def source(self):
+        """Get the event source."""
+        return self._source
+
+    def target(self):
+        """Get the event target."""
+        return self._target
+
+    def arguments(self):
+        """Get the event arguments."""
+        return self._arguments
+
+_LOW_LEVEL_QUOTE = "\020"
+_CTCP_LEVEL_QUOTE = "\134"
+_CTCP_DELIMITER = "\001"
+
+_low_level_mapping = {
+    "0": "\000",
+    "n": "\n",
+    "r": "\r",
+    _LOW_LEVEL_QUOTE: _LOW_LEVEL_QUOTE
+}
+
+_low_level_regexp = re.compile(_LOW_LEVEL_QUOTE + "(.)")
+
+def mask_matches(nick, mask):
+    """Check if a nick matches a mask.
+
+    Returns true if the nick matches, otherwise false.
+    """
+    nick = irc_lower(nick)
+    mask = irc_lower(mask)
+    mask = mask.replace("\\", "\\\\")
+    for ch in ".$|[](){}+":
+        mask = mask.replace(ch, "\\" + ch)
+    mask = mask.replace("?", ".")
+    mask = mask.replace("*", ".*")
+    r = re.compile(mask, re.IGNORECASE)
+    return r.match(nick)
+
+_special = "-[]\\`^{}"
+nick_characters = string.ascii_letters + string.digits + _special
+_ircstring_translation = string.maketrans(string.ascii_uppercase + "[]\\^",
+                                          string.ascii_lowercase + "{}|~")
+
+def irc_lower(s):
+    """Returns a lowercased string.
+
+    The definition of lowercased comes from the IRC specification (RFC
+    1459).
+    """
+    return s.translate(_ircstring_translation)
+
+def _ctcp_dequote(message):
+    """[Internal] Dequote a message according to CTCP specifications.
+
+    The function returns a list where each element can be either a
+    string (normal message) or a tuple of one or two strings (tagged
+    messages).  If a tuple has only one element (ie is a singleton),
+    that element is the tag; otherwise the tuple has two elements: the
+    tag and the data.
+
+    Arguments:
+
+        message -- The message to be decoded.
+    """
+
+    def _low_level_replace(match_obj):
+        ch = match_obj.group(1)
+
+        # If low_level_mapping doesn't have the character as key, we
+        # should just return the character.
+        return _low_level_mapping.get(ch, ch)
+
+    if _LOW_LEVEL_QUOTE in message:
+        # Yup, there was a quote.  Release the dequoter, man!
+        message = _low_level_regexp.sub(_low_level_replace, message)
+
+    if _CTCP_DELIMITER not in message:
+        return [message]
+    else:
+        # Split it into parts.  (Does any IRC client actually *use*
+        # CTCP stacking like this?)
+        chunks = message.split(_CTCP_DELIMITER)
+
+        messages = []
+        i = 0
+        while i < len(chunks)-1:
+            # Add message if it's non-empty.
+            if len(chunks[i]) > 0:
+                messages.append(chunks[i])
+
+            if i < len(chunks)-2:
+                # Aye!  CTCP tagged data ahead!
+                messages.append(tuple(chunks[i+1].split(" ", 1)))
+
+            i = i + 2
+
+        if len(chunks) % 2 == 0:
+            # Hey, a lonely _CTCP_DELIMITER at the end!  This means
+            # that the last chunk, including the delimiter, is a
+            # normal message!  (This is according to the CTCP
+            # specification.)
+            messages.append(_CTCP_DELIMITER + chunks[-1])
+
+        return messages
+
+def is_channel(string):
+    """Check if a string is a channel name.
+
+    Returns true if the argument is a channel name, otherwise false.
+    """
+    return string and string[0] in "#&+!"
+
+def ip_numstr_to_quad(num):
+    """Convert an IP number as an integer given in ASCII
+    representation (e.g. '3232235521') to an IP address string
+    (e.g. '192.168.0.1')."""
+    n = long(num)
+    p = map(str, map(int, [n >> 24 & 0xFF, n >> 16 & 0xFF,
+                           n >> 8 & 0xFF, n & 0xFF]))
+    return ".".join(p)
+
+def ip_quad_to_numstr(quad):
+    """Convert an IP address string (e.g. '192.168.0.1') to an IP
+    number as an integer given in ASCII representation
+    (e.g. '3232235521')."""
+    p = map(long, quad.split("."))
+    s = str((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3])
+    if s[-1] == "L":
+        s = s[:-1]
+    return s
+
+def nm_to_n(s):
+    """Get the nick part of a nickmask.
+
+    (The source of an Event is a nickmask.)
+    """
+    return s.split("!")[0]
+
+def nm_to_uh(s):
+    """Get the userhost part of a nickmask.
+
+    (The source of an Event is a nickmask.)
+    """
+    return s.split("!")[1]
+
+def nm_to_h(s):
+    """Get the host part of a nickmask.
+
+    (The source of an Event is a nickmask.)
+    """
+    return s.split("@")[1]
+
+def nm_to_u(s):
+    """Get the user part of a nickmask.
+
+    (The source of an Event is a nickmask.)
+    """
+    s = s.split("!")[1]
+    return s.split("@")[0]
+
+def parse_nick_modes(mode_string):
+    """Parse a nick mode string.
+
+    The function returns a list of lists with three members: sign,
+    mode and argument.  The sign is \"+\" or \"-\".  The argument is
+    always None.
+
+    Example:
+
+    >>> irclib.parse_nick_modes(\"+ab-c\")
+    [['+', 'a', None], ['+', 'b', None], ['-', 'c', None]]
+    """
+
+    return _parse_modes(mode_string, "")
+
+def parse_channel_modes(mode_string):
+    """Parse a channel mode string.
+
+    The function returns a list of lists with three members: sign,
+    mode and argument.  The sign is \"+\" or \"-\".  The argument is
+    None if mode isn't one of \"b\", \"k\", \"l\", \"v\" or \"o\".
+
+    Example:
+
+    >>> irclib.parse_channel_modes(\"+ab-c foo\")
+    [['+', 'a', None], ['+', 'b', 'foo'], ['-', 'c', None]]
+    """
+
+    return _parse_modes(mode_string, "bklvo")
+
+def _parse_modes(mode_string, unary_modes=""):
+    """[Internal]"""
+    modes = []
+    arg_count = 0
+
+    # State variable.
+    sign = ""
+
+    a = mode_string.split()
+    if len(a) == 0:
+        return []
+    else:
+        mode_part, args = a[0], a[1:]
+
+    if mode_part[0] not in "+-":
+        return []
+    for ch in mode_part:
+        if ch in "+-":
+            sign = ch
+        elif ch == " ":
+            collecting_arguments = 1
+        elif ch in unary_modes:
+            if len(args) >= arg_count + 1:
+                modes.append([sign, ch, args[arg_count]])
+                arg_count = arg_count + 1
+            else:
+                modes.append([sign, ch, None])
+        else:
+            modes.append([sign, ch, None])
+    return modes
+
+def _ping_ponger(connection, event):
+    """[Internal]"""
+    connection.pong(event.target())
+
+# Numeric table mostly stolen from the Perl IRC module (Net::IRC).
+numeric_events = {
+    "001": "welcome",
+    "002": "yourhost",
+    "003": "created",
+    "004": "myinfo",
+    "005": "featurelist",  # XXX
+    "200": "tracelink",
+    "201": "traceconnecting",
+    "202": "tracehandshake",
+    "203": "traceunknown",
+    "204": "traceoperator",
+    "205": "traceuser",
+    "206": "traceserver",
+    "207": "traceservice",
+    "208": "tracenewtype",
+    "209": "traceclass",
+    "210": "tracereconnect",
+    "211": "statslinkinfo",
+    "212": "statscommands",
+    "213": "statscline",
+    "214": "statsnline",
+    "215": "statsiline",
+    "216": "statskline",
+    "217": "statsqline",
+    "218": "statsyline",
+    "219": "endofstats",
+    "221": "umodeis",
+    "231": "serviceinfo",
+    "232": "endofservices",
+    "233": "service",
+    "234": "servlist",
+    "235": "servlistend",
+    "241": "statslline",
+    "242": "statsuptime",
+    "243": "statsoline",
+    "244": "statshline",
+    "250": "luserconns",
+    "251": "luserclient",
+    "252": "luserop",
+    "253": "luserunknown",
+    "254": "luserchannels",
+    "255": "luserme",
+    "256": "adminme",
+    "257": "adminloc1",
+    "258": "adminloc2",
+    "259": "adminemail",
+    "261": "tracelog",
+    "262": "endoftrace",
+    "263": "tryagain",
+    "265": "n_local",
+    "266": "n_global",
+    "300": "none",
+    "301": "away",
+    "302": "userhost",
+    "303": "ison",
+    "305": "unaway",
+    "306": "nowaway",
+    "311": "whoisuser",
+    "312": "whoisserver",
+    "313": "whoisoperator",
+    "314": "whowasuser",
+    "315": "endofwho",
+    "316": "whoischanop",
+    "317": "whoisidle",
+    "318": "endofwhois",
+    "319": "whoischannels",
+    "321": "liststart",
+    "322": "list",
+    "323": "listend",
+    "324": "channelmodeis",
+    "329": "channelcreate",
+    "331": "notopic",
+    "332": "currenttopic",
+    "333": "topicinfo",
+    "341": "inviting",
+    "342": "summoning",
+    "346": "invitelist",
+    "347": "endofinvitelist",
+    "348": "exceptlist",
+    "349": "endofexceptlist",
+    "351": "version",
+    "352": "whoreply",
+    "353": "namreply",
+    "361": "killdone",
+    "362": "closing",
+    "363": "closeend",
+    "364": "links",
+    "365": "endoflinks",
+    "366": "endofnames",
+    "367": "banlist",
+    "368": "endofbanlist",
+    "369": "endofwhowas",
+    "371": "info",
+    "372": "motd",
+    "373": "infostart",
+    "374": "endofinfo",
+    "375": "motdstart",
+    "376": "endofmotd",
+    "377": "motd2",        # 1997-10-16 -- tkil
+    "381": "youreoper",
+    "382": "rehashing",
+    "384": "myportis",
+    "391": "time",
+    "392": "usersstart",
+    "393": "users",
+    "394": "endofusers",
+    "395": "nousers",
+    "401": "nosuchnick",
+    "402": "nosuchserver",
+    "403": "nosuchchannel",
+    "404": "cannotsendtochan",
+    "405": "toomanychannels",
+    "406": "wasnosuchnick",
+    "407": "toomanytargets",
+    "409": "noorigin",
+    "411": "norecipient",
+    "412": "notexttosend",
+    "413": "notoplevel",
+    "414": "wildtoplevel",
+    "421": "unknowncommand",
+    "422": "nomotd",
+    "423": "noadmininfo",
+    "424": "fileerror",
+    "431": "nonicknamegiven",
+    "432": "erroneusnickname", # Thiss iz how its speld in thee RFC.
+    "433": "nicknameinuse",
+    "436": "nickcollision",
+    "437": "unavailresource",  # "Nick temporally unavailable"
+    "441": "usernotinchannel",
+    "442": "notonchannel",
+    "443": "useronchannel",
+    "444": "nologin",
+    "445": "summondisabled",
+    "446": "usersdisabled",
+    "451": "notregistered",
+    "461": "needmoreparams",
+    "462": "alreadyregistered",
+    "463": "nopermforhost",
+    "464": "passwdmismatch",
+    "465": "yourebannedcreep", # I love this one...
+    "466": "youwillbebanned",
+    "467": "keyset",
+    "471": "channelisfull",
+    "472": "unknownmode",
+    "473": "inviteonlychan",
+    "474": "bannedfromchan",
+    "475": "badchannelkey",
+    "476": "badchanmask",
+    "477": "nochanmodes",  # "Channel doesn't support modes"
+    "478": "banlistfull",
+    "481": "noprivileges",
+    "482": "chanoprivsneeded",
+    "483": "cantkillserver",
+    "484": "restricted",   # Connection is restricted
+    "485": "uniqopprivsneeded",
+    "491": "nooperhost",
+    "492": "noservicehost",
+    "501": "umodeunknownflag",
+    "502": "usersdontmatch",
+}
+
+generated_events = [
+    # Generated events
+    "dcc_connect",
+    "dcc_disconnect",
+    "dccmsg",
+    "disconnect",
+    "ctcp",
+    "ctcpreply",
+]
+
+protocol_events = [
+    # IRC protocol events
+    "error",
+    "join",
+    "kick",
+    "mode",
+    "part",
+    "ping",
+    "privmsg",
+    "privnotice",
+    "pubmsg",
+    "pubnotice",
+    "quit",
+    "invite",
+    "pong",
+]
+
+all_events = generated_events + protocol_events + numeric_events.values()
new file mode 100644
--- /dev/null
+++ b/muc.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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 3 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
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import xmppony as xmpp
+from time import sleep
+
+
+class muc:
+	
+	class PasswordNeeded(Exception): pass
+	class MembersOnlyRoom(Exception): pass
+	class BannedFromRoom(Exception): pass
+	class NicknameConflict(Exception): pass
+	class RoomIsFull(Exception): pass
+	class RoomIsLocked(Exception): pass
+	class ForgotNickname(Exception): pass
+	class UnknownError(Exception): pass
+	
+	def __init__(self, room_jid):
+		self.room_jid = room_jid
+		self.connected = False
+		self.participants = {}
+	
+	
+	def join(self, xmpp_c, nickname, status=None, callback=None):
+		"""Join room on xmpp_c connection using nickname"""
+		self.jid = self.room_jid+'/'+nickname
+		self.nickname = nickname
+		self.xmpp_c = xmpp_c
+		self.callback = callback
+		self.xmpp_c.RegisterHandler('presence', self._xmpp_presence_handler)
+		self.xmpp_c.send(xmpp.protocol.Presence(to=self.jid, status=status))
+	
+	
+	def _xmpp_presence_handler(self, xmpp_c, presence):
+		if presence.getFrom() == self.jid:
+			errors = []
+			if presence.getAttr('type') == 'error':
+				for c in presence.getChildren():
+					if c.getName() == 'error':
+						for cc in c.getChildren():
+							if cc.getNamespace() == 'urn:ietf:params:xml:ns:xmpp-stanzas' and cc.getName() != 'text':
+								err = c.getAttr('type')+' '+cc.getName()
+								if err == 'auth not-authorized':
+									# password-protected room
+									errors.append(self.__class__.PasswordNeeded())
+								elif err == 'auth registration-required':
+									# members-only room
+									errors.append(self.__class__.MembersOnlyRoom())
+								elif err == 'auth forbidden':
+									# banned from room
+									errors.append(self.__class__.BannedFromRoom())
+								elif err == 'cancel conflict':
+									# nickname conflict
+									errors.append(self.__class__.NicknameConflict())
+								elif err == 'wait service-unavailable':
+									# room is full
+									errors.append(self.__class__.RoomIsFull())
+								elif err == 'cancel item-not-found':
+									# room is locked
+									errors.append(self.__class__.RoomIsLocked())
+								elif err == 'modify jid-malformed':
+									# forgot to give a nickname
+									errors.append(self.__class__.ForgotNickname())
+								else:
+									errors.append(self.__class__.UnknownError(presence.__str__(fancy=1).decode('utf-8')))
+						break
+				if len(errors) == 0:
+					errors.append(self.__class__.UnknownError(presence.__str__(fancy=1).decode('utf-8')))
+			else:
+				self.connected = True
+				xmpp_c.UnregisterHandler('presence', self._xmpp_presence_handler)
+			if self.callback != None:
+				self.callback(errors)
+	
+	
+	def _check(self):
+		i = 0
+		while not self.connected:
+			i += 1
+			if i > 30:
+				raise Exception('Error: connection to room timed out')
+			sleep(1)
+	
+	
+	def say(self, message):
+		"""Say message in the room"""
+		self._check()
+		self.xmpp_c.send(xmpp.protocol.Message(to=self.room_jid, typ='groupchat', body=message))
+	
+	
+	def sayTo(self, to, message):
+		"""Send a private message"""
+		self._check()
+		self.xmpp_c.send(xmpp.protocol.Message(to=self.room_jid+'/'+to, typ='chat', body=message))
+	
+	
+	def change_nick(self, nickname, callback=None):
+		"""Change nickname"""
+		self._check()
+		self.jid = self.room_jid+'/'+nickname
+		self.callback = callback
+		self.xmpp_c.RegisterHandler('presence', self._xmpp_presence_handler)
+		self.xmpp_c.send(xmpp.protocol.Presence(to=self.jid))
+	
+	
+	def leave(self, message=''):
+		"""Leave the room"""
+		self.xmpp_c.send(xmpp.protocol.Presence(to=self.jid, typ='unavailable', status=message))
+		self.connected = False
+	
+	
+	def __del__(self):
+		if self.connected:
+			self.leave()
+
+xmpp.muc = muc
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/participant.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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 3 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
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import muc
+xmpp = muc.xmpp
+del muc
+import irclib
+from encoding import *
+from threading import Thread
+from time import sleep
+
+
+class participant:
+	def __init__(self, owner_bridge, protocol, nickname):
+		self.bridge = owner_bridge
+		self.protocol = protocol
+		self.nickname = nickname
+		self.irc_connection = None
+		self.xmpp_c = None
+		if protocol == 'xmpp':
+			self.createDuplicateOnIRC()
+		elif protocol == 'irc':
+			self.createDuplicateOnXMPP()
+		else:
+			raise Exception('Internal Error: bad protocol')
+			quit(1)
+	
+	
+	def createDuplicateOnXMPP(self):
+		if self.xmpp_c != None or self.irc_connection != None or self.protocol == 'both' or self.bridge.mode == 'minimal':
+			return
+		self.xmpp_c = xmpp.client.Client(self.bridge.bot.jid.getDomain(), debug=[])
+		self.xmpp_c.connect()
+		self.xmpp_c.auth(self.bridge.bot.jid.getNode(), self.bridge.bot.password, resource=self.nickname)
+		self.xmpp_c.RegisterHandler('presence', self.bridge.bot._xmpp_presence_handler)
+		self.xmpp_c.RegisterHandler('iq', self.bridge.bot._xmpp_iq_handler)
+		self.xmpp_c.RegisterHandler('message', self.bridge.bot._xmpp_message_handler)
+		self.xmpp_thread = Thread(target=self._xmpp_loop)
+		self.xmpp_thread.start()
+		self.xmpp_c.sendInitPresence()
+		self.muc = xmpp.muc(self.bridge.xmpp_room.room_jid)
+		self.muc.join(self.xmpp_c, self.nickname, status='From IRC', callback=self._xmpp_join_callback)
+	
+	
+	def createDuplicateOnIRC(self):
+		if self.irc_connection != None or self.xmpp_c != None or self.protocol == 'both' or self.bridge.mode != 'normal':
+			return
+		sleep(1) # try to prevent "reconnecting too fast" shit
+		self.irc_connection = self.bridge.bot.irc.server()
+		self.irc_connection.bridge = self.bridge
+		self.irc_connection.nick_callback = self._irc_nick_callback
+		self.irc_connection.connect(self.bridge.irc_server, self.bridge.irc_port, self.nickname)
+	
+	
+	def _irc_nick_callback(self, error):
+		if error == None:
+			self.irc_connection.join(self.bridge.irc_room)
+			self.irc_connection.nick_callback = None
+			self.bridge.bot.error('===> Debug: "'+self.nickname+'" duplicate succesfully created on IRC side of bridge "'+str(self.bridge)+'"', debug=True)
+		elif self.protocol != 'both':
+			if error == 'nicknameinuse':
+				self.bridge.bot.error('===> Debug: "'+self.nickname+'" is already used in the IRC chan of bridge "'+str(self.bridge)+'"', debug=True)
+				self.bridge.say('[Warning] The nickname "'+self.nickname+'" is used on both rooms, please avoid that if possible')
+				self.protocol = 'both'
+				self.irc_connection.close()
+				self.irc_connection = None
+			elif error == 'erroneusnickname':
+				self.bridge.bot.error('===> Debug: "'+self.nickname+'" got "erroneusnickname" on bridge "'+str(self.bridge)+'"', debug=True)
+				self.bridge.say('[Warning] The nickname "'+self.nickname+'" contains non-ASCII characters and cannot be used in the IRC channel, please avoid that if possible')
+				self.irc_connection.close()
+				self.irc_connection = None
+	
+	
+	def _xmpp_join_callback(self, errors):
+		if len(errors) == 0:
+			self.bridge.bot.error('===> Debug: "'+self.nickname+'" duplicate succesfully created on XMPP side of bridge "'+str(self.bridge)+'"', debug=True)
+		elif self.protocol != 'both':
+			for error in errors:
+				try:
+					raise error
+				except xmpp.muc.NicknameConflict:
+					self.bridge.bot.error('===> Debug: "'+self.nickname+'" is already used in the XMPP MUC or reserved on the XMPP server of bridge "'+str(self.bridge)+'"', debug=True)
+					self.bridge.say('[Warning] The nickname "'+self.nickname+'" is used on both rooms or reserved on the XMPP server, please avoid that if possible')
+					self.protocol = 'both'
+					self.xmpp_c = None
+	
+	
+	def _xmpp_loop(self):
+		while True:
+			if self.xmpp_c != None:
+				self.xmpp_c.Process(5)
+			else:
+				sleep(5)
+	
+	
+	def changeNickname(self, newnick, on_protocol):
+		if self.protocol == 'xmpp':
+			if on_protocol == 'xmpp':
+				raise Exception('Internal Error: wanted to change nickname on bad protocol')
+			if self.irc_connection:
+				self.irc_connection.nick(newnick)
+			self.nickname = newnick
+		elif self.protocol == 'irc':
+			if on_protocol == 'irc':
+				raise Exception('Internal Error: wanted to change nickname on bad protocol')
+			if self.muc:
+				self.muc.change_nick(newnick, callback=self._xmpp_join_callback)
+			self.nickname = newnick
+		elif self.protocol == 'both':
+			if on_protocol == 'irc':
+				self.protocol = 'xmpp'
+				self.createDuplicateOnIRC()
+			elif on_protocol == 'xmpp':
+				self.protocol = 'irc'
+				self.createDuplicateOnXMPP()
+	
+	
+	def sayOnIRC(self, message):
+		try:
+			if self.protocol == 'irc':
+				raise Exception('Internal Error: "'+self.nickname+'" comes from IRC')
+			elif self.protocol == 'both' or self.irc_connection == None:
+				self.bridge.irc_connection.privmsg(self.bridge.irc_room, '<'+self.nickname+'> '+message)
+			else:
+				self.irc_connection.privmsg(self.bridge.irc_room, message)
+		except EncodingException:
+			self.bridge.say('[Warning] "'+self.nickname+'" is sending messages using an unknown encoding')
+	
+	
+	def sayOnIRCTo(self, to, message):
+		if self.protocol == 'irc':
+			raise Exception('Internal Error: "'+self.nickname+'" comes from IRC')
+		elif self.irc_connection == None:
+			if self.bridge.mode != 'normal':
+				self.bridge.getParticipant(to).sayOnXMPPTo(self.nickname, 'Sorry but cross-protocol private messages are disabled in limited mode.')
+			else:
+				self.bridge.getParticipant(to).sayOnXMPPTo(self.nickname, 'Sorry but you cannot send cross-protocol private messages because I don\'t have an IRC duplicate with your nickname.')
+		else:
+			try:
+				self.irc_connection.privmsg(to, message)
+			except EncodingException:
+				self.bridge.say('[Warning] "'+self.nickname+'" is sending messages using an unknown encoding')
+	
+	
+	def sayOnXMPP(self, message):
+		if self.protocol == 'xmpp':
+			raise Exception('Internal Error: "'+self.nickname+'" comes from XMPP')
+		elif self.protocol == 'both' or self.xmpp_c == None:
+			self.bridge.xmpp_room.say('<'+self.nickname+'> '+auto_decode(message))
+		else:
+			try:
+				self.muc.say(auto_decode(message))
+			except EncodingException:
+				self.bridge.say('[Warning] "'+self.nickname+'" is sending messages using an unknown encoding')
+	
+	
+	def sayOnXMPPTo(self, to, message):
+		if self.protocol == 'xmpp':
+			raise Exception('Internal Error: "'+self.nickname+'" comes from XMPP')
+		else:
+			try:
+				self.muc.sayTo(to, auto_decode(message))
+			except EncodingException:
+				self.bridge.say('[Warning] "'+self.nickname+'" is sending messages using an unknown encoding')
+	
+	
+	def leave(self, message):
+		if message == None:
+			message = ''
+		try:
+			self.muc.leave(message)
+		except AttributeError:
+			pass
+		try:
+			self.irc_connection.disconnect(message)
+		except AttributeError:
+			pass
+		self.nickname = None
+	
+	
+	def __del__(self):
+		if self.nickname != None:
+			self.leave('')
\ No newline at end of file
new file mode 100755
--- /dev/null
+++ b/start_bots_from_xml_config.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+# *** LICENSE ***
+# 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 3 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
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from bot import bot
+from time import sleep
+from xml.dom.minidom import parse
+
+
+bots = []
+
+try:
+	config = parse('config.xml')
+except IOError:
+	print 'Error: config.xml is missing or cannot be read'
+	quit(1)
+
+bots_jids = []
+for bot_el in config.getElementsByTagName('bot'):
+	if bot_el.getAttribute('jid') in bots_jids:
+		print 'Error: you cannot have two bots using the same JID'
+		quit(2)
+	bots_jids.append(bot_el.getAttribute('jid'))
+for bot_el in config.getElementsByTagName('bot'):
+	debug = False
+	if bot_el.hasAttribute('debug'):
+		if bot_el.getAttribute('debug') == 'true':
+			debug = True
+	bot_ = bot(bot_el.getAttribute('jid'), bot_el.getAttribute('password'), bot_el.getAttribute('nickname'), debug=debug)
+	bots.append(bot_)
+	for bridge_el in bot_el.getElementsByTagName('bridge'):
+		xmpp_room = bridge_el.getElementsByTagName('xmpp-room')[0]
+		irc = bridge_el.getElementsByTagName('irc')[0]
+		bridge_ = bot_.new_bridge(xmpp_room.getAttribute('jid'), irc.getAttribute('chan'), irc.getAttribute('server'))
+
+
+try:
+	while True:
+		sleep(1)
+except:
+	del bots
+	quit(3)
\ No newline at end of file