505 lines
No EOL
20 KiB
Python
505 lines
No EOL
20 KiB
Python
import discord
|
|
import datetime
|
|
import typing
|
|
|
|
from redbot.core import Config, checks, commands
|
|
from redbot.core.utils.chat_formatting import humanize_list
|
|
|
|
from redbot.core.bot import Red
|
|
|
|
|
|
class Suggestions(commands.Cog):
|
|
"""
|
|
Per guild, as well as global, suggestion box voting system.
|
|
"""
|
|
|
|
__version__ = "1.7.1"
|
|
|
|
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,
|
|
)
|
|
|
|
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}\n\nVersion: {self.__version__}"
|
|
|
|
@commands.command()
|
|
@commands.guild_only()
|
|
@checks.bot_has_permissions(add_reactions=True)
|
|
async def suggest(self, ctx: commands.Context, *, suggestion: str):
|
|
"""Suggest something."""
|
|
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 {ctx.author.name}#{ctx.author.discriminator}",
|
|
ctx.author.avatar_url]
|
|
author = [f"{ctx.author.name}#{ctx.author.discriminator} ({ctx.author.id})", ctx.author.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 {author.name}#{author.discriminator} ({author.id}",
|
|
author.avatar_url_as(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."""
|
|
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!hjo8")
|
|
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."""
|
|
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="upemoji")
|
|
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="downemoji")
|
|
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 (guild):__**"
|
|
|
|
embed.set_footer(text="*required to function properly")
|
|
embed.add_field(name="Same channel*:", value=str(data["same"]), inline=False)
|
|
embed.add_field(name="Suggest 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="Up emoji:", value=up_emoji)
|
|
embed.add_field(name="Down emoji:", 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 {author.name}#{author.discriminator} ({author.id}",
|
|
author.avatar_url_as(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() |