mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 12:37:32 +01:00
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
110 lines
4.7 KiB
Markdown
110 lines
4.7 KiB
Markdown
# 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=<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
|
||
|
||
```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.
|