view bridge.py @ 145:ddc87b605019

minor fix in Participant.changeNickname() Signed-off-by: Charly COSTE <changaco@changaco.net>
author Charly COSTE <changaco@changaco.net>
date Sun, 17 Jan 2010 17:42:36 +0100
parents 38eb220142a1
children e0eea72ea493
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/>.


import re
import threading
import traceback

from encoding import *
from irclib import ServerConnection
import muc
xmpp = muc.xmpp
del muc

from participant import Participant


class Bridge:
	
	_all = 0
	_info = 1
	_notice = 2
	_warning = 3
	_error = 4
	_nothing = 5
	_say_levels = ['all', 'info', 'notice', 'warning', 'error', 'nothing']
	_modes = ['normal', 'bypass', 'limited', 'minimal']
	
	
	class NoSuchParticipantException(Exception): pass
	
	
	def __init__(self, owner_bot, xmpp_room_jid, irc_room, irc_server, mode, say_level, irc_port=6667):
		"""Create a new bridge."""
		self.bot = owner_bot
		self.irc_server = irc_server
		self.irc_port = irc_port
		self.irc_room = irc_room.lower()
		self.xmpp_room_jid = xmpp_room_jid
		if hasattr(self.__class__, '_'+say_level):
			self.say_level = getattr(self.__class__, '_'+say_level)
		else:
			raise Exception('[Error] "'+say_level+'" is not a correct value for a bridge\'s "say_level" attribute')
		self.participants = []
		if mode not in self.__class__._modes:
			raise Exception('[Error] "'+mode+'" is not a correct value for a bridge\'s "mode" attribute')
		self.mode = mode
		
		self.lock = threading.RLock()
		
		self.init2()
	
	
	def init2(self):
		# Join XMPP room
		try:
			self.xmpp_room = xmpp.muc(self.xmpp_room_jid)
			self.xmpp_room.join(self.bot.xmpp_c, self.bot.nickname, callback=self._xmpp_join_callback)
		except:
			self.bot.error('[Error] joining XMPP room failed')
			raise
		
		# Join IRC room
		try:
			self.irc_connections_limit = -1
			self.irc_connection = self.bot.irc.open_connection(self.irc_server, self.irc_port, self.bot.nickname)
			self.irc_connection.connect(nick_callback=self._irc_nick_callback)
		except:
			self.bot.error('[Error] joining IRC room failed')
			raise
		
		self.bot.error('[Notice] bridge "'+str(self)+'" is running in '+self.mode+' mode and a say_level of "'+self.__class__._say_levels[self.say_level]+'"')
	
	
	def _irc_nick_callback(self, error, arguments=[]):
		if error == None:
			if self.mode == None:
				return
			self.irc_connection.join(self.irc_room)
			self.bot.error('===> Debug: successfully connected on IRC side of bridge "'+str(self)+'"', debug=True)
			self.say('[Notice] bridge "'+str(self)+'" is running in '+self.mode+' mode', on_xmpp=False)
		else:
			self.mode = None
			if self.xmpp_room.connected == True:
				self.say('[Error] failed to connect to the IRC chan, leaving ...', on_irc=False)
			try:
				if error == 'nicknameinuse':
					raise Exception('[Error] "'+self.bot.nickname+'" is already used in the IRC chan or reserved on the IRC server of bridge "'+str(self)+'"')
				elif error == 'nickcollision':
					raise Exception('[Error] "'+self.bot.nickname+'" is already used or reserved on the IRC server of bridge "'+str(self)+'"')
				elif error == 'erroneusnickname':
					raise Exception('[Error] "'+self.bot.nickname+'" got "erroneusnickname" on bridge "'+str(self)+'"')
				elif error == 'nicknametoolong':
					raise Exception('[Error] "'+self.bot.nickname+'" got "nicknametoolong" on bridge "'+str(self)+'", limit seems to be '+str(arguments[0]))
				else:
					raise Exception('[Error] unknown error for "'+self.bot.nickname+'" on bridge "'+str(self)+'", limit seems to be '+str(arguments[0]))
			except:
				traceback.print_exc()
			self.bot.error('[Error] failed to connect to the IRC chan of bridge "'+str(self)+'", stopping bridge', send_to_admins=True)
			self.stop(message='failed to connect to the IRC chan')
	
	
	def _RemoteServerNotFound_handler(self):
		server = xmpp.protocol.JID(self.xmpp_room_jid).getDomain()
		bridges = self.bot.findBridges([server])
		error_message = '[Warning] The MUC server '+server+' seems to be down, the bot will try to recreate all bridges related to this server in 5 minutes'
		self.bot.restart_bridges_delayed(bridges, 300, error_message)
	
	
	def _xmpp_join_callback(self, errors):
		"""Called by muc._xmpp_presence_handler"""
		if len(errors) == 0:
			if hasattr(self, 'reconnecting'):
				del self.reconnecting
			if self.mode == None:
				return
			self.bot.error('===> Debug: succesfully connected on XMPP side of bridge "'+str(self)+'"', debug=True)
			self.say('[Notice] bridge "'+str(self)+'" is running in '+self.mode+' mode', on_irc=False)
		else:
			self.mode = None
			if self.irc_connection.really_connected == True:
				self.say('[Error] failed to connect to the XMPP room, leaving ...', on_xmpp=False)
			for error in errors:
				try:
					raise error
				except xmpp.muc.RemoteServerNotFound:
					self._RemoteServerNotFound_handler()
				except:
					traceback.print_exc()
			self.bot.error('[Error] failed to connect to the XMPP room of bridge "'+str(self)+'", stopping bridge', send_to_admins=True)
			self.stop(message='failed to connect to the XMPP room')
	
	
	def addParticipant(self, from_protocol, nickname, real_jid=None, irc_id=None):
		"""Add a participant to the bridge."""
		if (from_protocol == 'irc' and nickname == self.irc_connection.get_nickname()) or (from_protocol == 'xmpp' and nickname == self.xmpp_room.nickname):
			self.bot.error('===> Debug: not adding self ('+self.bot.nickname+') to bridge "'+str(self)+'"', debug=True)
			return
		try:
			p = self.getParticipant(nickname)
			if p.protocol != from_protocol:
				if from_protocol == 'irc' and isinstance(p.irc_connection, ServerConnection) and p.irc_connection.really_connected == True and p.irc_connection.real_nickname == nickname or from_protocol == 'xmpp' and isinstance(p.xmpp_c, xmpp.client.Client) and isinstance(p.muc, xmpp.muc) and p.xmpp_c.nickname == nickname:
					if irc_id:
						p.irc_connection.irc_id = irc_id
					return p
				p.set_both_sides()
			return p
		except self.NoSuchParticipantException:
			pass
		
		if nickname == 'ChanServ' and from_protocol == 'irc':
			return
		
		self.lock.acquire()
		self.bot.error('===> Debug: adding participant "'+nickname+'" from "'+from_protocol+'" to bridge "'+str(self)+'"', debug=True)
		try:
			p = Participant(self, from_protocol, nickname, real_jid=real_jid)
		except IOError:
			self.bot.error('===> Debug: IOError while adding participant "'+nickname+'" from "'+from_protocol+'" to bridge "'+str(self)+'", reconnectiong ...', debug=True)
			p.xmpp_c.reconnectAndReauth()
		except:
			self.bot.error('===> Debug: unknown error while adding participant "'+nickname+'" from "'+from_protocol+'" to bridge "'+str(self)+'"', debug=True)
			traceback.print_exc()
			return
		self.participants.append(p)
		self.lock.release()
		if self.mode not in ['normal', 'bypass']:
			if from_protocol == 'xmpp':
				xmpp_participants_nicknames = self.get_participants_nicknames_list(protocols=['xmpp'])
				self.say('[Info] Participants on XMPP: '+'  '.join(xmpp_participants_nicknames), on_xmpp=False)
			elif self.mode == 'minimal' and from_protocol == 'irc':
				irc_participants_nicknames = self.get_participants_nicknames_list(protocols=['irc'])
				self.say('[Info] Participants on IRC: '+'  '.join(irc_participants_nicknames), on_irc=False)
		return p
	
	
	def createDuplicatesOn(self, protocols):
		for p in self.participants:
			if p.protocol == 'xmpp' and 'irc' in protocols:
				p.createDuplicateOnIRC()
			elif p.protocol == 'irc' and 'xmpp' in protocols:
				p.createDuplicateOnXMPP()
	
	
	def changeMode(self, new_mode):
		if new_mode == self.mode:
			return 'Mode is already equal to '+self.mode
		
		old_mode = self.mode
		self.mode = new_mode
		
		unhandled = False
		
		if new_mode in ['normal', 'bypass']:
			
			if old_mode[-7:] == 'limited':
				# From  [{normal,bypass}-]limited  to  {normal,bypass}
				self.createDuplicatesOn(['irc'])
			
			elif old_mode in ['minimal', 'normal']:
				# From  {minimal,normal}  to  {normal,bypass}
				self.createDuplicatesOn(['irc', 'xmpp'])
			
			elif old_mode == 'bypass':
				# From  bypass  to  normal
				pass  # Handled below
			
			else:
				# Unhandled mode changing
				unhandled = True
			
		elif new_mode[-7:] == 'limited':
			
			if old_mode == 'minimal':
				self.createDuplicatesOn(['xmpp'])
			
			i = 0
			for p in self.participants:
				if p.protocol == 'xmpp':
					i += 1
					p._close_irc_connection('Bridge is switching to limited mode')
			
			if new_mode[-8:] == '-limited':
				# to {normal,bypass}-limited
				self.irc_connections_limit = i
				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 which seems to be '+str(self.irc_connections_limit)+' on "'+self.irc_server+'".', log=True)
				xmpp_participants_nicknames = self.get_participants_nicknames_list(protocols=['xmpp'])
				self.say('[Info] Participants on XMPP: '+'  '.join(xmpp_participants_nicknames), on_xmpp=False)
				return
		
		elif new_mode == 'minimal':
			for p in self.participants:
				p.leave('Bridge is switching to minimal mode')
		
		else:
			# Unhandled mode changing
			unhandled = True
		
		if unhandled:
			self.mode = old_mode
			return 'Error: unhandled mode changing from '+self.mode+' to '+new_mode
		
		if old_mode == 'bypass':
			# From  bypass  to  *
			for p in self.participants:
				if p.nickname != p.duplicate_nickname:
					p.leave('Bridge is switching to '+new_mode+' mode')
		
		self.say('[Notice] Bridge is switching from '+old_mode+' to '+new_mode+' mode.', log=True)
	
	
	def getParticipant(self, nickname):
		"""Returns a participant object if there is a participant using nickname in the bridge. Raises a NoSuchParticipantException otherwise."""
		self.lock.acquire()
		for p in self.participants:
			if nickname in [p.nickname, p.duplicate_nickname]:
				self.lock.release()
				return p
		self.lock.release()
		raise self.NoSuchParticipantException('there is no participant using the nickname "'+nickname+'" in this bridge')
	
	
	def get_participants_nicknames_list(self, protocols=['irc', 'xmpp']):
		"""Returns a list of the nicknames of the bridge's participants that are connected on the XMPP side."""
		self.lock.acquire()
		participants_nicknames = []
		for p in self.participants:
			if p.protocol in protocols:
				participants_nicknames.append('"'+p.nickname+'"')
		self.lock.release()
		return participants_nicknames
	
	
	def hasParticipant(self, nickname):
		try:
			self.getParticipant(nickname)
			return True
		except self.NoSuchParticipantException:
			return False
	
	
	def removeParticipant(self, left_protocol, nickname, leave_message):
		"""Remove the participant using nickname from the bridge. Raises a NoSuchParticipantException if nickname is not used in the bridge."""
		
		was_on_both = None
		p = self.getParticipant(nickname)
		if p.protocol == 'xmpp':
			if p.irc_connection == 'both':
				was_on_both = True
				if left_protocol == 'xmpp':
					p.protocol = 'irc'
					p.createDuplicateOnXMPP()
				elif left_protocol == 'irc':
					p.createDuplicateOnIRC()
			else:
				if left_protocol == 'xmpp':
					was_on_both = False
				elif left_protocol == 'irc':
					if isinstance(p.irc_connection, ServerConnection):
						p.irc_connection.join(self.irc_room)
					else:
						c = self.bot.irc.get_connection(self.irc_server, self.irc_port, p.duplicate_nickname)
						if not (c and self.irc_room in c.left_channels):
							p._close_irc_connection(leave_message)
							p.createDuplicateOnIRC()
					return
		
		elif p.protocol == 'irc':
			if p.xmpp_c == 'both':
				was_on_both = True
				if left_protocol == 'irc':
					p.protocol = 'xmpp'
					p.createDuplicateOnIRC()
				elif left_protocol == 'xmpp':
					p.createDuplicateOnXMPP()
			else:
				if left_protocol == 'irc':
					was_on_both = False
				elif left_protocol == 'xmpp':
					if isinstance(p.xmpp_c, xmpp.client.Client):
						self.bot.reopen_xmpp_connection(p.xmpp_c)
					return
		
		else:
			raise Exception('[Internal Error] bad protocol')
		
		if was_on_both == True:
			self.bot.error('===> Debug: "'+nickname+'" was on both sides of bridge "'+str(self)+'" but left '+left_protocol, debug=True)
		
		elif was_on_both == False:
			self.lock.acquire()
			self.bot.error('===> Debug: removing participant "'+nickname+'" from bridge "'+str(self)+'"', debug=True)
			self.participants.remove(p)
			p.leave(leave_message)
			del p
			self.lock.release()
			if left_protocol == 'xmpp':
				xmpp_participants_nicknames = self.get_participants_nicknames_list(protocols=['xmpp'])
				if self.irc_connections_limit != -1 and self.irc_connections_limit > len(xmpp_participants_nicknames):
					self.changeMode(self.mode[:-8])
				if self.mode not in ['normal', 'bypass']:
					self.say('[Info] Participants on XMPP: '+'  '.join(xmpp_participants_nicknames), on_xmpp=False)
			elif left_protocol == 'irc':
				if self.mode == 'minimal':
					irc_participants_nicknames = self.get_participants_nicknames_list(protocols=['irc'])
					self.say('[Info] Participants on IRC: '+'  '.join(irc_participants_nicknames), on_irc=False)
		
		else:
			self.bot.error('=> Debug: Bad decision tree,  p.protocol='+p.protocol+'  left_protocol='+left_protocol+'\np.xmpp_c='+str(p.xmpp_c)+'\np.irc_connection='+str(p.irc_connection), debug=True)
	
	
	def restart(self):
		"""Restart the bridge"""
		
		# Tell admins
		self.bot.error('Restarting bridge '+str(self), send_to_admins=True)
		
		# Stop the bridge
		self.stop(message='Restarting bridge')
		
		# Recreate the bridge
		self.init2()
	
	
	def say(self, message, on_irc=True, on_xmpp=True, log=False):
		"""Make the bot say something."""
		if message[0] != '[':
			raise Exception('[Internal Error] message does not start with "["')
		if log:
			self.bot.error(message+' ('+str(self)+')')
		if self.say_level == self.__class__._nothing:
			return
		level = re.findall('^\[(Info|Notice|Warning|Error)\]', message)
		if len(level) == 0:
			raise Exception('[Internal Error] unknown message importance "'+re.findall('^\[([^[\]]+)', message)[0]+'"')
		level = level[0].lower()
		if getattr(self.__class__, '_'+level) < self.say_level:
			return
		if on_xmpp == True:
			self.xmpp_room.say(message)
		if on_irc == True:
			self.irc_connection.privmsg(self.irc_room, message)
	
	
	def stop(self, message='Stopping bridge'):
		"""Stop the bridge"""
		
		# Close IRC connection if not used by an other bridge, just leave the room otherwise
		if isinstance(self.irc_connection, ServerConnection):
			self.irc_connection.used_by -= 1
			if self.irc_connection.used_by < 1:
				self.irc_connection.close(message)
			else:
				self.irc_connection.part(self.irc_room, message=message)
			self.irc_connection = None
		
		# Leave the MUC
		if isinstance(self.xmpp_room, xmpp.muc):
			self.xmpp_room.leave(message=message)
			self.xmpp_room.__del__()
			self.xmpp_room = None
		
		# Delete participants objects
		for p in self.participants:
			p.leave(message)
			del p
		self.participants = []
	
	
	def __str__(self):
		return self.irc_room+'@'+self.irc_server+' <-> '+self.xmpp_room_jid
	
	
	def __del__(self):
		self.stop(message='Removing bridge')