"""lg-rez / features / Tâches planifiées
Planification, liste, annulation, exécution de tâches planifiées
"""
import datetime
from discord.ext import commands
from lgrez.blocs import tools
from lgrez.bdd import Tache, Action
[docs]class GestionTaches(commands.Cog):
"""Commandes de planification, exécution, annulation de tâches"""
@commands.command()
@tools.mjs_only
async def taches(self, ctx):
"""Liste les tâches en attente (COMMANDE MJ)
Affiche les commandes en attente d'exécution (dans la table
:class:`.bdd.Tache`) et le timestamp d'exécution associé.
Lorsque la tâche est liée à une action, affiche le nom de
l'action et du joueur concerné.
"""
async with ctx.typing():
lst = Tache.query.order_by(Tache.timestamp).all()
LT = ""
for tache in lst:
LT += (
f"\n{str(tache.id).ljust(5)} "
f"{tache.timestamp.strftime('%d/%m/%Y %H:%M:%S')} "
f"{tache.commande.ljust(25)} ")
if (action := tache.action):
LT += f"{action.base.slug.ljust(20)} {action.joueur.nom}"
if LT:
mess = (
"Tâches en attente : \n\nID Timestamp "
"Commande Action Joueur"
f"\n{'-'*105}{LT}\n\n"
"Utilisez !cancel <ID> pour annuler une tâche."
)
else:
mess = "Aucune tâche en attente."
await tools.send_code_blocs(ctx, mess)
@commands.command(aliases=["doat"])
@tools.mjs_only
async def planif(self, ctx, quand, *, commande):
"""Planifie une tâche au moment voulu (COMMANDE MJ)
Args:
quand: format ``[<J>/<M>[/<AAAA>]-]<H>:<M>[:<S>]``,
avec ``<J>``, ``<M>``, ``<AAAA>``, ``<H>`` et ``<M>``
des entiers et ``<S>`` un entier/flottant optionnel.
La date est optionnelle (défaut : date du jour).
Si elle est précisée, elle doit être **séparée de
l'heure par un tiret** et l'année peut être omise ;
commande: commande à exécuter (commençant par un ``!``).
La commande sera exécutée PAR UN WEBHOOK dans LE CHAN
``#logs`` : toutes les commandes qui sont liées au
joueur ou réservées au chan privé sont à proscrire
(ou doivent a minima être précédées de ``!doas cible``)
Cette commande repose sur l'architecture en base de données,
ce qui garantit l'exécution de la tâche même si le bot plante
entre temps.
Si le bot est down à l'heure d'exécution prévue, la commande
sera exécutée dès le bot de retour en ligne.
Si la date est dans le passé, la commande est exécutée
immédiatement.
Examples:
- ``!planif 18:00 !close maire``
- ``!planif 13/06-10:00 !open maire``
- ``!planif 13/06/2020-10:00 !open maire``
- ``!planif 23:25:12 !close maire``
"""
now = datetime.datetime.now()
if "/" in quand: # Date précisée
date, time = quand.split("-")
J, MA = date.split("/", maxsplit=1)
day = int(J)
if "/" in MA: # Année précisée
M, A = MA.split("/")
month = int(M)
year = int(A)
else:
month = int(MA)
year = now.year
date = datetime.date(year=year, month=month, day=day)
else:
date = now.date()
time = quand
H, MS = time.split(":", maxsplit=1)
hour = int(H)
if ":" in MS: # Secondes précisées
M, S = MS.split(":")
minute = int(M)
second = int(S)
else:
minute = int(MS)
second = 0
time = datetime.time(hour=hour, minute=minute, second=second)
ts = datetime.datetime.combine(date, time)
action_id = None
# ID de l'action associée à la tâche le cas échéant
try:
quoi, id = commande.split()
if quoi in ["!open", "!close", "!remind"]:
action_id = int(id)
except ValueError:
pass
if ts < datetime.datetime.now():
mess = await ctx.send(
"Date dans le passé ==> exécution immédiate ! On valide ?"
)
if not await tools.yes_no(mess):
await ctx.send("Mission aborted.")
return
tache = Tache(timestamp=ts,
commande=commande,
action=Action.query.get(action_id))
tache.add() # Planifie la tâche
await ctx.send(
f"{tools.code(commande)} planifiée pour le "
f"{tools.code(ts.strftime('%d/%m/%Y %H:%M:%S'))}.\n"
f"{tools.code(f'!cancel {tache.id}')} pour annuler."
)
@commands.command(aliases=["retard", "doin"])
@tools.mjs_only
async def delay(self, ctx, duree, *, commande):
"""Exécute une commande après XhYmZs (COMMANDE MJ)
Args:
quand: format ``[<X>h][<Y>m][<Z>s]``, avec ``<X>`` (heures)
et ``<Y>`` (minutes) des entiers et ``<Z>`` (secondes)
un entier ou un flottant. Chacune des trois composantes
est optionnelle, mais au moins une doit être présente ;
commande: commande à exécuter (commençant par un ``!``).
La commande sera exécutée PAR UN WEBHOOK dans LE CHAN
``#logs`` : toutes les commandes qui sont liées au
joueur ou réservées au chan privé sont à proscrire
(ou doivent être précédées d'un ``!doas <cible>``).
Cette commande repose sur l'architecture en base de données,
ce qui garantit l'exécution de la commande même si le bot plante
entre temps.
Si le bot est down à l'heure d'exécution prévue, la commande
sera exécutée dès le bot de retour en ligne.
Examples:
- ``!delay 2h !close maire``
- ``!delay 1h30m !doas @moi !vote Yacine Oussar``
"""
secondes = 0
try:
if "h" in duree.lower():
h, duree = duree.split("h")
secondes += 3600*int(h)
if "m" in duree.lower():
m, duree = duree.split("m")
secondes += 60*int(m)
if "s" in duree.lower():
s, duree = duree.split("s")
secondes += float(s)
except Exception as e:
raise commands.BadArgument("<duree>") from e
if duree or not secondes:
raise commands.BadArgument("<duree>")
ts = datetime.datetime.now() + datetime.timedelta(seconds=secondes)
action_id = None
# ID de l'action associée à la tâche le cas échéant
try:
quoi, id = commande.split(" ")
if quoi in ["!open", "!close", "!remind"]:
action_id = int(id)
except ValueError:
pass
tache = Tache(timestamp=ts,
commande=commande,
action=Action.query.get(action_id))
tache.add() # Planifie la tâche
await ctx.send(
f"Commande {tools.code(commande)} planifiée pour le "
f"{tools.code(ts.strftime('%d/%m/%Y %H:%M:%S'))}\n"
f"{tools.code(f'!cancel {tache.id}')} pour annuler."
)
@commands.command()
@tools.mjs_only
async def cancel(self, ctx, *ids):
"""Annule une ou plusieurs tâche(s) planifiée(s) (COMMANDE MJ)
Args:
*ids: IDs des tâches à annuler, séparées par des espaces.
Utiliser ``!taches`` pour voir la liste des IDs.
"""
taches = []
for id in ids:
if id.isdigit() and (tache := Tache.query.get(int(id))):
taches.append(tache)
if not taches:
await ctx.send("Aucune tâche trouvée.")
return
message = await ctx.send("Annuler les tâches :\n" + "\n".join([
f" - {tools.code(tache.timestamp.strftime('%d/%m/%Y %H:%M:%S'))} "
f"> {tools.code(tache.commande)}" for tache in taches
]))
if not await tools.yes_no(message):
await ctx.send("Mission aborted.")
return
Tache.delete(*taches) # Annule les tâches
await ctx.send("Tâche(s) annulée(s).")