signal-bot/fake_signal/routes_fixtures.py
Krzysztof kuhy Rudnicki 08599e6056 feat: two commands that answer with somebody else's work
!sztuka posts a work of art, skewed 60% Polish; !wiersz posts one short
public-domain Polish poem, whole. Both follow the rule the rest of the bot
already keeps: the repository commits identifiers, and the words arrive over
the wire when somebody asks.

!wiersz was written the other way first, and the gate caught it. A pack of
short public-domain poems was harvested, name-filtered and committed, and
check_personal_data.sh refused it: three lines carried a real group member's
given name, and two still did after a filter that had discarded 31% of the
corpus. That is structural, not a tuning problem -- Polish verse capitalises
every line, so a line-initial capital carries no signal, and that is exactly
where a name sits. The pack now holds Wolne Lektury slugs; the poem, its
title and its author are fetched at post time and appear nowhere in git.
It is also the better version: with no name filter there are no false
positives, so the pool is larger and the attribution is a full name again.

The gate then refused the art category list too, over a painter whose given
name is somebody's. That is the popes.json case exactly, so it takes the
same answer: a file-scoped allowlist entry, still refused everywhere else.

The Commons harvest is now three modules by what they are, not by where the
line count fell: commons_api is the conversation with Commons, commons_harvest
turns categories into a pool of licensed page ids, commons_cache makes an
interrupted run resumable. Along the way it gained licences in the same
request that lists a category (the second pass was 1,500 requests at 2.3s
each), maxlag instead of a guessed sleep, 5xx retries because one 504 used to
destroy a whole run, and Wikidata for works by creators of Polish
citizenship: the Polish pool grew 15,221 -> 20,092.

Batching categories through CirrusSearch looked like a further 6x and was
measured out again: 2,404 files where the authoritative enumeration returns
2,429, a different ~1% missing on every run.

The offline run is 19s where it was four minutes, almost all of it the
deafness gate waiting out three thirty-second liveness probes to prove
something provable in three; the tick is configuration now, and e2e enforces
a 40s budget on itself so that never silently comes back.
2026-08-31 21:32:07 +02:00

77 lines
3.0 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""Local stand-ins for the image APIs, shaped like the real responses."""
from __future__ import annotations
from fastapi import APIRouter, Request
from fastapi.responses import Response
from fake_signal.state import PIXEL_PNG, State
def _image_url(request: Request) -> str:
"""Absolute URL of the fixture image, as this server sees itself."""
state: State = request.app.state.fake
return f"{state.base_url}/_fixtures/image.png"
def build_fixture_router() -> APIRouter:
"""Build the router serving fixture images and image-API responses."""
router = APIRouter(prefix="/_fixtures")
@router.get("/cat-api")
async def cat_api(request: Request) -> list[dict[str, str]]:
return [{"url": _image_url(request)}]
@router.get("/dog-api")
async def dog_api(request: Request) -> dict[str, str]:
return {"message": _image_url(request)}
@router.get("/gecko-api")
async def gecko_api(request: Request) -> dict[str, object]:
state: State = request.app.state.fake
photo = {"url": f"{state.base_url}/_fixtures/photos/square.png"}
return {"results": [{"photos": [photo]}]}
# Commons answers an imageinfo query, not a fixed URL. Echoing the
# requested titles= back proves the bot asked for the pope it drew,
# rather than a hard-coded file that would hide a lookup regression.
@router.get("/commons-api")
async def commons_api(request: Request) -> dict[str, object]:
page = {
"title": request.query_params.get("titles", ""),
"imageinfo": [{"thumburl": _image_url(request)}],
}
return {"query": {"pages": [page]}}
# Wolne Lektury serves the poem as plain text in a fixed shape: author,
# blank, title, the poem, then a footer behind a lone rule. The slug is
# echoed into the title so a scenario proves the bot asked for the poem
# it drew rather than a hard-coded one. The public-domain sentence is
# here because poem_command refuses anything without it.
@router.get("/poem-text")
async def poem_text(request: Request) -> Response:
slug = request.query_params.get("slug", "")
body = (
f"Adam Testowy\n\n{slug}\n\n\n"
"Pierwsza linijka wiersza,\n"
"druga linijka wiersza.\n\n"
"-----\n"
"Ten utwór jest w domenie publicznej.\n"
)
return Response(content=body, media_type="text/plain; charset=utf-8")
@router.get("/image.png")
async def image() -> Response:
return Response(content=PIXEL_PNG, media_type="image/png")
# Only the rewritten URL is served. iNaturalist names a square thumbnail
# and the bot is supposed to swap it for the full-size photo, so a
# regression in that rewrite must fail the run with a 404 rather than
# quietly posting a 75px image nobody looks at closely.
@router.get("/photos/large.png")
async def large_photo() -> Response:
return Response(content=PIXEL_PNG, media_type="image/png")
return router