Mercurial > xib
diff bot.py @ 17:32a35f7eff70
Rewrote/modified many things, multiple bridges should now work and are preferred over multiple bots.
Signed-off-by: Charly COSTE <changaco@changaco.net>
author | Charly COSTE <changaco@changaco.net> |
---|---|
date | Thu, 20 Aug 2009 01:00:54 +0200 |
parents | 1a1f2a0d35c7 |
children | 3cdf7bb580da |
line wrap: on
line diff
--- a/bot.py +++ b/bot.py @@ -36,27 +36,24 @@ class bot(Thread): def __init__(self, jid, password, nickname, error_fd=sys.stderr, debug=False): Thread.__init__(self) self.commands = ['!xmpp_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 = 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() + self.xmpp_c = self.get_xmpp_connection(self.jid.getResource()) except: self.error('Error: XMPP Connection failed') raise @@ -65,6 +62,7 @@ class bot(Thread): 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") @@ -73,55 +71,70 @@ class bot(Thread): def _xmpp_loop(self): + """[Internal] XMPP infinite loop.""" while True: self.xmpp_c.Process(5) + try: + for c in self.xmpp_connections.itervalues(): + if hasattr(c, 'Process'): + c.Process(5) + else: + sleep(1) + except RuntimeError: + pass 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) - 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 in ['xmpp', 'both']: - 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: - if presence.getType() != 'unavailable': - try: - bridge.addParticipant('xmpp', resource) - except Exception: - pass + 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 @@ -134,37 +147,42 @@ class bot(Thread): 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('==> 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(), 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 - 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) + # MUC delayed message return - self.error('==> Debug: Received XMPP message.', debug=True) - self.error(message.__str__(fancy=1), debug=True) + + 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: @@ -172,149 +190,251 @@ class bot(Thread): resource = unicode(from_.getResource()) if resource == '': # message comes from the room itself - pass + 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()) - 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('==> 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""" - 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', 'privnotice']: - 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 + """[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']: + 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: - connection.really_connected = True - self.error('===> Debug: now really connected', debug=True) - 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) + 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 - 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) + + + # 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], from_)) + 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': - 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 connection.closing == False: - connection.bridge.switchToLimitedMode() - if connection.closing == True: - connection.close() + # 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': - if connection.nick_callback: - connection.nick_callback('nicknameinuse') - else: - self.error('=> Debug: no nick callback for "'+str(event.target())+'"', debug=True) + connection._call_nick_callbacks('nicknameinuse') 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() == 'namreply': - for nickname in re.split('(?:^[@\+]?|(?: [@\+]?)*)', event.arguments()[2].strip()): - if nickname == '': - continue - 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) + connection._call_nick_callbacks('erroneusnickname') return - try: - if event.source() == None or not '!' in event.source(): - return - from_ = connection.bridge.getParticipant(event.source().split('!')[0]) - if event.eventtype() == 'quit' or event.eventtype() == 'part' and event.target() == connection.bridge.irc_room: - if from_.protocol in ['irc', 'both']: - connection.bridge.removeParticipant('irc', from_.nickname, event.arguments()[0]) - return - if event.eventtype() == 'nick' and from_.protocol in ['irc', 'both']: - from_.changeNickname(event.target(), 'xmpp') - except NoSuchParticipantException: - self.error('===> Debug: NoSuchParticipantException "'+event.source().split('!')[0]+'"', debug=True) - return - 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, self.respond(event.arguments()[0], from_)) - return - if to_.protocol == 'xmpp': - from_.sayOnXMPPTo(to_.nickname, event.arguments()[0]) + + + # Unhandled events + self.error('=> Debug: event not handled', debug=True) - def new_bridge(self, xmpp_room, irc_room, irc_server, irc_port=6667): + 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, irc_port=irc_port) + b = bridge(self, xmpp_room, irc_room, irc_server, mode, say_participants_list, 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) + 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): if message.strip() == '!xmpp_participants': - xmpp_participants_nicknames = participant.bridge.get_xmpp_participants_nicknames_list() + xmpp_participants_nicknames = participant.bridge.get_participants_nicknames_list(protocols=['xmpp']) return 'participants on '+participant.bridge.xmpp_room.room_jid+': '+' '.join(xmpp_participants_nicknames) else: return 'commands: '+' '.join(self.commands) + def __del__(self): - for bridge in bridges: - del bridge \ No newline at end of file + for bridge in self.bridges: + self.removeBridge(bridge) \ No newline at end of file