Refactored custom commands module into layered architecture with typed domain objects and comprehensive tests.
CI / Formatting (push) Successful in 6s
CI / Linting (push) Successful in 5s
CI / Tests (Python 3.12) (push) Successful in 15s
CI / Tests (Python 3.13) (push) Successful in 14s
CI / Tests (Python 3.14) (push) Successful in 11s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 6s

This commit is contained in:
2026-04-12 14:21:33 -04:00
parent 0b28ec39d0
commit 3413b1dfe4
13 changed files with 2394 additions and 844 deletions
@@ -14,17 +14,14 @@
"""Custom commands module for Owlbot.
Allows moderators to create, edit, delete, and list custom chat commands at runtime.
Custom commands are stored in SQLite and dynamically registered
Allows moderators to create, edit, delete, and list custom chat commands
at runtime. Custom commands are stored in SQLite and dynamically registered
with the CommandRegistry.
"""
from owlbot.api import ModuleContext, on_setup
from owlbot.api import ModuleContext, on_setup, on_teardown
from .handler import custom_command_handler
# Re-export decorated handlers so the module loader discovers them.
from .management_commands import (
from .commands import (
addalias,
addcommand,
commandcooldown,
@@ -36,6 +33,8 @@ from .management_commands import (
removealias,
resetcommand,
)
from .manager import CommandManager
from .repository import CommandRepository
from .routes import command_list_page
__all__ = [
@@ -51,6 +50,7 @@ __all__ = [
"removealias",
"resetcommand",
"setup",
"teardown",
]
@@ -58,64 +58,21 @@ __all__ = [
async def setup(ctx: ModuleContext) -> None:
"""Initialize the custom_commands module.
Creates the database schema and loads existing commands from the database.
:param ctx: Module context with config, storage, and other services.
"""
ctx.config.register_defaults({"max_nesting_depth": 4, "default_cooldown": 5})
await ctx.storage.execute("""
CREATE TABLE IF NOT EXISTS commands (
name TEXT PRIMARY KEY NOT NULL,
response TEXT NOT NULL,
use_count INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
requires_moderator INTEGER DEFAULT 0,
cooldown INTEGER DEFAULT 0
)
""")
repo = CommandRepository(ctx.storage)
await repo.setup()
manager = CommandManager(ctx, repo)
ctx.state["manager"] = manager
await ctx.storage.execute("""
CREATE TABLE IF NOT EXISTS command_aliases (
alias TEXT PRIMARY KEY NOT NULL,
command_name TEXT NOT NULL,
FOREIGN KEY (command_name) REFERENCES commands(name) ON DELETE CASCADE
)
""")
await manager.load_all()
await ctx.storage.execute("""
CREATE TABLE IF NOT EXISTS counters (
name TEXT PRIMARY KEY NOT NULL,
value INTEGER DEFAULT 0
)
""")
rows = await ctx.storage.fetch_all(
"SELECT c.name, c.requires_moderator, c.cooldown, "
"GROUP_CONCAT(ca.alias) AS aliases "
"FROM commands c "
"LEFT JOIN command_aliases ca ON c.name = ca.command_name "
"GROUP BY c.name"
)
skipped = 0
for row in rows:
aliases = row["aliases"].split(",") if row["aliases"] else []
try:
ctx.commands.register(
name=row["name"],
handler=custom_command_handler,
aliases=aliases,
requires_moderator=bool(row["requires_moderator"]),
cooldown=row["cooldown"],
)
except ValueError:
ctx.logger.warning(
f"Skipping custom command '{row['name']}': "
"conflicts with an existing command."
)
skipped += 1
@on_teardown
async def teardown(ctx: ModuleContext) -> None:
"""Clean up the custom_commands module.
loaded_count = len(rows) - skipped
ctx.logger.info(f"Loaded {loaded_count} custom command(s) from database.")
if skipped:
ctx.logger.info(f"Skipped {skipped} conflicting custom command(s).")
:param ctx: Module context.
"""
ctx.state["manager"] = None