From 2a70f3f5e7184b026b9d3664f48c59335053108c Mon Sep 17 00:00:00 2001 From: Mathieu B <47489943+le-potate@users.noreply.github.com> Date: Tue, 31 Aug 2021 15:18:51 -0400 Subject: [PATCH] Various changes to quotes (including mod deletion) Moderators can now delete quotes when they are quoted, from the list of quotes of a user, from the allq command, and delete all quotes from a certain user using ?purgequotes rebuild_mc has been added everywhere where quotes are deleted the delete emoji in ?lq can now only be clicked by the quote list author or by a mod (whereas any users used to be able to click it before and start the delete prompt, but only the author could use the prompt). The person who clicks the reacts is also the only one that can then delete a quote. By doing this, this fixes #215, since Marty cannot even start the prompt anymore. (The check that the message wasn't written by the bot was still added in case and might be useful in case Marty can ever react on other people's messages). --- cogs/quotes.py | 233 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 188 insertions(+), 45 deletions(-) diff --git a/cogs/quotes.py b/cogs/quotes.py index 54d09821..c67d9321 100644 --- a/cogs/quotes.py +++ b/cogs/quotes.py @@ -30,6 +30,7 @@ # Other utils import random from .utils.paginator import Pages +from .utils.checks import is_moderator GEN_SPACE_SYMBOLS = re.compile(r"[,“”\".?!]") GEN_BLANK_SYMBOLS = re.compile(r"['()`]") @@ -39,6 +40,18 @@ DEFAULT_AVATAR = "https://cdn.discordapp.com/embed/avatars/0.png" +class UserToIdConverter(commands.Converter): + async def convert(self, ctx, argument): + try: + user = await commands.MemberConverter().convert(ctx, argument) + return user.id + except commands.BadArgument: + try: + return int(argument) + except ValueError: + raise commands.BadArgument("Could not find user") + + class Quotes(commands.Cog): def __init__(self, bot): self.bot = bot @@ -129,14 +142,16 @@ async def add_quotes(self, def check(reaction, user): # returns True if all the following is true: - # The user who reacted is either the quoter or the quoted person + # The user who reacted is either the quoter, the quoted person, or a moderator # The user who reacted isn't the bot # The react is the delete emoji # The react is on the "Quote added." message - return ( - user == ctx.message.author - or user == member) and user != self.bot.user and str( - reaction.emoji) == '🚮' and reaction.message.id == msg.id + return all( + ((user == ctx.message.author or user == member + or discord.utils.get(user.roles, + name=self.bot.config.moderator_role)), + user != self.bot.user, str(reaction.emoji) == '🚮', + reaction.message.id == msg.id)) try: await self.bot.wait_for('reaction_add', check=check, timeout=120) @@ -201,12 +216,14 @@ async def quotes(self, ctx, str1: str = None, *, str2: str = None): def check(reaction, user): # returns True if all the following is true: + # The user who reacted is the one that called the command or a moderator # The user who reacted isn't the bot # The react is the ok emoji # The react is on the "Quote not found." message - return (user == ctx.message.author and user != self.bot.user - ) and (str(reaction.emoji) == '🆗' - and reaction.message.id == msg.id) + return all(((user == ctx.message.author or discord.utils.get( + user.roles, name=self.bot.config.moderator_role)), + user != self.bot.user, str(reaction.emoji) == '🆗', + reaction.message.id == msg.id)) try: await self.bot.wait_for('reaction_add', @@ -274,55 +291,61 @@ async def list_quotes(self, ctx, author: discord.Member = None): p = Pages(ctx, item_list=quote_list_text, - title='Quotes from {}'.format(quote_author.display_name)) + title='Quotes from {}'.format(quote_author.display_name), + return_user_on_edit=True) - await p.paginate() + user_deleting = await p.paginate() def msg_check(msg): try: - return (0 <= int(msg.content) <= len(quote_list) - and msg.author.id == author_id - and msg.channel == ctx.message.channel) + return all((0 <= int(msg.content) <= len(quote_list), + msg.author == user_deleting, + msg.channel == ctx.message.channel, + msg.author != self.bot.user)) except ValueError: return False while p.edit_mode: - await ctx.send( - 'Delete option selected. Enter a number to specify which ' - 'quote you want to delete, or enter 0 to return.', - delete_after=60) - - try: - message = await self.bot.wait_for('message', - check=msg_check, - timeout=60) - - except asyncio.TimeoutError: + if (user_deleting == author + or discord.utils.get(user_deleting.roles, + name=self.bot.config.moderator_role)): await ctx.send( - 'Command timeout. You may want to run the command again.', + 'Delete option selected. Enter a number to specify which ' + 'quote you want to delete, or enter 0 to return.', delete_after=60) - break - else: - index = int(message.content) - 1 - if index == -1: - await ctx.send('Exit delq.', delete_after=60) + try: + message = await self.bot.wait_for('message', + check=msg_check, + timeout=60) + + except asyncio.TimeoutError: + await ctx.send( + 'Command timeout. You may want to run the command again.', + delete_after=60) + break + else: - t = (quote_list[index][0], quote_list[index][2]) - del quote_list[index] - c.execute('DELETE FROM Quotes WHERE ID = ? AND Quote = ?', - t) - conn.commit() + index = int(message.content) - 1 + if index == -1: + await ctx.send('Exit delq.', delete_after=60) + else: + t = (quote_list[index][0], quote_list[index][2]) + del quote_list[index] + c.execute( + 'DELETE FROM Quotes WHERE ID = ? AND Quote = ?', t) + conn.commit() + self.rebuild_mc() - await ctx.send('Quote deleted', delete_after=60) - await message.delete() + await ctx.send('Quote deleted', delete_after=60) + await message.delete() - p.itemList = [ - f'[{i}] {quote[2]}' - for i, quote in enumerate(quote_list, 1) - ] + p.itemList = [ + f'[{i}] {quote[2]}' + for i, quote in enumerate(quote_list, 1) + ] - await p.paginate() + await p.paginate() conn.commit() conn.close() @@ -388,10 +411,59 @@ async def all_quotes(self, ctx, *, query): p = Pages(ctx, item_list=quote_list_text, title='Quotes that contain "{}"'.format(query), - editable_content=False, - current_page=pagenum) + current_page=pagenum, + return_user_on_edit=True) - await p.paginate() + user_deleting = await p.paginate() + + def msg_check(msg): + try: + return all((0 <= int(msg.content) <= len(quote_list), + msg.author == user_deleting, + msg.channel == ctx.message.channel)) + except ValueError: + return False + + while p.edit_mode: + if discord.utils.get(user_deleting.roles, + name=self.bot.config.moderator_role): + await ctx.send( + 'Delete option selected. Enter a number to specify which ' + 'quote you want to delete, or enter 0 to return.', + delete_after=60) + + try: + message = await self.bot.wait_for('message', + check=msg_check, + timeout=60) + + except asyncio.TimeoutError: + await ctx.send( + 'Command timeout. You may want to run the command again.', + delete_after=60) + break + + else: + index = int(message.content) - 1 + if index == -1: + await ctx.send('Exit delq.', delete_after=60) + else: + t = (quote_list[index][0], quote_list[index][2]) + del quote_list[index] + c.execute( + 'DELETE FROM Quotes WHERE ID = ? AND Quote = ?', t) + conn.commit() + self.rebuild_mc() + + await ctx.send('Quote deleted', delete_after=60) + await message.delete() + + p.itemList = [ + f'[{i}] {quote[2]}' + for i, quote in enumerate(quote_list, 1) + ] + + await p.paginate() @commands.command(aliases=['gen']) async def generate(self, ctx, seed: str = None, min_length: int = 1): @@ -466,6 +538,77 @@ async def generate(self, ctx, seed: str = None, min_length: int = 1): await ctx.send(' '.join(longest_sentence)) + @commands.command() + @is_moderator() + async def purgequotes(self, ctx, user: str): + """ + Mod-only: Purge all quote from user + Argument: A user (ID can be used even if the account is deleted) + """ + try: + id = await UserToIdConverter().convert(ctx, user) + except commands.BadArgument as err: + await ctx.send(err) + return + + def msg_check(msg): + return all( + (msg.author == ctx.message.author, msg.channel == ctx.channel)) + + conn = sqlite3.connect(self.bot.config.db_path) + c = conn.cursor() + c.execute('SELECT * FROM Quotes WHERE ID = ?', (id, )) + quote_list = c.fetchall() + + if not quote_list: + await ctx.send('No quote found.') + return + + quote_list_text = [ + f'[{i}] {quote[2]}' for i, quote in enumerate(quote_list, 1) + ] + + p = Pages(ctx, + item_list=quote_list_text, + title="List of quotes to be deleted", + editable_content_emoji="🆗", + return_user_on_edit=True) + + confirm_message = await ctx.send( + f"Do you want to delete the following {len(quote_list)} quotes?\n" + f"Please press 🆗 to confirm (You will be asked to confirm one more time)" + ) + user_deleting = await p.paginate() + + while p.edit_mode: + if user_deleting == ctx.message.author: + if user_deleting == ctx.message.author: + await ctx.send( + "Please type `yes` to confirm the deletion. (Type anything otherwise)" + ) + try: + confirmation_msg = await self.bot.wait_for( + 'message', check=msg_check, timeout=60) + except asyncio.TimeoutError: + await ctx.send("Command timed out.") + return + + confirmation_str = confirmation_msg.content + if confirmation_str.lower() != "yes": + await ctx.send("Exiting without deleting.") + return + + c.execute('DELETE FROM Quotes WHERE ID = ?', (id, )) + conn.commit() + self.rebuild_mc() + await ctx.send( + f"Successfully deleted {len(quote_list)} quotes") + break + await p.paginate() + + await confirm_message.delete() + conn.close() + def setup(bot): bot.add_cog(Quotes(bot))