Skip to content

Suggestion Command Draft PR #869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/settings.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"REPORTS": 1235096305160814652,
"GATE": 1235096247442870292,
"DEV": 1235095919788167269,
"PRIVATE": 1235108340791513129
"PRIVATE": 1235108340791513129,
"SUGGESTIONS": 1235096305160814652
},
"EMBED_COLORS": {
"DEFAULT": 16044058,
Expand Down
22 changes: 22 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -45,6 +51,7 @@ model Guild {
notes Note[]
reminders Reminder[]
guild_config GuildConfig[]
suggestions Suggestion[]

@@unique([guild_id])
@@index([guild_id])
Expand All @@ -56,6 +63,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?
Expand Down Expand Up @@ -138,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])
}
4 changes: 4 additions & 0 deletions tux/bot.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be in bot.py?

Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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:
"""
Expand Down
30 changes: 30 additions & 0 deletions tux/cogs/misc/suggestions.py
Original file line number Diff line number Diff line change
@@ -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))
2 changes: 2 additions & 0 deletions tux/database/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .note import NoteController
from .reminder import ReminderController
from .snippet import SnippetController
from .suggestion import SuggestionController


class DatabaseController:
Expand All @@ -14,3 +15,4 @@ def __init__(self):
self.reminder = ReminderController()
self.guild = GuildController()
self.guild_config = GuildConfigController()
self.suggestion = SuggestionController()
23 changes: 22 additions & 1 deletion tux/database/controllers/guild_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -116,7 +117,10 @@ async def get_private_log_id(self, guild_id: int) -> int | None:
async def get_report_log_id(self, guild_id: int) -> int | None:
return await self.get_guild_config_field_value(guild_id, "report_log_id")

async def get_dev_log_id(self, guild_id: int) -> int | None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this rename necessary?

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")

async def get_jail_channel_id(self, guild_id: int) -> int | None:
Expand Down Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions tux/database/controllers/suggestion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from prisma.enums import SuggestionStatus
from prisma.models import Guild, Suggestion
from tux.database.client import db


class SuggestionController:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This controller should probably have a method for retrieving suggestions and maybe even searching them.
create_suggestion should be an upsert method, rather than a pure insert.

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,
},
)
132 changes: 132 additions & 0 deletions tux/ui/modals/suggestion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import discord
from discord.ext import commands
from loguru import logger

from tux.database.controllers import DatabaseController
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__(timeout=None)
self.embed = embed

@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", 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)


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

suggestion_title = discord.ui.TextInput( # type: ignore
label="Suggestion Summary",
style=discord.TextStyle.short,
required=True,
max_length=100,
placeholder="Summarise your suggestion briefly",
)

suggestion_description = discord.ui.TextInput( # type: ignore
style=discord.TextStyle.long,
label="Suggestion Description",
required=True,
max_length=4000,
placeholder="Please provide as much detail as possible on your suggestion",
)

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 = 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")
""" 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)

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 channel has not been set up. Please contact an administrator.",
ephemeral=True,
delete_after=30,
)
return

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

await interaction.response.send_message(
"Your suggestion has been submitted.",
ephemeral=True,
delete_after=30,
)

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 = ["👍", "👎"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to constants file, or ideally make configurable.

for reaction in reactions:
await message.add_reaction(reaction)
20 changes: 20 additions & 0 deletions tux/ui/views/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down