Partage de mon script de remodelage du clavier QWERTY…
[progfou.git] / bot / Bot.py
CommitLineData
3c79fd35
P
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3"""
4Robot pour offrir des services via jabber.
5
6Copyright : Agence universitaire de la Francophonie
7Licence : GNU General Public Licence, version 2
8Auteur : Jean Christophe André
9Date de création : 22 juin 2008
10"""
11
12import sys, getopt, xmpp
13import urllib
14from xml.dom import minidom
15
16DEMLOG_URL="http://bacasable.auf/rest/demlog/%s"
17BUDGET_URL="http://intranet.auf/reflets-web/budget/?pp=%s"
18PHONE_URL="http://intranet.auf/intranet_annuaire_telephonique.php3"
19
20def 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
33def _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
52def 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
68def _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
99class 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 :
181aide cette aide
182dire <texte> répète ce texte en public
183inviter <jid> invite ce contact dans le salon en cours (cassé)
184entrer <salon> entre dans un salon
185sortir <salon> sort du salon en cours
186demlog <numéro> affiche les données de cette DEMLOG
187tel <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
319if __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