diff --git a/tux/cogs/moderation/tempban.py b/tux/cogs/moderation/tempban.py index 4641de854..2b5e70045 100644 --- a/tux/cogs/moderation/tempban.py +++ b/tux/cogs/moderation/tempban.py @@ -1,4 +1,5 @@ from datetime import UTC, datetime, timedelta +from typing import TYPE_CHECKING import discord from discord.ext import commands, tasks @@ -13,13 +14,16 @@ from . import ModerationCogBase +if TYPE_CHECKING: + from tux.bot import Tux + class TempBan(ModerationCogBase): def __init__(self, bot: Tux) -> None: super().__init__(bot) self.tempban.usage = generate_usage(self.tempban, TempBanFlags) self._processing_tempbans = False # Lock to prevent overlapping task runs - self.tempban_check.start() + self.check_tempbans.start() @commands.hybrid_command(name="tempban", aliases=["tb"]) @commands.guild_only() @@ -152,53 +156,51 @@ async def _process_tempban_case(self, case: Case) -> tuple[int, int]: return processed_count, failed_count - @tasks.loop(minutes=1) - async def tempban_check(self) -> None: - """ - Check for expired tempbans at a set interval and unban the user if the ban has expired. - - Uses a simple locking mechanism to prevent overlapping executions. - Processes bans in smaller batches to prevent timeout issues. - - Raises - ------ - Exception - If an error occurs while checking for expired tempbans. - """ - # Skip if already processing + @tasks.loop(minutes=1, name="tempban_checker") + async def check_tempbans(self) -> None: + """Checks for expired tempbans and unbans the user.""" if self._processing_tempbans: + logger.debug("Tempban check is already in progress. Skipping.") return + self._processing_tempbans = True try: - self._processing_tempbans = True - - # Get expired tempbans expired_cases = await self.db.case.get_expired_tempbans() - processed_cases = 0 - failed_cases = 0 + if not expired_cases: + return + + logger.info(f"Processing {len(expired_cases)} expired tempban cases.") + + processed, failed = 0, 0 for case in expired_cases: - # Process each case using the helper method - processed, failed = await self._process_tempban_case(case) - processed_cases += processed - failed_cases += failed + p, f = await self._process_tempban_case(case) + processed += p + failed += f - if processed_cases > 0 or failed_cases > 0: - logger.info(f"Tempban check: processed {processed_cases} cases, {failed_cases} failures") + if processed or failed: + logger.info(f"Finished processing tempbans. Processed: {processed}, Failed: {failed}.") - except Exception as e: - logger.error(f"Failed to check tempbans: {e}") finally: self._processing_tempbans = False - @tempban_check.before_loop - async def before_tempban_check(self) -> None: - """Wait for the bot to be ready before starting the loop.""" + @check_tempbans.before_loop + async def before_check_tempbans(self) -> None: + """Wait until the bot is ready.""" await self.bot.wait_until_ready() + @check_tempbans.error + async def on_tempban_error(self, error: BaseException) -> None: + """Handles errors in the tempban checking loop.""" + logger.error(f"Error in tempban checker loop: {error}") + + if isinstance(error, Exception): + self.bot.sentry_manager.capture_exception(error) + else: + raise error + async def cog_unload(self) -> None: - """Cancel the tempban check loop when the cog is unloaded.""" - self.tempban_check.cancel() + self.check_tempbans.cancel() async def setup(bot: Tux) -> None: diff --git a/tux/cogs/tools/wolfram.py b/tux/cogs/tools/wolfram.py index 51cef15ae..255105c70 100644 --- a/tux/cogs/tools/wolfram.py +++ b/tux/cogs/tools/wolfram.py @@ -1,4 +1,3 @@ -import asyncio import io from urllib.parse import quote_plus @@ -17,22 +16,7 @@ class Wolfram(commands.Cog): def __init__(self, bot: Tux) -> None: self.bot = bot - - # Verify AppID configuration; unload cog if missing - if not CONFIG.WOLFRAM_APP_ID: - logger.warning("Wolfram Alpha API ID is not set. Some Science/Math commands will not work.") - # Store the task reference - self._unload_task = asyncio.create_task(self._unload_self()) - else: - logger.info("Wolfram Alpha API ID is set, Science/Math commands that depend on it will work.") - - async def _unload_self(self): - """Unload this cog if configuration is missing.""" - try: - await self.bot.unload_extension("tux.cogs.tools.wolfram") - logger.info("Wolfram cog has been unloaded due to missing configuration") - except Exception as e: - logger.error(f"Failed to unload Wolfram cog: {e}") + logger.info("Wolfram Alpha cog initialized successfully.") @commands.hybrid_command(name="wolfram", description="Query Wolfram|Alpha Simple API and return an image result.") @app_commands.describe( @@ -96,4 +80,9 @@ async def wolfram(self, ctx: commands.Context[Tux], *, query: str) -> None: async def setup(bot: Tux) -> None: + # Check if Wolfram API ID is configured before loading the cog + if not CONFIG.WOLFRAM_APP_ID: + logger.warning("Wolfram Alpha API ID is not set. Skipping Wolfram cog.") + return + await bot.add_cog(Wolfram(bot)) diff --git a/tux/cogs/utility/afk.py b/tux/cogs/utility/afk.py index bafaec050..7a33c5eb7 100644 --- a/tux/cogs/utility/afk.py +++ b/tux/cogs/utility/afk.py @@ -6,6 +6,7 @@ import discord from discord.ext import commands, tasks +from loguru import logger from prisma.models import AFKModel from tux.bot import Tux @@ -24,6 +25,9 @@ def __init__(self, bot: Tux) -> None: self.afk.usage = generate_usage(self.afk) self.permafk.usage = generate_usage(self.permafk) + async def cog_unload(self) -> None: + self.handle_afk_expiration.cancel() + @commands.hybrid_command( name="afk", ) @@ -183,8 +187,8 @@ async def check_afk(self, message: discord.Message) -> None: ), ) - @tasks.loop(seconds=120) - async def handle_afk_expiration(self): + @tasks.loop(seconds=120, name="afk_expiration_handler") + async def handle_afk_expiration(self) -> None: """ Check AFK database at a regular interval, Remove AFK from users with an entry that has expired. @@ -201,6 +205,20 @@ async def handle_afk_expiration(self): else: await del_afk(self.db, member, entry.nickname) + @handle_afk_expiration.before_loop + async def before_handle_afk_expiration(self) -> None: + """Wait until the bot is ready.""" + await self.bot.wait_until_ready() + + @handle_afk_expiration.error + async def on_handle_afk_expiration_error(self, error: BaseException) -> None: + """Handles errors in the AFK expiration handler loop.""" + logger.error(f"Error in AFK expiration handler loop: {error}") + if isinstance(error, Exception): + self.bot.sentry_manager.capture_exception(error) + else: + raise error + async def _get_expired_afk_entries(self, guild_id: int) -> list[AFKModel]: """ Get all expired AFK entries for a guild. diff --git a/tux/cogs/utility/remindme.py b/tux/cogs/utility/remindme.py index 728127910..b0a5471f2 100644 --- a/tux/cogs/utility/remindme.py +++ b/tux/cogs/utility/remindme.py @@ -1,15 +1,13 @@ -import asyncio -import contextlib import datetime -import discord from discord.ext import commands from loguru import logger -from prisma.models import Reminder from tux.bot import Tux +from tux.cogs.services.reminders import ReminderService from tux.database.controllers import DatabaseController from tux.ui.embeds import EmbedCreator +from tux.utils.constants import CONST from tux.utils.functions import convert_to_seconds, generate_usage @@ -18,67 +16,6 @@ def __init__(self, bot: Tux) -> None: self.bot = bot self.db = DatabaseController() self.remindme.usage = generate_usage(self.remindme) - self._initialized = False - - async def send_reminder(self, reminder: Reminder) -> None: - user = self.bot.get_user(reminder.reminder_user_id) - if user is not None: - embed = EmbedCreator.create_embed( - bot=self.bot, - embed_type=EmbedCreator.INFO, - user_name=user.name, - user_display_avatar=user.display_avatar.url, - title="Reminder", - description=reminder.reminder_content, - ) - - try: - await user.send(embed=embed) - - except discord.Forbidden: - channel = self.bot.get_channel(reminder.reminder_channel_id) - - if isinstance(channel, discord.TextChannel | discord.Thread | discord.VoiceChannel): - with contextlib.suppress(discord.Forbidden): - await channel.send( - content=f"{user.mention} Failed to DM you, sending in channel", - embed=embed, - ) - return - - else: - logger.error( - f"Failed to send reminder {reminder.reminder_id}, DMs closed and channel not found.", - ) - - else: - logger.error( - f"Failed to send reminder {reminder.reminder_id}, user with ID {reminder.reminder_user_id} not found.", - ) - - try: - await self.db.reminder.delete_reminder_by_id(reminder.reminder_id) - except Exception as e: - logger.error(f"Failed to delete reminder: {e}") - - @commands.Cog.listener() - async def on_ready(self) -> None: - if self._initialized: - return - - self._initialized = True - - reminders = await self.db.reminder.get_all_reminders() - dt_now = datetime.datetime.now(datetime.UTC) - - for reminder in reminders: - seconds = (reminder.reminder_expires_at - dt_now).total_seconds() - - if seconds <= 0: - await self.send_reminder(reminder) - continue - - self.bot.loop.call_later(seconds, asyncio.create_task, self.send_reminder(reminder)) @commands.hybrid_command( name="remindme", @@ -102,7 +39,7 @@ async def remindme( - m = minutes - s = seconds - Example: `!remindme 1h30m "Take a break"` will remind you in 1 hour and 30 minutes. + Example: `$remindme 1h30m take a break` will remind you in 1 hour and 30 minutes. Parameters ---------- @@ -111,7 +48,7 @@ async def remindme( time : str The time to set the reminder for (e.g. 2d, 1h30m). reminder : str - The reminder message. + The reminder message (quotes are not required). """ seconds = convert_to_seconds(time) @@ -120,11 +57,17 @@ async def remindme( await ctx.reply( "Invalid time format. Please use the format `[number][M/w/d/h/m/s]`.", ephemeral=True, - delete_after=30, + delete_after=CONST.DEFAULT_DELETE_AFTER, ) return expires_at = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=seconds) + reminder_service = self.bot.get_cog("ReminderService") + + if not isinstance(reminder_service, ReminderService): + await ctx.reply("Reminder service not available.", ephemeral=True, delete_after=CONST.DEFAULT_DELETE_AFTER) + logger.error("ReminderService not found or is not the correct type.") + return try: reminder_obj = await self.db.reminder.insert_reminder( @@ -135,7 +78,8 @@ async def remindme( guild_id=ctx.guild.id if ctx.guild else 0, ) - self.bot.loop.call_later(seconds, asyncio.create_task, self.send_reminder(reminder_obj)) + # Schedule the reminder using our new queue system + await reminder_service.schedule_reminder(reminder_obj) embed = EmbedCreator.create_embed( bot=self.bot,