mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 18:17:02 +01:00
A photo sent to the chat no longer enters the album by itself. pack_add strips it, checks it against the album and the queue, and writes it to pending/ with a sidecar naming the people and labels the sender proposed; the reply is "czeka na zatwierdzenie". The management page lists the queue above the tagger, with the proposal pre-ticked, and Approve moves the file in, mints the labels, records approved_at and asks the bot to rebuild its table through /_reload -- the same publish path every other data change takes, so PackHolder.reload is gone. A reload the bot refuses is reported on the page, not rolled back: the photo is in the album either way. The personal-data gate now reads pending/*.json too, so a label that is still waiting for approval is forbidden in the tree already. The e2e run boots the management page beside the bot with the same environment, proves the queue is stripped and never reaches media/, approves it through the real routes, and has !losowe answer with the photo afterwards without a restart; exiftool is found through media_strip.locate() there rather than PATH, which lacks vendor_perl on Arch. !gadaj is dropped from !odcisz: it was within a typo of !dodaj, and the nearest-command suggestion must never be a coin toss. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
127 lines
6.2 KiB
Python
127 lines
6.2 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""Which commands exist: the one function that fills a CommandRegistry.
|
|
|
|
Split out of bot.py when the registry stopped being built once. ``build_bot``
|
|
still assembles the object graph; this is the part of it that reads data --
|
|
aliases.json, every album's manifests -- and so the part ``RegistryHolder``
|
|
runs again when that data changes. Everything with a lifetime longer than
|
|
one table (the HTTP client, the portrait cache, the store) is passed in, so
|
|
a rebuild reuses them rather than opening a second of each.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Final
|
|
|
|
from signal_bot.analytics_command import register_analytics_command
|
|
from signal_bot.animals import load_aliases, register_animal_commands
|
|
from signal_bot.art_command import register_art_command
|
|
from signal_bot.custom_aliases import default_path as custom_aliases_path
|
|
from signal_bot.custom_aliases import load as load_custom_aliases
|
|
from signal_bot.decorators import cooldown
|
|
from signal_bot.food_command import register_food_command
|
|
from signal_bot.good_bot_command import register_good_bot_command
|
|
from signal_bot.groups import ConfigError
|
|
from signal_bot.help_command import register_help_command
|
|
from signal_bot.media_strip import ExiftoolStripper
|
|
from signal_bot.mute import UNMUTE_NAME
|
|
from signal_bot.mute_command import MUTE_NAME, register_mute_commands
|
|
from signal_bot.pack_add import COMMAND_NAME as ADD_NAME
|
|
from signal_bot.pack_add import register_add_command
|
|
from signal_bot.pack_command import register_pack_commands
|
|
from signal_bot.pack_holder import PackHolder
|
|
from signal_bot.pack_load import load as load_pack
|
|
from signal_bot.pack_random import register_random_command
|
|
from signal_bot.poem_command import register_poem_command
|
|
from signal_bot.popes_command import register_pope_command
|
|
from signal_bot.registry import CommandRegistry
|
|
from signal_bot.stats.commands import register_stats_commands
|
|
from signal_bot.suggest_command import register_suggest_command
|
|
from signal_bot.traps_command import register_trap_command
|
|
from signal_bot.workout_command import register_workout_command
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
from signal_bot.config import Config
|
|
from signal_bot.decorators import Decorator
|
|
from signal_bot.http_client import ImageFetcher
|
|
from signal_bot.portraits import PortraitCache
|
|
|
|
COOLDOWN_MESSAGE = "BEEP BOOP POCZEKAJ {seconds} {unit}."
|
|
|
|
|
|
# The commands registered below without ``decorate=rate_limit``. Named so
|
|
# the switchboard can say "no cooldown" for them instead of showing the
|
|
# deployment's number next to an input that would do nothing.
|
|
# test_wiring_ungated walks the real table to keep this honest.
|
|
UNGATED: Final = frozenset({MUTE_NAME, UNMUTE_NAME, ADD_NAME})
|
|
|
|
|
|
def _register_album(registry: CommandRegistry, directory: Path, rate_limit: Decorator) -> None:
|
|
"""Wire the album's commands, all of them reading one holder.
|
|
|
|
!random takes the same cooldown as every other photo command. !add_photo
|
|
has none, and writes only to the album's approval queue: a photo posted
|
|
to a group reaches the album when the management page approves it.
|
|
"""
|
|
aliases = load_aliases()
|
|
holder = PackHolder(load_pack(directory))
|
|
register_pack_commands(registry, holder, decorate=rate_limit)
|
|
register_random_command(registry, holder, tuple(aliases["random"]), decorate=rate_limit)
|
|
register_add_command(registry, holder, ExiftoolStripper(), tuple(aliases["add_photo"]))
|
|
|
|
|
|
def build_registry(
|
|
config: Config, fetcher: ImageFetcher, portraits: PortraitCache
|
|
) -> CommandRegistry:
|
|
"""Read the data and register every command on a fresh table.
|
|
|
|
Raises:
|
|
ConfigError: An album no longer loads, the operator's alias file does
|
|
not, or it names a command that does not exist.
|
|
DuplicateTriggerError: Two commands live in one group claim a word.
|
|
"""
|
|
registry = CommandRegistry(load_custom_aliases(custom_aliases_path()))
|
|
rate_limit = cooldown(config.cooldown_seconds, COOLDOWN_MESSAGE)
|
|
register_animal_commands(
|
|
registry,
|
|
fetcher,
|
|
decorate=rate_limit,
|
|
endpoints=config.image_api_urls,
|
|
)
|
|
register_pope_command(registry, portraits, decorate=rate_limit)
|
|
register_trap_command(registry, fetcher, config.commons_api_url, decorate=rate_limit)
|
|
register_art_command(registry, fetcher, config.commons_api_url, decorate=rate_limit)
|
|
register_poem_command(registry, fetcher, config.poem_text_url, decorate=rate_limit)
|
|
register_stats_commands(registry, decorate=rate_limit)
|
|
# Reads the *command* history rather than the message history, and
|
|
# reads the registry when it runs -- so the album's commands, which
|
|
# are registered below this line, are still in the near-miss set.
|
|
register_analytics_command(registry, decorate=rate_limit)
|
|
# Rate-limited like every other command. The cost is real and accepted:
|
|
# somebody told "napisz !zaproponuj ..." who then files two ideas in a
|
|
# row is asked to wait for the second. The alternative is one person
|
|
# being able to fill a table nothing prunes.
|
|
register_suggest_command(registry, decorate=rate_limit)
|
|
register_good_bot_command(registry, decorate=rate_limit)
|
|
# Two logs other programs keep, read where they lie; the spellings the
|
|
# group uses for them carry a surname and come from aliases_custom.json.
|
|
register_food_command(registry, decorate=rate_limit)
|
|
register_workout_command(registry, decorate=rate_limit)
|
|
# No cooldown: "wait before you may ask for quiet" is the wrong answer,
|
|
# and the unmute is gated by trust, not by time.
|
|
register_mute_commands(registry)
|
|
# Absent unless this installation has an album, and scoped to that
|
|
# album's own groups when it does -- so on every other machine, and in
|
|
# every other group, these commands simply do not exist.
|
|
for album in config.photo_pack_dirs:
|
|
_register_album(registry, album, rate_limit)
|
|
# Last, so !pomoc trails the list it prints; it reads the registry
|
|
# when it runs, so the order is presentation only.
|
|
register_help_command(registry, decorate=rate_limit)
|
|
if registry.unclaimed_aliases:
|
|
msg = f"aliases for commands that do not exist: {sorted(registry.unclaimed_aliases)}"
|
|
raise ConfigError(msg)
|
|
return registry
|