- Correction to earlier patch. *Testing on live server is a pain, but I'm too lazy to fire up a dev server..*
92 lines
3.9 KiB
Python
92 lines
3.9 KiB
Python
# 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")
|