From 4058612e9669a3eea13d962dfaefae4f6b91ab94 Mon Sep 17 00:00:00 2001 From: Nichole Mattera <697668+NicholeMattera@users.noreply.github.com> Date: Tue, 25 Feb 2020 18:15:49 -0500 Subject: [PATCH 1/3] Added cog to manage channels dedicated towards lists. --- Robocop.py | 3 +- cogs/lists.py | 290 +++++++++++++++++++++++++++++++++++++++++++++ config_template.py | 6 + 3 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 cogs/lists.py diff --git a/Robocop.py b/Robocop.py index cc6877c..1c46a30 100755 --- a/Robocop.py +++ b/Robocop.py @@ -63,7 +63,8 @@ initial_extensions = ['cogs.common', 'cogs.meme', 'cogs.imagemanip', 'cogs.pin', - 'cogs.invites'] + 'cogs.invites', + 'cogs.lists'] bot = commands.Bot(command_prefix=get_prefix, description=config.bot_description) diff --git a/cogs/lists.py b/cogs/lists.py new file mode 100644 index 0000000..5285df6 --- /dev/null +++ b/cogs/lists.py @@ -0,0 +1,290 @@ +import config +import discord +import io +import urllib.parse +from discord.ext import commands +from discord.ext.commands import Cog + +class Lists(Cog): + """ + Manages channels that are dedicated to lists. + """ + + def __init__(self, bot): + self.bot = bot + + # Helpers + + def check_if_target_is_staff(self, target): + return any(r.id in config.staff_role_ids for r in target.roles) + + def is_edit(self, emoji): + return str(emoji)[0] == u"✏" or str(emoji)[0] == u"📝" + + def is_delete(self, emoji): + return str(emoji)[0] == u"❌" or str(emoji)[0] == u"❎" + + def is_recycle(self, emoji): + return str(emoji)[0] == u"♻" + + def is_insert_above(self, emoji): + return str(emoji)[0] == u"⤴️" or str(emoji)[0] == u"⬆" + + def is_insert_below(self, emoji): + return str(emoji)[0] == u"⤵️" or str(emoji)[0] == u"⬇" + + def is_reaction_valid(self, reaction): + allowed_reactions = [ + u"✏", + u"📝", + u"❌", + u"❎", + u"♻", + u"⤴️", + u"⬆", + u"⬇", + u"⤵️", + ] + return str(reaction.emoji)[0] in allowed_reactions + + async def find_reactions(self, user_id, channel_id, limit = None): + reactions = [] + channel = self.bot.get_channel(channel_id) + async for message in channel.history(limit = limit): + if len(message.reactions) == 0: + continue + + for reaction in message.reactions: + users = await reaction.users().flatten() + user_ids = map(lambda user: user.id, users) + if user_id in user_ids: + reactions.append(reaction) + + return reactions + + def create_log_message(self, emoji, action, user, channel, reason = ""): + msg = f"{emoji} **{action}** \n"\ + f"from {self.bot.escape_message(user.name)} ({user.id}), in {channel.mention}" + + if reason != "": + msg += f":\n`{reason}`" + + return msg + + async def clean_up_raw_text_file_message(self, message): + embeds = message.embeds + if len(embeds) == 0: + return + + fields = embeds[0].fields + for field in fields: + if field.name == "Message ID": + files_channel = self.bot.get_channel(config.list_files_channel) + file_message = await files_channel.fetch_message(int(field.value)) + await file_message.delete() + + await message.edit(embed = None) + + # Commands + + @commands.command(aliases = ["list"]) + async def listitem(self, ctx, channel: discord.TextChannel, number: int): + """Link to a specific list item.""" + if number <= 0: + await ctx.send(f"Number must be greater than 0.") + return + + if channel.id not in config.list_channels: + await ctx.send(f"{channel.mention} is not a list channel.") + return + + counter = 0 + async for message in channel.history(limit = None, oldest_first = True): + if message.content.strip(): + counter += 1 + + if counter == number: + embed = discord.Embed( + title = f"Item #{number} in #{channel.name}", + description = message.content, + url = message.jump_url + ) + await ctx.send( + content = "", + embed = embed + ) + return + + await ctx.send(f"Unable to find item #{number} in {channel.mention}.") + + # Listeners + + @Cog.listener() + async def on_raw_reaction_add(self, payload): + await self.bot.wait_until_ready() + + # We only care about reactions in Rules, and Support FAQ + if payload.channel_id not in config.list_channels: + return + + channel = self.bot.get_channel(payload.channel_id) + message = await channel.fetch_message(payload.message_id) + member = channel.guild.get_member(payload.user_id) + user = self.bot.get_user(payload.user_id) + reaction = next((reaction for reaction in message.reactions if str(reaction.emoji) == str(payload.emoji)), None) + if reaction is None: + return + + # Only staff can add reactions in these channels. + if not self.check_if_target_is_staff(member): + await reaction.remove(user) + return + + # Reactions are only allowed on messages from the bot. + if not message.author.bot: + await reaction.remove(user) + return + + # Only certain reactions are allowed. + if not self.is_reaction_valid(reaction): + await reaction.remove(user) + return + + # Remove all other reactions from user in this channel. + for r in await self.find_reactions(payload.user_id, payload.channel_id): + if r.message.id != message.id or (r.message.id == message.id and str(r.emoji) != str(reaction.emoji)): + await r.remove(user) + + # When editing we want to provide the user a copy of the raw text. + if self.is_edit(reaction.emoji) and config.list_files_channel != 0: + files_channel = self.bot.get_channel(config.list_files_channel) + file = discord.File(io.BytesIO(message.content.encode("utf-8")), filename = f"{message.id}.txt") + file_message = await files_channel.send(file = file) + + embed = discord.Embed( + title = "Click here to get the raw text to modify.", + url = f"{file_message.attachments[0].url}?" + ) + embed.add_field(name = "Message ID", value = file_message.id, inline = False) + await message.edit(embed = embed) + + @Cog.listener() + async def on_raw_reaction_remove(self, payload): + await self.bot.wait_until_ready() + + # We only care about reactions in Rules, and Support FAQ + if payload.channel_id not in config.list_channels: + return + + channel = self.bot.get_channel(payload.channel_id) + message = await channel.fetch_message(payload.message_id) + + # Reaction was removed from a message we don"t care about. + if not message.author.bot: + return + + # We want to remove the embed we added. + if self.is_edit(payload.emoji) and config.list_files_channel != 0: + await self.clean_up_raw_text_file_message(message) + + @Cog.listener() + async def on_message(self, message): + await self.bot.wait_until_ready() + + # We only care about messages in Rules, and Support FAQ + if message.channel.id not in config.list_channels: + return + + # We don"t care about messages from bots. + if message.author.bot: + return + + # Only staff can modify lists. + if not self.check_if_target_is_staff(message.author): + await message.delete() + return + + log_channel = self.bot.get_channel(config.log_channel) + channel = message.channel + content = message.content + user = message.author + + attachment_filename = None + attachment_data = None + if len(message.attachments) != 0: + # Lists will only reupload the first image. + attachment = next((a for a in message.attachments if a.filename.endswith(".png") or a.filename.endswith(".jpg") or a.filename.endswith(".jpeg")), None) + if attachment != None: + attachment_filename = attachment.filename + attachment_data = await attachment.read() + + await message.delete() + + reactions = await self.find_reactions(user.id, channel.id) + + # Add to the end of the list if there is no reactions or somehow more than one. + if len(reactions) != 1: + if attachment_filename != None and attachment_data != None: + file = discord.File(io.BytesIO(attachment_data), filename = attachment_filename) + await channel.send(content = content, file = file) + else: + await channel.send(content) + + for reaction in reactions: + await reaction.remove(user) + + await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + return + + targeted_reaction = reactions[0] + targeted_message = targeted_reaction.message + + if self.is_edit(targeted_reaction): + if config.list_files_channel != 0: + await self.clean_up_raw_text_file_message(targeted_message) + await targeted_message.edit(content = content) + await targeted_reaction.remove(user) + + await log_channel.send(self.create_log_message("📝", "List item edited:", user, channel)) + + elif self.is_delete(targeted_reaction): + await targeted_message.delete() + + await log_channel.send(self.create_log_message("❌", "List item deleted:", user, channel, content)) + + elif self.is_recycle(targeted_reaction): + messages = await channel.history(limit = None, after = targeted_message, oldest_first = True).flatten() + await channel.purge(limit = len(messages) + 1, bulk = True) + + await channel.send(targeted_message.content) + for message in messages: + await channel.send(message.content) + + await log_channel.send(self.create_log_message("♻", "List item recycled:", user, channel, content)) + + elif self.is_insert_above(targeted_reaction): + messages = await channel.history(limit = None, after = targeted_message, oldest_first = True).flatten() + await channel.purge(limit = len(messages) + 1, bulk = True) + + await channel.send(content) + await channel.send(targeted_message.content) + for message in messages: + self.bot.log.info(message.content) + await channel.send(message.content) + + await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + + elif self.is_insert_below(targeted_reaction): + messages = await channel.history(limit = None, after = targeted_message, oldest_first = True).flatten() + await channel.purge(limit = len(messages) + 1, bulk = True) + + await channel.send(targeted_message.content) + await channel.send(content) + for message in messages: + self.bot.log.info(message.content) + await channel.send(message.content) + + await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + +def setup(bot): + bot.add_cog(Lists(bot)) diff --git a/config_template.py b/config_template.py index 70f4e29..d9ac323 100644 --- a/config_template.py +++ b/config_template.py @@ -97,3 +97,9 @@ allowed_pin_roles = [] # Used for the pinboard. Leave empty if you don't wish for a gist pinboard. github_oauth_token = "" + +# Channel to upload text files while editing list items. (They are cleaned up.) +list_files_channel = 0 + +# Channels that are lists that are controlled by the lists cog. +list_channels = [] From fd3a26c80e86a0e690138c639796218ceb0e08a4 Mon Sep 17 00:00:00 2001 From: Nichole Mattera <697668+NicholeMattera@users.noreply.github.com> Date: Tue, 25 Feb 2020 20:08:55 -0500 Subject: [PATCH 2/3] Ran code through autopep8. --- cogs/lists.py | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/cogs/lists.py b/cogs/lists.py index 5285df6..72b4c61 100644 --- a/cogs/lists.py +++ b/cogs/lists.py @@ -5,6 +5,7 @@ import urllib.parse from discord.ext import commands from discord.ext.commands import Cog + class Lists(Cog): """ Manages channels that are dedicated to lists. @@ -59,7 +60,7 @@ class Lists(Cog): user_ids = map(lambda user: user.id, users) if user_id in user_ids: reactions.append(reaction) - + return reactions def create_log_message(self, emoji, action, user, channel, reason = ""): @@ -68,7 +69,7 @@ class Lists(Cog): if reason != "": msg += f":\n`{reason}`" - + return msg async def clean_up_raw_text_file_message(self, message): @@ -82,7 +83,7 @@ class Lists(Cog): files_channel = self.bot.get_channel(config.list_files_channel) file_message = await files_channel.fetch_message(int(field.value)) await file_message.delete() - + await message.edit(embed = None) # Commands @@ -131,7 +132,9 @@ class Lists(Cog): message = await channel.fetch_message(payload.message_id) member = channel.guild.get_member(payload.user_id) user = self.bot.get_user(payload.user_id) - reaction = next((reaction for reaction in message.reactions if str(reaction.emoji) == str(payload.emoji)), None) + reaction = next( + (reaction for reaction in message.reactions + if str(reaction.emoji) == str(payload.emoji)), None) if reaction is None: return @@ -152,20 +155,25 @@ class Lists(Cog): # Remove all other reactions from user in this channel. for r in await self.find_reactions(payload.user_id, payload.channel_id): - if r.message.id != message.id or (r.message.id == message.id and str(r.emoji) != str(reaction.emoji)): + if r.message.id != message.id or (r.message.id == message.id and + str(r.emoji) != str(reaction.emoji)): await r.remove(user) # When editing we want to provide the user a copy of the raw text. if self.is_edit(reaction.emoji) and config.list_files_channel != 0: files_channel = self.bot.get_channel(config.list_files_channel) - file = discord.File(io.BytesIO(message.content.encode("utf-8")), filename = f"{message.id}.txt") + file = discord.File( + io.BytesIO(message.content.encode("utf-8")), + filename = f"{message.id}.txt") file_message = await files_channel.send(file = file) embed = discord.Embed( title = "Click here to get the raw text to modify.", - url = f"{file_message.attachments[0].url}?" - ) - embed.add_field(name = "Message ID", value = file_message.id, inline = False) + url = f"{file_message.attachments[0].url}?") + embed.add_field( + name = "Message ID", + value = file_message.id, + inline = False) await message.edit(embed = embed) @Cog.listener() @@ -194,7 +202,7 @@ class Lists(Cog): # We only care about messages in Rules, and Support FAQ if message.channel.id not in config.list_channels: return - + # We don"t care about messages from bots. if message.author.bot: return @@ -213,8 +221,10 @@ class Lists(Cog): attachment_data = None if len(message.attachments) != 0: # Lists will only reupload the first image. - attachment = next((a for a in message.attachments if a.filename.endswith(".png") or a.filename.endswith(".jpg") or a.filename.endswith(".jpeg")), None) - if attachment != None: + attachment = next((a for a in message.attachments if + a.filename.endswith(".png") or a.filename.endswith(".jpg") or + a.filename.endswith(".jpeg")), None) + if attachment is not None: attachment_filename = attachment.filename attachment_data = await attachment.read() @@ -222,10 +232,13 @@ class Lists(Cog): reactions = await self.find_reactions(user.id, channel.id) - # Add to the end of the list if there is no reactions or somehow more than one. + # Add to the end of the list if there is no reactions or somehow more + # than one. if len(reactions) != 1: - if attachment_filename != None and attachment_data != None: - file = discord.File(io.BytesIO(attachment_data), filename = attachment_filename) + if attachment_filename is not None and attachment_data is not None: + file = discord.File( + io.BytesIO(attachment_data), + filename = attachment_filename) await channel.send(content = content, file = file) else: await channel.send(content) @@ -286,5 +299,6 @@ class Lists(Cog): await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + def setup(bot): bot.add_cog(Lists(bot)) From 1fe4dab6cd396b83788362dab2e8cc0999f222f9 Mon Sep 17 00:00:00 2001 From: Nichole Mattera <697668+NicholeMattera@users.noreply.github.com> Date: Wed, 26 Feb 2020 08:46:11 -0500 Subject: [PATCH 3/3] Forgot to remove logs after debugging. --- cogs/lists.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cogs/lists.py b/cogs/lists.py index 72b4c61..4305f54 100644 --- a/cogs/lists.py +++ b/cogs/lists.py @@ -282,7 +282,6 @@ class Lists(Cog): await channel.send(content) await channel.send(targeted_message.content) for message in messages: - self.bot.log.info(message.content) await channel.send(message.content) await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) @@ -294,7 +293,6 @@ class Lists(Cog): await channel.send(targeted_message.content) await channel.send(content) for message in messages: - self.bot.log.info(message.content) await channel.send(message.content) await log_channel.send(self.create_log_message("💬", "List item added:", user, channel))