#!/bin/bash # Stoat install hooks. # # Upstream configures an instance with an interactive generate_config.sh that # asks for a domain and writes five files. This is the non-interactive # equivalent, driven by the domain LibrePortal already knows and writing into # the app's install directory. # # The one rule that matters here: secrets.env is generated ONCE and never # rewritten. REVOLT__FILES__ENCRYPTION_KEY decrypts every file ever uploaded to # the instance, so regenerating it on a reinstall would permanently orphan the # entire media store — which is exactly the failure upstream's script warns # about at length. stoat_install_pre() { local app_name="$1" if ! appInstallCheckRequirements "$app_name" "$CFG_STOAT_REQUIRES"; then stoat=n return 1 fi } # The public host, read back from the deployed compose once tag substitution has # filled it in. Everything else in this file is derived from it. _stoatDomain() { local app_name="$1" tagsManagerGetTagContent "$containers_dir$app_name/docker-compose.yml" "DOMAINSUBNAME_TAG_1" } # Generate secrets.env if it does not already exist. Returns without touching an # existing file — see the warning at the top. _stoatWriteSecrets() { local secrets_file="$1" if [[ -s "$secrets_file" ]]; then isNotice "Existing secrets.env found — keeping it (regenerating would orphan every uploaded file)." return 0 fi # VAPID keypair for web push. The public key is the uncompressed EC point, # which is the last 65 bytes of the DER encoding, base64url-encoded without # padding — that is what the browser Push API expects. local vapid_pem vapid_private vapid_public vapid_pem=$(mktemp) openssl ecparam -name prime256v1 -genkey -noout -out "$vapid_pem" 2>/dev/null vapid_private=$(base64 < "$vapid_pem" | tr -d '\n' | tr -d '=') vapid_public=$(openssl ec -in "$vapid_pem" -outform DER 2>/dev/null | tail -c 65 | base64 | tr '/+' '_-' | tr -d '\n' | tr -d '=') rm -f "$vapid_pem" local files_key livekit_key livekit_secret files_key=$(openssl rand -base64 32) livekit_key=$(openssl rand -hex 6) livekit_secret=$(openssl rand -hex 24) runFileWrite "$secrets_file" </dev/null) livekit_secret=$(grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__SECRET='\K[^']*" "$app_dir/secrets.env" 2>/dev/null) if [[ -z "$livekit_key" || -z "$livekit_secret" ]]; then isError "Could not read the LiveKit credentials from secrets.env — voice will not work." return 1 fi # HOSTNAME=:80 is what puts Caddy in plain-HTTP mode behind Traefik. The # VITE_* values are compiled into the browser bundle, so they must be the # public https:// URLs, not internal container addresses. local video_enabled="" [[ "$CFG_STOAT_ENABLE_VIDEO" != "false" ]] && video_enabled="true" runFileWrite "$app_dir/.env.web" <&1) checkSuccess "Copying Caddyfile to $app_dir" runFileOp chown -R "$docker_install_user":"$docker_install_user" "$app_dir" checkSuccess "Setting ownership on the $app_name install directory" } stoat_install_post() { local app_name="$1" local domain domain=$(_stoatDomain "$app_name") echo "" isNotice "Stoat first run:" echo "" echo " Open https://${domain} and create an account — the first account" echo " registered on a fresh instance becomes the instance owner." echo "" echo " Give it a few minutes on first boot: sixteen containers start in" echo " dependency order, and the API restarts until MongoDB and RabbitMQ" echo " both report healthy. 'docker compose ps' in the app directory" echo " shows where it has got to." echo "" echo " Voice falls back to TCP 7881, which is already open. For proper" echo " low-latency WebRTC from outside your LAN, also allow the UDP" echo " media range — LibrePortal's firewall layer only emits TCP rules," echo " so this one is manual:" echo "" echo " sudo ufw allow 50000:50100/udp" echo "" }