]> gitweb.pimeys.fr Git - bots/basile.git/blob - basile.py
2fedf6ed76a4a0aa8dc9dfb3a644b892edd0dcc9
[bots/basile.git] / basile.py
1 #!/usr/bin/python
2 # -*- coding:utf8 -*-
3
4 # Codé par 20-100 (commencé le 23/04/12)
5
6 """ Un bot IRC destiné à s'interfacer avec la Note Kfet 2015 """
7
8 import threading
9 import random
10 import time
11 import json
12 import re
13 import os
14 import signal
15 import sys
16
17 # Oui, j'ai recodé ma version d'irclib pour pouvoir rattrapper les SIGHUP
18 sys.path.insert(0, "/home/vincent/scripts/python-myirclib")
19 import irclib
20 import ircbot
21
22 from commands import getstatusoutput as ex
23
24 #: Config de basile
25 import config
26 #: Module responsable du dialogue avec la NoteKfet2015
27 import nk
28
29 # la partie qui réfère au fichier lui-même est mieux ici
30 # sinon on réfère la config et pas le fichier lui-même
31 import os
32 config.thisfile = os.path.realpath(__file__)
33
34 def get_config_logfile(serveur):
35 """Renvoie le nom du fichier de log en fonction du ``serveur`` et de la config."""
36 serveurs = {"acoeur.crans.org" : "acoeur",
37 "irc.crans.org" : "crans"}
38 return config.logfile_template % (serveurs[serveur],)
39
40 def get_filesize():
41 """Récupère la taille de ce fichier."""
42 return ex("ls -s %s" % (config.thisfile))[1].split()[0]
43
44 def log(serveur, channel, auteur=None, message=None):
45 """Enregistre une ligne de log."""
46 if auteur == message == None:
47 # alors c'est que c'est pas un channel mais juste une ligne de log
48 chain = u"%s %s" % (time.strftime("%F %T"), channel)
49 else:
50 chain = u"%s [%s:%s] %s" % (time.strftime("%F %T"), channel, auteur, message)
51 f = open(get_config_logfile(serveur), "a")
52 f.write((chain + u"\n").encode("utf-8"))
53 f.close()
54 if config.debug_stdout:
55 print chain.encode("utf-8")
56
57 def ignore_event(serv, ev):
58 """Retourne ``True`` si il faut ignorer cet évènement."""
59 for (blackmask, exceptlist) in config.blacklisted_masks:
60 usermask = ev.source()
61 blackit = bool(irclib.mask_matches(usermask, blackmask))
62 exceptit = any([bool(irclib.mask_matches(usermask, exceptmask)) for exceptmask in exceptlist])
63 if exceptit: # Il est exempté
64 return False
65 else:
66 if blackit: # Il n'est pas exempté et matche la blacklist
67 return True
68
69 def regex_join(liste, avant=u".*(?:^| )", apres=u"(?:$|\.| |,|;).*"):
70 """Fabrique une regexp à partir d'une liste d'éléments à matcher."""
71 return avant + u"(" + u"|".join(liste) + u")" + apres
72
73 def is_something(chain, regexp=None, matches=[], avant=u".*(?:^| )", apres=u"(?:$|\.| |,|;).*",
74 case_sensitive=False):
75 """Vérifie si chain contient un des éléments de ``matches``.
76 Si ``regexp`` est fournie, c'est simplement elle qui est testée"""
77 if not case_sensitive:
78 chain = chain.lower()
79 apres = apres.lower()
80 avant = avant.lower()
81 if regexp == None:
82 regexp = regex_join(matches, avant, apres)
83 regexp = re.compile(regexp)
84 o = regexp.match(chain)
85 return o
86
87 def regexp_compile():
88 """Compilation des regexp à partir de la conf.
89 Place les résultats dans le namespace de ``config``"""
90 config.insult_regexp = regex_join(config.insultes, avant=u".*(?:^| |')")
91 config.insult_regexp_compiled = re.compile(config.insult_regexp)
92
93 config.not_insult_regexp = u".*pas %s%s" % (config.amplifier_regexp, config.insult_regexp)
94 config.not_insult_regexp_compiled = re.compile(config.not_insult_regexp)
95
96 config.compliment_regexp = regex_join(config.compliment_triggers, avant=u".*(?:^| |')")
97 config.compliment_regexp_compiled = re.compile(config.compliment_regexp)
98
99 config.perdu_regexp = regex_join(config.perdu)
100 config.perdu_regexp_compiled = re.compile(config.perdu_regexp)
101
102 config.tag_regexp = regex_join(config.tag_triggers)
103 config.tag_regexp_compiled = re.compile(config.tag_regexp)
104
105 config.gros_regexp = regex_join(config.gros)
106 config.gros_regexp_compiled = re.compile(config.gros_regexp)
107
108 config.tesla_regexp = regex_join(config.tesla_triggers, avant=u"^", apres="$")
109 config.tesla_regexp_compiled = re.compile(config.tesla_regexp)
110
111 config.merci_regexp = regex_join(config.merci_triggers)
112 config.merci_regexp_compiled = re.compile(config.merci_regexp)
113
114 config.tamere_regexp = regex_join(config.tamere_triggers)
115 config.tamere_regexp_compiled = re.compile(config.tamere_regexp)
116
117 config.bonjour_regexp = regex_join(config.bonjour_triggers, avant=u"^")
118 config.bonjour_regexp_compiled = re.compile(config.bonjour_regexp)
119
120 config.bonne_nuit_regexp = regex_join(config.bonne_nuit_triggers, avant=u"^")
121 config.bonne_nuit_regexp_compiled = re.compile(config.bonne_nuit_regexp)
122
123 config.pan_regexp = regex_join(config.pan_triggers, avant=".*", apres=".*")
124 config.pan_regexp_compiled = re.compile(config.pan_regexp)
125
126
127 regexp_compile()
128 def is_insult(chain, debug=True):
129 """Vérifie si ``chain`` contient une insulte."""
130 return is_something(chain, config.insult_regexp_compiled)
131 def is_not_insult(chain):
132 """Vérifie si ``chain`` contient une insulte à la forme négative."""
133 return is_something(chain, config.not_insult_regexp_compiled)
134 def is_compliment(chain, debug=True):
135 """Vérifie si ``chain`` contient un compliment."""
136 return is_something(chain, config.compliment_regexp_compiled)
137 def is_perdu(chain):
138 """Vérifie si ``chain`` contient une raison de perdre."""
139 return is_something(chain, config.perdu_regexp_compiled)
140 def is_tag(chain):
141 """Vérifie si ``chain`` demande de fermer sa gueule."""
142 return is_something(chain, config.tag_regexp_compiled)
143 def is_gros(chain):
144 """Vérifie si ``chain`` traite de gros."""
145 return is_something(chain, config.gros_regexp_compiled)
146 def is_tesla(chain):
147 """Vérifie si ``chain`` est un ping."""
148 return is_something(chain, config.tesla_regexp_compiled)
149 def is_merci(chain):
150 """Vérifie si ``chain`` contient un remerciement."""
151 return is_something(chain, config.merci_regexp_compiled)
152 def is_tamere(chain):
153 """Vérifie si ``chain`` traite ma mère."""
154 return is_something(chain, config.tamere_regexp_compiled)
155 def is_bad_action_trigger(chain,pseudo):
156 """Vérifie si ``chain`` est une action méchante.
157 On a besoin d'une regexp dynamique à cause du pseudo qui peut changer."""
158 return is_something(chain, matches=config.bad_action_triggers, avant=u"^",
159 apres="(?: [a-z]*ment)? %s($|\.| |,|;).*" % (pseudo))
160 def is_good_action_trigger(chain,pseudo):
161 """Vérifie si ``chain`` est une action gentille.
162 On a besoin d'une regexp dynamique à cause du pseudo qui peut changer."""
163 return is_something(chain, matches=config.good_action_triggers, avant=u"^",
164 apres="(?: [a-z]*ment)? %s($|\.| |,|;).*" % (pseudo))
165 def is_bonjour(chain):
166 """Vérifie si ``chain`` contient un bonjour."""
167 return is_something(chain, config.bonjour_regexp_compiled)
168 def is_bonne_nuit(chain):
169 """Vérifie si ``chain`` contient un bonne nuit."""
170 return is_something(chain, config.bonne_nuit_regexp_compiled)
171 def is_pan(chain):
172 """Vérifie si ``chain`` contient un pan."""
173 return is_something(chain, config.pan_regexp_compiled)
174
175 def is_time(conf):
176 """Vérifie si l'heure actuelle est entre les deux heures ``conf[0]`` et ``conf[1]``"""
177 _, _, _, h, m, s, _, _, _ = time.localtime()
178 return (conf[0], 0, 0) < (h, m, s) < (conf[1], 0, 0)
179 def is_day():
180 """Vérifie si on est le jour."""
181 return is_time(config.daytime)
182 def is_night():
183 """Vérifie si on est la nuit."""
184 return is_time(config.nighttime)
185
186
187 class UnicodeBotError(Exception):
188 """Erreur levée si quelqu'un fait du caca avec son encodage."""
189 pass
190
191 class CrashError(Exception):
192 """Pour pouvoir faire crasher Basile, parce que ça a l'air drôle."""
193 def __init__(self, msg=""):
194 Exception.__init__(self, msg)
195
196 def bot_unicode(chain):
197 """Essaye de décoder ``chain`` en UTF-8.
198 Lève une py:class:`UnicodeBotError` en cas d'échec."""
199 try:
200 return chain.decode("utf8")
201 except UnicodeDecodeError as exc:
202 raise UnicodeBotError
203
204
205 class Basile(ircbot.SingleServerIRCBot):
206 """Classe principale : définition du bot Basile."""
207 def __init__(self, serveur, debug=False):
208 temporary_pseudo = config.irc_pseudo + str(random.randrange(10000,100000))
209 ircbot.SingleServerIRCBot.__init__(self, [(serveur, 6667)],
210 temporary_pseudo, "Basile, le bot irc. [Codé par 20-100]", 10)
211 self.debug = debug
212 self.serveur = serveur
213 self.overops = config.overops
214 self.ops = self.overops + config.ops
215 self.report_bugs_to = config.report_bugs_to
216 self.chanlist = config.chanlist
217 self.identities = json.load(open(config.identities_file, "r"))
218 self.stay_channels = config.stay_channels
219 self.quiet_channels = config.quiet_channels
220 self.last_perdu = 0
221
222 ### Communication NK
223 def new_connection_NK(self, serv, username, password, typ="bdd"):
224 """Renvoie (``True``, <une socket ouverte et authentifiée sur la NoteKfet2015>)
225 ou bien (``False``, None)"""
226 try:
227 login_result, sock = nk.login(username, password, typ)
228 info, retcode, errmsg = login_result["msg"], login_result["retcode"], login_result["errmsg"]
229 except nk.NKRefused as exc:
230 for report in self.report_bugs_to:
231 serv.privmsg(report, "Le Serveur NK2015 est down.")
232 return (False, None, None)
233 except nk.NKHelloFailed as exc:
234 for report in self.report_bugs_to:
235 serv.privmsg(report,
236 "La version du protocole utilisée n'est pas supportée par le serveur NK2015.")
237 return (False, None, None)
238 except nk.NKUnknownError as exc:
239 erreurs = ["Une fucking erreur inconnue s'est produite"]
240 erreurs += str(exc).split("\n")
241 for report in self.report_bugs_to:
242 for err in erreurs:
243 serv.privmsg(report, err)
244 return (False, None, None)
245 except Exception as exc:
246 # Exception qui ne vient pas de la communication avec le serveur NK2015
247 log(self.serveur, "Erreur dans new_connection_NK\n" + str(exc))
248 return (False, None, None)
249 if retcode == 0:
250 return (True, info, sock)
251 else:
252 return (False, None, None)
253
254 ### Utilitaires
255 def _getnick(self):
256 """Récuère le nick effectif du bot sur le serveur."""
257 return self.serv.get_nickname()
258 nick = property(_getnick)
259
260 def give_me_my_pseudo(self, serv):
261 """Récupère le pseudo auprès de NickServ."""
262 serv.privmsg("NickServ", "RECOVER %s %s" % (config.irc_pseudo, config.irc_password))
263 serv.privmsg("NickServ", "RELEASE %s %s" % (config.irc_pseudo, config.irc_password))
264 time.sleep(0.3)
265 serv.nick(config.irc_pseudo)
266
267 def pourmoi(self, serv, message):
268 """Renvoie (False, lemessage) ou (True, le message amputé de "pseudo: ")"""
269 pseudo = self.nick
270 pseudo = pseudo.decode("utf-8")
271 size = len(pseudo)
272 if message[:size] == pseudo and len(message) > size and message[size] == ":":
273 return (True, message[size+1:].lstrip(" "))
274 else:
275 return (False, message)
276
277 ### Exécution d'actions
278 def lost(self, serv, channel, forced=False):
279 """Réaction à un trigger de perdu.
280 Annonce "J'ai perdu" sur le channel si on n'a pas perdu depuis un certain temps."""
281 if self.last_perdu + config.time_between_perdu < time.time() or forced:
282 if not channel in self.quiet_channels or forced:
283 serv.privmsg(channel, "J'ai perdu !")
284 self.last_perdu=time.time()
285 delay=config.time_between_perdu_trigger
286 delta=config.time_between_perdu_trigger_delta
287 serv.execute_delayed(random.randrange(delay-delta,delay+delta),self.lost,(serv,channel))
288
289 def quitter(self, chan, leave_message=None):
290 """Quitter un channel avec un message customisable."""
291 if leave_message == None:
292 leave_message = random.choice(config.leave_messages)
293 self.serv.part(chan, message=leave_message.encode("utf8"))
294
295 def mourir(self):
296 """Se déconnecter du serveur IRC avec un message customisable."""
297 quit_message = random.choice(config.quit_messages)
298 self.die(msg=quit_message.encode("utf8"))
299
300 def execute_reload(self, auteur=None):
301 """Recharge la config."""
302 reload(config)
303 regexp_compile()
304 if auteur in [None, "SIGHUP"]:
305 towrite = "Config reloaded" + " (SIGHUP received)" * (auteur == "SIGHUP")
306 for to in config.report_bugs_to:
307 self.serv.privmsg(to, towrite)
308 log(self.serveur, towrite)
309 return True, None
310 else:
311 return True, u"Config reloaded"
312
313 def crash(self, chan="nowhere", who="nobody"):
314 """Fait crasher le bot."""
315 where = "en privé" if chan == "priv" else "sur le chan %s" % chan
316 raise CrashError((u"Crash demandé par %s %s" % (who, where)).encode("utf-8"))
317
318 ACTIONS = {
319 "reload" : execute_reload,
320 }
321
322 def execute_something(self, something, params, place=None, auteur=None):
323 """Exécute une action et répond son résultat à ``auteur``
324 sur un chan ou en privé en fonction de ``place``"""
325 action = self.ACTIONS[something]
326 success, message = action(self, **params)
327 if message:
328 if irclib.is_channel(place):
329 message = "%s: %s" % (auteur, message.encode("utf-8"))
330 self.serv.privmsg(place, message)
331 log(self.serveur, place, auteur, something + "%r" % params + ("[successful]" if success else "[failed]"))
332
333 ### Surcharge des events du Bot
334 def on_welcome(self, serv, ev):
335 """À l'arrivée sur le serveur."""
336 self.serv = serv # ça serv ira :)
337 self.give_me_my_pseudo(serv)
338 serv.privmsg("NickServ", "IDENTIFY %s" % (config.irc_password))
339 log(self.serveur, "Connected")
340 if self.debug:
341 self.chanlist = ["#bot"]
342 for c in self.chanlist:
343 log(self.serveur, "JOIN %s" % (c))
344 serv.join(c)
345 # on ouvre la connexion note de Basile, special user
346 self.nk = self.new_connection_NK(serv, config.note_pseudo, config.note_password, "special")[2]
347 if self.nk == None:
348 for report in self.report_bugs_to:
349 serv.privmsg(report, "Connection to NK2015 failed, invalid password ?")
350
351 def on_privmsg(self, serv, ev):
352 """À la réception d'un message en privé."""
353 if ignore_event(serv, ev):
354 return
355 message = ev.arguments()[0]
356 auteur = irclib.nm_to_n(ev.source())
357 try:
358 message = bot_unicode(message)
359 except UnicodeBotError:
360 if config.utf8_trigger:
361 serv.privmsg(auteur, random.choice(config.utf8_fail_answers).encode("utf8"))
362 return
363 message = message.split()
364 cmd = message[0].lower()
365 notunderstood = False
366 if cmd == u"help":
367 op,overop=auteur in self.ops, auteur in self.overops
368 if len(message)==1:
369 helpmsg = config.helpmsg_default
370 if op:
371 helpmsg += config.helpmsg_ops
372 if overop:
373 helpmsg += config.helpmsg_overops
374 else:
375 helpmsgs = config.helpdico.get(message[1].lower(), ["Commande inconnue.", None, None])
376 helpmsg = helpmsgs[0]
377 if op and helpmsgs[1]:
378 if helpmsg:
379 helpmsg += "\n" + helpmsgs[1]
380 else:
381 helpmsg = helpmsgs[1]
382 if overop and helpmsgs[2]:
383 if helpmsg:
384 helpmsg += "\n" + helpmsgs[2]
385 else:
386 helpmsg = helpmsgs[2]
387 for ligne in helpmsg.split("\n"):
388 serv.privmsg(auteur, ligne.encode("utf-8"))
389 elif cmd == u"identify":
390 if len(message) == 1:
391 if self.identities.has_key(auteur):
392 serv.privmsg(auteur, "Je vous connais sous le pseudo note %s." % (
393 self.identities[auteur]["pseudo"].encode("utf8")))
394 else:
395 serv.privmsg(auteur, "Je ne connais pas votre pseudo note.")
396 elif len(message) >= 3:
397 username, password = message[1], " ".join(message[2:])
398 success, info, _ = self.new_connection_NK(serv, username, password)
399 if success:
400 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
401 serv.privmsg(auteur, "Identité enregistrée.")
402 self.identities[auteur] = info
403 json.dump(self.identities, open(config.identities_file,"w"))
404 else:
405 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
406 serv.privmsg(auteur, "Mot de passe invalide. (ou serveur down)")
407 else:
408 serv.privmsg(auteur, "Syntaxe : IDENTIFY [<username> <password>]")
409 elif cmd == u"drop":
410 if len(message) > 1:
411 if self.identities.has_key(auteur):
412 password = " ".join(message[1:])
413 success, _, _ = self.new_connection_NK(serv, self.identities[auteur], password)
414 if success:
415 del self.identities[auteur]
416 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
417 json.dump(self.identities, open(config.identities_file, "w"))
418 serv.privmsg(auteur, "Identité oubliée.")
419 else:
420 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
421 serv.privmsg(auteur, "Mot de passe invalide. (ou serveur down)")
422 else:
423 serv.privmsg(auteur, "Je ne connais pas ton pseudo note.")
424 else:
425 serv.privmsg(auteur, "Syntaxe : DROP <password>")
426 elif cmd == u"join":
427 if auteur in self.ops:
428 if len(message) > 1:
429 if message[1] in self.chanlist:
430 serv.privmsg(auteur, "Je suis déjà sur %s" % (message[1]))
431 else:
432 serv.join(message[1])
433 self.chanlist.append(message[1])
434 serv.privmsg(auteur, "Channels : " + " ".join(self.chanlist))
435 log(self.serveur, "priv", auteur, " ".join(message))
436 else:
437 serv.privmsg(auteur, "Channels : " + " ".join(self.chanlist))
438 else:
439 notunderstood = True
440 elif cmd == u"leave":
441 if auteur in self.ops and len(message) > 1:
442 if message[1] in self.chanlist:
443 if not (message[1] in self.stay_channels) or auteur in self.overops:
444 self.quitter(message[1].encode("utf-8"), " ".join(message[2:]))
445 self.chanlist.remove(message[1])
446 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
447 else:
448 serv.privmsg(auteur, "Non, je reste !")
449 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
450 else:
451 serv.privmsg(auteur, "Je ne suis pas sur %s" % (message[1]))
452 else:
453 notunderstood = True
454 elif cmd == u"stay":
455 if auteur in self.overops:
456 if len(message) > 1:
457 if message[1] in self.stay_channels:
458 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
459 serv.privmsg(auteur, "Je stay déjà sur %s." % (message[1]))
460 else:
461 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
462 self.stay_channels.append(message[1])
463 serv.privmsg(auteur, "Stay channels : " + " ".join(self.stay_channels))
464 else:
465 serv.privmsg(auteur, "Stay channels : " + " ".join(self.stay_channels))
466 else:
467 notunderstood = True
468 elif cmd == u"nostay":
469 if auteur in self.overops:
470 if len(message) > 1:
471 if message[1] in self.stay_channels:
472 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
473 self.stay_channels.remove(message[1])
474 serv.privmsg(auteur, "Stay channels : " + " ".join(self.stay_channels))
475 else:
476 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
477 serv.privmsg(auteur, "Je ne stay pas sur %s." % (message[1]))
478 else:
479 notunderstood = True
480 elif cmd == u"die":
481 if auteur in self.overops:
482 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
483 self.mourir()
484 else:
485 notunderstood = True
486 elif cmd == u"crash":
487 if auteur in self.overops:
488 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
489 self.crash("priv", auteur)
490 else:
491 notunderstood = True
492 elif cmd == u"reload":
493 if auteur in self.ops:
494 self.execute_something("reload", {"auteur" : auteur}, place=auteur, auteur=auteur)
495 else:
496 notunderstood = True
497 elif cmd == u"reconnect":
498 if auteur in self.ops:
499 try:
500 self.nk = self.new_connection_NK(serv, config.note_pseudo,
501 config.note_password, "special")[2]
502 except Exception as exc:
503 self.nk = None
504 log(self.serveur, 'Erreur dans on_pubmsg/"cmd in ["reconnect"]\n' + str(exc))
505 if self.nk != None:
506 serv.privmsg(auteur, "%s: done" % (auteur))
507 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
508 else:
509 serv.privmsg(auteur, "%s: failed" % (auteur))
510 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
511 for report in self.report_bugs_to:
512 serv.privmsg(report, "Connection to NK2015 failed, invalid password ? Server dead ?")
513 else:
514 notunderstood = True
515 elif cmd == u"quiet":
516 if auteur in self.ops:
517 if len(message) > 1:
518 if message[1] in self.quiet_channels:
519 serv.privmsg(auteur, "Je me la ferme déjà sur %s" % (message[1]))
520 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
521 else:
522 self.quiet_channels.append(message[1])
523 serv.privmsg(auteur, "Quiet channels : " + " ".join(self.quiet_channels))
524 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
525 else:
526 serv.privmsg(auteur, "Quiet channels : " + " ".join(self.quiet_channels))
527 else:
528 notunderstood = True
529 elif cmd == u"noquiet":
530 if auteur in self.ops:
531 if len(message) > 1:
532 if message[1] in self.quiet_channels:
533 self.quiet_channels.remove(message[1])
534 serv.privmsg(auteur, "Quiet channels : " + " ".join(self.quiet_channels))
535 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
536 else:
537 serv.privmsg(auteur, "Je ne me la ferme pas sur %s." % (message[1]))
538 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
539 else:
540 notunderstood = True
541 elif cmd == u"say":
542 if auteur in self.overops and len(message) > 2:
543 serv.privmsg(message[1].encode("utf-8"), (u" ".join(message[2:])).encode("utf-8"))
544 log(self.serveur, "priv", auteur, " ".join(message))
545 elif len(message) <= 2:
546 serv.privmsg(auteur, "Syntaxe : SAY <channel> <message>")
547 else:
548 notunderstood = True
549 elif cmd == u"do":
550 if auteur in self.overops and len(message) > 2:
551 serv.action(message[1], " ".join(message[2:]))
552 log(self.serveur, "priv", auteur, " ".join(message))
553 elif len(message) <= 2:
554 serv.privmsg(auteur, "Syntaxe : DO <channel> <action>")
555 else:
556 notunderstood = True
557 elif cmd == u"kick":
558 if auteur in self.overops and len(message) > 2:
559 serv.kick(message[1].encode("utf-8"), message[2].encode("utf-8"), " ".join(message[3:]).encode("utf-8"))
560 log(self.serveur, "priv", auteur, " ".join(message))
561 elif len(message) <= 2:
562 serv.privmsg(auteur, "Syntaxe : KICK <channel> <pseudo> [<raison>]")
563 else:
564 notunderstood = True
565 elif cmd == u"lost":
566 if auteur in self.ops and len(message) > 1:
567 serv.privmsg(message[1], "J'ai perdu !")
568 log(self.serveur, "priv", auteur, " ".join(message))
569 elif len(message) <= 1:
570 serv.privmsg(auteur, "Syntaxe : LOST <channel>")
571 else:
572 notunderstood = True
573 elif cmd == u"solde":
574 if len(message) == 1:
575 if self.identities.has_key(auteur):
576 success, solde, pseudo = nk.get_solde(self.nk, self.identities[auteur]["idbde"], serv, auteur)
577 if success:
578 serv.privmsg(auteur, "%.2f (%s)" % (solde/100.0, pseudo.encode("utf8")))
579 log(self.serveur, "priv", auteur, " ".join(message) + ("[successful]" if success else "[failed]"))
580 else:
581 serv.privmsg(canal, "Je ne connais pas ton pseudo note.")
582 elif cmd == u"ops":
583 if auteur in self.overops:
584 serv.privmsg(auteur, " ".join(self.ops))
585 else:
586 notunderstood = True
587 elif cmd == u"overops":
588 if auteur in self.overops:
589 serv.privmsg(auteur, " ".join(self.overops))
590 else:
591 notunderstood = True
592 else:
593 notunderstood = True
594 if notunderstood:
595 serv.privmsg(auteur, "Je n'ai pas compris. Essayez HELP…")
596
597 def on_pubmsg(self, serv, ev):
598 """À la réception d'un message sur un channel."""
599 if ignore_event(serv, ev):
600 return
601 auteur = irclib.nm_to_n(ev.source())
602 canal = ev.target()
603 message = ev.arguments()[0]
604 try:
605 message = bot_unicode(message)
606 except UnicodeBotError:
607 if config.utf8_trigger and not canal in self.quiet_channels:
608 serv.privmsg(canal, (u"%s: %s"% ( auteur, random.choice(config.utf8_fail_answers))).encode("utf8"))
609 return
610 pour_moi, message = self.pourmoi(serv, message)
611 if pour_moi and message.split()!=[]:
612 cmd = message.split()[0].lower()
613 try:
614 args = " ".join(message.split()[1:])
615 except:
616 args = ""
617 if cmd in [u"meurs", u"die", u"crève"]:
618 if auteur in self.overops:
619 log(self.serveur, canal, auteur, message + "[successful]")
620 self.mourir()
621 else:
622 serv.privmsg(canal,(u"%s: %s"%(auteur, random.choice(config.quit_fail_messages))).encode("utf8"))
623 log(self.serveur, canal, auteur, message + "[failed]")
624 elif cmd == u"reload":
625 if auteur in self.ops:
626 self.execute_something("reload", {"auteur" : auteur}, place=canal, auteur=auteur)
627 elif cmd == u"crash":
628 if auteur in self.overops:
629 self.crash(auteur, message)
630 elif cmd in [u"part", u"leave", u"dégage", u"va-t-en", u"tut'tiresailleurs,c'estmesgalets"]:
631 if auteur in self.ops and (not (canal in self.stay_channels)
632 or auteur in self.overops):
633 self.quitter(canal)
634 log(self.serveur, canal, auteur, message + "[successful]")
635 if canal in self.chanlist:
636 self.chanlist.remove(canal)
637 else:
638 serv.privmsg(canal,(u"%s: %s" % (auteur, random.choice(config.leave_fail_messages))).encode("utf8"))
639 log(self.serveur, canal, auteur, message + "[failed]")
640
641 elif cmd == u"reconnect":
642 if auteur in self.ops:
643 try:
644 self.nk = self.new_connection_NK(serv, config.note_pseudo,
645 config.note_password, "special")[2]
646 except Exception as exc:
647 self.nk = None
648 log(self.serveur, 'Erreur dans on_pubmsg/"cmd in ["reconnect"]\n' + str(exc))
649 if self.nk != None:
650 serv.privmsg(canal, "%s: done" % (auteur))
651 log(self.serveur, canal, auteur, message + "[successful]")
652 else:
653 serv.privmsg(canal, "%s: failed" % (auteur))
654 log(self.serveur, canal, auteur, message + "[failed]")
655 for report in self.report_bugs_to:
656 serv.privmsg(report, "Connection to NK2015 failed, invalid password ? Server dead ?")
657 else:
658 serv.privmsg(canal, "%s: %s" % (auteur, random.choice(config.pas_programme_pour_tobeir).encode("utf8")))
659 log(self.serveur, canal, auteur, message + "[failed]")
660
661 elif cmd in [u"deviens", u"pseudo"]:
662 if auteur in self.ops:
663 become = args
664 serv.nick(become)
665 log(self.serveur, canal, auteur, message + "[successful]")
666
667 if cmd in [u"meur", u"meurt", u"meurre", u"meurres"] and not canal in self.quiet_channels:
668 serv.privmsg(canal, '%s: Mourir, impératif, 2ème personne du singulier : "meurs" (de rien)' % (auteur))
669 elif cmd in [u"ping"] and not canal in self.quiet_channels:
670 serv.privmsg(canal, "%s: pong" % (auteur))
671
672 elif cmd in [u"solde", u"!solde", u"!coca"] or cmd.startswith("!"):
673 if self.identities.has_key(auteur):
674 idbde = self.identities[auteur]["idbde"]
675 if cmd in [u"solde", u"!solde"]:
676 success, solde, pseudo = nk.get_solde(self.nk, self.identities[auteur]["idbde"], serv, canal)
677 if success:
678 serv.privmsg(canal, "%s: %s (%s)" % (auteur, float(solde)/100, pseudo.encode("utf8")))
679 elif cmd in [u"!coca"] or cmd.startswith("!"):
680 success = nk.consomme(self.nk, self.identities[auteur]["idbde"], message[1:], serv, canal)
681 log(self.serveur, canal, auteur, message + ("[successful]" if success else "[failed]"))
682 else:
683 serv.privmsg(canal, "%s: Je ne connais pas votre pseudo note." % (auteur))
684 log(self.serveur, canal, auteur, message + "[unknown]")
685 elif (re.match("(pain au chocolat|chocolatine)", message.lower())
686 and not canal in self.quiet_channels):
687 serv.action(canal, "sert un pain au chocolat à %s" % (auteur))
688 elif re.match("manzana",message.lower()) and not canal in self.quiet_channels:
689 if auteur in config.manzana:
690 serv.action(canal, "sert une bouteille de manzana à %s" % (auteur))
691 elif auteur in config.manzana_bis:
692 serv.action(canal, "sert un grand verre de jus de pomme à %s : tout le monde sait qu'il ne boit pas." % (auteur))
693 else:
694 serv.action(canal, "sert un verre de manzana à %s" % (auteur))
695 if is_insult(message) and not canal in self.quiet_channels:
696 if is_not_insult(message):
697 answer = random.choice(config.compliment_answers)
698 for ligne in answer.split("\n"):
699 serv.privmsg(canal, "%s: %s" % (auteur, ligne.encode("utf8")))
700 else:
701 answer = random.choice(config.insultes_answers)
702 for ligne in answer.split("\n"):
703 serv.privmsg(canal, "%s: %s" % (auteur, ligne.encode("utf8")))
704 elif is_compliment(message) and not canal in self.quiet_channels:
705 answer = random.choice(config.compliment_answers)
706 for ligne in answer.split("\n"):
707 serv.privmsg(canal, "%s: %s" % (auteur,ligne.encode("utf8")))
708 gros_match = is_gros(message)
709 if gros_match and not canal in self.quiet_channels:
710 taille = get_filesize()
711 answer = u"Mais non, je ne suis pas %s, %sKo tout au plus…" % (gros_match.groups()[0], taille)
712 serv.privmsg(canal, "%s: %s"%(auteur, answer.encode("utf8")))
713 if is_tesla(message) and not canal in self.quiet_channels:
714 l1, l2 = config.tesla_answers, config.tesla_actions
715 n1, n2 = len(l1), len(l2)
716 i = random.randrange(n1 + n2)
717 if i >= n1:
718 serv.action(canal, l2[i - n1].encode("utf8"))
719 else:
720 serv.privmsg(canal, "%s: %s" % (auteur, l1[i].encode("utf8")))
721 if is_tamere(message) and not canal in self.quiet_channels:
722 answer = random.choice(config.tamere_answers)
723 for ligne in answer.split("\n"):
724 serv.privmsg(canal, "%s: %s"%(auteur, ligne.encode("utf8")))
725 if is_tag(message) and not canal in self.quiet_channels:
726 if auteur in self.ops:
727 action = random.choice(config.tag_actions)
728 serv.action(canal, action.encode("utf8"))
729 self.quiet_channels.append(canal)
730 else:
731 answer = random.choice(config.tag_answers)
732 for ligne in answer.split("\n"):
733 serv.privmsg(canal, "%s: %s" % (auteur, ligne.encode("utf8")))
734 if is_merci(message):
735 answer = random.choice(config.merci_answers)
736 for ligne in answer.split("\n"):
737 serv.privmsg(canal, "%s: %s"%(auteur, ligne.encode("utf8")))
738 out = re.match(ur"^([A-Z[]|\\|[0-9]+|(¹|²|³|⁴|⁵|⁶|⁷|⁸|⁹|⁰)+)(?:| \?| !)$", message.upper())
739 if re.match("ma bite dans ton oreille", message) and not canal in self.quiet_channels:
740 serv.privmsg(canal, "%s: Seul un olasd peut imiter un olasd dans un de ses grands jours !" % (auteur))
741 if out and not canal in self.quiet_channels:
742 out = out.groups()[0]
743 try:
744 iout = int(out)
745 serv.privmsg(canal, "%s: %s !" % (auteur, iout + 1))
746 if iout == 2147483647:
747 serv.privmsg(canal, "%s: Ciel, un maxint ! Heureusement que je suis en python…" % (auteur))
748 return
749 if iout + 1 > 1000 and random.randrange(4) == 0:
750 serv.privmsg(canal, "%s: Vous savez, moi et les chiffres…" % (auteur))
751 return
752 except Exception as exc:
753 pass
754 if re.match("[A-Y]", out):
755 alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
756 serv.privmsg(canal, "%s: %s !"%(auteur, alphabet[alphabet.index(out) + 1]))
757 elif out == "Z":
758 serv.privmsg(canal, "%s: Je ne vous remercie pas, j'ai l'air idiot ainsi… [ ?" % (auteur))
759 elif out in "[\\":
760 serv.privmsg(canal, "%s: Nous devrions nous en tenir là, ça va finir par poser des problèmes…" % (auteur))
761 elif re.match(ur"(¹|²|³|⁴|⁵|⁶|⁷|⁸|⁹|⁰)+", out):
762 def translate(mess):
763 return "".join([{u"⁰¹²³⁴⁵⁶⁷⁸⁹0123456789"[i]:u"0123456789⁰¹²³⁴⁵⁶⁷⁸⁹"[i]
764 for i in range(20)}[j]
765 for j in mess])
766 out = int(translate(out))
767 serv.privmsg(canal,"%s: %s !" % (auteur, translate(str(out + 1)).encode("utf8")))
768 if is_bonjour(message) and not canal in self.quiet_channels:
769 if is_night():
770 answer = random.choice(config.night_answers)
771 elif is_day():
772 answer = random.choice(config.bonjour_answers)
773 else:
774 answer = random.choice(config.bonsoir_answers)
775 serv.privmsg(canal, answer.format(auteur).encode("utf8"))
776 if is_bonne_nuit(message) and not canal in self.quiet_channels:
777 answer = random.choice(config.bonne_nuit_answers)
778 serv.privmsg(canal, answer.format(auteur).encode("utf8"))
779 if is_pan(message) and not canal in self.quiet_channels:
780 serv.privmsg(canal, "%s: ce n'est pas sur moi qu'il faut tirer, même si je sais que j'attire l'œil !" % (auteur))
781 else:
782 if message in [u"!pain au chocolat", u"!chocolatine"] and not canal in self.quiet_channels:
783 serv.action(canal, "sert un pain au chocolat à %s" % (auteur))
784 if message in [u"!manzana"] and not canal in self.quiet_channels:
785 if auteur in config.manzana:
786 serv.action(canal, "sert une bouteille de manzana à %s" % (auteur))
787 elif auteur in config.manzana_bis:
788 serv.action(canal, "sert un grand verre de jus de pomme à %s : tout le monde sait qu'il ne boit pas." % (auteur))
789 else:
790 serv.action(canal, "sert un verre de manzana à %s" % (auteur))
791 if re.match(u'^ *(.|§|!|/|/|:|)(w|b) [0-9]+$', message) and not canal in self.quiet_channels:
792 failanswers = config.buffer_fail_answers
793 answer = random.choice(failanswers)
794 serv.privmsg(canal, ("%s: %s"%(auteur,answer)).encode("utf8"))
795 if not canal in self.quiet_channels:
796 mypseudo = self.nick
797 if re.match((u"^(" + u"|".join(config.bonjour_triggers)
798 + ur")( {}| all| tout le monde| (à )?tous)(\.| ?!)?$"
799 ).format(mypseudo).lower(), message.strip().lower()):
800 answer = random.choice(config.bonjour_answers)
801 serv.privmsg(canal, answer.format(auteur).encode("utf8"))
802 if (is_perdu(message) and not canal in self.quiet_channels):
803 # proba de perdre sur trigger :
804 # avant 30min (enfin, config) : 0
805 # ensuite, +25%/30min, linéairement
806 deltat = time.time() - self.last_perdu
807 barre = (deltat - config.time_between_perdu)/(2*3600.0)
808 if random.uniform(0, 1) < barre:
809 serv.privmsg(canal, "%s: J'ai perdu !" % (auteur))
810 self.last_perdu = time.time()
811
812 def on_action(self, serv, ev):
813 """À la réception d'une action."""
814 if ignore_event(serv, ev):
815 return
816 action = ev.arguments()[0]
817 auteur = irclib.nm_to_n(ev.source())
818 channel = ev.target()
819 try:
820 action = bot_unicode(action)
821 except UnicodeBotError:
822 if config.utf8_trigger and not channel in self.quiet_channels:
823 serv.privmsg(channel, (u"%s: %s"%(auteur,random.choice(config.utf8_fail_answers))).encode("utf8"))
824 return
825 mypseudo = self.nick
826
827 if is_bad_action_trigger(action, mypseudo) and not channel in self.quiet_channels:
828 l1, l2 = config.bad_action_answers, config.bad_action_actions
829 n1, n2 = len(l1), len(l2)
830 i = random.randrange(n1 + n2)
831 if i >= n1:
832 serv.action(channel, l2[i - n1].format(auteur).encode("utf8"))
833 else:
834 serv.privmsg(channel, l1[i].format(auteur).encode("utf8"))
835 if is_good_action_trigger(action, mypseudo) and not channel in self.quiet_channels:
836 l1, l2 = config.good_action_answers, config.good_action_actions
837 n1, n2 = len(l1), len(l2)
838 i = random.randrange(n1 + n2)
839 if i >= n1:
840 serv.action(channel, l2[i-n1].format(auteur).encode("utf8"))
841 else:
842 serv.privmsg(channel, l1[i].format(auteur).encode("utf8"))
843
844 def on_kick(self, serv, ev):
845 """À la réception d'une action."""
846 auteur = irclib.nm_to_n(ev.source())
847 channel = ev.target()
848 victime = ev.arguments()[0]
849 raison = ev.arguments()[1]
850 if victime == self.nick:
851 log(self.serveur, u"%s kické de %s par %s (raison : %s)" % (victime, channel.decode("utf-8"), auteur, raison))
852 time.sleep(2)
853 serv.join(channel)
854 l1, l2 = config.kick_answers, config.kick_actions
855 n1, n2 = len(l1), len(l2)
856 i = random.randrange(n1 + n2)
857 if i >= n1:
858 serv.action(channel, l2[i-n1].format(auteur).encode("utf8"))
859 else:
860 serv.privmsg(channel, l1[i].format(auteur).encode("utf8"))
861
862 ### .fork trick
863 def start_as_daemon(self, outfile):
864 sys.stderr = Logger(outfile)
865 self.start()
866
867
868 class Logger(object):
869 """Pour écrire ailleurs que sur stdout"""
870 def __init__(self, filename="basile.full.log"):
871 self.filename = filename
872
873 def write(self, message):
874 f = open(self.filename, "a")
875 f.write(message)
876 f.close()
877
878 def main():
879 """Exécution principal : lecture des paramètres et lancement du bot."""
880 if len(sys.argv) == 1:
881 print "Usage : basile.py <serveur> [--debug] [--no-output] [--daemon [--pidfile]] [--outfile]"
882 print " --outfile sans --no-output ni --daemon n'a aucun effet"
883 exit(1)
884 serveur = sys.argv[1]
885 if "--daemon" in sys.argv:
886 thisfile = os.path.realpath(__file__)
887 thisdirectory = thisfile.rsplit("/", 1)[0]
888 os.chdir(thisdirectory)
889 daemon = True
890 else:
891 daemon = False
892 if "debug" in sys.argv or "--debug" in sys.argv:
893 debug = True
894 else:
895 debug = False
896 if "--quiet" in sys.argv:
897 config.debug_stdout = False
898 serveurs = {"a♡" : "acoeur.crans.org",
899 "acoeur" : "acoeur.crans.org",
900 "acoeur.crans.org" : "acoeur.crans.org",
901 "irc" : "irc.crans.org",
902 "crans" : "irc.crans.org",
903 "irc.crans.org" : "irc.crans.org"}
904 if "--no-output" in sys.argv or "--daemon" in sys.argv:
905 outfile = "/var/log/bots/basile.full.log"
906 for arg in sys.argv:
907 arg = arg.split("=")
908 if arg[0].strip('-') in ["out", "outfile", "logfile"]:
909 outfile = arg[1]
910 sys.stdout = Logger(outfile)
911 try:
912 serveur = serveurs[serveur]
913 except KeyError:
914 print "Server Unknown : %s" % (serveur)
915 exit(404)
916 basile = Basile(serveur,debug)
917 # Si on reçoit un SIGHUP, on reload la config
918 def sighup_handler(signum, frame):
919 basile.execute_reload(auteur="SIGHUP")
920 signal.signal(signal.SIGHUP, sighup_handler)
921 # Daemonization
922 if daemon:
923 child_pid = os.fork()
924 if child_pid == 0:
925 os.setsid()
926 basile.start_as_daemon(outfile)
927 else:
928 # on enregistre le pid de basile
929 pidfile = "/var/run/bots/basile.pid"
930 for arg in sys.argv:
931 arg = arg.split("=")
932 if arg[0].strip('-') in ["pidfile"]:
933 pidfile = arg[1]
934 f = open(pidfile, "w")
935 f.write("%s\n" % child_pid)
936 f.close()
937 else:
938 basile.start()
939
940 if __name__ == "__main__":
941 main()