# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The !pułapka command: a photograph of the device, and what it is. The joke is literalism -- whatever the word meant to whoever typed it, what comes back is a bear trap, a mousetrap, a snare, a cage, a fish weir. The caption is not an apology for the joke, it is the answer to the next question: Commons has plenty of files whose subject is genuinely unguessable ("Molchreuse", "K Pañayacu 0811"), and a newt trap nobody can identify is just a photograph of a basket. Nothing is cached. !papież caches because 267 portraits are a closed set a group works through; a uniform draw from a couple of thousand traps warms a cache essentially never, so the disk would fill with images nobody asks for twice. This works the way !kot and !pies do instead: fetch, post, forget. """ from __future__ import annotations import json import secrets from base64 import b64encode from pathlib import Path from typing import TYPE_CHECKING, Final from signal_bot.analytics import probe from signal_bot.animals import load_aliases from signal_bot.commons import caption_for, imageinfo_params, page_of from signal_bot.endpoints import THUMB_WIDTH if TYPE_CHECKING: from collections.abc import Sequence from signal_bot.context import CommandContext from signal_bot.decorators import Decorator from signal_bot.http_client import ImageFetcher from signal_bot.registry import CommandRegistry, Handler COMMAND_NAME: Final = "trap" DESCRIPTION: Final = "losowe zdjęcie pułapki" TRAPS_FILE: Final = Path(__file__).with_name("traps.json") def load_traps(path: Path = TRAPS_FILE) -> tuple[int, ...]: """Read the trap pool. The file holds Commons page ids -- bare integers -- and that is checked here rather than assumed. Filenames would be free text carrying photographers' and sitters' names, and a name committed here collides sooner or later with a label somebody minted from the chat, at which point scripts/check_personal_data.py refuses every commit for good over data nobody can edit by hand. An integer cannot do that, and this is the guard that keeps it an integer. Args: path: Where to read the pool from. Defaults to the shipped file. Returns: Every Commons page id in the pool. Raises: ValueError: If the pool is empty or holds anything but integers. """ document = json.loads(path.read_text(encoding="utf-8")) traps = document["traps"] if not traps or not all(isinstance(page, int) for page in traps): msg = f"{path} must hold a non-empty list of Commons page ids" raise ValueError(msg) return tuple(traps) def random_trap(traps: Sequence[int]) -> int: """Return one page id, uniformly at random. secrets rather than random for the reason animals.paged_url gives: the choice is not security-sensitive, but ruff's ALL rule set flags `random` and this repository forbids the noqa that would silence it. """ return traps[secrets.randbelow(len(traps))] def trap_handler( fetcher: ImageFetcher, api_url: str, traps: Sequence[int] | None = None, ) -> Handler: """Build the handler that posts one random trap.""" pool = load_traps() if traps is None else tuple(traps) async def handler(ctx: CommandContext) -> None: # One request, because the caption and the picture have to describe # the same file; two would be a chance for them to disagree. page_id = random_trap(pool) # A Commons page id, for the reason art_command records one. probe.note_item(str(page_id)) document = await fetcher.fetch_json( api_url, imageinfo_params(THUMB_WIDTH, page_id=page_id, describe=True), ) page = page_of(document) image = await fetcher.fetch_image(str(page["imageinfo"][0]["thumburl"])) await ctx.reply_image(b64encode(image).decode("ascii"), caption_for(page)) return handler def register_trap_command( registry: CommandRegistry, fetcher: ImageFetcher, api_url: str, *, decorate: Decorator | None = None, aliases: dict[str, list[str]] | None = None, traps: Sequence[int] | None = None, ) -> None: """Register !pułapka under every trigger aliases.json gives it.""" table = load_aliases() if aliases is None else aliases handler = trap_handler(fetcher, api_url, traps) if decorate is not None: handler = decorate(handler) registry.register(COMMAND_NAME, table[COMMAND_NAME], handler, DESCRIPTION)