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 |