export DISCORD_BRIDGE_YAML_VERSION=v2 export ENTRYPOINT_CONF_VERSION=v3 export HOMESERVER_YAML_VERSION=v37 export LOG_CONFIG_VERSION=v2 export SHARED_SECRET_AUTH_VERSION=v2 export SIGNAL_BRIDGE_YAML_VERSION=v6 export TELEGRAM_BRIDGE_YAML_VERSION=v6 export NGINX_CONFIG_VERSION=v13 export WK_SERVER_VERSION=v1 export WK_CLIENT_VERSION=v2 export MAS_CONFIG_VERSION=v2 export PG_BACKUP_VERSION=v2 export ADMIN_CONFIG_VERSION=v1 export COMPRESS_STATE_ENTRYPOINT_VERSION=v5 ############################################################################### # Database maintenance — shrink a bloated Synapse database # # See https://levans.fr/shrink-synapse-database.html # # Recommended steps to reclaim disk space: # 1. abra app cmd compress-state run_compressor 500 10000 # (compress redundant state — safe while Synapse is running) # 2. abra app cmd db reindex # (rebuild indexes — stop Synapse first) # 3. abra app cmd db vacuum_full # (rewrite tables and reclaim disk — stop Synapse first) # # Diagnostic commands (safe to run anytime): # abra app cmd db db_size # abra app cmd db state_bloat # abra app cmd db empty_rooms # # Purge commands (require an admin token): # abra app cmd app register_admin # abra app cmd app get_token # abra app cmd app purge_remote_media # abra app cmd app purge_empty_rooms # abra app cmd app purge_room # abra app cmd app purge_history ############################################################################### # --- Diagnostics (db) --- db_size() { echo "=== Database size ===" psql -U synapse -d synapse -c "SELECT pg_size_pretty(pg_database_size('synapse')) AS db_size;" echo "" echo "=== Top 10 largest tables ===" psql -U synapse -d synapse -c " SELECT nspname || '.' || relname AS table, pg_size_pretty(pg_total_relation_size(C.oid)) AS total_size FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) WHERE nspname NOT IN ('pg_catalog', 'information_schema') ORDER BY pg_total_relation_size(C.oid) DESC LIMIT 10;" } state_bloat() { echo "=== Rooms with most state bloat ===" psql -U synapse -d synapse -c " SELECT room_id, count(*) AS state_entries FROM state_groups_state GROUP BY room_id ORDER BY state_entries DESC LIMIT 20;" } empty_rooms() { echo "=== Rooms with no local members ===" psql -U synapse -d synapse -c " SELECT room_id, room_version FROM rooms WHERE room_id NOT IN ( SELECT room_id FROM local_current_membership WHERE membership = 'join' );" } # --- Compression (compress-state) --- run_compressor() { CHUNK_SIZE="${1:-${STATE_COMPRESS_CHUNK_SIZE:-500}}" CHUNKS="${2:-${STATE_COMPRESS_CHUNKS:-100}}" DB_PASS=$(cat /run/secrets/db_password) echo "Running synapse_auto_compressor (chunk_size=$CHUNK_SIZE, chunks=$CHUNKS)..." /build/synapse_auto_compressor \ -p "postgresql://synapse:${DB_PASS}@db:5432/synapse" \ -c "$CHUNK_SIZE" -n "$CHUNKS" } # --- Maintenance (db) — stop Synapse before running these --- reindex() { echo "WARNING: REINDEX locks tables. Synapse should be stopped before running this." echo "Running REINDEX on synapse database..." psql -U synapse -d synapse -c "REINDEX (VERBOSE) DATABASE synapse;" echo "REINDEX complete." psql -U synapse -d synapse -c "SELECT pg_size_pretty(pg_database_size('synapse')) AS db_size;" } vacuum_full() { echo "WARNING: VACUUM FULL locks tables and requires temporary disk space." echo "Synapse should be stopped before running this." echo "Running VACUUM FULL on synapse database..." psql -U synapse -d synapse -c "VACUUM FULL;" echo "VACUUM FULL complete." psql -U synapse -d synapse -c "SELECT pg_size_pretty(pg_database_size('synapse')) AS db_size;" } # --- Purge commands (app) — require an admin access token --- register_admin() { USER="${1}" PASS="${2}" if [ -z "$USER" ] || [ -z "$PASS" ]; then echo "Usage: register_admin " return 1 fi register_new_matrix_user -u "$USER" -p "$PASS" -a -c /data/homeserver.yaml http://localhost:8008 } get_token() { USER="${1}" PASS="${2}" if [ -z "$USER" ] || [ -z "$PASS" ]; then echo "Usage: get_token " echo "Returns an admin access token for use with purge commands." return 1 fi curl -s -X POST "http://localhost:8008/_matrix/client/r0/login" \ -H "Content-Type: application/json" \ -d "{\"type\":\"m.login.password\",\"user\":\"$USER\",\"password\":\"$PASS\"}" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('access_token', d.get('error', 'unknown error')))" } purge_remote_media() { DAYS="${1:-30}" TOKEN="${2}" if [ -z "$TOKEN" ]; then echo "Usage: purge_remote_media " return 1 fi BEFORE_TS=$(( $(date +%s) * 1000 - DAYS * 86400000 )) echo "Purging remote media older than $DAYS days..." curl -s -X POST "http://localhost:8008/_synapse/admin/v1/purge_media_cache?before_ts=$BEFORE_TS" \ -H "Authorization: Bearer $TOKEN" echo "" } purge_room() { ROOM_ID="${1}" TOKEN="${2}" if [ -z "$ROOM_ID" ] || [ -z "$TOKEN" ]; then echo "Usage: purge_room " return 1 fi echo "Purging room $ROOM_ID..." curl -s -X DELETE "http://localhost:8008/_synapse/admin/v1/rooms/$ROOM_ID" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"purge": true}' echo "" } purge_history() { ROOM_ID="${1}" DAYS="${2:-90}" TOKEN="${3}" if [ -z "$ROOM_ID" ] || [ -z "$TOKEN" ]; then echo "Usage: purge_history " return 1 fi BEFORE_TS=$(( $(date +%s) * 1000 - DAYS * 86400000 )) echo "Purging history older than $DAYS days from $ROOM_ID..." curl -s -X POST "http://localhost:8008/_synapse/admin/v1/purge_history/$ROOM_ID" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"purge_up_to_ts\": $BEFORE_TS}" echo "" } purge_empty_rooms() { TOKEN="${1}" if [ -z "$TOKEN" ]; then echo "Usage: purge_empty_rooms " return 1 fi echo "Fetching rooms with no local members..." ROOMS=$(curl -s "http://localhost:8008/_synapse/admin/v1/rooms?limit=1000" \ -H "Authorization: Bearer $TOKEN" \ | python3 -c " import sys, json data = json.load(sys.stdin) for r in data.get('rooms', []): if r.get('joined_local_members', 0) == 0: print(r['room_id']) ") COUNT=$(echo "$ROOMS" | grep -c '.' || true) echo "Found $COUNT empty rooms." if [ "$COUNT" -eq 0 ]; then echo "Nothing to purge." return 0 fi echo "$ROOMS" echo "" echo "Purging..." for ROOM_ID in $ROOMS; do echo " Purging $ROOM_ID" curl -s -X DELETE "http://localhost:8008/_synapse/admin/v1/rooms/$ROOM_ID" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"purge": true}' > /dev/null done echo "Done." } ############################################################################### # Other commands ############################################################################### ensure_mas_database () { if ! psql -U synapse -d postgres -v ON_ERROR_STOP=1 -Atqc "SELECT 1 FROM pg_database WHERE datname = 'mas'" | grep -qx 1 then psql -U synapse -d postgres -v ON_ERROR_STOP=1 -c "CREATE DATABASE mas OWNER synapse" fi } # Generate a PEM RSA private key and insert it as the MAS signing secret. # `abra app secret generate` can only produce random hex/charset strings, so this # secret is marked `generate=false` in .env.sample and handled here instead. generate_mas_signing_rsa() { if ! command -v openssl &> /dev/null; then echo "openssl is required on your local machine to generate the MAS signing key." echo "It could not be found in your PATH, please install openssl to proceed." exit 1 fi KEY=$(openssl genrsa 2048 2>/dev/null) if [ -z "$KEY" ]; then echo "Failed to generate RSA private key with openssl." exit 1 fi if printf '%s\n' "$KEY" | abra app secret insert -C "$APP_NAME" mas_signing_rsa v1; then echo "MAS signing RSA key generated and inserted as v1." else echo "Failed to insert MAS signing RSA key." exit 1 fi } # Local helper: fetch homeserver.yaml from app, push to mas, then syn2mas check + dry-run. prepare_mas_migration () { local syn_cfg syn_cfg=/tmp/homeserver.yaml cleanup_prepare_mas_migration() { rm -f "homeserver.yaml" } trap cleanup_prepare_mas_migration EXIT echo "Fetching /data/homeserver.yaml from app to homeserver.yaml (abra app run … cat)..." if ! abra app run -t "$DOMAIN" app cat /data/homeserver.yaml > "homeserver.yaml" then return 1 fi if [ ! -s "homeserver.yaml" ]; then echo "Error: fetched homeserver.yaml is empty." >&2 return 1 fi echo "Copying into mas:/tmp" abra app cp "$DOMAIN" "homeserver.yaml" "mas:/tmp" || return 1 echo "Running mas-cli syn2mas check..." abra app run -t "$DOMAIN" mas -- mas-cli syn2mas check \ --config /etc/mas/config.yaml \ --synapse-config "$syn_cfg" || return 1 echo "Running mas-cli syn2mas migrate --dry-run..." abra app run -t "$DOMAIN" mas -- mas-cli syn2mas migrate \ --config /etc/mas/config.yaml \ --synapse-config "$syn_cfg" \ --dry-run || return 1 trap - EXIT cleanup_prepare_mas_migration echo "" echo "=== Next migration step: stop Synapse (downtime) ===" echo "Run on a host whose Docker CLI targets this Swarm (same machine you use for 'abra app deploy')." if [ -n "${STACK_NAME:-}" ]; then echo " docker service scale ${STACK_NAME}_app=0" else echo "STACK_NAME is not set here; resolve the Synapse service name with 'docker service ls' on that host, then:" echo "docker service scale _app=0" fi } # Run syn2mas migrate for real (writes MAS data). Run from your operator machine as MAS image is distroless. # Requires /tmp/homeserver.yaml in the mas container (e.g. from prepare_mas_migration) and # Synapse scaled down before migrate. run_mas_migration () { local syn_cfg=/tmp/homeserver.yaml echo "Running mas-cli syn2mas migrate in mas via abra app run..." abra app run -t "$DOMAIN" mas -- mas-cli syn2mas migrate \ --config /etc/mas/config.yaml \ --synapse-config "$syn_cfg" } set_admin () { admin=akadmin if [ -n "$1" ] then admin=$1 fi psql -U synapse -c "UPDATE users SET admin = 1 WHERE name = '@$admin:$DOMAIN'"; } set_bridge_tokens() { if [ -z "$1" ]; then echo "Error: Missing parameter. Usage: set_bridge_tokens " return 1 fi BRIDGETYPE=$1 echo "retrieve tokens from registration.yaml..." output=$(abra app run $DOMAIN app cat /${BRIDGETYPE}-data/registration.yaml) if [ $? -ne 0 ]; then echo "Error: Failed to retrieve registration.yaml for ${BRIDGETYPE} bridge:" echo "$output" return 1 fi hs_token=$(echo "$output" | sed -n 's/^hs_token:[[:space:]]*\(.*\)$/\1/p') as_token=$(echo "$output" | sed -n 's/^as_token:[[:space:]]*\(.*\)$/\1/p') echo "HS Token: $hs_token" echo "AS Token: $as_token" echo "UNDEPLOY $DOMAIN?" abra app undeploy $DOMAIN echo "Replacing tokens:" abra app secret rm $DOMAIN ${BRIDGETYPE}_as_token abra app secret insert $DOMAIN ${BRIDGETYPE}_as_token v1 $as_token abra app secret rm $DOMAIN ${BRIDGETYPE}_hs_token abra app secret insert $DOMAIN ${BRIDGETYPE}_hs_token v1 $hs_token echo "Redeploying $DOMAIN..." abra app deploy -n $DOMAIN }