GalaxyCogs/suggestions/suggestions.py
2023-09-21 13:01:42 -04:00

677 lines
28 KiB
Python

import re
import typing
import discord
from redbot.core import Config, app_commands, checks, commands
from redbot.core.bot import Red
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.command(name='suggest')
@commands.guild_only()
@checks.bot_has_permissions(add_reactions=True)
async def suggest(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()
@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_get_results(self, interaction: discord.Interaction, message):
up_emoji, down_emoji = await self._get_emojis(interaction)
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 _interaction_get_emojis(self, interaction: discord.Interaction):
up_emoji = self.bot.get_emoji(await self.config.guild(interaction.guild).up_emoji())
if not up_emoji:
up_emoji = "✅"
down_emoji = self.bot.get_emoji(await self.config.guild(interaction.guild).down_emoji())
if not down_emoji:
down_emoji = "❎"
return up_emoji, down_emoji
async def _interaction_finish_suggestion(self, interaction: discord.Interaction, message: discord.Message, approve: bool, reason: str = 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(content="This suggestion has been finished already.", ephemeral=True)
try:
old_msg = await old_channel.fetch_message(msg_id)
except discord.NotFound:
return await interaction.response.send_message(content="Uh oh, message with this ID doesn't exist.", ephemeral=True)
if not old_msg:
return await interaction.response.send_message(content="Uh oh, message with this ID doesn't exist.", ephemeral=True)
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._interaction_get_results(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
)
class SuggestionApproveModal(discord.ui.Modal, title="Approving suggestion..."):
def __init__(self, message):
super().__init__()
self.message: discord.Message = message
reason = discord.ui.TextInput(
label="Approval Reason",
placeholder="Why are you approving this suggestion?",
style=discord.TextStyle.paragraph,
required=False,
max_length=1024
)
async def on_submit(self, interaction: discord.Interaction):
cog = interaction.client.get_cog('Suggestions')
if self.reason.value != "":
await Suggestions._interaction_finish_suggestion(cog, interaction, self.message, True, self.reason.value)
else:
await Suggestions._interaction_finish_suggestion(cog, interaction, self.message, True, None)
msg = await interaction.response.send_message(content="Suggestion approved!", ephemeral=True)
await msg.delete(10)
class SuggestionDenyModal(discord.ui.Modal, title="Denying suggestion..."):
def __init__(self, message):
super().__init__()
self.message: discord.Message = message
reason = discord.ui.TextInput(
label="Denial Reason",
placeholder="Why are you denying this suggestion?",
style=discord.TextStyle.paragraph,
required=False,
max_length=1024
)
async def on_submit(self, interaction: discord.Interaction):
cog = interaction.client.get_cog('Suggestions')
if self.reason.value != "":
await Suggestions._interaction_finish_suggestion(cog, interaction, self.message, False, self.reason.value)
else:
await Suggestions._interaction_finish_suggestion(cog, interaction, self.message, False, None)
msg: discord.Message = await interaction.response.send_message(content="Suggestion denied!", ephemeral=True)
await msg.delete(10)
@app_commands.context_menu(name="Approve Suggestion")
async def approve_context(interaction: discord.Interaction, message: discord.Message):
if message.author.id == interaction.client.user.id and len(message.embeds) == 1 and message.embeds[0].title.startswith('Suggestion #'):
await interaction.response.send_modal(SuggestionApproveModal(message))
else:
await interaction.response.send_message(content="This is not a suggestion!", ephemeral=True)
@app_commands.context_menu(name="Deny Suggestion")
async def deny_context(interaction: discord.Interaction, message: discord.Message):
if message.author.id == interaction.client.user.id and len(message.embeds) == 1 and message.embeds[0].title.startswith('Suggestion #'):
await interaction.response.send_modal(SuggestionDenyModal(message))
else:
await interaction.response.send_message(content="This is not a suggestion!", ephemeral=True)