# modules/usage/usage_stats.py from __future__ import annotations from discord.ext import commands import discord COUNTER_KEY_PREFIX = "cmd::" def _key_from_app(cmd: discord.app_commands.Command) -> str: name = getattr(cmd, "qualified_name", None) or getattr(cmd, "name", "unknown") return f"{COUNTER_KEY_PREFIX}{name}" def _key_from_ctx(ctx: commands.Context) -> str: c = getattr(ctx, "command", None) name = getattr(c, "qualified_name", None) or getattr(c, "name", "unknown") return f"{COUNTER_KEY_PREFIX}{name}" class UsageStatsCog(commands.Cog): """Lightweight command run counters with storage-level dedup.""" def __init__(self, bot: commands.Bot): self.bot = bot print("[usage] UsageStatsCog init") # ---- successful completions ---- @commands.Cog.listener() async def on_app_command_completion(self, interaction: discord.Interaction, command: discord.app_commands.Command): dm = getattr(self.bot, "data_manager", None) if not dm: print("[usage] app ~~ skip (no data_manager)") return try: # Use the interaction id as the dedup event key event_key = str(getattr(interaction, "id", None) or "no-iid") counter_key = _key_from_app(command) newv = dm.incr_counter_once(counter_key=counter_key, event_key=event_key, window_sec=3.0, namespace="app") if newv is not None: print(f"[usage] app ++ {counter_key} -> {newv}") else: print(f"[usage] app ~~ dup ignored ({event_key})") except Exception as e: print("[usage] app !! incr failed:", repr(e)) @commands.Cog.listener() async def on_command_completion(self, ctx: commands.Context): # If a HybridCommand was invoked via slash, skip here (the app listener handled it). if isinstance(getattr(ctx, "command", None), commands.HybridCommand) and getattr(ctx, "interaction", None): print("[usage] px ~~ hybrid-as-slash; ignore here") return dm = getattr(self.bot, "data_manager", None) if not dm: print("[usage] px ~~ skip (no data_manager)") return try: # Prefer interaction id when present (hybrid-as-slash defense), else message id event_key = None if getattr(ctx, "interaction", None): event_key = str(getattr(ctx.interaction, "id", None) or "") if not event_key: msg = getattr(ctx, "message", None) event_key = str(getattr(msg, "id", None) or "no-mid") counter_key = _key_from_ctx(ctx) newv = dm.incr_counter_once(counter_key=counter_key, event_key=event_key, window_sec=3.0, namespace="px") if newv is not None: print(f"[usage] px ++ {counter_key} -> {newv}") else: print(f"[usage] px ~~ dup ignored ({event_key})") except Exception as e: print("[usage] px !! incr failed:", repr(e)) # ---- error visibility (optional) ---- @commands.Cog.listener() async def on_app_command_error(self, interaction: discord.Interaction, error: Exception): iid = getattr(interaction, "id", None) print(f"[usage] app ** error ({type(error).__name__}) iid={iid}") @commands.Cog.listener() async def on_command_error(self, ctx: commands.Context, error: Exception): mid = getattr(getattr(ctx, "message", None), "id", None) print(f"[usage] px ** error ({type(error).__name__}) mid={mid}") async def setup(bot: commands.Bot): # Prevent duplicate registration if extensions are reloaded / auto-discovered twice if getattr(bot, "_usage_stats_loaded", False): print("[usage] UsageStatsCog already loaded; skipping duplicate add") return await bot.add_cog(UsageStatsCog(bot)) bot._usage_stats_loaded = True print("[usage] UsageStatsCog loaded")