"""lg-rez / features / Commandes spéciales
Commandes spéciales (méta-commandes, imitant ou impactant le
déroulement des autres ou le fonctionnement du bot)
"""
import asyncio
import os
import re
import sys
# Unused imports because useful for !do / !shell globals
import discord
from discord.ext import commands
from lgrez import __version__, config, features, blocs, bdd
from lgrez.blocs import gsheets, tools, realshell, one_command
from lgrez.bdd import * # toutes les tables dans globals()
async def _filter_runnables(commands, ctx):
"""Retourne les commandes pouvant run parmis commands"""
runnables = []
with one_command.bypass(ctx):
# On désactive la limitation de une commande simultanée
# sinon can_run renvoie toujours False
for cmd in commands:
try:
runnable = await cmd.can_run(ctx)
except Exception:
runnable = False
if runnable:
runnables.append(cmd)
return runnables
[docs]class Special(commands.Cog):
"""Commandes spéciales (méta-commandes et expérimentations)"""
@one_command.do_not_limit
@commands.command(aliases=["kill"])
@tools.mjs_only
async def panik(self, ctx):
"""Tue instantanément le bot, sans confirmation (COMMANDE MJ)
PAAAAANIK
"""
sys.exit()
@commands.command()
@tools.mjs_only
async def do(self, ctx, *, code):
"""Exécute du code Python et affiche le résultat (COMMANDE MJ)
Args:
code: instructions valides dans le contexte du LGbot
(utilisables notemment : ``ctx``, ``config``, ``blocs``,
``features``, ``bdd``, ``<table>``...)
Si ``code`` est une coroutine, elle sera awaited
(ne pas inclure ``await`` dans ``code``).
Aussi connue sous le nom de « faille de sécurité », cette
commande permet de faire environ tout ce qu'on veut sur le bot
(y compris le crasher, importer des modules, exécuter des
fichiers .py... même si c'est un peu compliqué) voire d'impacter
le serveur sur lequel le bot tourne si on est motivé.
À utiliser avec parcimonie donc, et QUE pour du
développement/debug !
"""
class Answer:
rep = None
_a = Answer()
locs = globals()
locs["ctx"] = ctx
locs["_a"] = _a
exec(f"_a.rep = {code}", locs)
if asyncio.iscoroutine(_a.rep):
_a.rep = await _a.rep
await tools.send_code_blocs(ctx, str(_a.rep))
@commands.command()
@tools.mjs_only
async def shell(self, ctx):
"""Lance un terminal Python directement dans Discord (COMMANDE MJ)
Envoyer ``help`` dans le pseudo-terminal pour plus
d'informations sur son fonctionnement.
Évidemment, les avertissements dans ``!do`` s'appliquent ici :
ne pas faire n'imp avec cette commande !! (même si ça peut être
très utile, genre pour ajouter des gens en masse à un channel)
"""
locs = globals()
locs["ctx"] = ctx
shell = realshell.RealShell(ctx.channel, locs)
try:
await shell.interact()
except realshell.RealShellExit as exc:
raise tools.CommandExit(*exc.args or ["!shell: Forced to end."])
@commands.command()
@tools.mjs_only
async def co(self, ctx, cible=None):
"""Lance la procédure d'inscription pour un membre (COMMANDE MJ)
Fat comme si on se connectait au serveur pour la première fois.
Args:
cible: le nom exact ou la mention (``@joueur``) du joueur
à inscrire, par défaut le lançeur de la commande.
Cette commande est principalement destinée aux tests de
développement, mais peut être utile si un joueur chibre son
inscription (à utiliser dans son channel, ou ``#bienvenue``
(avec ``!autodestruct``) si même le début a chibré).
"""
if cible:
try:
member = tools.member(cible)
except ValueError:
await ctx.send("Cible introuvable.")
return
else:
member = ctx.author
await features.inscription.main(member)
@commands.command()
@tools.mjs_only
async def doas(self, ctx, *, qui_quoi):
"""Exécute une commande en tant qu'un autre joueur (COMMANDE MJ)
Args:
qui_quoi: nom de la cible (nom/mention d'un joueur INSCRIT)
suivi de la commande à exécuter (commençant par un ``!``).
Example:
``!doas Vincent Croquette !vote Annie Colin``
"""
sep = " " + config.bot.command_prefix
qui, _, quoi = qui_quoi.partition(sep)
# !doas <@!id> !vote R ==> qui = "<@!id>", quoi = "vote R"
if not quoi:
raise commands.UserInputError(f"'{sep}' not found in qui_quoi")
joueur = await tools.boucle_query_joueur(ctx, qui.strip())
ctx.message.content = config.bot.command_prefix + quoi
try:
member = joueur.member
except ValueError:
await ctx.send(f"{joueur} absent du serveur, "
"tentative de contournement")
class PseudoMember:
__class__ = discord.Member
id = joueur.discord_id
display_name = joueur.nom
guild = config.guild
mention = f"[@{joueur.nom}]"
top_role = (config.Role.joueur_en_vie if joueur.est_vivant
else config.Role.joueur_mort)
roles = [config.Role.everyone, top_role]
member = PseudoMember()
ctx.message.author = member
await ctx.send(f":robot: Exécution en tant que {joueur.nom} :")
with one_command.bypass(ctx):
await config.bot.process_commands(ctx.message)
@commands.command(aliases=["autodestruct", "ad"])
@tools.mjs_only
async def secret(self, ctx, *, quoi):
"""Supprime le message puis exécute la commande (COMMANDE MJ)
Args:
quoi: commande à exécuter, commençant par un ``!``
Utile notemment pour faire des commandes dans un channel public,
pour que la commande (moche) soit immédiatement supprimée.
"""
await ctx.message.delete()
ctx.message.content = quoi
with one_command.bypass(ctx):
await config.bot.process_commands(ctx.message)
@one_command.do_not_limit
@commands.command()
@tools.private
async def stop(self, ctx):
"""Peut débloquer des situations compliquées (beta)
Ne pas utiliser cette commande sauf en cas de force majeure
où plus rien ne marche, et sur demande d'un MJ (après c'est
pas dit que ça marche mieux après l'avoir utilisée)
"""
if ctx.channel.id in config.bot.in_command:
config.bot.in_command.remove(ctx.channel.id)
await ctx.send("Te voilà libre, camarade !")
@commands.command(aliases=["aide", "aled", "oskour"])
async def help(self, ctx, *, command=None):
"""Affiche la liste des commandes utilisables et leur utilisation
Args:
command (optionnel): nom exact d'une commande à expliquer
(ou un de ses alias)
Si ``command`` n'est pas précisée, liste l'ensemble des commandes
accessibles à l'utilisateur.
"""
pref = config.bot.command_prefix
cogs = config.bot.cogs # Dictionnaire nom: cog
commandes = {cmd.name: cmd for cmd in config.bot.commands}
aliases = {alias: nom for nom, cmd in commandes.items()
for alias in cmd.aliases}
# Dictionnaire alias: nom de la commande
len_max = max(len(cmd) for cmd in commandes)
def descr_command(cmd):
return f"\n - {pref}{cmd.name.ljust(len_max)} {cmd.short_doc}"
if not command:
# Pas d'argument ==> liste toutes les commandes
r = f"{config.bot.description} (v{__version__})"
for cog in cogs.values():
runnables = await _filter_runnables(cog.get_commands(), ctx)
if not runnables:
# pas de runnables dans le cog, on passe
continue
r += f"\n\n{type(cog).__name__} - {cog.description} :"
for cmd in runnables: # pour chaque commande runnable
r += descr_command(cmd)
runnables_hors_cog = await _filter_runnables(
(cmd for cmd in config.bot.commands if not cmd.cog), ctx
)
if runnables_hors_cog:
r += "\n\nCommandes isolées :"
for cmd in runnables_hors_cog:
r += descr_command(cmd)
r += (f"\n\nUtilise <{pref}help command> pour "
"plus d'information sur une commande.")
else:
# Aide détaillée sur une commande
if command.startswith(pref):
# Si le joueur fait !help !command (ou !help ! command)
command = command.lstrip(pref).strip()
if command in aliases:
# Si !help d'un alias
command = aliases[command]
if command in commandes: # Si commande existante
cmd = commandes[command]
doc = cmd.help or ""
doc = doc.replace("``", "`")
doc = doc.replace("Args:", "Arguments :")
doc = doc.replace("Warning:", "Avertissement :")
doc = doc.replace("Examples:", "Exemples :")
doc = re.sub(r":\w+?:`[\.~!]*(.+?)`", r"`\1`", doc)
# enlève les :class: et consors
if isinstance(cmd, commands.Group):
r = (f"{pref}{command} <option> [args...] – {doc}\n\n"
"Options :\n")
scommands = sorted(cmd.commands, key=lambda cmd: cmd.name)
options = [f"{scmd.name} {scmd.signature}"
for scmd in scommands]
slen_max = max(len(opt) for opt in options)
r += "\n".join(f" - {pref}{command} "
f"{opt.ljust(slen_max)} {scmd.short_doc}"
for scmd, opt in zip(scommands, options))
else:
r = f"{pref}{command} {cmd.signature} – {doc}"
if cmd.aliases: # Si la commande a des alias
r += f"\n\nAlias : {pref}" + f", {pref}".join(cmd.aliases)
else:
r = (f"Commande '{pref}{command}' non trouvée.\n"
f"Utilise '{pref}help' pour la liste des commandes.")
r += ("\n\nSi besoin, n'hésite pas à appeler un MJ "
"en les mentionnant (@MJ).")
await tools.send_code_blocs(ctx, r, sep="\n\n")
# On envoie, en séparant enntre les cogs de préférence
@commands.command(aliases=["about", "copyright", "licence", "auteurs"])
async def apropos(self, ctx):
"""Informations et mentions légales du projet
N'hésitez-pas à nous contacter pour en savoir plus !
"""
embed = discord.Embed(
title=f"**LG-bot** - v{__version__}",
description=config.bot.description
).set_author(
name="À propos de ce bot :",
icon_url=config.bot.user.avatar_url,
).set_image(
url=("https://gist.githubusercontent.com/loic-simon/"
"66c726053323017dba67f85d942495ef/raw/"
"48f2607a61f3fc1b7285fd64873621035c6fbbdb/logo_espci.png"),
).add_field(
name="Auteurs",
value="Loïc Simon\nTom Lacoma",
inline=True,
).add_field(
name="Licence",
value="Projet open-source sous licence MIT\n"
"https://opensource.org/licenses/MIT",
inline=True,
).add_field(
name="Pour en savoir plus :",
value="https://github.com/loic-simon/lg-rez",
inline=False,
).add_field(
name="Copyright :",
value=":copyright: 2022 Club BD-Jeux × GRIs – ESPCI Paris - PSL",
inline=False,
).set_footer(
text="Retrouvez-nous sur Discord : LaCarpe#1674, TaupeOrAfk#3218",
)
await ctx.send(embed=embed)
@commands.command()
@commands.check(lambda ctx: not config.is_setup)
async def setup(self, ctx):
"""✨ Prépare un serveur nouvellement crée (COMMANDE MJ)
À n'utiliser que dans un nouveau serveur, pour créer les rôles,
catégories, salons et emojis nécessaires.
"""
msg = await ctx.reply("Setup le serveur ?")
if not await tools.yes_no(msg):
await ctx.send("Mission aborted.")
return
structure = config.server_structure
# Création rôles
await ctx.send("Création des rôles...")
roles = {}
for slug, role in structure["roles"].items():
roles[slug] = tools.role(role["name"], must_be_found=False)
if roles[slug]:
continue
if isinstance(role["permissions"], list):
perms = discord.Permissions(
**{perm: True for perm in role["permissions"]}
)
else:
perms = getattr(discord.Permissions, role["permissions"])()
roles[slug] = await config.guild.create_role(
name=role["name"],
color=int(role["color"], base=16),
hoist=role["hoist"],
mentionable=role["mentionable"],
permissions=perms,
)
# Modification @everyone
roles["@everyone"] = tools.role("@everyone")
await roles["@everyone"].edit(permissions=discord.Permissions(
**{perm: True for perm in structure["everyone_permissions"]}
))
await ctx.send(f"{len(roles)} rôles créés.")
# Assignation rôles
for member in config.guild.members:
await member.add_roles(
roles["bot"] if member == config.bot.user else roles["mj"]
)
# Création catégories et channels
await ctx.send("Création des salons...")
categs = {}
channels = {}
for slug, categ in structure["categories"].items():
categs[slug] = tools.channel(categ["name"], must_be_found=False)
if not categs[slug]:
categs[slug] = await config.guild.create_category(
name=categ["name"],
overwrites={
roles[role]: discord.PermissionOverwrite(**perms)
for role, perms in categ["overwrites"].items()
}
)
for position, (chan_slug, channel) in enumerate(
categ["channels"].items()
):
channels[chan_slug] = tools.channel(
channel["name"],
must_be_found=False
)
if channels[chan_slug]:
continue
channels[chan_slug] = await categs[slug].create_text_channel(
name=channel["name"],
topic=channel["topic"],
position=position,
overwrites={
roles[role]: discord.PermissionOverwrite(**perms)
for role, perms in channel["overwrites"].items()
}
)
for position, (chan_slug, channel) in enumerate(
categ["voice_channels"].items()
):
channels[chan_slug] = tools.channel(
channel["name"],
must_be_found=False
)
if channels[chan_slug]:
continue
channels[chan_slug] = await categs[slug].create_voice_channel(
name=channel["name"],
position=position,
overwrites={
roles[role]: discord.PermissionOverwrite(**perms)
for role, perms in channel["overwrites"]
}
)
await ctx.send(
f"{len(channels)} salons créés dans {len(categs)} catégories."
)
# Création emojis
await ctx.send("Import des emojis... (oui c'est très long)")
async def _create_emoji(name: str, data: bytes):
can_use = None
if restrict := structure["emojis"]["restrict_roles"].get(name):
can_use = [roles[role] for role in restrict]
await config.guild.create_custom_emoji(
name=name,
image=data,
roles=can_use,
)
n_emojis = 0
if structure["emojis"]["drive"]:
folder_id = structure["emojis"]["folder_path_or_id"]
for file in gsheets.get_files_in_folder(folder_id):
if file["extension"] != "png":
continue
name = file["name"].removesuffix(".png")
if tools.emoji(name, must_be_found=False):
continue
data = gsheets.download_file(file["file_id"])
await _create_emoji(name, data)
n_emojis += 1
else:
root = structure["emojis"]["folder_path_or_id"]
for file in os.scandir(root):
name, extension = os.path.splitext(file.name)
if extension != ".png":
continue
if tools.emoji(name, must_be_found=False):
continue
with open(file.path, "rb") as fh:
data = fh.read()
await _create_emoji(name, data)
n_emojis += 1
await ctx.send(f"{n_emojis} emojis importés.")
# Paramètres généraux du serveur
await ctx.send("Configuration du serveur...")
if not structure["icon"]:
icon_data = None
elif structure["icon"]["drive"]:
file_id = structure["icon"]["png_path_or_id"]
icon_data = gsheets.download_file(file_id)
else:
with open(structure["icon"]["png_path_or_id"], "rb") as fh:
icon_data = fh.read()
await config.guild.edit(
name=structure["name"],
icon=icon_data,
afk_channel=channels.get(structure["afk_channel"]),
afk_timeout=int(structure["afk_timeout"]),
verification_level=discord.VerificationLevel[
structure["verification_level"]
],
default_notifications=discord.NotificationLevel[
structure["default_notifications"]
],
explicit_content_filter=discord.ContentFilter[
structure["explicit_content_filter"]
],
system_channel=channels[structure["system_channel"]],
system_channel_flags=discord.SystemChannelFlags(
**structure["system_channel_flags"]
),
preferred_locale=structure["preferred_locale"],
reason="Guild set up!"
)
await ctx.send(f"Fin de la configuration !")
config.is_setup = True
# Delete current chan (will also trigger on_ready)
msg = await ctx.send(
"Terminé ! Ce salon va être détruit (ce n'est pas une question) ; "
"il restera les deux catégories et le salon vocal à clean."
)
await tools.yes_no(msg)
await ctx.channel.delete()