]>
gitweb.pimeys.fr Git - bots/parrot.git/blob - parrot.py
2 # -*- encoding: utf-8 -*-
4 """ Un bot IRC qui enregistre et ressort des citations """
15 # Oui, j'ai recodé ma version d'irclib pour pouvoir rattrapper les SIGHUP
16 sys
.path
.insert(0, "/home/vincent/scripts/python-myirclib")
20 from commands
import getstatusoutput
as ex
24 #: Module définissant les erreurs
26 #: Module de gestion des quotes
29 # Je veux pouvoir éditer ce que crée ce bot
32 def get_config_logfile(serveur
):
33 """Renvoie le nom du fichier de log en fonction du ``serveur`` et de la config."""
34 serveurs
= {"irc.crans.org" : "crans"}
35 return config
.logfile_template
% (serveurs
[serveur
],)
37 def log(serveur
, channel
, auteur
=None, message
=None):
38 """Enregistre une ligne de log."""
39 if auteur
== message
== None:
40 # alors c'est que c'est pas un channel mais juste une ligne de log
41 chain
= u
"%s %s" % (time
.strftime("%F %T"), channel
)
43 chain
= u
"%s [%s:%s] %s" % (time
.strftime("%F %T"), channel
, auteur
, message
)
44 f
= open(get_config_logfile(serveur
), "a")
45 f
.write((chain
+ u
"\n").encode("utf-8"))
47 if config
.debug_stdout
:
48 print chain
.encode("utf-8")
50 def ignore_event(serv
, ev
):
51 """Retourne ``True`` si il faut ignorer cet évènement."""
52 for (blackmask
, exceptlist
) in config
.blacklisted_masks
:
53 usermask
= ev
.source()
54 blackit
= bool(irclib
.mask_matches(usermask
, blackmask
))
55 exceptit
= any([bool(irclib
.mask_matches(usermask
, exceptmask
)) for exceptmask
in exceptlist
])
56 if exceptit
: # Il est exempté
59 if blackit
: # Il n'est pas exempté et matche la blacklist
63 def bot_unicode(chain
):
64 """Essaye de décoder ``chain`` en UTF-8.
65 Lève une py:class:`errors.UnicodeBotError` en cas d'échec."""
67 return chain
.decode("utf8")
68 except UnicodeDecodeError as exc
:
69 raise errors
.UnicodeBotError
72 class Parrot(ircbot
.SingleServerIRCBot
):
73 """Classe principale : définition du bot."""
74 def __init__(self
, serveur
, debug
=False):
75 temporary_pseudo
= config
.irc_pseudo
+ str(random
.randrange(10000,100000))
76 ircbot
.SingleServerIRCBot
.__init
__(self
, [(serveur
, 6667)],
77 temporary_pseudo
, "Parrot, le bot irc. [Codé par 20-100]", 10)
79 self
.serveur
= serveur
80 self
.overops
= config
.overops
81 self
.ops
= self
.overops
+ config
.ops
82 self
.report_bugs_to
= config
.report_bugs_to
83 self
.chanlist
= config
.chanlist
84 self
.stay_channels
= config
.stay_channels
85 self
.quiet_channels
= config
.quiet_channels
88 self
.quotedb
= quotes
.QuoteDB()
91 # Pour agir à la réception des whois 307 = registered nick
92 self
.ircobj
.add_global_handler("307", self
.on_registered_nick
)
97 """Récuère le nick effectif du bot sur le serveur."""
98 return self
.serv
.get_nickname()
99 nick
= property(_getnick
)
101 def give_me_my_pseudo(self
, serv
):
102 """Récupère le pseudo auprès de NickServ."""
103 serv
.privmsg("NickServ", "GHOST %s %s" % (config
.irc_pseudo
, config
.irc_password
))
104 serv
.privmsg("NickServ", "RELEASE %s %s" % (config
.irc_pseudo
, config
.irc_password
))
106 serv
.nick(config
.irc_pseudo
)
108 def pourmoi(self
, serv
, message
):
109 """Renvoie (False, lemessage) ou (True, le message amputé de "pseudo: ")"""
111 pseudo
= pseudo
.decode("utf-8")
113 if message
[:size
] == pseudo
and len(message
) > size
and message
[size
] == ":":
114 return (True, message
[size
+1:].lstrip(" "))
116 return (False, message
)
118 ### Exécution d'actions
119 def quitter(self
, chan
, leave_message
=None):
120 """Quitter un channel avec un message customisable."""
121 if leave_message
== None:
122 leave_message
= random
.choice(config
.leave_messages
)
123 self
.serv
.part(chan
, message
=leave_message
.encode("utf8"))
126 """Se déconnecter du serveur IRC avec un message customisable."""
127 quit_message
= random
.choice(config
.quit_messages
)
128 self
.die(msg
=quit_message
.encode("utf8"))
130 def reload_quotes(self
):
131 """ Recharge la base de données des quotes et recompile la regexp de quote """
133 self
.quote_pattern
= re
.compile(config
.quote_regexp
, flags
=re
.UNICODE
)
135 def execute_reload(self
, auteur
=None):
136 """Recharge la config."""
139 if auteur
in [None, "SIGHUP"]:
140 towrite
= "Config reloaded" + " (SIGHUP received)" * (auteur
== "SIGHUP")
141 for to
in config
.report_bugs_to
:
142 self
.serv
.privmsg(to
, towrite
)
143 log(self
.serveur
, towrite
)
146 return True, u
"Config reloaded"
148 def crash(self
, who
="nobody", chan
="nowhere"):
149 """Fait crasher le bot."""
150 where
= "en privé" if chan
== "priv" else "sur le chan %s" % chan
151 raise errors
.CrashError((u
"Crash demandé par %s %s" % (who
, where
)).encode("utf-8"))
154 "reload" : execute_reload
,
157 def execute_something(self
, something
, params
, place
=None, auteur
=None):
158 """Exécute une action et répond son résultat à ``auteur``
159 sur un chan ou en privé en fonction de ``place``"""
160 action
= self
.ACTIONS
[something
]
161 success
, message
= action(self
, **params
)
163 if irclib
.is_channel(place
):
164 message
= "%s: %s" % (auteur
, message
.encode("utf-8"))
165 self
.serv
.privmsg(place
, message
)
166 log(self
.serveur
, place
, auteur
, something
+ "%r" % params
+ ("[successful]" if success
else "[failed]"))
169 def acknowledge(self
, asked_by
, asked_where
, message
):
170 """Répond quelque chose au demandeur d'une action.
171 ``asked_where=None`` signifie en privé."""
172 if asked_where
is None:
173 self
.serv
.privmsg(asked_by
, message
)
175 self
.serv
.privmsg(asked_where
, "%s: %s" % (asked_by
, message
))
177 def dump(self
, asked_by
, asked_where
=None):
178 """Dumpe les quotes. ``asked_where=None`` signifie en privé."""
179 quotes
.dump(self
.quotedb
)
180 self
.acknowledge(asked_by
, asked_where
, "Quotes dumpées")
182 def restore(self
, asked_by
, asked_where
=None):
183 """Restaure les quotes à partir du dump. ``asked_where=None`` signifie en privé."""
184 self
.quotedb
= quotes
.restore()
185 self
.acknowledge(asked_by
, asked_where
, "Quotes restaurées à partir du dump (pas de backup effectué).")
186 many
= self
.quotedb
.get_clash_authors()
188 self
.acknowledge(asked_by
, asked_where
, "Auteurs de casse différente : %s" % (many
))
190 ### Surcharge des events du Bot
191 def on_welcome(self
, serv
, ev
):
192 """À l'arrivée sur le serveur."""
193 self
.serv
= serv
# ça serv ira :)
194 self
.give_me_my_pseudo(serv
)
195 serv
.privmsg("NickServ", "IDENTIFY %s" % (config
.irc_password
))
196 log(self
.serveur
, "Connected")
198 self
.chanlist
= ["#bot"]
199 for c
in self
.chanlist
:
200 log(self
.serveur
, "JOIN %s" % (c
))
203 def on_privmsg(self
, serv
, ev
):
204 """À la réception d'un message en privé."""
205 if ignore_event(serv
, ev
):
207 message
= ev
.arguments()[0]
208 auteur
= irclib
.nm_to_n(ev
.source())
210 message
= bot_unicode(message
)
211 except errors
.UnicodeBotError
:
212 if config
.utf8_trigger
:
213 serv
.privmsg(auteur
, random
.choice(config
.utf8_fail_answers
).encode("utf8"))
215 message
= message
.split()
216 cmd
= message
[0].lower()
217 notunderstood
= False
219 op
,overop
=auteur
in self
.ops
, auteur
in self
.overops
221 helpmsg
= config
.helpmsg_default
223 helpmsg
+= config
.helpmsg_ops
225 helpmsg
+= config
.helpmsg_overops
227 helpmsgs
= config
.helpdico
.get(message
[1].lower(), ["Commande inconnue.", None, None])
228 helpmsg
= helpmsgs
[0]
229 if op
and helpmsgs
[1]:
231 helpmsg
+= "\n" + helpmsgs
[1]
233 helpmsg
= helpmsgs
[1]
234 if overop
and helpmsgs
[2]:
236 helpmsg
+= "\n" + helpmsgs
[2]
238 helpmsg
= helpmsgs
[2]
239 for ligne
in helpmsg
.split("\n"):
240 serv
.privmsg(auteur
, ligne
.encode("utf-8"))
242 if auteur
in self
.ops
:
244 if message
[1] in self
.chanlist
:
245 serv
.privmsg(auteur
, "Je suis déjà sur %s" % (message
[1]))
247 serv
.join(message
[1])
248 self
.chanlist
.append(message
[1])
249 serv
.privmsg(auteur
, "Channels : " + " ".join(self
.chanlist
))
250 log(self
.serveur
, "priv", auteur
, " ".join(message
))
252 serv
.privmsg(auteur
, "Channels : " + " ".join(self
.chanlist
))
255 elif cmd
== u
"leave":
256 if auteur
in self
.ops
and len(message
) > 1:
257 if message
[1] in self
.chanlist
:
258 if not (message
[1] in self
.stay_channels
) or auteur
in self
.overops
:
259 self
.quitter(message
[1].encode("utf-8"), " ".join(message
[2:]))
260 self
.chanlist
.remove(message
[1])
261 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
263 serv
.privmsg(auteur
, "Non, je reste !")
264 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
266 serv
.privmsg(auteur
, "Je ne suis pas sur %s" % (message
[1]))
270 if auteur
in self
.overops
:
272 if message
[1] in self
.stay_channels
:
273 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
274 serv
.privmsg(auteur
, "Je stay déjà sur %s." % (message
[1]))
276 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
277 self
.stay_channels
.append(message
[1])
278 serv
.privmsg(auteur
, "Stay channels : " + " ".join(self
.stay_channels
))
280 serv
.privmsg(auteur
, "Stay channels : " + " ".join(self
.stay_channels
))
283 elif cmd
== u
"nostay":
284 if auteur
in self
.overops
:
286 if message
[1] in self
.stay_channels
:
287 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
288 self
.stay_channels
.remove(message
[1])
289 serv
.privmsg(auteur
, "Stay channels : " + " ".join(self
.stay_channels
))
291 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
292 serv
.privmsg(auteur
, "Je ne stay pas sur %s." % (message
[1]))
296 if auteur
in self
.overops
:
297 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
301 elif cmd
== u
"crash":
302 if auteur
in self
.overops
:
303 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
304 self
.crash(auteur
, "priv")
307 elif cmd
== u
"reload":
308 if auteur
in self
.ops
:
309 self
.execute_something("reload", {"auteur" : auteur
}, place
=auteur
, auteur
=auteur
)
312 elif cmd
== u
"quiet":
313 if auteur
in self
.ops
:
315 if message
[1] in self
.quiet_channels
:
316 serv
.privmsg(auteur
, "Je me la ferme déjà sur %s" % (message
[1]))
317 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
319 self
.quiet_channels
.append(message
[1])
320 serv
.privmsg(auteur
, "Quiet channels : " + " ".join(self
.quiet_channels
))
321 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
323 serv
.privmsg(auteur
, "Quiet channels : " + " ".join(self
.quiet_channels
))
326 elif cmd
== u
"noquiet":
327 if auteur
in self
.ops
:
329 if message
[1] in self
.quiet_channels
:
330 self
.quiet_channels
.remove(message
[1])
331 serv
.privmsg(auteur
, "Quiet channels : " + " ".join(self
.quiet_channels
))
332 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
334 serv
.privmsg(auteur
, "Je ne me la ferme pas sur %s." % (message
[1]))
335 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
339 if auteur
in self
.overops
and len(message
) > 2:
340 serv
.privmsg(message
[1].encode("utf-8"), (u
" ".join(message
[2:])).encode("utf-8"))
341 log(self
.serveur
, "priv", auteur
, " ".join(message
))
342 elif len(message
) <= 2:
343 serv
.privmsg(auteur
, "Syntaxe : SAY <channel> <message>")
347 if auteur
in self
.overops
and len(message
) > 2:
348 serv
.action(message
[1], " ".join(message
[2:]))
349 log(self
.serveur
, "priv", auteur
, " ".join(message
))
350 elif len(message
) <= 2:
351 serv
.privmsg(auteur
, "Syntaxe : DO <channel> <action>")
355 if auteur
in self
.overops
and len(message
) > 2:
356 serv
.kick(message
[1].encode("utf-8"), message
[2].encode("utf-8"), " ".join(message
[3:]).encode("utf-8"))
357 log(self
.serveur
, "priv", auteur
, " ".join(message
))
358 elif len(message
) <= 2:
359 serv
.privmsg(auteur
, "Syntaxe : KICK <channel> <pseudo> [<raison>]")
363 if auteur
in self
.overops
:
364 serv
.privmsg(auteur
, " ".join(self
.ops
))
367 elif cmd
== u
"overops":
368 if auteur
in self
.overops
:
369 serv
.privmsg(auteur
, " ".join(self
.overops
))
372 elif cmd
== u
"dump" and auteur
in self
.ops
:
373 self
.dump(asked_by
=auteur
)
374 elif cmd
== u
"restore" and auteur
in self
.overops
:
375 self
.restore(asked_by
=auteur
)
379 serv
.privmsg(auteur
, "Je n'ai pas compris. Essayez HELP…")
381 def on_pubmsg(self
, serv
, ev
):
382 """À la réception d'un message sur un channel."""
383 if ignore_event(serv
, ev
):
385 auteur
= irclib
.nm_to_n(ev
.source())
387 message
= ev
.arguments()[0]
389 message
= bot_unicode(message
)
390 except errors
.UnicodeBotError
:
391 if config
.utf8_trigger
and not canal
in self
.quiet_channels
:
392 serv
.privmsg(canal
, (u
"%s: %s"% ( auteur
, random
.choice(config
.utf8_fail_answers
))).encode("utf8"))
394 pour_moi
, message
= self
.pourmoi(serv
, message
)
395 if pour_moi
and message
.split()!=[]:
396 cmd
= message
.split()[0].lower()
398 args
= " ".join(message
.split()[1:])
401 if cmd
in [u
"meurs", u
"die", u
"crève"]:
402 if auteur
in self
.overops
:
403 log(self
.serveur
, canal
, auteur
, message
+ "[successful]")
406 serv
.privmsg(canal
,(u
"%s: %s"%(auteur
, random
.choice(config
.quit_fail_messages
))).encode("utf8"))
407 log(self
.serveur
, canal
, auteur
, message
+ "[failed]")
408 elif cmd
== u
"reload":
409 if auteur
in self
.ops
:
410 self
.execute_something("reload", {"auteur" : auteur
}, place
=canal
, auteur
=auteur
)
411 elif cmd
== u
"crash":
412 if auteur
in self
.overops
:
413 self
.crash(auteur
, canal
)
414 elif cmd
in [u
"part", u
"leave", u
"dégage", u
"va-t-en"]:
415 if auteur
in self
.ops
and (not (canal
in self
.stay_channels
)
416 or auteur
in self
.overops
):
418 log(self
.serveur
, canal
, auteur
, message
+ "[successful]")
419 if canal
in self
.chanlist
:
420 self
.chanlist
.remove(canal
)
422 serv
.privmsg(canal
,(u
"%s: %s" % (auteur
, random
.choice(config
.leave_fail_messages
))).encode("utf8"))
423 log(self
.serveur
, canal
, auteur
, message
+ "[failed]")
425 elif cmd
in [u
"ping"] and not canal
in self
.quiet_channels
:
426 serv
.privmsg(canal
, "%s: pong" % (auteur
))
427 elif cmd
in [u
"dump"]:
428 self
.dump(asked_by
=auteur
, asked_where
=canal
)
429 elif cmd
in [u
"restore"] and auteur
in self
.overops
:
430 self
.restore(asked_by
=auteur
, asked_where
=canal
)
431 elif cmd
in [u
"display"]:
432 self
.serv
.privmsg(canal
, "%s: %s" % (auteur
, config
.quote_display_url
.encode("utf-8")))
434 # Vu que ce bot est prévu pour parser des quotes il va falloir bosser ici
435 match
= self
.quote_pattern
.match(message
)
437 d
= match
.groupdict()
438 # On n'autorise pas les gens à déclarer le quoter
439 d
["quoter"] = auteur
.decode("utf-8")
440 if self
.quotedb
.store(**d
):
441 serv
.privmsg(canal
, (u
"%s: Ce sera retenu, répété, amplifié" % (auteur
,)).encode("utf-8"))
444 serv
.privmsg(canal
, (u
"%s: Je le savais déjà." % (auteur
,)).encode("utf-8"))
445 if message
.startswith(u
"!quote"):
446 if message
.strip() == u
"!quote":
447 q
= self
.quotedb
.random()
448 serv
.privmsg(canal
, str(q
))
449 elif message
.startswith("!quote "):
450 author
= message
[7:].strip()
452 q
= self
.quotedb
.randomfrom(author
)
454 serv
.privmsg(canal
, (u
"Pas de quote de %s en mémoire." % author
).encode("utf-8"))
456 serv
.privmsg(canal
, str(q
))
457 elif message
.startswith(u
"!author") or message
.startswith(u
"!from"):
458 words
= message
.split()
459 cmd
= words
[0].lstrip("!")
460 regexp
= any([cmd
.endswith(suffix
) for suffix
in config
.regex_suffixes
])
461 search
= u
" ".join(words
[1:])
462 authors
= self
.quotedb
.search_authors(search
, regexp
)
464 serv
.privmsg(canal
, "%s: Pas d'auteur correspondant à la recherche." % (auteur
,))
466 if cmd
.startswith("author"):
467 if len(authors
) > config
.search_max_authors
:
468 authors
= authors
[:config
.search_max_authors
+1] + ["+%s" % (len(authors
) - config
.search_max_authors
)]
469 serv
.privmsg(canal
, "%s: %s" % (auteur
, (u
", ".join(authors
)).encode("utf-8")))
470 elif cmd
.startswith("from"):
471 quotes
= sum([self
.quotedb
.quotesfrom(a
) for a
in authors
], [])
472 q
= random
.choice(quotes
)
473 serv
.privmsg(canal
, str(q
))
474 elif message
.startswith(u
"!search"):
475 words
= message
.split()
476 cmd
= words
[0].lstrip("!")
477 regexp
= cmd
in ["search" + suffix
for suffix
in config
.regex_suffixes
]
478 search
= u
" ".join(words
[1:])
479 quotes
= self
.quotedb
.search(inquote
=search
, regexp
=regexp
)
481 q
= random
.choice(quotes
)
482 serv
.privmsg(canal
, str(q
))
484 serv
.privmsg(canal
, "%s: Pas de quotes correspondant à la recherche." % (auteur
,))
486 def on_action(self
, serv
, ev
):
487 """À la réception d'une action."""
488 if ignore_event(serv
, ev
):
490 action
= ev
.arguments()[0]
491 auteur
= irclib
.nm_to_n(ev
.source())
492 channel
= ev
.target()
494 action
= bot_unicode(action
)
495 except errors
.UnicodeBotError
:
496 if config
.utf8_trigger
and not channel
in self
.quiet_channels
:
497 serv
.privmsg(channel
, (u
"%s: %s"%(auteur
,random
.choice(config
.utf8_fail_answers
))).encode("utf8"))
501 def on_kick(self
, serv
, ev
):
502 """À la réception d'une action."""
503 auteur
= irclib
.nm_to_n(ev
.source())
504 channel
= ev
.target()
505 victime
= ev
.arguments()[0]
506 raison
= ev
.arguments()[1]
507 if victime
== self
.nick
:
508 log(self
.serveur
, "%s kické de %s par %s (raison : %s)" % (victime
, channel
, auteur
, raison
))
512 def on_registered_nick(self
, serv
, ev
):
513 """À la réception d'un résultat de whois."""
514 pseudo
, regis
= ev
.arguments()
515 if regis
== 'is a registered nick':
516 print "%s est enregistré \o/" % pseudo
519 def start_as_daemon(self
, outfile
):
520 sys
.stderr
= Logger(outfile
)
524 class Logger(object):
525 """Pour écrire ailleurs que sur stdout"""
526 def __init__(self
, filename
="parrot.full.log"):
527 self
.filename
= filename
529 def write(self
, message
):
530 f
= open(self
.filename
, "a")
535 """Exécution principale : lecture des paramètres et lancement du bot."""
536 if len(sys
.argv
) == 1:
537 print "Usage : parrot.py <serveur> [--debug] [--no-output] [--daemon [--pidfile]] [--outfile]"
538 print " --outfile sans --no-output ni --daemon n'a aucun effet"
540 serveur
= sys
.argv
[1]
541 if "--daemon" in sys
.argv
:
542 thisfile
= os
.path
.realpath(__file__
)
543 thisdirectory
= thisfile
.rsplit("/", 1)[0]
544 os
.chdir(thisdirectory
)
548 if "debug" in sys
.argv
or "--debug" in sys
.argv
:
552 if "--quiet" in sys
.argv
:
553 config
.debug_stdout
= False
555 "irc" : "irc.crans.org",
556 "crans" : "irc.crans.org",
557 "irc.crans.org" : "irc.crans.org"}
558 if "--no-output" in sys
.argv
or "--daemon" in sys
.argv
:
559 outfile
= "/var/log/bots/parrot.full.log"
562 if arg
[0].strip('-') in ["out", "outfile", "logfile"]:
564 sys
.stdout
= Logger(outfile
)
566 serveur
= serveurs
[serveur
]
568 print "Server Unknown : %s" % (serveur
)
570 parrot
= Parrot(serveur
,debug
)
571 # Si on reçoit un SIGHUP, on reload la config
572 def sighup_handler(signum
, frame
):
573 parrot
.execute_reload(auteur
="SIGHUP")
574 signal
.signal(signal
.SIGHUP
, sighup_handler
)
577 child_pid
= os
.fork()
580 parrot
.start_as_daemon(outfile
)
582 # on enregistre le pid de parrot
583 pidfile
= "/var/run/bots/parrot.pid"
586 if arg
[0].strip('-') in ["pidfile"]:
588 f
= open(pidfile
, "w")
589 f
.write("%s\n" % child_pid
)
594 if __name__
== "__main__":