Mercurial > xib
view bot.py @ 22:e2bd4de698e5
Solved an XMPP resource conflict that would have happened when someone on IRC changed its nickname and later its old nickname would be used again. In other words, the bot no longer uses nicknames as XMPP resources.
Signed-off-by: Charly COSTE <changaco@changaco.net>
author | Charly COSTE <changaco@changaco.net> |
---|---|
date | Thu, 20 Aug 2009 17:49:40 +0200 |
parents | 801160b4136f |
children | abdb7a2b6c6d |
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 import xml.parsers.expat 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.nickname = 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.nickname) 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 xml.parsers.expat.ExpatError: self.error('=> Debug: received invalid stanza', debug=True) continue def _xmpp_presence_handler(self, dispatcher, presence): """[Internal] Manage XMPP presence.""" xmpp_c = dispatcher._owner if xmpp_c.nickname != self.nickname: 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, dispatcher, 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, dispatcher, message): """[Internal] Manage XMPP messages.""" xmpp_c = dispatcher._owner 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 xmpp_c.nickname == self.nickname: 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', 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 xmpp_c.nickname != self.nickname: 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_level, 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_level, 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, nickname): if self.xmpp_connections.has_key(nickname): c = self.xmpp_connections[nickname] c.used_by += 1 self.error('===> Debug: using existing XMPP connection for "'+nickname+'", now used by '+str(c.used_by)+' bridges', debug=True) return c self.error('===> Debug: opening new XMPP connection for "'+nickname+'"', debug=True) c = xmpp.client.Client(self.bare_jid.getDomain(), debug=[]) self.xmpp_connections[nickname] = c c.used_by = 1 c.nickname = nickname c.connect() c.auth(self.bare_jid.getNode(), self.password) 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, nickname): self.xmpp_connections[nickname].used_by -= 1 if self.xmpp_connections[nickname].used_by < 1: self.error('===> Debug: closing XMPP connection for "'+nickname+'"', debug=True) del self.xmpp_connections[nickname] else: self.error('===> Debug: XMPP connection for "'+nickname+'" is now used by '+str(self.xmpp_connections[nickname].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)