"""lg-rez / features / Commandes de gestion des salons
Création, ajout, suppression de membres
"""
import asyncio
import functools
import datetime
import discord
from discord.ext import commands
from lgrez import config
from lgrez.blocs import tools, one_command
from lgrez.bdd import Joueur, Boudoir, Bouderie
from lgrez.features.sync import transtype
[docs]def in_boudoir(callback):
"""Décorateur : commande utilisable dans un boudoir uniquement.
Lors d'une invocation de la commande décorée hors d'un boudoir
(enregistré dans :class:`.bdd.Boudoir`), affiche un message d'erreur.
Ce décorateur n'est utilisable que sur une commande définie dans un Cog.
"""
@functools.wraps(callback)
async def new_callback(cog, ctx, *args, **kwargs):
try:
Boudoir.from_channel(ctx.channel)
except ValueError:
await ctx.reply("Cette commande est invalide en dehors "
"d'un boudoir.")
else:
return await callback(cog, ctx, *args, **kwargs)
return new_callback
[docs]def gerant_only(callback):
"""Décorateur : commande utilisable par le gérant d'un boudoir uniquement.
Lors d'une invocation de la commande décorée par un membre qui n'est
pas gérant du boudoir, affiche un message d'erreur.
Ce décorateur doit toujours être utilisé en combinaison avec
:func:`in_boudoir` et positionné après lui.
Ce décorateur n'est utilisable que sur une commande définie dans un Cog.
"""
@functools.wraps(callback)
async def new_callback(cog, ctx, *args, **kwargs):
boudoir = Boudoir.from_channel(ctx.channel)
gerant = Joueur.from_member(ctx.author)
if boudoir.gerant != gerant:
await ctx.reply("Seul le gérant du boudoir peut utiliser "
"cette commande.")
else:
return await callback(cog, ctx, *args, **kwargs)
return new_callback
[docs]async def add_joueur_to_boudoir(boudoir, joueur, gerant=False):
"""Ajoute un joueur sur un boudoir.
Crée la :class:`.Bouderie` correspondante et modifie les
permissions du salon.
Args:
boudoir (.Boudoir): Le boudoir où ajouter un joueur.
joueur (.Joueur): Le joueur à ajouter.
gerant (bool): Si le joueur doit être ajouté avec les
permissions de gérant.
Returns:
:class:`bool` - ``True`` si le joueur a été ajouté,
``False`` si il y était déjà / le boudoir est fermé.
"""
if joueur in boudoir.joueurs:
# Joueur déjà dans le boudoir
return False
if not boudoir.joueurs and not gerant:
# Boudoir fermé (plus de joueurs) et pas ajout comme gérant
return False
now = datetime.datetime.now()
Bouderie(boudoir=boudoir, joueur=joueur, gerant=gerant,
ts_added=now, ts_promu=now if gerant else None).add()
await boudoir.chan.set_permissions(joueur.member, read_messages=True)
# Sortie du cimetière le cas échéant
if tools.in_multicateg(boudoir.chan.category,
config.old_boudoirs_category_name):
await boudoir.chan.send(tools.ital(
"[Ce boudoir contient au moins deux joueurs vivants, "
"désarchivage...]"
))
categ = await tools.multicateg(config.boudoirs_category_name)
await boudoir.chan.edit(name=boudoir.nom, category=categ)
return True
[docs]async def remove_joueur_from_boudoir(boudoir, joueur):
"""Retire un joueur d'un boudoir.
Supprime la :class:`.Bouderie` correspondante et modifie les
permissions du salon.
Args:
boudoir (.Boudoir): Le boudoir d'où enlever un joueur.
joueur (.Joueur): Le joueur à enlever.
"""
Bouderie.query.filter_by(boudoir=boudoir, joueur=joueur).one().delete()
await boudoir.chan.set_permissions(joueur.member, overwrite=None)
# Déplacement dans le cimetière si nécessaire
vivants = [jr for jr in boudoir.joueurs if jr.est_vivant]
if len(vivants) < 2:
if tools.in_multicateg(boudoir.chan.category,
config.old_boudoirs_category_name):
# Boudoir déjà au cimetière
return
await boudoir.chan.send(tools.ital(
"[Ce boudoir contient moins de deux joueurs vivants, "
"archivage...]"
))
categ = await tools.multicateg(config.old_boudoirs_category_name)
await boudoir.chan.edit(
name=f"\N{CROSS MARK} {boudoir.nom}",
category=categ,
)
async def _create_boudoir(joueur, nom):
"""Crée un boudoir avec le gérant et le nom requis"""
now = datetime.datetime.now()
categ = await tools.multicateg(config.boudoirs_category_name)
chan = await config.guild.create_text_channel(
nom,
topic=f"Boudoir crée le {now:%d/%m à %H:%M}. "
f"Gérant(e) : {joueur.nom}",
category=categ,
)
boudoir = Boudoir(chan_id=chan.id, nom=nom, ts_created=now)
boudoir.add()
await add_joueur_to_boudoir(boudoir, joueur, gerant=True)
await tools.log(f"Boudoir {chan.mention} créé par {joueur.nom}.")
return boudoir
async def _invite(joueur, boudoir, invite_msg):
"""Invitation d'un joueur dans un boudoir (lancer comme tâche à part)"""
pc = joueur.private_chan
bc = boudoir.chan
mess = await pc.send(
f"{joueur.member.mention} {boudoir.gerant.nom} t'as invité(e) à "
f"rejoindre son boudoir : « {boudoir.nom} » !\nAcceptes-tu ?"
)
if await tools.yes_no(mess):
ok = await add_joueur_to_boudoir(boudoir, joueur)
if ok:
info = f"{joueur.nom} a rejoint le boudoir !"
confirm = f"Tu as bien rejoint {bc.mention} !"
else:
info = None
confirm = f"Impossible de rejoindre le boudoir."
else:
info = f"{joueur.nom} a refusé l'invitation à rejoindre ce boudoir."
confirm = "Invitation refusée."
if info:
try:
await invite_msg.reply(info)
except discord.HTTPException: # Message d'inviation supprimé
await bc.send(info)
await mess.reply(confirm)
[docs]class GestionChans(commands.Cog):
"""Gestion des salons"""
@commands.group(aliases=["boudoirs"])
async def boudoir(self, ctx):
"""Gestion des boudoirs
Les options relatives à un boudoir précis ne peuvent être
exécutées que dans ce boudoir ; certaines sont réservées au
gérant dudit boudoir.
"""
if not ctx.invoked_subcommand:
# Pas de sous-commande (correspondante)
if ctx.subcommand_passed:
# Tentative de subcommand
raise commands.BadArgument(
f"Option '{ctx.subcommand_passed}' inconnue"
)
ctx.message.content = f"!help {ctx.invoked_with}"
with one_command.bypass(ctx):
await config.bot.process_commands(ctx.message)
@boudoir.command(aliases=["liste"])
@tools.joueurs_only
@tools.private
async def list(self, ctx):
"""Liste les boudoirs dans lesquels tu es"""
joueur = Joueur.from_member(ctx.author)
bouderies = joueur.bouderies
if not bouderies:
await ctx.reply(
"Tu n'es dans aucun boudoir pour le moment.\n"
f"{tools.code('!boudoir create')} pour en créer un."
)
return
rep = "Tu es dans les boudoirs suivants :"
for bouderie in bouderies:
rep += f"\n - {bouderie.boudoir.chan.mention}"
if bouderie.gerant:
rep += " (gérant)"
rep += "\n\nUtilise `!boudoir leave` dans un boudoir pour le quitter."
await ctx.send(rep)
@boudoir.command(aliases=["new", "creer", "créer"])
@tools.vivants_only
@tools.private
async def create(self, ctx, *, nom=None):
"""Crée un nouveau boudoir dont tu es gérant"""
member = ctx.author
joueur = Joueur.from_member(member)
if not nom:
await ctx.send("Comment veux-tu nommer ton boudoir ?\n"
+ tools.ital("(`stop` pour annuler)"))
mess = await tools.wait_for_message_here(ctx)
nom = mess.content
if len(nom) > 32:
await ctx.send("Le nom des boudoirs est limité à 32 caractères.")
return
await ctx.send("Création du boudoir...")
async with ctx.typing():
boudoir = await _create_boudoir(joueur, nom)
await boudoir.chan.send(
f"{member.mention}, voici ton boudoir ! "
"Tu peux maintenant y inviter des gens avec la commande "
"`!boudoir invite`."
)
await ctx.send(f"Ton boudoir a bien été créé : {boudoir.chan.mention}")
@boudoir.command(aliases=["add"])
@tools.joueurs_only
@in_boudoir
@gerant_only
async def invite(self, ctx, *, cible=None):
"""Invite un joueur à rejoindre ce boudoir"""
boudoir = Boudoir.from_channel(ctx.channel)
joueur = await tools.boucle_query_joueur(
ctx, cible=cible, message="Qui souhaites-tu inviter ?"
)
if joueur in boudoir.joueurs:
await ctx.send(f"{joueur.nom} est déjà dans ce boudoir !")
return
mess = await ctx.send(f"Invitation envoyée à {joueur.nom}.")
asyncio.create_task(_invite(joueur, boudoir, mess))
# On envoie l'invitation en arrière-plan (libération du chan).
@boudoir.command(aliases=["remove", "kick"])
@tools.joueurs_only
@in_boudoir
@gerant_only
async def expulse(self, ctx, *, cible=None):
"""Expulse un membre de ce boudoir"""
boudoir = Boudoir.from_channel(ctx.channel)
joueur = await tools.boucle_query_joueur(
ctx, cible=cible, message="Qui souhaites-tu expulser ?"
)
if joueur not in boudoir.joueurs:
await ctx.send(f"{joueur.nom} n'est pas membre du boudoir !")
return
await remove_joueur_from_boudoir(boudoir, joueur)
await joueur.private_chan.send(f"Tu as été expulsé(e) du boudoir "
f"« {boudoir.nom} ».")
await ctx.send(f"{joueur.nom} a bien été expulsé de ce boudoir.")
@boudoir.command(aliases=["quit"])
@tools.joueurs_only
@in_boudoir
async def leave(self, ctx):
"""Quitte ce boudoir"""
joueur = Joueur.from_member(ctx.author)
boudoir = Boudoir.from_channel(ctx.channel)
if boudoir.gerant == joueur:
await ctx.send(
"Tu ne peux pas quitter un boudoir que tu gères. "
"Utilise `!boudoir transfer` pour passer les droits "
"de gestion ou `!boudoir delete` pour le supprimer."
)
return
mess = await ctx.reply(
"Veux-tu vraiment quitter ce boudoir ? Tu ne "
"pourras pas y retourner sans invitation."
)
if not await tools.yes_no(mess):
await ctx.send("Mission aborted.")
return
await remove_joueur_from_boudoir(boudoir, joueur)
await ctx.send(tools.ital(f"{joueur.nom} a quitté ce boudoir."))
@boudoir.command(aliases=["transmit"])
@tools.joueurs_only
@in_boudoir
@gerant_only
async def transfer(self, ctx, cible=None):
"""Transfère les droits de gestion de ce boudoir"""
boudoir = Boudoir.from_channel(ctx.channel)
gerant = Joueur.from_member(ctx.author)
joueur = await tools.boucle_query_joueur(
ctx, cible=cible, message=("À qui souhaites-tu confier "
"la gestion de ce boudoir ?")
)
if joueur not in boudoir.joueurs:
await ctx.send(f"{joueur.nom} n'est pas membre de ce boudoir !")
return
mess = await ctx.reply(
"Veux-tu vraiment transférer les droits de ce boudoir ? "
"Tu ne pourras pas les récupérer par toi-même."
)
if not await tools.yes_no(mess):
await ctx.send("Mission aborted.")
return
boudoir.gerant = joueur
boudoir.update()
await boudoir.chan.edit(
topic=f"Boudoir crée le {boudoir.ts_created:%d/%m à %H:%M}. "
f"Gérant(e) : {joueur.nom}"
)
await ctx.send(f"Boudoir transféré à {joueur.nom}.")
@boudoir.command()
@tools.joueurs_only
@in_boudoir
@gerant_only
async def delete(self, ctx):
"""Supprime ce boudoir"""
boudoir = Boudoir.from_channel(ctx.channel)
mess = await ctx.reply("Veux-tu vraiment supprimer ce boudoir ? "
"Cette action est irréversible.")
if not await tools.yes_no(mess):
await ctx.send("Mission aborted.")
return
await ctx.send("Suppression...")
for joueur in boudoir.joueurs:
await remove_joueur_from_boudoir(boudoir, joueur)
await joueur.private_chan.send(
f"Le boudoir « {boudoir.nom } » a été supprimé."
)
await boudoir.chan.edit(name=f"\N{CROSS MARK} {boudoir.nom}")
await ctx.send(tools.ital(
"[Tous les joueurs ont été exclus de ce boudoir ; "
"le channel reste présent pour archive.]"
))
@boudoir.command()
@tools.joueurs_only
@in_boudoir
@gerant_only
async def rename(self, ctx, *, nom=None):
"""Renomme ce boudoir"""
boudoir = Boudoir.from_channel(ctx.channel)
if not nom:
await ctx.send("Comment veux-tu renommer ce boudoir ?\n"
+ tools.ital("(`stop` pour annuler)"))
mess = await tools.wait_for_message_here(ctx)
nom = mess.content
if len(nom) > 32:
await ctx.send("Le nom des boudoirs est limité à 32 caractères.")
return
boudoir.nom = nom
boudoir.update()
await boudoir.chan.edit(name=nom)
await ctx.send("Boudoir renommé avec succès.")
@boudoir.command(aliases=["hého"])
@tools.joueurs_only
@in_boudoir
@gerant_only
async def ping(self, ctx, *, mess=""):
"""Mentionne tous les joueurs vivants dans le boudoir."""
await ctx.channel.send(f"{config.Role.joueur_en_vie.mention} {mess}")
@boudoir.command(aliases=["locate", "whereis"])
@tools.mjs_only
async def find(self, ctx, *cibles):
"""✨ Trouve le(s) boudoir(s) réunissant certains joueurs (COMMANDE MJ)
Args:
*cibles: noms ou mentions des joueurs qui nous intéressent.
"""
joueurs = [await tools.boucle_query_joueur(ctx, cible=cible)
for cible in cibles]
boudoirs = [boudoir for boudoir in Boudoir.query.all()
if all(joueur in boudoir.joueurs for joueur in joueurs)]
if not boudoirs:
await ctx.reply("Pas de boudoir(s) réunissant ces joueurs.")
else:
liste = "\n".join(
f"- {boudoir.chan.mention} ({len(boudoir.joueurs)} joueurs)"
for boudoir in boudoirs
)
await tools.send_blocs(ctx, f"{len(boudoirs)} boudoirs :\n{liste}")
@boudoir.command()
async def help(self, ctx):
"""✨ Informations sur les sous-commandes disponibles
Alias pour ``!help boudoir``.
"""
ctx.message.content = "!help boudoir"
with one_command.bypass(ctx):
await config.bot.process_commands(ctx.message)
@commands.command()
@tools.vivants_only
@tools.private
async def mp(self, ctx, *, cible=None):
"""✨ Raccourci pour créer un boudoir et y ajouter un joueur.
Si un boudoir existe déjà avec uniquement ce joueur et toi,
n'en crée pas un nouveau.
Args:
cible: la personne avec qui créer un boudoir.
"""
member = ctx.author
joueur = Joueur.from_member(member)
autre = await tools.boucle_query_joueur(
ctx, cible=cible, message="À qui souhaites-tu parler ?"
)
if joueur == autre:
await ctx.send(
f"Ton boudoir a bien été créé : {ctx.channel.mention}"
)
await ctx.send("(tocard)")
return
# Recherche si boudoir existant
boudoir = next((boudoir for boudoir in joueur.boudoirs
if set(boudoir.joueurs) == {joueur, autre}), None)
if boudoir:
await ctx.reply(f"Ce boudoir existe déjà : {boudoir.chan.mention}")
return
await ctx.reply("Création du boudoir...")
async with ctx.typing():
nom = f"{joueur.nom} × {autre.nom}"
boudoir = await _create_boudoir(joueur, nom)
await boudoir.chan.send(f"{member.mention}, voici ton boudoir !")
await ctx.send(f"Ton boudoir a bien été créé : {boudoir.chan.mention}")
mess = await boudoir.chan.send(f"Invitation envoyée à {autre.nom}.")
asyncio.create_task(_invite(autre, boudoir, mess))
# On envoie l'invitation en arrière-plan (libération du chan).
@commands.command()
@tools.mjs_only
async def addhere(self, ctx, *joueurs):
"""Ajoute les membres au chan courant (COMMANDE MJ)
Args:
*joueurs: membres à ajouter, chacun entouré par des
guillemets si nom + prénom
Si ``*joueurs`` est un seul élément, il peut être de la forme
``<crit>=<filtre>`` tel que décrit dans l'aide de ``!send``.
"""
ts_debut = ctx.message.created_at - datetime.timedelta(microseconds=1)
if len(joueurs) == 1 and "=" in joueurs[0]:
# Si critère : on remplace joueurs
crit, _, filtre = joueurs[0].partition("=")
crit = crit.strip()
if crit in Joueur.attrs:
col = Joueur.attrs[crit]
arg = transtype(filtre.strip(), col)
joueurs = Joueur.query.filter_by(**{crit: arg}).all()
else:
raise commands.UserInputError(f"critère '{crit}' incorrect")
else:
# Sinon, si noms / mentions
joueurs = [await tools.boucle_query_joueur(ctx, cible)
for cible in joueurs]
for joueur in joueurs:
await ctx.channel.set_permissions(joueur.member,
read_messages=True)
await ctx.send(f"{joueur.nom} ajouté")
mess = await ctx.send("Fini, purge les messages ?")
if await tools.yes_no(mess):
await ctx.channel.purge(after=ts_debut)
@commands.command()
@tools.mjs_only
async def purge(self, ctx, N=None):
"""Supprime tous les messages de ce chan (COMMANDE MJ)
Args:
N: nombre de messages à supprimer (défaut : tous)
"""
if N:
mess = await ctx.send(
f"Supprimer les {N} messages les plus récents de ce chan ? "
"(sans compter le `!purge` et ce message)"
)
else:
mess = await ctx.send("Supprimer tous les messages de ce chan ?")
if await tools.yes_no(mess):
await ctx.channel.purge(limit=int(N) + 2 if N else None)