Commit | Line | Data |
---|---|---|
3c79fd35 P |
1 | #!/usr/bin/env python |
2 | # -*- coding: utf-8 -*- | |
3 | """ | |
4 | Robot pour offrir des services via jabber. | |
5 | ||
6 | Copyright : Agence universitaire de la Francophonie | |
7 | Licence : GNU General Public Licence, version 2 | |
8 | Auteur : Jean Christophe André | |
9 | Date de création : 22 juin 2008 | |
10 | """ | |
11 | ||
12 | import sys, getopt, xmpp | |
13 | import urllib | |
14 | from xml.dom import minidom | |
15 | ||
16 | DEMLOG_URL="http://bacasable.auf/rest/demlog/%s" | |
17 | BUDGET_URL="http://intranet.auf/reflets-web/budget/?pp=%s" | |
18 | PHONE_URL="http://intranet.auf/intranet_annuaire_telephonique.php3" | |
19 | ||
20 | def xmlurlopen(url): | |
21 | xmlfile = urllib.urlopen(url) | |
22 | xml = xmlfile.read() | |
23 | if not xml.startswith('<?xml '): | |
24 | typeheader = xmlfile.headers.typeheader | |
25 | if typeheader and (typeheader.find('=') >= 0): | |
26 | (junk,charset) = typeheader.rsplit('=',1) | |
27 | else: | |
28 | charset = 'utf-8' | |
29 | xml = ('<?xml version="1.0" encoding="%s"?>\n' % charset) + xml | |
30 | xmlfile.close() | |
31 | return xml | |
32 | ||
33 | def _xmltojab_r(node, prefix = ''): | |
34 | if node.nodeType == node.TEXT_NODE: | |
35 | result = node.nodeValue.strip().replace('\n', '\\n') | |
36 | if result <> '': result = prefix + result + '\n' | |
37 | return result | |
38 | elif node.nodeType == node.ELEMENT_NODE: | |
39 | prefix += node.nodeName + ' : ' | |
40 | if node.nodeValue <> None: | |
41 | value = node.nodeValue.strip().replace('\n', '\\n') | |
42 | if value <> '': value = prefix + value + '\n' | |
43 | result = value | |
44 | else: | |
45 | result = '' | |
46 | for child in node.childNodes: | |
47 | result += _xmltojab_r(child, prefix) | |
48 | return result | |
49 | else: | |
50 | return prefix + u'Type de noeud inconnu...\n' | |
51 | ||
52 | def xmltojab(xml, startwith=''): | |
53 | try: | |
54 | doc = minidom.parseString(xml) | |
55 | except: | |
56 | return '' | |
57 | if startwith <> '': | |
58 | nodes = [] | |
59 | for node in doc.getElementsByTagName(startwith): | |
60 | nodes += node.childNodes | |
61 | else: | |
62 | nodes = doc.documentElement.childNodes | |
63 | result='' | |
64 | for node in nodes: | |
65 | result += _xmltojab_r(node) | |
66 | return result | |
67 | ||
68 | def _phonefind(xhtml, nom): | |
69 | try: | |
70 | xmldoc = minidom.parseString(xhtml) | |
71 | except: | |
72 | return '' | |
73 | # recherche de la division contenant les infos | |
74 | main_content = None | |
75 | for div in xmldoc.getElementsByTagName('div'): | |
76 | if div.hasAttribute('id') and div.getAttribute('id') == 'main_content': | |
77 | main_content = div | |
78 | break | |
79 | if main_content == None: return '' | |
80 | # parcours des infos pour trouver le nom demandé | |
81 | result = u'' | |
82 | nom = nom.lower() | |
83 | for tr in main_content.getElementsByTagName('tr'): | |
84 | tds = tr.getElementsByTagName('td') | |
85 | if len(tds) == 3: | |
86 | # FIXME: gérer les futurs changements de structure !! | |
87 | nom_complet = tds[0].firstChild.firstChild.nodeValue | |
88 | if nom_complet.lower().find(nom) >= 0: | |
89 | result += nom_complet + ':' | |
90 | telpub = tds[1].firstChild | |
91 | if telpub <> None: | |
92 | result += u" %s (public)" % telpub.nodeValue | |
93 | telip = tds[2].firstChild | |
94 | if telip <> None: | |
95 | result += u" %s (VoIP)" % telip.nodeValue | |
96 | result += "\n" | |
97 | return result | |
98 | ||
99 | class Bot(object): | |
100 | _jid = _password = '' | |
101 | _admins = ['jean-christophe.andre@auf.org', 'doan.manh.ha@auf.org', 'thomas.noel@auf.org'] | |
102 | _salons = {} | |
103 | _quitter = False | |
104 | _quitter_message = 'Au revoir !' | |
105 | ||
106 | def __init__(self, jid, password): | |
107 | self._jid = xmpp.JID(jid) | |
108 | self._password = password | |
109 | if self._jid.getResource() == '': | |
110 | self._jid.setResource('botap') | |
111 | ||
112 | self._client = xmpp.Client(self._jid.getDomain(), debug=[]) | |
113 | try: | |
114 | if not self._client.connect(): | |
115 | raise IOError, u"can't connect to '%s'" % self._jid.getDomain() | |
116 | except IOError: | |
117 | raise IOError, u"can't connect to '%s'" % self._jid.getDomain() | |
118 | self._client.RegisterHandler('presence', self._presenceHandler) | |
119 | self._client.RegisterHandler('message', self._messageHandler) | |
120 | self._client.RegisterHandler('iq', self._iqHandler) | |
121 | # self._client.UnregisterDisconnectHandler(self._client.DisconnectHandler) | |
122 | ||
123 | auth = self._client.auth(self._jid.getNode(), self._password, self._jid.getResource()) | |
124 | if not auth: | |
125 | raise IOError, u"unable to authenticate '%s'" % self._jid.getNode() | |
126 | self._client.sendPresence(jid=self._jid.getDomain()) | |
127 | if self._jid.getDomain() == 'auf.org': | |
128 | self.cmd_join('test@reunion.auf.org/' + self._jid.getResource()) | |
129 | #self.cmd_join('tap@reunion.auf.org/' + self._jid.getResource()) | |
130 | if self._jid.getDomain() == 'net127': | |
131 | self.cmd_join('test@conf.net127/' + self._jid.getResource()) | |
132 | ||
133 | def __del__(self): | |
134 | if not self._client.isConnected(): return | |
135 | if self._jid.getDomain() == 'auf.org': | |
136 | self.cmd_part('tap@reunion.auf.org/' + self._jid.getResource()) | |
137 | self.cmd_part('test@reunion.auf.org/' + self._jid.getResource()) | |
138 | if self._jid.getDomain() == 'net127': | |
139 | self.cmd_part('test@conf.net127/' + self._jid.getResource()) | |
140 | self._client.sendPresence(typ='unavailable') | |
141 | self._client.disconnect() | |
142 | ||
143 | def _publicReply(self, message, text): | |
144 | reply = message.buildReply(text) | |
145 | if message.getType() == 'groupchat': | |
146 | reply.setType('groupchat') | |
147 | reply.getTo().setResource('') | |
148 | self._client.send(reply) | |
149 | print u"--> reply=[%s]" % reply | |
150 | ||
151 | def _presenceHandler(self, client, presence): | |
152 | print u"<-- presence=[%s]" % presence | |
153 | # gestion de notre propre présence | |
154 | if presence.getTo() <> self._jid: return | |
155 | # confirmation de l'entrée dans un salon | |
156 | if presence.getFrom() in self._salons: | |
157 | self._salons[presence.getFrom()]['joined'] = True | |
158 | self._client.send(xmpp.Message(to=presence.getFrom().getStripped(),typ='groupchat',body='Bonjour tout le monde !')) | |
159 | ||
160 | def _messageHandler(self, client, message): | |
161 | print u"<-- message=[%s]" % message | |
162 | jid = message.getFrom() | |
163 | text = message.getBody() | |
164 | # FIXME: on ne traite pas les messages d'erreur (pour le moment) | |
165 | if message.getType() == 'error': return | |
166 | # FIXME: on ne traite pas les messages vides de texte (pour le moment) | |
167 | # FIXME: du coup on rate les invitations, au moins... | |
168 | if text == None: return | |
169 | # on ne traite pas les messages qui viennent de nous-même (sinon boucle) | |
170 | if (jid == self._jid) or (jid in self._salons): return | |
171 | # on ne traite pas les messages des salons hormis les commandes explicites | |
172 | if (message.getType() == 'groupchat') and not text.startswith('!'): return | |
173 | print u"... on traite..." | |
174 | if text.startswith('!'): text = text[1:] | |
175 | if text.find(' ') >= 0: | |
176 | command,args = text.split(' ',1) | |
177 | else: | |
178 | command,args = text,'' | |
179 | if command == 'aide': | |
180 | reply = """Aide de %s : | |
181 | aide cette aide | |
182 | dire <texte> répète ce texte en public | |
183 | inviter <jid> invite ce contact dans le salon en cours (cassé) | |
184 | entrer <salon> entre dans un salon | |
185 | sortir <salon> sort du salon en cours | |
186 | demlog <numéro> affiche les données de cette DEMLOG | |
187 | tel <nom> affiche le(s) numéro(s) de téléphone""" % self._jid | |
188 | self._publicReply(message, reply) | |
189 | elif command == 'dire': | |
190 | if args == '': | |
191 | self._publicReply(message, "Dire quoi ?") | |
192 | else: | |
193 | # est-ce une commande publique ? | |
194 | if message.getType() == 'groupchat': | |
195 | reply = message.buildReply(args) | |
196 | reply.setType('groupchat') | |
197 | reply.getTo().setResource('') | |
198 | self._client.send(reply) | |
199 | # est-ce qu'on parle au bot via le contact d'un salon ? | |
200 | elif message.getTo() in self._salons: | |
201 | reply = message.buildReply(args) | |
202 | reply.setTo(message.getTo().getStripped()) | |
203 | self._client.send(reply) | |
204 | # envoyer sur tous les salons... gasp! | |
205 | else: | |
206 | reply = message.buildReply(args) | |
207 | reply.setType('groupchat') | |
208 | for salon in self._salons.keys(): | |
209 | reply.setTo(xmpp.JID(salon).getStripped()) | |
210 | self._client.send(reply) | |
211 | # envoi à une destination précise : trouver à qui on veut parler | |
212 | #if (args.find('@') >= 0) and (args.find('@') < args.find(' ')): | |
213 | # say_jid,args = args.split(' ',1) | |
214 | # reply = message.buildReply(args) | |
215 | # reply.setTo(say_jid) | |
216 | elif command == 'inviter': | |
217 | if args == '': | |
218 | self._publicReply(message, "Inviter qui ?") | |
219 | else: | |
220 | for un_jid in args.split(' '): | |
221 | self.cmd_invite(un_jid, jid.getStripped()) | |
222 | elif command == 'entrer': | |
223 | if args == '': | |
224 | self._publicReply(message, "Entrer dans quel salon ?") | |
225 | else: | |
226 | self.cmd_join(args + '/' + self._jid.getResource()) | |
227 | elif command == 'sortir': | |
228 | if args == '' and (message.getType() == "groupchat") : | |
229 | args = jid.getStripped() | |
230 | if args == '': | |
231 | self._publicReply(message, "Sortir de quel salon ?") | |
232 | else: | |
233 | self.cmd_part(args + '/' + self._jid.getResource()) | |
234 | elif command == u'numéro': | |
235 | self._publicReply(message, """C'est une référence à la série culte « Le Prisonnier ».\nvoir http://fr.wikipedia.org/wiki/Le_Prisonnier""") | |
236 | elif command == 'quitter': | |
237 | if jid.getStripped() == self._jid.getStripped(): | |
238 | self._publicReply(message, "D'accord, au revoir !") | |
239 | if args <> '': self._quitter_message = args | |
240 | self._quitter = True | |
241 | else: | |
242 | self._publicReply(message, u"Commande '%s' non autorisée." % command) | |
243 | #elif command == 'budget': | |
244 | # try: | |
245 | # xml = urllib.urlopen(BUDGET_URL % args).read() | |
246 | # doc = minidom.parseString(xml).documentElement | |
247 | # reply = u"Budget pour %s :" % args | |
248 | # for node in doc.childNodes: | |
249 | # if node.nodeType == node.ELEMENT_NODE: | |
250 | # for value in [n.wholeText for n in node.childNodes]: | |
251 | # reply += '\n%s : %s' % (node.nodeName, value) | |
252 | # except: | |
253 | # reply = u"Erreur pour %s." % args | |
254 | # self._publicReply(message, reply) | |
255 | elif command == 'demlog': | |
256 | if args == '': | |
257 | reply = u"Quelle numéro de DEMLOG ?" | |
258 | else: | |
259 | demlog = xmlurlopen(DEMLOG_URL % args) | |
260 | reply = xmltojab(demlog, 'demlog').rstrip() | |
261 | if reply <> '': | |
262 | reply = (u"Données pour la DEMLOG %s :\n" % args) + reply | |
263 | else: | |
264 | reply = u"Erreur pour la DEMLOG %s." % args | |
265 | self._publicReply(message, reply) | |
266 | elif command == 'tel': | |
267 | if args == '': | |
268 | reply = u"Téléphone de qui ?" | |
269 | else: | |
270 | reply = _phonefind(xmlurlopen(PHONE_URL), args).rstrip() | |
271 | if reply <> '': | |
272 | reply = u"Recherche de téléphone pour '%s' :\n%s" % (args, reply) | |
273 | else: | |
274 | reply = u"Pas de téléphone trouvé pour '%s'." % args | |
275 | self._publicReply(message, reply) | |
276 | else: | |
277 | self._publicReply(message, "Commande '%s' inconnue." % command) | |
278 | ||
279 | def _iqHandler(self, client, iq): | |
280 | print u"<-- iq=[%s]" % iq | |
281 | ||
282 | def cmd_join(self, room, password=''): | |
283 | #if self._salons.has_key(room) and self._salons[room]['joined']: return | |
284 | self._salons[room] = { 'joined': False, 'password': password } | |
285 | p = xmpp.Presence(to=room, priority='0', show='available', status="Je ne suis pas un numéro, je suis un bot libre !") | |
286 | x = p.setTag('x', namespace=xmpp.NS_MUC) | |
287 | if password <> '': x.setTagData('password', password) | |
288 | x.addChild('history', {'maxchars':'0','maxstanzas':'0'}) | |
289 | self._client.send(p) | |
290 | ||
291 | def cmd_part(self, room): | |
292 | p = xmpp.Presence(to=room, typ='unavailable') | |
293 | p.setTag('x', namespace=xmpp.NS_MUC) | |
294 | self._client.send(p) | |
295 | ||
296 | def cmd_invite(self, jid, room): | |
297 | m = xmpp.Message(to=jid,typ='normal',frm=room) | |
298 | x = m.setTag('x', namespace=xmpp.NS_MUC_USER) | |
299 | invite = x.addChild('invite', {'from':self._jid}) | |
300 | reason = invite.addChild('reason') | |
301 | reason.addData(u"Vous êtes invité sur '%s'." % room) | |
302 | m.setTag('x', {'jid':room}, 'jabber:x:conference') | |
303 | print "DEBUG: m=[%s]" % m | |
304 | self._client.send(m) | |
305 | ||
306 | def admins(self, new_admins=False): | |
307 | old_admins = self._admins | |
308 | if new_admins: self.admins = new_admins | |
309 | return old_admins | |
310 | ||
311 | def run(self): | |
312 | while (not self._quitter): | |
313 | try: | |
314 | self._client.Process(1) | |
315 | except KeyboardInterrupt: | |
316 | self._quitter = True | |
317 | self._client.send(xmpp.Message(to='test@reunion.auf.org', body=self._quitter_message, typ='groupchat')) | |
318 | ||
319 | if __name__ == "__main__": | |
320 | if len(sys.argv)<>3: | |
321 | print "Usage: %s username@domain.tld password" % __file__ | |
322 | sys.exit(-1) | |
323 | try: | |
324 | bot = Bot(jid=sys.argv[1], password=sys.argv[2]) | |
325 | except IOError, msg: | |
326 | print "ERROR: %s" % msg | |
327 | sys.exit(-1) | |
328 | bot.run() | |
329 | ||
330 | # vim: ts=4 sw=4 et |