Initial commit.

This commit is contained in:
2026-02-14 15:20:52 -05:00
commit 067b7c5a0a
48 changed files with 12169 additions and 0 deletions
@@ -0,0 +1,175 @@
# Copyright 2026 Logan Fick
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Shared custom command handler and database/registry helpers."""
from datetime import UTC, datetime
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import aiosqlite
from owlbot.api import CommandContext, ModuleCommands, ModuleStorage
from .placeholders import DEFAULT_MAX_DEPTH, process_placeholders
async def get_aliases_for_command(
storage: ModuleStorage, command_name: str
) -> list[str]:
"""
Fetch all aliases for a command from the database.
:param storage: The module storage instance.
:param command_name: The canonical command name.
:return: List of alias names (may be empty).
"""
rows = await storage.fetch_all(
"SELECT alias FROM command_aliases WHERE command_name = ? ORDER BY alias",
(command_name,),
)
return [row["alias"] for row in rows]
async def resolve_command_name(
storage: ModuleStorage, name: str
) -> aiosqlite.Row | None:
"""
Resolve a command name or alias to the full command row.
Checks the commands table first, then falls back to the aliases table.
Returns the command's name, requires_moderator, and cooldown columns.
:param storage: The module storage instance.
:param name: A command name or alias (already lowercased).
:return: The command row, or None if not found.
"""
return await storage.fetch_one(
"SELECT c.name, c.requires_moderator, c.cooldown FROM commands c "
"WHERE c.name = ? "
"UNION ALL "
"SELECT c.name, c.requires_moderator, c.cooldown "
"FROM command_aliases a JOIN commands c ON c.name = a.command_name "
"WHERE a.alias = ? "
"LIMIT 1",
(name, name),
)
async def custom_command_handler(ctx: CommandContext) -> None:
"""
Shared handler for all custom commands.
Looks up the command in the database, increments use count,
processes placeholders, and sends the response.
:param ctx: The command context.
"""
cmd_name = ctx.command
row = await ctx.storage.fetch_one(
"UPDATE commands SET use_count = use_count + 1 WHERE name = ? "
"RETURNING response, use_count",
(cmd_name,),
)
# Shouldn't happen unless the command was deleted but not unregistered.
if not row:
ctx.logger.warning(f"Custom command '{cmd_name}' not found in database.")
return
max_depth = ctx.config.get("max_nesting_depth", DEFAULT_MAX_DEPTH)
response = await process_placeholders(
row["response"],
ctx.args_list,
ctx.user.display_name,
row["use_count"],
ctx.storage,
max_depth=max_depth,
)
await ctx.owncast_client.send_message(response)
def reregister_command(
commands: ModuleCommands,
name: str,
*,
aliases: list[str],
requires_moderator: bool,
cooldown: int,
) -> None:
"""
Unregister and re-register a custom command with updated settings.
Both registry operations are synchronous, so no other coroutine can observe
the intermediate unregistered state.
:param commands: The module-scoped command API (ModuleCommands).
:param name: The canonical command name.
:param aliases: List of alias names.
:param requires_moderator: Whether the command requires moderator.
:param cooldown: Cooldown in seconds.
"""
commands.unregister(name)
commands.register(
name=name,
handler=custom_command_handler,
aliases=aliases,
requires_moderator=requires_moderator,
cooldown=cooldown,
)
async def update_and_reregister(
ctx: CommandContext,
command_row: aiosqlite.Row,
*,
requires_moderator: int | None = None,
cooldown: int | None = None,
) -> None:
"""Update a command's settings in the database and re-register it.
Uses the provided *command_row* for current settings, applies any
overrides, writes them back, and re-registers the command so the
in-memory registry matches the database.
:param ctx: The command context.
:param command_row: The command row (from ``resolve_command_name``).
:param requires_moderator: New value, or None to keep the current one.
:param cooldown: New value, or None to keep the current one.
"""
name: str = command_row["name"]
final_mod = (
requires_moderator
if requires_moderator is not None
else command_row["requires_moderator"]
)
final_cooldown = cooldown if cooldown is not None else command_row["cooldown"]
now = datetime.now(UTC).isoformat()
await ctx.storage.execute(
"UPDATE commands SET requires_moderator = ?, cooldown = ?, updated_at = ? "
"WHERE name = ?",
(final_mod, final_cooldown, now, name),
)
aliases = await get_aliases_for_command(ctx.storage, name)
reregister_command(
ctx.commands,
name,
aliases=aliases,
requires_moderator=bool(final_mod),
cooldown=final_cooldown,
)