import discord from discord.ext import commands, tasks from discord import app_commands import json import asyncio from datetime import datetime, timedelta import os TIMER_FILE = "data/timers.json" class TimerCog(commands.Cog): def __init__(self, bot): self.bot = bot self.timers = [] self.load_timers() self.timer_check_task.start() def load_timers(self): if not os.path.exists("data"): os.makedirs("data") if os.path.exists(TIMER_FILE): with open(TIMER_FILE, "r") as f: try: data = json.load(f) # Convert string timestamps back to datetime objects for timer_data in data: timer_data["expires_at"] = datetime.fromisoformat( timer_data["expires_at"] ) self.timers.append(timer_data) except json.JSONDecodeError: self.timers = [] else: self.timers = [] print(f"Loaded {len(self.timers)} timers.") def save_timers(self): # Convert datetime objects to ISO format strings for JSON serialization serializable_timers = [] for timer in self.timers: timer_copy = timer.copy() timer_copy["expires_at"] = timer_copy["expires_at"].isoformat() serializable_timers.append(timer_copy) with open(TIMER_FILE, "w") as f: json.dump(serializable_timers, f, indent=4) print(f"Saved {len(self.timers)} timers.") @tasks.loop(seconds=10) # Check every 10 seconds async def timer_check_task(self): now = datetime.now() expired_timers = [] for timer in self.timers: if timer["expires_at"] <= now: expired_timers.append(timer) for timer in expired_timers: self.timers.remove(timer) try: channel = self.bot.get_channel(timer["channel_id"]) if channel: user = self.bot.get_user(timer["user_id"]) if user: message_content = f"{user.mention}, your timer for '{timer['message']}' has expired!" if timer.get( "ephemeral", True ): # Default to True if not specified # Ephemeral messages require interaction context, which we don't have here. # For now, we'll send non-ephemeral if it was originally ephemeral. # A better solution would be to store interaction context or use webhooks. await channel.send(message_content) else: await channel.send(message_content) else: print(f"Could not find user {timer['user_id']} for timer.") else: print(f"Could not find channel {timer['channel_id']} for timer.") except Exception as e: print(f"Error sending timer message: {e}") if expired_timers: self.save_timers() @timer_check_task.before_loop async def before_timer_check_task(self): await self.bot.wait_until_ready() @app_commands.command(name="timer", description="Sets a timer, reminder, or alarm.") @app_commands.describe( time_str="Duration for the timer (e.g., 1h30m, 5m, 2d). Supports s, m, h, d.", message="The message for your reminder.", ephemeral="Whether the response should only be visible to you (defaults to True).", ) async def timer_slash( self, interaction: discord.Interaction, time_str: str, message: str = "a reminder", ephemeral: bool = True, ): """ Sets a timer, reminder, or alarm as a slash command. Usage: /timer time_str:1h30m message:Your reminder message ephemeral:False Supports: s (seconds), m (minutes), h (hours), d (days) """ duration_seconds = 0 time_str = time_str.lower() # Parse time string (e.g., 1h30m, 5m, 2d) current_num = "" for char in time_str: if char.isdigit(): current_num += char else: if current_num: num = int(current_num) if char == "s": duration_seconds += num elif char == "m": duration_seconds += num * 60 elif char == "h": duration_seconds += num * 60 * 60 elif char == "d": duration_seconds += num * 60 * 60 * 24 else: await interaction.response.send_message( "Invalid time unit. Use s, m, h, or d.", ephemeral=ephemeral ) return current_num = "" else: await interaction.response.send_message( "Invalid time format. Example: `1h30m` or `5m`.", ephemeral=ephemeral, ) return if current_num: # Handle cases like "30s" without a unit at the end await interaction.response.send_message( "Invalid time format. Please specify a unit (s, m, h, d) for all numbers.", ephemeral=ephemeral, ) return if duration_seconds <= 0: await interaction.response.send_message( "Duration must be a positive value.", ephemeral=ephemeral ) return expires_at = datetime.now() + timedelta(seconds=duration_seconds) timer_data = { "user_id": interaction.user.id, "channel_id": interaction.channel_id, "message": message, "expires_at": expires_at, "ephemeral": ephemeral, } self.timers.append(timer_data) self.save_timers() await interaction.response.send_message( f"Timer set for {timedelta(seconds=duration_seconds)} from now for '{message}'.", ephemeral=ephemeral, ) def cog_unload(self): self.timer_check_task.cancel() self.save_timers() # Ensure timers are saved on unload async def setup(bot: commands.Bot): await bot.add_cog(TimerCog(bot))