changeset 118:b29fd5696a78

new mode bypass and new method bridge.changeMode Signed-off-by: Charly COSTE <changaco@changaco.net>
author Charly COSTE <changaco@changaco.net>
date Sat, 28 Nov 2009 23:47:11 +0100
parents d062efcfbe9d
children ae55fc0d783d
files README bot.py bridge.py participant.py
diffstat 4 files changed, 174 insertions(+), 70 deletions(-) [+]
line wrap: on
line diff
--- a/README
+++ b/README
@@ -21,6 +21,8 @@ Changaco <changaco@changaco.net>
 
 
 > Changelog:
+0.3:
+ - new mode "bypass", this allows to connect more than one bot for each IRC server without having so many nickname conflicts that the bot looks like it is in limited mode
 0.2:
  - fixed many many bugs
  - new command system
@@ -52,6 +54,10 @@ Copy "example_config.xml" and modify it 
 - 'normal':
 The bot connects on IRC on behalf of XMPP users and connects on XMPP on behalf of IRC users.
 In this mode the bot automatically switches from normal to limited mode and back again when the IRC server clones limit is crossed.
+- 'bypass':
+The bot connects on IRC on behalf of XMPP users and connects on XMPP on behalf of IRC users.
+If a nickname is already used the bot tries to connect with a slightly different nickname, adding a "_" to the original nickname.
+In this mode the bot automatically switches to limited mode and back again when the IRC server clones limit is crossed.
 - 'limited':
 The bot only connects on XMPP on behalf of IRC users but NOT on IRC on behalf of XMPP.
 - 'minimal':
--- a/bot.py
+++ b/bot.py
@@ -294,7 +294,7 @@ class bot(Thread):
 						p = bridge.addParticipant('xmpp', resource, real_jid)
 						
 						# if we have the real jid check if the participant is a bot admin
-						if real_jid:
+						if real_jid and isinstance(p, participant):
 							for jid in self.admins_jid:
 								if xmpp.protocol.JID(jid).bareMatch(real_jid):
 									p.bot_admin = True
@@ -683,8 +683,8 @@ class bot(Thread):
 					continue
 				try:
 					p = bridge.getParticipant(connection.get_nickname())
-					if bridge.mode == 'normal':
-						bridge.switchFromNormalToLimitedMode()
+					if bridge.mode in ['normal', 'bypass']:
+						bridge.changeMode('limited')
 					else:
 						if p.irc_connection.really_connected == True:
 							p.irc_connection.part(bridge.irc_room, message=message)
@@ -700,13 +700,13 @@ class bot(Thread):
 		# Nickname callbacks
 		# TODO: move this into irclib.py
 		if event.eventtype() == 'nicknameinuse':
-			connection._call_nick_callbacks('nicknameinuse')
+			connection._call_nick_callbacks('nicknameinuse', arguments=[event])
 			return
 		if event.eventtype() == 'nickcollision':
-			connection._call_nick_callbacks('nickcollision')
+			connection._call_nick_callbacks('nickcollision', arguments=[event])
 			return
 		if event.eventtype() == 'erroneusnickname':
-			connection._call_nick_callbacks('erroneusnickname')
+			connection._call_nick_callbacks('erroneusnickname', arguments=[event])
 			return
 		
 		
--- a/bridge.py
+++ b/bridge.py
@@ -38,7 +38,7 @@ class bridge:
 	_error = 4
 	_nothing = 5
 	_say_levels = ['all', 'info', 'notice', 'warning', 'error', 'nothing']
-	_modes = ['normal', 'limited', 'minimal']
+	_modes = ['normal', 'bypass', 'limited', 'minimal']
 	
 	
 	def __init__(self, owner_bot, xmpp_room_jid, irc_room, irc_server, mode, say_level, irc_port=6667):
@@ -144,7 +144,7 @@ class bridge:
 				if from_protocol == 'irc' and isinstance(p.irc_connection, ServerConnection) and p.irc_connection.really_connected == True or from_protocol == 'xmpp' and isinstance(p.xmpp_c, xmpp.client.Client) and isinstance(p.muc, xmpp.muc):
 					if irc_id:
 						p.irc_connection.irc_id = irc_id
-					return
+					return p
 				self.bot.error('===> Debug: "'+nickname+'" is on both sides of bridge "'+str(self)+'"', debug=True)
 				self.say('[Warning] The nickname "'+nickname+'" is used on both sides of the bridge, please avoid that if possible')
 				if isinstance(p.irc_connection, ServerConnection):
@@ -159,6 +159,10 @@ class bridge:
 			return p
 		except 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:
@@ -172,7 +176,7 @@ class bridge:
 			return
 		self.participants.append(p)
 		self.lock.release()
-		if self.mode != 'normal':
+		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)
@@ -182,13 +186,71 @@ class bridge:
 		return p
 	
 	
+	def changeMode(self, new_mode):
+		if new_mode == self.mode:
+			return 'Mode is already equal to '+self.mode
+		
+		unhandled = 'Error: unhandled mode changing from '+self.mode+' to '+new_mode
+		
+		if new_mode in ['normal', 'bypass']:
+			
+			if self.mode[-7:] == 'limited':
+				# From  [{normal,bypass}-]limited  to  {normal,bypass}
+				pass  # duplicates of XMPP users are created below
+			
+			elif self.mode == 'minimal':
+				# From  minimal  to  {normal,bypass}
+				# create duplicates of IRC users, duplicates of XMPP users are created below
+				for p in self.participants:
+					if p.protocol == 'irc':
+						p.createDuplicateOnXMPP()
+			
+			else:
+				# Unhandled mode changing
+				return unhandled
+			
+			# create duplicates of XMPP users
+			for p in self.participants:
+				if p.protocol == 'xmpp':
+					p.createDuplicateOnIRC()
+			
+		elif new_mode[-7:] == 'limited':
+			
+			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.bot.error('===> Bridge is switching to limited mode. Limit seems to be '+str(self.irc_connections_limit)+' on "'+self.irc_server+'".')
+				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+'".')
+				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 limited mode')
+		
+		else:
+			# Unhandled mode changing
+			return unhandled
+		
+		self.mode = new_mode
+		self.bot.error('===> Bridge is switching from '+self.mode+' to '+new_mode+' mode.')
+		self.say('[Notice] Bridge is switching from '+self.mode+' to '+new_mode+' mode.')
+	
+	
 	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 participant_ in self.participants:
-			if participant_.nickname == nickname:
+		for p in self.participants:
+			if nickname in [p.nickname, p.duplicate_nickname]:
 				self.lock.release()
-				return participant_
+				return p
 		self.lock.release()
 		raise NoSuchParticipantException('there is no participant using the nickname "'+nickname+'" in this bridge')
 	
@@ -268,8 +330,8 @@ class bridge:
 			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.switchFromLimitedToNormalMode()
-				if self.mode != 'normal':
+					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':
@@ -336,35 +398,6 @@ class bridge:
 		self.participants = []
 	
 	
-	def switchFromLimitedToNormalMode(self):
-		if self.mode != 'normal-limited':
-			return
-		self.bot.error('===> Bridge is switching to normal mode.')
-		self.say('[Notice] Bridge is switching to normal mode.')
-		self.mode = 'normal'
-		for p in self.participants:
-			if p.protocol == 'xmpp':
-				p.createDuplicateOnIRC()
-	
-	
-	def switchFromNormalToLimitedMode(self):
-		if self.mode != 'normal':
-			return
-		self.mode = 'normal-limited'
-		i = 0
-		for p in self.participants:
-			if p.protocol == 'xmpp':
-				i += 1
-				if isinstance(self.irc_connection, ServerConnection):
-					p.irc_connection.close('Bridge is switching to limited mode')
-					p.irc_connection = None
-		self.irc_connections_limit = i
-		self.bot.error('===> Bridge is switching to limited mode. Limit seems to be '+str(self.irc_connections_limit)+' on "'+self.irc_server+'".')
-		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+'".')
-		xmpp_participants_nicknames = self.get_participants_nicknames_list(protocols=['xmpp'])
-		self.say('[Info] Participants on XMPP: '+'  '.join(xmpp_participants_nicknames), on_xmpp=False)
-	
-	
 	def __str__(self):
 		return self.irc_room+'@'+self.irc_server+' <-> '+self.xmpp_room_jid
 	
--- a/participant.py
+++ b/participant.py
@@ -22,6 +22,7 @@ from irclib import ServerNotConnectedErr
 from encoding import *
 from threading import Thread
 from time import sleep
+import re
 
 
 class participant:
@@ -31,37 +32,70 @@ class participant:
 		self.bridge = owner_bridge
 		self.protocol = protocol
 		self.nickname = nickname
+		self.duplicate_nickname = self.nickname
 		self.irc_connection = None
 		self.xmpp_c = None
 		self.muc = None
-		if protocol == 'xmpp':
+		if protocol == 'xmpp' and self.bridge.mode in ['normal', 'bypass']:
 			self.createDuplicateOnIRC()
-		elif protocol == 'irc':
+		elif protocol == 'irc' and self.bridge.mode != 'minimal':
 			self.createDuplicateOnXMPP()
-		else:
-			raise Exception('[Internal Error] bad protocol')
+	
+	
+	def _get_new_duplicate_nickname(self):
+		new_duplicate_nickname = self.duplicate_nickname
+		for i in xrange(5):
+			new_duplicate_nickname = new_duplicate_nickname+'_'
+			if not self.bridge.hasParticipant(new_duplicate_nickname):
+				return new_duplicate_nickname
+		return None
 	
 	
 	def createDuplicateOnXMPP(self):
-		if isinstance(self.xmpp_c, xmpp.client.Client) or isinstance(self.irc_connection, ServerConnection) or self.bridge.mode == 'minimal' or self.nickname == 'ChanServ':
+		if isinstance(self.xmpp_c, xmpp.client.Client) or isinstance(self.irc_connection, ServerConnection):
 			return
-		self.xmpp_c = self.bridge.bot.get_xmpp_connection(self.nickname)
+		self.xmpp_c = self.bridge.bot.get_xmpp_connection(self.duplicate_nickname)
 		self.muc = xmpp.muc(self.bridge.xmpp_room_jid)
-		self.muc.join(self.xmpp_c, self.nickname, status='From IRC', callback=self._xmpp_join_callback)
+		self.join_muc()
+	
+	
+	def join_muc(self):
+		self.muc.join(self.xmpp_c, self.duplicate_nickname, status='From IRC', callback=self._xmpp_join_callback)
 	
 	
 	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)
+			m = '===> Debug: "'+self.nickname+'" duplicate succesfully created on XMPP side of bridge "'+str(self.bridge)+'"'
+			if self.nickname != self.duplicate_nickname:
+				m += ' using nickname "'+self.duplicate_nickname+'"'
+				self.bridge.say('[Info] "'+self.nickname+'" will appear as "'+self.duplicate_nickname+'" on XMPP because its real nickname is reserved or contains unauthorized characters')
+			self.bridge.bot.error(m, debug=True)
 		elif self.xmpp_c != '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')
-					if self.muc.connected == True:
-						self.muc.leave('Changed nickname to "'+self.nickname+'"')
+				except xmpp.muc.NicknameConflict as e:
+					if xmpp.protocol.JID(e.args[0]).getResource() != self.duplicate_nickname:
+						return
+					
+					if self.bridge.mode == 'bypass':
+						new_duplicate_nickname = self._get_new_duplicate_nickname()
+						if new_duplicate_nickname != None:
+							self.bridge.bot.error('===> Debug: "'+self.duplicate_nickname+'" is already used in the XMPP MUC or reserved on the XMPP server of bridge "'+str(self.bridge)+'", trying "'+new_duplicate_nickname+'"', debug=True)
+							if self.duplicate_nickname == self.nickname:
+								self.bridge.say('[Info] The nickname "'+self.duplicate_nickname+'" is used on both rooms or reserved on the XMPP server, please avoid that if possible')
+							self.duplicate_nickname = new_duplicate_nickname
+							if isinstance(self.xmpp_c, xmpp.client.Client):
+								self.bridge.bot.close_xmpp_connection(self.nickname)
+								self.xmpp_c = None
+							self.createDuplicateOnXMPP()
+							return
+					
+					else:
+						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')
+						if self.muc.connected == True:
+							self.muc.leave('Changed nickname to "'+self.nickname+'"')
 				except xmpp.muc.RoomIsFull:
 					self.bridge.bot.error('[Warning] XMPP MUC of bridge "'+str(self.bridge)+'" is full', send_to_admins=True)
 					self.bridge.say('[Warning] XMPP room is full')
@@ -72,30 +106,61 @@ class participant:
 	
 	
 	def createDuplicateOnIRC(self):
-		if isinstance(self.xmpp_c, xmpp.client.Client) or isinstance(self.irc_connection, ServerConnection) or self.bridge.mode != 'normal':
+		if isinstance(self.xmpp_c, xmpp.client.Client) or isinstance(self.irc_connection, ServerConnection):
 			return
 		sleep(1) # try to prevent "reconnecting too fast" shit
-		self.irc_connection = self.bridge.bot.irc.server(self.bridge.irc_server, self.bridge.irc_port, self.nickname)
+		self.irc_connection = self.bridge.bot.irc.server(self.bridge.irc_server, self.bridge.irc_port, self.duplicate_nickname)
 		self.irc_connection.connect(nick_callback=self._irc_nick_callback)
 	
 	
 	def _irc_nick_callback(self, error, arguments=[]):
 		if error == None:
 			self.irc_connection.join(self.bridge.irc_room)
-			self.bridge.bot.error('===> Debug: "'+self.nickname+'" duplicate succesfully created on IRC side of bridge "'+str(self.bridge)+'"', debug=True)
+			m = '===> Debug: "'+self.nickname+'" duplicate succesfully created on IRC side of bridge "'+str(self.bridge)+'"'
+			if self.nickname != self.duplicate_nickname:
+				m += ' using nickname "'+self.duplicate_nickname+'"'
+				self.bridge.say('[Info] "'+self.nickname+'" will appear as "'+self.duplicate_nickname+'" on IRC because its real nickname is reserved or contains unauthorized characters')
+			self.bridge.bot.error(m, debug=True)
+		
 		elif self.irc_connection != 'both':
-			if error == 'nicknameinuse':
-				self.bridge.bot.error('===> Debug: "'+self.nickname+'" is used or reserved on the IRC server of bridge "'+str(self.bridge)+'"', debug=True)
-				self.bridge.say('[Warning] The nickname "'+self.nickname+'" is used or reserved on the IRC server, please avoid that if possible')
-			elif error == 'nickcollision':
-				self.bridge.bot.error('===> Debug: "'+self.nickname+'" is used or reserved on the IRC server of bridge "'+str(self.bridge)+'"', debug=True)
-				self.bridge.say('[Warning] The nickname "'+self.nickname+'" is used or reserved on the IRC server, please avoid that if possible')
+			
+			if error in ['nicknameinuse', 'nickcollision']:
+				if arguments[0].arguments()[0] != self.duplicate_nickname:
+					return
+				
+				if self.bridge.mode == 'bypass':
+					new_duplicate_nickname = self._get_new_duplicate_nickname()
+					if new_duplicate_nickname != None:
+						self.bridge.bot.error('===> Debug: "'+self.duplicate_nickname+'" is already used or reserved on the IRC server of bridge "'+str(self.bridge)+'", trying "'+new_duplicate_nickname+'"', debug=True)
+						if self.duplicate_nickname == self.nickname:
+							self.bridge.say('[Info] The nickname "'+self.duplicate_nickname+'" is used or reserved on the IRC server, please avoid that if possible')
+						self.duplicate_nickname = new_duplicate_nickname
+						if isinstance(self.irc_connection, ServerConnection):
+							self.irc_connection.close('')
+							self.irc_connection = error
+						self.createDuplicateOnIRC()
+						return
+				
+				else:
+					self.bridge.bot.error('===> Debug: "'+self.nickname+'" is used or reserved on the IRC server of bridge "'+str(self.bridge)+'"', debug=True)
+					self.bridge.say('[Warning] The nickname "'+self.nickname+'" is used or reserved on the IRC server, please avoid that if possible')
+			
 			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 unauthorized characters and cannot be used in the IRC channel, please avoid that if possible')
+				if self.bridge.mode == 'bypass':
+					self.duplicate_nickname = re.sub('[^a-zA-Z]', '', self.nickname)
+					if isinstance(self.irc_connection, ServerConnection):
+						self.irc_connection.close('')
+						self.irc_connection = error
+					self.createDuplicateOnIRC()
+					return
+				else:
+					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 unauthorized characters and cannot be used in the IRC channel, please avoid that if possible')
+			
 			elif error == 'nicknametoolong':
 				self.bridge.bot.error('===> Debug: "'+self.nickname+'" got "nicknametoolong" on bridge "'+str(self.bridge)+'"', debug=True)
 				self.bridge.say('[Warning] The nickname "'+self.nickname+'" is too long (limit seems to be '+str(arguments[0])+') and cannot be used in the IRC channel, please avoid that if possible')
+			
 			else:
 				self.bridge.bot.error('===> Debug: unknown error while adding "'+self.nickname+'" to IRC side of bridge "'+str(self.bridge)+'"', debug=True)
 				self.bridge.say('[Warning] unknown error while adding "'+self.nickname+'" to IRC side of bridge')
@@ -117,6 +182,7 @@ class participant:
 			
 			else:
 				self.nickname = newnick
+				self.duplicate_nickname = newnick
 				if isinstance(self.irc_connection, ServerConnection):
 					if self.irc_connection.used_by == 1:
 						self.irc_connection.nick(newnick, callback=self._irc_nick_callback)
@@ -135,6 +201,7 @@ class participant:
 			
 			else:
 				self.nickname = newnick
+				self.duplicate_nickname = newnick
 				if isinstance(self.xmpp_c, xmpp.client.Client):
 					for b in self.bridge.bot.bridges:
 						if b.hasParticipant(oldnick) and b.irc_server != self.bridge.irc_server:
@@ -227,7 +294,6 @@ class participant:
 			message = ''
 		self._close_xmpp_connection(message)
 		self._close_irc_connection(message)
-		self.nickname = None
 	
 	
 	def _close_xmpp_connection(self, message):
@@ -256,5 +322,4 @@ class participant:
 	
 	
 	def __del__(self):
-		if self.nickname != None:
-			self.leave('')
\ No newline at end of file
+		self.leave('')