Skip to content

Commit 30c19b7

Browse files
author
SpectacleBot
committed
Merge remote-tracking branch 'origin/leaderboard'
2 parents a11b7b7 + 03909e6 commit 30c19b7

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""
2+
Logging service for tracking message edits and deletions.
3+
4+
This module implements a logging system that sends message delete and edit events
5+
to a designated private log channel, helping moderators track changes.
6+
"""
7+
8+
import discord
9+
from discord.ext import commands
10+
11+
from tux.core.base_cog import BaseCog
12+
from tux.core.bot import Tux
13+
from tux.ui.embeds import EmbedCreator, EmbedType
14+
15+
16+
class Logging(BaseCog):
17+
"""Discord cog for logging message events.
18+
19+
This cog monitors message deletions and edits and logs them to the
20+
configured private log channel for the guild.
21+
"""
22+
23+
@commands.Cog.listener()
24+
async def on_message_delete(self, message: discord.Message) -> None:
25+
"""Handle message delete events.
26+
27+
Parameters
28+
----------
29+
message : discord.Message
30+
The message that was deleted.
31+
"""
32+
# Skip bots, DMs, and non-guild channels to ensure channel has .mention
33+
if (
34+
message.author.bot
35+
or not message.guild
36+
or not isinstance(message.channel, discord.abc.GuildChannel)
37+
):
38+
return
39+
40+
# Skip if maintenance mode is enabled
41+
if getattr(self.bot, "maintenance_mode", False):
42+
return
43+
44+
private_log_id = await self.db.guild_config.get_private_log_id(message.guild.id)
45+
if not private_log_id:
46+
return
47+
48+
channel = message.guild.get_channel(private_log_id)
49+
if not isinstance(channel, discord.TextChannel):
50+
return
51+
52+
embed = EmbedCreator.create_embed(
53+
embed_type=EmbedType.INFO,
54+
title="Message Deleted",
55+
description=(
56+
f"**Author:** {message.author.mention} (`{message.author.id}`)\n"
57+
f"**Channel:** {message.channel.mention} (`{message.channel.id}`)\n\n"
58+
f"**Content:**\n{message.content or '*No content (attachment or embed only)*'}"
59+
),
60+
custom_author_text=str(message.author),
61+
custom_author_icon_url=message.author.display_avatar.url,
62+
message_timestamp=discord.utils.utcnow(),
63+
)
64+
65+
# Handle attachments
66+
if message.attachments:
67+
attachment_info = "\n".join(
68+
[f"[{a.filename}]({a.url})" for a in message.attachments],
69+
)
70+
embed.add_field(name="Attachments", value=attachment_info, inline=False)
71+
72+
await channel.send(embed=embed)
73+
74+
@commands.Cog.listener()
75+
async def on_message_edit(
76+
self,
77+
before: discord.Message,
78+
after: discord.Message,
79+
) -> None:
80+
"""Handle message edit events.
81+
82+
Parameters
83+
----------
84+
before : discord.Message
85+
The message state before the edit.
86+
after : discord.Message
87+
The message state after the edit.
88+
"""
89+
# Skip bots, DMs, and non-guild channels to ensure channel has .mention
90+
if (
91+
before.author.bot
92+
or not before.guild
93+
or not isinstance(before.channel, discord.abc.GuildChannel)
94+
):
95+
return
96+
97+
# Skip if content didn't change (e.g. only embeds/pins changed)
98+
if before.content == after.content:
99+
return
100+
101+
# Skip if maintenance mode is enabled
102+
if getattr(self.bot, "maintenance_mode", False):
103+
return
104+
105+
private_log_id = await self.db.guild_config.get_private_log_id(before.guild.id)
106+
if not private_log_id:
107+
return
108+
109+
channel = before.guild.get_channel(private_log_id)
110+
if not isinstance(channel, discord.TextChannel):
111+
return
112+
113+
embed = EmbedCreator.create_embed(
114+
embed_type=EmbedType.INFO,
115+
title="Message Edited",
116+
description=(
117+
f"**Author:** {before.author.mention} (`{before.author.id}`)\n"
118+
f"**Channel:** {before.channel.mention} (`{before.channel.id}`)\n"
119+
f"**Jump:** [Click here to jump]({after.jump_url})"
120+
),
121+
custom_author_text=str(before.author),
122+
custom_author_icon_url=before.author.display_avatar.url,
123+
message_timestamp=discord.utils.utcnow(),
124+
)
125+
126+
# Truncate content to fit in embed fields (max 1024 chars)
127+
def truncate(text: str) -> str:
128+
return (text[:1021] + "...") if len(text) > 1024 else text
129+
130+
embed.add_field(
131+
name="Before",
132+
value=truncate(before.content or "*No content*"),
133+
inline=False,
134+
)
135+
embed.add_field(
136+
name="After",
137+
value=truncate(after.content or "*No content*"),
138+
inline=False,
139+
)
140+
141+
await channel.send(embed=embed)
142+
143+
144+
async def setup(bot: Tux) -> None:
145+
"""Set up the Logging cog.
146+
147+
Parameters
148+
----------
149+
bot : Tux
150+
The bot instance.
151+
"""
152+
await bot.add_cog(Logging(bot))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Custom plugins package for user-defined extensions.
2+
3+
This package is intended for custom modules created by self-hosters.
4+
Modules placed here will be automatically discovered and loaded by the bot.
5+
"""
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Spectacle Studios Discord Servers - XP Leaderboard Plugin."""
2+
3+
import discord
4+
from discord import app_commands
5+
from discord.ext import commands
6+
7+
from tux.core.base_cog import BaseCog
8+
from tux.core.bot import Tux
9+
from tux.modules.features.levels import LevelsService
10+
from tux.ui.embeds import EmbedCreator, EmbedType
11+
12+
13+
class Leaderboard(BaseCog):
14+
"""Manage and expose the XP leaderboard functionality.
15+
16+
This cog provides commands that allow users to view XP rankings and related
17+
leaderboard information within the Discord server.
18+
"""
19+
20+
def __init__(self, bot: Tux) -> None:
21+
super().__init__(bot)
22+
23+
self.levels_service = LevelsService(bot)
24+
25+
@commands.command(name="leaderboard", aliases=["lb", "top"])
26+
@app_commands.guild_only()
27+
async def leaderboard(self, ctx: commands.Context[Tux]) -> None:
28+
"""Show the XP leaderboard for the current Discord server.
29+
30+
This command responds with a placeholder message until the leaderboard
31+
functionality is fully implemented.
32+
"""
33+
await ctx.defer()
34+
35+
top_members = await self.levels_service.db.levels.get_top_members(0, 10)
36+
embed = EmbedCreator.create_embed(
37+
embed_type=EmbedType.INFO,
38+
title="XP Leaderboard",
39+
message_timestamp=discord.utils.utcnow(),
40+
)
41+
for member in top_members:
42+
try:
43+
user = await self.bot.fetch_user(member.member_id)
44+
embed.add_field(
45+
name=f"{user.name}",
46+
value=f"{int(member.xp):,d}",
47+
inline=False,
48+
)
49+
except discord.NotFound:
50+
embed.add_field(
51+
name="Unknown user",
52+
value=f"{int(member.xp):,d}",
53+
inline=False,
54+
)
55+
56+
await ctx.send(embed=embed)
57+
58+
59+
async def setup(bot: Tux) -> None:
60+
"""Register the leaderboard cog with the bot.
61+
62+
This function initializes the leaderboard plugin and adds it to the running bot instance.
63+
"""
64+
await bot.add_cog(Leaderboard(bot))

0 commit comments

Comments
 (0)