signal-bot/DOCS-photo-pack.md
Krzysztof kuhy Rudnicki 80c0fa8e45 fix: take real people, numbers and a member's uuid out of a public repository
This repository is public, and five things in it were not invented.

The worst was mine, today: the tagger's help text used the group's actual
first names and nicknames as its worked example, in a file that is on
GitHub. It uses ala/basia/celina now, like every other example here.

The rest predate this work. DOCS-signal-envelopes.md carried the operator's
own phone number and the bot's, captured from a live envelope; conftest.py
had the same number as its trusted inviter; and test_trust.py pinned a group
member's full Signal uuid as a fixture. Numbers keep their shape so the
documents still show what a real envelope looks like, but no digit of them
belongs to anybody.

Comments that reached for a real first name to illustrate the diacritic case
now use an invented one.

None of this removes them from the history, which is a separate decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
2026-08-30 23:01:26 +02:00

4.7 KiB
Raw Blame History

The photo pack

A pack is a private album the bot posts from: !alfa returns a random photo of alfa, and !alfa beta returns one with both of them on it.

Nothing about a real pack is in this repository, and nothing ever will be. Who the people are, what somebody types to get their photo and which file shows whom live in one directory outside the repo. This document, the tests and the offline end-to-end run all use the same invented people — alfa, beta, gamma — so the mechanism can be public while the album stays private.

Where it lives

~/.local/share/signal-bot/pack/
  people.json     # the commands, and the only place names exist
  tags.json       # which file shows whom
  media/          # the photos and videos themselves

Found in this order: PHOTO_PACK_DIR, then $STATE_DIRECTORY/pack (what systemd hands a system unit), then $XDG_DATA_HOME/signal-bot/pack. StateDirectory= is deliberately not in the unit this repo installs: for a user unit systemd resolves it below $XDG_STATE_HOME, which is not where the album is. Discovery finds the same directory, and leaving PHOTO_PACK_DIR unset is what keeps a not-yet-filled-in pack from turning Restart=always into a restart loop.

Two absences, deliberately different:

  • a pack nobody named that is not there, or that has no people.json yet, is an installation without an album: the commands do not exist, and that is the normal case on every machine and in the whole test suite;
  • a pack named in PHOTO_PACK_DIR that is missing, or whose manifest cannot be honoured, fails startup. load_config raising is an answer, and a bot that came up healthy and said "no such photo" forever would not be.

people.json

{
  "groups": ["group.WlRKbExYZGhkR05vWldRdA=="],
  "people": [
    {"key": "p1", "triggers": ["!alfa", "!alfka"]},
    {"key": "p2", "triggers": ["!beta"]}
  ],
  "combos": [
    {"key": "c1", "triggers": ["!oboje"], "all_of": ["p1", "p2"]},
    {"key": "c2", "triggers": ["!ktokolwiek"], "any_of": ["p1", "p2"]}
  ]
}
  • groups is the whole privacy guarantee. In any other group these are not commands at all: not answered, not counted as a command, not named in a log line. A handler that quietly returned would still leak all three, which is why the dispatcher enforces it — registry.resolve_with_args takes the group and gives back nothing outside it.
  • key is a pseudonym, and the only identifier that reaches the journal or the command_uses table. A command is pack:p1; reading either says somebody asked for a photo and nothing about whose.
  • all_of means one photo with all of them; any_of means either, and where a choice exists the bot picks among the readings that actually have a photo, so a combination that could be answered is not refused.
  • Triggers may not contain a digit — folding.py maps 0134578 onto letters, so !alfa7 is not what you typed. Two spellings that fold the same are fine within one entry (!celina/!celiną is one person written twice) and refused across two.

tags.json

{"alfa.png": ["p1"], "both.png": ["p1", "p2"], "clip.mp4": ["p1"]}

A tag naming a file that is not in media/ fails startup: a renamed file would otherwise drop out of the album in silence. An empty list means "seen, nobody in it", which is how the tagger records a photo you skipped.

Videos work. The attachment goes out as data:video/mp4;filename=<name>;base64,<data> — the spelling is the container's own, from its /swagger/doc.json — because told it is a photo, Signal renders a clip as a file nobody can play.

Filling it in: tool/tagger.py

python tool/tagger.py --import ~/photos    # copy the files in, then tag
python tool/tagger.py                      # carry on where you left off

It serves one page on 127.0.0.1:8140: the item, a numbered button per person, 1–9 to toggle, →/← to move. Every toggle rewrites tags.json atomically, so closing the tab loses nothing. Names are typed into the page, never as arguments — an argument would land in the shell history — and the group is picked from the live /v1/groups listing rather than pasted.

When you stop it, it prints how many items were never opened and how many photos each command would have. A command showing 0 is dead on arrival; that is the number to check before restarting the bot, not after somebody in the group tries it.

Running the mechanism without an album

scripts/e2e.sh writes a synthetic pack with tool/e2e_pack.py and proves, offline and with no Signal account, that one name posts a photo, two names intersect, an impossible combination gets one line and no attachment, a video leaves as a video, and the same command in another group produces zero sends.