コード例 #1
0
    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.")
コード例 #2
0
async def _do_refill(motif, actions):
    # Détermination nouveau nombre de charges
    if motif in config.refills_full:
        # Refill -> nombre de charges initial de l'action
        new_charges = {action: action.base.base_charges for action in actions}
    else:
        # Refill -> + 1 charge
        new_charges = {action: action.charges + 1 for action in actions}

    # Refill proprement dit
    for action, charge in new_charges.items():
        if charge <= action.charges:
            # Pas de rechargement à faire (déjà base_charges)
            continue

        if (not action.charges
                and action.base.trigger_debut == ActionTrigger.perma):
            # Action permanente qui était épuisée : on ré-ouvre !
            if tools.en_pause():
                ts = tools.fin_pause()
            else:
                ts = datetime.datetime.now() + datetime.timedelta(seconds=10)
                # + 10 secondes pour ouvrir après le message de refill
            Tache(timestamp=ts, commande=f"!open {action.id}",
                  action=action).add()

        action.charges = charge

        await action.joueur.private_chan.send(
            f"Ton action {action.base.slug} vient d'être rechargée, "
            f"tu as maintenant {charge} charge(s) disponible(s) !")

    config.session.commit()
コード例 #3
0
def delete_action(action):
    """Archive une action et annule les tâches en cours liées.

    Depuis la version 2.1, l'action n'est plus supprimée mais est
    passée à :attr:`~.bdd.Action.active` = ``False``.

    Args:
        action (.bdd.Action): l'action à supprimer
    """
    # Suppression tâches liées à l'action
    if action.taches:
        Tache.delete(*action.taches)

    if action.is_open:
        action.utilisation_ouverte.close()

    action.active = False
    action.update()
コード例 #4
0
def add_action(action):
    """Enregistre une action et programme son ouverture le cas échéant.

    Args:
        action (.bdd.Action): l'action à enregistrer
    """
    if not action.active:
        action.active = True
    action.add()

    # Ajout tâche ouverture
    if action.base.trigger_debut == ActionTrigger.temporel:
        # Temporel : on programme
        Tache(timestamp=tools.next_occurence(action.base.heure_debut),
              commande=f"!open {action.id}",
              action=action).add()

    elif action.base.trigger_debut == ActionTrigger.perma:
        # Perma : ON LANCE DIRECT
        Tache(timestamp=datetime.datetime.now(),
              commande=f"!open {action.id}",
              action=action).add()
コード例 #5
0
async def open_action(action):
    """Ouvre l'action.

    Args:
        action (.bdd.Action): l'action à ouvir

    Opérations réalisées :
        - Vérification des conditions (cooldown, charges...) et
          reprogrammation si nécessaire ;
        - Gestion des tâches planifiées (planifie remind/close si
          applicable) ;
        - Information du joueur.
    """
    joueur = action.joueur
    chan = joueur.private_chan

    # Vérification base
    if not action.base:
        await tools.log(f"{action} : pas de base, exit")
        return

    # Vérification active
    if not action.active:
        await tools.log(f"{action} : inactive, exit (pas de reprogrammation)")
        return

    # Vérification cooldown
    if action.cooldown > 0:  # Action en cooldown
        action.cooldown = action.cooldown - 1
        config.session.commit()
        await tools.log(f"{action} : en cooldown, exit "
                        "(reprogrammation si temporel).")
        if action.base.trigger_debut == ActionTrigger.temporel:
            # Programmation action du lendemain
            ts = tools.next_occurence(action.base.heure_debut)
            Tache(timestamp=ts, commande=f"!open {action.id}",
                  action=action).add()
        return

    # Vérification role_actif
    if not joueur.role_actif:
        # role_actif == False : on reprogramme la tâche au lendemain tanpis
        await tools.log(f"{action} : role_actif == False, exit "
                        "(reprogrammation si temporel).")
        if action.base.trigger_debut == ActionTrigger.temporel:
            ts = tools.next_occurence(action.base.heure_debut)
            Tache(timestamp=ts, commande=f"!open {action.id}",
                  action=action).add()
        return

    # Vérification charges
    if action.charges == 0:
        # Plus de charges, mais action maintenue en base car refill / ...
        await tools.log(f"{action} : plus de charges, exit "
                        "(reprogrammation si temporel).")
        return

    # Action "automatiques" (passives : notaire...) :
    # lance la procédure de clôture / résolution
    if action.base.trigger_fin == ActionTrigger.auto:
        if action.base.trigger_debut == ActionTrigger.temporel:
            await tools.log(
                f"Action {action.base.slug} pour {joueur.nom} pas vraiment "
                f"automatique, {config.Role.mj.mention} VENEZ M'AIDER "
                "JE PANIQUE 😱 (comme je suis vraiment sympa je vous "
                f"file son chan, {chan.mention})")
        else:
            await tools.log(
                f"{action} : automatique, appel processus de clôture")

        await close_action(action)
        return

    # Tous tests préliminaires n'ont pas return ==> Vraie action à lancer

    # Calcul heure de fin (si applicable)
    heure_fin = None
    if action.base.trigger_fin == ActionTrigger.temporel:
        heure_fin = action.base.heure_fin
        ts = tools.next_occurence(heure_fin)
    elif action.base.trigger_fin == ActionTrigger.delta:
        # Si delta, on calcule la vraie heure de fin (pas modifié en base)
        delta = action.base.heure_fin
        ts = (datetime.datetime.now() + datetime.timedelta(
            hours=delta.hour, minutes=delta.minute, seconds=delta.second))
        heure_fin = ts.time()

    # Programmation remind / close
    if action.base.trigger_fin in [
            ActionTrigger.temporel, ActionTrigger.delta
    ]:
        Tache(timestamp=ts - datetime.timedelta(minutes=30),
              commande=f"!remind {action.id}",
              action=action).add()
        Tache(timestamp=ts, commande=f"!close {action.id}",
              action=action).add()
    elif action.base.trigger_fin == ActionTrigger.perma:
        # Action permanente : fermer pour le WE
        # ou rappel / réinitialisation chaque jour
        ts_matin = tools.next_occurence(datetime.time(hour=7))
        ts_pause = tools.debut_pause()
        if ts_matin < ts_pause:
            # Réopen le lendamain
            Tache(timestamp=ts_matin,
                  commande=f"!open {action.id}",
                  action=action).add()
        else:
            # Sauf si pause d'ici là
            Tache(timestamp=ts_pause,
                  commande=f"!close {action.id}",
                  action=action).add()

    # Information du joueur
    if action.is_open:  # déjà ouverte
        message = await chan.send(
            f"{tools.montre()}  Rappel : tu peux utiliser quand tu le "
            f"souhaites ton action {tools.code(action.base.slug)} ! "
            f" {config.Emoji.action} \n" +
            (f"Tu as jusqu'à {heure_fin} pour le faire. \n"
             if heure_fin else "") +
            tools.ital(f"Tape {tools.code('!action (ce que tu veux faire)')}"
                       " ou utilise la réaction pour agir."))
    else:
        # on ouvre !
        util = Utilisation(action=action)
        util.add()
        util.open()
        message = await chan.send(
            f"{tools.montre()}  Tu peux maintenant utiliser ton action "
            f"{tools.code(action.base.slug)} !  {config.Emoji.action} \n" +
            (f"Tu as jusqu'à {heure_fin} pour le faire. \n"
             if heure_fin else "") +
            tools.ital(f"Tape {tools.code('!action (ce que tu veux faire)')}"
                       " ou utilise la réaction pour agir."))

    await message.add_reaction(config.Emoji.action)

    config.session.commit()
コード例 #6
0
async def close_action(action):
    """Ferme l'action.

    Args:
        action (.bdd.Action): l'action à enregistrer

    Opérations réalisées :
        - Archivage si nécessaire ;
        - Gestion des tâches planifiées (planifie prochaine ouverture
          si applicable) ;
        - Information du joueur (si charge-- seulement).
    """
    joueur = action.joueur
    chan = joueur.private_chan

    # Vérification base
    if not action.base:
        await tools.log(f"{action} : pas de base, exit")
        return

    # Vérification active
    if not action.active:
        await tools.log(f"{action} : inactive, exit")
        return

    # Vérification ouverte
    if not action.is_open:
        await tools.log(f"{action} : pas ouverte, exit")
        return

    deleted = False
    if not action.is_waiting:  # décision prise
        # Résolution de l'action
        # (pour l'instant juste charge -= 1 et suppression le cas échéant)
        if action.charges:
            action.charges = action.charges - 1
            pcs = (" pour cette semaine"
                   if "weekends" in action.base.refill else "")
            await chan.send(f"Il te reste {action.charges} charge(s){pcs}.")

            if action.charges == 0 and not action.base.refill:
                delete_action(action)
                deleted = True

    if not deleted:
        # Si l'action a été faite et a un cooldown, on le met
        if (not action.is_waiting) and (action.base.base_cooldown > 0):
            action.cooldown = action.base.base_cooldown

        action.utilisation_ouverte.close()

        # Programmation prochaine ouverture
        if action.base.trigger_debut == ActionTrigger.temporel:
            ts = tools.next_occurence(action.base.heure_debut)
            Tache(timestamp=ts, commande=f"!open {action.id}",
                  action=action).add()
        elif action.base.trigger_debut == ActionTrigger.perma:
            # Action permanente : ouvrir après le WE
            ts = tools.fin_pause()
            Tache(timestamp=ts, commande=f"!open {action.id}",
                  action=action).add()

    config.session.commit()
コード例 #7
0
    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.")
コード例 #8
0
    @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).")
コード例 #9
0
    async def cparti(self, ctx):
        """Lance le jeu (COMMANDE MJ)

        - Programme les votes condamnés quotidiens (avec chaînage) 10h-18h
        - Programme un vote maire 10h-18h
        - Programme les actions au lancement du jeu (choix de mentor...)
          et permanentes (forgeron)... à 19h
        - Crée les "actions de vote", sans quoi !open plante

        À utiliser le jour du lancement après 10h (lance les premières
        actions le soir et les votes le lendemain)
        """

        message = await ctx.send(
            "C'est parti ?\n"
            "Les rôles ont bien été attribués et synchronisés ?"
            " (si non, le faire AVANT de valider)\n\n"
            "On est bien après 10h le jour du lancement ?\n\n"
            "Tu es conscient que tous les joueurs reçevront à 18h55 un message"
            " en mode « happy Hunger Games » ? (codé en dur parce que flemme)")
        if not await tools.yes_no(message):
            await ctx.send("Mission aborted.")
            return

        message = await ctx.send(
            "Les actions des joueurs ont été attribuées à la synchronisation "
            "des rôles, mais les !open n'ont aucun impact tant que tout le "
            "monde est en `role_actif == False` sur le Tableau de bord.\n"
            "Il faut donc **passer tout le monde à `True` maintenant**"
            "(puis `!sync silent`) avant de continuer.")
        if not await tools.yes_no(message):
            await ctx.send("Mission aborted.")
            return

        taches = []
        r = "C'est parti !\n"

        n10 = tools.next_occurence(datetime.time(hour=10))
        n19 = tools.next_occurence(datetime.time(hour=19))

        # Programmation votes condamnés chainés 10h-18h
        r += "\nProgrammation des votes :\n"
        taches.append(Tache(timestamp=n10, commande="!open cond 18h 10h"))
        r += " - À 10h : !open cond 18h 10h\n"

        # Programmation votes loups chainés 19h-23h
        taches.append(Tache(timestamp=n19, commande="!open loups 23h 19h"))
        r += " - À 19h : !open loups 23h 19h\n"

        # Programmation premier vote maire 10h-17h
        taches.append(Tache(timestamp=n10, commande="!open maire 17h"))
        r += " - À 10h : !open maire 17h\n"

        # Programmation actions au lancement et actions permanentes
        r += "\nProgrammation des actions start / perma :\n"
        start_perma = Action.query.filter(
            Action.base.has(
                BaseAction.trigger_debut.in_(
                    [ActionTrigger.start, ActionTrigger.perma]))).all()
        for action in start_perma:
            r += (f" - À 19h : !open {action.id} "
                  f"(trigger_debut == {action.base.trigger_debut})\n")
            taches.append(
                Tache(timestamp=n19,
                      commande=f"!open {action.id}",
                      action=action))

        # Programmation refill weekends
        # r += "\nProgrammation des refills weekends :\n"
        # ts = tools.fin_pause() - datetime.timedelta(minutes=5)
        # taches.append(Tache(timestamp=ts,
        #                     commande=f"!refill weekends all"))
        # r += " - Dimanche à 18h55 : !refill weekends all\n"

        # Programmation envoi d'un message aux connards
        r += ("\nEt, à 18h50 : !send all [message de hype oue oue "
              "c'est génial]\n")
        taches.append(
            Tache(
                timestamp=(n19 - datetime.timedelta(minutes=10)),
                commande=("!send all Ah {member.mention}... J'espère que tu "
                          "es prêt(e), parce que la partie commence DANS 10 "
                          " MINUTES !!! https://tenor.com/view/thehungergames-"
                          "hungergames-thggifs-effie-gif-5114734")))
        await tools.log(r, code=True)

        # Drop (éventuel) et (re-)création actions de vote
        Action.query.filter_by(base=None).delete()
        actions = []
        for joueur in Joueur.query.all():
            for vote in Vote:
                actions.append(Action(joueur=joueur, vote=vote))

        Tache.add(*taches)  # On enregistre et programme le tout !
        Action.add(*actions)

        await ctx.send(
            f"C'est tout bon ! (détails dans {config.Channel.logs.mention})")
コード例 #10
0
    async def close(self, ctx, qui, heure=None, heure_chain=None):
        """Ferme un vote / des actions de rôle (COMMANDE BOT / MJ)

        Args:
            qui:
                ===========     ===========
                ``cond``        pour le vote du condamné
                ``maire``       pour le vote du maire
                ``loups``       pour le vote des loups
                ``action``      pour les actions se terminant à ``heure``
                ``{id}``        pour une action spécifique
                ===========     ===========

            heure:
                - si ``qui == "cond"``, ``"maire"`` ou ``"loup"``,
                  programme en plus une prochaine ouverture à ``heure``
                  (et un rappel 30 minutes avant) ;
                - si ``qui == "action"``, il est obligatoire : heure des
                  actions à lancer (cf plus haut). Pour les actions, la
                  prochaine est de toute façon programmée le cas échéant
                  (cooldown à 0 et reste des charges).

                Dans tous les cas, format ``HHh`` ou ``HHhMM``.

            heure_chain:
                permet de chaîner des votes : ferme le vote immédiatement
                et programme une prochaine ouverture à ``heure``, en
                appellant ``!close`` de sorte à programmer une nouvelle
                fermeture le lendemain à ``heure_chain``, et ainsi de suite.
                Format ``HHh`` ou ``HHhMM``.

        Une sécurité empêche de fermer un vote ou une action
        qui n'est pas en cours.

        Cette commande a pour vocation première d'être exécutée
        automatiquement par des tâches planifiées.
        Elle peut être utilisée à la main, mais attention à ne pas
        faire n'importe quoi (penser à envoyer / planifier la fermeture
        des votes, par exemple).

        Examples:
            - ``!close maire`` :        ferme le vote condamné maintenant
            - ``!close cond 10h`` :     ferme le vote condamné maintenant
              et programme une prochaine ouverture à 10h00
            - ``!close cond 10h 18h`` : ferme le vote condamné maintenant,
              programme une prochaine ouverture à 10h00, qui sera fermé à
              18h, puis une nouvelle ouverture à 10h, etc
            - ``!close action 22h`` :   ferme toutes les actions
              se terminant à 22h00
            - ``!close 122`` :          ferme l'action d'ID 122
        """
        try:
            qui = Vote[qui.lower()]  # cond / maire / loups
        except KeyError:
            pass
        joueurs = await recup_joueurs("close", qui, heure)

        str_joueurs = "\n - ".join([joueur.nom for joueur in joueurs])
        await tools.send_code_blocs(
            ctx,
            f"Utilisateur(s) répondant aux critères ({len(joueurs)}) : \n" +
            str_joueurs)

        # Fermeture utilisations et envoi messages
        for joueur in joueurs:
            chan = joueur.private_chan

            if isinstance(qui, Vote):
                util = joueur.action_vote(qui).utilisation_ouverte
                nom_cible = util.cible.nom if util.cible else "*non défini*"

                util.close()  # update direct pour empêcher de voter

            if qui == Vote.cond:
                await chan.send(
                    f"{tools.montre()}  Fin du vote pour le condamné du jour !"
                    f"\nVote définitif : {nom_cible}\n"
                    f"Les résultats arrivent dans l'heure !\n")

            elif qui == Vote.maire:
                await chan.send(
                    f"{tools.montre()}  Fin du vote pour le maire ! \n"
                    f"Vote définitif : {nom_cible}")

            elif qui == Vote.loups:
                await chan.send(
                    f"{tools.montre()}  Fin du vote pour la victime du soir !"
                    f"\nVote définitif : {nom_cible}")

            else:  # Action
                for action in joueurs[joueur]:
                    await chan.send(
                        f"{tools.montre()}  Fin de la possiblité d'utiliser "
                        f"ton action {tools.code(action.base.slug)} ! \n"
                        f"Action définitive : {action.decision}")
                    await gestion_actions.close_action(action)

        config.session.commit()

        # Actions déclenchées par fermeture
        if isinstance(qui, Vote):
            for action in Action.query.filter(
                    Action.base.has(
                        BaseAction.trigger_debut == ActionTrigger.close(qui))):
                await gestion_actions.open_action(action)

            for action in Action.query.filter(
                    Action.base.has(
                        BaseAction.trigger_fin == ActionTrigger.close(qui))):
                await gestion_actions.close_action(action)

        # Programme prochaine ouverture
        if isinstance(qui, Vote) and heure:
            ts = tools.next_occurence(tools.heure_to_time(heure))
            if heure_chain:
                Tache(
                    timestamp=ts,
                    commande=f"!open {qui.name} {heure_chain} {heure}").add()
                # Programmera prochaine fermeture
            else:
                Tache(timestamp=ts, commande=f"!open {qui.name}").add()
コード例 #11
0
    async def open(self, ctx, qui, heure=None, heure_chain=None):
        """Lance un vote / des actions de rôle (COMMANDE BOT / MJ)

        Args:
            qui:
                ===========     ===========
                ``cond``        pour le vote du condamné
                ``maire``       pour le vote du maire
                ``loups``       pour le vote des loups
                ``action``      pour les actions commençant à ``heure``
                ``{id}``        pour une action spécifique
                ===========     ===========

            heure:
                - si ``qui == "cond"``, ``"maire"`` ou ``"loup"``,
                  programme en plus la fermeture à ``heure``
                  (et un rappel 30 minutes avant) ;
                - si ``qui == "action"``, il est obligatoire : heure des
                  actions à lancer (cf plus haut). Pour les actions, la
                  fermeture est de toute façon programmée le cas échéant
                  (``trigger_fin`` ``temporel`` ou ``delta``).

                Dans tous les cas, format ``HHh`` ou ``HHhMM``.

            heure_chain:
                permet de chaîner des votes : lance le vote immédiatement
                et programme sa fermeture à ``heure``, en appellant ``!close``
                de sorte à programmer une nouvelle ouverture le lendemain à
                ``heure_chain``, et ainsi de suite.
                Format ``HHh`` ou ``HHhMM``.

        Une sécurité empêche de lancer un vote ou une action déjà en cours.

        Cette commande a pour vocation première d'être exécutée
        automatiquement par des tâches planifiées.
        Elle peut être utilisée à la main, mais attention à ne pas
        faire n'importe quoi (penser à envoyer / planifier la fermeture
        des votes, par exemple).

        Examples:
            - ``!open maire`` :        lance un vote condamné maintenant
            - ``!open cond 19h`` :     lance un vote condamné maintenant
              et programme sa fermeture à 19h00 (ex. Juge Bègue)
            - ``!open cond 18h 10h`` : lance un vote condamné maintenant,
              programme sa fermeture à 18h00, et une prochaine ouverture
              à 10h qui se fermera à 18h, et ainsi de suite
            - ``!open action 19h`` :   lance toutes les actions
              commençant à 19h00
            - ``!open 122`` :          lance l'action d'ID 122

        """
        try:
            qui = Vote[qui.lower()]  # cond / maire / loups
        except KeyError:
            pass
        joueurs = await recup_joueurs("open", qui, heure)
        # Liste de joueurs (votes) ou dictionnaire joueur : action

        str_joueurs = "\n - ".join([joueur.nom for joueur in joueurs])
        await tools.send_code_blocs(
            ctx,
            f"Utilisateur(s) répondant aux critères ({len(joueurs)}) : \n" +
            str_joueurs)

        # Création utilisations & envoi messages
        for joueur in joueurs:
            chan = joueur.private_chan

            if isinstance(qui, Vote):
                action = joueur.action_vote(qui)
                util = Utilisation(action=action)
                util.add()
                util.open()

            if qui == Vote.cond:
                message = await chan.send(
                    f"{tools.montre()}  Le vote pour le condamné du "
                    f"jour est ouvert !  {config.Emoji.bucher} \n" +
                    (f"Tu as jusqu'à {heure} pour voter. \n" if heure else ""
                     ) +
                    tools.ital(f"Tape {tools.code('!vote (nom du joueur)')}"
                               " ou utilise la réaction pour voter."))
                await message.add_reaction(config.Emoji.bucher)

            elif qui == Vote.maire:
                message = await chan.send(
                    f"{tools.montre()}  Le vote pour l'élection du "
                    f"maire est ouvert !  {config.Emoji.maire} \n" +
                    (f"Tu as jusqu'à {heure} pour voter. \n" if heure else ""
                     ) + tools.ital(
                         f"Tape {tools.code('!votemaire (nom du joueur)')} "
                         "ou utilise la réaction pour voter."))
                await message.add_reaction(config.Emoji.maire)

            elif qui == Vote.loups:
                message = await chan.send(
                    f"{tools.montre()}  Le vote pour la victime de "
                    f"cette nuit est ouvert !  {config.Emoji.lune} \n" +
                    (f"Tu as jusqu'à {heure} pour voter. \n" if heure else ""
                     ) + tools.ital(
                         f"Tape {tools.code('!voteloups (nom du joueur)')} "
                         "ou utilise la réaction pour voter."))
                await message.add_reaction(config.Emoji.lune)

            else:  # Action
                for action in joueurs[joueur]:
                    await gestion_actions.open_action(action)

        config.session.commit()

        # Actions déclenchées par ouverture
        if isinstance(qui, Vote):
            for action in Action.query.filter(
                    Action.base.has(
                        BaseAction.trigger_debut == ActionTrigger.open(qui))):
                await gestion_actions.open_action(action)

            for action in Action.query.filter(
                    Action.base.has(
                        BaseAction.trigger_fin == ActionTrigger.open(qui))):
                await gestion_actions.close_action(action)

        # Réinitialise haros/candids
        items = []
        if qui == Vote.cond:
            items = CandidHaro.query.filter_by(type=CandidHaroType.haro).all()
        elif qui == Vote.maire:
            items = CandidHaro.query.filter_by(
                type=CandidHaroType.candidature).all()
        if items:
            CandidHaro.delete(*items)
            await tools.log(f"!open {qui.name} : haros/candids wiped")
            await config.Channel.haros.send(
                f"{config.Emoji.void}\n" * 30 +
                "Nouveau vote, nouveaux haros !\n" + tools.ital(
                    "Les posts ci-dessus sont invalides pour le vote actuel. "
                    f"Utilisez {tools.code('!haro')} pour en relancer."))

        # Programme fermeture
        if isinstance(qui, Vote) and heure:
            ts = tools.next_occurence(tools.heure_to_time(heure))
            Tache(timestamp=ts - datetime.timedelta(minutes=30),
                  commande=f"!remind {qui.name}").add()
            if heure_chain:
                Tache(
                    timestamp=ts,
                    commande=f"!close {qui.name} {heure_chain} {heure}").add()
                # Programmera prochaine ouverture
            else:
                Tache(timestamp=ts, commande=f"!close {qui.name}").add()