From d57086cd000baa91b934a1f2c94c3c2801349801 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Wed, 8 Jul 2026 12:13:13 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20unified=20messaging=20TUI=20MVP=20?= =?UTF-8?q?=E2=80=94=20Synapse=20+=207=20mautrix=20bridges=20+=20Textual?= =?UTF-8?q?=20TUI=20+=20MCP=20draft=20staging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One-command local stack: cp .env.example .env && make up. Idempotent bootstrap renders Synapse + bridge configs from pristine upstream examples, provisions databases, registrations, double puppeting, and the admin user. Textual TUI with bridge manager, in-terminal QR login, and drafts panel. MCP server exposes read tools and a human-gated draft send workflow. Co-Authored-By: Claude Fable 5 --- .env.example | 39 ++ .gitignore | 5 + Makefile | 28 ++ README.md | 149 +++++++ bootstrap/Dockerfile | 5 + bootstrap/bootstrap.sh | 181 +++++++++ bootstrap/provision.sh | 55 +++ bridges/templates/discord.yaml | 381 ++++++++++++++++++ bridges/templates/gmessages.yaml | 534 +++++++++++++++++++++++++ bridges/templates/linkedin.yaml | 520 ++++++++++++++++++++++++ bridges/templates/meta.yaml | 581 +++++++++++++++++++++++++++ bridges/templates/telegram.yaml | 641 ++++++++++++++++++++++++++++++ bridges/templates/twitter.yaml | 533 +++++++++++++++++++++++++ bridges/templates/whatsapp.yaml | 640 +++++++++++++++++++++++++++++ docker-compose.yml | 203 ++++++++++ mcp/Dockerfile | 19 + mcp/pyproject.toml | 22 + mcp/server/vortex_mcp/__init__.py | 3 + mcp/server/vortex_mcp/server.py | 319 +++++++++++++++ scripts/verify.sh | 101 +++++ shared/drafts/.gitkeep | 0 synapse/homeserver.template.yaml | 60 +++ tui/Dockerfile | 19 + tui/app/vortex_tui/__init__.py | 3 + tui/app/vortex_tui/drafts.py | 53 +++ tui/app/vortex_tui/main.py | 369 +++++++++++++++++ tui/app/vortex_tui/qr.py | 35 ++ tui/app/vortex_tui/session.py | 82 ++++ tui/pyproject.toml | 22 + 29 files changed, 5602 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 bootstrap/Dockerfile create mode 100644 bootstrap/bootstrap.sh create mode 100644 bootstrap/provision.sh create mode 100644 bridges/templates/discord.yaml create mode 100644 bridges/templates/gmessages.yaml create mode 100644 bridges/templates/linkedin.yaml create mode 100644 bridges/templates/meta.yaml create mode 100644 bridges/templates/telegram.yaml create mode 100644 bridges/templates/twitter.yaml create mode 100644 bridges/templates/whatsapp.yaml create mode 100644 docker-compose.yml create mode 100644 mcp/Dockerfile create mode 100644 mcp/pyproject.toml create mode 100644 mcp/server/vortex_mcp/__init__.py create mode 100644 mcp/server/vortex_mcp/server.py create mode 100644 scripts/verify.sh create mode 100644 shared/drafts/.gitkeep create mode 100644 synapse/homeserver.template.yaml create mode 100644 tui/Dockerfile create mode 100644 tui/app/vortex_tui/__init__.py create mode 100644 tui/app/vortex_tui/drafts.py create mode 100644 tui/app/vortex_tui/main.py create mode 100644 tui/app/vortex_tui/qr.py create mode 100644 tui/app/vortex_tui/session.py create mode 100644 tui/pyproject.toml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..82ab2b9 --- /dev/null +++ b/.env.example @@ -0,0 +1,39 @@ +# ── Vortex unified messaging stack ───────────────────────────────── +# Copy to .env, edit, then `make up`. Nothing else to configure. + +# Matrix server name (domain part of user IDs). Keep `localhost` for local use. +MATRIX_SERVER_NAME=localhost + +# Admin account created automatically on first boot. +MATRIX_USER=admin +MATRIX_PASSWORD=changeme + +# Postgres superuser password (user: vortex). +POSTGRES_PASSWORD=changeme + +# Shared secret for Synapse admin registration. `auto` = generated once and +# persisted to the data volume. +REGISTRATION_SHARED_SECRET=auto + +# Shared secret for bridge double puppeting (appservice method). `auto` = generated. +DOUBLEPUPPET_SHARED_SECRET=auto + +# Which bridges to run. Comma-separated, no spaces. +# Full set: gmessages,telegram,whatsapp,twitter,linkedin,discord,meta +BRIDGES_ENABLED=gmessages,telegram,whatsapp,twitter,linkedin,discord,meta +# Compose starts exactly the bridges listed above (do not edit this line): +COMPOSE_PROFILES=${BRIDGES_ENABLED} + +# Telegram needs API credentials from https://my.telegram.org. +# Leave empty to skip the Telegram bridge gracefully (its container idles with a log line). +TELEGRAM_API_ID= +TELEGRAM_API_HASH= + +# MCP server (streamable HTTP) host port. +MCP_HTTP_PORT=8765 + +# When false (default), the MCP send_draft tool refuses to send; a human must +# send drafts from the TUI. Set true to let AI agents send staged drafts. +MCP_ALLOW_SEND=false + +TZ=UTC diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5bbc72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +shared/drafts/*.db* +**/__pycache__/ +*.egg-info/ +.venv*/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d95802c --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +.PHONY: up down logs bootstrap tui verify clean nuke + +up: .env + docker compose up -d --build --wait + +.env: + @test -f .env || { echo "No .env found - copy .env.example to .env first:"; echo " cp .env.example .env"; exit 1; } + +down: + docker compose down + +logs: + docker compose logs -f --tail=100 + +bootstrap: + docker compose run --rm bootstrap + +tui: + docker compose run --rm --build tui + +verify: + bash scripts/verify.sh + +clean: + docker compose down --remove-orphans + +nuke: + docker compose --profile '*' down -v --remove-orphans diff --git a/README.md b/README.md new file mode 100644 index 0000000..73276da --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +# Vortex — unified messaging terminal client + +All your chat networks (WhatsApp, Telegram, Google Messages, X/Twitter, LinkedIn, +Discord, Instagram) in one terminal UI, built on Matrix + mautrix bridges, with an +MCP server that lets AI agents read your messages and stage drafts — but never +send without a human. + +``` +┌────────────┐ ┌─────────┐ ┌──────────────────────────────┐ +│ Textual TUI│──▶│ Synapse │◀──│ mautrix bridges (7 networks) │ +└─────┬──────┘ │ +Postgres│ └──────────────────────────────┘ + │ drafts └────┬────┘ + ▼ SQLite │ +┌────────────┐ │ +│ MCP server │────────┘ read tools + draft staging for AI agents +└────────────┘ +``` + +## Quickstart + +```sh +cp .env.example .env # edit MATRIX_PASSWORD / POSTGRES_PASSWORD at minimum +make up # builds + provisions + starts everything +make tui # open the terminal client (already logged in) +make verify # smoke-test the whole stack +``` + +That is the entire setup. Bootstrap renders the Synapse config, creates +databases, generates bridge configs + appservice registrations, registers the +admin user, and writes credentials to a shared volume. Re-running `make up` is +a no-op. No YAML editing, no token copying. + +## Make targets + +| target | what | +|---|---| +| `make up` | build + start the full stack, wait for healthy | +| `make tui` | run the Textual TUI (interactive) | +| `make verify` | smoke tests: health, appservice round-trip, MCP draft gating | +| `make logs` | follow all logs | +| `make down` | stop containers | +| `make nuke` | stop and delete **all data** (volumes included) | + +## Logging into bridges + +Open the TUI, press `b` for the bridge screen, pick a network — it opens a DM +with that bridge's bot. Send `help` to list commands and `login` to start the +login flow. QR codes sent by bots render in-terminal. + +| network | bot | login flow | +|---|---|---| +| WhatsApp | `@whatsappbot` | `login qr` (scan with phone) or `login pairing-code` | +| Google Messages | `@gmessagesbot` | `login` → QR scan from the Messages app | +| Telegram | `@telegrambot` | `login` → phone number + code. **Requires `TELEGRAM_API_ID`/`TELEGRAM_API_HASH` in `.env`** (get them at [my.telegram.org](https://my.telegram.org)). Without them the bridge is skipped with a log line. | +| X/Twitter | `@twitterbot` | `login cookies` — paste browser cookies | +| LinkedIn | `@linkedinbot` | `login cookies` | +| Discord | `@discordbot` | `login qr` (scan with Discord app) or `login token` | +| Instagram | `@metabot` | `login cookies` (bridge runs in `instagram` mode) | + +Cookie flows: the bot explains exactly which cookies to paste. Double puppeting +is pre-configured for all bridges — your own messages sent from other devices +appear as you. + +## The TUI + +Three panes: room list grouped by network (with `[wa] [tg] [gm] [tw] [li] [dc] +[ig] [mx]` labels and unread counts), message timeline, input bar. + +Keys: `q` quit · `b` bridge manager · `d` drafts panel · `tab` cycle focus · +`enter` (in room list) open room. Type `/reply ` to reply to the last +message. Media shows as download links. + +The drafts panel lists drafts staged by AI agents via MCP (live, 2s poll): +`e` edit · `s` send · `x` discard. This is the human approval step. + +Native run (no Docker): `pip install -e ./tui`, then +`MATRIX_HOMESERVER=http://localhost:8008 vortex-tui` (needs the shared volume +mounted or `CREDENTIALS_FILE`/`DRAFTS_DB`/`STORE_DIR` pointed somewhere useful). + +## MCP server for AI agents + +Runs at `http://localhost:8765/mcp` (streamable HTTP) and over stdio. + +Tools: `list_rooms`, `read_messages`, `search_messages`, `create_draft`, +`list_drafts`, `update_draft`, `discard_draft`, `send_draft`. + +`create_draft` never sends. `send_draft` is refused while `MCP_ALLOW_SEND=false` +(the default) — the agent is told a human must send from the TUI. Drafts appear +in the TUI drafts panel live. + +Claude Code: + +```sh +claude mcp add --transport http vortex http://localhost:8765/mcp +``` + +Claude Desktop (`claude_desktop_config.json`), HTTP transport: + +```json +{ "mcpServers": { "vortex": { "url": "http://localhost:8765/mcp" } } } +``` + +stdio transport (spawns inside the running container): + +```json +{ "mcpServers": { "vortex": { + "command": "docker", + "args": ["compose", "-f", "/absolute/path/to/vortex/docker-compose.yml", + "exec", "-i", "mcp", "vortex-mcp", "--stdio"] } } } +``` + +## Environment variables + +See `.env.example` — every value is documented inline. Highlights: + +- `BRIDGES_ENABLED` — comma-separated bridge list; compose starts exactly these. + After changing it, run `make down && make up`. +- `REGISTRATION_SHARED_SECRET` / `DOUBLEPUPPET_SHARED_SECRET` — leave as `auto`; + generated once and persisted in the data volume. +- `MCP_ALLOW_SEND` — set `true` only if you want AI agents to send without you. + +## How provisioning works + +`bootstrap` (one-shot, runs before Synapse) renders `homeserver.yaml` from +`synapse/homeserver.template.yaml`, generates a signing key and secrets, creates +one Postgres DB per bridge, patches each bridge's config from the pristine +upstream example in `bridges/templates/` (homeserver address, appservice +address, Postgres URI, admin permissions, double-puppet secret; Telegram API +creds; Instagram mode), and writes matching `registration.yaml` files for +Synapse *and* each bridge. `provision` (one-shot, after Synapse is healthy) +registers the admin user via the shared-secret admin API and writes +`credentials.json` to the shared volume; the TUI and MCP server log in from it +with their own devices. Everything is keyed on "file already exists" — re-runs +change nothing. + +## Troubleshooting + +- **`The as_token was not accepted` in a bridge log** — registration drift; + run `make down && make up` (bootstrap rewrites registrations from persisted + tokens). If it persists: `make nuke && make up`. +- **Telegram container prints "skipped"** — expected without + `TELEGRAM_API_ID`/`TELEGRAM_API_HASH`. Set them, then `make down && make up`. +- **Bridge bot doesn't answer** — `docker compose logs `; the bridge + must show a successful websocket/appservice connection to Synapse. +- **TUI can't log in** — check `docker compose logs provision`; delete + `tui-store/session.json` in the shared volume to force a fresh login. +- **Changed `MATRIX_SERVER_NAME` after first boot** — Synapse server names are + permanent; `make nuke && make up`. +- **Ports busy** — Synapse uses host `8008`, MCP uses `MCP_HTTP_PORT` (8765). diff --git a/bootstrap/Dockerfile b/bootstrap/Dockerfile new file mode 100644 index 0000000..25fbd17 --- /dev/null +++ b/bootstrap/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine:3.20 +RUN apk add --no-cache bash curl openssl yq gettext postgresql16-client python3 +COPY bootstrap.sh provision.sh / +RUN chmod +x /bootstrap.sh /provision.sh +CMD ["/bootstrap.sh"] diff --git a/bootstrap/bootstrap.sh b/bootstrap/bootstrap.sh new file mode 100644 index 0000000..47ee0d9 --- /dev/null +++ b/bootstrap/bootstrap.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# Idempotent one-shot provisioning: renders Synapse + bridge configs, creates +# bridge databases, generates registrations. Safe to re-run on every `up`. +set -euo pipefail + +DATA=/data +SECRETS=$DATA/secrets +DOMAIN=${MATRIX_SERVER_NAME:-localhost} +ADMIN_MXID="@${MATRIX_USER:-admin}:${DOMAIN}" +ALL_BRIDGES="gmessages telegram whatsapp twitter linkedin discord meta" + +log() { echo "[bootstrap] $*"; } + +mkdir -p "$SECRETS" "$DATA/synapse/appservices" "$DATA/shared/drafts" "$DATA/shared/tui-store" "$DATA/shared/mcp-store" +for b in $ALL_BRIDGES; do mkdir -p "$DATA/bridges/$b"; done # subpath mounts need these even for disabled bridges +chmod -R a+rwX "$DATA/shared" + +# ── secrets ───────────────────────────────────────────────────────── +# secret_of NAME ENV_VALUE: if env value is "auto"/"auto-or-changeme"/empty, +# generate once and persist; else use env value verbatim. +secret_of() { + local name=$1 val=${2:-auto} f=$SECRETS/$1 + case "$val" in auto|auto-or-changeme|"") + [ -f "$f" ] || openssl rand -hex 32 > "$f" + cat "$f" ;; + *) echo "$val" ;; + esac +} +REG_SECRET=$(secret_of registration_shared_secret "${REGISTRATION_SHARED_SECRET:-auto}") +DP_SECRET=$(secret_of doublepuppet_shared_secret "${DOUBLEPUPPET_SHARED_SECRET:-auto}") +MACAROON=$(secret_of macaroon_secret_key auto) +FORM=$(secret_of form_secret auto) + +# per-bridge appservice tokens, generated once +bridge_token() { # bridge kind(as|hs) + local f=$SECRETS/${1}_${2}_token + [ -f "$f" ] || openssl rand -hex 32 > "$f" + cat "$f" +} + +# ── synapse ───────────────────────────────────────────────────────── +if [ ! -f "$DATA/synapse/signing.key" ]; then + echo "ed25519 a_$(openssl rand -hex 2) $(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')" > "$DATA/synapse/signing.key" + log "generated synapse signing key" +fi + +cat > "$DATA/synapse/log.config" <<'EOF' +version: 1 +formatters: + precise: + format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' +handlers: + console: + class: logging.StreamHandler + formatter: precise +loggers: + synapse.storage.SQL: + level: INFO +root: + level: INFO + handlers: [console] +disable_existing_loggers: false +EOF + +IFS=',' read -ra ENABLED <<< "${BRIDGES_ENABLED:-}" +enabled() { local b; for b in "${ENABLED[@]}"; do [ "$b" = "$1" ] && return 0; done; return 1; } + +# telegram needs API creds; drop it from the effective set if missing +telegram_ok=true +if enabled telegram && { [ -z "${TELEGRAM_API_ID:-}" ] || [ -z "${TELEGRAM_API_HASH:-}" ]; }; then + telegram_ok=false + log "WARNING: telegram enabled but TELEGRAM_API_ID/TELEGRAM_API_HASH empty - skipping telegram bridge" +fi + +export MATRIX_SERVER_NAME="$DOMAIN" POSTGRES_PASSWORD REGISTRATION_SHARED_SECRET="$REG_SECRET" \ + MACAROON_SECRET_KEY="$MACAROON" FORM_SECRET="$FORM" +envsubst < /templates/synapse/homeserver.template.yaml > "$DATA/synapse/homeserver.yaml.new" + +# ── double puppet appservice ──────────────────────────────────────── +DP_HS_TOKEN=$(bridge_token doublepuppet hs) +cat > "$DATA/synapse/appservices/doublepuppet.yaml" <> "$DATA/synapse/homeserver.yaml.new" +echo " - /data/appservices/doublepuppet.yaml" >> "$DATA/synapse/homeserver.yaml.new" + +# ── bridges ───────────────────────────────────────────────────────── +pg() { PGPASSWORD=$POSTGRES_PASSWORD psql -h postgres -U vortex -d postgres -qtAc "$1"; } + +for b in $ALL_BRIDGES; do + enabled "$b" || continue + [ "$b" = telegram ] && [ "$telegram_ok" = false ] && continue + + db="mautrix_$b" + if [ "$(pg "SELECT 1 FROM pg_database WHERE datname='$db'")" != 1 ]; then + pg "CREATE DATABASE $db OWNER vortex" >/dev/null + log "created database $db" + fi + + mkdir -p "$DATA/bridges/$b" + cfg=$DATA/bridges/$b/config.yaml + AS_TOKEN=$(bridge_token "$b" as) + HS_TOKEN=$(bridge_token "$b" hs) + port=$(yq '.appservice.port' "/templates/bridges/$b.yaml") + + if [ ! -f "$cfg" ]; then + cp "/templates/bridges/$b.yaml" "$cfg" + export B="$b" PORT="$port" AS_TOKEN HS_TOKEN DP_SECRET DOMAIN ADMIN_MXID \ + DB_URI="postgres://vortex:${POSTGRES_PASSWORD}@postgres/${db}?sslmode=disable" + yq -i ' + .homeserver.address = "http://synapse:8008" | + .homeserver.domain = strenv(DOMAIN) | + .appservice.address = "http://" + strenv(B) + ":" + (strenv(PORT) | tostring) | + .appservice.hostname = "0.0.0.0" | + .appservice.as_token = strenv(AS_TOKEN) | + .appservice.hs_token = strenv(HS_TOKEN) + ' "$cfg" + if [ "$b" = discord ]; then # legacy config layout + yq -i ' + .appservice.database.type = "postgres" | + .appservice.database.uri = strenv(DB_URI) | + .bridge.permissions = {"*": "relay", strenv(DOMAIN): "user", strenv(ADMIN_MXID): "admin"} | + .bridge.login_shared_secret_map = {strenv(DOMAIN): ("as_token:" + strenv(DP_SECRET))} + ' "$cfg" + else # bridgev2 layout + yq -i ' + .database.type = "postgres" | + .database.uri = strenv(DB_URI) | + .bridge.permissions = {"*": "relay", strenv(DOMAIN): "user", strenv(ADMIN_MXID): "admin"} | + .double_puppet.servers = {} | + .double_puppet.secrets = {strenv(DOMAIN): ("as_token:" + strenv(DP_SECRET))} | + .provisioning.shared_secret = "disable" + ' "$cfg" + fi + if [ "$b" = telegram ]; then + yq -i '.network.api_id = (strenv(TELEGRAM_API_ID) | tonumber) | .network.api_hash = strenv(TELEGRAM_API_HASH)' "$cfg" + fi + if [ "$b" = meta ]; then + yq -i '.network.mode = "instagram"' "$cfg" + fi + log "rendered $b config" + fi + + # registration: regenerated deterministically from persisted tokens. + # Written to the bridge dir too, else the mautrix docker-run.sh entrypoint + # regenerates it with fresh tokens and clobbers the ones in config.yaml. + esc_domain=$(echo "$DOMAIN" | sed 's/\./\\./g') + cat > "$DATA/bridges/$b/registration.yaml" <> "$DATA/synapse/homeserver.yaml.new" + log "bridge $b ready (port $port)" +done + +mv "$DATA/synapse/homeserver.yaml.new" "$DATA/synapse/homeserver.yaml" +chown -R 991:991 "$DATA/synapse" # synapse image runs as uid 991 +log "done" diff --git a/bootstrap/provision.sh b/bootstrap/provision.sh new file mode 100644 index 0000000..bf41b7d --- /dev/null +++ b/bootstrap/provision.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# One-shot post-synapse provisioning: create admin user (idempotent) and +# write /data/shared/credentials.json for the TUI and MCP server. +set -euo pipefail + +DATA=/data +DOMAIN=${MATRIX_SERVER_NAME:-localhost} +USER=${MATRIX_USER:-admin} +PASS=${MATRIX_PASSWORD:?} +MXID="@${USER}:${DOMAIN}" +HS=http://synapse:8008 +REG_SECRET=$(cat $DATA/secrets/registration_shared_secret 2>/dev/null || true) + +log() { echo "[provision] $*"; } + +# already provisioned and token still valid? then no-op +if [ -f "$DATA/shared/credentials.json" ]; then + TOK=$(yq -p json '.access_token' "$DATA/shared/credentials.json") + if curl -sf -H "Authorization: Bearer $TOK" "$HS/_matrix/client/v3/account/whoami" >/dev/null; then + log "credentials already valid, nothing to do" + exit 0 + fi +fi + +login() { + curl -sf -X POST "$HS/_matrix/client/v3/login" \ + -d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"$USER\"},\"password\":$(python3 -c "import json,sys;print(json.dumps(sys.argv[1]))" "$PASS"),\"initial_device_display_name\":\"provision\"}" +} + +RESP=$(login || true) +if [ -z "$RESP" ]; then + log "admin user missing, registering $MXID" + NONCE=$(curl -sf "$HS/_synapse/admin/v1/register" | yq -p json '.nonce') + MAC=$(printf '%s\0%s\0%s\0admin' "$NONCE" "$USER" "$PASS" | openssl dgst -sha1 -hmac "$REG_SECRET" | awk '{print $NF}') + RESP=$(python3 - "$NONCE" "$USER" "$PASS" "$MAC" <<'EOF' | curl -sf -X POST "$HS/_synapse/admin/v1/register" -d @- +import json, sys +n, u, p, m = sys.argv[1:5] +print(json.dumps({"nonce": n, "username": u, "password": p, "admin": True, "mac": m})) +EOF + ) + [ -n "$RESP" ] || { log "registration failed"; exit 1; } +fi + +TOKEN=$(echo "$RESP" | yq -p json '.access_token') +DEVICE=$(echo "$RESP" | yq -p json '.device_id') + +python3 - "$HS" "$MXID" "$PASS" "$TOKEN" "$DEVICE" <<'EOF' > $DATA/shared/credentials.json.new +import json, sys +hs, mxid, pw, tok, dev = sys.argv[1:6] +print(json.dumps({"homeserver": hs, "user_id": mxid, "password": pw, + "access_token": tok, "device_id": dev}, indent=2)) +EOF +mv $DATA/shared/credentials.json.new $DATA/shared/credentials.json +chmod a+rw $DATA/shared/credentials.json +log "credentials written for $MXID" diff --git a/bridges/templates/discord.yaml b/bridges/templates/discord.yaml new file mode 100644 index 0000000..bfe30d4 --- /dev/null +++ b/bridges/templates/discord.yaml @@ -0,0 +1,381 @@ +# Homeserver details. +homeserver: + # The address that this appservice can use to connect to the homeserver. + address: https://matrix.example.com + # The domain of the homeserver (also known as server_name, used for MXIDs, etc). + domain: example.com + + # What software is the homeserver running? + # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. + software: standard + # The URL to push real-time bridge status to. + # If set, the bridge will make POST requests to this URL whenever a user's discord connection state changes. + # The bridge will use the appservice as_token to authorize requests. + status_endpoint: null + # Endpoint for reporting per-message status. + message_send_checkpoint_endpoint: null + # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? + async_media: false + + # Should the bridge use a websocket for connecting to the homeserver? + # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy, + # mautrix-asmux (deprecated), and hungryserv (proprietary). + websocket: false + # How often should the websocket be pinged? Pinging will be disabled if this is zero. + ping_interval_seconds: 0 + +# Application service host/registration related details. +# Changing these values requires regeneration of the registration. +appservice: + # The address that the homeserver can use to connect to this appservice. + address: http://localhost:29334 + + # The hostname and port where this appservice should listen. + hostname: 0.0.0.0 + port: 29334 + + # Database config. + database: + # The database type. "sqlite3-fk-wal" and "postgres" are supported. + type: postgres + # The database URI. + # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended. + # https://github.com/mattn/go-sqlite3#connection-string + # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable + # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql + uri: postgres://user:password@host/database?sslmode=disable + # Maximum number of connections. Mostly relevant for Postgres. + max_open_conns: 20 + max_idle_conns: 2 + # Maximum connection idle time and lifetime before they're closed. Disabled if null. + # Parsed with https://pkg.go.dev/time#ParseDuration + max_conn_idle_time: null + max_conn_lifetime: null + + # The unique ID of this appservice. + id: discord + # Appservice bot details. + bot: + # Username of the appservice bot. + username: discordbot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + displayname: Discord bridge bot + avatar: mxc://maunium.net/nIdEykemnwdisvHbpxflpDlC + + # Whether or not to receive ephemeral events via appservice transactions. + # Requires MSC2409 support (i.e. Synapse 1.22+). + ephemeral_events: true + + # Should incoming events be handled asynchronously? + # This may be necessary for large public instances with lots of messages going through. + # However, messages will not be guaranteed to be bridged in the same order they were sent in. + async_transactions: false + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + +# Bridge config +bridge: + # Localpart template of MXIDs for Discord users. + # {{.}} is replaced with the internal ID of the Discord user. + username_template: discord_{{.}} + # Displayname template for Discord users. This is also used as the room name in DMs if private_chat_portal_meta is enabled. + # Available variables: + # .ID - Internal user ID + # .Username - Legacy display/username on Discord + # .GlobalName - New displayname on Discord + # .Discriminator - The 4 numbers after the name on Discord + # .Bot - Whether the user is a bot + # .System - Whether the user is an official system user + # .Webhook - Whether the user is a webhook and is not an application + # .Application - Whether the user is an application + displayname_template: '{{if .Webhook}}Webhook{{else}}{{or .GlobalName .Username}}{{if .Bot}} (bot){{end}}{{end}}' + # Displayname template for Discord channels (bridged as rooms, or spaces when type=4). + # Available variables: + # .Name - Channel name, or user displayname (pre-formatted with displayname_template) in DMs. + # .ParentName - Parent channel name (used for categories). + # .GuildName - Guild name. + # .NSFW - Whether the channel is marked as NSFW. + # .Type - Channel type (see values at https://github.com/bwmarrin/discordgo/blob/v0.25.0/structs.go#L251-L267) + channel_name_template: '{{if or (eq .Type 3) (eq .Type 4)}}{{.Name}}{{else}}#{{.Name}}{{end}}' + # Displayname template for Discord guilds (bridged as spaces). + # Available variables: + # .Name - Guild name + guild_name_template: '{{.Name}}' + # Whether to explicitly set the avatar and room name for private chat portal rooms. + # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms. + # If set to `always`, all DM rooms will have explicit names and avatars set. + # If set to `never`, DM rooms will never have names and avatars set. + private_chat_portal_meta: default + + # Publicly accessible base URL that Discord can use to reach the bridge, used for avatars in relay mode. + # If not set, avatars will not be bridged. Only the /mautrix-discord/avatar/{server}/{id}/{hash} endpoint is used on this address. + # This should not have a trailing slash, the endpoint above will be appended to the provided address. + public_address: null + # A random key used to sign the avatar URLs. The bridge will only accept requests with a valid signature. + avatar_proxy_key: generate + + portal_message_buffer: 128 + + # Number of private channel portals to create on bridge startup. + # Other portals will be created when receiving messages. + startup_private_channel_create_limit: 5 + # Should the bridge send a read receipt from the bridge bot when a message has been sent to Discord? + delivery_receipts: false + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Whether the bridge should send error notices via m.notice events when a message fails to bridge. + message_error_notices: true + # Should the bridge use space-restricted join rules instead of invite-only for guild rooms? + # This can avoid unnecessary invite events in guild rooms when members are synced in. + restricted_rooms: false + # Should the bridge automatically join the user to threads on Discord when the thread is opened on Matrix? + # This only works with clients that support thread read receipts (MSC3771 added in Matrix v1.4). + autojoin_thread_on_open: true + # Should inline fields in Discord embeds be bridged as HTML tables to Matrix? + # Tables aren't supported in all clients, but are the only way to emulate the Discord inline field UI. + embed_fields_as_tables: true + # Should guild channels be muted when the portal is created? This only meant for single-user instances, + # it won't mute it for all users if there are multiple Matrix users in the same Discord guild. + mute_channels_on_create: false + # Should the bridge update the m.direct account data event when double puppeting is enabled. + # Note that updating the m.direct event is not atomic (except with mautrix-asmux) + # and is therefore prone to race conditions. + sync_direct_chat_list: false + # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run. + # This field will automatically be changed back to false after it, except if the config file is not writable. + resend_bridge_info: false + # Should incoming custom emoji reactions be bridged as mxc:// URIs? + # If set to false, custom emoji reactions will be bridged as the shortcode instead, and the image won't be available. + custom_emoji_reactions: true + # Should the bridge attempt to completely delete portal rooms when a channel is deleted on Discord? + # If true, the bridge will try to kick Matrix users from the room. Otherwise, the bridge only makes ghosts leave. + delete_portal_on_channel_delete: false + # Should the bridge delete all portal rooms when you leave a guild on Discord? + # This only applies if the guild has no other Matrix users on this bridge instance. + delete_guild_on_leave: true + # Whether or not created rooms should have federation enabled. + # If false, created portal rooms will never be federated. + federate_rooms: true + # Prefix messages from webhooks with the profile info? This can be used along with a custom displayname_template + # to better handle webhooks that change their name all the time (like ones used by bridges). + # + # This will use the fallback mode in MSC4144, which means clients that support MSC4144 will not show the prefix + # (and will instead show the name and avatar as the message sender). + prefix_webhook_messages: true + # Bridge webhook avatars? + enable_webhook_avatars: false + # Should the bridge upload media to the Discord CDN directly before sending the message when using a user token, + # like the official client does? The other option is sending the media in the message send request as a form part + # (which is always used by bots and webhooks). + use_discord_cdn_upload: true + # Proxy for Discord connections + proxy: + # Should mxc uris copied from Discord be cached? + # This can be `never` to never cache, `unencrypted` to only cache unencrypted mxc uris, or `always` to cache everything. + # If you have a media repo that generates non-unique mxc uris, you should set this to never. + cache_media: unencrypted + # Settings for converting Discord media to custom mxc:// URIs instead of reuploading. + # More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html + direct_media: + # Should custom mxc:// URIs be used instead of reuploading media? + enabled: false + # The server name to use for the custom mxc:// URIs. + # This server name will effectively be a real Matrix server, it just won't implement anything other than media. + # You must either set up .well-known delegation from this domain to the bridge, or proxy the domain directly to the bridge. + server_name: discord-media.example.com + # Optionally a custom .well-known response. This defaults to `server_name:443` + well_known_response: + # The bridge supports MSC3860 media download redirects and will use them if the requester supports it. + # Optionally, you can force redirects and not allow proxying at all by setting this to false. + allow_proxy: true + # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file. + # This key is also used to sign the mxc:// URIs to ensure only the bridge can generate them. + server_key: generate + # Settings for converting animated stickers. + animated_sticker: + # Format to which animated stickers should be converted. + # disable - No conversion, send as-is (lottie JSON) + # png - converts to non-animated png (fastest) + # gif - converts to animated gif + # webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support + # webp - converts to animated webp, requires ffmpeg executable with webp codec/container support + target: webp + # Arguments for converter. All converters take width and height. + args: + width: 320 + height: 320 + fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended) + # Servers to always allow double puppeting from + double_puppet_server_map: + example.com: https://example.com + # Allow using double puppeting from any server with a valid client .well-known file. + double_puppet_allow_discovery: false + # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth + # + # If set, double puppeting will be enabled automatically for local users + # instead of users having to find an access token and run `login-matrix` + # manually. + login_shared_secret_map: + example.com: foobar + + # The prefix for commands. Only required in non-management rooms. + command_prefix: '!discord' + # Messages sent upon joining a management room. + # Markdown is supported. The defaults are listed below. + management_room_text: + # Sent when joining a room. + welcome: "Hello, I'm a Discord bridge bot." + # Sent when joining a management room and the user is already logged in. + welcome_connected: "Use `help` for help." + # Sent when joining a management room and the user is not logged in. + welcome_unconnected: "Use `help` for help or `login` to log in." + # Optional extra text sent when joining a management room. + additional_help: "" + + # Settings for backfilling messages. + backfill: + # Limits for forward backfilling. + forward_limits: + # Initial backfill (when creating portal). 0 means backfill is disabled. + # A special unlimited value is not supported, you must set a limit. Initial backfill will + # fetch all messages first before backfilling anything, so high limits can take a lot of time. + initial: + dm: 0 + channel: 0 + thread: 0 + # Missed message backfill (on startup). + # 0 means backfill is disabled, -1 means fetch all messages since last bridged message. + # When using unlimited backfill (-1), messages are backfilled as they are fetched. + # With limits, all messages up to the limit are fetched first and backfilled afterwards. + missed: + dm: 0 + channel: 0 + thread: 0 + # Maximum members in a guild to enable backfilling. Set to -1 to disable limit. + # This can be used as a rough heuristic to disable backfilling in channels that are too active. + # Currently only applies to missed message backfill. + max_guild_members: -1 + + # End-to-bridge encryption support options. + # + # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. + encryption: + # Allow encryption, work in group chat rooms with e2ee enabled + allow: false + # Default to encryption, force-enable encryption in all portals the bridge creates + # This will cause the bridge bot to be in private chats for the encryption to work properly. + default: false + # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data. + # Changing this option requires updating the appservice registration file. + appservice: false + # Whether to use MSC4190 instead of appservice login to create the bridge bot device. + # Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202. + # Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861). + # Changing this option requires updating the appservice registration file. + msc4190: false + # Require encryption, drop any unencrypted messages. + require: false + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow_key_sharing: false + # Should users mentions be in the event wire content to enable the server to send push notifications? + plaintext_mentions: false + # Options for deleting megolm sessions from the bridge. + delete_keys: + # Beeper-specific: delete outbound sessions when hungryserv confirms + # that the user has uploaded the key to key backup. + delete_outbound_on_ack: false + # Don't store outbound sessions in the inbound table. + dont_store_outbound: false + # Ratchet megolm sessions forward after decrypting messages. + ratchet_on_decrypt: false + # Delete fully used keys (index >= max_messages) after decrypting messages. + delete_fully_used_on_decrypt: false + # Delete previous megolm sessions from same device when receiving a new one. + delete_prev_on_new_session: false + # Delete megolm sessions received from a device when the device is deleted. + delete_on_device_delete: false + # Periodically delete megolm sessions when 2x max_age has passed since receiving the session. + periodically_delete_expired: false + # Delete inbound megolm sessions that don't have the received_at field used for + # automatic ratcheting and expired session deletion. This is meant as a migration + # to delete old keys prior to the bridge update. + delete_outdated_inbound: false + # What level of device verification should be required from users? + # + # Valid levels: + # unverified - Send keys to all device in the room. + # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys. + # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes). + # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot. + # Note that creating user signatures from the bridge bot is not currently possible. + # verified - Require manual per-device verification + # (currently only possible by modifying the `trust` column in the `crypto_device` database table). + verification_levels: + # Minimum level for which the bridge should send keys to when bridging messages from WhatsApp to Matrix. + receive: unverified + # Minimum level that the bridge should accept for incoming Matrix messages. + send: unverified + # Minimum level that the bridge should require for accepting key requests. + share: cross-signed-tofu + # Options for Megolm room key rotation. These options allow you to + # configure the m.room.encryption event content. See: + # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for + # more information about that event. + rotation: + # Enable custom Megolm room key rotation settings. Note that these + # settings will only apply to rooms created after this option is + # set. + enable_custom: false + # The maximum number of milliseconds a session should be used + # before changing it. The Matrix spec recommends 604800000 (a week) + # as the default. + milliseconds: 604800000 + # The maximum number of messages that should be sent with a given a + # session before changing it. The Matrix spec recommends 100 as the + # default. + messages: 100 + + # Disable rotating keys when a user's devices change? + # You should not enable this option unless you understand all the implications. + disable_device_change_key_rotation: false + + # Settings for provisioning API + provisioning: + # Prefix for the provisioning API paths. + prefix: /_matrix/provision + # Shared secret for authentication. If set to "generate", a random secret will be generated, + # or if set to "disable", the provisioning API will be disabled. + shared_secret: generate + # Enable debug API at /debug with provisioning authentication. + debug_endpoints: false + + # Permissions for using the bridge. + # Permitted values: + # relay - Talk through the relaybot (if enabled), no access otherwise + # user - Access to use the bridge to chat with a Discord account. + # admin - User level and some additional administration tools + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": relay + "example.com": user + "@admin:example.com": admin + +# Logging config. See https://github.com/tulir/zeroconfig for details. +logging: + min_level: debug + writers: + - type: stdout + format: pretty-colored + - type: file + format: json + filename: ./logs/mautrix-discord.log + max_size: 100 + max_backups: 10 + compress: true diff --git a/bridges/templates/gmessages.yaml b/bridges/templates/gmessages.yaml new file mode 100644 index 0000000..a519af3 --- /dev/null +++ b/bridges/templates/gmessages.yaml @@ -0,0 +1,534 @@ +# Network-specific config options +network: + # Displayname template for SMS users. + # {{.FullName}} - Full name provided by the phone + # {{.FirstName}} - First name provided by the phone + # {{.PhoneNumber}} - Formatted phone number provided by the phone + displayname_template: "{{or .FullName .PhoneNumber}}" + # Settings for how the bridge appears to the phone. + device_meta: + # OS name to tell the phone. This is the name that shows up in the paired devices list. + os: mautrix-gmessages + # Browser type to tell the phone. This decides which icon is shown. + # Valid types: OTHER, CHROME, FIREFOX, SAFARI, OPERA, IE, EDGE + browser: OTHER + # Device type to tell the phone. This also affects the icon, as well as how many sessions are allowed simultaneously. + # One web, two tablets and one PWA should be able to connect at the same time. + # Valid types: WEB, TABLET, PWA + type: TABLET + # Should the bridge aggressively set itself as the active device if the user opens Google Messages in a browser? + # If this is disabled, the user must manually use the `set-active` command to reactivate the bridge. + aggressive_reconnect: false + # Number of chats to sync when connecting to Google Messages. + initial_chat_sync_count: 25 + # Interval at which to ping the phone to check if it's still connected. + ping_interval: 20m + # How many ping timeouts should happen before a bridge state update is sent? + # This only starts counting after a normal ping doesn't respond within a minute. + # After that, new pings are sent with doubled timeouts (up to an hour). + # The default value of 4 means 1 + 2 + 4 + 8 = 15 minutes of no response before a notice. + alert_timeout_count: 4 + + +# Config options that affect the central bridge module. +bridge: + # The prefix for commands. Only required in non-management rooms. + command_prefix: '!gm' + # Should the bridge create a space for each login containing the rooms that account is in? + personal_filtering_spaces: true + # Whether the bridge should set names and avatars explicitly for DM portals. + # This is only necessary when using clients that don't support MSC4171. + private_chat_portal_meta: true + # Should events be handled asynchronously within portal rooms? + # If true, events may end up being out of order, but slow events won't block other ones. + # This is not yet safe to use. + async_events: false + # Should every user have their own portals rather than sharing them? + # By default, users who are in the same group on the remote network will be + # in the same Matrix room bridged to that group. If this is set to true, + # every user will get their own Matrix room instead. + # SETTING THIS IS IRREVERSIBLE AND POTENTIALLY DESTRUCTIVE IF PORTALS ALREADY EXIST. + split_portals: false + # Should the bridge resend `m.bridge` events to all portals on startup? + resend_bridge_info: false + # Should `m.bridge` events be sent without a state key? + # By default, the bridge uses a unique key that won't conflict with other bridges. + no_bridge_info_state_key: false + # Should bridge connection status be sent to the management room as `m.notice` events? + # These contain the same data that can be posted to an external HTTP server using homeserver -> status_endpoint. + # Allowed values: none, errors, all + bridge_status_notices: errors + # How long after an unknown error should the bridge attempt a full reconnect? + # Must be at least 1 minute. The bridge will add an extra ±20% jitter to this value. + unknown_error_auto_reconnect: null + # Maximum number of times to do the auto-reconnect above. + # The counter is per login, but is never reset except on logout and restart. + unknown_error_max_auto_reconnects: 10 + + # Should leaving Matrix rooms be bridged as leaving groups on the remote network? + bridge_matrix_leave: false + # Should `m.notice` messages be bridged? + bridge_notices: false + # Should room tags only be synced when creating the portal? Tags mean things like favorite/pin and archive/low priority. + # Tags currently can't be synced back to the remote network, so a continuous sync means tagging from Matrix will be undone. + tag_only_on_create: true + # List of tags to allow bridging. If empty, no tags will be bridged. + only_bridge_tags: [m.favourite, m.lowpriority] + # Should room mute status only be synced when creating the portal? + # Like tags, mutes can't currently be synced back to the remote network. + mute_only_on_create: true + # Should the bridge check the db to ensure that incoming events haven't been handled before + deduplicate_matrix_messages: false + # Should cross-room reply metadata be bridged? + # Most Matrix clients don't support this and servers may reject such messages too. + cross_room_replies: false + # If a state event fails to bridge, should the bridge revert any state changes made by that event? + revert_failed_state_changes: false + # In portals with no relay set, should Matrix users be kicked if they're + # not logged into an account that's in the remote chat? + kick_matrix_users: true + # Should the bridge listen to com.beeper.state_request events? + # This is not necessary for anything outside of Beeper. + enable_send_state_requests: false + # Should the com.beeper.bridge.identifiers list in global ghost profiles include phone numbers? + phone_numbers_in_profile: false + + # What should be done to portal rooms when a user logs out or is logged out? + # Permitted values: + # nothing - Do nothing, let the user stay in the portals + # kick - Remove the user from the portal rooms, but don't delete them + # unbridge - Remove all ghosts in the room and disassociate it from the remote chat + # delete - Remove all ghosts and users from the room (i.e. delete it) + cleanup_on_logout: + # Should cleanup on logout be enabled at all? + enabled: false + # Settings for manual logouts (explicitly initiated by the Matrix user) + manual: + # Action for private portals which will never be shared with other Matrix users. + private: nothing + # Action for portals with a relay user configured. + relayed: nothing + # Action for portals which may be shared, but don't currently have any other Matrix users. + shared_no_users: nothing + # Action for portals which have other logged-in Matrix users. + shared_has_users: nothing + # Settings for credentials being invalidated (initiated by the remote network, possibly through user action). + # Keys have the same meanings as in the manual section. + bad_credentials: + private: nothing + relayed: nothing + shared_no_users: nothing + shared_has_users: nothing + + # Settings for relay mode + relay: + # Whether relay mode should be allowed. If allowed, the set-relay command can be used to turn any + # authenticated user into a relaybot for that chat. + enabled: false + # Should only admins be allowed to set themselves as relay users? + # If true, non-admins can only set users listed in default_relays as relays in a room. + admin_only: true + # Should default relays be preferred when an explicit login ID isn't specified even if the user is logged in? + # This applies to the set-relay and bridge commands sent by any user, including admins. + prefer_default: true + # Should non-admins be allowed to use the bridge and sync-chat commands via default relays specified below? + allow_bridge: true + # List of user login IDs which anyone can set as a relay, as long as the relay user is in the room. + default_relays: [] + # The formats to use when sending messages via the relaybot. + # Available variables: + # .Sender.UserID - The Matrix user ID of the sender. + # .Sender.Displayname - The display name of the sender (if set). + # .Sender.RequiresDisambiguation - Whether the sender's name may be confused with the name of another user in the room. + # .Sender.DisambiguatedName - The disambiguated name of the sender. This will be the displayname if set, + # plus the user ID in parentheses if the displayname is not unique. + # If the displayname is not set, this is just the user ID. + # .Message - The `formatted_body` field of the message. + # .Caption - The `formatted_body` field of the message, if it's a caption. Otherwise an empty string. + # .FileName - The name of the file being sent. + message_formats: + m.text: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.notice: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.emote: "* {{ .Sender.DisambiguatedName }} {{ .Message }}" + m.file: "{{ .Sender.DisambiguatedName }} sent a file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.image: "{{ .Sender.DisambiguatedName }} sent an image{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.audio: "{{ .Sender.DisambiguatedName }} sent an audio file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.video: "{{ .Sender.DisambiguatedName }} sent a video{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.location: "{{ .Sender.DisambiguatedName }} sent a location{{ if .Caption }}: {{ .Caption }}{{ end }}" + # For networks that support per-message displaynames (i.e. Slack and Discord), the template for those names. + # This has all the Sender variables available under message_formats (but without the .Sender prefix). + # Note that you need to manually remove the displayname from message_formats above. + displayname_format: "{{ .DisambiguatedName }}" + + # Filter for automatically creating portals. + portal_create_filter: + # The mode for filtering, either `deny` or `allow` + mode: deny + # The list of portal IDs to deny or allow depending on the mode config. + # Items here can either be the plain portal ID as a string, or an object with `id` and `receiver` fields. + # The receiver field is necessary if you want to target a specific DM portal for example. + list: [] + # A list of user login IDs from which to always deny creating portals. + # This is meant to be used with default relays, such that the relay bot + # being added to a group wouldn't automatically trigger portal creation. + always_deny_from_login: [] + + # Permissions for using the bridge. + # Permitted values: + # relay - Talk through the relaybot (if enabled), no access otherwise + # commands - Access to use commands in the bridge, but not login. + # user - Access to use the bridge with puppeting. + # admin - Full access, user level with some additional administration tools. + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": relay + "example.com": user + "@admin:example.com": admin + +# Config for the bridge's database. +database: + # The database type. "sqlite3-fk-wal" and "postgres" are supported. + type: postgres + # The database URI. + # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended. + # https://github.com/mattn/go-sqlite3#connection-string + # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable + # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql + uri: postgres://user:password@host/database?sslmode=disable + # Maximum number of connections. + max_open_conns: 5 + max_idle_conns: 1 + # Maximum connection idle time and lifetime before they're closed. Disabled if null. + # Parsed with https://pkg.go.dev/time#ParseDuration + max_conn_idle_time: null + max_conn_lifetime: null + +# Homeserver details. +homeserver: + # The address that this appservice can use to connect to the homeserver. + # Local addresses without HTTPS are generally recommended when the bridge is running on the same machine, + # but https also works if they run on different machines. + address: http://example.localhost:8008 + # The domain of the homeserver (also known as server_name, used for MXIDs, etc). + domain: example.com + + # What software is the homeserver running? + # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. + software: standard + # The URL to push real-time bridge status to. + # If set, the bridge will make POST requests to this URL whenever a user's remote network connection state changes. + # The bridge will use the appservice as_token to authorize requests. + status_endpoint: + # Endpoint for reporting per-message status. + # If set, the bridge will make POST requests to this URL when processing a message from Matrix. + # It will make one request when receiving the message (step BRIDGE), one after decrypting if applicable + # (step DECRYPTED) and one after sending to the remote network (step REMOTE). Errors will also be reported. + # The bridge will use the appservice as_token to authorize requests. + message_send_checkpoint_endpoint: + # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? + async_media: false + + # Should the bridge use a websocket for connecting to the homeserver? + # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy, + # mautrix-asmux (deprecated), and hungryserv (proprietary). + websocket: false + # How often should the websocket be pinged? Pinging will be disabled if this is zero. + ping_interval_seconds: 0 + +# Application service host/registration related details. +# Changing these values requires regeneration of the registration (except when noted otherwise) +appservice: + # The address that the homeserver can use to connect to this appservice. + # Like the homeserver address, a local non-https address is recommended when the bridge is on the same machine. + # If the bridge is elsewhere, you must secure the connection yourself (e.g. with https or wireguard) + # If you want to use https, you need to use a reverse proxy. The bridge does not have TLS support built in. + address: http://localhost:29336 + # A public address that external services can use to reach this appservice. + # This is only needed for things like public media. A reverse proxy is generally necessary when using this field. + # This value doesn't affect the registration file. + public_address: https://bridge.example.com + + # The hostname and port where this appservice should listen. + # For Docker, you generally have to change the hostname to 0.0.0.0. + hostname: 127.0.0.1 + port: 29336 + + # The unique ID of this appservice. + id: gmessages + # Appservice bot details. + bot: + # Username of the appservice bot. + username: gmessagesbot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + displayname: Google Messages bridge bot + avatar: mxc://maunium.net/yGOdcrJcwqARZqdzbfuxfhzb + + # Whether to receive ephemeral events via appservice transactions. + ephemeral_events: true + # Should incoming events be handled asynchronously? + # This may be necessary for large public instances with lots of messages going through. + # However, messages will not be guaranteed to be bridged in the same order they were sent in. + # This value doesn't affect the registration file. + async_transactions: false + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + + # Localpart template of MXIDs for remote users. + # {{.}} is replaced with the internal ID of the user. + username_template: gmessages_{{.}} + +# Config options that affect the Matrix connector of the bridge. +matrix: + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Whether the bridge should send a read receipt after successfully bridging a message. + delivery_receipts: false + # Whether the bridge should send error notices via m.notice events when a message fails to bridge. + message_error_notices: true + # Whether the bridge should update the m.direct account data event when double puppeting is enabled. + sync_direct_chat_list: true + # Whether created rooms should have federation enabled. If false, created portal rooms + # will never be federated. Changing this option requires recreating rooms. + federate_rooms: true + # The threshold as bytes after which the bridge should roundtrip uploads via the disk + # rather than keeping the whole file in memory. + upload_file_threshold: 5242880 + # Should the bridge set additional custom profile info for ghosts? + # This can make a lot of requests, as there's no batch profile update endpoint. + ghost_extra_profile_info: false + +# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors. +analytics: + # API key to send with tracking requests. Tracking is disabled if this is null. + token: null + # Address to send tracking requests to. + url: https://api.segment.io/v1/track + # Optional user ID for tracking events. If null, defaults to using Matrix user ID. + user_id: null + +# Settings for provisioning API +provisioning: + # Shared secret for authentication. If set to "generate" or null, a random secret will be generated, + # or if set to "disable", the provisioning API will be disabled. Must be at least 16 characters. + shared_secret: generate + # Whether to allow provisioning API requests to be authed using Matrix access tokens. + # This follows the same rules as double puppeting to determine which server to contact to check the token, + # which means that by default, it only works for users on the same server as the bridge. + allow_matrix_auth: true + # Enable debug API at /debug with provisioning authentication. + debug_endpoints: false + # Enable session transfers between bridges. Note that this only validates Matrix or shared secret + # auth before passing live network client credentials down in the response. + enable_session_transfers: false + +# Some networks require publicly accessible media download links (e.g. for user avatars when using Discord webhooks). +# These settings control whether the bridge will provide such public media access. +public_media: + # Should public media be enabled at all? + # The public_address field under the appservice section MUST be set when enabling public media. + enabled: false + # A key for signing public media URLs. + # If set to "generate", a random key will be generated. + signing_key: generate + # Number of seconds that public media URLs are valid for. + # If set to 0, URLs will never expire. + expiry: 0 + # Length of hash to use for public media URLs. Must be between 0 and 32. + hash_length: 32 + # The path prefix for generated URLs. Note that this will NOT change the path where media is actually served. + # If you change this, you must configure your reverse proxy to rewrite the path accordingly. + path_prefix: /_mautrix/publicmedia + # Should the bridge store media metadata in the database in order to support encrypted media and generate shorter URLs? + # If false, the generated URLs will just have the MXC URI and a HMAC signature. + # The hash_length field will be used to decide the length of the generated URL. + # This also allows invalidating URLs by deleting the database entry. + use_database: false + +# Settings for converting remote media to custom mxc:// URIs instead of reuploading. +# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html +direct_media: + # Should custom mxc:// URIs be used instead of reuploading media? + enabled: false + # The server name to use for the custom mxc:// URIs. + # This server name will effectively be a real Matrix server, it just won't implement anything other than media. + # You must either set up .well-known delegation from this domain to the bridge, or proxy the domain directly to the bridge. + server_name: discord-media.example.com + # Optionally a custom .well-known response. This defaults to `server_name:443` + well_known_response: + # Optionally specify a custom prefix for the media ID part of the MXC URI. + media_id_prefix: + # If the remote network supports media downloads over HTTP, then the bridge will use MSC3860/MSC3916 + # media download redirects if the requester supports it. Optionally, you can force redirects + # and not allow proxying at all by setting this to false. + # This option does nothing if the remote network does not support media downloads over HTTP. + allow_proxy: true + # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file. + # This key is also used to sign the mxc:// URIs to ensure only the bridge can generate them. + server_key: generate + +# Settings for backfilling messages. +# Note that the exact way settings are applied depends on the network connector. +# See https://docs.mau.fi/bridges/general/backfill.html for more details. +backfill: + # Whether to do backfilling at all. + enabled: false + # Maximum number of messages to backfill in empty rooms. + # If this is zero or negative, backfill will be disabled in new rooms. + max_initial_messages: 50 + # Maximum number of missed messages to backfill after bridge restarts. + max_catchup_messages: 500 + # If a backfilled chat is older than this number of hours, + # mark it as read even if it's unread on the remote network. + unread_hours_threshold: 720 + # Settings for backfilling threads within other backfills. + threads: + # Maximum number of messages to backfill in a new thread. + max_initial_messages: 50 + # Settings for the backwards backfill queue. This only applies when connecting to + # Beeper as standard Matrix servers don't support inserting messages into history. + queue: + # Should the backfill queue be enabled? + enabled: false + # Should manual calls to backfill queue tasks be allowed? + manual: false + # Number of messages to backfill in one batch. + batch_size: 100 + # Delay between batches in seconds. + batch_delay: 20 + # Maximum number of batches to backfill per portal. + # If set to -1, all available messages will be backfilled. + max_batches: -1 + # Optional network-specific overrides for max batches. + # Interpretation of this field depends on the network connector. + max_batches_override: {} + +# Settings for enabling double puppeting +double_puppet: + # Servers to always allow double puppeting from. + # This is only for other servers and should NOT contain the server the bridge is on. + servers: + anotherserver.example.org: https://matrix.anotherserver.example.org + # Whether to allow client API URL discovery for other servers. When using this option, + # users on other servers can use double puppeting even if their server URLs aren't + # explicitly added to the servers map above. + allow_discovery: false + # Shared secrets for automatic double puppeting. + # See https://docs.mau.fi/bridges/general/double-puppeting.html for instructions. + secrets: + example.com: as_token:foobar + +# End-to-bridge encryption support options. +# +# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. +encryption: + # Whether to enable encryption at all. If false, the bridge will not function in encrypted rooms. + allow: false + # Whether to force-enable encryption in all bridged rooms. + default: false + # Whether to require all messages to be encrypted and drop any unencrypted messages. + require: false + # Whether to use MSC3202/MSC4203 instead of /sync long polling for receiving encryption-related data. + # This is an experimental option, see the docs for more info. + # Changing this option requires updating the appservice registration file. + appservice: false + # Whether to use MSC4190 instead of appservice login to create the bridge bot device. + # Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202. + # Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861). + msc4190: false + # Whether to encrypt reactions and reply metadata as per MSC4392. + # This is not supported by most clients. + msc4392: false + # Should the bridge bot generate a recovery key and cross-signing keys and verify itself? + # Note that without the latest version of MSC4190, this will fail if you reset the bridge database. + # The generated recovery key will be saved in the kv_store table under `recovery_key`. + self_sign: false + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow_key_sharing: true + # Pickle key for encrypting encryption keys in the bridge database. + # If set to generate, a random key will be generated. + pickle_key: generate + # Options for deleting megolm sessions from the bridge. + delete_keys: + # Beeper-specific: delete outbound sessions when hungryserv confirms + # that the user has uploaded the key to key backup. + delete_outbound_on_ack: false + # Don't store outbound sessions in the inbound table. + dont_store_outbound: false + # Ratchet megolm sessions forward after decrypting messages. + ratchet_on_decrypt: false + # Delete fully used keys (index >= max_messages) after decrypting messages. + delete_fully_used_on_decrypt: false + # Delete previous megolm sessions from same device when receiving a new one. + delete_prev_on_new_session: false + # Delete megolm sessions received from a device when the device is deleted. + delete_on_device_delete: false + # Periodically delete megolm sessions when 2x max_age has passed since receiving the session. + periodically_delete_expired: false + # Delete inbound megolm sessions that don't have the received_at field used for + # automatic ratcheting and expired session deletion. This is meant as a migration + # to delete old keys prior to the bridge update. + delete_outdated_inbound: false + # What level of device verification should be required from users? + # + # Valid levels: + # unverified - Send keys to all device in the room. + # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys. + # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes). + # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot. + # Note that creating user signatures from the bridge bot is not currently possible. + # verified - Require manual per-device verification + # (currently only possible by modifying the `trust` column in the `crypto_device` database table). + verification_levels: + # Minimum level for which the bridge should send keys to when bridging messages from the remote network to Matrix. + receive: unverified + # Minimum level that the bridge should accept for incoming Matrix messages. + send: unverified + # Minimum level that the bridge should require for accepting key requests. + share: cross-signed-tofu + # Options for Megolm room key rotation. These options allow you to configure the m.room.encryption event content. + # See https://spec.matrix.org/v1.10/client-server-api/#mroomencryption for more information about that event. + rotation: + # Enable custom Megolm room key rotation settings. Note that these + # settings will only apply to rooms created after this option is set. + enable_custom: false + # The maximum number of milliseconds a session should be used + # before changing it. The Matrix spec recommends 604800000 (a week) + # as the default. + milliseconds: 604800000 + # The maximum number of messages that should be sent with a given a + # session before changing it. The Matrix spec recommends 100 as the + # default. + messages: 100 + # Disable rotating keys when a user's devices change? + # You should not enable this option unless you understand all the implications. + disable_device_change_key_rotation: false + +# Prefix for environment variables. All variables with this prefix must map to valid config fields. +# Nesting in variable names is represented with a dot (.). +# If there are no dots in the name, two underscores (__) are replaced with a dot. +# +# e.g. if the prefix is set to `BRIDGE_`, then `BRIDGE_APPSERVICE__AS_TOKEN` will set appservice.as_token. +# `BRIDGE_appservice.as_token` would work as well, but can't be set in a shell as easily. +# +# If this is null, reading config fields from environment will be disabled. +env_config_prefix: null + +# Logging config. See https://github.com/tulir/zeroconfig for details. +logging: + min_level: debug + writers: + - type: stdout + format: pretty-colored + - type: file + format: json + filename: ./logs/bridge.log + max_size: 100 + max_backups: 10 + compress: false diff --git a/bridges/templates/linkedin.yaml b/bridges/templates/linkedin.yaml new file mode 100644 index 0000000..f1831fc --- /dev/null +++ b/bridges/templates/linkedin.yaml @@ -0,0 +1,520 @@ +# Network-specific config options +network: + # Displayname template for LinkedIn users. + # .FirstName is replaced with the first name + # .LastName is replaced with the last name + # .Organization is replaced with the organization name + displayname_template: "{{ with .Organization }}{{ . }}{{ else }}{{ .FirstName }} {{ .LastName }}{{ end }} (LinkedIn)" + + sync: + # Number of most recently active dialogs to check when syncing chats. + # Set to 0 to remove limit. + update_limit: 0 + # Number of most recently active dialogs to create portals for when syncing + # chats. + # Set to 0 to remove limit. + create_limit: 10 + + +# Config options that affect the central bridge module. +bridge: + # The prefix for commands. Only required in non-management rooms. + command_prefix: '!linkedin' + # Should the bridge create a space for each login containing the rooms that account is in? + personal_filtering_spaces: true + # Whether the bridge should set names and avatars explicitly for DM portals. + # This is only necessary when using clients that don't support MSC4171. + private_chat_portal_meta: true + # Should events be handled asynchronously within portal rooms? + # If true, events may end up being out of order, but slow events won't block other ones. + # This is not yet safe to use. + async_events: false + # Should every user have their own portals rather than sharing them? + # By default, users who are in the same group on the remote network will be + # in the same Matrix room bridged to that group. If this is set to true, + # every user will get their own Matrix room instead. + # SETTING THIS IS IRREVERSIBLE AND POTENTIALLY DESTRUCTIVE IF PORTALS ALREADY EXIST. + split_portals: false + # Should the bridge resend `m.bridge` events to all portals on startup? + resend_bridge_info: false + # Should `m.bridge` events be sent without a state key? + # By default, the bridge uses a unique key that won't conflict with other bridges. + no_bridge_info_state_key: false + # Should bridge connection status be sent to the management room as `m.notice` events? + # These contain the same data that can be posted to an external HTTP server using homeserver -> status_endpoint. + # Allowed values: none, errors, all + bridge_status_notices: errors + # How long after an unknown error should the bridge attempt a full reconnect? + # Must be at least 1 minute. The bridge will add an extra ±20% jitter to this value. + unknown_error_auto_reconnect: null + # Maximum number of times to do the auto-reconnect above. + # The counter is per login, but is never reset except on logout and restart. + unknown_error_max_auto_reconnects: 10 + + # Should leaving Matrix rooms be bridged as leaving groups on the remote network? + bridge_matrix_leave: false + # Should `m.notice` messages be bridged? + bridge_notices: false + # Should room tags only be synced when creating the portal? Tags mean things like favorite/pin and archive/low priority. + # Tags currently can't be synced back to the remote network, so a continuous sync means tagging from Matrix will be undone. + tag_only_on_create: true + # List of tags to allow bridging. If empty, no tags will be bridged. + only_bridge_tags: [m.favourite, m.lowpriority] + # Should room mute status only be synced when creating the portal? + # Like tags, mutes can't currently be synced back to the remote network. + mute_only_on_create: true + # Should the bridge check the db to ensure that incoming events haven't been handled before + deduplicate_matrix_messages: false + # Should cross-room reply metadata be bridged? + # Most Matrix clients don't support this and servers may reject such messages too. + cross_room_replies: false + # If a state event fails to bridge, should the bridge revert any state changes made by that event? + revert_failed_state_changes: false + # In portals with no relay set, should Matrix users be kicked if they're + # not logged into an account that's in the remote chat? + kick_matrix_users: true + # Should the bridge listen to com.beeper.state_request events? + # This is not necessary for anything outside of Beeper. + enable_send_state_requests: false + # Should the com.beeper.bridge.identifiers list in global ghost profiles include phone numbers? + phone_numbers_in_profile: false + + # What should be done to portal rooms when a user logs out or is logged out? + # Permitted values: + # nothing - Do nothing, let the user stay in the portals + # kick - Remove the user from the portal rooms, but don't delete them + # unbridge - Remove all ghosts in the room and disassociate it from the remote chat + # delete - Remove all ghosts and users from the room (i.e. delete it) + cleanup_on_logout: + # Should cleanup on logout be enabled at all? + enabled: false + # Settings for manual logouts (explicitly initiated by the Matrix user) + manual: + # Action for private portals which will never be shared with other Matrix users. + private: nothing + # Action for portals with a relay user configured. + relayed: nothing + # Action for portals which may be shared, but don't currently have any other Matrix users. + shared_no_users: nothing + # Action for portals which have other logged-in Matrix users. + shared_has_users: nothing + # Settings for credentials being invalidated (initiated by the remote network, possibly through user action). + # Keys have the same meanings as in the manual section. + bad_credentials: + private: nothing + relayed: nothing + shared_no_users: nothing + shared_has_users: nothing + + # Settings for relay mode + relay: + # Whether relay mode should be allowed. If allowed, the set-relay command can be used to turn any + # authenticated user into a relaybot for that chat. + enabled: false + # Should only admins be allowed to set themselves as relay users? + # If true, non-admins can only set users listed in default_relays as relays in a room. + admin_only: true + # Should default relays be preferred when an explicit login ID isn't specified even if the user is logged in? + # This applies to the set-relay and bridge commands sent by any user, including admins. + prefer_default: true + # Should non-admins be allowed to use the bridge and sync-chat commands via default relays specified below? + allow_bridge: true + # List of user login IDs which anyone can set as a relay, as long as the relay user is in the room. + default_relays: [] + # The formats to use when sending messages via the relaybot. + # Available variables: + # .Sender.UserID - The Matrix user ID of the sender. + # .Sender.Displayname - The display name of the sender (if set). + # .Sender.RequiresDisambiguation - Whether the sender's name may be confused with the name of another user in the room. + # .Sender.DisambiguatedName - The disambiguated name of the sender. This will be the displayname if set, + # plus the user ID in parentheses if the displayname is not unique. + # If the displayname is not set, this is just the user ID. + # .Message - The `formatted_body` field of the message. + # .Caption - The `formatted_body` field of the message, if it's a caption. Otherwise an empty string. + # .FileName - The name of the file being sent. + message_formats: + m.text: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.notice: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.emote: "* {{ .Sender.DisambiguatedName }} {{ .Message }}" + m.file: "{{ .Sender.DisambiguatedName }} sent a file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.image: "{{ .Sender.DisambiguatedName }} sent an image{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.audio: "{{ .Sender.DisambiguatedName }} sent an audio file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.video: "{{ .Sender.DisambiguatedName }} sent a video{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.location: "{{ .Sender.DisambiguatedName }} sent a location{{ if .Caption }}: {{ .Caption }}{{ end }}" + # For networks that support per-message displaynames (i.e. Slack and Discord), the template for those names. + # This has all the Sender variables available under message_formats (but without the .Sender prefix). + # Note that you need to manually remove the displayname from message_formats above. + displayname_format: "{{ .DisambiguatedName }}" + + # Filter for automatically creating portals. + portal_create_filter: + # The mode for filtering, either `deny` or `allow` + mode: deny + # The list of portal IDs to deny or allow depending on the mode config. + # Items here can either be the plain portal ID as a string, or an object with `id` and `receiver` fields. + # The receiver field is necessary if you want to target a specific DM portal for example. + list: [] + # A list of user login IDs from which to always deny creating portals. + # This is meant to be used with default relays, such that the relay bot + # being added to a group wouldn't automatically trigger portal creation. + always_deny_from_login: [] + + # Permissions for using the bridge. + # Permitted values: + # relay - Talk through the relaybot (if enabled), no access otherwise + # commands - Access to use commands in the bridge, but not login. + # user - Access to use the bridge with puppeting. + # admin - Full access, user level with some additional administration tools. + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": relay + "example.com": user + "@admin:example.com": admin + +# Config for the bridge's database. +database: + # The database type. "sqlite3-fk-wal" and "postgres" are supported. + type: postgres + # The database URI. + # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended. + # https://github.com/mattn/go-sqlite3#connection-string + # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable + # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql + uri: postgres://user:password@host/database?sslmode=disable + # Maximum number of connections. + max_open_conns: 5 + max_idle_conns: 1 + # Maximum connection idle time and lifetime before they're closed. Disabled if null. + # Parsed with https://pkg.go.dev/time#ParseDuration + max_conn_idle_time: null + max_conn_lifetime: null + +# Homeserver details. +homeserver: + # The address that this appservice can use to connect to the homeserver. + # Local addresses without HTTPS are generally recommended when the bridge is running on the same machine, + # but https also works if they run on different machines. + address: http://example.localhost:8008 + # The domain of the homeserver (also known as server_name, used for MXIDs, etc). + domain: example.com + + # What software is the homeserver running? + # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. + software: standard + # The URL to push real-time bridge status to. + # If set, the bridge will make POST requests to this URL whenever a user's remote network connection state changes. + # The bridge will use the appservice as_token to authorize requests. + status_endpoint: + # Endpoint for reporting per-message status. + # If set, the bridge will make POST requests to this URL when processing a message from Matrix. + # It will make one request when receiving the message (step BRIDGE), one after decrypting if applicable + # (step DECRYPTED) and one after sending to the remote network (step REMOTE). Errors will also be reported. + # The bridge will use the appservice as_token to authorize requests. + message_send_checkpoint_endpoint: + # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? + async_media: false + + # Should the bridge use a websocket for connecting to the homeserver? + # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy, + # mautrix-asmux (deprecated), and hungryserv (proprietary). + websocket: false + # How often should the websocket be pinged? Pinging will be disabled if this is zero. + ping_interval_seconds: 0 + +# Application service host/registration related details. +# Changing these values requires regeneration of the registration (except when noted otherwise) +appservice: + # The address that the homeserver can use to connect to this appservice. + # Like the homeserver address, a local non-https address is recommended when the bridge is on the same machine. + # If the bridge is elsewhere, you must secure the connection yourself (e.g. with https or wireguard) + # If you want to use https, you need to use a reverse proxy. The bridge does not have TLS support built in. + address: http://localhost:29341 + # A public address that external services can use to reach this appservice. + # This is only needed for things like public media. A reverse proxy is generally necessary when using this field. + # This value doesn't affect the registration file. + public_address: https://bridge.example.com + + # The hostname and port where this appservice should listen. + # For Docker, you generally have to change the hostname to 0.0.0.0. + hostname: 127.0.0.1 + port: 29341 + + # The unique ID of this appservice. + id: linkedin + # Appservice bot details. + bot: + # Username of the appservice bot. + username: linkedinbot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + displayname: LinkedIn bridge bot + avatar: mxc://maunium.net/CqzBEHjrLsfdqixWZgNHMlRT + + # Whether to receive ephemeral events via appservice transactions. + ephemeral_events: true + # Should incoming events be handled asynchronously? + # This may be necessary for large public instances with lots of messages going through. + # However, messages will not be guaranteed to be bridged in the same order they were sent in. + # This value doesn't affect the registration file. + async_transactions: false + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + + # Localpart template of MXIDs for remote users. + # {{.}} is replaced with the internal ID of the user. + username_template: linkedin_{{.}} + +# Config options that affect the Matrix connector of the bridge. +matrix: + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Whether the bridge should send a read receipt after successfully bridging a message. + delivery_receipts: false + # Whether the bridge should send error notices via m.notice events when a message fails to bridge. + message_error_notices: true + # Whether the bridge should update the m.direct account data event when double puppeting is enabled. + sync_direct_chat_list: true + # Whether created rooms should have federation enabled. If false, created portal rooms + # will never be federated. Changing this option requires recreating rooms. + federate_rooms: true + # The threshold as bytes after which the bridge should roundtrip uploads via the disk + # rather than keeping the whole file in memory. + upload_file_threshold: 5242880 + # Should the bridge set additional custom profile info for ghosts? + # This can make a lot of requests, as there's no batch profile update endpoint. + ghost_extra_profile_info: false + +# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors. +analytics: + # API key to send with tracking requests. Tracking is disabled if this is null. + token: null + # Address to send tracking requests to. + url: https://api.segment.io/v1/track + # Optional user ID for tracking events. If null, defaults to using Matrix user ID. + user_id: null + +# Settings for provisioning API +provisioning: + # Shared secret for authentication. If set to "generate" or null, a random secret will be generated, + # or if set to "disable", the provisioning API will be disabled. Must be at least 16 characters. + shared_secret: generate + # Whether to allow provisioning API requests to be authed using Matrix access tokens. + # This follows the same rules as double puppeting to determine which server to contact to check the token, + # which means that by default, it only works for users on the same server as the bridge. + allow_matrix_auth: true + # Enable debug API at /debug with provisioning authentication. + debug_endpoints: false + # Enable session transfers between bridges. Note that this only validates Matrix or shared secret + # auth before passing live network client credentials down in the response. + enable_session_transfers: false + +# Some networks require publicly accessible media download links (e.g. for user avatars when using Discord webhooks). +# These settings control whether the bridge will provide such public media access. +public_media: + # Should public media be enabled at all? + # The public_address field under the appservice section MUST be set when enabling public media. + enabled: false + # A key for signing public media URLs. + # If set to "generate", a random key will be generated. + signing_key: generate + # Number of seconds that public media URLs are valid for. + # If set to 0, URLs will never expire. + expiry: 0 + # Length of hash to use for public media URLs. Must be between 0 and 32. + hash_length: 32 + # The path prefix for generated URLs. Note that this will NOT change the path where media is actually served. + # If you change this, you must configure your reverse proxy to rewrite the path accordingly. + path_prefix: /_mautrix/publicmedia + # Should the bridge store media metadata in the database in order to support encrypted media and generate shorter URLs? + # If false, the generated URLs will just have the MXC URI and a HMAC signature. + # The hash_length field will be used to decide the length of the generated URL. + # This also allows invalidating URLs by deleting the database entry. + use_database: false + +# Settings for converting remote media to custom mxc:// URIs instead of reuploading. +# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html +direct_media: + # Should custom mxc:// URIs be used instead of reuploading media? + enabled: false + # The server name to use for the custom mxc:// URIs. + # This server name will effectively be a real Matrix server, it just won't implement anything other than media. + # You must either set up .well-known delegation from this domain to the bridge, or proxy the domain directly to the bridge. + server_name: discord-media.example.com + # Optionally a custom .well-known response. This defaults to `server_name:443` + well_known_response: + # Optionally specify a custom prefix for the media ID part of the MXC URI. + media_id_prefix: + # If the remote network supports media downloads over HTTP, then the bridge will use MSC3860/MSC3916 + # media download redirects if the requester supports it. Optionally, you can force redirects + # and not allow proxying at all by setting this to false. + # This option does nothing if the remote network does not support media downloads over HTTP. + allow_proxy: true + # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file. + # This key is also used to sign the mxc:// URIs to ensure only the bridge can generate them. + server_key: generate + +# Settings for backfilling messages. +# Note that the exact way settings are applied depends on the network connector. +# See https://docs.mau.fi/bridges/general/backfill.html for more details. +backfill: + # Whether to do backfilling at all. + enabled: false + # Maximum number of messages to backfill in empty rooms. + # If this is zero or negative, backfill will be disabled in new rooms. + max_initial_messages: 50 + # Maximum number of missed messages to backfill after bridge restarts. + max_catchup_messages: 500 + # If a backfilled chat is older than this number of hours, + # mark it as read even if it's unread on the remote network. + unread_hours_threshold: 720 + # Settings for backfilling threads within other backfills. + threads: + # Maximum number of messages to backfill in a new thread. + max_initial_messages: 50 + # Settings for the backwards backfill queue. This only applies when connecting to + # Beeper as standard Matrix servers don't support inserting messages into history. + queue: + # Should the backfill queue be enabled? + enabled: false + # Should manual calls to backfill queue tasks be allowed? + manual: false + # Number of messages to backfill in one batch. + batch_size: 100 + # Delay between batches in seconds. + batch_delay: 20 + # Maximum number of batches to backfill per portal. + # If set to -1, all available messages will be backfilled. + max_batches: -1 + # Optional network-specific overrides for max batches. + # Interpretation of this field depends on the network connector. + max_batches_override: {} + +# Settings for enabling double puppeting +double_puppet: + # Servers to always allow double puppeting from. + # This is only for other servers and should NOT contain the server the bridge is on. + servers: + anotherserver.example.org: https://matrix.anotherserver.example.org + # Whether to allow client API URL discovery for other servers. When using this option, + # users on other servers can use double puppeting even if their server URLs aren't + # explicitly added to the servers map above. + allow_discovery: false + # Shared secrets for automatic double puppeting. + # See https://docs.mau.fi/bridges/general/double-puppeting.html for instructions. + secrets: + example.com: as_token:foobar + +# End-to-bridge encryption support options. +# +# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. +encryption: + # Whether to enable encryption at all. If false, the bridge will not function in encrypted rooms. + allow: false + # Whether to force-enable encryption in all bridged rooms. + default: false + # Whether to require all messages to be encrypted and drop any unencrypted messages. + require: false + # Whether to use MSC3202/MSC4203 instead of /sync long polling for receiving encryption-related data. + # This is an experimental option, see the docs for more info. + # Changing this option requires updating the appservice registration file. + appservice: false + # Whether to use MSC4190 instead of appservice login to create the bridge bot device. + # Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202. + # Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861). + msc4190: false + # Whether to encrypt reactions and reply metadata as per MSC4392. + # This is not supported by most clients. + msc4392: false + # Should the bridge bot generate a recovery key and cross-signing keys and verify itself? + # Note that without the latest version of MSC4190, this will fail if you reset the bridge database. + # The generated recovery key will be saved in the kv_store table under `recovery_key`. + self_sign: false + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow_key_sharing: true + # Pickle key for encrypting encryption keys in the bridge database. + # If set to generate, a random key will be generated. + pickle_key: generate + # Options for deleting megolm sessions from the bridge. + delete_keys: + # Beeper-specific: delete outbound sessions when hungryserv confirms + # that the user has uploaded the key to key backup. + delete_outbound_on_ack: false + # Don't store outbound sessions in the inbound table. + dont_store_outbound: false + # Ratchet megolm sessions forward after decrypting messages. + ratchet_on_decrypt: false + # Delete fully used keys (index >= max_messages) after decrypting messages. + delete_fully_used_on_decrypt: false + # Delete previous megolm sessions from same device when receiving a new one. + delete_prev_on_new_session: false + # Delete megolm sessions received from a device when the device is deleted. + delete_on_device_delete: false + # Periodically delete megolm sessions when 2x max_age has passed since receiving the session. + periodically_delete_expired: false + # Delete inbound megolm sessions that don't have the received_at field used for + # automatic ratcheting and expired session deletion. This is meant as a migration + # to delete old keys prior to the bridge update. + delete_outdated_inbound: false + # What level of device verification should be required from users? + # + # Valid levels: + # unverified - Send keys to all device in the room. + # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys. + # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes). + # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot. + # Note that creating user signatures from the bridge bot is not currently possible. + # verified - Require manual per-device verification + # (currently only possible by modifying the `trust` column in the `crypto_device` database table). + verification_levels: + # Minimum level for which the bridge should send keys to when bridging messages from the remote network to Matrix. + receive: unverified + # Minimum level that the bridge should accept for incoming Matrix messages. + send: unverified + # Minimum level that the bridge should require for accepting key requests. + share: cross-signed-tofu + # Options for Megolm room key rotation. These options allow you to configure the m.room.encryption event content. + # See https://spec.matrix.org/v1.10/client-server-api/#mroomencryption for more information about that event. + rotation: + # Enable custom Megolm room key rotation settings. Note that these + # settings will only apply to rooms created after this option is set. + enable_custom: false + # The maximum number of milliseconds a session should be used + # before changing it. The Matrix spec recommends 604800000 (a week) + # as the default. + milliseconds: 604800000 + # The maximum number of messages that should be sent with a given a + # session before changing it. The Matrix spec recommends 100 as the + # default. + messages: 100 + # Disable rotating keys when a user's devices change? + # You should not enable this option unless you understand all the implications. + disable_device_change_key_rotation: false + +# Prefix for environment variables. All variables with this prefix must map to valid config fields. +# Nesting in variable names is represented with a dot (.). +# If there are no dots in the name, two underscores (__) are replaced with a dot. +# +# e.g. if the prefix is set to `BRIDGE_`, then `BRIDGE_APPSERVICE__AS_TOKEN` will set appservice.as_token. +# `BRIDGE_appservice.as_token` would work as well, but can't be set in a shell as easily. +# +# If this is null, reading config fields from environment will be disabled. +env_config_prefix: null + +# Logging config. See https://github.com/tulir/zeroconfig for details. +logging: + min_level: debug + writers: + - type: stdout + format: pretty-colored + - type: file + format: json + filename: ./logs/bridge.log + max_size: 100 + max_backups: 10 + compress: false diff --git a/bridges/templates/meta.yaml b/bridges/templates/meta.yaml new file mode 100644 index 0000000..4822884 --- /dev/null +++ b/bridges/templates/meta.yaml @@ -0,0 +1,581 @@ +# Network-specific config options +network: + # Which service is this bridge for? Available options: + # * unset - allow users to pick any service when logging in (except facebook-tor) + # * facebook - connect to FB Messenger via facebook.com + # * facebook-tor - connect to FB Messenger via facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion + # * messenger - connect to FB Messenger via messenger.com (can be used with the facebook side deactivated) + # * messenger-lite - connect to FB Messenger via Messenger iOS API + # * instagram - connect to Instagram DMs via instagram.com + # + # Remember to change the appservice id, bot profile info, bridge username_template and management_room_text too. + mode: + # Should users be allowed to pick messenger.com login when mode is set to `facebook`? + allow_messenger_com_on_fb: false + # Explicit list of allowed login methods. Overrides other login method options if non-empty. + allowed_modes: [] + + # Displayname template for FB/IG users. Available variables: + # .DisplayName - The display name set by the user. + # .Username - The username set by the user. + # .ID - The internal user ID of the user. + displayname_template: '{{or .DisplayName .Username "Unknown user"}}' + + # Static proxy address (HTTP or SOCKS5) for connecting to Meta. + proxy: + # HTTP endpoint to request new proxy address from, for dynamically assigned proxies. + # The endpoint must return a JSON body with a string field called proxy_url. + get_proxy_from: + # Should media be proxied too? + proxy_media: false + # Should E2EE messages be proxied too? + proxy_e2ee: false + # Should Messenger Lite login traffic be proxied? + proxy_messenger_lite: true + # Should other traffic, not configured here, be proxied? + proxy_other: true + # Minimum interval between full reconnects in seconds, default is 1 hour + min_full_reconnect_interval_seconds: 3600 + # Interval to force refresh the connection (full reconnect), default is 20 hours. Set 0 to disable force refreshes. + force_refresh_interval_seconds: 72000 + # Should connection state be cached to allow quicker restarts? + cache_connection_state: false + # Disable fetching XMA media (reels, stories, etc) when backfilling. + disable_xma_backfill: true + # Disable fetching XMA media entirely. + disable_xma_always: false + # Should the bridge mark you as online when you send typing + # notifications? Currently, this only has an effect for E2EE chats on + # Facebook/Messenger. Full presence bridging is not supported. + send_presence_on_typing: false + # Should the bridge, when operating in Instagram mode, subscribe to an + # additional websocket to receive typing indicators from other users? + # An additional connection is only needed for receiving Instagram + # typing indicators; the existing connection(s) are sufficient to send + # and receive typing indicators in all other cases. + receive_instagram_typing_indicators: true + # Should view-once messages be disabled entirely? + disable_view_once: false + # Should FB marketplace chats have a separate space inside the main Facebook space? + marketplace_space: true + # Log raw Bloks payloads even at debug level, with any user identifiers or credentials redacted. + log_redacted_bloks_payloads: false + + # Thread backfill settings for syncing older conversations + thread_backfill: + # Number of batches (pages) to backfill (-1 for unlimited, 0 to disable) + batch_count: -1 + # Delay between fetching each batch of threads (to avoid rate limiting) + batch_delay: 2s + + +# Config options that affect the central bridge module. +bridge: + # The prefix for commands. Only required in non-management rooms. + command_prefix: '!meta' + # Should the bridge create a space for each login containing the rooms that account is in? + personal_filtering_spaces: true + # Whether the bridge should set names and avatars explicitly for DM portals. + # This is only necessary when using clients that don't support MSC4171. + private_chat_portal_meta: true + # Should events be handled asynchronously within portal rooms? + # If true, events may end up being out of order, but slow events won't block other ones. + # This is not yet safe to use. + async_events: false + # Should every user have their own portals rather than sharing them? + # By default, users who are in the same group on the remote network will be + # in the same Matrix room bridged to that group. If this is set to true, + # every user will get their own Matrix room instead. + # SETTING THIS IS IRREVERSIBLE AND POTENTIALLY DESTRUCTIVE IF PORTALS ALREADY EXIST. + split_portals: false + # Should the bridge resend `m.bridge` events to all portals on startup? + resend_bridge_info: false + # Should `m.bridge` events be sent without a state key? + # By default, the bridge uses a unique key that won't conflict with other bridges. + no_bridge_info_state_key: false + # Should bridge connection status be sent to the management room as `m.notice` events? + # These contain the same data that can be posted to an external HTTP server using homeserver -> status_endpoint. + # Allowed values: none, errors, all + bridge_status_notices: errors + # How long after an unknown error should the bridge attempt a full reconnect? + # Must be at least 1 minute. The bridge will add an extra ±20% jitter to this value. + unknown_error_auto_reconnect: null + # Maximum number of times to do the auto-reconnect above. + # The counter is per login, but is never reset except on logout and restart. + unknown_error_max_auto_reconnects: 10 + + # Should leaving Matrix rooms be bridged as leaving groups on the remote network? + bridge_matrix_leave: false + # Should `m.notice` messages be bridged? + bridge_notices: false + # Should room tags only be synced when creating the portal? Tags mean things like favorite/pin and archive/low priority. + # Tags currently can't be synced back to the remote network, so a continuous sync means tagging from Matrix will be undone. + tag_only_on_create: true + # List of tags to allow bridging. If empty, no tags will be bridged. + only_bridge_tags: [m.favourite, m.lowpriority] + # Should room mute status only be synced when creating the portal? + # Like tags, mutes can't currently be synced back to the remote network. + mute_only_on_create: true + # Should the bridge check the db to ensure that incoming events haven't been handled before + deduplicate_matrix_messages: false + # Should cross-room reply metadata be bridged? + # Most Matrix clients don't support this and servers may reject such messages too. + cross_room_replies: false + # If a state event fails to bridge, should the bridge revert any state changes made by that event? + revert_failed_state_changes: false + # In portals with no relay set, should Matrix users be kicked if they're + # not logged into an account that's in the remote chat? + kick_matrix_users: true + # Should the bridge listen to com.beeper.state_request events? + # This is not necessary for anything outside of Beeper. + enable_send_state_requests: false + # Should the com.beeper.bridge.identifiers list in global ghost profiles include phone numbers? + phone_numbers_in_profile: false + + # What should be done to portal rooms when a user logs out or is logged out? + # Permitted values: + # nothing - Do nothing, let the user stay in the portals + # kick - Remove the user from the portal rooms, but don't delete them + # unbridge - Remove all ghosts in the room and disassociate it from the remote chat + # delete - Remove all ghosts and users from the room (i.e. delete it) + cleanup_on_logout: + # Should cleanup on logout be enabled at all? + enabled: false + # Settings for manual logouts (explicitly initiated by the Matrix user) + manual: + # Action for private portals which will never be shared with other Matrix users. + private: nothing + # Action for portals with a relay user configured. + relayed: nothing + # Action for portals which may be shared, but don't currently have any other Matrix users. + shared_no_users: nothing + # Action for portals which have other logged-in Matrix users. + shared_has_users: nothing + # Settings for credentials being invalidated (initiated by the remote network, possibly through user action). + # Keys have the same meanings as in the manual section. + bad_credentials: + private: nothing + relayed: nothing + shared_no_users: nothing + shared_has_users: nothing + + # Settings for relay mode + relay: + # Whether relay mode should be allowed. If allowed, the set-relay command can be used to turn any + # authenticated user into a relaybot for that chat. + enabled: false + # Should only admins be allowed to set themselves as relay users? + # If true, non-admins can only set users listed in default_relays as relays in a room. + admin_only: true + # Should default relays be preferred when an explicit login ID isn't specified even if the user is logged in? + # This applies to the set-relay and bridge commands sent by any user, including admins. + prefer_default: true + # Should non-admins be allowed to use the bridge and sync-chat commands via default relays specified below? + allow_bridge: true + # List of user login IDs which anyone can set as a relay, as long as the relay user is in the room. + default_relays: [] + # The formats to use when sending messages via the relaybot. + # Available variables: + # .Sender.UserID - The Matrix user ID of the sender. + # .Sender.Displayname - The display name of the sender (if set). + # .Sender.RequiresDisambiguation - Whether the sender's name may be confused with the name of another user in the room. + # .Sender.DisambiguatedName - The disambiguated name of the sender. This will be the displayname if set, + # plus the user ID in parentheses if the displayname is not unique. + # If the displayname is not set, this is just the user ID. + # .Message - The `formatted_body` field of the message. + # .Caption - The `formatted_body` field of the message, if it's a caption. Otherwise an empty string. + # .FileName - The name of the file being sent. + message_formats: + m.text: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.notice: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.emote: "* {{ .Sender.DisambiguatedName }} {{ .Message }}" + m.file: "{{ .Sender.DisambiguatedName }} sent a file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.image: "{{ .Sender.DisambiguatedName }} sent an image{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.audio: "{{ .Sender.DisambiguatedName }} sent an audio file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.video: "{{ .Sender.DisambiguatedName }} sent a video{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.location: "{{ .Sender.DisambiguatedName }} sent a location{{ if .Caption }}: {{ .Caption }}{{ end }}" + # For networks that support per-message displaynames (i.e. Slack and Discord), the template for those names. + # This has all the Sender variables available under message_formats (but without the .Sender prefix). + # Note that you need to manually remove the displayname from message_formats above. + displayname_format: "{{ .DisambiguatedName }}" + + # Filter for automatically creating portals. + portal_create_filter: + # The mode for filtering, either `deny` or `allow` + mode: deny + # The list of portal IDs to deny or allow depending on the mode config. + # Items here can either be the plain portal ID as a string, or an object with `id` and `receiver` fields. + # The receiver field is necessary if you want to target a specific DM portal for example. + list: [] + # A list of user login IDs from which to always deny creating portals. + # This is meant to be used with default relays, such that the relay bot + # being added to a group wouldn't automatically trigger portal creation. + always_deny_from_login: [] + + # Permissions for using the bridge. + # Permitted values: + # relay - Talk through the relaybot (if enabled), no access otherwise + # commands - Access to use commands in the bridge, but not login. + # user - Access to use the bridge with puppeting. + # admin - Full access, user level with some additional administration tools. + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": relay + "example.com": user + "@admin:example.com": admin + +# Config for the bridge's database. +database: + # The database type. "sqlite3-fk-wal" and "postgres" are supported. + type: postgres + # The database URI. + # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended. + # https://github.com/mattn/go-sqlite3#connection-string + # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable + # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql + uri: postgres://user:password@host/database?sslmode=disable + # Maximum number of connections. + max_open_conns: 5 + max_idle_conns: 1 + # Maximum connection idle time and lifetime before they're closed. Disabled if null. + # Parsed with https://pkg.go.dev/time#ParseDuration + max_conn_idle_time: null + max_conn_lifetime: null + +# Homeserver details. +homeserver: + # The address that this appservice can use to connect to the homeserver. + # Local addresses without HTTPS are generally recommended when the bridge is running on the same machine, + # but https also works if they run on different machines. + address: http://example.localhost:8008 + # The domain of the homeserver (also known as server_name, used for MXIDs, etc). + domain: example.com + + # What software is the homeserver running? + # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. + software: standard + # The URL to push real-time bridge status to. + # If set, the bridge will make POST requests to this URL whenever a user's remote network connection state changes. + # The bridge will use the appservice as_token to authorize requests. + status_endpoint: + # Endpoint for reporting per-message status. + # If set, the bridge will make POST requests to this URL when processing a message from Matrix. + # It will make one request when receiving the message (step BRIDGE), one after decrypting if applicable + # (step DECRYPTED) and one after sending to the remote network (step REMOTE). Errors will also be reported. + # The bridge will use the appservice as_token to authorize requests. + message_send_checkpoint_endpoint: + # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? + async_media: false + + # Should the bridge use a websocket for connecting to the homeserver? + # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy, + # mautrix-asmux (deprecated), and hungryserv (proprietary). + websocket: false + # How often should the websocket be pinged? Pinging will be disabled if this is zero. + ping_interval_seconds: 0 + # When requests to the homeserver fail with a 502/503/504/429 status or a network error, + # how many times should the bridge retry the request before giving up? + retry_limit: 4 + +# Application service host/registration related details. +# Changing these values requires regeneration of the registration (except when noted otherwise) +appservice: + # The address that the homeserver can use to connect to this appservice. + # Like the homeserver address, a local non-https address is recommended when the bridge is on the same machine. + # If the bridge is elsewhere, you must secure the connection yourself (e.g. with https or wireguard) + # If you want to use https, you need to use a reverse proxy. The bridge does not have TLS support built in. + address: http://localhost:29319 + # A public address that external services can use to reach this appservice. + # This is only needed for things like public media. A reverse proxy is generally necessary when using this field. + # This value doesn't affect the registration file. + public_address: https://bridge.example.com + + # The hostname and port where this appservice should listen. + # For Docker, you generally have to change the hostname to 0.0.0.0. + hostname: 127.0.0.1 + port: 29319 + + # The unique ID of this appservice. + id: meta + # Appservice bot details. + bot: + # Username of the appservice bot. + username: metabot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + displayname: Meta bridge bot + avatar: mxc://maunium.net/DxpVrwwzPUwaUSazpsjXgcKB + + # Whether to receive ephemeral events via appservice transactions. + ephemeral_events: true + # Should incoming events be handled asynchronously? + # This may be necessary for large public instances with lots of messages going through. + # However, messages will not be guaranteed to be bridged in the same order they were sent in. + # This value doesn't affect the registration file. + async_transactions: false + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + + # Localpart template of MXIDs for remote users. + # {{.}} is replaced with the internal ID of the user. + username_template: meta_{{.}} + +# Config options that affect the Matrix connector of the bridge. +matrix: + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Whether the bridge should send a read receipt after successfully bridging a message. + delivery_receipts: false + # Whether the bridge should send error notices via m.notice events when a message fails to bridge. + message_error_notices: true + # Whether the bridge should update the m.direct account data event when double puppeting is enabled. + sync_direct_chat_list: true + # Whether created rooms should have federation enabled. If false, created portal rooms + # will never be federated. Changing this option requires recreating rooms. + federate_rooms: true + # The threshold as bytes after which the bridge should roundtrip uploads via the disk + # rather than keeping the whole file in memory. + upload_file_threshold: 5242880 + # Should the bridge set additional custom profile info for ghosts? + # This can make a lot of requests, as there's no batch profile update endpoint. + ghost_extra_profile_info: false + +# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors. +analytics: + # API key to send with tracking requests. Tracking is disabled if this is null. + token: null + # Address to send tracking requests to. + url: https://api.segment.io/v1/track + # Optional user ID for tracking events. If null, defaults to using Matrix user ID. + user_id: null + +# Settings for provisioning API +provisioning: + # Shared secret for authentication. If set to "generate" or null, a random secret will be generated, + # or if set to "disable", the provisioning API will be disabled. Must be at least 16 characters. + shared_secret: generate + # Whether to allow provisioning API requests to be authed using Matrix access tokens. + # This follows the same rules as double puppeting to determine which server to contact to check the token, + # which means that by default, it only works for users on the same server as the bridge. + allow_matrix_auth: true + # Enable debug API at /debug with provisioning authentication. + debug_endpoints: false + # Enable session transfers between bridges. Note that this only validates Matrix or shared secret + # auth before passing live network client credentials down in the response. + enable_session_transfers: false + +# Some networks require publicly accessible media download links (e.g. for user avatars when using Discord webhooks). +# These settings control whether the bridge will provide such public media access. +public_media: + # Should public media be enabled at all? + # The public_address field under the appservice section MUST be set when enabling public media. + enabled: false + # A key for signing public media URLs. + # If set to "generate", a random key will be generated. + signing_key: generate + # Number of seconds that public media URLs are valid for. + # If set to 0, URLs will never expire. + expiry: 0 + # Length of hash to use for public media URLs. Must be between 0 and 32. + hash_length: 32 + # The path prefix for generated URLs. Note that this will NOT change the path where media is actually served. + # If you change this, you must configure your reverse proxy to rewrite the path accordingly. + path_prefix: /_mautrix/publicmedia + # Should the bridge store media metadata in the database in order to support encrypted media and generate shorter URLs? + # If false, the generated URLs will just have the MXC URI and a HMAC signature. + # The hash_length field will be used to decide the length of the generated URL. + # This also allows invalidating URLs by deleting the database entry. + use_database: false + +# Settings for converting remote media to custom mxc:// URIs instead of reuploading. +# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html +direct_media: + # Should custom mxc:// URIs be used instead of reuploading media? + enabled: false + # The server name to use for the custom mxc:// URIs. + # This server name will effectively be a real Matrix server, it just won't implement anything other than media. + # You must either set up .well-known delegation from this domain to the bridge, or proxy the domain directly to the bridge. + server_name: discord-media.example.com + # Optionally a custom .well-known response. This defaults to `server_name:443` + well_known_response: + # Optionally specify a custom prefix for the media ID part of the MXC URI. + media_id_prefix: + # If the remote network supports media downloads over HTTP, then the bridge will use MSC3860/MSC3916 + # media download redirects if the requester supports it. Optionally, you can force redirects + # and not allow proxying at all by setting this to false. + # This option does nothing if the remote network does not support media downloads over HTTP. + allow_proxy: true + # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file. + # This key is also used to sign the mxc:// URIs to ensure only the bridge can generate them. + server_key: generate + +# Settings for backfilling messages. +# Note that the exact way settings are applied depends on the network connector. +# See https://docs.mau.fi/bridges/general/backfill.html for more details. +backfill: + # Whether to do backfilling at all. + enabled: false + # Maximum number of messages to backfill in empty rooms. + # If this is zero or negative, backfill will be disabled in new rooms. + max_initial_messages: 50 + # Maximum number of missed messages to backfill after bridge restarts. + max_catchup_messages: 500 + # If a backfilled chat is older than this number of hours, + # mark it as read even if it's unread on the remote network. + unread_hours_threshold: 720 + # Settings for backfilling threads within other backfills. + threads: + # Maximum number of messages to backfill in a new thread. + max_initial_messages: 50 + # Settings for the backwards backfill queue. This only applies when connecting to + # Beeper as standard Matrix servers don't support inserting messages into history. + queue: + # Should the backfill queue be enabled? + enabled: false + # Should manual calls to backfill queue tasks be allowed? + manual: false + # Number of messages to backfill in one batch. + batch_size: 100 + # Delay between batches in seconds. + batch_delay: 20 + # Maximum number of batches to backfill per portal. + # If set to -1, all available messages will be backfilled. + max_batches: -1 + # Optional network-specific overrides for max batches. + # Interpretation of this field depends on the network connector. + max_batches_override: {} + +# Settings for enabling double puppeting +double_puppet: + # Servers to always allow double puppeting from. + # This is only for other servers and should NOT contain the server the bridge is on. + servers: + anotherserver.example.org: https://matrix.anotherserver.example.org + # Whether to allow client API URL discovery for other servers. When using this option, + # users on other servers can use double puppeting even if their server URLs aren't + # explicitly added to the servers map above. + allow_discovery: false + # Shared secrets for automatic double puppeting. + # See https://docs.mau.fi/bridges/general/double-puppeting.html for instructions. + secrets: + example.com: as_token:foobar + +# End-to-bridge encryption support options. +# +# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. +encryption: + # Whether to enable encryption at all. If false, the bridge will not function in encrypted rooms. + allow: false + # Whether to force-enable encryption in all bridged rooms. + default: false + # Whether to require all messages to be encrypted and drop any unencrypted messages. + require: false + # Whether to use MSC3202/MSC4203 instead of /sync long polling for receiving encryption-related data. + # This is an experimental option, see the docs for more info. + # Changing this option requires updating the appservice registration file. + appservice: false + # Whether to use MSC4190 instead of appservice login to create the bridge bot device. + # Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202. + # Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861). + msc4190: false + # Whether to encrypt reactions and reply metadata as per MSC4392. + # This is not supported by most clients. + msc4392: false + # Should the bridge bot generate a recovery key and cross-signing keys and verify itself? + # Note that without the latest version of MSC4190, this will fail if you reset the bridge database. + # The generated recovery key will be saved in the kv_store table under `recovery_key`. + self_sign: false + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow_key_sharing: true + # Should m.mentions be sent in the unencrypted content? This is non-standard and should not be enabled. + plaintext_mentions: false + # Pickle key for encrypting encryption keys in the bridge database. + # If set to generate, a random key will be generated. + pickle_key: generate + # Options for deleting megolm sessions from the bridge. + delete_keys: + # Beeper-specific: delete outbound sessions when hungryserv confirms + # that the user has uploaded the key to key backup. + delete_outbound_on_ack: false + # Don't store outbound sessions in the inbound table. + dont_store_outbound: false + # Ratchet megolm sessions forward after decrypting messages. + ratchet_on_decrypt: false + # Delete fully used keys (index >= max_messages) after decrypting messages. + delete_fully_used_on_decrypt: false + # Delete previous megolm sessions from same device when receiving a new one. + delete_prev_on_new_session: false + # Delete megolm sessions received from a device when the device is deleted. + delete_on_device_delete: false + # Periodically delete megolm sessions when 2x max_age has passed since receiving the session. + periodically_delete_expired: false + # Delete inbound megolm sessions that don't have the received_at field used for + # automatic ratcheting and expired session deletion. This is meant as a migration + # to delete old keys prior to the bridge update. + delete_outdated_inbound: false + # What level of device verification should be required from users? + # + # Valid levels: + # unverified - Send keys to all device in the room. + # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys. + # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes). + # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot. + # Note that creating user signatures from the bridge bot is not currently possible. + # verified - Require manual per-device verification + # (currently only possible by modifying the `trust` column in the `crypto_device` database table). + verification_levels: + # Minimum level for which the bridge should send keys to when bridging messages from the remote network to Matrix. + receive: unverified + # Minimum level that the bridge should accept for incoming Matrix messages. + send: unverified + # Minimum level that the bridge should require for accepting key requests. + share: cross-signed-tofu + # Options for Megolm room key rotation. These options allow you to configure the m.room.encryption event content. + # See https://spec.matrix.org/v1.10/client-server-api/#mroomencryption for more information about that event. + rotation: + # Enable custom Megolm room key rotation settings. Note that these + # settings will only apply to rooms created after this option is set. + enable_custom: false + # The maximum number of milliseconds a session should be used + # before changing it. The Matrix spec recommends 604800000 (a week) + # as the default. + milliseconds: 604800000 + # The maximum number of messages that should be sent with a given a + # session before changing it. The Matrix spec recommends 100 as the + # default. + messages: 100 + # Disable rotating keys when a user's devices change? + # You should not enable this option unless you understand all the implications. + disable_device_change_key_rotation: false + +# Prefix for environment variables. All variables with this prefix must map to valid config fields. +# Nesting in variable names is represented with a dot (.). +# If there are no dots in the name, two underscores (__) are replaced with a dot. +# +# e.g. if the prefix is set to `BRIDGE_`, then `BRIDGE_APPSERVICE__AS_TOKEN` will set appservice.as_token. +# `BRIDGE_appservice.as_token` would work as well, but can't be set in a shell as easily. +# +# The variable names can also have a `_FILE` suffix to tell the bridge to read the value from the +# path set in the variable rather than using the value directly. +# +# If this is null, reading config fields from environment will be disabled. +env_config_prefix: null + +# Logging config. See https://github.com/tulir/zeroconfig for details. +logging: + min_level: debug + writers: + - type: stdout + format: pretty-colored + - type: file + format: json + filename: ./logs/bridge.log + max_size: 100 + max_backups: 10 + compress: false diff --git a/bridges/templates/telegram.yaml b/bridges/templates/telegram.yaml new file mode 100644 index 0000000..10a1b50 --- /dev/null +++ b/bridges/templates/telegram.yaml @@ -0,0 +1,641 @@ +# Network-specific config options +network: + # Get your own API keys at https://my.telegram.org/apps + api_id: 12345 + api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz + + # Device info shown in the Telegram device list. + device_info: + device_model: mautrix-telegram + system_version: + app_version: auto + lang_code: en + system_lang_code: en + + # Settings for converting animated stickers. + animated_sticker: + # Format to which animated stickers should be converted. + # + # disable - no conversion, send as-is (gzipped lottie) + # png - converts to non-animated png (fastest), + # gif - converts to animated gif + # webm - converts to webm video, requires ffmpeg executable with vp9 codec + # and webm container support + # webp - converts to animated webp, requires ffmpeg executable with webp + # codec/container support + target: gif + # Should video stickers be converted to the specified format as well? + convert_from_webm: false + # Arguments for converter. All converters take width and height. + args: + width: 256 + height: 256 + fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended) + + # Settings for syncing the member list for portals. + member_list: + # Maximum number of members to sync per portal when starting up. Other + # members will be synced when they send messages. The maximum is 10000, + # after which the Telegram server will not send any more members. + # + # -1 means no limit (which means it's limited to 10000 by the server) + max_initial_sync: 100 + # Whether or not to sync the member list in broadcast channels. If + # disabled, members will still be synced when they send messages. + # + # If no channel admins have logged into the bridge, the bridge won't be + # able to sync the member list regardless of this setting. + sync_broadcast_channels: false + # Whether or not to skip deleted members when syncing members. + skip_deleted: true + + # Settings for pings to the Telegram server. + ping: + # The interval (in seconds) between pings. + interval_seconds: 30 + # The timeout (in seconds) for a single ping. + timeout_seconds: 10 + + # Proxy settings + proxy: + # Allowed types: disabled, socks5, mtproxy + type: disabled + # Proxy IP address/domain name and port. + address: "127.0.0.1:1080" + # Proxy authentication (optional). Put MTProxy secret in password field. + username: + password: + + sync: + # Number of most recently active dialogs to check when syncing chats. + # Set to -1 to remove limit. + update_limit: 100 + # Number of most recently active dialogs to create portals for when syncing chats. + # Set to -1 to remove limit. + create_limit: 15 + # Number of chats to sync immediately on login before the data export is accepted. + # The create_limit above still applies. This is ignored if takeout.dialog_sync is false. + login_sync_limit: 15 + # Whether or not to sync and create portals for direct chats at startup. + direct_chats: true + + takeout: + # Should the bridge use the data export mode for syncing the full chat list? + # If true, login_sync_limit of chats is synced immediately on login, + # then the rest are synced after the takeout is accepted. + dialog_sync: false + # Should the bridge use the data export mode for forward backfilling messages? + # This should be set to true if the forward backfill limits are set to high values, + # but is probably not necessary otherwise. + forward_backfill: false + # Should the bridge use the data export mode for backward backfilling messages? + # This only affects the backfill queue, which is only available on Beeper. + backward_backfill: false + + # Maximum number of participants in chats to bridge. Only applies when the + # portal is being created. If there are more members when trying to create a + # room, the room creation will be cancelled. + # + # -1 means no limit (which means all chats can be bridged) + max_member_count: -1 + # Should personal avatars (that are only visible to specific users) be allowed? + contact_avatars: false + # Should contact names be updated from any source even if a name is already set? + # Note that contact names will still be used if there's no other name available. + contact_names: false + # Should the bridge send all unicode reactions as custom emoji reactions to + # Telegram? By default, the bridge only uses custom emojis for unicode emojis + # that aren't allowed in reactions. + always_custom_emoji_reaction: false + # The avatar to use for the Telegram Saved Messages chat + saved_message_avatar: mxc://maunium.net/XhhfHoPejeneOngMyBbtyWDk + # Create a new room and tombstone the old one when upgrading rooms + always_tombstone_on_supergroup_migration: false + # Maximum number of pixels in an image before sending to Telegram as a + # document. Defaults to 4096x4096 = 16777216. + image_as_file_pixels: 16777216 + # Should view-once messages be disabled entirely? + disable_view_once: false + # Should video URL previews be bridged as m.video messages to Matrix? + # By default, video URL previews will not be bridged. + video_url_preview_as_file: false + # Displayname template for Telegram users. + # {{ .FullName }} - the full name of the Telegram user + # {{ .FirstName }} - the first name of the Telegram user + # {{ .LastName }} - the last name of the Telegram user + # {{ .Username }} - the primary username of the Telegram user, if the user has one + # {{ .UserID }} - the internal user ID of the Telegram user + # {{ .Deleted }} - true if the user has been deleted, false otherwise + displayname_template: "{{ if .Deleted }}Deleted account {{ .UserID }}{{ else }}{{ .FullName }}{{ end }}" + + +# Config options that affect the central bridge module. +bridge: + # The prefix for commands. Only required in non-management rooms. + command_prefix: '!tg' + # Should the bridge create a space for each login containing the rooms that account is in? + personal_filtering_spaces: true + # Whether the bridge should set names and avatars explicitly for DM portals. + # This is only necessary when using clients that don't support MSC4171. + private_chat_portal_meta: true + # Should events be handled asynchronously within portal rooms? + # If true, events may end up being out of order, but slow events won't block other ones. + # This is not yet safe to use. + async_events: false + # Should every user have their own portals rather than sharing them? + # By default, users who are in the same group on the remote network will be + # in the same Matrix room bridged to that group. If this is set to true, + # every user will get their own Matrix room instead. + # SETTING THIS IS IRREVERSIBLE AND POTENTIALLY DESTRUCTIVE IF PORTALS ALREADY EXIST. + split_portals: false + # Should the bridge resend `m.bridge` events to all portals on startup? + resend_bridge_info: false + # Should `m.bridge` events be sent without a state key? + # By default, the bridge uses a unique key that won't conflict with other bridges. + no_bridge_info_state_key: false + # Should bridge connection status be sent to the management room as `m.notice` events? + # These contain the same data that can be posted to an external HTTP server using homeserver -> status_endpoint. + # Allowed values: none, errors, all + bridge_status_notices: errors + # How long after an unknown error should the bridge attempt a full reconnect? + # Must be at least 1 minute. The bridge will add an extra ±20% jitter to this value. + unknown_error_auto_reconnect: null + # Maximum number of times to do the auto-reconnect above. + # The counter is per login, but is never reset except on logout and restart. + unknown_error_max_auto_reconnects: 10 + + # Should leaving Matrix rooms be bridged as leaving groups on the remote network? + bridge_matrix_leave: false + # Should `m.notice` messages be bridged? + bridge_notices: false + # Should room tags only be synced when creating the portal? Tags mean things like favorite/pin and archive/low priority. + # Tags currently can't be synced back to the remote network, so a continuous sync means tagging from Matrix will be undone. + tag_only_on_create: true + # List of tags to allow bridging. If empty, no tags will be bridged. + only_bridge_tags: [m.favourite, m.lowpriority] + # Should room mute status only be synced when creating the portal? + # Like tags, mutes can't currently be synced back to the remote network. + mute_only_on_create: true + # Should the bridge check the db to ensure that incoming events haven't been handled before + deduplicate_matrix_messages: false + # Should cross-room reply metadata be bridged? + # Most Matrix clients don't support this and servers may reject such messages too. + cross_room_replies: false + # If a state event fails to bridge, should the bridge revert any state changes made by that event? + revert_failed_state_changes: false + # In portals with no relay set, should Matrix users be kicked if they're + # not logged into an account that's in the remote chat? + kick_matrix_users: true + # Should the bridge listen to com.beeper.state_request events? + # This is not necessary for anything outside of Beeper. + enable_send_state_requests: false + # Should the com.beeper.bridge.identifiers list in global ghost profiles include phone numbers? + phone_numbers_in_profile: false + + # What should be done to portal rooms when a user logs out or is logged out? + # Permitted values: + # nothing - Do nothing, let the user stay in the portals + # kick - Remove the user from the portal rooms, but don't delete them + # unbridge - Remove all ghosts in the room and disassociate it from the remote chat + # delete - Remove all ghosts and users from the room (i.e. delete it) + cleanup_on_logout: + # Should cleanup on logout be enabled at all? + enabled: false + # Settings for manual logouts (explicitly initiated by the Matrix user) + manual: + # Action for private portals which will never be shared with other Matrix users. + private: nothing + # Action for portals with a relay user configured. + relayed: nothing + # Action for portals which may be shared, but don't currently have any other Matrix users. + shared_no_users: nothing + # Action for portals which have other logged-in Matrix users. + shared_has_users: nothing + # Settings for credentials being invalidated (initiated by the remote network, possibly through user action). + # Keys have the same meanings as in the manual section. + bad_credentials: + private: nothing + relayed: nothing + shared_no_users: nothing + shared_has_users: nothing + + # Settings for relay mode + relay: + # Whether relay mode should be allowed. If allowed, the set-relay command can be used to turn any + # authenticated user into a relaybot for that chat. + enabled: false + # Should only admins be allowed to set themselves as relay users? + # If true, non-admins can only set users listed in default_relays as relays in a room. + admin_only: true + # Should default relays be preferred when an explicit login ID isn't specified even if the user is logged in? + # This applies to the set-relay and bridge commands sent by any user, including admins. + prefer_default: true + # Should non-admins be allowed to use the bridge and sync-chat commands via default relays specified below? + allow_bridge: true + # List of user login IDs which anyone can set as a relay, as long as the relay user is in the room. + default_relays: [] + # The formats to use when sending messages via the relaybot. + # Available variables: + # .Sender.UserID - The Matrix user ID of the sender. + # .Sender.Displayname - The display name of the sender (if set). + # .Sender.RequiresDisambiguation - Whether the sender's name may be confused with the name of another user in the room. + # .Sender.DisambiguatedName - The disambiguated name of the sender. This will be the displayname if set, + # plus the user ID in parentheses if the displayname is not unique. + # If the displayname is not set, this is just the user ID. + # .Message - The `formatted_body` field of the message. + # .Caption - The `formatted_body` field of the message, if it's a caption. Otherwise an empty string. + # .FileName - The name of the file being sent. + message_formats: + m.text: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.notice: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.emote: "* {{ .Sender.DisambiguatedName }} {{ .Message }}" + m.file: "{{ .Sender.DisambiguatedName }} sent a file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.image: "{{ .Sender.DisambiguatedName }} sent an image{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.audio: "{{ .Sender.DisambiguatedName }} sent an audio file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.video: "{{ .Sender.DisambiguatedName }} sent a video{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.location: "{{ .Sender.DisambiguatedName }} sent a location{{ if .Caption }}: {{ .Caption }}{{ end }}" + # For networks that support per-message displaynames (i.e. Slack and Discord), the template for those names. + # This has all the Sender variables available under message_formats (but without the .Sender prefix). + # Note that you need to manually remove the displayname from message_formats above. + displayname_format: "{{ .DisambiguatedName }}" + + # Filter for automatically creating portals. + portal_create_filter: + # The mode for filtering, either `deny` or `allow` + mode: deny + # The list of portal IDs to deny or allow depending on the mode config. + # Items here can either be the plain portal ID as a string, or an object with `id` and `receiver` fields. + # The receiver field is necessary if you want to target a specific DM portal for example. + list: [] + # A list of user login IDs from which to always deny creating portals. + # This is meant to be used with default relays, such that the relay bot + # being added to a group wouldn't automatically trigger portal creation. + always_deny_from_login: [] + + # Permissions for using the bridge. + # Permitted values: + # relay - Talk through the relaybot (if enabled), no access otherwise + # commands - Access to use commands in the bridge, but not login. + # user - Access to use the bridge with puppeting. + # admin - Full access, user level with some additional administration tools. + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": relay + "example.com": user + "@admin:example.com": admin + +# Config for the bridge's database. +database: + # The database type. "sqlite3-fk-wal" and "postgres" are supported. + type: postgres + # The database URI. + # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended. + # https://github.com/mattn/go-sqlite3#connection-string + # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable + # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql + uri: postgres://user:password@host/database?sslmode=disable + # Maximum number of connections. + max_open_conns: 5 + max_idle_conns: 1 + # Maximum connection idle time and lifetime before they're closed. Disabled if null. + # Parsed with https://pkg.go.dev/time#ParseDuration + max_conn_idle_time: null + max_conn_lifetime: null + +# Homeserver details. +homeserver: + # The address that this appservice can use to connect to the homeserver. + # Local addresses without HTTPS are generally recommended when the bridge is running on the same machine, + # but https also works if they run on different machines. + address: http://example.localhost:8008 + # The domain of the homeserver (also known as server_name, used for MXIDs, etc). + domain: example.com + + # What software is the homeserver running? + # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. + software: standard + # The URL to push real-time bridge status to. + # If set, the bridge will make POST requests to this URL whenever a user's remote network connection state changes. + # The bridge will use the appservice as_token to authorize requests. + status_endpoint: + # Endpoint for reporting per-message status. + # If set, the bridge will make POST requests to this URL when processing a message from Matrix. + # It will make one request when receiving the message (step BRIDGE), one after decrypting if applicable + # (step DECRYPTED) and one after sending to the remote network (step REMOTE). Errors will also be reported. + # The bridge will use the appservice as_token to authorize requests. + message_send_checkpoint_endpoint: + # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? + async_media: false + + # Should the bridge use a websocket for connecting to the homeserver? + # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy, + # mautrix-asmux (deprecated), and hungryserv (proprietary). + websocket: false + # How often should the websocket be pinged? Pinging will be disabled if this is zero. + ping_interval_seconds: 0 + # When requests to the homeserver fail with a 502/503/504/429 status or a network error, + # how many times should the bridge retry the request before giving up? + retry_limit: 4 + +# Application service host/registration related details. +# Changing these values requires regeneration of the registration (except when noted otherwise) +appservice: + # The address that the homeserver can use to connect to this appservice. + # Like the homeserver address, a local non-https address is recommended when the bridge is on the same machine. + # If the bridge is elsewhere, you must secure the connection yourself (e.g. with https or wireguard) + # If you want to use https, you need to use a reverse proxy. The bridge does not have TLS support built in. + address: http://localhost:29317 + # A public address that external services can use to reach this appservice. + # This is only needed for things like public media. A reverse proxy is generally necessary when using this field. + # This value doesn't affect the registration file. + public_address: https://bridge.example.com + + # The hostname and port where this appservice should listen. + # For Docker, you generally have to change the hostname to 0.0.0.0. + hostname: 127.0.0.1 + port: 29317 + + # The unique ID of this appservice. + id: telegram + # Appservice bot details. + bot: + # Username of the appservice bot. + username: telegrambot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + displayname: Telegram bridge bot + avatar: mxc://maunium.net/tJCRmUyJDsgRNgqhOgoiHWbX + + # Whether to receive ephemeral events via appservice transactions. + ephemeral_events: true + # Should incoming events be handled asynchronously? + # This may be necessary for large public instances with lots of messages going through. + # However, messages will not be guaranteed to be bridged in the same order they were sent in. + # This value doesn't affect the registration file. + async_transactions: false + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + + # Localpart template of MXIDs for remote users. + # {{.}} is replaced with the internal ID of the user. + username_template: telegram_{{.}} + +# Config options that affect the Matrix connector of the bridge. +matrix: + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Whether the bridge should send a read receipt after successfully bridging a message. + delivery_receipts: false + # Whether the bridge should send error notices via m.notice events when a message fails to bridge. + message_error_notices: true + # Whether the bridge should update the m.direct account data event when double puppeting is enabled. + sync_direct_chat_list: true + # Whether created rooms should have federation enabled. If false, created portal rooms + # will never be federated. Changing this option requires recreating rooms. + federate_rooms: true + # The threshold as bytes after which the bridge should roundtrip uploads via the disk + # rather than keeping the whole file in memory. + upload_file_threshold: 5242880 + # Should the bridge set additional custom profile info for ghosts? + # This can make a lot of requests, as there's no batch profile update endpoint. + ghost_extra_profile_info: false + +# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors. +analytics: + # API key to send with tracking requests. Tracking is disabled if this is null. + token: null + # Address to send tracking requests to. + url: https://api.segment.io/v1/track + # Optional user ID for tracking events. If null, defaults to using Matrix user ID. + user_id: null + +# Settings for provisioning API +provisioning: + # Shared secret for authentication. If set to "generate" or null, a random secret will be generated, + # or if set to "disable", the provisioning API will be disabled. Must be at least 16 characters. + shared_secret: generate + # Whether to allow provisioning API requests to be authed using Matrix access tokens. + # This follows the same rules as double puppeting to determine which server to contact to check the token, + # which means that by default, it only works for users on the same server as the bridge. + allow_matrix_auth: true + # Enable debug API at /debug with provisioning authentication. + debug_endpoints: false + # Enable session transfers between bridges. Note that this only validates Matrix or shared secret + # auth before passing live network client credentials down in the response. + enable_session_transfers: false + +# Some networks require publicly accessible media download links (e.g. for user avatars when using Discord webhooks). +# These settings control whether the bridge will provide such public media access. +public_media: + # Should public media be enabled at all? + # The public_address field under the appservice section MUST be set when enabling public media. + enabled: false + # A key for signing public media URLs. + # If set to "generate", a random key will be generated. + signing_key: generate + # Number of seconds that public media URLs are valid for. + # If set to 0, URLs will never expire. + expiry: 0 + # Length of hash to use for public media URLs. Must be between 0 and 32. + hash_length: 32 + # The path prefix for generated URLs. Note that this will NOT change the path where media is actually served. + # If you change this, you must configure your reverse proxy to rewrite the path accordingly. + path_prefix: /_mautrix/publicmedia + # Should the bridge store media metadata in the database in order to support encrypted media and generate shorter URLs? + # If false, the generated URLs will just have the MXC URI and a HMAC signature. + # The hash_length field will be used to decide the length of the generated URL. + # This also allows invalidating URLs by deleting the database entry. + use_database: false + +# Settings for converting remote media to custom mxc:// URIs instead of reuploading. +# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html +direct_media: + # Should custom mxc:// URIs be used instead of reuploading media? + enabled: false + # The server name to use for the custom mxc:// URIs. + # This server name will effectively be a real Matrix server, it just won't implement anything other than media. + # You must either set up .well-known delegation from this domain to the bridge, or proxy the domain directly to the bridge. + server_name: discord-media.example.com + # Optionally a custom .well-known response. This defaults to `server_name:443` + well_known_response: + # Optionally specify a custom prefix for the media ID part of the MXC URI. + media_id_prefix: + # If the remote network supports media downloads over HTTP, then the bridge will use MSC3860/MSC3916 + # media download redirects if the requester supports it. Optionally, you can force redirects + # and not allow proxying at all by setting this to false. + # This option does nothing if the remote network does not support media downloads over HTTP. + allow_proxy: true + # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file. + # This key is also used to sign the mxc:// URIs to ensure only the bridge can generate them. + server_key: generate + +# Settings for backfilling messages. +# Note that the exact way settings are applied depends on the network connector. +# See https://docs.mau.fi/bridges/general/backfill.html for more details. +backfill: + # Whether to do backfilling at all. + enabled: false + # Maximum number of messages to backfill in empty rooms. + # If this is zero or negative, backfill will be disabled in new rooms. + max_initial_messages: 50 + # Maximum number of missed messages to backfill after bridge restarts. + max_catchup_messages: 500 + # If a backfilled chat is older than this number of hours, + # mark it as read even if it's unread on the remote network. + unread_hours_threshold: 720 + # Settings for backfilling threads within other backfills. + threads: + # Maximum number of messages to backfill in a new thread. + max_initial_messages: 50 + # Settings for the backwards backfill queue. This only applies when connecting to + # Beeper as standard Matrix servers don't support inserting messages into history. + queue: + # Should the backfill queue be enabled? + enabled: false + # Should manual calls to backfill queue tasks be allowed? + manual: false + # Number of messages to backfill in one batch. + batch_size: 100 + # Delay between batches in seconds. + batch_delay: 20 + # Maximum number of batches to backfill per portal. + # If set to -1, all available messages will be backfilled. + max_batches: -1 + # Optional network-specific overrides for max batches. + # Interpretation of this field depends on the network connector. + max_batches_override: {} + +# Settings for enabling double puppeting +double_puppet: + # Servers to always allow double puppeting from. + # This is only for other servers and should NOT contain the server the bridge is on. + servers: + anotherserver.example.org: https://matrix.anotherserver.example.org + # Whether to allow client API URL discovery for other servers. When using this option, + # users on other servers can use double puppeting even if their server URLs aren't + # explicitly added to the servers map above. + allow_discovery: false + # Shared secrets for automatic double puppeting. + # See https://docs.mau.fi/bridges/general/double-puppeting.html for instructions. + secrets: + example.com: as_token:foobar + +# End-to-bridge encryption support options. +# +# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. +encryption: + # Whether to enable encryption at all. If false, the bridge will not function in encrypted rooms. + allow: false + # Whether to force-enable encryption in all bridged rooms. + default: false + # Whether to require all messages to be encrypted and drop any unencrypted messages. + require: false + # Whether to use MSC3202/MSC4203 instead of /sync long polling for receiving encryption-related data. + # This is an experimental option, see the docs for more info. + # Changing this option requires updating the appservice registration file. + appservice: false + # Whether to use MSC4190 instead of appservice login to create the bridge bot device. + # Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202. + # Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861). + msc4190: false + # Whether to encrypt reactions and reply metadata as per MSC4392. + # This is not supported by most clients. + msc4392: false + # Should the bridge bot generate a recovery key and cross-signing keys and verify itself? + # Note that without the latest version of MSC4190, this will fail if you reset the bridge database. + # The generated recovery key will be saved in the kv_store table under `recovery_key`. + self_sign: false + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow_key_sharing: true + # Should m.mentions be sent in the unencrypted content? This is non-standard and should not be enabled. + plaintext_mentions: false + # Pickle key for encrypting encryption keys in the bridge database. + # If set to generate, a random key will be generated. + pickle_key: generate + # Options for deleting megolm sessions from the bridge. + delete_keys: + # Beeper-specific: delete outbound sessions when hungryserv confirms + # that the user has uploaded the key to key backup. + delete_outbound_on_ack: false + # Don't store outbound sessions in the inbound table. + dont_store_outbound: false + # Ratchet megolm sessions forward after decrypting messages. + ratchet_on_decrypt: false + # Delete fully used keys (index >= max_messages) after decrypting messages. + delete_fully_used_on_decrypt: false + # Delete previous megolm sessions from same device when receiving a new one. + delete_prev_on_new_session: false + # Delete megolm sessions received from a device when the device is deleted. + delete_on_device_delete: false + # Periodically delete megolm sessions when 2x max_age has passed since receiving the session. + periodically_delete_expired: false + # Delete inbound megolm sessions that don't have the received_at field used for + # automatic ratcheting and expired session deletion. This is meant as a migration + # to delete old keys prior to the bridge update. + delete_outdated_inbound: false + # What level of device verification should be required from users? + # + # Valid levels: + # unverified - Send keys to all device in the room. + # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys. + # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes). + # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot. + # Note that creating user signatures from the bridge bot is not currently possible. + # verified - Require manual per-device verification + # (currently only possible by modifying the `trust` column in the `crypto_device` database table). + verification_levels: + # Minimum level for which the bridge should send keys to when bridging messages from the remote network to Matrix. + receive: unverified + # Minimum level that the bridge should accept for incoming Matrix messages. + send: unverified + # Minimum level that the bridge should require for accepting key requests. + share: cross-signed-tofu + # Options for Megolm room key rotation. These options allow you to configure the m.room.encryption event content. + # See https://spec.matrix.org/v1.10/client-server-api/#mroomencryption for more information about that event. + rotation: + # Enable custom Megolm room key rotation settings. Note that these + # settings will only apply to rooms created after this option is set. + enable_custom: false + # The maximum number of milliseconds a session should be used + # before changing it. The Matrix spec recommends 604800000 (a week) + # as the default. + milliseconds: 604800000 + # The maximum number of messages that should be sent with a given a + # session before changing it. The Matrix spec recommends 100 as the + # default. + messages: 100 + # Disable rotating keys when a user's devices change? + # You should not enable this option unless you understand all the implications. + disable_device_change_key_rotation: false + +# Prefix for environment variables. All variables with this prefix must map to valid config fields. +# Nesting in variable names is represented with a dot (.). +# If there are no dots in the name, two underscores (__) are replaced with a dot. +# +# e.g. if the prefix is set to `BRIDGE_`, then `BRIDGE_APPSERVICE__AS_TOKEN` will set appservice.as_token. +# `BRIDGE_appservice.as_token` would work as well, but can't be set in a shell as easily. +# +# The variable names can also have a `_FILE` suffix to tell the bridge to read the value from the +# path set in the variable rather than using the value directly. +# +# If this is null, reading config fields from environment will be disabled. +env_config_prefix: null + +# Logging config. See https://github.com/tulir/zeroconfig for details. +logging: + min_level: debug + writers: + - type: stdout + format: pretty-colored + - type: file + format: json + filename: ./logs/bridge.log + max_size: 100 + max_backups: 10 + compress: false diff --git a/bridges/templates/twitter.yaml b/bridges/templates/twitter.yaml new file mode 100644 index 0000000..6bc8c4b --- /dev/null +++ b/bridges/templates/twitter.yaml @@ -0,0 +1,533 @@ +# Network-specific config options +network: + # Proxy to use for all Twitter connections. + proxy: null + # Alternative to proxy: an HTTP endpoint that returns the proxy URL to use for Twitter connections. + get_proxy_url: null + + # Displayname template for Twitter users. + # {{ .DisplayName }} is replaced with the display name of the Twitter user. + # {{ .Username }} is replaced with the username of the Twitter user. + displayname_template: "{{ .DisplayName }} (Twitter)" + + # Maximum number of conversations to sync on startup + conversation_sync_limit: 20 + + # Should the bridge cache sessions instead of resyncing chats on every restart? + cache_session: true + + # Should the bridge use "X" instead of "Twitter" in certain places, + # such as the management room welcome message and MSC2346 bridge info? + x: false + + +# Config options that affect the central bridge module. +bridge: + # The prefix for commands. Only required in non-management rooms. + command_prefix: '!twitter' + # Should the bridge create a space for each login containing the rooms that account is in? + personal_filtering_spaces: true + # Whether the bridge should set names and avatars explicitly for DM portals. + # This is only necessary when using clients that don't support MSC4171. + private_chat_portal_meta: true + # Should events be handled asynchronously within portal rooms? + # If true, events may end up being out of order, but slow events won't block other ones. + # This is not yet safe to use. + async_events: false + # Should every user have their own portals rather than sharing them? + # By default, users who are in the same group on the remote network will be + # in the same Matrix room bridged to that group. If this is set to true, + # every user will get their own Matrix room instead. + # SETTING THIS IS IRREVERSIBLE AND POTENTIALLY DESTRUCTIVE IF PORTALS ALREADY EXIST. + split_portals: false + # Should the bridge resend `m.bridge` events to all portals on startup? + resend_bridge_info: false + # Should `m.bridge` events be sent without a state key? + # By default, the bridge uses a unique key that won't conflict with other bridges. + no_bridge_info_state_key: false + # Should bridge connection status be sent to the management room as `m.notice` events? + # These contain the same data that can be posted to an external HTTP server using homeserver -> status_endpoint. + # Allowed values: none, errors, all + bridge_status_notices: errors + # How long after an unknown error should the bridge attempt a full reconnect? + # Must be at least 1 minute. The bridge will add an extra ±20% jitter to this value. + unknown_error_auto_reconnect: null + # Maximum number of times to do the auto-reconnect above. + # The counter is per login, but is never reset except on logout and restart. + unknown_error_max_auto_reconnects: 10 + + # Should leaving Matrix rooms be bridged as leaving groups on the remote network? + bridge_matrix_leave: false + # Should `m.notice` messages be bridged? + bridge_notices: false + # Should room tags only be synced when creating the portal? Tags mean things like favorite/pin and archive/low priority. + # Tags currently can't be synced back to the remote network, so a continuous sync means tagging from Matrix will be undone. + tag_only_on_create: true + # List of tags to allow bridging. If empty, no tags will be bridged. + only_bridge_tags: [m.favourite, m.lowpriority] + # Should room mute status only be synced when creating the portal? + # Like tags, mutes can't currently be synced back to the remote network. + mute_only_on_create: true + # Should the bridge check the db to ensure that incoming events haven't been handled before + deduplicate_matrix_messages: false + # Should cross-room reply metadata be bridged? + # Most Matrix clients don't support this and servers may reject such messages too. + cross_room_replies: false + # If a state event fails to bridge, should the bridge revert any state changes made by that event? + revert_failed_state_changes: false + # In portals with no relay set, should Matrix users be kicked if they're + # not logged into an account that's in the remote chat? + kick_matrix_users: true + # Should the bridge listen to com.beeper.state_request events? + # This is not necessary for anything outside of Beeper. + enable_send_state_requests: false + # Should the com.beeper.bridge.identifiers list in global ghost profiles include phone numbers? + phone_numbers_in_profile: false + + # What should be done to portal rooms when a user logs out or is logged out? + # Permitted values: + # nothing - Do nothing, let the user stay in the portals + # kick - Remove the user from the portal rooms, but don't delete them + # unbridge - Remove all ghosts in the room and disassociate it from the remote chat + # delete - Remove all ghosts and users from the room (i.e. delete it) + cleanup_on_logout: + # Should cleanup on logout be enabled at all? + enabled: false + # Settings for manual logouts (explicitly initiated by the Matrix user) + manual: + # Action for private portals which will never be shared with other Matrix users. + private: nothing + # Action for portals with a relay user configured. + relayed: nothing + # Action for portals which may be shared, but don't currently have any other Matrix users. + shared_no_users: nothing + # Action for portals which have other logged-in Matrix users. + shared_has_users: nothing + # Settings for credentials being invalidated (initiated by the remote network, possibly through user action). + # Keys have the same meanings as in the manual section. + bad_credentials: + private: nothing + relayed: nothing + shared_no_users: nothing + shared_has_users: nothing + + # Settings for relay mode + relay: + # Whether relay mode should be allowed. If allowed, the set-relay command can be used to turn any + # authenticated user into a relaybot for that chat. + enabled: false + # Should only admins be allowed to set themselves as relay users? + # If true, non-admins can only set users listed in default_relays as relays in a room. + admin_only: true + # Should default relays be preferred when an explicit login ID isn't specified even if the user is logged in? + # This applies to the set-relay and bridge commands sent by any user, including admins. + prefer_default: true + # Should non-admins be allowed to use the bridge and sync-chat commands via default relays specified below? + allow_bridge: true + # List of user login IDs which anyone can set as a relay, as long as the relay user is in the room. + default_relays: [] + # The formats to use when sending messages via the relaybot. + # Available variables: + # .Sender.UserID - The Matrix user ID of the sender. + # .Sender.Displayname - The display name of the sender (if set). + # .Sender.RequiresDisambiguation - Whether the sender's name may be confused with the name of another user in the room. + # .Sender.DisambiguatedName - The disambiguated name of the sender. This will be the displayname if set, + # plus the user ID in parentheses if the displayname is not unique. + # If the displayname is not set, this is just the user ID. + # .Message - The `formatted_body` field of the message. + # .Caption - The `formatted_body` field of the message, if it's a caption. Otherwise an empty string. + # .FileName - The name of the file being sent. + message_formats: + m.text: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.notice: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.emote: "* {{ .Sender.DisambiguatedName }} {{ .Message }}" + m.file: "{{ .Sender.DisambiguatedName }} sent a file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.image: "{{ .Sender.DisambiguatedName }} sent an image{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.audio: "{{ .Sender.DisambiguatedName }} sent an audio file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.video: "{{ .Sender.DisambiguatedName }} sent a video{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.location: "{{ .Sender.DisambiguatedName }} sent a location{{ if .Caption }}: {{ .Caption }}{{ end }}" + # For networks that support per-message displaynames (i.e. Slack and Discord), the template for those names. + # This has all the Sender variables available under message_formats (but without the .Sender prefix). + # Note that you need to manually remove the displayname from message_formats above. + displayname_format: "{{ .DisambiguatedName }}" + + # Filter for automatically creating portals. + portal_create_filter: + # The mode for filtering, either `deny` or `allow` + mode: deny + # The list of portal IDs to deny or allow depending on the mode config. + # Items here can either be the plain portal ID as a string, or an object with `id` and `receiver` fields. + # The receiver field is necessary if you want to target a specific DM portal for example. + list: [] + # A list of user login IDs from which to always deny creating portals. + # This is meant to be used with default relays, such that the relay bot + # being added to a group wouldn't automatically trigger portal creation. + always_deny_from_login: [] + + # Permissions for using the bridge. + # Permitted values: + # relay - Talk through the relaybot (if enabled), no access otherwise + # commands - Access to use commands in the bridge, but not login. + # user - Access to use the bridge with puppeting. + # admin - Full access, user level with some additional administration tools. + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": relay + "example.com": user + "@admin:example.com": admin + +# Config for the bridge's database. +database: + # The database type. "sqlite3-fk-wal" and "postgres" are supported. + type: postgres + # The database URI. + # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended. + # https://github.com/mattn/go-sqlite3#connection-string + # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable + # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql + uri: postgres://user:password@host/database?sslmode=disable + # Maximum number of connections. + max_open_conns: 5 + max_idle_conns: 1 + # Maximum connection idle time and lifetime before they're closed. Disabled if null. + # Parsed with https://pkg.go.dev/time#ParseDuration + max_conn_idle_time: null + max_conn_lifetime: null + +# Homeserver details. +homeserver: + # The address that this appservice can use to connect to the homeserver. + # Local addresses without HTTPS are generally recommended when the bridge is running on the same machine, + # but https also works if they run on different machines. + address: http://example.localhost:8008 + # The domain of the homeserver (also known as server_name, used for MXIDs, etc). + domain: example.com + + # What software is the homeserver running? + # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. + software: standard + # The URL to push real-time bridge status to. + # If set, the bridge will make POST requests to this URL whenever a user's remote network connection state changes. + # The bridge will use the appservice as_token to authorize requests. + status_endpoint: + # Endpoint for reporting per-message status. + # If set, the bridge will make POST requests to this URL when processing a message from Matrix. + # It will make one request when receiving the message (step BRIDGE), one after decrypting if applicable + # (step DECRYPTED) and one after sending to the remote network (step REMOTE). Errors will also be reported. + # The bridge will use the appservice as_token to authorize requests. + message_send_checkpoint_endpoint: + # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? + async_media: false + + # Should the bridge use a websocket for connecting to the homeserver? + # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy, + # mautrix-asmux (deprecated), and hungryserv (proprietary). + websocket: false + # How often should the websocket be pinged? Pinging will be disabled if this is zero. + ping_interval_seconds: 0 + # When requests to the homeserver fail with a 502/503/504/429 status or a network error, + # how many times should the bridge retry the request before giving up? + retry_limit: 4 + +# Application service host/registration related details. +# Changing these values requires regeneration of the registration (except when noted otherwise) +appservice: + # The address that the homeserver can use to connect to this appservice. + # Like the homeserver address, a local non-https address is recommended when the bridge is on the same machine. + # If the bridge is elsewhere, you must secure the connection yourself (e.g. with https or wireguard) + # If you want to use https, you need to use a reverse proxy. The bridge does not have TLS support built in. + address: http://localhost:29327 + # A public address that external services can use to reach this appservice. + # This is only needed for things like public media. A reverse proxy is generally necessary when using this field. + # This value doesn't affect the registration file. + public_address: https://bridge.example.com + + # The hostname and port where this appservice should listen. + # For Docker, you generally have to change the hostname to 0.0.0.0. + hostname: 127.0.0.1 + port: 29327 + + # The unique ID of this appservice. + id: twitter + # Appservice bot details. + bot: + # Username of the appservice bot. + username: twitterbot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + displayname: Twitter bridge bot + avatar: mxc://maunium.net/HVHcnusJkQcpVcsVGZRELLCn + + # Whether to receive ephemeral events via appservice transactions. + ephemeral_events: true + # Should incoming events be handled asynchronously? + # This may be necessary for large public instances with lots of messages going through. + # However, messages will not be guaranteed to be bridged in the same order they were sent in. + # This value doesn't affect the registration file. + async_transactions: false + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + + # Localpart template of MXIDs for remote users. + # {{.}} is replaced with the internal ID of the user. + username_template: twitter_{{.}} + +# Config options that affect the Matrix connector of the bridge. +matrix: + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Whether the bridge should send a read receipt after successfully bridging a message. + delivery_receipts: false + # Whether the bridge should send error notices via m.notice events when a message fails to bridge. + message_error_notices: true + # Whether the bridge should update the m.direct account data event when double puppeting is enabled. + sync_direct_chat_list: true + # Whether created rooms should have federation enabled. If false, created portal rooms + # will never be federated. Changing this option requires recreating rooms. + federate_rooms: true + # The threshold as bytes after which the bridge should roundtrip uploads via the disk + # rather than keeping the whole file in memory. + upload_file_threshold: 5242880 + # Should the bridge set additional custom profile info for ghosts? + # This can make a lot of requests, as there's no batch profile update endpoint. + ghost_extra_profile_info: false + +# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors. +analytics: + # API key to send with tracking requests. Tracking is disabled if this is null. + token: null + # Address to send tracking requests to. + url: https://api.segment.io/v1/track + # Optional user ID for tracking events. If null, defaults to using Matrix user ID. + user_id: null + +# Settings for provisioning API +provisioning: + # Shared secret for authentication. If set to "generate" or null, a random secret will be generated, + # or if set to "disable", the provisioning API will be disabled. Must be at least 16 characters. + shared_secret: generate + # Whether to allow provisioning API requests to be authed using Matrix access tokens. + # This follows the same rules as double puppeting to determine which server to contact to check the token, + # which means that by default, it only works for users on the same server as the bridge. + allow_matrix_auth: true + # Enable debug API at /debug with provisioning authentication. + debug_endpoints: false + # Enable session transfers between bridges. Note that this only validates Matrix or shared secret + # auth before passing live network client credentials down in the response. + enable_session_transfers: false + +# Some networks require publicly accessible media download links (e.g. for user avatars when using Discord webhooks). +# These settings control whether the bridge will provide such public media access. +public_media: + # Should public media be enabled at all? + # The public_address field under the appservice section MUST be set when enabling public media. + enabled: false + # A key for signing public media URLs. + # If set to "generate", a random key will be generated. + signing_key: generate + # Number of seconds that public media URLs are valid for. + # If set to 0, URLs will never expire. + expiry: 0 + # Length of hash to use for public media URLs. Must be between 0 and 32. + hash_length: 32 + # The path prefix for generated URLs. Note that this will NOT change the path where media is actually served. + # If you change this, you must configure your reverse proxy to rewrite the path accordingly. + path_prefix: /_mautrix/publicmedia + # Should the bridge store media metadata in the database in order to support encrypted media and generate shorter URLs? + # If false, the generated URLs will just have the MXC URI and a HMAC signature. + # The hash_length field will be used to decide the length of the generated URL. + # This also allows invalidating URLs by deleting the database entry. + use_database: false + +# Settings for converting remote media to custom mxc:// URIs instead of reuploading. +# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html +direct_media: + # Should custom mxc:// URIs be used instead of reuploading media? + enabled: false + # The server name to use for the custom mxc:// URIs. + # This server name will effectively be a real Matrix server, it just won't implement anything other than media. + # You must either set up .well-known delegation from this domain to the bridge, or proxy the domain directly to the bridge. + server_name: discord-media.example.com + # Optionally a custom .well-known response. This defaults to `server_name:443` + well_known_response: + # Optionally specify a custom prefix for the media ID part of the MXC URI. + media_id_prefix: + # If the remote network supports media downloads over HTTP, then the bridge will use MSC3860/MSC3916 + # media download redirects if the requester supports it. Optionally, you can force redirects + # and not allow proxying at all by setting this to false. + # This option does nothing if the remote network does not support media downloads over HTTP. + allow_proxy: true + # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file. + # This key is also used to sign the mxc:// URIs to ensure only the bridge can generate them. + server_key: generate + +# Settings for backfilling messages. +# Note that the exact way settings are applied depends on the network connector. +# See https://docs.mau.fi/bridges/general/backfill.html for more details. +backfill: + # Whether to do backfilling at all. + enabled: false + # Maximum number of messages to backfill in empty rooms. + # If this is zero or negative, backfill will be disabled in new rooms. + max_initial_messages: 50 + # Maximum number of missed messages to backfill after bridge restarts. + max_catchup_messages: 500 + # If a backfilled chat is older than this number of hours, + # mark it as read even if it's unread on the remote network. + unread_hours_threshold: 720 + # Settings for backfilling threads within other backfills. + threads: + # Maximum number of messages to backfill in a new thread. + max_initial_messages: 50 + # Settings for the backwards backfill queue. This only applies when connecting to + # Beeper as standard Matrix servers don't support inserting messages into history. + queue: + # Should the backfill queue be enabled? + enabled: false + # Should manual calls to backfill queue tasks be allowed? + manual: false + # Number of messages to backfill in one batch. + batch_size: 100 + # Delay between batches in seconds. + batch_delay: 20 + # Maximum number of batches to backfill per portal. + # If set to -1, all available messages will be backfilled. + max_batches: -1 + # Optional network-specific overrides for max batches. + # Interpretation of this field depends on the network connector. + max_batches_override: {} + +# Settings for enabling double puppeting +double_puppet: + # Servers to always allow double puppeting from. + # This is only for other servers and should NOT contain the server the bridge is on. + servers: + anotherserver.example.org: https://matrix.anotherserver.example.org + # Whether to allow client API URL discovery for other servers. When using this option, + # users on other servers can use double puppeting even if their server URLs aren't + # explicitly added to the servers map above. + allow_discovery: false + # Shared secrets for automatic double puppeting. + # See https://docs.mau.fi/bridges/general/double-puppeting.html for instructions. + secrets: + example.com: as_token:foobar + +# End-to-bridge encryption support options. +# +# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. +encryption: + # Whether to enable encryption at all. If false, the bridge will not function in encrypted rooms. + allow: false + # Whether to force-enable encryption in all bridged rooms. + default: false + # Whether to require all messages to be encrypted and drop any unencrypted messages. + require: false + # Whether to use MSC3202/MSC4203 instead of /sync long polling for receiving encryption-related data. + # This is an experimental option, see the docs for more info. + # Changing this option requires updating the appservice registration file. + appservice: false + # Whether to use MSC4190 instead of appservice login to create the bridge bot device. + # Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202. + # Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861). + msc4190: false + # Whether to encrypt reactions and reply metadata as per MSC4392. + # This is not supported by most clients. + msc4392: false + # Should the bridge bot generate a recovery key and cross-signing keys and verify itself? + # Note that without the latest version of MSC4190, this will fail if you reset the bridge database. + # The generated recovery key will be saved in the kv_store table under `recovery_key`. + self_sign: false + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow_key_sharing: true + # Should m.mentions be sent in the unencrypted content? This is non-standard and should not be enabled. + plaintext_mentions: false + # Pickle key for encrypting encryption keys in the bridge database. + # If set to generate, a random key will be generated. + pickle_key: generate + # Options for deleting megolm sessions from the bridge. + delete_keys: + # Beeper-specific: delete outbound sessions when hungryserv confirms + # that the user has uploaded the key to key backup. + delete_outbound_on_ack: false + # Don't store outbound sessions in the inbound table. + dont_store_outbound: false + # Ratchet megolm sessions forward after decrypting messages. + ratchet_on_decrypt: false + # Delete fully used keys (index >= max_messages) after decrypting messages. + delete_fully_used_on_decrypt: false + # Delete previous megolm sessions from same device when receiving a new one. + delete_prev_on_new_session: false + # Delete megolm sessions received from a device when the device is deleted. + delete_on_device_delete: false + # Periodically delete megolm sessions when 2x max_age has passed since receiving the session. + periodically_delete_expired: false + # Delete inbound megolm sessions that don't have the received_at field used for + # automatic ratcheting and expired session deletion. This is meant as a migration + # to delete old keys prior to the bridge update. + delete_outdated_inbound: false + # What level of device verification should be required from users? + # + # Valid levels: + # unverified - Send keys to all device in the room. + # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys. + # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes). + # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot. + # Note that creating user signatures from the bridge bot is not currently possible. + # verified - Require manual per-device verification + # (currently only possible by modifying the `trust` column in the `crypto_device` database table). + verification_levels: + # Minimum level for which the bridge should send keys to when bridging messages from the remote network to Matrix. + receive: unverified + # Minimum level that the bridge should accept for incoming Matrix messages. + send: unverified + # Minimum level that the bridge should require for accepting key requests. + share: cross-signed-tofu + # Options for Megolm room key rotation. These options allow you to configure the m.room.encryption event content. + # See https://spec.matrix.org/v1.10/client-server-api/#mroomencryption for more information about that event. + rotation: + # Enable custom Megolm room key rotation settings. Note that these + # settings will only apply to rooms created after this option is set. + enable_custom: false + # The maximum number of milliseconds a session should be used + # before changing it. The Matrix spec recommends 604800000 (a week) + # as the default. + milliseconds: 604800000 + # The maximum number of messages that should be sent with a given a + # session before changing it. The Matrix spec recommends 100 as the + # default. + messages: 100 + # Disable rotating keys when a user's devices change? + # You should not enable this option unless you understand all the implications. + disable_device_change_key_rotation: false + +# Prefix for environment variables. All variables with this prefix must map to valid config fields. +# Nesting in variable names is represented with a dot (.). +# If there are no dots in the name, two underscores (__) are replaced with a dot. +# +# e.g. if the prefix is set to `BRIDGE_`, then `BRIDGE_APPSERVICE__AS_TOKEN` will set appservice.as_token. +# `BRIDGE_appservice.as_token` would work as well, but can't be set in a shell as easily. +# +# The variable names can also have a `_FILE` suffix to tell the bridge to read the value from the +# path set in the variable rather than using the value directly. +# +# If this is null, reading config fields from environment will be disabled. +env_config_prefix: null + +# Logging config. See https://github.com/tulir/zeroconfig for details. +logging: + min_level: debug + writers: + - type: stdout + format: pretty-colored + - type: file + format: json + filename: ./logs/bridge.log + max_size: 100 + max_backups: 10 + compress: false diff --git a/bridges/templates/whatsapp.yaml b/bridges/templates/whatsapp.yaml new file mode 100644 index 0000000..2205cba --- /dev/null +++ b/bridges/templates/whatsapp.yaml @@ -0,0 +1,640 @@ +# Network-specific config options +network: + # Device name that's shown in the "WhatsApp Web" section in the mobile app. + os_name: Mautrix-WhatsApp bridge + # Browser name that determines the logo shown in the mobile app. + # Must be "unknown" for a generic icon or a valid browser name if you want a specific icon. + # List of valid browser names: https://github.com/tulir/whatsmeow/blob/efc632c008604016ddde63bfcfca8de4e5304da9/binary/proto/def.proto#L43-L64 + browser_name: unknown + + # Proxy to use for all WhatsApp connections. + proxy: null + # Alternative to proxy: an HTTP endpoint that returns the proxy URL to use for WhatsApp connections. + get_proxy_url: null + # Whether the proxy options should only apply to the login websocket and not to authenticated connections. + proxy_only_login: false + + # Displayname template for WhatsApp users. + # {{.PushName}} - nickname set by the WhatsApp user + # {{.BusinessName}} - validated WhatsApp business name + # {{.Phone}} - phone number (international format) + # {{.RedactedPhone}} - phone number with middle digits replaced by "∙" + # {{.FullName}} - Name you set in the contacts list + displayname_template: '{{or .BusinessName .PushName .Phone .RedactedPhone "Unknown user"}} (WA)' + + # Should incoming calls send a message to the Matrix room? + call_start_notices: true + # Should another user's cryptographic identity changing send a message to Matrix? + identity_change_notices: false + # Should the bridge mark you as online on WhatsApp when you send typing notifications? + # Full presence bridging is not supported. + send_presence_on_typing: false + # Should WhatsApp status messages be bridged into a Matrix room? + enable_status_broadcast: true + # Should sending WhatsApp status messages be allowed? + # This can cause issues if the user has lots of contacts, so it's disabled by default. + disable_status_broadcast_send: true + # Should the status broadcast room be muted and moved into low priority by default? + # This is only applied when creating the room, the user can unmute it later. + mute_status_broadcast: true + # Tag to apply to pinned chats on WhatsApp. + pinned_tag: m.favourite + # Tag to apply to archived chats on WhatsApp. + # Set to m.lowpriority to move them to low priority. + archive_tag: + # Tag to apply to the status broadcast room. + status_broadcast_tag: m.lowpriority + # Should the bridge use thumbnails from WhatsApp? + # They're disabled by default due to very low resolution. + whatsapp_thumbnail: false + # Should the bridge detect URLs in outgoing messages, ask the homeserver to generate a preview, + # and send it to WhatsApp? URL previews can always be sent using the `com.beeper.linkpreviews` + # key in the event content even if this is disabled. + url_previews: false + # Should polls be sent using unstable MSC3381 event types? + extev_polls: false + # Should view-once messages be disabled entirely? + disable_view_once: false + # Should the bridge always send "active" delivery receipts (two gray ticks on WhatsApp) + # even if the user isn't marked as online (e.g. when presence bridging isn't enabled)? + # + # By default, the bridge acts like WhatsApp web, which only sends active delivery + # receipts when it's in the foreground. + force_active_delivery_receipts: false + # When direct media is enabled and a piece of media isn't available on the WhatsApp servers, + # should it be automatically requested from the phone? + direct_media_auto_request: true + # Should the bridge automatically reconnect if it fails to connect on startup? + initial_auto_reconnect: true + # WhatsApp messages are sometimes undecryptable. Should the bridge store messages it sends in the + # bridge database in order to accept retry receipts from other WhatsApp users for messages sent via + # the bridge? By default, the bridge only stores messages in memory, and therefore can't accept + # retry receipts if the bridge is restarted after the message is sent. + use_whatsapp_retry_store: false + + # Settings for converting animated stickers. + animated_sticker: + # Format to which animated stickers should be converted. + # disable - No conversion, just unzip and send raw lottie JSON + # png - converts to non-animated png (fastest) + # gif - converts to animated gif + # webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support + # webp - converts to animated webp, requires ffmpeg executable with webp codec/container support + target: webp + # Arguments for converter. All converters take width and height. + args: + width: 320 + height: 320 + fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended) + + # Settings for handling history sync payloads. + history_sync: + # How many conversations should the bridge create after login? + # If -1, all conversations received from history sync will be bridged. + # Other conversations will be backfilled on demand when receiving a message. + max_initial_conversations: -1 + # Should the bridge request a full sync from the phone when logging in? + # This bumps the size of history syncs from 3 months to 1 year. + request_full_sync: false + # Time to wait for history sync payloads before starting backfill. Each new payload resets the timer. + # If this is too low, the backfill may happen with incomplete history + # and backfill less messages than what is configured in the backfill section. + dispatch_wait: 1m + # Configuration parameters that are sent to the phone along with the request full sync flag. + # By default, (when the values are null or 0), the config isn't sent at all. + full_sync_config: + # Number of days of history to request. + # The limit seems to be around 3 years, but using higher values doesn't break. + days_limit: null + # This is presumably the maximum size of the transferred history sync blob, which may affect what the phone includes in the blob. + size_mb_limit: null + # This is presumably the local storage quota, which may affect what the phone includes in the history sync blob. + storage_quota_mb: null + # Settings for media requests. If the media expired, then it will not be on the WA servers. + # Media can always be requested by reacting with the ♻️ (recycle) emoji. + # These settings determine if the media requests should be done automatically during or after backfill. + media_requests: + # Should the expired media be automatically requested from the server as part of the backfill process? + auto_request_media: true + # Whether to request the media immediately after the media message is backfilled ("immediate") + # or at a specific time of the day ("local_time"). + request_method: immediate + # If request_method is "local_time", what time should the requests be sent (in minutes after midnight)? + request_local_time: 120 + # Maximum number of media request responses to handle in parallel per user. + max_async_handle: 2 + # Use on-demand history sync requests for fetching older messages? + # This only applies when using the backfill queue, never for forward backfills. + backwards_on_demand: false + + +# Config options that affect the central bridge module. +bridge: + # The prefix for commands. Only required in non-management rooms. + command_prefix: '!wa' + # Should the bridge create a space for each login containing the rooms that account is in? + personal_filtering_spaces: true + # Whether the bridge should set names and avatars explicitly for DM portals. + # This is only necessary when using clients that don't support MSC4171. + private_chat_portal_meta: true + # Should events be handled asynchronously within portal rooms? + # If true, events may end up being out of order, but slow events won't block other ones. + # This is not yet safe to use. + async_events: false + # Should every user have their own portals rather than sharing them? + # By default, users who are in the same group on the remote network will be + # in the same Matrix room bridged to that group. If this is set to true, + # every user will get their own Matrix room instead. + # SETTING THIS IS IRREVERSIBLE AND POTENTIALLY DESTRUCTIVE IF PORTALS ALREADY EXIST. + split_portals: false + # Should the bridge resend `m.bridge` events to all portals on startup? + resend_bridge_info: false + # Should `m.bridge` events be sent without a state key? + # By default, the bridge uses a unique key that won't conflict with other bridges. + no_bridge_info_state_key: false + # Should bridge connection status be sent to the management room as `m.notice` events? + # These contain the same data that can be posted to an external HTTP server using homeserver -> status_endpoint. + # Allowed values: none, errors, all + bridge_status_notices: errors + # How long after an unknown error should the bridge attempt a full reconnect? + # Must be at least 1 minute. The bridge will add an extra ±20% jitter to this value. + unknown_error_auto_reconnect: null + # Maximum number of times to do the auto-reconnect above. + # The counter is per login, but is never reset except on logout and restart. + unknown_error_max_auto_reconnects: 10 + + # Should leaving Matrix rooms be bridged as leaving groups on the remote network? + bridge_matrix_leave: false + # Should `m.notice` messages be bridged? + bridge_notices: false + # Should room tags only be synced when creating the portal? Tags mean things like favorite/pin and archive/low priority. + # Tags currently can't be synced back to the remote network, so a continuous sync means tagging from Matrix will be undone. + tag_only_on_create: true + # List of tags to allow bridging. If empty, no tags will be bridged. + only_bridge_tags: [m.favourite, m.lowpriority] + # Should room mute status only be synced when creating the portal? + # Like tags, mutes can't currently be synced back to the remote network. + mute_only_on_create: true + # Should the bridge check the db to ensure that incoming events haven't been handled before + deduplicate_matrix_messages: false + # Should cross-room reply metadata be bridged? + # Most Matrix clients don't support this and servers may reject such messages too. + cross_room_replies: false + # If a state event fails to bridge, should the bridge revert any state changes made by that event? + revert_failed_state_changes: false + # In portals with no relay set, should Matrix users be kicked if they're + # not logged into an account that's in the remote chat? + kick_matrix_users: true + # Should the bridge listen to com.beeper.state_request events? + # This is not necessary for anything outside of Beeper. + enable_send_state_requests: false + # Should the com.beeper.bridge.identifiers list in global ghost profiles include phone numbers? + phone_numbers_in_profile: false + + # What should be done to portal rooms when a user logs out or is logged out? + # Permitted values: + # nothing - Do nothing, let the user stay in the portals + # kick - Remove the user from the portal rooms, but don't delete them + # unbridge - Remove all ghosts in the room and disassociate it from the remote chat + # delete - Remove all ghosts and users from the room (i.e. delete it) + cleanup_on_logout: + # Should cleanup on logout be enabled at all? + enabled: false + # Settings for manual logouts (explicitly initiated by the Matrix user) + manual: + # Action for private portals which will never be shared with other Matrix users. + private: nothing + # Action for portals with a relay user configured. + relayed: nothing + # Action for portals which may be shared, but don't currently have any other Matrix users. + shared_no_users: nothing + # Action for portals which have other logged-in Matrix users. + shared_has_users: nothing + # Settings for credentials being invalidated (initiated by the remote network, possibly through user action). + # Keys have the same meanings as in the manual section. + bad_credentials: + private: nothing + relayed: nothing + shared_no_users: nothing + shared_has_users: nothing + + # Settings for relay mode + relay: + # Whether relay mode should be allowed. If allowed, the set-relay command can be used to turn any + # authenticated user into a relaybot for that chat. + enabled: false + # Should only admins be allowed to set themselves as relay users? + # If true, non-admins can only set users listed in default_relays as relays in a room. + admin_only: true + # Should default relays be preferred when an explicit login ID isn't specified even if the user is logged in? + # This applies to the set-relay and bridge commands sent by any user, including admins. + prefer_default: true + # Should non-admins be allowed to use the bridge and sync-chat commands via default relays specified below? + allow_bridge: true + # List of user login IDs which anyone can set as a relay, as long as the relay user is in the room. + default_relays: [] + # The formats to use when sending messages via the relaybot. + # Available variables: + # .Sender.UserID - The Matrix user ID of the sender. + # .Sender.Displayname - The display name of the sender (if set). + # .Sender.RequiresDisambiguation - Whether the sender's name may be confused with the name of another user in the room. + # .Sender.DisambiguatedName - The disambiguated name of the sender. This will be the displayname if set, + # plus the user ID in parentheses if the displayname is not unique. + # If the displayname is not set, this is just the user ID. + # .Message - The `formatted_body` field of the message. + # .Caption - The `formatted_body` field of the message, if it's a caption. Otherwise an empty string. + # .FileName - The name of the file being sent. + message_formats: + m.text: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.notice: "{{ .Sender.DisambiguatedName }}: {{ .Message }}" + m.emote: "* {{ .Sender.DisambiguatedName }} {{ .Message }}" + m.file: "{{ .Sender.DisambiguatedName }} sent a file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.image: "{{ .Sender.DisambiguatedName }} sent an image{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.audio: "{{ .Sender.DisambiguatedName }} sent an audio file{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.video: "{{ .Sender.DisambiguatedName }} sent a video{{ if .Caption }}: {{ .Caption }}{{ end }}" + m.location: "{{ .Sender.DisambiguatedName }} sent a location{{ if .Caption }}: {{ .Caption }}{{ end }}" + # For networks that support per-message displaynames (i.e. Slack and Discord), the template for those names. + # This has all the Sender variables available under message_formats (but without the .Sender prefix). + # Note that you need to manually remove the displayname from message_formats above. + displayname_format: "{{ .DisambiguatedName }}" + + # Filter for automatically creating portals. + portal_create_filter: + # The mode for filtering, either `deny` or `allow` + mode: deny + # The list of portal IDs to deny or allow depending on the mode config. + # Items here can either be the plain portal ID as a string, or an object with `id` and `receiver` fields. + # The receiver field is necessary if you want to target a specific DM portal for example. + list: [] + # A list of user login IDs from which to always deny creating portals. + # This is meant to be used with default relays, such that the relay bot + # being added to a group wouldn't automatically trigger portal creation. + always_deny_from_login: [] + + # Permissions for using the bridge. + # Permitted values: + # relay - Talk through the relaybot (if enabled), no access otherwise + # commands - Access to use commands in the bridge, but not login. + # user - Access to use the bridge with puppeting. + # admin - Full access, user level with some additional administration tools. + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": relay + "example.com": user + "@admin:example.com": admin + +# Config for the bridge's database. +database: + # The database type. "sqlite3-fk-wal" and "postgres" are supported. + type: postgres + # The database URI. + # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended. + # https://github.com/mattn/go-sqlite3#connection-string + # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable + # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql + uri: postgres://user:password@host/database?sslmode=disable + # Maximum number of connections. + max_open_conns: 5 + max_idle_conns: 1 + # Maximum connection idle time and lifetime before they're closed. Disabled if null. + # Parsed with https://pkg.go.dev/time#ParseDuration + max_conn_idle_time: null + max_conn_lifetime: null + +# Homeserver details. +homeserver: + # The address that this appservice can use to connect to the homeserver. + # Local addresses without HTTPS are generally recommended when the bridge is running on the same machine, + # but https also works if they run on different machines. + address: http://example.localhost:8008 + # The domain of the homeserver (also known as server_name, used for MXIDs, etc). + domain: example.com + + # What software is the homeserver running? + # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. + software: standard + # The URL to push real-time bridge status to. + # If set, the bridge will make POST requests to this URL whenever a user's remote network connection state changes. + # The bridge will use the appservice as_token to authorize requests. + status_endpoint: + # Endpoint for reporting per-message status. + # If set, the bridge will make POST requests to this URL when processing a message from Matrix. + # It will make one request when receiving the message (step BRIDGE), one after decrypting if applicable + # (step DECRYPTED) and one after sending to the remote network (step REMOTE). Errors will also be reported. + # The bridge will use the appservice as_token to authorize requests. + message_send_checkpoint_endpoint: + # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? + async_media: false + + # Should the bridge use a websocket for connecting to the homeserver? + # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy, + # mautrix-asmux (deprecated), and hungryserv (proprietary). + websocket: false + # How often should the websocket be pinged? Pinging will be disabled if this is zero. + ping_interval_seconds: 0 + # When requests to the homeserver fail with a 502/503/504/429 status or a network error, + # how many times should the bridge retry the request before giving up? + retry_limit: 4 + +# Application service host/registration related details. +# Changing these values requires regeneration of the registration (except when noted otherwise) +appservice: + # The address that the homeserver can use to connect to this appservice. + # Like the homeserver address, a local non-https address is recommended when the bridge is on the same machine. + # If the bridge is elsewhere, you must secure the connection yourself (e.g. with https or wireguard) + # If you want to use https, you need to use a reverse proxy. The bridge does not have TLS support built in. + address: http://localhost:29318 + # A public address that external services can use to reach this appservice. + # This is only needed for things like public media. A reverse proxy is generally necessary when using this field. + # This value doesn't affect the registration file. + public_address: https://bridge.example.com + + # The hostname and port where this appservice should listen. + # For Docker, you generally have to change the hostname to 0.0.0.0. + hostname: 127.0.0.1 + port: 29318 + + # The unique ID of this appservice. + id: whatsapp + # Appservice bot details. + bot: + # Username of the appservice bot. + username: whatsappbot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + displayname: WhatsApp bridge bot + avatar: mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr + + # Whether to receive ephemeral events via appservice transactions. + ephemeral_events: true + # Should incoming events be handled asynchronously? + # This may be necessary for large public instances with lots of messages going through. + # However, messages will not be guaranteed to be bridged in the same order they were sent in. + # This value doesn't affect the registration file. + async_transactions: false + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + + # Localpart template of MXIDs for remote users. + # {{.}} is replaced with the internal ID of the user. + username_template: whatsapp_{{.}} + +# Config options that affect the Matrix connector of the bridge. +matrix: + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Whether the bridge should send a read receipt after successfully bridging a message. + delivery_receipts: false + # Whether the bridge should send error notices via m.notice events when a message fails to bridge. + message_error_notices: true + # Whether the bridge should update the m.direct account data event when double puppeting is enabled. + sync_direct_chat_list: true + # Whether created rooms should have federation enabled. If false, created portal rooms + # will never be federated. Changing this option requires recreating rooms. + federate_rooms: true + # The threshold as bytes after which the bridge should roundtrip uploads via the disk + # rather than keeping the whole file in memory. + upload_file_threshold: 5242880 + # Should the bridge set additional custom profile info for ghosts? + # This can make a lot of requests, as there's no batch profile update endpoint. + ghost_extra_profile_info: false + +# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors. +analytics: + # API key to send with tracking requests. Tracking is disabled if this is null. + token: null + # Address to send tracking requests to. + url: https://api.segment.io/v1/track + # Optional user ID for tracking events. If null, defaults to using Matrix user ID. + user_id: null + +# Settings for provisioning API +provisioning: + # Shared secret for authentication. If set to "generate" or null, a random secret will be generated, + # or if set to "disable", the provisioning API will be disabled. Must be at least 16 characters. + shared_secret: generate + # Whether to allow provisioning API requests to be authed using Matrix access tokens. + # This follows the same rules as double puppeting to determine which server to contact to check the token, + # which means that by default, it only works for users on the same server as the bridge. + allow_matrix_auth: true + # Enable debug API at /debug with provisioning authentication. + debug_endpoints: false + # Enable session transfers between bridges. Note that this only validates Matrix or shared secret + # auth before passing live network client credentials down in the response. + enable_session_transfers: false + +# Some networks require publicly accessible media download links (e.g. for user avatars when using Discord webhooks). +# These settings control whether the bridge will provide such public media access. +public_media: + # Should public media be enabled at all? + # The public_address field under the appservice section MUST be set when enabling public media. + enabled: false + # A key for signing public media URLs. + # If set to "generate", a random key will be generated. + signing_key: generate + # Number of seconds that public media URLs are valid for. + # If set to 0, URLs will never expire. + expiry: 0 + # Length of hash to use for public media URLs. Must be between 0 and 32. + hash_length: 32 + # The path prefix for generated URLs. Note that this will NOT change the path where media is actually served. + # If you change this, you must configure your reverse proxy to rewrite the path accordingly. + path_prefix: /_mautrix/publicmedia + # Should the bridge store media metadata in the database in order to support encrypted media and generate shorter URLs? + # If false, the generated URLs will just have the MXC URI and a HMAC signature. + # The hash_length field will be used to decide the length of the generated URL. + # This also allows invalidating URLs by deleting the database entry. + use_database: false + +# Settings for converting remote media to custom mxc:// URIs instead of reuploading. +# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html +direct_media: + # Should custom mxc:// URIs be used instead of reuploading media? + enabled: false + # The server name to use for the custom mxc:// URIs. + # This server name will effectively be a real Matrix server, it just won't implement anything other than media. + # You must either set up .well-known delegation from this domain to the bridge, or proxy the domain directly to the bridge. + server_name: discord-media.example.com + # Optionally a custom .well-known response. This defaults to `server_name:443` + well_known_response: + # Optionally specify a custom prefix for the media ID part of the MXC URI. + media_id_prefix: + # If the remote network supports media downloads over HTTP, then the bridge will use MSC3860/MSC3916 + # media download redirects if the requester supports it. Optionally, you can force redirects + # and not allow proxying at all by setting this to false. + # This option does nothing if the remote network does not support media downloads over HTTP. + allow_proxy: true + # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file. + # This key is also used to sign the mxc:// URIs to ensure only the bridge can generate them. + server_key: generate + +# Settings for backfilling messages. +# Note that the exact way settings are applied depends on the network connector. +# See https://docs.mau.fi/bridges/general/backfill.html for more details. +backfill: + # Whether to do backfilling at all. + enabled: false + # Maximum number of messages to backfill in empty rooms. + # If this is zero or negative, backfill will be disabled in new rooms. + max_initial_messages: 50 + # Maximum number of missed messages to backfill after bridge restarts. + max_catchup_messages: 500 + # If a backfilled chat is older than this number of hours, + # mark it as read even if it's unread on the remote network. + unread_hours_threshold: 720 + # Settings for backfilling threads within other backfills. + threads: + # Maximum number of messages to backfill in a new thread. + max_initial_messages: 50 + # Settings for the backwards backfill queue. This only applies when connecting to + # Beeper as standard Matrix servers don't support inserting messages into history. + queue: + # Should the backfill queue be enabled? + enabled: false + # Should manual calls to backfill queue tasks be allowed? + manual: false + # Number of messages to backfill in one batch. + batch_size: 100 + # Delay between batches in seconds. + batch_delay: 20 + # Maximum number of batches to backfill per portal. + # If set to -1, all available messages will be backfilled. + max_batches: -1 + # Optional network-specific overrides for max batches. + # Interpretation of this field depends on the network connector. + max_batches_override: {} + +# Settings for enabling double puppeting +double_puppet: + # Servers to always allow double puppeting from. + # This is only for other servers and should NOT contain the server the bridge is on. + servers: + anotherserver.example.org: https://matrix.anotherserver.example.org + # Whether to allow client API URL discovery for other servers. When using this option, + # users on other servers can use double puppeting even if their server URLs aren't + # explicitly added to the servers map above. + allow_discovery: false + # Shared secrets for automatic double puppeting. + # See https://docs.mau.fi/bridges/general/double-puppeting.html for instructions. + secrets: + example.com: as_token:foobar + +# End-to-bridge encryption support options. +# +# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. +encryption: + # Whether to enable encryption at all. If false, the bridge will not function in encrypted rooms. + allow: false + # Whether to force-enable encryption in all bridged rooms. + default: false + # Whether to require all messages to be encrypted and drop any unencrypted messages. + require: false + # Whether to use MSC3202/MSC4203 instead of /sync long polling for receiving encryption-related data. + # This is an experimental option, see the docs for more info. + # Changing this option requires updating the appservice registration file. + appservice: false + # Whether to use MSC4190 instead of appservice login to create the bridge bot device. + # Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202. + # Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861). + msc4190: false + # Whether to encrypt reactions and reply metadata as per MSC4392. + # This is not supported by most clients. + msc4392: false + # Should the bridge bot generate a recovery key and cross-signing keys and verify itself? + # Note that without the latest version of MSC4190, this will fail if you reset the bridge database. + # The generated recovery key will be saved in the kv_store table under `recovery_key`. + self_sign: false + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow_key_sharing: true + # Should m.mentions be sent in the unencrypted content? This is non-standard and should not be enabled. + plaintext_mentions: false + # Pickle key for encrypting encryption keys in the bridge database. + # If set to generate, a random key will be generated. + pickle_key: generate + # Options for deleting megolm sessions from the bridge. + delete_keys: + # Beeper-specific: delete outbound sessions when hungryserv confirms + # that the user has uploaded the key to key backup. + delete_outbound_on_ack: false + # Don't store outbound sessions in the inbound table. + dont_store_outbound: false + # Ratchet megolm sessions forward after decrypting messages. + ratchet_on_decrypt: false + # Delete fully used keys (index >= max_messages) after decrypting messages. + delete_fully_used_on_decrypt: false + # Delete previous megolm sessions from same device when receiving a new one. + delete_prev_on_new_session: false + # Delete megolm sessions received from a device when the device is deleted. + delete_on_device_delete: false + # Periodically delete megolm sessions when 2x max_age has passed since receiving the session. + periodically_delete_expired: false + # Delete inbound megolm sessions that don't have the received_at field used for + # automatic ratcheting and expired session deletion. This is meant as a migration + # to delete old keys prior to the bridge update. + delete_outdated_inbound: false + # What level of device verification should be required from users? + # + # Valid levels: + # unverified - Send keys to all device in the room. + # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys. + # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes). + # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot. + # Note that creating user signatures from the bridge bot is not currently possible. + # verified - Require manual per-device verification + # (currently only possible by modifying the `trust` column in the `crypto_device` database table). + verification_levels: + # Minimum level for which the bridge should send keys to when bridging messages from the remote network to Matrix. + receive: unverified + # Minimum level that the bridge should accept for incoming Matrix messages. + send: unverified + # Minimum level that the bridge should require for accepting key requests. + share: cross-signed-tofu + # Options for Megolm room key rotation. These options allow you to configure the m.room.encryption event content. + # See https://spec.matrix.org/v1.10/client-server-api/#mroomencryption for more information about that event. + rotation: + # Enable custom Megolm room key rotation settings. Note that these + # settings will only apply to rooms created after this option is set. + enable_custom: false + # The maximum number of milliseconds a session should be used + # before changing it. The Matrix spec recommends 604800000 (a week) + # as the default. + milliseconds: 604800000 + # The maximum number of messages that should be sent with a given a + # session before changing it. The Matrix spec recommends 100 as the + # default. + messages: 100 + # Disable rotating keys when a user's devices change? + # You should not enable this option unless you understand all the implications. + disable_device_change_key_rotation: false + +# Prefix for environment variables. All variables with this prefix must map to valid config fields. +# Nesting in variable names is represented with a dot (.). +# If there are no dots in the name, two underscores (__) are replaced with a dot. +# +# e.g. if the prefix is set to `BRIDGE_`, then `BRIDGE_APPSERVICE__AS_TOKEN` will set appservice.as_token. +# `BRIDGE_appservice.as_token` would work as well, but can't be set in a shell as easily. +# +# The variable names can also have a `_FILE` suffix to tell the bridge to read the value from the +# path set in the variable rather than using the value directly. +# +# If this is null, reading config fields from environment will be disabled. +env_config_prefix: null + +# Logging config. See https://github.com/tulir/zeroconfig for details. +logging: + min_level: debug + writers: + - type: stdout + format: pretty-colored + - type: file + format: json + filename: ./logs/bridge.log + max_size: 100 + max_backups: 10 + compress: false diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bd00a97 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,203 @@ +name: vortex + +x-bridge-common: &bridge-common + restart: unless-stopped + depends_on: + bootstrap: + condition: service_completed_successfully + synapse: + condition: service_healthy + environment: + TZ: ${TZ:-UTC} + +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: vortex + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + POSTGRES_DB: synapse + POSTGRES_INITDB_ARGS: --encoding=UTF8 --locale=C + TZ: ${TZ:-UTC} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U vortex -d synapse"] + interval: 5s + timeout: 5s + retries: 12 + + bootstrap: + build: ./bootstrap + environment: + MATRIX_SERVER_NAME: ${MATRIX_SERVER_NAME:-localhost} + MATRIX_USER: ${MATRIX_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + REGISTRATION_SHARED_SECRET: ${REGISTRATION_SHARED_SECRET:-auto} + DOUBLEPUPPET_SHARED_SECRET: ${DOUBLEPUPPET_SHARED_SECRET:-auto} + BRIDGES_ENABLED: ${BRIDGES_ENABLED:-} + TELEGRAM_API_ID: ${TELEGRAM_API_ID:-} + TELEGRAM_API_HASH: ${TELEGRAM_API_HASH:-} + TZ: ${TZ:-UTC} + volumes: + - data:/data + - ./synapse:/templates/synapse:ro + - ./bridges/templates:/templates/bridges:ro + depends_on: + postgres: + condition: service_healthy + command: /bootstrap.sh + + synapse: + image: matrixdotorg/synapse:v1.156.0 + restart: unless-stopped + environment: + TZ: ${TZ:-UTC} + volumes: + - type: volume + source: data + target: /data + volume: { subpath: synapse } + ports: + - "8008:8008" + depends_on: + bootstrap: + condition: service_completed_successfully + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8008/health')\""] + interval: 5s + timeout: 5s + retries: 24 + start_period: 30s + + provision: + build: ./bootstrap + environment: + MATRIX_SERVER_NAME: ${MATRIX_SERVER_NAME:-localhost} + MATRIX_USER: ${MATRIX_USER:-admin} + MATRIX_PASSWORD: ${MATRIX_PASSWORD:?set MATRIX_PASSWORD in .env} + TZ: ${TZ:-UTC} + volumes: + - data:/data + depends_on: + synapse: + condition: service_healthy + command: /provision.sh + + whatsapp: + <<: *bridge-common + image: dock.mau.dev/mautrix/whatsapp:v0.2606.0 + profiles: [whatsapp] + volumes: + - type: volume + source: data + target: /data + volume: { subpath: bridges/whatsapp } + + gmessages: + <<: *bridge-common + image: dock.mau.dev/mautrix/gmessages:v26.05 + profiles: [gmessages] + volumes: + - type: volume + source: data + target: /data + volume: { subpath: bridges/gmessages } + + telegram: + <<: *bridge-common + image: dock.mau.dev/mautrix/telegram:v26.06 + profiles: [telegram] + entrypoint: ["/bin/sh", "-c", "if [ ! -f /data/config.yaml ]; then echo 'telegram bridge skipped: TELEGRAM_API_ID / TELEGRAM_API_HASH not set in .env'; exec sleep infinity; fi; exec /docker-run.sh"] + volumes: + - type: volume + source: data + target: /data + volume: { subpath: bridges/telegram } + + twitter: + <<: *bridge-common + image: dock.mau.dev/mautrix/twitter:v26.06 + profiles: [twitter] + volumes: + - type: volume + source: data + target: /data + volume: { subpath: bridges/twitter } + + linkedin: + <<: *bridge-common + image: dock.mau.dev/mautrix/linkedin:v26.04 + profiles: [linkedin] + volumes: + - type: volume + source: data + target: /data + volume: { subpath: bridges/linkedin } + + discord: + <<: *bridge-common + image: dock.mau.dev/mautrix/discord:v0.7.6 + profiles: [discord] + volumes: + - type: volume + source: data + target: /data + volume: { subpath: bridges/discord } + + meta: + <<: *bridge-common + image: dock.mau.dev/mautrix/meta:v26.06 + profiles: [meta] + volumes: + - type: volume + source: data + target: /data + volume: { subpath: bridges/meta } + + mcp: + build: ./mcp + restart: unless-stopped + environment: + MCP_HTTP_PORT: ${MCP_HTTP_PORT:-8765} + MCP_ALLOW_SEND: ${MCP_ALLOW_SEND:-false} + CREDENTIALS_FILE: /shared/credentials.json + DRAFTS_DB: /shared/drafts/drafts.db + STORE_DIR: /shared/mcp-store + TZ: ${TZ:-UTC} + ports: + - "${MCP_HTTP_PORT:-8765}:${MCP_HTTP_PORT:-8765}" + volumes: + - type: volume + source: data + target: /shared + volume: { subpath: shared } + depends_on: + provision: + condition: service_completed_successfully + + tui: + build: ./tui + profiles: [tui] + stdin_open: true + tty: true + environment: + CREDENTIALS_FILE: /shared/credentials.json + DRAFTS_DB: /shared/drafts/drafts.db + STORE_DIR: /shared/tui-store + TZ: ${TZ:-UTC} + volumes: + - type: volume + source: data + target: /shared + volume: { subpath: shared } + depends_on: + synapse: + condition: service_healthy + +volumes: + pgdata: + data: diff --git a/mcp/Dockerfile b/mcp/Dockerfile new file mode 100644 index 0000000..7860d93 --- /dev/null +++ b/mcp/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.12-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential libolm-dev libolm3 curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY pyproject.toml ./ +COPY server ./server +RUN pip install --no-cache-dir . + +ENV MCP_HTTP_PORT=8765 + +# Any HTTP response (even 4xx/406) means the server is up; curl exits 0 on connect. +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -s -o /dev/null "http://localhost:${MCP_HTTP_PORT:-8765}/mcp" || exit 1 + +CMD ["vortex-mcp"] diff --git a/mcp/pyproject.toml b/mcp/pyproject.toml new file mode 100644 index 0000000..540928a --- /dev/null +++ b/mcp/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "vortex-mcp" +version = "0.1.0" +description = "MCP server for the Vortex Matrix messaging stack" +requires-python = ">=3.11" +dependencies = [ + "fastmcp>=2", + "matrix-nio[e2e]>=0.25", +] + +[project.scripts] +vortex-mcp = "vortex_mcp.server:main" + +[tool.setuptools] +package-dir = { "" = "server" } + +[tool.setuptools.packages.find] +where = ["server"] diff --git a/mcp/server/vortex_mcp/__init__.py b/mcp/server/vortex_mcp/__init__.py new file mode 100644 index 0000000..39c08a3 --- /dev/null +++ b/mcp/server/vortex_mcp/__init__.py @@ -0,0 +1,3 @@ +"""Vortex MCP server package.""" + +__version__ = "0.1.0" diff --git a/mcp/server/vortex_mcp/server.py b/mcp/server/vortex_mcp/server.py new file mode 100644 index 0000000..d7af56f --- /dev/null +++ b/mcp/server/vortex_mcp/server.py @@ -0,0 +1,319 @@ +"""Vortex MCP server: read-first Matrix access with a human-gated draft outbox.""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import logging +import os +import sqlite3 +import uuid +from collections import deque +from datetime import datetime, timezone +from pathlib import Path + +from fastmcp import FastMCP +from fastmcp.exceptions import ToolError +from nio import ( + AsyncClient, + AsyncClientConfig, + LoginResponse, + MessageDirection, + RoomMessagesResponse, + RoomMessageText, +) +from nio.crypto import ENCRYPTION_ENABLED + +log = logging.getLogger("vortex-mcp") + +CREDENTIALS_FILE = os.environ.get("CREDENTIALS_FILE", "/shared/credentials.json") +STORE_DIR = Path(os.environ.get("STORE_DIR", "/shared/mcp-store")) +DRAFTS_DB = os.environ.get("DRAFTS_DB", "/shared/drafts/drafts.db") +NETWORKS = ("gmessages", "telegram", "whatsapp", "twitter", "linkedin", "discord", "meta") + +mcp = FastMCP("vortex") + +_client: AsyncClient | None = None +_sync_task: asyncio.Task | None = None +_init_lock = asyncio.Lock() +# room_id -> deque of message dicts, filled by the background sync loop +_cache: dict[str, deque] = {} + + +# ---------------------------------------------------------------- drafts db + +def _db() -> sqlite3.Connection: + Path(DRAFTS_DB).parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(DRAFTS_DB) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute( + """CREATE TABLE IF NOT EXISTS drafts( + draft_id TEXT PRIMARY KEY, room_id TEXT NOT NULL, body TEXT NOT NULL, + reply_to_event_id TEXT, created_by TEXT NOT NULL, created_at TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'draft', sent_event_id TEXT)""" + ) + return conn + + +def _get_draft(conn: sqlite3.Connection, draft_id: str) -> dict: + row = conn.execute("SELECT * FROM drafts WHERE draft_id=?", (draft_id,)).fetchone() + if row is None: + raise ToolError(f"Draft not found: {draft_id}") + return dict(row) + + +# ------------------------------------------------------------- matrix client + +async def _load_credentials() -> dict: + for _ in range(150): # bootstrap provisions the file; wait up to ~5 min + if os.path.exists(CREDENTIALS_FILE): + with open(CREDENTIALS_FILE) as f: + return json.load(f) + log.info("Waiting for credentials file %s ...", CREDENTIALS_FILE) + await asyncio.sleep(2) + raise ToolError(f"Credentials file never appeared: {CREDENTIALS_FILE}") + + +async def _login(creds: dict) -> AsyncClient: + homeserver = os.environ.get("MATRIX_HOMESERVER") or creds["homeserver"] + STORE_DIR.mkdir(parents=True, exist_ok=True) + session_file = STORE_DIR / "session.json" + config = AsyncClientConfig( + encryption_enabled=ENCRYPTION_ENABLED, store_sync_tokens=True + ) + client = AsyncClient( + homeserver, creds["user_id"], store_path=str(STORE_DIR), config=config + ) + + if session_file.exists(): + sess = json.loads(session_file.read_text()) + client.restore_login(sess["user_id"], sess["device_id"], sess["access_token"]) + log.info("Restored session for %s (device %s)", sess["user_id"], sess["device_id"]) + return client + + resp = await client.login(creds.get("password") or "", device_name="vortex-mcp") + if isinstance(resp, LoginResponse): + session_file.write_text(json.dumps({ + "user_id": resp.user_id, + "device_id": resp.device_id, + "access_token": resp.access_token, + })) + log.info("Password login ok, session persisted") + return client + + # ponytail: fall back to the shared bootstrap token if password login fails + log.warning("Password login failed (%s); using shared access_token", resp) + client.restore_login(creds["user_id"], creds.get("device_id") or "", creds["access_token"]) + return client + + +async def _on_message(room, event) -> None: + dq = _cache.setdefault(room.room_id, deque(maxlen=1000)) + dq.append({ + "event_id": event.event_id, + "sender": event.sender, + "sender_display": room.user_name(event.sender) or event.sender, + "body": event.body, + "timestamp_ms": event.server_timestamp, + "room_id": room.room_id, + }) + + +async def _get_client() -> AsyncClient: + """Lazy-init the nio client and background sync task on first tool call.""" + global _client, _sync_task + async with _init_lock: + if _client is None: + creds = await _load_credentials() + _client = await _login(creds) + _client.add_event_callback(_on_message, RoomMessageText) + if _sync_task is None or _sync_task.done(): + _sync_task = asyncio.create_task( + _client.sync_forever(timeout=30000, full_state=True) + ) + return _client + + +def _room_network(client: AsyncClient, room) -> str: + domain = client.user_id.split(":", 1)[1] if ":" in (client.user_id or "") else "" + for uid in room.users: + for net in NETWORKS: + if uid.startswith(f"@{net}_") or uid == f"@{net}bot:{domain}": + return net + return "matrix" + + +# -------------------------------------------------------------------- tools + +@mcp.tool +async def list_rooms(network_filter: str | None = None) -> list[dict]: + """List joined rooms. Optionally filter by bridge network + (gmessages, telegram, whatsapp, twitter, linkedin, discord, meta, matrix).""" + client = await _get_client() + out = [] + for room_id, room in client.rooms.items(): + network = _room_network(client, room) + if network_filter and network != network_filter: + continue + out.append({ + "room_id": room_id, + "name": room.display_name, + "network": network, + "unread_count": getattr(room, "unread_notifications", 0) or 0, + "member_count": room.member_count, + }) + return out + + +@mcp.tool +async def read_messages(room_id: str, limit: int = 50, before: str | None = None) -> dict: + """Read recent messages from a room, newest first. Pass the returned `end` + token as `before` to paginate further back.""" + client = await _get_client() + resp = await client.room_messages( + room_id, + start=before or client.next_batch, + direction=MessageDirection.back, + limit=limit, + ) + if not isinstance(resp, RoomMessagesResponse): + raise ToolError(f"room_messages failed: {resp}") + room = client.rooms.get(room_id) + messages = [ + { + "event_id": ev.event_id, + "sender": ev.sender, + "sender_display": room.user_name(ev.sender) if room else ev.sender, + "body": ev.body, + "timestamp_ms": ev.server_timestamp, + "room_id": room_id, + } + for ev in resp.chunk + if isinstance(ev, RoomMessageText) + ] + return {"messages": messages, "end": resp.end} + + +@mcp.tool +async def search_messages(query: str, room_id: str | None = None) -> list[dict]: + """Case-insensitive substring search over messages seen since this server + started syncing. Optionally restrict to one room.""" + await _get_client() + needle = query.lower() + rooms = [room_id] if room_id else list(_cache) + return [ + msg + for rid in rooms + for msg in _cache.get(rid, ()) + if needle in msg["body"].lower() + ] + + +@mcp.tool +def create_draft(room_id: str, body: str, reply_to_event_id: str | None = None) -> dict: + """Create a message draft for a room. Never sends anything; a human reviews + drafts and sends them from the TUI.""" + draft = { + "draft_id": uuid.uuid4().hex, + "room_id": room_id, + "body": body, + "reply_to_event_id": reply_to_event_id, + "created_by": "mcp", + "created_at": datetime.now(timezone.utc).isoformat(), + "status": "draft", + "sent_event_id": None, + } + with _db() as conn: + conn.execute( + "INSERT INTO drafts VALUES(:draft_id,:room_id,:body,:reply_to_event_id," + ":created_by,:created_at,:status,:sent_event_id)", + draft, + ) + return draft + + +@mcp.tool +def list_drafts() -> list[dict]: + """List all pending (unsent, undiscarded) drafts.""" + with _db() as conn: + rows = conn.execute("SELECT * FROM drafts WHERE status='draft'").fetchall() + return [dict(r) for r in rows] + + +@mcp.tool +def update_draft(draft_id: str, body: str) -> dict: + """Replace the body of a pending draft.""" + with _db() as conn: + draft = _get_draft(conn, draft_id) + if draft["status"] != "draft": + raise ToolError(f"Draft {draft_id} is {draft['status']}, not editable") + conn.execute("UPDATE drafts SET body=? WHERE draft_id=?", (body, draft_id)) + draft["body"] = body + return draft + + +@mcp.tool +def discard_draft(draft_id: str) -> dict: + """Mark a draft as discarded.""" + with _db() as conn: + draft = _get_draft(conn, draft_id) + conn.execute("UPDATE drafts SET status='discarded' WHERE draft_id=?", (draft_id,)) + draft["status"] = "discarded" + return draft + + +@mcp.tool +async def send_draft(draft_id: str) -> dict: + """Send a pending draft to its room. Only works when MCP_ALLOW_SEND=true; + otherwise a human must send it from the TUI.""" + if os.environ.get("MCP_ALLOW_SEND", "false").lower() != "true": + raise ToolError( + "Sending disabled: a human must send this draft from the TUI (MCP_ALLOW_SEND=false)" + ) + with _db() as conn: + draft = _get_draft(conn, draft_id) + if draft["status"] != "draft": + raise ToolError(f"Draft {draft_id} is {draft['status']}, cannot send") + + client = await _get_client() + content: dict = {"msgtype": "m.text", "body": draft["body"]} + if draft["reply_to_event_id"]: + content["m.relates_to"] = { + "m.in_reply_to": {"event_id": draft["reply_to_event_id"]} + } + resp = await client.room_send( + draft["room_id"], "m.room.message", content, ignore_unverified_devices=True + ) + event_id = getattr(resp, "event_id", None) + if not event_id: + raise ToolError(f"Send failed: {resp}") + with _db() as conn: + conn.execute( + "UPDATE drafts SET status='sent', sent_event_id=? WHERE draft_id=?", + (event_id, draft_id), + ) + draft.update(status="sent", sent_event_id=event_id) + return draft + + +# --------------------------------------------------------------------- main + +def main() -> None: + logging.basicConfig(level=logging.INFO) + parser = argparse.ArgumentParser(description="Vortex MCP server") + parser.add_argument("--stdio", action="store_true", help="use stdio transport") + args = parser.parse_args() + if args.stdio: + mcp.run() + else: + mcp.run( + transport="http", + host="0.0.0.0", + port=int(os.environ.get("MCP_HTTP_PORT", "8765")), + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/verify.sh b/scripts/verify.sh new file mode 100644 index 0000000..e466c71 --- /dev/null +++ b/scripts/verify.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# Smoke tests: stack health, appservice round-trip, MCP tools + draft gating. +set -euo pipefail +cd "$(dirname "$0")/.." +set -a; source .env; set +a + +HS=http://localhost:8008 +pass() { echo "PASS: $*"; } +fail() { echo "FAIL: $*"; exit 1; } + +echo "== 1/4 containers" +docker compose ps --format '{{.Service}} {{.State}} {{.Health}}' | tee /tmp/vortex-ps.txt +for svc in postgres synapse mcp; do + grep -q "^$svc running" /tmp/vortex-ps.txt || fail "$svc not running" +done +IFS=',' read -ra BRIDGES <<< "${BRIDGES_ENABLED:-}" +for b in "${BRIDGES[@]}"; do + grep -q "^$b running" /tmp/vortex-ps.txt || fail "bridge $b not running" +done +for oneshot in bootstrap provision; do + st=$(docker compose ps -a --format '{{.Service}} {{.ExitCode}}' $oneshot | awk '{print $2}') + [ "$st" = 0 ] || fail "$oneshot exit code $st" +done +pass "all services up, one-shots exited 0" + +echo "== 2/4 synapse health + login" +curl -sf $HS/health >/dev/null || fail "synapse /health" +TOKEN=$(curl -sf -X POST $HS/_matrix/client/v3/login -d "{ + \"type\":\"m.login.password\", + \"identifier\":{\"type\":\"m.id.user\",\"user\":\"$MATRIX_USER\"}, + \"password\":\"$MATRIX_PASSWORD\",\"initial_device_display_name\":\"verify\"}" | + python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])') +[ -n "$TOKEN" ] || fail "login" +pass "synapse healthy, admin login works" + +echo "== 3/4 bridge bot round-trip" +FIRST_BRIDGE=$(echo "${BRIDGES_ENABLED}" | cut -d, -f1) +BOT="@${FIRST_BRIDGE}bot:${MATRIX_SERVER_NAME}" +python3 - "$HS" "$TOKEN" "$BOT" <<'EOF' || fail "bridge bot did not respond" +import json, sys, time, urllib.request + +hs, token, bot = sys.argv[1:4] +def api(method, path, body=None): + req = urllib.request.Request(hs + path, method=method, + data=json.dumps(body).encode() if body is not None else None, + headers={"Authorization": "Bearer " + token}) + return json.load(urllib.request.urlopen(req)) + +room = api("POST", "/_matrix/client/v3/createRoom", + {"invite": [bot], "is_direct": True, "preset": "trusted_private_chat"})["room_id"] +api("PUT", f"/_matrix/client/v3/rooms/{room}/send/m.room.message/{time.time_ns()}", + {"msgtype": "m.text", "body": "help"}) +deadline = time.time() + 30 +since = None +while time.time() < deadline: + q = f"/_matrix/client/v3/sync?timeout=5000" + (f"&since={since}" if since else "") + resp = api("GET", q) + since = resp["next_batch"] + events = resp.get("rooms", {}).get("join", {}).get(room, {}).get("timeline", {}).get("events", []) + for e in events: + if e["sender"] == bot and e["type"] == "m.room.message": + print("bot replied:", e["content"]["body"][:80].replace("\n", " ")) + sys.exit(0) +sys.exit(1) +EOF +pass "appservice round-trip via $BOT" + +echo "== 4/4 MCP tools + draft gating" +docker compose exec -T mcp python - <<'EOF' || fail "mcp checks" +import asyncio, json, os +from fastmcp import Client + +async def main(): + port = os.environ.get("MCP_HTTP_PORT", "8765") + async with Client(f"http://localhost:{port}/mcp") as c: + tools = {t.name for t in await c.list_tools()} + assert {"list_rooms", "read_messages", "create_draft", "send_draft"} <= tools, tools + rooms = (await c.call_tool("list_rooms")).data + assert isinstance(rooms, list), rooms + print(f"list_rooms: {len(rooms)} rooms") + room_id = rooms[0]["room_id"] if rooms else "!dummy:localhost" + d = (await c.call_tool("create_draft", + {"room_id": room_id, "body": "verify draft"})).data + did = d["draft_id"] + drafts = (await c.call_tool("list_drafts")).data + assert any(x["draft_id"] == did for x in drafts) + try: + await c.call_tool("send_draft", {"draft_id": did}) + raise SystemExit("send_draft should have been blocked") + except Exception as e: + assert "disabled" in str(e).lower() or "human" in str(e).lower(), e + print("send_draft correctly blocked:", str(e)[:80]) + await c.call_tool("discard_draft", {"draft_id": did}) + print("draft lifecycle OK") + +asyncio.run(main()) +EOF +pass "mcp list_rooms + draft lifecycle + send gating" + +echo +echo "ALL CHECKS PASSED" diff --git a/shared/drafts/.gitkeep b/shared/drafts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/synapse/homeserver.template.yaml b/synapse/homeserver.template.yaml new file mode 100644 index 0000000..ed12896 --- /dev/null +++ b/synapse/homeserver.template.yaml @@ -0,0 +1,60 @@ +server_name: "${MATRIX_SERVER_NAME}" +pid_file: /data/homeserver.pid +report_stats: false + +listeners: + - port: 8008 + tls: false + type: http + x_forwarded: true + bind_addresses: ['0.0.0.0'] + resources: + - names: [client, federation] + compress: false + +database: + name: psycopg2 + args: + user: vortex + password: "${POSTGRES_PASSWORD}" + database: synapse + host: postgres + cp_min: 5 + cp_max: 10 + +log_config: /data/log.config +media_store_path: /data/media_store +signing_key_path: /data/signing.key + +registration_shared_secret: "${REGISTRATION_SHARED_SECRET}" +enable_registration: false + +macaroon_secret_key: "${MACAROON_SECRET_KEY}" +form_secret: "${FORM_SECRET}" + +trusted_key_servers: [] +suppress_key_server_warning: true +serve_server_wellknown: false + +# Relaxed rate limits for local single-user use. +rc_message: + per_second: 1000 + burst_count: 1000 +rc_login: + address: { per_second: 1000, burst_count: 1000 } + account: { per_second: 1000, burst_count: 1000 } + failed_attempts: { per_second: 1000, burst_count: 1000 } +rc_registration: + per_second: 1000 + burst_count: 1000 +rc_joins: + local: { per_second: 1000, burst_count: 1000 } + remote: { per_second: 1000, burst_count: 1000 } +rc_invites: + per_room: { per_second: 1000, burst_count: 1000 } + per_user: { per_second: 1000, burst_count: 1000 } +rc_admin_redaction: + per_second: 1000 + burst_count: 1000 + +# app_service_config_files is appended by bootstrap.sh diff --git a/tui/Dockerfile b/tui/Dockerfile new file mode 100644 index 0000000..3e9f7b7 --- /dev/null +++ b/tui/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.12-slim + +# libolm + toolchain: matrix-nio[e2e] builds python-olm against libolm +RUN apt-get update \ + && apt-get install -y --no-install-recommends libolm-dev libolm3 build-essential \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/vortex-tui +COPY pyproject.toml ./ +COPY app ./app +RUN pip install --no-cache-dir . + +ENV TERM=xterm-256color \ + CREDENTIALS_FILE=/shared/credentials.json \ + STORE_DIR=/shared/tui-store \ + DRAFTS_DB=/shared/drafts/drafts.db + +# interactive TUI: run with `docker compose run --rm tui` (TTY) +CMD ["vortex-tui"] diff --git a/tui/app/vortex_tui/__init__.py b/tui/app/vortex_tui/__init__.py new file mode 100644 index 0000000..7795693 --- /dev/null +++ b/tui/app/vortex_tui/__init__.py @@ -0,0 +1,3 @@ +"""Vortex TUI - unified Matrix messaging client.""" + +__version__ = "0.1.0" diff --git a/tui/app/vortex_tui/drafts.py b/tui/app/vortex_tui/drafts.py new file mode 100644 index 0000000..f4a5b6c --- /dev/null +++ b/tui/app/vortex_tui/drafts.py @@ -0,0 +1,53 @@ +"""SQLite drafts store, shared with other vortex components via DRAFTS_DB.""" + +import os +import sqlite3 +from pathlib import Path + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS drafts( + draft_id TEXT PRIMARY KEY, + room_id TEXT NOT NULL, + body TEXT NOT NULL, + reply_to_event_id TEXT, + created_by TEXT NOT NULL, + created_at TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'draft', + sent_event_id TEXT +); +""" + + +def connect(path: str | None = None) -> sqlite3.Connection: + db = Path(path or os.environ.get("DRAFTS_DB", "/shared/drafts/drafts.db")) + db.parent.mkdir(parents=True, exist_ok=True) + con = sqlite3.connect(db) + con.row_factory = sqlite3.Row + con.execute("PRAGMA journal_mode=WAL") + con.execute(SCHEMA) + con.commit() + return con + + +def pending(con: sqlite3.Connection) -> list[sqlite3.Row]: + return con.execute( + "SELECT * FROM drafts WHERE status='draft' ORDER BY created_at" + ).fetchall() + + +def update_body(con: sqlite3.Connection, draft_id: str, body: str) -> None: + con.execute("UPDATE drafts SET body=? WHERE draft_id=?", (body, draft_id)) + con.commit() + + +def mark( + con: sqlite3.Connection, + draft_id: str, + status: str, + sent_event_id: str | None = None, +) -> None: + con.execute( + "UPDATE drafts SET status=?, sent_event_id=? WHERE draft_id=?", + (status, sent_event_id, draft_id), + ) + con.commit() diff --git a/tui/app/vortex_tui/main.py b/tui/app/vortex_tui/main.py new file mode 100644 index 0000000..bb5cb26 --- /dev/null +++ b/tui/app/vortex_tui/main.py @@ -0,0 +1,369 @@ +"""Vortex TUI application: room list, timeline, bridges, drafts.""" + +from __future__ import annotations + +from nio import ( + AsyncClient, + MatrixRoom, + RoomCreateResponse, + RoomMessageMedia, + RoomMessageNotice, + RoomMessageText, + SyncResponse, +) +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Footer, Input, Label, ListItem, ListView, RichLog, Static + +from . import drafts as drafts_db +from .qr import looks_like_qr_payload, render_qr +from .session import build_client, load_credentials + +NETWORKS = ["gmessages", "telegram", "whatsapp", "twitter", "linkedin", "discord", "meta"] +LABELS = { + "gmessages": "gm", + "telegram": "tg", + "whatsapp": "wa", + "twitter": "tw", + "linkedin": "li", + "discord": "dc", + "meta": "ig", + "matrix": "mx", +} + + +class BridgeScreen(Screen[str | None]): + """List the known bridge bots; Enter opens or creates the bot DM.""" + + BINDINGS = [Binding("escape", "back", "Back")] + + def compose(self) -> ComposeResult: + yield Static( + "Bridges - Enter opens (or creates) the bot DM, then send it a login " + "command there (e.g. '!wa help', 'login', 'help').", + id="bridge-help", + ) + yield ListView(id="bridge-list") + yield Footer() + + def on_mount(self) -> None: + app: VortexApp = self.app + lv = self.query_one("#bridge-list", ListView) + for net in NETWORKS: + bot = f"@{net}bot:{app.domain}" + dm = app.find_bot_dm(bot) + status = "DM open" if dm else "no DM (Enter creates one)" + item = ListItem(Label(f"[{LABELS[net]}] {net:<10} {bot:<32} {status}")) + item.bot, item.dm = bot, dm + lv.append(item) + lv.focus() + + async def on_list_view_selected(self, event: ListView.Selected) -> None: + event.stop() + item = event.item + if item.dm: + self.dismiss(item.dm) + return + resp = await self.app.client.room_create(invite=[item.bot], is_direct=True) + if isinstance(resp, RoomCreateResponse): + self.dismiss(resp.room_id) + else: + self.notify(f"room_create failed: {resp}", severity="error") + + def action_back(self) -> None: + self.dismiss(None) + + +class DraftsScreen(Screen): + """Pending drafts from the shared SQLite DB, polled every 2s.""" + + BINDINGS = [ + Binding("escape", "back", "Back"), + Binding("e", "edit", "Edit"), + Binding("s", "send", "Send"), + Binding("x", "discard", "Discard"), + ] + + def compose(self) -> ComposeResult: + yield Static("Drafts - e edit, s send, x discard, esc back", id="drafts-help") + yield ListView(id="draft-list") + yield Input(placeholder="Edit draft body, Enter saves", id="draft-edit") + yield Footer() + + def on_mount(self) -> None: + self.con = drafts_db.connect() + self._editing: str | None = None + self.query_one("#draft-edit", Input).display = False + self.call_later(self._refresh) + # ponytail: 2s poll instead of watchfiles; watch the db file if this chafes + self.set_interval(2, self._refresh) + self.query_one("#draft-list", ListView).focus() + + async def _refresh(self) -> None: + app: VortexApp = self.app + lv = self.query_one("#draft-list", ListView) + idx = lv.index + items = [] + for row in drafts_db.pending(self.con): + room = app.client.rooms.get(row["room_id"]) if app.client else None + name = (room.display_name if room else row["room_id"])[:24] + body = row["body"].replace("\n", " ")[:48] + item = ListItem( + Label(f"{name:<24} | {body:<48} | {row['created_by']} @ {row['created_at']}") + ) + item.draft = dict(row) + items.append(item) + await lv.clear() + await lv.extend(items) + if idx is not None and items: + lv.index = min(idx, len(items) - 1) + + def _current(self) -> dict | None: + item = self.query_one("#draft-list", ListView).highlighted_child + return getattr(item, "draft", None) + + def action_edit(self) -> None: + draft = self._current() + if not draft: + return + self._editing = draft["draft_id"] + box = self.query_one("#draft-edit", Input) + box.value = draft["body"] + box.display = True + box.focus() + + def on_input_submitted(self, event: Input.Submitted) -> None: + event.stop() + if event.input.id != "draft-edit" or not self._editing: + return + drafts_db.update_body(self.con, self._editing, event.value) + self._editing = None + event.input.display = False + self.query_one("#draft-list", ListView).focus() + self.call_later(self._refresh) + + async def action_send(self) -> None: + draft = self._current() + app: VortexApp = self.app + if not draft or not app.client: + return + resp = await app.client.room_send( + draft["room_id"], + "m.room.message", + {"msgtype": "m.text", "body": draft["body"]}, + ignore_unverified_devices=True, + ) + drafts_db.mark(self.con, draft["draft_id"], "sent", getattr(resp, "event_id", None)) + await self._refresh() + + def action_discard(self) -> None: + draft = self._current() + if draft: + drafts_db.mark(self.con, draft["draft_id"], "discarded") + self.call_later(self._refresh) + + def action_back(self) -> None: + self.con.close() + self.dismiss() + + +class VortexApp(App): + """Three panes: room list | timeline over input, plus footer bindings.""" + + CSS = """ + #rooms { width: 36; border-right: solid $accent; } + #timeline { height: 1fr; } + #bridge-help, #drafts-help { padding: 0 1; color: $text-muted; } + """ + TITLE = "Vortex" + SUB_TITLE = "/reply replies to the last message in the room" + BINDINGS = [ + Binding("q", "quit", "Quit"), + Binding("b", "bridges", "Bridges"), + Binding("d", "drafts", "Drafts"), + Binding("tab", "focus_next", "Cycle focus"), + ] + + def __init__(self, creds: dict): + super().__init__() + self.creds = creds + self.client: AsyncClient | None = None + self.domain = creds["user_id"].split(":", 1)[1] + self.selected_room: str | None = None + self.pending_select: str | None = None + self.timelines: dict[str, list[str]] = {} + self.last_event: dict[str, str] = {} + self.unread: dict[str, int] = {} + self.first_sync = False + + def compose(self) -> ComposeResult: + with Horizontal(): + yield ListView(id="rooms") + with Vertical(): + yield RichLog(id="timeline", wrap=True, markup=False, highlight=False) + yield Input( + placeholder="Message (Enter sends, /reply replies)", id="msg" + ) + yield Footer() + + def on_mount(self) -> None: + self.run_worker(self._connect_and_sync(), exclusive=True) + + # ---- matrix ----------------------------------------------------------- + async def _connect_and_sync(self) -> None: + log = self.query_one("#timeline", RichLog) + log.write("Connecting...") + try: + self.client = await build_client(self.creds) + except Exception as exc: # surface login problems in the UI + log.write(f"Login failed: {exc}") + return + self.client.add_event_callback( + self._on_event, (RoomMessageText, RoomMessageNotice, RoomMessageMedia) + ) + self.client.add_response_callback(self._on_sync, SyncResponse) + log.write( + f"Logged in as {self.client.user_id} ({self.client.device_id}). Syncing..." + ) + await self.client.sync_forever(timeout=30000, full_state=True) + + async def _on_sync(self, _response) -> None: + self.first_sync = True + await self._refresh_rooms() + if self.pending_select and self.pending_select in self.client.rooms: + rid, self.pending_select = self.pending_select, None + self._select_room(rid) + + async def _on_event(self, room: MatrixRoom, event) -> None: + line = self._format_event(room, event) + self.timelines.setdefault(room.room_id, []).append(line) + self.last_event[room.room_id] = event.event_id + if room.room_id == self.selected_room: + self.query_one("#timeline", RichLog).write(line) + elif self.first_sync: + self.unread[room.room_id] = self.unread.get(room.room_id, 0) + 1 + await self._refresh_rooms() + + def _format_event(self, room: MatrixRoom, event) -> str: + sender = room.user_name(event.sender) or event.sender + if isinstance(event, RoomMessageMedia): + path = (event.url or "")[len("mxc://"):] + url = f"{self.client.homeserver}/_matrix/media/v3/download/{path}" + return f"<{sender}> [file] {event.body}: {url}" + body = event.body or "" + is_bot = any(event.sender == f"@{n}bot:{self.domain}" for n in NETWORKS) + if looks_like_qr_payload(body, is_bot): + return f"<{sender}> QR for {body}\n{render_qr(body)}" + return f"<{sender}> {body}" + + # ---- rooms ------------------------------------------------------------ + def room_network(self, room: MatrixRoom) -> str: + for uid in room.users: + for net in NETWORKS: + if uid.startswith(f"@{net}_") or uid == f"@{net}bot:{self.domain}": + return net + return "matrix" + + def find_bot_dm(self, bot: str) -> str | None: + # ponytail: "DM" = bot is a member and room has <=2 members + for room in self.client.rooms.values(): + if bot in room.users and len(room.users) <= 2: + return room.room_id + return None + + def _room_label(self, room: MatrixRoom) -> str: + n = self.unread.get(room.room_id, 0) + badge = f" ({n})" if n else "" + return f"[{LABELS[self.room_network(room)]}] {room.display_name or room.room_id}{badge}" + + async def _refresh_rooms(self) -> None: + lv = self.query_one("#rooms", ListView) + rooms = sorted( + self.client.rooms.values(), + key=lambda r: (self.room_network(r), (r.display_name or r.room_id).lower()), + ) + items = [] + for room in rooms: + item = ListItem(Label(self._room_label(room))) + item.room_id = room.room_id + items.append(item) + idx = next( + (i for i, r in enumerate(rooms) if r.room_id == self.selected_room), None + ) + await lv.clear() + await lv.extend(items) + if idx is not None: + lv.index = idx + + def on_list_view_selected(self, event: ListView.Selected) -> None: + rid = getattr(event.item, "room_id", None) + if rid: + self._select_room(rid) + + def _select_room(self, rid: str) -> None: + self.selected_room = rid + self.unread.pop(rid, None) + log = self.query_one("#timeline", RichLog) + log.clear() + for line in self.timelines.get(rid, []): + log.write(line) + room = self.client.rooms.get(rid) if self.client else None + if room: # update badge in place, no full rebuild + for item in self.query_one("#rooms", ListView).children: + if getattr(item, "room_id", None) == rid: + item.query_one(Label).update(self._room_label(room)) + self.query_one("#msg", Input).focus() + + # ---- sending ---------------------------------------------------------- + async def on_input_submitted(self, event: Input.Submitted) -> None: + if event.input.id != "msg": + return + text = event.value.strip() + event.input.value = "" + if not text or not self.client or not self.selected_room: + return + content: dict = {"msgtype": "m.text", "body": text} + if text.startswith("/reply "): + content["body"] = text[len("/reply "):] + last = self.last_event.get(self.selected_room) + if last: + content["m.relates_to"] = {"m.in_reply_to": {"event_id": last}} + await self.client.room_send( + self.selected_room, + "m.room.message", + content, + ignore_unverified_devices=True, + ) + + # ---- actions ---------------------------------------------------------- + def action_bridges(self) -> None: + if self.client: + self.push_screen(BridgeScreen(), self._after_bridge) + + def _after_bridge(self, room_id: str | None) -> None: + if not room_id: + return + if self.client and room_id in self.client.rooms: + self._select_room(room_id) + else: + self.pending_select = room_id + self.notify("DM created - it will open once it syncs.") + + def action_drafts(self) -> None: + self.push_screen(DraftsScreen()) + + async def action_quit(self) -> None: + if self.client: + await self.client.close() + self.exit() + + +def run() -> None: + creds = load_credentials() + VortexApp(creds).run() + + +if __name__ == "__main__": + run() diff --git a/tui/app/vortex_tui/qr.py b/tui/app/vortex_tui/qr.py new file mode 100644 index 0000000..9fbc15e --- /dev/null +++ b/tui/app/vortex_tui/qr.py @@ -0,0 +1,35 @@ +"""Render QR-ish message payloads as unicode half-block QR codes.""" + +import qrcode + +QR_SCHEMES = ("https://wa.me/", "gmessages://") + + +def looks_like_qr_payload(body: str, sender_is_bridge_bot: bool) -> bool: + """Deliberately simple heuristic: + + - message starts with a known pairing scheme (whatsapp / gmessages), or + - a bridge-bot message that is a single URL/token: 20-199 chars, no + whitespace, and either contains '://' or is alnum-ish (pairing code). + """ + body = body.strip() + if body.startswith(QR_SCHEMES): + return True + if sender_is_bridge_bot and 20 <= len(body) < 200 and not any(c.isspace() for c in body): + return "://" in body or body.replace("-", "").replace("_", "").isalnum() + return False + + +def render_qr(text: str) -> str: + """QR code as text, two matrix rows per output line via half blocks.""" + qr = qrcode.QRCode(border=1) + qr.add_data(text) + qr.make(fit=True) + matrix = qr.get_matrix() + if len(matrix) % 2: + matrix.append([False] * len(matrix[0])) + chars = {(True, True): "█", (True, False): "▀", (False, True): "▄", (False, False): " "} + return "\n".join( + "".join(chars[(top[x], bottom[x])] for x in range(len(top))) + for top, bottom in zip(matrix[::2], matrix[1::2]) + ) diff --git a/tui/app/vortex_tui/session.py b/tui/app/vortex_tui/session.py new file mode 100644 index 0000000..799c2c5 --- /dev/null +++ b/tui/app/vortex_tui/session.py @@ -0,0 +1,82 @@ +"""Credentials loading and Matrix client construction.""" + +import json +import os +import sys +import time +from pathlib import Path + +from nio import AsyncClient, AsyncClientConfig, LoginResponse + + +def store_dir() -> Path: + return Path(os.environ.get("STORE_DIR", "/shared/tui-store")) + + +def load_credentials(retries: int = 10, delay: float = 3.0) -> dict: + """Read CREDENTIALS_FILE, waiting a bit in case bootstrap is still provisioning.""" + path = Path(os.environ.get("CREDENTIALS_FILE", "/shared/credentials.json")) + for attempt in range(retries): + try: + return json.loads(path.read_text()) + except (OSError, json.JSONDecodeError): + if attempt < retries - 1: + print( + f"vortex-tui: waiting for {path} (bootstrap still provisioning?)...", + file=sys.stderr, + ) + time.sleep(delay) + sys.exit( + f"vortex-tui: credentials file {path} missing or invalid after " + f"{retries} attempts. Run the bootstrap first, or set CREDENTIALS_FILE." + ) + + +async def build_client(creds: dict) -> AsyncClient: + """Return a logged-in AsyncClient with its own persisted E2EE device. + + Order: STORE_DIR/session.json (our own device) -> password login (persist + new device) -> bootstrap access_token fallback. + """ + store = store_dir() + store.mkdir(parents=True, exist_ok=True) + homeserver = ( + os.environ.get("MATRIX_HOMESERVER") + or creds.get("homeserver") + or "http://localhost:8008" + ) + user_id = creds["user_id"] + # encryption_enabled defaults to True only when olm is importable, + # so this works with or without the [e2e] extra installed. + client = AsyncClient( + homeserver, + user_id, + store_path=str(store), + config=AsyncClientConfig(store_sync_tokens=True), + ) + session_file = store / "session.json" + if session_file.exists(): + s = json.loads(session_file.read_text()) + client.restore_login(s["user_id"], s["device_id"], s["access_token"]) + return client + if creds.get("password"): + resp = await client.login(creds["password"], device_name="vortex-tui") + if isinstance(resp, LoginResponse): + session_file.write_text( + json.dumps( + { + "user_id": resp.user_id, + "device_id": resp.device_id, + "access_token": resp.access_token, + } + ) + ) + return client + if creds.get("access_token"): + # Fall back to the bootstrap token (shares the bootstrap device). + client.restore_login( + user_id, creds.get("device_id") or "VORTEXTUI", creds["access_token"] + ) + return client + await client.close() + raise RuntimeError("no usable password or access_token in credentials file") diff --git a/tui/pyproject.toml b/tui/pyproject.toml new file mode 100644 index 0000000..12f18aa --- /dev/null +++ b/tui/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "vortex-tui" +version = "0.1.0" +description = "Unified Matrix messaging TUI (Synapse + mautrix bridges)" +requires-python = ">=3.11" +dependencies = [ + "textual>=0.80", + "matrix-nio[e2e]>=0.25", + "qrcode>=7", + "watchfiles>=0.21", +] + +[project.scripts] +vortex-tui = "vortex_tui.main:run" + +[tool.setuptools] +package-dir = { "" = "app" } +packages = ["vortex_tui"]