2026-07-08 12:13:13 -04:00
|
|
|
#!/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}")
|
2026-07-08 12:36:26 -04:00
|
|
|
PROV_SECRET=$(secret_of provisioning_shared_secret auto)
|
|
|
|
|
export PROV_SECRET
|
2026-07-08 12:13:13 -04:00
|
|
|
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"; }
|
|
|
|
|
|
2026-07-08 12:36:26 -04:00
|
|
|
BRIDGES_JSON="{" # network -> provisioning endpoint info, consumed by the TUI
|
|
|
|
|
|
2026-07-08 12:13:13 -04:00
|
|
|
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 = {} |
|
2026-07-08 12:36:26 -04:00
|
|
|
.double_puppet.secrets = {strenv(DOMAIN): ("as_token:" + strenv(DP_SECRET))}
|
2026-07-08 12:13:13 -04:00
|
|
|
' "$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
|
|
|
|
|
|
2026-07-08 12:36:26 -04:00
|
|
|
# applied every run (idempotent) so existing configs pick up changes:
|
|
|
|
|
# provisioning API on with matrix-token auth, for the TUI's guided login
|
|
|
|
|
if [ "$b" != discord ]; then
|
|
|
|
|
yq -i '.provisioning.shared_secret = strenv(PROV_SECRET) | .provisioning.allow_matrix_auth = true' "$cfg"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-07-08 12:13:13 -04:00
|
|
|
# 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"
|
2026-07-08 12:36:26 -04:00
|
|
|
kind=v2; [ "$b" = discord ] && kind=legacy
|
|
|
|
|
BRIDGES_JSON="$BRIDGES_JSON\"$b\": {\"base_url\": \"http://$b:$port\", \"kind\": \"$kind\"},"
|
2026-07-08 12:13:13 -04:00
|
|
|
log "bridge $b ready (port $port)"
|
|
|
|
|
done
|
|
|
|
|
|
2026-07-08 12:36:26 -04:00
|
|
|
echo "${BRIDGES_JSON%,}}" | yq -p json -o json > "$DATA/shared/bridges.json"
|
|
|
|
|
chmod a+r "$DATA/shared/bridges.json"
|
|
|
|
|
|
2026-07-08 12:13:13 -04:00
|
|
|
mv "$DATA/synapse/homeserver.yaml.new" "$DATA/synapse/homeserver.yaml"
|
|
|
|
|
chown -R 991:991 "$DATA/synapse" # synapse image runs as uid 991
|
|
|
|
|
log "done"
|