mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 18:17:02 +01:00
!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.
77 lines
3.0 KiB
Python
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
|