Mercurial > xib
comparison bot.py @ 0:4c842d23d4ce
Initial commit, version 0.1
Signed-off-by: Charly COSTE <changaco@changaco.net>
| author | Charly COSTE <changaco@changaco.net> |
|---|---|
| date | Sun, 16 Aug 2009 01:47:03 +0200 |
| parents | |
| children | f2d0a8b448db |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:4c842d23d4ce |
|---|---|
| 1 #!/usr/bin/env python | |
| 2 # -*- coding: utf-8 -*- | |
| 3 | |
| 4 # This program is free software: you can redistribute it and/or modify | |
| 5 # it under the terms of the GNU General Public License as published by | |
| 6 # the Free Software Foundation, either version 3 of the License, or | |
| 7 # (at your option) any later version. | |
| 8 # | |
| 9 # This program is distributed in the hope that it will be useful, | |
| 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 12 # GNU General Public License for more details. | |
| 13 # | |
| 14 # You should have received a copy of the GNU General Public License | |
| 15 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| 16 | |
| 17 | |
| 18 # *** CONTRIBUTORS *** | |
| 19 # Contributor: Changaco <changaco@changaco.net> | |
| 20 | |
| 21 | |
| 22 # *** Changelog *** | |
| 23 # 0.1: First release | |
| 24 | |
| 25 | |
| 26 # *** Versioning *** | |
| 27 # Major will pass to 1 when xib will be considered fault-tolerant | |
| 28 # After that major will only be changed if the new version is not retro-compatible (e.g. requires changes in config file) | |
| 29 | |
| 30 version = 0, 1 | |
| 31 | |
| 32 | |
| 33 import irclib | |
| 34 import xmppony as xmpp | |
| 35 from threading import Thread | |
| 36 from bridge import * | |
| 37 from time import sleep | |
| 38 import re | |
| 39 import sys | |
| 40 | |
| 41 | |
| 42 class bot(Thread): | |
| 43 | |
| 44 def __init__(self, jid, password, nickname, error_fd=sys.stderr, debug=False): | |
| 45 Thread.__init__(self) | |
| 46 self.jid = xmpp.protocol.JID(jid=jid) | |
| 47 self.nickname = nickname | |
| 48 self.password = password | |
| 49 self.error_fd = error_fd | |
| 50 self.debug = debug | |
| 51 self.bridges = [] | |
| 52 self.irc = irclib.IRC() | |
| 53 self.irc.add_global_handler('all_events', self._irc_event_handler) | |
| 54 self.irc_thread = Thread(target=self.irc.process_forever) | |
| 55 self.irc_thread.start() | |
| 56 # Open connection with XMPP server | |
| 57 try: | |
| 58 self.xmpp_c = xmpp.client.Client(self.jid.getDomain(), debug=[]) | |
| 59 self.xmpp_c.connect() | |
| 60 if self.jid.getResource() == '': | |
| 61 self.jid.setResource('xib-bot') | |
| 62 self.xmpp_c.auth(self.jid.getNode(), password, resource=self.jid.getResource()) | |
| 63 self.xmpp_c.RegisterHandler('presence', self._xmpp_presence_handler) | |
| 64 self.xmpp_c.RegisterHandler('iq', self._xmpp_iq_handler) | |
| 65 self.xmpp_c.RegisterHandler('message', self._xmpp_message_handler) | |
| 66 self.xmpp_c.sendInitPresence() | |
| 67 except: | |
| 68 self.error('Error: XMPP Connection failed') | |
| 69 raise | |
| 70 self.xmpp_thread = Thread(target=self._xmpp_loop) | |
| 71 self.xmpp_thread.start() | |
| 72 | |
| 73 | |
| 74 def error(self, s, debug=False): | |
| 75 if not debug or debug and self.debug: | |
| 76 try: | |
| 77 self.error_fd.write(auto_encode(s)+"\n") | |
| 78 except EncodingException: | |
| 79 self.error_fd.write('Error message cannot be transcoded.\n') | |
| 80 | |
| 81 | |
| 82 def _xmpp_loop(self): | |
| 83 while True: | |
| 84 self.xmpp_c.Process(5) | |
| 85 | |
| 86 | |
| 87 def _xmpp_presence_handler(self, xmpp_c, presence): | |
| 88 """[Internal] Manage XMPP presence.""" | |
| 89 self.error('==> Debug: Received XMPP presence.', debug=True) | |
| 90 self.error(presence.__str__(fancy=1), debug=True) | |
| 91 | |
| 92 if presence.getTo() != self.jid: | |
| 93 #self.error('=> Debug: Skipping XMPP presence not received on bot connection.', debug=True) | |
| 94 return | |
| 95 from_ = xmpp.protocol.JID(presence.getFrom()) | |
| 96 bare_jid = unicode(from_.getNode()+'@'+from_.getDomain()) | |
| 97 for bridge in self.bridges: | |
| 98 if bare_jid == bridge.xmpp_room.room_jid: | |
| 99 # presence comes from a muc | |
| 100 resource = unicode(from_.getResource()) | |
| 101 if resource == '': | |
| 102 # presence comes from the muc itself | |
| 103 # TODO: handle room deletion and muc server reboot | |
| 104 pass | |
| 105 else: | |
| 106 # presence comes from a participant of the muc | |
| 107 try: | |
| 108 p = bridge.getParticipant(resource) | |
| 109 if p.protocol == 'xmpp': | |
| 110 if presence.getType() == 'unavailable': | |
| 111 x = presence.getTag('x', namespace='http://jabber.org/protocol/muc#user') | |
| 112 if x and x.getTag('status', attrs={'code': '303'}): | |
| 113 # participant changed its nickname | |
| 114 item = x.getTag('item') | |
| 115 if not item: | |
| 116 self.error('Debug: bad stanza, no item element', debug=True) | |
| 117 return | |
| 118 new_nick = item.getAttr('nick') | |
| 119 if not new_nick: | |
| 120 self.error('Debug: bad stanza, new nick is not given', debug=True) | |
| 121 return | |
| 122 p.changeNickname(new_nick, 'irc') | |
| 123 return | |
| 124 # participant left | |
| 125 bridge.removeParticipant('xmpp', resource, presence.getStatus()) | |
| 126 except NoSuchParticipantException: | |
| 127 try: | |
| 128 bridge.addParticipant('xmpp', resource) | |
| 129 except Exception: | |
| 130 pass | |
| 131 return | |
| 132 | |
| 133 | |
| 134 def _xmpp_iq_handler(self, xmpp_c, iq): | |
| 135 """[Internal] Manage XMPP IQs.""" | |
| 136 self.error('=> Debug: Received XMPP iq.', debug=True) | |
| 137 self.error(iq.__str__(fancy=1), debug=True) | |
| 138 | |
| 139 | |
| 140 def _xmpp_message_handler(self, xmpp_c, message): | |
| 141 """[Internal] Manage XMPP messages.""" | |
| 142 if message.getType() == 'chat': | |
| 143 self.error('==> Debug: Received XMPP message.', debug=True) | |
| 144 self.error(message.__str__(fancy=1), debug=True) | |
| 145 if message.getTo() == self.jid: | |
| 146 xmpp_c.send(xmpp.protocol.Message(to=message.getFrom(), body=u'Sorry I am a bot I don\'t speak …', typ='chat')) | |
| 147 else: | |
| 148 from_bare_jid = unicode(message.getFrom().getNode()+'@'+message.getFrom().getDomain()) | |
| 149 for bridge in self.bridges: | |
| 150 if from_bare_jid == bridge.xmpp_room.room_jid: | |
| 151 # message comes from a room participant | |
| 152 try: | |
| 153 to_ = bridge.getParticipant(message.getTo().getResource()) | |
| 154 from_ = bridge.getParticipant(message.getFrom().getResource()) | |
| 155 except NoSuchParticipantException: | |
| 156 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) | |
| 157 return | |
| 158 if from_.protocol in ['xmpp', 'both']: | |
| 159 from_.sayOnIRCTo(to_.nickname, message.getBody()) | |
| 160 else: | |
| 161 self.error('==> Debug: received XMPP chat message from a non-XMPP participant, WTF ?', debug=True) | |
| 162 | |
| 163 elif message.getType() == 'groupchat': | |
| 164 # message comes from a room | |
| 165 if message.getTo() != self.jid: | |
| 166 self.error('=> Debug: Skipping XMPP MUC message not received on bot connection.', debug=True) | |
| 167 return | |
| 168 for child in message.getChildren(): | |
| 169 if child.getName() == 'delay': | |
| 170 self.error('=> Debug: Skipping XMPP MUC delayed message.', debug=True) | |
| 171 return | |
| 172 self.error('==> Debug: Received XMPP message.', debug=True) | |
| 173 self.error(message.__str__(fancy=1), debug=True) | |
| 174 from_ = xmpp.protocol.JID(message.getFrom()) | |
| 175 room_jid = unicode(from_.getNode()+'@'+from_.getDomain()) | |
| 176 for bridge in self.bridges: | |
| 177 if room_jid == bridge.xmpp_room.room_jid: | |
| 178 resource = unicode(from_.getResource()) | |
| 179 if resource == '': | |
| 180 # message comes from the room itself | |
| 181 pass | |
| 182 else: | |
| 183 # message comes from a participant of the room | |
| 184 try: | |
| 185 participant_ = bridge.getParticipant(resource) | |
| 186 except NoSuchParticipantException: | |
| 187 return | |
| 188 if participant_.protocol == 'xmpp': | |
| 189 participant_.sayOnIRC(message.getBody()) | |
| 190 elif participant_.protocol == 'both': | |
| 191 bridge.irc_connection.privmsg(bridge.irc_room, '<'+participant_.nickname+'> '+message.getBody()) | |
| 192 else: | |
| 193 self.error('==> Debug: Received XMPP message.', debug=True) | |
| 194 self.error(message.__str__(fancy=1), debug=True) | |
| 195 | |
| 196 | |
| 197 def _irc_event_handler(self, connection, event): | |
| 198 """[internal] Manage IRC events""" | |
| 199 if not connection.bridge in self.bridges: | |
| 200 # Not for us, ignore | |
| 201 return | |
| 202 if 'all' in event.eventtype(): | |
| 203 return | |
| 204 if 'motd' in event.eventtype(): | |
| 205 self.error('=> Debug: ignoring event containing "motd" in the eventtype ('+event.eventtype()+')', debug=True) | |
| 206 return | |
| 207 if event.eventtype() in ['pong', 'welcome', 'yourhost', 'created', 'myinfo', 'featurelist', 'luserclient', 'luserop', 'luserchannels', 'luserme', 'n_local', 'n_global', 'endofnames', 'luserunknown']: | |
| 208 self.error('=> Debug: ignoring '+event.eventtype(), debug=True) | |
| 209 return | |
| 210 if event.eventtype() == 'pubmsg' and connection.get_nickname() != connection.bridge.irc_connection.get_nickname(): | |
| 211 self.error('=> Debug: ignoring IRC pubmsg not received on bridge connection', debug=True) | |
| 212 return | |
| 213 if event.eventtype() == 'ping': | |
| 214 connection.pong(connection.get_server_name()) | |
| 215 return | |
| 216 self.error('==> Debug: Received IRC event.', debug=True) | |
| 217 self.error('server='+connection.get_server_name(), debug=True) | |
| 218 self.error('eventtype='+event.eventtype(), debug=True) | |
| 219 self.error('source='+str(event.source()), debug=True) | |
| 220 self.error('target='+str(event.target()), debug=True) | |
| 221 self.error('arguments='+str(event.arguments()), debug=True) | |
| 222 if event.eventtype() == 'disconnect': | |
| 223 if connection.get_nickname() == connection.bridge.irc_connection.get_nickname(): | |
| 224 # Lost bridge IRC connection, we must reconnect if we want the bridge to work | |
| 225 self.recreate_bridge(connection.bridge) | |
| 226 return | |
| 227 if connection.bridge.mode == 'normal' and event.arguments()[0] != 'Closing object': | |
| 228 connection.bridge.switchToLimitedMode() | |
| 229 elif event.eventtype() == 'nicknameinuse': | |
| 230 if connection.nick_callback: | |
| 231 connection.nick_callback('nicknameinuse') | |
| 232 else: | |
| 233 self.error('=> Debug: no nick callback for "'+str(event.target())+'"', debug=True) | |
| 234 return | |
| 235 elif event.eventtype() == 'erroneusnickname': | |
| 236 if connection.nick_callback: | |
| 237 connection.nick_callback('erroneusnickname') | |
| 238 else: | |
| 239 self.error('=> Debug: no nick callback for "'+str(event.target())+'"', debug=True) | |
| 240 return | |
| 241 elif event.eventtype() == 'umode': | |
| 242 if connection.nick_callback: | |
| 243 connection.nick_callback(None) | |
| 244 else: | |
| 245 self.error('=> Debug: no nick callback for "'+str(event.target())+'"', debug=True) | |
| 246 self.error('connection.nick_callback='+str(connection.nick_callback), debug=True) | |
| 247 return | |
| 248 elif event.eventtype() == 'namreply': | |
| 249 for nickname in re.split(' [@]?', event.arguments()[2].strip()): | |
| 250 try: | |
| 251 connection.bridge.addParticipant('irc', nickname) | |
| 252 except: | |
| 253 pass | |
| 254 return | |
| 255 elif event.eventtype() == 'join': | |
| 256 nickname = event.source().split('!')[0] | |
| 257 if nickname == self.nickname: | |
| 258 pass | |
| 259 else: | |
| 260 try: | |
| 261 connection.bridge.getParticipant(nickname) | |
| 262 except NoSuchParticipantException: | |
| 263 connection.bridge.addParticipant('irc', nickname) | |
| 264 try: | |
| 265 from_ = connection.bridge.getParticipant(event.source().split('!')[0]) | |
| 266 if event.eventtype() == 'quit': | |
| 267 if from_.protocol == 'irc': | |
| 268 connection.bridge.removeParticipant('irc', from_.nickname, event.arguments()[0]) | |
| 269 return | |
| 270 except NoSuchParticipantException: | |
| 271 return | |
| 272 except AttributeError: | |
| 273 pass | |
| 274 if event.eventtype() == 'pubmsg': | |
| 275 if from_.protocol == 'irc' or from_.protocol == 'both': | |
| 276 from_.sayOnXMPP(event.arguments()[0]) | |
| 277 elif event.eventtype() == 'privmsg': | |
| 278 if event.target() == None: | |
| 279 return | |
| 280 try: | |
| 281 to_ = connection.bridge.getParticipant(event.target().split('!')[0]) | |
| 282 except NoSuchParticipantException: | |
| 283 if event.target().split('!')[0] == self.nickname: | |
| 284 connection.privmsg(from_.nickname, u'Sorry I am a bot I don\'t speak …') | |
| 285 return | |
| 286 if to_.protocol == 'xmpp': | |
| 287 from_.sayOnXMPPTo(to_.nickname, event.arguments()[0]) | |
| 288 | |
| 289 | |
| 290 def new_bridge(self, xmpp_room, irc_room, irc_server, irc_port=6667): | |
| 291 """Create a bridge between xmpp_room and irc_room at irc_server.""" | |
| 292 b = bridge(self, xmpp_room, irc_room, irc_server, irc_port=irc_port) | |
| 293 self.bridges.append(b) | |
| 294 return b | |
| 295 | |
| 296 | |
| 297 def recreate_bridge(self, bridge): | |
| 298 """Disconnect and reconnect.""" | |
| 299 self.new_bridge(bridge.xmpp_room.room_jid, bridge.irc_room, bridge.irc_server) | |
| 300 self.bridges.remove(bridge) | |
| 301 del bridge | |
| 302 | |
| 303 | |
| 304 def __del__(self): | |
| 305 for bridge in bridges: | |
| 306 del bridge |
