"""Plugin: countdown timer with async notifications (pure stdlib).""" from __future__ import annotations import asyncio import re from derp.plugin import command # In-memory timer storage: {channel_or_nick: {label: task}} _timers: dict[str, dict[str, asyncio.Task]] = {} _MAX_TIMERS = 10 _MAX_DURATION = 86400 # 24h _DURATION_RE = re.compile( r"(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$", ) def _parse_duration(spec: str) -> int | None: """Parse a duration like '5m', '1h30m', '90s', or raw seconds.""" # Try raw integer seconds try: secs = int(spec) return secs if 0 < secs <= _MAX_DURATION else None except ValueError: pass m = _DURATION_RE.match(spec.lower()) if not m or not any(m.groups()): return None hours = int(m.group(1) or 0) mins = int(m.group(2) or 0) secs = int(m.group(3) or 0) total = hours * 3600 + mins * 60 + secs return total if 0 < total <= _MAX_DURATION else None def _format_duration(secs: int) -> str: """Format seconds into compact duration.""" parts = [] if secs >= 3600: parts.append(f"{secs // 3600}h") secs %= 3600 if secs >= 60: parts.append(f"{secs // 60}m") secs %= 60 if secs or not parts: parts.append(f"{secs}s") return "".join(parts) async def _timer_task(bot, target: str, label: str, duration: int, nick: str) -> None: """Background task that waits and then notifies.""" try: await asyncio.sleep(duration) await bot.send(target, f"{nick}: timer '{label}' ({_format_duration(duration)}) finished") except asyncio.CancelledError: pass finally: # Clean up if target in _timers: _timers[target].pop(label, None) if not _timers[target]: del _timers[target] @command("timer", help="Timer: !timer [label] | !timer list | !timer cancel