view bot.py @ 19:c1b84196c100

Changed format of non-debug error messages, fixed IRC namreply handling, prevented crash when receiving bad XMPP stanza. Signed-off-by: Charly COSTE <changaco@changaco.net>
author Charly COSTE <changaco@changaco.net>
date Thu, 20 Aug 2009 13:20:50 +0200
parents 3cdf7bb580da
children 08cde283621a
line wrap: on
line source

#!/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/>.


# *** 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.commands = ['!xmpp_participants', '!irc_participants']
		self.bare_jid = xmpp.protocol.JID(jid=jid)
		self.bare_jid.setResource('')
		self.jid = xmpp.protocol.JID(jid=jid)
		self.nickname = nickname
		self.jid.setResource(self.nickname)
		self.password = password
		self.error_fd = error_fd
		self.debug = debug
		self.bridges = []
		self.xmpp_connections = {}
		self.irc = irclib.IRC()
		self.irc.bot = self
		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 = self.get_xmpp_connection(self.jid.getResource())
		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):
		"""Output an error message."""
		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):
		"""[Internal] XMPP infinite loop."""
		while True:
			try:
				self.xmpp_c.Process(0.5)
				try:
					for c in self.xmpp_connections.itervalues():
						if hasattr(c, 'Process'):
							c.Process(0.5)
						else:
							sleep(0.5)
				except RuntimeError:
					pass
			except ExpatError:
				self.error('=> Debug: received invalid stanza', debug=True)
				continue
	
	
	def _xmpp_presence_handler(self, xmpp_c, presence):
		"""[Internal] Manage XMPP presence."""
		
		if presence.getTo() != self.jid:
			self.error('=> Debug: Skipping XMPP presence not received on bot connection.', debug=True)
			return
		
		self.error('==> Debug: Received XMPP presence.', debug=True)
		self.error(presence.__str__(fancy=1), debug=True)
		
		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)
						
					except NoSuchParticipantException:
						if presence.getType() != 'unavailable' and resource != bridge.bot.nickname:
							bridge.addParticipant('xmpp', resource)
						return
					
					
					if p.protocol == 'xmpp' and 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')
						
						else:
							# participant left
							bridge.removeParticipant('xmpp', resource, presence.getStatus())
					
				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 chat message.', debug=True)
			self.error(message.__str__(fancy=1), debug=True)
			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:
						from_ = bridge.getParticipant(message.getFrom().getResource())
						to_ = bridge.getParticipant(message.getTo().getResource())
						
						if from_.protocol == 'xmpp':
							from_.sayOnIRCTo(to_.nickname, message.getBody())
						else:
							self.error('==> Debug: received XMPP chat message from a non-XMPP participant, WTF ?', debug=True)
						
					except NoSuchParticipantException:
						if message.getTo() == self.jid:
							xmpp_c.send(xmpp.protocol.Message(to=message.getFrom(), body=self.respond(message.getBody(), participant=from_), typ='chat'))
							return
						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
		
		elif message.getType() == 'groupchat':
			# message comes from a room
			
			for child in message.getChildren():
				if child.getName() == 'delay':
					# MUC delayed message
					return
			
			if message.getTo() != self.jid:
				self.error('=> Debug: Ignoring XMPP MUC message not received on bot connection.', debug=True)
				return
			
			
			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
						self.error('=> Debug: Ignoring XMPP groupchat message sent by the room.', debug=True)
						return
					else:
						# message comes from a participant of the room
						self.error('==> Debug: Received XMPP groupchat message.', debug=True)
						self.error(message.__str__(fancy=1), debug=True)
						
						try:
							participant_ = bridge.getParticipant(resource)
						except NoSuchParticipantException:
							if resource != self.nickname:
								self.error('=> Debug: NoSuchParticipantException "'+resource+'", WTF ?', debug=True)
							return
						
						if participant_.protocol == 'xmpp':
							participant_.sayOnIRC(message.getBody())
		
		else:
			self.error('==> Debug: Received XMPP message of unknown type "'+message.getType()+'".', debug=True)
			self.error(message.__str__(fancy=1), debug=True)
	
	
	def _irc_event_handler(self, connection, event):
		"""[Internal] Manage IRC events"""
		
		# Answer ping
		if event.eventtype() == 'ping':
			connection.pong(connection.get_server_name())
			return
		
		
		# Events we always want to ignore
		if 'all' in event.eventtype() or 'motd' in event.eventtype():
			return
		if event.eventtype() in ['pong', 'privnotice', 'ctcp', 'nochanmodes', 'notexttosend']:
			self.error('=> Debug: ignoring '+event.eventtype(), debug=True)
			return
		
		
		nickname = None
		if '!' in event.source():
			nickname = event.source().split('!')[0]
		
		
		# Events that we want to ignore only in some cases
		if event.eventtype() in ['umode', 'welcome', 'yourhost', 'created', 'myinfo', 'featurelist', 'luserclient', 'luserop', 'luserchannels', 'luserme', 'n_local', 'n_global', 'endofnames', 'luserunknown', 'luserconns']:
			if connection.really_connected == False:
				if event.target() == connection.nickname:
					connection.really_connected = True
					connection._call_nick_callbacks(None)
				elif len(connection.nick_callbacks) > 0:
					self.error('===> Debug: event target ('+event.target()+') and connection nickname ('+connection.nickname+') don\'t match')
					connection._call_nick_callbacks('nicknametoolong', arguments=[len(event.target())])
			self.error('=> Debug: ignoring '+event.eventtype(), debug=True)
			return
		
		
		# A string representation of the event
		event_str = '==> Debug: Received IRC event.\nconnection='+str(connection)+'\neventtype='+event.eventtype()+'\nsource='+str(event.source())+'\ntarget='+str(event.target())+'\narguments='+str(event.arguments())
		
		
		if event.eventtype() in ['pubmsg', 'privmsg', 'quit', 'part', 'nick']:
			if nickname == None:
				return
			
			# TODO: lock self.bridges for thread safety
			for bridge in self.bridges:
				try:
					from_ = bridge.getParticipant(nickname)
					
				except NoSuchParticipantException:
					self.error('===> Debug: NoSuchParticipantException "'+nickname+'" in bridge "'+str(bridge)+'"', debug=True)
					continue
				
				
				# Private message
				if event.eventtype() == 'privmsg':
					if event.target() == None:
						return
					
					try:
						to_ = bridge.getParticipant(event.target().split('!')[0])
						self.error(event_str, debug=True)
						from_.sayOnXMPPTo(to_.nickname, event.arguments()[0])
						return
						
					except NoSuchParticipantException:
						if event.target().split('!')[0] == self.nickname:
							# Message is for the bot
							self.error(event_str, debug=True)
							connection.privmsg(from_.nickname, self.respond(event.arguments()[0]))
							return
						else:
							continue
				
				
				# From here we skip if the event was not received on bot connection
				if connection.get_nickname() != self.nickname:
					self.error('=> Debug: ignoring IRC '+event.eventtype()+' not received on bridge connection', debug=True)
					continue
				
				self.error(event_str, debug=True)
				
				
				# Leaving events
				if event.eventtype() == 'quit' or event.eventtype() == 'part' and event.target() == bridge.irc_room:
					if from_.protocol == 'irc':
						bridge.removeParticipant('irc', from_.nickname, event.arguments()[0])
					continue
				
				
				# Nickname change
				if event.eventtype() == 'nick' and from_.protocol == 'irc':
					from_.changeNickname(event.target(), 'xmpp')
					continue
				
				
				# Chan message
				if event.eventtype() == 'pubmsg':
					if bridge.irc_room == event.target() and bridge.irc_server == connection.server:
						if from_.protocol != 'xmpp':
							from_.sayOnXMPP(event.arguments()[0])
						return
					else:
						continue
			
			return
		
		
		if event.eventtype() in ['namreply', 'join']:
			if connection.get_nickname() != self.nickname:
				self.error('=> Debug: ignoring IRC '+event.eventtype()+' not received on bridge connection', debug=True)
				return
			
			if event.eventtype() == 'namreply':
				# TODO: lock self.bridges for thread safety
				for bridge in self.getBridges(irc_room=event.arguments()[1], irc_server=connection.server):
					for nickname in re.split('(?:^[&@\+]?|(?: [&@\+]?)*)', event.arguments()[2].strip()):
						if nickname == '' or nickname == self.nickname:
							continue
						bridge.addParticipant('irc', nickname)
				return
			elif event.eventtype() == 'join':
				bridges = self.getBridges(irc_room=event.target(), irc_server=connection.server)
				if len(bridges) == 0:
					self.error('===> Debug: no bridge found for "'+event.target()+' at '+connection.server+'"', debug=True)
					return
				for bridge in bridges:
					bridge.addParticipant('irc', nickname)
				return
		
		
		# From here the event is shown
		self.error(event_str, debug=True)
		
		if event.eventtype() == 'disconnect':
			# TODO: lock self.bridges for thread safety
			for bridge in self.bridges:
				try:
					bridge.getParticipant(connection.get_nickname())
					if bridge.mode == 'normal':
						bridge.switchFromNormalToLimitedMode()
				except NoSuchParticipantException:
					pass
			return
		elif event.eventtype() == 'nicknameinuse':
			connection._call_nick_callbacks('nicknameinuse')
			return
		elif event.eventtype() == 'erroneusnickname':
			connection._call_nick_callbacks('erroneusnickname')
			return
		
		
		# Unhandled events
		self.error('=> Debug: event not handled', debug=True)
	
	
	def new_bridge(self, xmpp_room, irc_room, irc_server, mode, say_participants_list, irc_port=6667):
		"""Create a bridge between xmpp_room and irc_room at irc_server."""
		b = bridge(self, xmpp_room, irc_room, irc_server, mode, say_participants_list, irc_port=irc_port)
		self.bridges.append(b)
		return b
	
	
	def getBridges(self, irc_room=None, irc_server=None, xmpp_room_jid=None):
		bridges = [b for b in self.bridges]
		if irc_room != None:
			for bridge in bridges:
				if bridge.irc_room != irc_room:
					if bridge in bridges:
						bridges.remove(bridge)
		if irc_server != None:
			for bridge in bridges:
				if bridge.irc_server != irc_server:
					if bridge in bridges:
						bridges.remove(bridge)
		if xmpp_room_jid != None:
			for bridge in bridges:
				if bridge.xmpp_room.room_jid != xmpp_room_jid:
					if bridge in bridges:
						bridges.remove(bridge)
		return bridges
	
	
	def get_xmpp_connection(self, resource):
		if self.xmpp_connections.has_key(resource):
			c = self.xmpp_connections[resource]
			c.used_by += 1
			self.error('===> Debug: using existing XMPP connection for "'+str(self.bare_jid)+'/'+resource+'", now used by '+str(c.used_by)+' bridges', debug=True)
			return c
		self.error('===> Debug: opening new XMPP connection for "'+str(self.bare_jid)+'/'+resource+'"', debug=True)
		c = xmpp.client.Client(self.jid.getDomain(), debug=[])
		self.xmpp_connections[resource] = c
		c.used_by = 1
		c.connect()
		c.auth(self.jid.getNode(), self.password, resource=resource)
		c.RegisterHandler('presence', self._xmpp_presence_handler)
		c.RegisterHandler('iq', self._xmpp_iq_handler)
		c.RegisterHandler('message', self._xmpp_message_handler)
		c.sendInitPresence()
		return c
	
	
	def close_xmpp_connection(self, resource):
		self.xmpp_connections[resource].used_by -= 1
		if self.xmpp_connections[resource].used_by < 1:
			self.error('===> Debug: closing XMPP connection for "'+str(self.bare_jid)+'/'+resource+'"', debug=True)
			del self.xmpp_connections[resource]
		else:
			self.error('===> Debug: XMPP connection for "'+str(self.bare_jid)+'/'+resource+'" is now used by '+str(self.xmpp_connections[resource].used_by)+' bridges', debug=True)
	
	
	def removeBridge(self, bridge):
		self.bridges.remove(bridge)
		del bridge
	
	
	def respond(self, message, participant=None):
		ret = ''
		if message.strip() == '!xmpp_participants':
			if participant == None:
				for bridge in self.bridges:
					xmpp_participants_nicknames = bridge.get_participants_nicknames_list(protocols=['xmpp'])
					ret += '\nparticipants on '+bridge.xmpp_room.room_jid+': '+' '.join(xmpp_participants_nicknames)
				return ret
			else:
				xmpp_participants_nicknames = participant.bridge.get_participants_nicknames_list(protocols=['xmpp'])
				return 'participants on '+participant.bridge.xmpp_room.room_jid+': '+' '.join(xmpp_participants_nicknames)
		elif message.strip() == '!irc_participants':
			if participant == None:
				for bridge in self.bridges:
					irc_participants_nicknames = bridge.get_participants_nicknames_list(protocols=['irc'])
					ret += '\nparticipants on '+bridge.irc_room+' at '+bridge.irc_server+': '+' '.join(irc_participants_nicknames)
				return ret
			else:
				irc_participants_nicknames = participant.bridge.get_participants_nicknames_list(protocols=['irc'])
				return 'participants on '+participant.bridge.irc_room+' at '+participant.bridge.irc_server+': '+' '.join(irc_participants_nicknames)
		else:
			return 'commands: '+' '.join(self.commands)
	
	
	def __del__(self):
		for bridge in self.bridges:
			self.removeBridge(bridge)