]>
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
28 def get_config_logfile(serveur
):
29 """Renvoie le nom du fichier de log en fonction du ``serveur`` et de la config."""
30 serveurs
= {"irc.crans.org" : "crans"}
31 return config
.logfile_template
% (serveurs
[serveur
],)
33 def log(serveur
, channel
, auteur
=None, message
=None):
34 """Enregistre une ligne de log."""
35 if auteur
== message
== None:
36 # alors c'est que c'est pas un channel mais juste une ligne de log
37 chain
= u
"%s %s" % (time
.strftime("%F %T"), channel
)
39 chain
= u
"%s [%s:%s] %s" % (time
.strftime("%F %T"), channel
, auteur
, message
)
40 f
= open(get_config_logfile(serveur
), "a")
41 f
.write((chain
+ u
"\n").encode("utf-8"))
43 if config
.debug_stdout
:
44 print chain
.encode("utf-8")
46 def ignore_event(serv
, ev
):
47 """Retourne ``True`` si il faut ignorer cet évènement."""
48 for (blackmask
, exceptlist
) in config
.blacklisted_masks
:
49 usermask
= ev
.source()
50 blackit
= bool(irclib
.mask_matches(usermask
, blackmask
))
51 exceptit
= any([bool(irclib
.mask_matches(usermask
, exceptmask
)) for exceptmask
in exceptlist
])
52 if exceptit
: # Il est exempté
55 if blackit
: # Il n'est pas exempté et matche la blacklist
59 def bot_unicode(chain
):
60 """Essaye de décoder ``chain`` en UTF-8.
61 Lève une py:class:`errors.UnicodeBotError` en cas d'échec."""
63 return chain
.decode("utf8")
64 except UnicodeDecodeError as exc
:
65 raise errors
.UnicodeBotError
68 class Parrot(ircbot
.SingleServerIRCBot
):
69 """Classe principale : définition du bot."""
70 def __init__(self
, serveur
, debug
=False):
71 temporary_pseudo
= config
.irc_pseudo
+ str(random
.randrange(10000,100000))
72 ircbot
.SingleServerIRCBot
.__init
__(self
, [(serveur
, 6667)],
73 temporary_pseudo
, "Parrot, le bot irc. [Codé par 20-100]", 10)
75 self
.serveur
= serveur
76 self
.overops
= config
.overops
77 self
.ops
= self
.overops
+ config
.ops
78 self
.report_bugs_to
= config
.report_bugs_to
79 self
.chanlist
= config
.chanlist
80 self
.stay_channels
= config
.stay_channels
81 self
.quiet_channels
= config
.quiet_channels
86 """Récuère le nick effectif du bot sur le serveur."""
87 return self
.serv
.get_nickname()
88 nick
= property(_getnick
)
90 def give_me_my_pseudo(self
, serv
):
91 """Récupère le pseudo auprès de NickServ."""
92 serv
.privmsg("NickServ", "RECOVER %s %s" % (config
.irc_pseudo
, config
.irc_password
))
93 serv
.privmsg("NickServ", "RELEASE %s %s" % (config
.irc_pseudo
, config
.irc_password
))
95 serv
.nick(config
.irc_pseudo
)
97 def pourmoi(self
, serv
, message
):
98 """Renvoie (False, lemessage) ou (True, le message amputé de "pseudo: ")"""
100 pseudo
= pseudo
.decode("utf-8")
102 if message
[:size
] == pseudo
and len(message
) > size
and message
[size
] == ":":
103 return (True, message
[size
+1:].lstrip(" "))
105 return (False, message
)
107 ### Exécution d'actions
108 def quitter(self
, chan
, leave_message
=None):
109 """Quitter un channel avec un message customisable."""
110 if leave_message
== None:
111 leave_message
= random
.choice(config
.leave_messages
)
112 self
.serv
.part(chan
, message
=leave_message
.encode("utf8"))
115 """Se déconnecter du serveur IRC avec un message customisable."""
116 quit_message
= random
.choice(config
.quit_messages
)
117 self
.die(msg
=quit_message
.encode("utf8"))
119 def execute_reload(self
, auteur
=None):
120 """Recharge la config."""
122 isit
.regexp_compile()
123 if auteur
in [None, "SIGHUP"]:
124 towrite
= "Config reloaded" + " (SIGHUP received)" * (auteur
== "SIGHUP")
125 for to
in config
.report_bugs_to
:
126 self
.serv
.privmsg(to
, towrite
)
127 log(self
.serveur
, towrite
)
130 return True, u
"Config reloaded"
132 def crash(self
, who
="nobody", chan
="nowhere"):
133 """Fait crasher le bot."""
134 where
= "en privé" if chan
== "priv" else "sur le chan %s" % chan
135 raise errors
.CrashError((u
"Crash demandé par %s %s" % (who
, where
)).encode("utf-8"))
138 "reload" : execute_reload
,
141 def execute_something(self
, something
, params
, place
=None, auteur
=None):
142 """Exécute une action et répond son résultat à ``auteur``
143 sur un chan ou en privé en fonction de ``place``"""
144 action
= self
.ACTIONS
[something
]
145 success
, message
= action(self
, **params
)
147 if irclib
.is_channel(place
):
148 message
= "%s: %s" % (auteur
, message
.encode("utf-8"))
149 self
.serv
.privmsg(place
, message
)
150 log(self
.serveur
, place
, auteur
, something
+ "%r" % params
+ ("[successful]" if success
else "[failed]"))
152 ### Gestion des quotes
155 ### Surcharge des events du Bot
156 def on_welcome(self
, serv
, ev
):
157 """À l'arrivée sur le serveur."""
158 self
.serv
= serv
# ça serv ira :)
159 self
.give_me_my_pseudo(serv
)
160 serv
.privmsg("NickServ", "IDENTIFY %s" % (config
.irc_password
))
161 log(self
.serveur
, "Connected")
163 self
.chanlist
= ["#bot"]
164 for c
in self
.chanlist
:
165 log(self
.serveur
, "JOIN %s" % (c
))
168 def on_privmsg(self
, serv
, ev
):
169 """À la réception d'un message en privé."""
170 if ignore_event(serv
, ev
):
172 message
= ev
.arguments()[0]
173 auteur
= irclib
.nm_to_n(ev
.source())
175 message
= bot_unicode(message
)
176 except errors
.UnicodeBotError
:
177 if config
.utf8_trigger
:
178 serv
.privmsg(auteur
, random
.choice(config
.utf8_fail_answers
).encode("utf8"))
180 message
= message
.split()
181 cmd
= message
[0].lower()
182 notunderstood
= False
184 op
,overop
=auteur
in self
.ops
, auteur
in self
.overops
186 helpmsg
= config
.helpmsg_default
188 helpmsg
+= config
.helpmsg_ops
190 helpmsg
+= config
.helpmsg_overops
192 helpmsgs
= config
.helpdico
.get(message
[1].lower(), ["Commande inconnue.", None, None])
193 helpmsg
= helpmsgs
[0]
194 if op
and helpmsgs
[1]:
196 helpmsg
+= "\n" + helpmsgs
[1]
198 helpmsg
= helpmsgs
[1]
199 if overop
and helpmsgs
[2]:
201 helpmsg
+= "\n" + helpmsgs
[2]
203 helpmsg
= helpmsgs
[2]
204 for ligne
in helpmsg
.split("\n"):
205 serv
.privmsg(auteur
, ligne
.encode("utf-8"))
207 if auteur
in self
.ops
:
209 if message
[1] in self
.chanlist
:
210 serv
.privmsg(auteur
, "Je suis déjà sur %s" % (message
[1]))
212 serv
.join(message
[1])
213 self
.chanlist
.append(message
[1])
214 serv
.privmsg(auteur
, "Channels : " + " ".join(self
.chanlist
))
215 log(self
.serveur
, "priv", auteur
, " ".join(message
))
217 serv
.privmsg(auteur
, "Channels : " + " ".join(self
.chanlist
))
220 elif cmd
== u
"leave":
221 if auteur
in self
.ops
and len(message
) > 1:
222 if message
[1] in self
.chanlist
:
223 if not (message
[1] in self
.stay_channels
) or auteur
in self
.overops
:
224 self
.quitter(message
[1].encode("utf-8"), " ".join(message
[2:]))
225 self
.chanlist
.remove(message
[1])
226 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
228 serv
.privmsg(auteur
, "Non, je reste !")
229 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
231 serv
.privmsg(auteur
, "Je ne suis pas sur %s" % (message
[1]))
235 if auteur
in self
.overops
:
237 if message
[1] in self
.stay_channels
:
238 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
239 serv
.privmsg(auteur
, "Je stay déjà sur %s." % (message
[1]))
241 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
242 self
.stay_channels
.append(message
[1])
243 serv
.privmsg(auteur
, "Stay channels : " + " ".join(self
.stay_channels
))
245 serv
.privmsg(auteur
, "Stay channels : " + " ".join(self
.stay_channels
))
248 elif cmd
== u
"nostay":
249 if auteur
in self
.overops
:
251 if message
[1] in self
.stay_channels
:
252 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
253 self
.stay_channels
.remove(message
[1])
254 serv
.privmsg(auteur
, "Stay channels : " + " ".join(self
.stay_channels
))
256 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
257 serv
.privmsg(auteur
, "Je ne stay pas sur %s." % (message
[1]))
261 if auteur
in self
.overops
:
262 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
266 elif cmd
== u
"crash":
267 if auteur
in self
.overops
:
268 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
269 self
.crash(auteur
, "priv")
272 elif cmd
== u
"reload":
273 if auteur
in self
.ops
:
274 self
.execute_something("reload", {"auteur" : auteur
}, place
=auteur
, auteur
=auteur
)
277 elif cmd
== u
"quiet":
278 if auteur
in self
.ops
:
280 if message
[1] in self
.quiet_channels
:
281 serv
.privmsg(auteur
, "Je me la ferme déjà sur %s" % (message
[1]))
282 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
284 self
.quiet_channels
.append(message
[1])
285 serv
.privmsg(auteur
, "Quiet channels : " + " ".join(self
.quiet_channels
))
286 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
288 serv
.privmsg(auteur
, "Quiet channels : " + " ".join(self
.quiet_channels
))
291 elif cmd
== u
"noquiet":
292 if auteur
in self
.ops
:
294 if message
[1] in self
.quiet_channels
:
295 self
.quiet_channels
.remove(message
[1])
296 serv
.privmsg(auteur
, "Quiet channels : " + " ".join(self
.quiet_channels
))
297 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[successful]")
299 serv
.privmsg(auteur
, "Je ne me la ferme pas sur %s." % (message
[1]))
300 log(self
.serveur
, "priv", auteur
, " ".join(message
) + "[failed]")
304 if auteur
in self
.overops
and len(message
) > 2:
305 serv
.privmsg(message
[1].encode("utf-8"), (u
" ".join(message
[2:])).encode("utf-8"))
306 log(self
.serveur
, "priv", auteur
, " ".join(message
))
307 elif len(message
) <= 2:
308 serv
.privmsg(auteur
, "Syntaxe : SAY <channel> <message>")
312 if auteur
in self
.overops
and len(message
) > 2:
313 serv
.action(message
[1], " ".join(message
[2:]))
314 log(self
.serveur
, "priv", auteur
, " ".join(message
))
315 elif len(message
) <= 2:
316 serv
.privmsg(auteur
, "Syntaxe : DO <channel> <action>")
320 if auteur
in self
.overops
and len(message
) > 2:
321 serv
.kick(message
[1].encode("utf-8"), message
[2].encode("utf-8"), " ".join(message
[3:]).encode("utf-8"))
322 log(self
.serveur
, "priv", auteur
, " ".join(message
))
323 elif len(message
) <= 2:
324 serv
.privmsg(auteur
, "Syntaxe : KICK <channel> <pseudo> [<raison>]")
328 if auteur
in self
.overops
:
329 serv
.privmsg(auteur
, " ".join(self
.ops
))
332 elif cmd
== u
"overops":
333 if auteur
in self
.overops
:
334 serv
.privmsg(auteur
, " ".join(self
.overops
))
340 serv
.privmsg(auteur
, "Je n'ai pas compris. Essayez HELP…")
342 def on_pubmsg(self
, serv
, ev
):
343 """À la réception d'un message sur un channel."""
344 if ignore_event(serv
, ev
):
346 auteur
= irclib
.nm_to_n(ev
.source())
348 message
= ev
.arguments()[0]
350 message
= bot_unicode(message
)
351 except errors
.UnicodeBotError
:
352 if config
.utf8_trigger
and not canal
in self
.quiet_channels
:
353 serv
.privmsg(canal
, (u
"%s: %s"% ( auteur
, random
.choice(config
.utf8_fail_answers
))).encode("utf8"))
355 pour_moi
, message
= self
.pourmoi(serv
, message
)
356 if pour_moi
and message
.split()!=[]:
357 cmd
= message
.split()[0].lower()
359 args
= " ".join(message
.split()[1:])
362 if cmd
in [u
"meurs", u
"die", u
"crève"]:
363 if auteur
in self
.overops
:
364 log(self
.serveur
, canal
, auteur
, message
+ "[successful]")
367 serv
.privmsg(canal
,(u
"%s: %s"%(auteur
, random
.choice(config
.quit_fail_messages
))).encode("utf8"))
368 log(self
.serveur
, canal
, auteur
, message
+ "[failed]")
369 elif cmd
== u
"reload":
370 if auteur
in self
.ops
:
371 self
.execute_something("reload", {"auteur" : auteur
}, place
=canal
, auteur
=auteur
)
372 elif cmd
== u
"crash":
373 if auteur
in self
.overops
:
374 self
.crash(auteur
, canal
)
375 elif cmd
in [u
"part", u
"leave", u
"dégage", u
"va-t-en"]:
376 if auteur
in self
.ops
and (not (canal
in self
.stay_channels
)
377 or auteur
in self
.overops
):
379 log(self
.serveur
, canal
, auteur
, message
+ "[successful]")
380 if canal
in self
.chanlist
:
381 self
.chanlist
.remove(canal
)
383 serv
.privmsg(canal
,(u
"%s: %s" % (auteur
, random
.choice(config
.leave_fail_messages
))).encode("utf8"))
384 log(self
.serveur
, canal
, auteur
, message
+ "[failed]")
386 elif cmd
in [u
"ping"] and not canal
in self
.quiet_channels
:
387 serv
.privmsg(canal
, "%s: pong" % (auteur
))
389 # Vu que ce bot est prévu pour parser des quotes il va falloir bosser ici
392 def on_action(self
, serv
, ev
):
393 """À la réception d'une action."""
394 if ignore_event(serv
, ev
):
396 action
= ev
.arguments()[0]
397 auteur
= irclib
.nm_to_n(ev
.source())
398 channel
= ev
.target()
400 action
= bot_unicode(action
)
401 except errors
.UnicodeBotError
:
402 if config
.utf8_trigger
and not channel
in self
.quiet_channels
:
403 serv
.privmsg(channel
, (u
"%s: %s"%(auteur
,random
.choice(config
.utf8_fail_answers
))).encode("utf8"))
407 def on_kick(self
, serv
, ev
):
408 """À la réception d'une action."""
409 auteur
= irclib
.nm_to_n(ev
.source())
410 channel
= ev
.target()
411 victime
= ev
.arguments()[0]
412 raison
= ev
.arguments()[1]
413 if victime
== self
.nick
:
414 log(self
.serveur
, u
"%s kické de %s par %s (raison : %s)" % (victime
, channel
.decode("utf-8"), auteur
, raison
))
419 def start_as_daemon(self
, outfile
):
420 sys
.stderr
= Logger(outfile
)
424 class Logger(object):
425 """Pour écrire ailleurs que sur stdout"""
426 def __init__(self
, filename
="parrot.full.log"):
427 self
.filename
= filename
429 def write(self
, message
):
430 f
= open(self
.filename
, "a")
435 """Exécution principale : lecture des paramètres et lancement du bot."""
436 if len(sys
.argv
) == 1:
437 print "Usage : parrot.py <serveur> [--debug] [--no-output] [--daemon [--pidfile]] [--outfile]"
438 print " --outfile sans --no-output ni --daemon n'a aucun effet"
440 serveur
= sys
.argv
[1]
441 if "--daemon" in sys
.argv
:
442 thisfile
= os
.path
.realpath(__file__
)
443 thisdirectory
= thisfile
.rsplit("/", 1)[0]
444 os
.chdir(thisdirectory
)
448 if "debug" in sys
.argv
or "--debug" in sys
.argv
:
452 if "--quiet" in sys
.argv
:
453 config
.debug_stdout
= False
455 "irc" : "irc.crans.org",
456 "crans" : "irc.crans.org",
457 "irc.crans.org" : "irc.crans.org"}
458 if "--no-output" in sys
.argv
or "--daemon" in sys
.argv
:
459 outfile
= "/var/log/bots/parrot.full.log"
462 if arg
[0].strip('-') in ["out", "outfile", "logfile"]:
464 sys
.stdout
= Logger(outfile
)
466 serveur
= serveurs
[serveur
]
468 print "Server Unknown : %s" % (serveur
)
470 parrot
= Parrot(serveur
,debug
)
471 # Si on reçoit un SIGHUP, on reload la config
472 def sighup_handler(signum
, frame
):
473 parrot
.execute_reload(auteur
="SIGHUP")
474 signal
.signal(signal
.SIGHUP
, sighup_handler
)
477 child_pid
= os
.fork()
480 parrot
.start_as_daemon(outfile
)
482 # on enregistre le pid de parrot
483 pidfile
= "/var/run/bots/parror.pid"
486 if arg
[0].strip('-') in ["pidfile"]:
488 f
= open(pidfile
, "w")
489 f
.write("%s\n" % child_pid
)
494 if __name__
== "__main__":