import datetime import re import typing import discord from redbot.core import app_commands, Config, checks, commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import humanize_list class Suggestions(commands.Cog): """ Adds a suggestions system. """ def __init__(self, bot: Red): self.bot = bot self.config = Config.get_conf( self, identifier=2115656421364, force_registration=True ) self.config.register_guild( same=False, suggest_id=None, approve_id=None, denied_id=None, next_id=1, up_emoji=1071095785455886436, down_emoji=1071096039119007804, delete_suggest=True, delete_suggestion=True, anonymous=False, ) self.config.init_custom("SUGGESTION", 2) # server_id, suggestion_id self.config.register_custom( "SUGGESTION", author=[], # id, name, discriminator guild_id=0, msg_id=0, finished=False, approved=False, denied=False, reason=False, stext=None, rtext=None, ) def check_discrim(self, user: discord.User): if user.discriminator == "0": return user.name else: return f"{user.name}#{user.discriminator}" async def red_delete_data_for_user(self, *, requester, user_id): # per guild suggestions for guild in self.bot.guilds: for suggestion_id in range(1, await self.config.guild(guild).next_id()): author_info = await self.config.custom( "SUGGESTION", guild.id, suggestion_id ).author() if user_id in author_info: await self.config.custom( "SUGGESTION", guild.id, suggestion_id ).author.clear() def format_help_for_context(self, ctx: commands.Context) -> str: context = super().format_help_for_context(ctx) return f"{context}" @commands.hybrid_command() @commands.guild_only() @checks.bot_has_permissions(add_reactions=True) async def suggest_cmd(self, ctx: commands.Context, *, suggestion: str): """Suggest something.""" if ctx.interaction is True: await ctx.defer() suggest_id = await self.config.guild(ctx.guild).suggest_id() if not suggest_id: if not await self.config.toggle(): return await ctx.send("Uh oh, suggestions aren't enabled.") if ctx.guild.id in await self.config.ignore(): return await ctx.send("Uh oh, suggestions aren't enabled.") else: channel = ctx.guild.get_channel(suggest_id) if not channel: return await ctx.send( "Uh oh, looks like the Admins haven't added the required channel." ) embed = discord.Embed(color=await ctx.embed_colour(), description=suggestion) footer = [f"Suggested by {self.check_discrim(ctx.author)} • ({ctx.author.id})", ctx.author.display_avatar.url] author = [f"{ctx.author.display_name}", ctx.author.display_avatar.url] embed.set_footer( text=footer[0], icon_url=footer[1] ) embed.set_author( name=author[0], icon_url=author[1] ) if ctx.message.attachments: embed.set_image(url=ctx.message.attachments[0].url) s_id = await self.config.guild(ctx.guild).next_id() await self.config.guild(ctx.guild).next_id.set(s_id + 1) server = ctx.guild.id content = f"Suggestion #{s_id}" embed.title = content msg = await channel.send(embed=embed) up_emoji, down_emoji = await self._get_emojis(ctx) await msg.add_reaction(up_emoji) await msg.add_reaction(down_emoji) async with self.config.custom("SUGGESTION", server, s_id).author() as author: author.append(ctx.author.id) author.append(ctx.author.name) author.append(ctx.author.discriminator) await self.config.custom("SUGGESTION", server, s_id).guild_id.set(ctx.guild.id) await self.config.custom("SUGGESTION", server, s_id).stext.set(suggestion) await self.config.custom("SUGGESTION", server, s_id).msg_id.set(msg.id) if await self.config.guild(ctx.guild).delete_suggest(): try: await ctx.message.delete() except discord.errors.NotFound: pass else: await ctx.tick() @checks.admin() @commands.command() @commands.guild_only() @checks.bot_has_permissions(manage_messages=True) async def approve( self, ctx: commands.Context, suggestion_id: int, *, reason: typing.Optional[str], ): """Approve a suggestion.""" await self._finish_suggestion(ctx, suggestion_id, True, reason, ctx.author) @checks.admin() @commands.command() @commands.guild_only() @checks.bot_has_permissions(manage_messages=True) async def deny( self, ctx: commands.Context, suggestion_id: int, *, reason: typing.Optional[str], ): """Deny a suggestion. Reason is optional.""" await self._finish_suggestion(ctx, suggestion_id, False, reason, ctx.author) @checks.admin() @commands.command() @commands.guild_only() @checks.bot_has_permissions(manage_messages=True) async def addreason( self, ctx: commands.Context, suggestion_id: int, *, reason: str, ): server = ctx.guild.id author = ctx.author approved_channel = ctx.guild.get_channel( await self.config.guild(ctx.guild).approve_id() ) reject_channel = ctx.guild.get_channel( await self.config.guild(ctx.guild).denied_id() ) msg_id = await self.config.custom("SUGGESTION", server, suggestion_id).msg_id() try: old_msg = await approved_channel.fetch_message(msg_id) approve = True except discord.NotFound: try: old_msg = await reject_channel.fetch_message(msg_id) approve = False except discord.NotFound: return await ctx.send("Uh oh, message with this ID doesn't exist.") if not old_msg: return await ctx.send("Uh oh, message with this ID doesn't exist.") embed = old_msg.embeds[0] content = old_msg.content approved = "Approved" if approve else "Denied" embed.title = f"Suggestion {approved} (#{suggestion_id})" footer = [f"{approved} by {self.check_discrim(author)} • ({author.id})", author.display_avatar.replace(format="png", size=512)] embed.set_footer( text=footer[0], icon_url=footer[1] ) if reason: embed.add_field(name="Reason", value=reason, inline=False) await self.config.custom("SUGGESTION", server, suggestion_id).reason.set( True ) await self.config.custom("SUGGESTION", server, suggestion_id).rtext.set( reason ) await old_msg.edit(content=content, embed=embed) await ctx.tick() @app_commands.context_menu(name="Approve Suggestion") async def approve_context(self, interaction: discord.Interaction, message: discord.Message): await interaction.response.send_modal(self.SuggestionApproveModal(self, message)) @app_commands.context_menu(name="Deny Suggestion") async def deny_context(self, interaction: discord.Interaction, message: discord.Message): await interaction.response.send_modal(self.SuggestionDenyModal(self, message)) @checks.admin() @checks.bot_has_permissions( manage_channels=True, add_reactions=True, manage_messages=True ) @commands.group(autohelp=True, aliases=["suggestion"]) @commands.guild_only() async def suggestset(self, ctx: commands.Context): """Various Suggestion settings.""" @suggestset.command(name="channel") async def suggestset_channel( self, ctx: commands.Context, channel: typing.Optional[discord.TextChannel] ): """Set the channel for suggestions. If the channel is not provided, suggestions will be disabled.""" if channel: await self.config.guild(ctx.guild).suggest_id.set(channel.id) else: await self.config.guild(ctx.guild).suggest_id.clear() await ctx.tick() @suggestset.command(name="approved") async def suggestset_approved( self, ctx: commands.Context, channel: typing.Optional[discord.TextChannel] ): """Set the channel for approved suggestions. If the channel is not provided, approved suggestions will not be reposted. This cannot be the same channel as denied suggestions.""" old_channel = ctx.guild.get_channel(await self.config.guild(ctx.guild).approve_id()) if channel: await self.config.guild(ctx.guild).approve_id.set(channel.id) if ctx.guild.get_channel(await self.config.guild(ctx.guild).approve_id()) == \ ctx.guild.get_channel(await self.config.guild(ctx.guild).denied_id()): await ctx.send("Cannot make approved and denied the same channel!") try: await self.config.guild(ctx.guild).approve_id.set(old_channel.id) except AttributeError: await self.config.guild(ctx.guild).approve_id.clear() return else: await self.config.guild(ctx.guild).approve_id.clear() await ctx.tick() @suggestset.command(name="denied") async def suggestset_denied( self, ctx: commands.Context, channel: typing.Optional[discord.TextChannel] ): """Set the channel for denied suggestions. If the channel is not provided, denied suggestions will not be reposted. This cannot be the same channel as approved suggestions.""" old_channel = ctx.guild.get_channel(await self.config.guild(ctx.guild).denied_id()) if channel: await self.config.guild(ctx.guild).denied_id.set(channel.id) if ctx.guild.get_channel(await self.config.guild(ctx.guild).approve_id()) == \ ctx.guild.get_channel(await self.config.guild(ctx.guild).denied_id()): await ctx.send("Cannot make approved and denied the same channel!") try: await self.config.guild(ctx.guild).denied_id.set(old_channel.id) except AttributeError: await self.config.guild(ctx.guild).denied_id.clear() return else: await self.config.guild(ctx.guild).denied_id.clear() await ctx.tick() @suggestset.command(name="same") async def suggestset_same(self, ctx: commands.Context, same: bool): """Set whether to use the same channel for new and finished suggestions.""" await ctx.send( "Suggestions won't be reposted anywhere, only their title will change accordingly." if same else "Suggestions will go to their appropriate channels upon approving/denying." ) await self.config.guild(ctx.guild).same.set(same) @suggestset.command(name="upvote") async def suggestset_upemoji( self, ctx: commands.Context, up_emoji: typing.Optional[discord.Emoji] ): """Set a custom upvote emoji instead of <:upvote:1071095785455886436>.""" if not up_emoji: await self.config.guild(ctx.guild).up_emoji.clear() else: try: await ctx.message.add_reaction(up_emoji) except discord.HTTPException: return await ctx.send("Uh oh, I cannot use that emoji.") await self.config.guild(ctx.guild).up_emoji.set(up_emoji.id) await ctx.tick() @suggestset.command(name="downvote") async def suggestset_downemoji( self, ctx: commands.Context, down_emoji: typing.Optional[discord.Emoji] ): """Set a custom downvote emoji instead of <:downvote:1071096039119007804>.""" if not down_emoji: await self.config.guild(ctx.guild).down_emoji.clear() else: try: await ctx.message.add_reaction(down_emoji) except discord.HTTPException: return await ctx.send("Uh oh, I cannot use that emoji.") await self.config.guild(ctx.guild).down_emoji.set(down_emoji.id) await ctx.tick() @suggestset.command(name="autodelete") async def suggestset_autodelete( self, ctx: commands.Context, on_off: typing.Optional[bool] ): """Toggle whether after `[p]suggest`, the bot deletes the command message.""" target_state = on_off or not ( await self.config.guild(ctx.guild).delete_suggest() ) await self.config.guild(ctx.guild).delete_suggest.set(target_state) await ctx.send( "Auto deletion is now enabled." if target_state else "Auto deletion is now disabled." ) @suggestset.command(name="delete") async def suggestset_delete( self, ctx: commands.Context, on_off: typing.Optional[bool] ): """Toggle whether suggestions in the original suggestion channel get deleted after being approved/denied. If `on_off` is not provided, the state will be flipped.""" target_state = on_off or not ( await self.config.guild(ctx.guild).delete_suggestion() ) await self.config.guild(ctx.guild).delete_suggestion.set(target_state) await ctx.send( "Suggestions will be deleted upon approving/denying from the original suggestion channel." if target_state else "Suggestions will stay in the original channel after approving/denying." ) @suggestset.command(name="settings") async def suggestset_settings(self, ctx: commands.Context): """See current settings.""" data = await self.config.guild(ctx.guild).all() suggest_channel = ctx.guild.get_channel( await self.config.guild(ctx.guild).suggest_id() ) suggest_channel = "None" if not suggest_channel else suggest_channel.mention approve_channel = ctx.guild.get_channel( await self.config.guild(ctx.guild).approve_id() ) approve_channel = "None" if not approve_channel else approve_channel.mention reject_channel = ctx.guild.get_channel( await self.config.guild(ctx.guild).denied_id() ) reject_channel = "None" if not reject_channel else reject_channel.mention up_emoji, down_emoji = await self._get_emojis(ctx) embed = discord.Embed( colour=await ctx.embed_colour() ) embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon_url) embed.title = "**Suggestion settings:**" embed.add_field(name="Suggestions channel:", value=suggest_channel) embed.add_field(name="Approved channel:", value=approve_channel) embed.add_field(name="Denied channel:", value=reject_channel) embed.add_field(name="Upvote:", value=up_emoji) embed.add_field(name="Same channel:", value=str(data["same"])) embed.add_field(name="Downvote:", value=down_emoji) embed.add_field( name=f"Delete `{ctx.clean_prefix}suggest` upon use:", value=data["delete_suggest"], inline=False, ) embed.add_field( name="Delete suggestion upon approving/denying:", value=data["delete_suggestion"], inline=False, ) await ctx.send(embed=embed) @commands.Cog.listener() async def on_reaction_add(self, reaction, user): message = reaction.message if user.id == self.bot.user.id: return if not message.guild: return # server suggestions if message.channel.id == await self.config.guild(message.guild).suggest_id(): for message_reaction in message.reactions: if ( message_reaction.emoji != reaction.emoji and user in await message_reaction.users().flatten() ): await message_reaction.remove(user) async def _get_results(self, ctx, message): up_emoji, down_emoji = await self._get_emojis(ctx) up_count = 0 down_count = 0 for reaction in message.reactions: if reaction.emoji == up_emoji: up_count = reaction.count - 1 # minus the bot if reaction.emoji == down_emoji: down_count = reaction.count - 1 # minus the bot return f"{up_count}x {up_emoji}\n{down_count}x {down_emoji}" async def _get_emojis(self, ctx): up_emoji = self.bot.get_emoji(await self.config.guild(ctx.guild).up_emoji()) if not up_emoji: up_emoji = "✅" down_emoji = self.bot.get_emoji(await self.config.guild(ctx.guild).down_emoji()) if not down_emoji: down_emoji = "❎" return up_emoji, down_emoji async def _finish_suggestion(self, ctx, suggestion_id, approve, reason, author): server = ctx.guild.id old_channel = ctx.guild.get_channel( await self.config.guild(ctx.guild).suggest_id() ) if approve: channel = ctx.guild.get_channel( await self.config.guild(ctx.guild).approve_id() ) else: channel = ctx.guild.get_channel( await self.config.guild(ctx.guild).denied_id() ) msg_id = await self.config.custom("SUGGESTION", server, suggestion_id).msg_id() if ( msg_id != 0 and await self.config.custom("SUGGESTION", server, suggestion_id).finished() ): return await ctx.send("This suggestion has been finished already.") try: old_msg = await old_channel.fetch_message(msg_id) except discord.NotFound: return await ctx.send("Uh oh, message with this ID doesn't exist.") if not old_msg: return await ctx.send("Uh oh, message with this ID doesn't exist.") embed = old_msg.embeds[0] content = old_msg.content approved = "Approved" if approve else "Denied" embed.title = f"Suggestion {approved} (#{suggestion_id})" footer = [f"{approved} by {self.check_discrim(author)} • ({author.id})", author.display_avatar.replace(format="png", size=512)] embed.set_footer( text=footer[0], icon_url=footer[1] ) embed.add_field( name="Results", value=await self._get_results(ctx, old_msg), inline=False ) if reason: embed.add_field(name="Reason", value=reason, inline=False) await self.config.custom("SUGGESTION", server, suggestion_id).reason.set( True ) await self.config.custom("SUGGESTION", server, suggestion_id).rtext.set( reason ) if channel: if not await self.config.guild(ctx.guild).same(): if await self.config.guild(ctx.guild).delete_suggestion(): await old_msg.delete() nmsg = await channel.send(content=content, embed=embed) await self.config.custom( "SUGGESTION", server, suggestion_id ).msg_id.set(nmsg.id) else: await old_msg.edit(content=content, embed=embed) else: if not await self.config.guild(ctx.guild).same(): if await self.config.guild(ctx.guild).delete_suggestion(): await old_msg.delete() await self.config.custom( "SUGGESTION", server, suggestion_id ).msg_id.set(1) else: await old_msg.edit(content=content, embed=embed) await self.config.custom("SUGGESTION", server, suggestion_id).finished.set(True) if approve: await self.config.custom("SUGGESTION", server, suggestion_id).approved.set( True ) else: await self.config.custom("SUGGESTION", server, suggestion_id).denied.set( True ) await ctx.tick() async def _interaction_finish_suggestion(self, interaction: discord.Interaction, message: discord.Message, approve, reason = None): embed = message.embeds title = embed[0].title if not title.startswith("Suggestion #"): await interaction.response.send_message(content="This message is not a suggestion!", ephemeral=True) return numbers = re.findall(r'\d+', title) suggestion_id = ''.join(numbers) author = interaction.user server = interaction.guild.id old_channel = interaction.guild.get_channel( await self.config.guild(interaction.guild).suggest_id() ) if approve: channel = interaction.guild.get_channel( await self.config.guild(interaction.guild).approve_id() ) else: channel = interaction.guild.get_channel( await self.config.guild(interaction.guild).denied_id() ) msg_id = await self.config.custom("SUGGESTION", server, suggestion_id).msg_id() if ( msg_id != 0 and await self.config.custom("SUGGESTION", server, suggestion_id).finished() ): return await interaction.response.send_message("This suggestion has been finished already.") try: old_msg = await old_channel.fetch_message(msg_id) except discord.NotFound: return await interaction.response.send_message("Uh oh, message with this ID doesn't exist.") if not old_msg: return await interaction.response.send_message("Uh oh, message with this ID doesn't exist.") embed = old_msg.embeds[0] content = old_msg.content approved = "Approved" if approve else "Denied" embed.title = f"Suggestion {approved} (#{suggestion_id})" footer = [f"{approved} by {self.check_discrim(author)} • ({author.id})", author.display_avatar.replace(format="png", size=512)] embed.set_footer( text=footer[0], icon_url=footer[1] ) embed.add_field( name="Results", value=await self._get_results(await commands.Context.from_interaction(interaction), old_msg), inline=False ) if reason: embed.add_field(name="Reason", value=reason, inline=False) await self.config.custom("SUGGESTION", server, suggestion_id).reason.set( True ) await self.config.custom("SUGGESTION", server, suggestion_id).rtext.set( reason ) if channel: if not await self.config.guild(interaction.guild).same(): if await self.config.guild(interaction.guild).delete_suggestion(): await old_msg.delete() nmsg = await channel.send(content=content, embed=embed) await self.config.custom( "SUGGESTION", server, suggestion_id ).msg_id.set(nmsg.id) else: await old_msg.edit(content=content, embed=embed) else: if not await self.config.guild(interaction.guild).same(): if await self.config.guild(interaction.guild).delete_suggestion(): await old_msg.delete() await self.config.custom( "SUGGESTION", server, suggestion_id ).msg_id.set(1) else: await old_msg.edit(content=content, embed=embed) await self.config.custom("SUGGESTION", server, suggestion_id).finished.set(True) if approve: await self.config.custom("SUGGESTION", server, suggestion_id).approved.set( True ) else: await self.config.custom("SUGGESTION", server, suggestion_id).denied.set( True )