Files
vortex/bootstrap/bootstrap.sh
T
Prad NukalaandClaude Fable 5 d57086cd00 feat: unified messaging TUI MVP — Synapse + 7 mautrix bridges + Textual TUI + MCP draft staging
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 <noreply@anthropic.com>
2026-07-08 12:13:13 -04:00

182 lines
7.0 KiB
Bash

#!/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" <<EOF
id: doublepuppet
url:
as_token: "$DP_SECRET"
hs_token: "$DP_HS_TOKEN"
sender_localpart: doublepuppet-sender
rate_limited: false
namespaces:
users:
- regex: '@.*:$(echo "$DOMAIN" | sed 's/\./\\./g')'
exclusive: false
EOF
echo "app_service_config_files:" >> "$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" <<EOF
id: $b
url: http://$b:$port
as_token: "$AS_TOKEN"
hs_token: "$HS_TOKEN"
sender_localpart: ${b}-as-sender
rate_limited: false
de.sorunome.msc2409.push_ephemeral: true
receive_ephemeral: true
namespaces:
users:
- regex: '@${b}bot:${esc_domain}'
exclusive: true
- regex: '@${b}_.*:${esc_domain}'
exclusive: true
EOF
cp "$DATA/bridges/$b/registration.yaml" "$DATA/synapse/appservices/$b.yaml"
echo " - /data/appservices/$b.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"