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