feat: no photo reaches the album from the chat until somebody approves it
Some checks are pending
CI / check (push) Waiting to run
CI / web (push) Waiting to run

!dodaj is unwired: register_add_command is no longer called, its aliases
are gone from aliases.json, and the word is an unknown one like any
other -- answered with "Nie znam !dodaj", storing nothing. The ingest
path itself (pack_add, pack_ingest, pack_store, the stripper) stays,
tested at 100%, because the approval queue that replaces the direct
write will reuse every piece of it: !dodaj lands in pending/, and the
tagger view gets approve / reject.

The eight e2e scenarios that proved the direct write, the media
generation step and prove_the_add_photo_invariants leave with it;
64852ae has them for the queue to bring back in their new shape. One
scenario takes their place and pins the disabled state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-09-04 09:47:42 +02:00
parent 6b69deb37d
commit 3107a9e55f
9 changed files with 38 additions and 62 deletions

View File

@ -1,5 +1,9 @@
# Adding a photo from the chat: `!add_photo`
> **Unwired since 2026-09-04.** `!dodaj` is not a command until photos land
> in an approval queue instead of straight in the album. Everything below
> describes the ingest path as it was wired, and as the queue will reuse it.
How a member of an album's group puts a photo into it by sending one, and
what keeps that path from leaking anything. The album itself — where it
lives, what its manifests look like, how several albums coexist — is

View File

@ -30,7 +30,6 @@ This file is the trigger reference — every spelling that resolves, which
| stats_me | `!ja` `!moje` `!mnie` |
| command_stats | `!analityka` `!command_stats` `!commandstats` `!analytics` `!uzycie` `!użycie` `!ranking` |
| help | `!pomoc` `!pomocy` `!help` `!komendy` `!commands` |
| add_photo | `!dodaj` `!dodaj_zdjecie` `!dodajzdjecie` `!add_photo` `!addphoto` |
| random | `!losowe` `!losuj` `!random` `!rand` |
| suggest | `!zaproponuj` `!propozycja` `!sugestia` `!suggest` `!pomysl` |
@ -66,11 +65,16 @@ group — the album's, whose triggers are real people's names — is neither
answered nor recorded, because a refusal would announce that the album
exists.
`add_photo` and `random` exist **only in the photo album's own groups**, and
only on an installation that has an album at all — everywhere else they are
not commands, not refused and never logged. Both take arguments, and those
`random` exists **only in the photo album's own groups**, and only on an
installation that has an album at all — everywhere else it is not a command,
not refused and never logged. It takes arguments, and those
arguments are resolved to pseudonym keys by the registry before any handler,
log line or `command_uses` row sees them. See `DOCS-photo-pack.md` and `DOCS-add-photo.md`.
log line or `command_uses` row sees them. See `DOCS-photo-pack.md`.
`add_photo` (`!dodaj`) is **unwired since 2026-09-04**: a photo posted to a
group must not reach the album without somebody approving it, and no approval
queue exists yet. It comes back when one does; `DOCS-add-photo.md` describes
the path as it was.
## Why `!pomoc` shows the first alias and not a trigger

View File

@ -0,0 +1,13 @@
# Adding a photo from the chat is switched off until an approval queue
# exists (2026-09-04): a photo somebody posts must not reach the album
# unreviewed. Unwired rather than gated, so !dodaj is an unknown word like
# any other -- answered, and never storing anything. The eight scenarios
# that proved the ingest path are at 64852ae, for the queue to bring back.
name: "!dodaj is not a command while photos cannot be approved"
inject:
- {
text: "!dodaj alfa", uuid: uuid-add-off, name: AddOff,
attachment: att-off,
}
expect:
- { kind: text, to: reply, contains: "Nie znam !dodaj" }

View File

@ -158,9 +158,6 @@ main() {
wait_for_url "http://127.0.0.1:${BOT_PORT}/healthz" "bot"
wait_for_subscription
echo "== generating media for !add_photo (real exif, never committed)"
"$PYTHON" "$REPO_ROOT/tool/e2e_media.py" --upload "$FAKE_URL"
echo "== replaying scenarios"
"$PYTHON" -m fake_signal.cli \
"$REPO_ROOT"/fake_signal/scenarios/*.yaml \
@ -176,7 +173,9 @@ main() {
--group "fourth_reply=${FOURTH_GROUP}" \
--group "other=${OTHER_GROUP}"
prove_the_add_photo_invariants
# !dodaj is unwired until photos land in an approval queue instead of
# the album (2026-09-04). Its scenarios, the media generation step and
# prove_the_add_photo_invariants left with it; git has them at 64852ae.
prove_the_deafness_gate
enforce_the_time_budget "$STARTED_AT"
}

View File

@ -8,45 +8,6 @@
# and the same bot process the scenarios used.
# ============================================================================
prove_the_add_photo_invariants() {
local pack="${TEMP_DIR}/pack"
echo "== checking every stored file was actually stripped"
"$PYTHON" "$REPO_ROOT/tool/e2e_media.py" --verify-pack "$pack"
echo "== checking a typed label never reached custom.json as a key"
if grep -qE '"(wakacje|piesek)"[[:space:]]*:' "${pack}/custom.json"; then
echo "Error: a typed label became a key in custom.json" >&2
exit 1
fi
echo "== checking !dodaj with no attachment minted nothing"
if grep -q "piesek" "${pack}/custom.json"; then
echo "Error: a mint survived a message that carried no attachment" >&2
exit 1
fi
echo "== checking the refused uploads were not stored"
if grep -q "att-junk" "${pack}/tags.json"; then
echo "Error: a refused file reached tags.json" >&2
exit 1
fi
echo "== checking signal-cli's own copies were deleted"
local left
left="$(curl -sf "${FAKE_URL}/_control/attachments")"
if [[ "$left" == *"att-person"* || "$left" == *"att-custom"* ]]; then
echo "Error: an ingested attachment is still in the container store: ${left}" >&2
exit 1
fi
echo "== checking the adder was recorded for a future !undo_photo"
if [[ ! -s "${pack}/adds.json" ]]; then
echo "Error: adds.json was never written" >&2
exit 1
fi
}
# The gate that did not exist on 2026-08-29, proven against the real process:
# with signal-cli unable to reach Signal, /healthz must go red rather than
# keep answering 200 while nothing arrives.

View File

@ -145,13 +145,6 @@
"!komendy",
"!commands"
],
"add_photo": [
"!dodaj",
"!dodaj_zdjecie",
"!dodajzdjecie",
"!add_photo",
"!addphoto"
],
"random": [
"!losowe",
"!losuj",

View File

@ -23,8 +23,6 @@ from signal_bot.decorators import cooldown
from signal_bot.dispatcher import Dispatcher
from signal_bot.help_command import register_help_command
from signal_bot.http_client import ImageFetcher
from signal_bot.media_strip import ExiftoolStripper
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
@ -178,7 +176,6 @@ def _register_album(registry: CommandRegistry, directory: Path, rate_limit: Deco
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_bot(

View File

@ -109,21 +109,26 @@ def test_two_albums_may_share_a_trigger_when_their_groups_do_not_overlap(
assert first is not second
def test_add_photo_and_random_exist_separately_in_each_album(
def test_random_exists_separately_in_each_album(
tmp_path: Path,
config: Config,
store: Store,
transport: FakeTransport,
directory: GroupDirectory,
) -> None:
"""!dodaj and !losowe carry the same triggers in both, by construction."""
"""!losowe carries the same triggers in both, by construction.
!dodaj used to be asserted here too. It is unwired until an approval
queue exists (2026-09-04), and a word nothing registers is the same in
every group: the scoping this test guards is the album's, not its.
"""
real = _album(tmp_path / "real", WATCHED_GROUP)
test = _album(tmp_path / "test", SECOND_GROUP, key="p9", trigger="!zeta")
registry = _registry(config, store, transport, directory, (real, test))
for group in (WATCHED_GROUP, SECOND_GROUP):
assert _claim(registry, "!dodaj alfa", group) is not None
assert _claim(registry, "!losowe", group) is not None
assert _claim(registry, "!dodaj alfa", group) is None
def test_two_albums_sharing_a_group_are_refused_at_startup(

View File

@ -88,7 +88,7 @@ def test_a_label_that_is_a_command_word_is_not_forbidden(
assert "ciocia_hania" in words
@pytest.mark.parametrize("label", ["losowe", "papiez", "add_photo", "staty"])
@pytest.mark.parametrize("label", ["losowe", "papiez", "komendy", "staty"])
def test_no_command_word_can_be_outlawed_by_a_label(
sources: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, label: str
) -> None: