Skip to content

Commit 2e2a3b2

Browse files
committed
refactor: First starboard prototype
1 parent a6c8aaa commit 2e2a3b2

File tree

12 files changed

+494
-51
lines changed

12 files changed

+494
-51
lines changed

poetry.lock

+12-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prisma/schema.prisma

+25-8
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,15 @@ enum CaseType {
4040
// Docs: https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-models
4141
// Docs: https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-attributes
4242
model Guild {
43-
guild_id BigInt @id
44-
guild_joined_at DateTime? @default(now())
45-
cases Case[]
46-
snippets Snippet[]
47-
notes Note[]
48-
reminders Reminder[]
49-
guild_config GuildConfig[]
50-
Starboard Starboard[]
43+
guild_id BigInt @id
44+
guild_joined_at DateTime? @default(now())
45+
cases Case[]
46+
snippets Snippet[]
47+
notes Note[]
48+
reminders Reminder[]
49+
guild_config GuildConfig[]
50+
Starboard Starboard[]
51+
StarboardMessage StarboardMessage[]
5152
5253
@@unique([guild_id])
5354
@@index([guild_id])
@@ -154,3 +155,19 @@ model Starboard {
154155
@@unique([guild_id])
155156
@@index([guild_id])
156157
}
158+
159+
model StarboardMessage {
160+
message_id BigInt @id
161+
message_content String
162+
message_created_at DateTime @default(now())
163+
message_expires_at DateTime
164+
message_channel_id BigInt
165+
message_user_id BigInt
166+
message_guild_id BigInt
167+
star_count Int @default(0)
168+
starboard_message_id BigInt
169+
guild Guild @relation(fields: [message_guild_id], references: [guild_id])
170+
171+
@@unique([message_id, message_guild_id])
172+
@@index([message_id, message_guild_id])
173+
}

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ sentry-sdk = {extras = ["httpx", "loguru"], version = "^2.7.0"}
3737
types-aiofiles = "^24.1.0.20240626"
3838
types-psutil = "^6.0.0.20240621"
3939
typing-extensions = "^4.12.2"
40+
emojis = "^0.7.0"
4041

4142
[tool.poetry.group.docs.dependencies]
4243
mkdocs-material = "^9.5.30"

tux/cogs/starboard/starboard.py

+216
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
from datetime import UTC, datetime, timedelta
2+
3+
import discord
4+
import emojis
5+
from discord.ext import commands
6+
from loguru import logger
7+
8+
from tux.bot import Tux
9+
from tux.database.controllers.starboard import StarboardController, StarboardMessageController
10+
from tux.utils import checks
11+
12+
13+
class Starboard(commands.Cog):
14+
def __init__(self, bot: Tux) -> None:
15+
self.bot = bot
16+
self.starboard_controller = StarboardController()
17+
self.starboard_message_controller = StarboardMessageController()
18+
19+
@commands.hybrid_group(
20+
name="starboard",
21+
usage="starboard <subcommand>",
22+
description="Configure the starboard for this server",
23+
)
24+
@commands.guild_only()
25+
@checks.has_pl(7) # server owner only
26+
async def starboard(self, ctx: commands.Context[Tux]) -> None:
27+
if ctx.invoked_subcommand is None:
28+
await ctx.send_help("starboard")
29+
30+
@starboard.command(
31+
name="setup",
32+
aliases=["s"],
33+
usage="starboard setup <channel> <emoji> <threshold>",
34+
description="Configure the starboard for this server",
35+
)
36+
@commands.has_permissions(manage_guild=True)
37+
async def configure_starboard(
38+
self,
39+
ctx: commands.Context[Tux],
40+
channel: discord.TextChannel,
41+
emoji: str,
42+
threshold: int,
43+
) -> None:
44+
"""
45+
Configure the starboard for this server.
46+
47+
Parameters
48+
----------
49+
channel: discord.TextChannel
50+
The channel to configure the starboard for
51+
emoji: str
52+
The emoji to use for the starboard
53+
threshold: int
54+
The threshold for the starboard
55+
"""
56+
if not ctx.guild:
57+
await ctx.send("This command can only be used in a server.")
58+
return
59+
60+
try:
61+
if not emojis.count(emoji, unique=True) or emojis.count(emoji, unique=True) > 1: # type: ignore
62+
await ctx.send("Invalid emoji. Please use a single default Discord emoji.")
63+
return
64+
65+
if threshold < 1:
66+
await ctx.send("Threshold must be at least 1.")
67+
return
68+
69+
if not channel.permissions_for(ctx.guild.me).send_messages:
70+
await ctx.send(f"I don't have permission to send messages in {channel.mention}.")
71+
return
72+
73+
await self.starboard_controller.create_or_update_starboard(ctx.guild.id, channel.id, emoji, threshold)
74+
await ctx.send(
75+
f"Starboard configured successfully. Channel: {channel.mention}, Emoji: {emoji}, Threshold: {threshold}",
76+
)
77+
except Exception as e:
78+
logger.error(f"Error configuring starboard: {e!s}")
79+
await ctx.send(f"An error occurred while configuring the starboard: {e!s}")
80+
81+
@starboard.command(
82+
name="remove",
83+
aliases=["r"],
84+
usage="starboard remove",
85+
description="Remove the starboard configuration for this server",
86+
)
87+
@commands.has_permissions(manage_guild=True)
88+
async def remove_starboard(self, ctx: commands.Context[Tux]) -> None:
89+
if not ctx.guild:
90+
await ctx.send("This command can only be used in a server.")
91+
return
92+
93+
try:
94+
result = await self.starboard_controller.delete_starboard_by_guild_id(ctx.guild.id)
95+
if result:
96+
await ctx.send("Starboard configuration removed successfully.")
97+
else:
98+
await ctx.send("No starboard configuration found for this server.")
99+
except Exception as e:
100+
logger.error(f"Error removing starboard configuration: {e!s}")
101+
await ctx.send(f"An error occurred while removing the starboard configuration: {e!s}")
102+
103+
@commands.Cog.listener("on_reaction_add")
104+
async def starboard_check(self, reaction: discord.Reaction, user: discord.User) -> None:
105+
if not reaction.message.guild:
106+
return
107+
108+
try:
109+
starboard = await self.starboard_controller.get_starboard_by_guild_id(reaction.message.guild.id)
110+
if not starboard:
111+
return
112+
113+
if str(reaction.emoji) != starboard.starboard_emoji:
114+
logger.debug(
115+
f"Reaction emoji {reaction.emoji} does not match starboard emoji {starboard.starboard_emoji}",
116+
)
117+
return
118+
119+
# # Check if the user is not the author of the message
120+
# if user.id == reaction.message.author.id:
121+
# logger.debug(f"User {user.id} tried to star their own message")
122+
# return
123+
124+
reaction_count = sum(
125+
r.count for r in reaction.message.reactions if str(r.emoji) == starboard.starboard_emoji
126+
)
127+
128+
if reaction_count >= starboard.starboard_threshold:
129+
starboard_channel = reaction.message.guild.get_channel(starboard.starboard_channel_id)
130+
logger.info(f"Starboard channel: {starboard_channel}")
131+
132+
if not isinstance(starboard_channel, discord.TextChannel):
133+
logger.error(
134+
f"Starboard channel {starboard.starboard_channel_id} not found or is not a text channel",
135+
)
136+
return
137+
138+
await self.create_or_update_starboard_message(starboard_channel, reaction.message, reaction_count)
139+
except Exception as e:
140+
logger.error(f"Error in starboard_check: {e!s}")
141+
142+
async def get_existing_starboard_message(
143+
self,
144+
starboard_channel: discord.TextChannel,
145+
original_message: discord.Message,
146+
) -> discord.Message | None:
147+
assert original_message.guild
148+
try:
149+
starboard_message = await self.starboard_message_controller.get_starboard_message_by_id(
150+
original_message.id,
151+
original_message.guild.id,
152+
)
153+
logger.info(f"Starboard message: {starboard_message}")
154+
if starboard_message:
155+
return await starboard_channel.fetch_message(starboard_message.starboard_message_id)
156+
except Exception as e:
157+
logger.error(f"Error while fetching starboard message: {e!s}")
158+
159+
return None
160+
161+
async def create_or_update_starboard_message(
162+
self,
163+
starboard_channel: discord.TextChannel,
164+
original_message: discord.Message,
165+
reaction_count: int,
166+
) -> None:
167+
if not original_message.guild:
168+
logger.error("Original message has no guild")
169+
return
170+
171+
try:
172+
starboard = await self.starboard_controller.get_starboard_by_guild_id(original_message.guild.id)
173+
if not starboard:
174+
logger.error(f"No starboard configuration found for guild {original_message.guild.id}")
175+
return
176+
177+
embed = discord.Embed(
178+
description=original_message.content,
179+
color=discord.Color.gold(),
180+
timestamp=original_message.created_at,
181+
)
182+
embed.set_author(
183+
name=original_message.author.display_name,
184+
icon_url=original_message.author.avatar.url if original_message.author.avatar else None,
185+
)
186+
embed.add_field(name="Source", value=f"[Jump to message]({original_message.jump_url})")
187+
embed.set_footer(text=f"Star count: {reaction_count} {starboard.starboard_emoji}")
188+
189+
if original_message.attachments:
190+
embed.set_image(url=original_message.attachments[0].url)
191+
192+
starboard_message = await self.get_existing_starboard_message(starboard_channel, original_message)
193+
194+
if starboard_message:
195+
await starboard_message.edit(embed=embed)
196+
else:
197+
starboard_message = await starboard_channel.send(embed=embed)
198+
199+
# Create or update the starboard message entry in the database
200+
await self.starboard_message_controller.create_or_update_starboard_message(
201+
message_id=original_message.id,
202+
message_content=original_message.content,
203+
message_expires_at=datetime.now(UTC) + timedelta(days=30),
204+
message_channel_id=original_message.channel.id,
205+
message_user_id=original_message.author.id,
206+
message_guild_id=original_message.guild.id,
207+
star_count=reaction_count,
208+
starboard_message_id=starboard_message.id,
209+
)
210+
211+
except Exception as e:
212+
logger.error(f"Error while creating or updating starboard message: {e!s}")
213+
214+
215+
async def setup(bot: Tux) -> None:
216+
await bot.add_cog(Starboard(bot))

tux/cogs/starboard/test.py

-40
This file was deleted.

tux/database/controllers/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .note import NoteController
55
from .reminder import ReminderController
66
from .snippet import SnippetController
7-
from .starboard import StarboardController
7+
from .starboard import StarboardController, StarboardMessageController
88

99

1010
class DatabaseController:
@@ -16,3 +16,4 @@ def __init__(self):
1616
self.guild = GuildController()
1717
self.guild_config = GuildConfigController()
1818
self.starboard = StarboardController()
19+
self.starboard_message = StarboardMessageController()

0 commit comments

Comments
 (0)