176 lines
5.4 KiB
Python
176 lines
5.4 KiB
Python
# 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,
|
|
)
|