diff --git a/src/cogs/__init__.py b/src/cogs/__init__.py index 3dc3149..ba35cb1 100644 --- a/src/cogs/__init__.py +++ b/src/cogs/__init__.py @@ -4,3 +4,4 @@ of the TFJM² bot. """ from .tirages import TirageCog +from .help import TfjmHelpCommand diff --git a/src/cogs/help.py b/src/cogs/help.py new file mode 100644 index 0000000..27b1bd5 --- /dev/null +++ b/src/cogs/help.py @@ -0,0 +1,86 @@ +from discord.ext.commands import MinimalHelpCommand + + +# This is mostly a copy paste of MinimalHelpCommand +# With updated defaults and styles to match what I like +class TfjmHelpCommand(MinimalHelpCommand): + def __init__(self, **options): + options.setdefault("no_category", "Autres") + super().__init__(**options) + + def get_opening_note(self): + """Text at the beginning of the help command.""" + + command_name = self.invoked_with + return ( + "`{0}{1} [commande]` permet d'avoir plus d'info sur une commande.\n" + "Vous pouvez aussi utiliser `{0}{1} [catégorie]` " + "pour plus d'infos sur une catégorie.".format( + self.clean_prefix, command_name + ) + ) + + def get_command_signature(self, command): + return "`%s`" % super().get_command_signature(command) + + def add_bot_commands_formatting(self, commands, heading): + """Adds the minified bot heading with commands to the output. + + The formatting should be added to the :attr:`paginator`. + + The default implementation is a bold underline heading followed + by commands separated by an EN SPACE (U+2002) in the next line. + + Parameters + ----------- + commands: Sequence[:class:`Command`] + A list of commands that belong to the heading. + heading: :class:`str` + The heading to add to the line. + """ + + if commands: + # U+2002 Middle Dot + self.paginator.add_line("__**%s**__" % heading) + for c in commands: + self.add_subcommand_formatting(c) + self.paginator.add_line() + + def add_subcommand_formatting(self, command): + """Adds formatting information on a subcommand. + + The formatting should be added to the :attr:`paginator`. + + The default implementation is the prefix and the :attr:`Command.qualified_name` + optionally followed by an En dash and the command's :attr:`Command.short_doc`. + + Parameters + ----------- + command: :class:`Command` + The command to show information of. + """ + fmt = "`{0}{1}` \N{EN DASH} {2}" if command.short_doc else "`{0}{1}`" + self.paginator.add_line( + fmt.format(self.clean_prefix, command.qualified_name, command.short_doc) + ) + + def add_aliases_formatting(self, aliases): + """Adds the formatting information on a command's aliases. + + The formatting should be added to the :attr:`paginator`. + + The default implementation is the :attr:`aliases_heading` bolded + followed by a comma separated list of aliases. + + This is not called if there are no aliases to format. + + Parameters + ----------- + aliases: Sequence[:class:`str`] + A list of aliases to format. + """ + + aliases_str = ", ".join("`%s`" % a for a in aliases) + self.paginator.add_line( + "**%s** %s" % (self.aliases_heading, aliases_str), empty=True + ) diff --git a/src/cogs/tirages.py b/src/cogs/tirages.py index b5d66dc..57f198c 100644 --- a/src/cogs/tirages.py +++ b/src/cogs/tirages.py @@ -530,11 +530,78 @@ class TirageCog(Cog, name="Tirages"): self.tirages = tirages + # ---------- Commandes hors du groupe draw ----------- # + + @commands.command( + name="dice", aliases=["de", "dé", "roll"], usage="n", + ) + async def dice(self, ctx: Context, n: int): + """Lance un dé à `n` faces.""" + channel = ctx.channel.id + if channel in self.tirages: + await self.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}**" + ) + + @commands.command( + name="random-problem", + aliases=["rp", "problème-aléatoire", "probleme-aleatoire", "pa"], + ) + async def random_problem(self, ctx: Context): + """Choisit un problème parmi ceux de cette année.""" + + channel = ctx.channel.id + if channel in self.tirages: + await self.tirages[channel].choose_problem(ctx) + else: + problem = random.choice(PROBLEMS) + await ctx.send(f"Le problème tiré est... **{problem}**") + + @commands.command( + name="oui", aliases=["accept", "yes", "o", "accepte", "ouiiiiiii"], + ) + async def accept_cmd(self, ctx): + """ + Accepte le problème qui vient d'être tiré. + + Sans effet si il n'y a pas de tirage en cours. + """ + + channel = ctx.channel.id + if channel in self.tirages: + await self.tirages[channel].accept(ctx, True) + else: + await ctx.send(f"{ctx.author.mention} approuve avec vigeur !") + + @commands.command( + name="non", aliases=["refuse", "no", "n", "nope", "jaaamais"], + ) + async def refuse_cmd(self, ctx): + """ + Refuse le problème qui vient d'être tiré. + + Sans effet si il n'y a pas de tirage en cours. + """ + + channel = ctx.channel.id + if channel in self.tirages: + await self.tirages[channel].accept(ctx, False) + else: + await ctx.send(f"{ctx.author.mention} nie tout en block !") + + # ---------- Commandes du groupe draw ----------- # + @group( name="draw", aliases=["d", "tirage"], ) async def draw_group(self, ctx: Context) -> None: - """Commandes pour les tirages.""" + """Groupe de commandes pour les tirages.""" @draw_group.command( name="start", usage="équipe1 équipe2 équipe3 (équipe4)", @@ -596,6 +663,13 @@ class TirageCog(Cog, name="Tirages"): ) @commands.has_role(ORGA_ROLE) async def abort_draw_cmd(self, ctx): + """ + Annule le tirage en cours. + + Le tirage ne pourra pas être continué. Si besoin, + n'hésitez pas à appeller un @dev : il peut réparer + plus de choses qu'on imagine (mais moins qu'on voudrait). + """ channel_id = ctx.channel.id if channel_id in self.tirages: await self.tirages[channel_id].end(ctx) @@ -604,6 +678,7 @@ class TirageCog(Cog, name="Tirages"): @draw_group.command(name="skip", aliases=["s"]) @commands.has_role(CNO_ROLE) async def draw_skip(self, ctx, *teams): + """Skip certaines phases du tirage.""" channel = ctx.channel.id self.tirages[channel] = tirage = Tirage(ctx, channel, teams) @@ -620,7 +695,15 @@ class TirageCog(Cog, name="Tirages"): await tirage.update_phase(ctx) @draw_group.command(name="show") - async def show_cmd(self, ctx: Context, arg: str): + async def show_cmd(self, ctx: Context, tirage_id: str): + """ + Affiche le résumé d'un tirage + + Les ID de tirages valides sont visibles avec + `!draw show all` et les details avec `!draw show 42` + (si l'ID qui vous intéresse est 42). + """ + if not TIRAGES_FILE.exists(): await ctx.send("Il n'y a pas encore eu de tirages.") return @@ -628,7 +711,7 @@ class TirageCog(Cog, name="Tirages"): with open(TIRAGES_FILE) as f: tirages = list(yaml.load_all(f)) - if arg.lower() == "all": + if tirage_id.lower() == "all": msg = "\n".join( f"{i}: {', '.join(team.name for team in tirage.teams)}" for i, tirage in enumerate(tirages) @@ -640,13 +723,13 @@ class TirageCog(Cog, name="Tirages"): await ctx.send(msg) else: try: - n = int(arg) + n = int(tirage_id) if n < 0: raise ValueError tirage = tirages[n] except (ValueError, IndexError): await ctx.send( - f"`{arg}` n'est pas un identifiant valide. " + f"`{tirage_id}` n'est pas un identifiant valide. " f"Les identifiants valides sont visibles avec `!show all`" ) else: diff --git a/src/tfjm_discord_bot.py b/src/tfjm_discord_bot.py index 2250394..916d03d 100644 --- a/src/tfjm_discord_bot.py +++ b/src/tfjm_discord_bot.py @@ -9,10 +9,11 @@ import discord from discord.ext import commands from discord.ext.commands import Context +from src.cogs import TfjmHelpCommand from src.constants import * from src.errors import TfjmError, UnwantedCommand -bot = commands.Bot("!", help_command=commands.MinimalHelpCommand(no_category="Autres")) +bot = commands.Bot("!", help_command=TfjmHelpCommand()) # Variable globale qui contient les tirages. tirages = {} @@ -23,78 +24,34 @@ async def on_ready(): print(f"{bot.user} has connected to Discord!") -@bot.command( - name="dice", - help="Lance un dé à `n` faces. ", - aliases=["de", "dé", "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...", + usage='choix1 choix2 "choix 3"...', aliases=["choice", "choix", "ch"], ) async def choose(ctx: Context, *args): + """ + Choisit une option parmi tous les arguments. + + Pour les options qui contiennent une espace, + il suffit de mettre des guillemets (`"`) autour. + """ + 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="interrupt") @commands.has_role(CNO_ROLE) async def interrupt_cmd(ctx): + """ + :warning: Ouvre une console là où un @dev m'a lancé. :warning: + + A utiliser en dernier recours: + - le bot sera inactif pendant ce temps. + - toutes les commandes seront executées à sa reprise. + """ + 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"