tfjm-discord-bot/src/tfjm_discord_bot.py

265 lines
8.1 KiB
Python

#!/bin/python
import asyncio
import code
import random
import sys
import traceback
from collections import defaultdict, namedtuple
from pprint import pprint
from typing import Dict, Type
import discord
import yaml
from discord.ext import commands
from discord.ext.commands import Context
from discord.utils import get
from src.cogs.tirages import Tirage, TiragePhase
from src.constants import *
from src.errors import TfjmError, UnwantedCommand
bot = commands.Bot(
"!", help_command=commands.DefaultHelpCommand(no_category="Commandes")
)
# global variable to hold every running tirage
tirages: Dict[int, Tirage] = {}
@bot.command(
name="start-draw",
help="Commence un tirage avec 3 ou 4 équipes.",
usage="équipe1 équipe2 équipe3 (équipe4)",
)
@commands.has_role(ORGA_ROLE)
async def start_draw(ctx: Context, *teams):
channel: discord.TextChannel = ctx.channel
channel_id = channel.id
if channel_id in tirages:
raise TfjmError("Il y a déjà un tirage en cours sur cette Channel.")
if len(teams) not in (3, 4):
raise TfjmError("Il faut 3 ou 4 équipes pour un tirage.")
roles = {role.name for role in ctx.guild.roles}
for team in teams:
if team not in roles:
raise TfjmError("Le nom de l'équipe doit être exactement celui du rôle.")
# Here all data should be valid
# Prevent everyone from writing except Capitaines, Orga, CNO, Benevole
read = discord.PermissionOverwrite(send_messages=False)
send = discord.PermissionOverwrite(send_messages=True)
r = lambda role_name: get(ctx.guild.roles, name=role_name)
overwrites = {
ctx.guild.default_role: read,
r(CAPTAIN_ROLE): send,
r(BENEVOLE_ROLE): send,
}
await channel.edit(overwrites=overwrites)
await ctx.send(
"Nous allons commencer le tirage du premier tour. "
"Seuls les capitaines de chaque équipe peuvent désormais écrire ici. "
"Merci de d'envoyer seulement ce que est nécessaire et suffisant au "
"bon déroulement du tournoi. Vous pouvez à tout moment poser toute question "
"si quelque chose n'est pas clair ou ne va pas. \n\n"
"Pour plus de détails sur le déroulement du tirgae au sort, le règlement "
"est accessible sur https://tfjm.org/reglement."
)
tirages[channel_id] = Tirage(ctx, channel_id, teams)
await tirages[channel_id].phase.start(ctx)
@bot.command(
name="abort-draw", help="Annule le tirage en cours.",
)
@commands.has_role(ORGA_ROLE)
async def abort_draw_cmd(ctx):
channel_id = ctx.channel.id
if channel_id in tirages:
await tirages[channel_id].end(ctx)
await ctx.send("Le tirage est annulé.")
@bot.command(name="draw-skip", aliases=["skip"])
@commands.has_role(CNO_ROLE)
async def draw_skip(ctx, *teams):
channel = ctx.channel.id
tirages[channel] = tirage = Tirage(ctx, channel, teams)
tirage.phase = TiragePhase(tirage, round=1)
for i, team in enumerate(tirage.teams):
team.tirage_order = [i + 1, i + 1]
team.passage_order = [i + 1, i + 1]
team.accepted_problems = [PROBLEMS[i], PROBLEMS[-i - 1]]
tirage.teams[0].rejected = [{PROBLEMS[3]}, set(PROBLEMS[4:8])]
tirage.teams[1].rejected = [{PROBLEMS[7]}, set()]
await ctx.send(f"Skipping to {tirage.phase.__class__.__name__}.")
await tirage.phase.start(ctx)
await tirage.update_phase(ctx)
@bot.event
async def on_ready():
print(f"{bot.user} has connected to Discord!")
@bot.command(
name="dice",
help="Lance un dé à `n` faces. ",
aliases=["de", "", "roll"],
usage="n",
)
async def dice(ctx: Context, n: int):
channel = ctx.channel.id
if channel in tirages:
await tirages[channel].dice(ctx, n)
else:
if n < 1:
raise TfjmError(f"Je ne peux pas lancer un dé à {n} faces, désolé.")
dice = random.randint(1, n)
await ctx.send(f"Le dé à {n} face{'s'*(n>1)} s'est arrêté sur... **{dice}**")
@bot.command(
name="choose",
help="Choisit une option parmi tous les arguments.",
usage="choix1 choix2...",
aliases=["choice", "choix", "ch"],
)
async def choose(ctx: Context, *args):
choice = random.choice(args)
await ctx.send(f"J'ai choisi... **{choice}**")
@bot.command(
name="random-problem",
help="Choisit un problème parmi ceux de cette année.",
aliases=["rp", "problème-aléatoire", "probleme-aleatoire", "pa"],
)
async def random_problem(ctx: Context):
channel = ctx.channel.id
if channel in tirages:
await tirages[channel].choose_problem(ctx)
else:
problem = random.choice(PROBLEMS)
await ctx.send(f"Le problème tiré est... **{problem}**")
@bot.command(
name="accept",
help="Accepte le problème qui vient d'être tiré. \n Ne fonctionne que lors d'un tirage.",
aliases=["oui", "yes", "o", "accepte", "ouiiiiiii"],
)
async def accept_cmd(ctx):
channel = ctx.channel.id
if channel in tirages:
await tirages[channel].accept(ctx, True)
else:
await ctx.send(f"{ctx.author.mention} approuve avec vigeur !")
@bot.command(
name="refuse",
help="Refuse le problème qui vient d'être tiré. \n Ne fonctionne que lors d'un tirage.",
aliases=["non", "no", "n", "nope", "jaaamais"],
)
async def refuse_cmd(ctx):
channel = ctx.channel.id
if channel in tirages:
await tirages[channel].accept(ctx, False)
else:
await ctx.send(f"{ctx.author.mention} nie tout en block !")
@bot.command(name="show")
async def show_cmd(ctx: Context, arg: str):
if not TIRAGES_FILE.exists():
await ctx.send("Il n'y a pas encore eu de tirages.")
return
with open(TIRAGES_FILE) as f:
tirages = list(yaml.load_all(f))
if arg.lower() == "all":
msg = "\n".join(
f"{i}: {', '.join(team.name for team in tirage.teams)}"
for i, tirage in enumerate(tirages)
)
await ctx.send(
"Voici in liste de tous les tirages qui ont été faits. "
"Vous pouvez en consulter un en particulier avec `!show ID`."
)
await ctx.send(msg)
else:
try:
n = int(arg)
if n < 0:
raise ValueError
tirage = tirages[n]
except (ValueError, IndexError):
await ctx.send(
f"`{arg}` n'est pas un identifiant valide. "
f"Les identifiants valides sont visibles avec `!show all`"
)
else:
await tirage.show(ctx)
@bot.command(name="interrupt")
@commands.has_role(CNO_ROLE)
async def interrupt_cmd(ctx):
await ctx.send(
"J'ai été arrêté et une console interactive a été ouverte là où je tourne. "
"Toutes les commandes rateront tant que cette console est ouverte.\n"
"Soyez rapides, je déteste les opérations à coeur ouvert... :confounded:"
)
# Utility function
local = {
**globals(),
**locals(),
"pprint": pprint,
"_show": lambda o: print(*dir(o), sep="\n"),
"__name__": "__console__",
"__doc__": None,
}
code.interact(banner="Ne SURTOUT PAS FAIRE Ctrl+C !\n(TFJM² debugger)", local=local)
await ctx.send("Tout va mieux !")
@bot.event
async def on_command_error(ctx: Context, error, *args, **kwargs):
if isinstance(error, commands.CommandInvokeError):
if isinstance(error.original, UnwantedCommand):
await ctx.message.delete()
author: discord.Message
await ctx.author.send(
"J'ai supprimé ton message:\n> "
+ ctx.message.clean_content
+ "\nC'est pas grave, c'est juste pour ne pas encombrer "
"le chat lors du tirage."
)
await ctx.author.send("Raison: " + error.original.msg)
return
else:
msg = str(error.original) or str(error)
traceback.print_tb(error.original.__traceback__, file=sys.stderr)
else:
msg = str(error)
print(repr(error), dir(error), file=sys.stderr)
await ctx.send(msg)
if __name__ == "__main__":
bot.run(TOKEN)