# 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 custom.json # labels approved from !add_photo (optional) adds.json # who added what, read by nothing (optional) media/ # the photos and videos themselves pending/ # !add_photo sends waiting for approval (optional) ``` 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 keeps a not-yet-filled-in pack from turning `Restart=always` into a 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. ### More than one album `PHOTO_PACK_DIR` takes a **comma-separated list**, the same spelling `GROUP_IDS` and `TRUSTED_INVITERS` use: ```sh PHOTO_PACK_DIR=~/.local/share/signal-bot/pack,~/.local/share/signal-bot/pack-testbot ``` Each album carries its own group scope in its own `people.json`, and the two share nothing but the process: separate people, separate tags, separate custom labels. That is what lets a test group have working `!add_photo` and `!losowe` over invented people while the real album does not exist there at all — which one album cannot do, because its `groups` list *is* the privacy guarantee. Trigger collisions between albums are therefore legal. Two albums are two sets of people and both may call somebody `!alfa`; no message can ever mean both, because a command a group is not scoped to is not a command there. `!dodaj` and `!losowe` are registered once per album for exactly this reason. **Two albums whose groups overlap are refused at startup** with `DuplicateTriggerError`. Overlapping scopes would make "which album does `!dodaj` write to?" a coin toss, and a coin toss about which private album receives a photo is the outcome this whole design exists to prevent. ## 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. ## custom.json ```json {"t1": ["wakacje2019"], "t2": ["gorybieszczady"]} ``` Labels `!add_photo` learned from the chat that match no person. The key is a pseudonym in the same family as `p1` — it is what reaches `tags.json`, a log line and a reply — and the label is the only place the typed word is kept. Custom tags define **no commands**: `!random