#!/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"