Skip to content

Commit 3298959

Browse files
committed
feat: enhance service cogs with improved functionality
- Add new reminders service - Update levels service integration - Improve influxdb logging capabilities - Enhance gif limiter and status roles
1 parent 702458f commit 3298959

File tree

5 files changed

+289
-63
lines changed

5 files changed

+289
-63
lines changed

tux/cogs/services/gif_limiter.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import discord
66
from discord.ext import commands, tasks
7+
from loguru import logger
78

89
from tux.bot import Tux
910
from tux.utils.config import CONFIG
@@ -120,7 +121,7 @@ async def on_message(self, message: discord.Message) -> None:
120121
if await self._should_process_message(message):
121122
await self._handle_gif_message(message)
122123

123-
@tasks.loop(seconds=20)
124+
@tasks.loop(seconds=20, name="old_gif_remover")
124125
async def old_gif_remover(self) -> None:
125126
"""
126127
Regularly cleans old GIF timestamps
@@ -143,6 +144,23 @@ async def old_gif_remover(self) -> None:
143144
else:
144145
del self.recent_gifs_by_user[user_id]
145146

147+
@old_gif_remover.before_loop
148+
async def before_old_gif_remover(self) -> None:
149+
"""Wait until the bot is ready."""
150+
await self.bot.wait_until_ready()
151+
152+
@old_gif_remover.error
153+
async def on_old_gif_remover_error(self, error: BaseException) -> None:
154+
"""Handles errors in the old_gif_remover loop."""
155+
logger.error(f"Error in old_gif_remover loop: {error}")
156+
157+
if isinstance(error, Exception):
158+
self.bot.sentry_manager.capture_exception(error)
159+
else:
160+
# For BaseExceptions that are not Exceptions (like KeyboardInterrupt),
161+
# it's often better to let them propagate.
162+
raise error
163+
146164
async def cog_unload(self) -> None:
147165
"""Cancel the background task when the cog is unloaded."""
148166
self.old_gif_remover.cancel()

tux/cogs/services/influxdblogger.py

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any
22

3+
import discord
34
from discord.ext import commands, tasks
45
from influxdb_client.client.influxdb_client import InfluxDBClient
56
from influxdb_client.client.write.point import Point
@@ -12,13 +13,14 @@
1213

1314

1415
class InfluxLogger(commands.Cog):
15-
def __init__(self, bot: Tux):
16+
def __init__(self, bot: Tux) -> None:
1617
self.bot = bot
1718
self.db = DatabaseController()
1819
self.influx_write_api: Any | None = None
1920
self.influx_org: str = ""
2021

2122
if self.init_influx():
23+
self._log_guild_stats.start()
2224
self.logger.start()
2325
else:
2426
logger.warning("InfluxDB logger failed to init. Check .env configuration if you want to use it.")
@@ -42,7 +44,46 @@ def init_influx(self) -> bool:
4244
return True
4345
return False
4446

45-
@tasks.loop(seconds=60)
47+
@tasks.loop(seconds=60, name="influx_guild_stats")
48+
async def _log_guild_stats(self) -> None:
49+
"""Logs guild statistics to InfluxDB."""
50+
if not self.bot.is_ready() or not self.influx_write_api:
51+
logger.debug("Bot not ready or InfluxDB writer not initialized, skipping InfluxDB logging.")
52+
return
53+
54+
for guild in self.bot.guilds:
55+
online_members = sum(m.status != discord.Status.offline for m in guild.members)
56+
57+
tags = {"guild": guild.name}
58+
fields = {
59+
"members": guild.member_count,
60+
"online": online_members,
61+
}
62+
63+
point = {"measurement": "guild_stats", "tags": tags, "fields": fields}
64+
65+
self.influx_write_api.write(bucket="tux_stats", org=self.influx_org, record=point)
66+
67+
@_log_guild_stats.before_loop
68+
async def before_log_guild_stats(self) -> None:
69+
"""Wait until the bot is ready."""
70+
await self.bot.wait_until_ready()
71+
72+
@_log_guild_stats.error
73+
async def on_log_guild_stats_error(self, error: BaseException) -> None:
74+
"""Handles errors in the guild stats logging loop."""
75+
logger.error(f"Error in InfluxDB guild stats logger loop: {error}")
76+
if isinstance(error, Exception):
77+
self.bot.sentry_manager.capture_exception(error)
78+
else:
79+
raise error
80+
81+
async def cog_unload(self) -> None:
82+
if self.influx_write_api:
83+
self._log_guild_stats.cancel()
84+
self.logger.cancel()
85+
86+
@tasks.loop(seconds=60, name="influx_db_logger")
4687
async def logger(self) -> None:
4788
"""Log statistics to InfluxDB at regular intervals.
4889
@@ -55,40 +96,54 @@ async def logger(self) -> None:
5596
influx_bucket = "tux stats"
5697

5798
# Collect the guild list from the database
58-
try:
59-
guild_list = await self.db.guild.find_many(where={})
99+
guild_list = await self.db.guild.find_many(where={})
100+
101+
# Iterate through each guild and collect metrics
102+
for guild in guild_list:
103+
if not guild.guild_id:
104+
continue
60105

61-
# Iterate through each guild and collect metrics
62-
for guild in guild_list:
63-
if not guild.guild_id:
64-
continue
106+
guild_id = int(guild.guild_id)
65107

66-
guild_id = int(guild.guild_id)
108+
# Collect data by querying controllers
109+
starboard_stats = await self.db.starboard_message.find_many(where={"message_guild_id": guild_id})
67110

68-
# Collect data by querying controllers
69-
starboard_stats = await self.db.starboard_message.find_many(where={"message_guild_id": guild_id})
111+
snippet_stats = await self.db.snippet.find_many(where={"guild_id": guild_id})
70112

71-
snippet_stats = await self.db.snippet.find_many(where={"guild_id": guild_id})
113+
afk_stats = await self.db.afk.find_many(where={"guild_id": guild_id})
72114

73-
afk_stats = await self.db.afk.find_many(where={"guild_id": guild_id})
115+
case_stats = await self.db.case.find_many(where={"guild_id": guild_id})
74116

75-
case_stats = await self.db.case.find_many(where={"guild_id": guild_id})
117+
# Create data points with type ignores for InfluxDB methods
118+
# The InfluxDB client's type hints are incomplete
119+
points: list[Point] = [
120+
Point("guild stats").tag("guild", guild_id).field("starboard count", len(starboard_stats)), # type: ignore
121+
Point("guild stats").tag("guild", guild_id).field("snippet count", len(snippet_stats)), # type: ignore
122+
Point("guild stats").tag("guild", guild_id).field("afk count", len(afk_stats)), # type: ignore
123+
Point("guild stats").tag("guild", guild_id).field("case count", len(case_stats)), # type: ignore
124+
]
76125

77-
# Create data points with type ignores for InfluxDB methods
78-
# The InfluxDB client's type hints are incomplete
79-
points: list[Point] = [
80-
Point("guild stats").tag("guild", guild_id).field("starboard count", len(starboard_stats)), # type: ignore
81-
Point("guild stats").tag("guild", guild_id).field("snippet count", len(snippet_stats)), # type: ignore
82-
Point("guild stats").tag("guild", guild_id).field("afk count", len(afk_stats)), # type: ignore
83-
Point("guild stats").tag("guild", guild_id).field("case count", len(case_stats)), # type: ignore
84-
]
126+
# Write to InfluxDB
127+
self.influx_write_api.write(bucket=influx_bucket, org=self.influx_org, record=points)
85128

86-
# Write to InfluxDB
87-
self.influx_write_api.write(bucket=influx_bucket, org=self.influx_org, record=points)
129+
@logger.before_loop
130+
async def before_logger(self) -> None:
131+
"""Wait until the bot is ready."""
132+
await self.bot.wait_until_ready()
88133

89-
except Exception as e:
90-
logger.error(f"Error collecting metrics for InfluxDB: {e}")
134+
@logger.error
135+
async def on_logger_error(self, error: BaseException) -> None:
136+
"""Handles errors in the logger loop."""
137+
logger.error(f"Error in InfluxDB logger loop: {error}")
138+
if isinstance(error, Exception):
139+
self.bot.sentry_manager.capture_exception(error)
140+
else:
141+
raise error
91142

92143

93144
async def setup(bot: Tux) -> None:
94-
await bot.add_cog(InfluxLogger(bot))
145+
# Only load the cog if InfluxDB configuration is available
146+
if all([CONFIG.INFLUXDB_TOKEN, CONFIG.INFLUXDB_URL, CONFIG.INFLUXDB_ORG]):
147+
await bot.add_cog(InfluxLogger(bot))
148+
else:
149+
logger.warning("InfluxDB configuration incomplete, skipping InfluxLogger cog")

tux/cogs/services/levels.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from discord.ext import commands
66
from loguru import logger
77

8-
from tux.app import get_prefix
8+
from prisma.models import Levels
99
from tux.bot import Tux
1010
from tux.database.controllers import DatabaseController
1111
from tux.ui.embeds import EmbedCreator
@@ -33,20 +33,40 @@ async def xp_listener(self, message: discord.Message) -> None:
3333
message : discord.Message
3434
The message object.
3535
"""
36-
if message.author.bot or message.guild is None or message.channel.id in CONFIG.XP_BLACKLIST_CHANNELS:
36+
if message.author.bot or not message.guild or message.channel.id in CONFIG.XP_BLACKLIST_CHANNELS:
3737
return
3838

39-
prefixes = await get_prefix(self.bot, message)
40-
if any(message.content.startswith(prefix) for prefix in prefixes):
39+
# Ignore messages that are commands
40+
ctx = await self.bot.get_context(message)
41+
if ctx.valid:
4142
return
4243

44+
# Fetch member object
4345
member = message.guild.get_member(message.author.id)
44-
if member is None:
46+
if not member:
4547
return
4648

47-
await self.process_xp_gain(member, message.guild)
49+
# Fetch all user level data in one query to minimize DB calls
50+
user_level_data = await self.db.levels.get_user_level_data(member.id, message.guild.id)
4851

49-
async def process_xp_gain(self, member: discord.Member, guild: discord.Guild) -> None:
52+
# Check if the user is blacklisted
53+
if user_level_data and user_level_data.blacklisted:
54+
return
55+
56+
# Check if the user is on cooldown
57+
last_message_time = user_level_data.last_message if user_level_data else None
58+
if last_message_time and self.is_on_cooldown(last_message_time):
59+
return
60+
61+
# Process XP gain with the already fetched data
62+
await self.process_xp_gain(member, message.guild, user_level_data)
63+
64+
async def process_xp_gain(
65+
self,
66+
member: discord.Member,
67+
guild: discord.Guild,
68+
user_level_data: Levels | None,
69+
) -> None:
5070
"""
5171
Processes XP gain for a member.
5272
@@ -56,17 +76,11 @@ async def process_xp_gain(self, member: discord.Member, guild: discord.Guild) ->
5676
The member gaining XP.
5777
guild : discord.Guild
5878
The guild where the member is gaining XP.
79+
user_level_data : Levels | None
80+
The existing level data for the user.
5981
"""
60-
# Get blacklist status
61-
is_blacklisted = await self.db.levels.is_blacklisted(member.id, guild.id)
62-
if is_blacklisted:
63-
return
64-
65-
last_message_time = await self.db.levels.get_last_message_time(member.id, guild.id)
66-
if last_message_time and self.is_on_cooldown(last_message_time):
67-
return
68-
69-
current_xp, current_level = await self.db.levels.get_xp_and_level(member.id, guild.id)
82+
current_xp = user_level_data.xp if user_level_data else 0.0
83+
current_level = user_level_data.level if user_level_data else 0
7084

7185
xp_increment = self.calculate_xp_increment(member)
7286
new_xp = current_xp + xp_increment

0 commit comments

Comments
 (0)