From 03cee9963b8247b1d9707a179ade35edc9a640a4 Mon Sep 17 00:00:00 2001 From: Elnath Date: Sat, 12 Jun 2021 19:44:41 +0200 Subject: [PATCH] Added possibility to confirm some actions --- SecretBot.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/SecretBot.py b/SecretBot.py index 6d9f8c9..fedf18d 100755 --- a/SecretBot.py +++ b/SecretBot.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 import argparse +import asyncio import logging import sys from pathlib import Path +from typing import Dict, Tuple, Coroutine import discord import discord.utils @@ -21,11 +23,14 @@ class SecretBot(commands.Cog): self.bot = commands.Bot( "!", help_command = commands.MinimalHelpCommand(), - intents = discord.Intents(guild_messages = True, guilds = True, members = True), + intents = discord.Intents(guild_messages = True, guilds = True, members = True, reactions = True), allowed_mentions = discord.AllowedMentions.none(), # By default we do not allow mentions, as we will white-list them on each required message ) self.bot.add_cog(self) self.games_file = games_file + # Each guild can have a confirmation message to execute an action. We store: + # The confirmation message, the coroutine to execute if the user reacts with :thumbsup: to the confirmation message, a task that will delete the message after a certain time (so that we can stop it if the user confirms instead) + self.confirmation_messages: Dict[discord.guild, Tuple[discord.Message, Coroutine, asyncio.Task]] = {} def run(self, token): self.bot.run(token) @@ -114,6 +119,54 @@ class SecretBot(commands.Cog): await ctx.reply(":warning: You should do this in the admin channel!") raise utils.CheckFailDoNotNotify + async def confirm_action(self, message_text, text_channel: discord.TextChannel, action: Coroutine, reply_to: discord.Message = None, max_delay = 60): + """ + Asks the user to confirm an action by reacting to a message. + If the user does not react in max_delay seconds, the confirmation message is deleted and the action is canceled. + For now, only one such confirmation per server is allowed. If another confirmation is asked while a previous one exists, the previous one is treated as if the delay passed. + """ + if text_channel.guild in self.confirmation_messages: # If there was already a pending confirmation + # We delete it + message, action, task = self.confirmation_messages[text_channel.guild] + asyncio.create_task(message.delete()) + action.close() + task.cancel() + + message = await text_channel.send(message_text + "\nReact to this message with :thumbsup: to confirm or :thumbsdown: to cancel", reference = reply_to) + + async def add_reaction_prompts(): + await message.add_reaction("👍") + await message.add_reaction("👎") + asyncio.create_task(add_reaction_prompts()) + + async def delete_confirmation_message_after_max_delay(): + await asyncio.sleep(max_delay) + del self.confirmation_messages[text_channel.guild] + action.close() + await message.delete() + + self.confirmation_messages[text_channel.guild] = (message, action, asyncio.create_task(delete_confirmation_message_after_max_delay())) + + @commands.Cog.listener() + async def on_reaction_add(self, reaction: discord.Reaction, user: discord.Member): + if user.bot: # We ignore reactions added by bots + return + if reaction.message.guild in self.confirmation_messages: + message, action, task = self.confirmation_messages[reaction.message.guild] + if reaction.message == message: + # Note that we do not check which user added the reaction. + # The confirmation message is just supposed to be a safeguard against typing mistakes + if reaction.emoji == "👍": + del self.confirmation_messages[reaction.message.guild] + task.cancel() + asyncio.create_task(message.delete()) + await action + elif reaction.emoji == "👎": + del self.confirmation_messages[reaction.message.guild] + task.cancel() + action.close() + await message.delete() + @commands.command(help = "See if I'm alive") async def ping(self, ctx: commands.Context): await ctx.reply(":ping_pong:", mention_author = True) @@ -130,9 +183,13 @@ class SecretBot(commands.Cog): await ctx.reply(":white_check_mark: Game started!") @commands.command("DeleteGame", help = "Delete a running game and all of its associated channels") + async def delete_game_with_confirmation(self, ctx: commands.Context): + await self.get_running_game_or_error_message(ctx) + await self.check_is_administrator_or_gm(ctx) + await self.confirm_action("Do you really want to delete the current game?", ctx.channel, self.delete_game(ctx), ctx.message) + async def delete_game(self, ctx: commands.Context): game = await self.get_running_game_or_error_message(ctx) - await self.check_is_administrator_or_gm(ctx) gm_role = game.get_gm_role() await game.delete() await ctx.guild.get_member(self.bot.user.id).remove_roles(gm_role) @@ -174,9 +231,16 @@ class SecretBot(commands.Cog): await self.cast_vote(ctx, False) @commands.command("StopTheCount") - async def stop_vote(self, ctx: commands.Context): + async def stop_vote_with_confirmation(self, ctx: commands.Context): game = await self.get_running_game_or_error_message(ctx) await self.check_is_administrator_or_gm(ctx) + if not game.is_vote_running(): + await ctx.reply(":x: No vote is running") + return + await self.confirm_action("Do you really want to stop the vote and reveal all votes?", ctx.channel, self.stop_vote(ctx), ctx.message) + + async def stop_vote(self, ctx: commands.Context): + game = await self.get_running_game_or_error_message(ctx) if not game.is_vote_running(): await ctx.reply(":x: No vote is running") return