# 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 ```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 ```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=;base64,` — 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 ```sh 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.