From 9b9e38c6cadd3550a28e19be01099b5e20ef15f3 Mon Sep 17 00:00:00 2001 From: Atmois Date: Tue, 13 Aug 2024 00:09:21 +0100 Subject: [PATCH 1/5] Create suggestion command based on the reporting command --- config/settings.json.example | 3 +- prisma/schema.prisma | 1 + tux/cogs/misc/suggestions.py | 30 ++++++++ tux/database/controllers/guild_config.py | 21 ++++++ tux/ui/modals/suggestion.py | 89 ++++++++++++++++++++++++ tux/ui/views/config.py | 20 ++++++ 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 tux/cogs/misc/suggestions.py create mode 100644 tux/ui/modals/suggestion.py diff --git a/config/settings.json.example b/config/settings.json.example index 7dfae7be..ea3f9306 100644 --- a/config/settings.json.example +++ b/config/settings.json.example @@ -25,7 +25,8 @@ "REPORTS": 1235096305160814652, "GATE": 1235096247442870292, "DEV": 1235095919788167269, - "PRIVATE": 1235108340791513129 + "PRIVATE": 1235108340791513129, + "SUGGESTIONS": 1235096305160814652 }, "EMBED_COLORS": { "DEFAULT": 16044058, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 39ab6074..d6b03538 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -56,6 +56,7 @@ model GuildConfig { join_log_id BigInt? private_log_id BigInt? report_log_id BigInt? + suggestion_log_id BigInt? dev_log_id BigInt? jail_channel_id BigInt? general_channel_id BigInt? diff --git a/tux/cogs/misc/suggestions.py b/tux/cogs/misc/suggestions.py new file mode 100644 index 00000000..a177c30f --- /dev/null +++ b/tux/cogs/misc/suggestions.py @@ -0,0 +1,30 @@ +import discord +from discord import app_commands +from discord.ext import commands + +from tux.ui.modals.suggestion import SuggestionModal + + +class Suggestion(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @app_commands.command(name="suggest") + @app_commands.guild_only() + async def suggestion(self, interaction: discord.Interaction) -> None: + """ + Submit a suggestion for a server + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command. + """ + + modal = SuggestionModal(bot=self.bot) + + await interaction.response.send_modal(modal) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Suggestion(bot)) diff --git a/tux/database/controllers/guild_config.py b/tux/database/controllers/guild_config.py index d3456a2c..a4516e4a 100644 --- a/tux/database/controllers/guild_config.py +++ b/tux/database/controllers/guild_config.py @@ -43,6 +43,7 @@ async def get_log_channel(self, guild_id: int, log_type: str) -> int | None: "join": "join_log_id", "private": "private_log_id", "report": "report_log_id", + "suggestion": "suggestion_log_id", "dev": "dev_log_id", } return await self.get_guild_config_field_value(guild_id, log_channel_ids[log_type]) @@ -116,6 +117,9 @@ async def get_private_log_channel(self, guild_id: int) -> int | None: async def get_report_log_channel(self, guild_id: int) -> int | None: return await self.get_guild_config_field_value(guild_id, "report_log_id") + async def get_suggestion_log_channel(self, guild_id: int) -> int | None: + return await self.get_guild_config_field_value(guild_id, "suggestion_log_id") + async def get_dev_log_channel(self, guild_id: int) -> int | None: return await self.get_guild_config_field_value(guild_id, "dev_log_id") @@ -256,6 +260,23 @@ async def update_report_log_id( }, ) + async def update_suggestion_log_id( + self, + guild_id: int, + suggestion_log_id: int, + ) -> GuildConfig | None: + await self.ensure_guild_exists(guild_id) + return await self.table.upsert( + where={"guild_id": guild_id}, + data={ + "create": { + "guild_id": guild_id, + "suggestion_log_id": suggestion_log_id, + }, + "update": {"suggestion_log_id": suggestion_log_id}, + }, + ) + async def update_dev_log_id( self, guild_id: int, diff --git a/tux/ui/modals/suggestion.py b/tux/ui/modals/suggestion.py new file mode 100644 index 00000000..c9e85562 --- /dev/null +++ b/tux/ui/modals/suggestion.py @@ -0,0 +1,89 @@ +import discord +from discord.ext import commands +from loguru import logger + +from tux.database.controllers import DatabaseController +from tux.utils.embeds import EmbedCreator + + +class SuggestionModal(discord.ui.Modal): + def __init__(self, *, title: str = "Submit a suggestion", bot: commands.Bot) -> None: + super().__init__(title=title) + self.bot = bot + self.config = DatabaseController().guild_config + + short = discord.ui.TextInput( # type: ignore + label="Suggestion Summary", + style=discord.TextStyle.short, + required=True, + max_length=100, + placeholder="Add an AI chatbot to Tux", + ) + + long = discord.ui.TextInput( # type: ignore + style=discord.TextStyle.long, + label="The suggestion", + required=True, + max_length=4000, + placeholder="Please provide as much detail as possible", + ) + + async def on_submit(self, interaction: discord.Interaction) -> None: + """ + Sends the suggestion to dedicated channel. + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command. + """ + + if not interaction.guild: + logger.error("Guild is None") + return + + embed = EmbedCreator.create_log_embed( + title=(f"Suggestion for {self.short.value}"), # type: ignore + description=self.long.value, # type: ignore + interaction=None, + ) + + try: + suggestion_log_channel_id = await self.config.get_suggestion_log_channel(interaction.guild.id) + except Exception as e: + logger.error(f"Failed to get suggestion log channel for guild {interaction.guild.id}. {e}") + await interaction.response.send_message( + "Failed to submit your suggestion. Please try again later.", + ephemeral=True, + delete_after=30, + ) + return + + if not suggestion_log_channel_id: + logger.error(f"Suggestion log channel not set for guild {interaction.guild.id}") + await interaction.response.send_message( + "The suggestion log channel has not been set up. Please contact an administrator.", + ephemeral=True, + delete_after=30, + ) + return + + # Get the suggestion log channel object + suggestion_log_channel = interaction.guild.get_channel(suggestion_log_channel_id) + if not suggestion_log_channel or not isinstance(suggestion_log_channel, discord.TextChannel): + logger.error(f"Failed to get suggestion log channel for guild {interaction.guild.id}") + await interaction.response.send_message( + "Failed to submit your suggestion. Please try again later.", + ephemeral=True, + delete_after=30, + ) + return + + # Send confirmation message to user + await interaction.response.send_message( + "Your suggestion has been submitted.", + ephemeral=True, + delete_after=30, + ) + + await suggestion_log_channel.send(embed=embed) diff --git a/tux/ui/views/config.py b/tux/ui/views/config.py index 1847f3f8..f2d1dfd8 100644 --- a/tux/ui/views/config.py +++ b/tux/ui/views/config.py @@ -136,6 +136,26 @@ async def _set_join_log( delete_after=30, ) + @discord.ui.select( + cls=discord.ui.ChannelSelect, + channel_types=[discord.ChannelType.text], + placeholder="Set the suggestion channel.", + ) + async def _set_suggestion_log( + self, + interaction: discord.Interaction, + select: discord.ui.ChannelSelect[Any], + ) -> None: + if interaction.guild is None: + return + + await self.db.update_suggestion_log_id(interaction.guild.id, select.values[0].id) + await interaction.response.send_message( + f"Suggestion log channel set to {select.values[0]}.", + ephemeral=True, + delete_after=30, + ) + class ConfigSetChannels(discord.ui.View): def __init__(self, *, timeout: float = 180): From 7ab2de504bc6a37e7df59b79141765297e4787e6 Mon Sep 17 00:00:00 2001 From: Atmois Date: Tue, 13 Aug 2024 06:40:16 +0100 Subject: [PATCH 2/5] Add buttons --- tux/ui/modals/suggestion.py | 59 ++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/tux/ui/modals/suggestion.py b/tux/ui/modals/suggestion.py index c9e85562..80a75c8d 100644 --- a/tux/ui/modals/suggestion.py +++ b/tux/ui/modals/suggestion.py @@ -3,7 +3,27 @@ from loguru import logger from tux.database.controllers import DatabaseController -from tux.utils.embeds import EmbedCreator +from tux.utils import checks +from tux.utils.constants import Constants as CONST +from tux.utils.embeds import create_embed_footer + + +class ButtonView(discord.ui.View): + def __init__(self, embed: discord.Embed): + super().__init__() + self.embed = embed + + @discord.ui.button(label="Accept Suggestion", style=discord.ButtonStyle.green) + @checks.has_pl(5) + async def green_button(self, interaction: discord.Interaction, button: discord.ui.Button[discord.ui.View]): + self.embed.set_author(name="Suggestion Status: Accepted") + await interaction.response.edit_message(embed=self.embed, view=self) + + @discord.ui.button(label="Deny Suggestion", style=discord.ButtonStyle.red) + @checks.has_pl(5) + async def red_button(self, interaction: discord.Interaction, button: discord.ui.Button[discord.ui.View]): + self.embed.set_author(name="Suggestion Status: Rejected") + await interaction.response.edit_message(embed=self.embed, view=self) class SuggestionModal(discord.ui.Modal): @@ -12,20 +32,20 @@ def __init__(self, *, title: str = "Submit a suggestion", bot: commands.Bot) -> self.bot = bot self.config = DatabaseController().guild_config - short = discord.ui.TextInput( # type: ignore + suggestion_title = discord.ui.TextInput( # type: ignore label="Suggestion Summary", style=discord.TextStyle.short, required=True, max_length=100, - placeholder="Add an AI chatbot to Tux", + placeholder="Summarise your suggestion briefly", ) - long = discord.ui.TextInput( # type: ignore + suggestion_description = discord.ui.TextInput( # type: ignore style=discord.TextStyle.long, - label="The suggestion", + label="Suggestion Description", required=True, max_length=4000, - placeholder="Please provide as much detail as possible", + placeholder="Please provide as much detail as possible on your suggestion", ) async def on_submit(self, interaction: discord.Interaction) -> None: @@ -42,11 +62,19 @@ async def on_submit(self, interaction: discord.Interaction) -> None: logger.error("Guild is None") return - embed = EmbedCreator.create_log_embed( - title=(f"Suggestion for {self.short.value}"), # type: ignore - description=self.long.value, # type: ignore - interaction=None, + embed = discord.Embed( + title=self.suggestion_title.value, # type: ignore + description=self.suggestion_description.value, # type: ignore + color=CONST.EMBED_COLORS["DEFAULT"], + ) + embed.set_author(name="Suggestion Status: Under Review") + embed.add_field( + name="Review Info", + value="No review has been submitted", + inline=False, ) + footer_text, footer_icon_url = create_embed_footer(interaction=interaction) + embed.set_footer(text=footer_text, icon_url=footer_icon_url) try: suggestion_log_channel_id = await self.config.get_suggestion_log_channel(interaction.guild.id) @@ -62,13 +90,12 @@ async def on_submit(self, interaction: discord.Interaction) -> None: if not suggestion_log_channel_id: logger.error(f"Suggestion log channel not set for guild {interaction.guild.id}") await interaction.response.send_message( - "The suggestion log channel has not been set up. Please contact an administrator.", + "The suggestion channel has not been set up. Please contact an administrator.", ephemeral=True, delete_after=30, ) return - # Get the suggestion log channel object suggestion_log_channel = interaction.guild.get_channel(suggestion_log_channel_id) if not suggestion_log_channel or not isinstance(suggestion_log_channel, discord.TextChannel): logger.error(f"Failed to get suggestion log channel for guild {interaction.guild.id}") @@ -79,11 +106,15 @@ async def on_submit(self, interaction: discord.Interaction) -> None: ) return - # Send confirmation message to user await interaction.response.send_message( "Your suggestion has been submitted.", ephemeral=True, delete_after=30, ) - await suggestion_log_channel.send(embed=embed) + view = ButtonView(embed=embed) + message = await suggestion_log_channel.send(embed=embed, view=view) + + reactions = ["👍", "👎"] + for reaction in reactions: + await message.add_reaction(reaction) From 3007f7a868a0c55fe9e46334b2d6787ce91de75f Mon Sep 17 00:00:00 2001 From: Atmois Date: Tue, 13 Aug 2024 06:59:26 +0100 Subject: [PATCH 3/5] Create thread with every suggestion message --- tux/ui/modals/suggestion.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tux/ui/modals/suggestion.py b/tux/ui/modals/suggestion.py index 80a75c8d..a6d12848 100644 --- a/tux/ui/modals/suggestion.py +++ b/tux/ui/modals/suggestion.py @@ -115,6 +115,12 @@ async def on_submit(self, interaction: discord.Interaction) -> None: view = ButtonView(embed=embed) message = await suggestion_log_channel.send(embed=embed, view=view) + await suggestion_log_channel.create_thread( + name=f"Suggestion: {self.suggestion_title.value}", # type: ignore + message=message, + auto_archive_duration=1440, + ) + reactions = ["👍", "👎"] for reaction in reactions: await message.add_reaction(reaction) From b35c714551f005e0973b73d2fcddcd2528af4c98 Mon Sep 17 00:00:00 2001 From: Atmois Date: Thu, 15 Aug 2024 06:31:49 +0100 Subject: [PATCH 4/5] Edit buttonview class --- tux/ui/modals/suggestion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tux/ui/modals/suggestion.py b/tux/ui/modals/suggestion.py index a6d12848..8e1c5ff4 100644 --- a/tux/ui/modals/suggestion.py +++ b/tux/ui/modals/suggestion.py @@ -10,16 +10,16 @@ class ButtonView(discord.ui.View): def __init__(self, embed: discord.Embed): - super().__init__() + super().__init__(timeout=None) self.embed = embed - @discord.ui.button(label="Accept Suggestion", style=discord.ButtonStyle.green) + @discord.ui.button(label="Accept Suggestion", style=discord.ButtonStyle.green, custom_id="accept_suggestion_button") @checks.has_pl(5) async def green_button(self, interaction: discord.Interaction, button: discord.ui.Button[discord.ui.View]): self.embed.set_author(name="Suggestion Status: Accepted") await interaction.response.edit_message(embed=self.embed, view=self) - @discord.ui.button(label="Deny Suggestion", style=discord.ButtonStyle.red) + @discord.ui.button(label="Deny Suggestion", style=discord.ButtonStyle.red, custom_id="deny_suggestion_button") @checks.has_pl(5) async def red_button(self, interaction: discord.Interaction, button: discord.ui.Button[discord.ui.View]): self.embed.set_author(name="Suggestion Status: Rejected") From c6167a9b33dec8a001126e5b9c80ddc865bbfb3e Mon Sep 17 00:00:00 2001 From: Atmois Date: Sun, 18 Aug 2024 06:03:53 +0100 Subject: [PATCH 5/5] Add in the controller and db model for suggest command, to be finished by kaizen --- prisma/schema.prisma | 21 +++++++++++ tux/bot.py | 4 +++ tux/database/controllers/__init__.py | 2 ++ tux/database/controllers/suggestion.py | 48 ++++++++++++++++++++++++++ tux/ui/modals/suggestion.py | 10 ++++-- 5 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 tux/database/controllers/suggestion.py diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d6b03538..dd57fe50 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,6 +35,12 @@ enum CaseType { UNJAIL } +enum SuggestionStatus { + APPROVED + PENDING + DENIED +} + // Docs: https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-models // Docs: https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-attributes model Guild { @@ -45,6 +51,7 @@ model Guild { notes Note[] reminders Reminder[] guild_config GuildConfig[] + suggestions Suggestion[] @@unique([guild_id]) @@index([guild_id]) @@ -139,3 +146,17 @@ model Reminder { @@unique([reminder_id, guild_id]) @@index([reminder_id, guild_id]) } + +model Suggestion { + suggestion_id BigInt @id @default(autoincrement()) + suggestion_title String + suggestion_description String + suggestion_status SuggestionStatus @default(PENDING) + suggestion_review String? + suggestion_user_id BigInt + guild_id BigInt + guild Guild @relation(fields: [guild_id], references: [guild_id]) + + @@unique([suggestion_id, guild_id]) + @@index([suggestion_id, guild_id]) +} \ No newline at end of file diff --git a/tux/bot.py b/tux/bot.py index f705ae12..44aea05c 100644 --- a/tux/bot.py +++ b/tux/bot.py @@ -1,12 +1,14 @@ import asyncio from typing import Any +import discord from discord.ext import commands from loguru import logger from tux.cog_loader import CogLoader from tux.database.client import db from tux.help import TuxHelp +from tux.ui.modals.suggestion import ButtonView class Tux(commands.Bot): @@ -53,6 +55,8 @@ async def on_ready(self) -> None: if not self.setup_task.done(): await self.setup_task + self.add_view(ButtonView(embed=discord.Embed())) + @commands.Cog.listener() async def on_disconnect(self) -> None: """ diff --git a/tux/database/controllers/__init__.py b/tux/database/controllers/__init__.py index 324ac233..b4d972ac 100644 --- a/tux/database/controllers/__init__.py +++ b/tux/database/controllers/__init__.py @@ -4,6 +4,7 @@ from .note import NoteController from .reminder import ReminderController from .snippet import SnippetController +from .suggestion import SuggestionController class DatabaseController: @@ -14,6 +15,7 @@ def __init__(self): self.reminder = ReminderController() self.guild = GuildController() self.guild_config = GuildConfigController() + self.suggestion = SuggestionController() db_controller = DatabaseController() diff --git a/tux/database/controllers/suggestion.py b/tux/database/controllers/suggestion.py new file mode 100644 index 00000000..0a3cc7a0 --- /dev/null +++ b/tux/database/controllers/suggestion.py @@ -0,0 +1,48 @@ +from prisma.enums import SuggestionStatus +from prisma.models import Guild, Suggestion +from tux.database.client import db + + +class SuggestionController: + def __init__(self) -> None: + self.table = db.suggestion + self.guild_table = db.guild + + async def ensure_guild_exists(self, guild_id: int) -> Guild: + """ + Ensure a guild exists in the database and return the found or created object. + + Parameters + ---------- + guild_id : int + The ID of the guild to ensure exists. + + Returns + ------- + Guild + The guild database object. + """ + guild = await self.guild_table.find_first(where={"guild_id": guild_id}) + if guild is None: + return await self.guild_table.create(data={"guild_id": guild_id}) + return guild + + async def create_suggestion( + self, + suggestion_title: str, + suggestion_description: str, + suggestion_status: SuggestionStatus, + suggestion_user_id: int, + guild_id: int, + ) -> Suggestion: + await self.ensure_guild_exists(guild_id) + + return await self.table.create( + data={ + "suggestion_title": suggestion_title, + "suggestion_description": suggestion_description, + "suggestion_status": suggestion_status, + "suggestion_user_id": suggestion_user_id, + "guild_id": guild_id, + }, + ) diff --git a/tux/ui/modals/suggestion.py b/tux/ui/modals/suggestion.py index 8e1c5ff4..a81951b8 100644 --- a/tux/ui/modals/suggestion.py +++ b/tux/ui/modals/suggestion.py @@ -13,16 +13,20 @@ def __init__(self, embed: discord.Embed): super().__init__(timeout=None) self.embed = embed - @discord.ui.button(label="Accept Suggestion", style=discord.ButtonStyle.green, custom_id="accept_suggestion_button") + @discord.ui.button(label="Accept", style=discord.ButtonStyle.green, custom_id="accept_suggestion_button") @checks.has_pl(5) async def green_button(self, interaction: discord.Interaction, button: discord.ui.Button[discord.ui.View]): self.embed.set_author(name="Suggestion Status: Accepted") + self.embed.add_field(name="Reviewed by", value=interaction.user.name, inline=False) + self.embed.color = discord.Color.green() await interaction.response.edit_message(embed=self.embed, view=self) - @discord.ui.button(label="Deny Suggestion", style=discord.ButtonStyle.red, custom_id="deny_suggestion_button") + @discord.ui.button(label="Deny", style=discord.ButtonStyle.red, custom_id="deny_suggestion_button") @checks.has_pl(5) async def red_button(self, interaction: discord.Interaction, button: discord.ui.Button[discord.ui.View]): self.embed.set_author(name="Suggestion Status: Rejected") + self.embed.add_field(name="Reviewed by", value=interaction.user.name, inline=False) + self.embed.color = discord.Color.red() await interaction.response.edit_message(embed=self.embed, view=self) @@ -68,11 +72,13 @@ async def on_submit(self, interaction: discord.Interaction) -> None: color=CONST.EMBED_COLORS["DEFAULT"], ) embed.set_author(name="Suggestion Status: Under Review") + """ Commented until I finish the rest of the code embed.add_field( name="Review Info", value="No review has been submitted", inline=False, ) + """ footer_text, footer_icon_url = create_embed_footer(interaction=interaction) embed.set_footer(text=footer_text, icon_url=footer_icon_url)