Signal-Bot der als Dungeon Master via Claude API antwortet. Spieler schreiben in eine Signal-Gruppe (oder 1:1), der Bot antwortet als DM.
Spielwelt-Daten: Engine, Templates und Abenteuer-Struktur kommen aus phieb/ttrpg — wird beim ersten
docker compose upautomatisch geklont.
- signal-cli (
bbernhard/signal-cli-rest-api) — Signal Protokoll - Python 3.11 — Bot-Service
- DM AI — konfigurierbar: OpenAI GPT-4o, Claude Sonnet, oder Gemini (siehe
DM_PROVIDER) - Claude Haiku — Hilfsaufgaben (Charakter-Extraktion, Session-Komprimierung)
- Vertex AI Imagen 4 — Charakter-Portrait-Generierung
- Docker — containerisiert
mkdir ttrpg-signal && cd ttrpg-signal
curl -O https://raw.githubusercontent.com/phieb/ttrpg-signal/main/docker-compose.example.yml
curl -O https://raw.githubusercontent.com/phieb/ttrpg-signal/main/.env.example
cp docker-compose.example.yml docker-compose.yml
cp .env.example .envDer Bot-Code kommt als fertiges Image von ghcr.io/phieb/ttrpg-signal:latest — kein Repo-Clone nötig.
Das ttrpg Engine-Repo wird beim ersten docker compose up automatisch geklont.
Signal-CLI-Daten werden als Bind Mount eingebunden — Pfad anpassen:
signal-cli:
volumes:
- /pfad/zu/signal-cli-data:/home/.local/share/signal-cliWer signal-cli schon laufen hat: einfach den bestehenden Datenpfad eintragen, fertig — keine Neuregistrierung nötig.
docker compose up -d signal-cliAls linked device registrieren — QR-Code generieren:
curl -s "http://localhost:8085/v1/qrcodelink?device_name=ttrpg-bot" -o qrcode.pngPNG öffnen → Signal → Einstellungen → Verknüpfte Geräte → Gerät hinzufügen → scannen.
cp .env.example .env# DM Provider: openai | anthropic | gemini
DM_PROVIDER=openai
OPENAI_API_KEY=sk-... # für DM_PROVIDER=openai
ANTHROPIC_API_KEY=sk-ant-... # immer benötigt (Charakter-Extraktion, Komprimierung)
# GEMINI_API_KEY=... # für DM_PROVIDER=gemini
# Optionale Modell-Overrides (Defaults siehe config.py)
# OPENAI_DM_MODEL=gpt-4o
# ANTHROPIC_DM_MODEL=claude-sonnet-4-6
# GEMINI_DM_MODEL=gemini-2.0-flash
SIGNAL_PHONE_NUMBER=+43... # Bot-Nummer (linked device)
ADMIN_PHONE_NUMBER=+43... # Wer !kommandos schicken darf
GCP_PROJECT=... # GCP Projekt-ID für Vertex AI (Avatar-Generierung)
GCP_LOCATION=us-central1Alle Variablen mit Beschreibung und Defaults: siehe .env.example.
gcloud iam service-accounts create ttrpg-bot \
--display-name="TTRPG Bot" --project=PROJEKT_ID
gcloud projects add-iam-policy-binding PROJEKT_ID \
--member="serviceAccount:ttrpg-bot@PROJEKT_ID.iam.gserviceaccount.com" \
--role="roles/aiplatform.user"
gcloud iam service-accounts keys create gcp-sa.json \
--iam-account="ttrpg-bot@PROJEKT_ID.iam.gserviceaccount.com"gcp-sa.json im Projektordner ablegen (in .gitignore, nie ins Git!).
Addons werden als zusätzliche Volume-Mounts aktiviert — jeder Flavour eines Addons bekommt eine eigene Zeile in docker-compose.yml:
volumes:
- /pfad/zu/mein-addon/flavours/mein-flavour:/mnt/ttrpg/_engine/flavours/mein-flavourKein Code-Change nötig — der Bot erkennt neue Flavour-Ordner automatisch. Wie du ein eigenes Addon baust steht in ttrpg-adult/README.md als Referenzimplementierung (Struktur, manifest.yaml, CHARACTER_FIELDS.yaml).
docker compose up -dBeim ersten Start klont Docker automatisch das ttrpg Engine-Repo und legt status.yaml aus der Vorlage an. Spieler danach per !invite direkt über den Bot registrieren.
Der Bot bietet zwei Modi, um eingehende Signal-Nachrichten zu empfangen.
Standardverhalten — keine Konfiguration nötig. Der Bot ruft signal-cli periodisch
über GET /v1/receive/<number> ab und verarbeitet neue Envelopes. Standalone deploybar,
keine externen Abhängigkeiten.
Statt aktiv zu pollen, kann ein externer Dispatcher (z.B. n8n, ein Reverse-Proxy oder
eine eigene Integration) eingehende Envelopes per POST /receive an den Bot pushen.
Der Polling-Loop läuft in diesem Modus nicht — der Bot wartet ausschließlich auf
Webhook-Aufrufe.
Aktivieren:
RECEIVE_MODE=webhook
WEBHOOK_SECRET=ein_geheimes_token # optional, aber empfohlen
WEBHOOK_PORT=8090 # defaultIn docker-compose.yml den Port veröffentlichen (oder hinter einen Reverse-Proxy stellen):
ttrpg-bot:
ports:
- "8090:8090"Der Dispatcher sendet pro eingehender Signal-Nachricht ein POST:
POST /receive
Content-Type: application/json
Authorization: Bearer <WEBHOOK_SECRET> # oder: X-Webhook-Secret: <WEBHOOK_SECRET>
<envelope-json wie von signal-cli /v1/receive zurückgegeben>
Der Body ist entweder ein einzelnes Envelope-Objekt oder eine Liste — beides wird akzeptiert.
Antwort: 200 OK bei Erfolg, 401 bei falschem Secret.
Health-Check: GET /health → {"status": "ok"}.
Routing (für geteilte Signal-Nummer / mehrere Bots am selben Webhook): Der Dispatcher fragt vor dem
Weiterleiten per POST /claims ab, ob eine Nachricht zu diesem Bot gehört. Der Body ist dasselbe
Envelope, das sonst an /receive ginge — akzeptiert wird das native {"envelope": …}, ein blankes
Envelope oder ein n8n-{"body": {"envelope": …}}. Antwort immer 200 mit {"claims": true} bzw.
{"claims": false}. Beansprucht werden nur Nachrichten, die der Bot auch verarbeiten würde: keine
eigenen Echos, keine unbekannten Absender, Gruppen nur wenn Setup-Kanal oder registrierte
Abenteuer-Gruppe, DMs bekannter Spieler/Admins immer. Read-only, kein Secret nötig — bei Parse-Fehlern
oder nicht zustellbaren Envelopes {"claims": false}.
- Spieler registrieren (einmalig pro Spieler):
!invite +43... Name - Abenteuer anlegen — erstellt Ordnerstruktur, Signal-Gruppe und schickt Willkommenstext:
Optionale Flavours mit
!new Mein Abenteuer @Spieler1 @Spieler2 --fantasy--nameanhängen. Addon-Flavours funktionieren genauso sobald das Addon eingebunden ist. - Session 0 starten (Charaktererstellung + Weltenbau):
!session0
Der Bot finalisiert Session 0 automatisch sobald alle Charakterblätter vollständig sind — er legt YAMLs an, generiert Portraits und schickt Charakterblatt-PDFs in die Gruppe.
Commands work in all three contexts: the adventure group, the private 1:1 setup channel, and direct messages.
| Command | Description |
|---|---|
!help |
Show available commands |
!status |
In group: current adventure state. In DM: list your adventures |
!status <name> |
In DM: details of a named adventure (own adventures only) |
!charakter |
Show your character sheet + PDF |
!charakter <name> |
Find a specific character by name |
!avatar |
Show your current portrait and prompt |
!avatar regen |
Regenerate portrait with the existing prompt |
!avatar prompt |
Show the current image generation prompt |
!avatar prompt <text> |
Update the prompt and regenerate portrait |
!bugreport <text> |
Report a bug |
| Command | Description |
|---|---|
!save |
Compress & save game state → session.yaml, end session |
!session0 |
Start Session 0 — DM leads world-building + intro scene |
!new <name> [@Player1 @Player2 ...] |
Create adventure, Signal group, private setup channels per player |
!invite +43... Name |
Register player (creates players/Name.yaml) + welcome message |
!dm @Player <text> |
Secret 1:1 message to a player |
!players |
List all registered players with number and role |
!showme [idea] |
Generate and send an atmospheric scene image — optional idea as inspiration |
!usage |
API usage & estimated costs (all providers + Vertex AI) |
ttrpg-signal/ ← dieses Repo (Bot-Code)
├── Dockerfile
├── docker-compose.yml
├── .env ← nie ins Git!
├── gcp-sa.json ← nie ins Git!
└── bot/
├── main.py ← Event Loop, Kommando-Router
├── signal_client.py ← signal-cli REST API Wrapper
├── dm_engine.py ← DM-Logik, History, Log, Komprimierung
├── llm_client.py ← AI-Provider-Adapter (OpenAI / Anthropic / Gemini)
├── session_manager.py ← YAML lesen/schreiben, Kontext-Builder
├── generate_avatar.py ← Vertex AI Imagen, Prompt-Verwaltung
├── usage_tracker.py ← Token- und Kosten-Tracking
└── config.py ← alle Env-Variablen
ttrpg/ ← separates Repo, eingebunden via TTRPG_PATH
├── status.yaml ← Abenteuer-Übersicht + Signal-Gruppen
├── status.example.yaml ← Vorlage
├── players/ ← ein YAML pro Spieler (Telefonnummer etc.)
├── _engine/
│ ├── DUNGEON_MASTER.md ← DM System-Prompt
│ ├── CHARACTER_SETUP.md ← privater Setup-Kanal Prompt
│ └── templates/ ← YAML-Vorlagen für neue Abenteuer
└── adventures/
└── mein-abenteuer/
├── session.yaml
├── setting.yaml
├── npcs.yaml
├── spielprotokoll.jsonl ← Crash-sicheres Log (wird bei !save geleert)
└── characters/
├── held.yaml
├── held_avatar.png
└── held_avatar.txt ← Imagen-Prompt (optional, überschreibt YAML-Feld)
| Was | Wo | Wann |
|---|---|---|
| Jede Nachricht | spielprotokoll.jsonl |
sofort (append) |
| History bei Neustart | aus spielprotokoll.jsonl |
beim ersten Zugriff |
| Spielstand/Zusammenfassung | session.yaml |
bei !save (Claude komprimiert) |
| JSONL | geleert | bei !save |
Die Engine-Dateien (_engine/, Templates) kommen aus Git und sind jederzeit wiederherstellbar.
Was nicht in Git liegt und gesichert werden sollte:
| Was | Wo im ttrpg-data Volume |
|---|---|
| Spielstände & Szenen | adventures/*/session.yaml |
| Charakterblätter | adventures/*/characters/*.yaml |
| Portraits & PDFs | adventures/*/characters/*.png, *.pdf |
| Spielprotokolle | adventures/*/spielprotokoll.jsonl |
| Spieler-Registry | players/*.yaml |
| Abenteuer-Übersicht | status.yaml |
Einfachstes Backup — ttrpg/-Ordner sichern (Bind Mount, direkt zugänglich):
tar czf /pfad/zu/backup/ttrpg-backup-$(date +%Y%m%d).tar.gz -C /pfad/zu/ttrpg-signal ttrpgGitHub Actions baut bei jedem Push auf main automatisch ein neues Image und pusht es nach ghcr.io/phieb/ttrpg-signal:latest. Update auf dem Server:
docker compose pull ttrpg-bot
docker compose up -d ttrpg-bot