signal-bot/signal_bot/manage/routes_pending.py
Krzysztof kuhy Rudnicki da016e1b94
Some checks failed
CI / check (push) Has been cancelled
CI / web (push) Has been cancelled
!dodaj lands in a queue; the management page approves it into the album
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>
2026-09-04 22:55:39 +02:00

104 lines
3.7 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""The approval queue on the page: what is waiting, and the two decisions.
The only route from ``pending/`` into the album. The bot's process never
approves anything; it writes the queue and reads the result back through
``POST /_reload``, which is asked for here, after the write. A reload the
bot refuses is *reported*, not rolled back: the file is already in the
album with the tags the approver chose, and the bot's reason (a stale
alias, say) is about something else, which the page can now show.
The media route takes the queued file's name, not an index like the
album's, because the queue is re-ordered by every send; but only a name
that is in the queue's own listing is ever opened, so nothing typed into a
URL reaches the filesystem.
"""
from __future__ import annotations
import time
from typing import TYPE_CHECKING, Any
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import FileResponse
from pydantic import BaseModel
from signal_bot.groups import ConfigError
from signal_bot.manage.bot_reload import reload_bot
from signal_bot.pack_pending import PENDING_DIR, approve, discard, listing, names
if TYPE_CHECKING:
from signal_bot.manage.deps import ManageDeps
class ApproveRequest(BaseModel):
"""The approver's decision: which proposed keys and labels the photo keeps."""
keys: list[str]
labels: list[str]
def router(deps: ManageDeps) -> APIRouter:
"""The queue's four routes.
Args:
deps: The running app's album and shared HTTP client.
Returns:
A router to include in the management app.
"""
api = APIRouter(prefix="/api/album/pending")
directory = deps.album.directory
@api.get("")
async def pending() -> dict[str, list[dict[str, Any]]]:
"""Everything waiting, oldest first; the sender is not the page's business."""
return {
"pending": [
{
"name": entry.name,
"keys": list(entry.keys),
"labels": list(entry.labels),
"at": entry.at,
}
for entry in listing(directory)
]
}
@api.get("/{name}/media")
async def media(name: str) -> FileResponse:
"""One queued file, by the name the queue itself lists it under."""
if name not in names(directory):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return FileResponse(directory / PENDING_DIR / name)
@api.post("/{name}/approve")
async def approve_one(name: str, request: ApproveRequest) -> dict[str, Any]:
"""Move it into the album, then ask the bot to pick it up."""
try:
keys = approve(directory, name, request.keys, request.labels, int(time.time()))
except KeyError as error:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
except (ValueError, ConfigError) as error:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)
) from error
try:
await reload_bot(deps)
except HTTPException as error:
return {
"status": "approved",
"keys": list(keys),
"reloaded": False,
"reason": error.detail,
}
return {"status": "approved", "keys": list(keys), "reloaded": True, "reason": None}
@api.post("/{name}/reject")
async def reject_one(name: str) -> dict[str, str]:
"""Delete it, and its sidecar. Nothing else to undo: it never reached the album."""
discard(directory, name)
return {"status": "rejected"}
return api