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 is not 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 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) 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: 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: 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))