#!/usr/bin/env bash # SPDX-License-Identifier: AGPL-3.0-or-later # # birdwatch-relay interactive setup. Run on the NVR (the same Linux machine # that runs Frigate). Asks the questions you need to answer once, generates # secrets, writes config, builds the image, starts the container, and prints # the exact strings to paste into the BirdWatch app. set -euo pipefail BOLD='\033[1m' DIM='\033[2m' GREEN='\033[32m' RED='\033[31m' YELLOW='\033[33m' RESET='\033[0m' err() { printf "${RED}error:${RESET} %s\n" "$*" >&2; } warn() { printf "${YELLOW}warn:${RESET} %s\n" "$*"; } ok() { printf "${GREEN}ok:${RESET} %s\n" "$*"; } ask() { printf "${BOLD}%s${RESET}" "$*"; } hr() { printf "${DIM}--------------------------------------------------------${RESET}\n"; } echo printf "${BOLD}BirdWatch Relay — interactive setup${RESET}\n" hr echo cd "$(dirname "$0")" # --- Pre-flight ---------------------------------------------------------- if ! command -v docker >/dev/null 2>&1; then err "Docker is not installed. Install Docker first:" err " https://docs.docker.com/engine/install/" exit 1 fi if docker compose version >/dev/null 2>&1; then DC="docker compose" elif command -v docker-compose >/dev/null 2>&1; then DC="docker-compose" else err "Docker Compose v2 is required. Install: https://docs.docker.com/compose/install/" exit 1 fi if ! command -v openssl >/dev/null 2>&1; then err "openssl is required (used to generate the pairing code)." exit 1 fi if ! command -v curl >/dev/null 2>&1; then err "curl is required (used to verify the relay is healthy)." exit 1 fi if ! command -v ss >/dev/null 2>&1; then err "ss is required (install iproute2)." exit 1 fi ok "Docker: $(docker --version)" ok "Compose: $($DC version --short 2>/dev/null || $DC --version)" echo # is_port_free returns 0 if no process is listening on $1, non-zero otherwise. is_port_free() { ! ss -tlnH 2>/dev/null | awk '{print $4}' | grep -qE ":$1$" } # find_free_port walks upward from $1 to find a port nothing is listening on. find_free_port() { local p=$1 while [ "$p" -le 65535 ]; do if is_port_free "$p"; then echo "$p" return 0 fi p=$((p + 1)) done return 1 } # --- Step 1: Firebase service-account key -------------------------------- printf "${BOLD}Step 1 of 5 — Firebase service-account key${RESET}\n" hr cat <<'EOF' The relay needs a Firebase service-account JSON file. This is a small file you download from Google's Firebase Console — it lets the relay send pushes via Firebase Cloud Messaging. If you don't have one yet: 1. Open https://console.firebase.google.com 2. Click "Add project". Name it (e.g. "birdwatch-home"). Skip Analytics. 3. Once created, gear icon → "Project settings" → "Service accounts". 4. Click "Generate new private key" → confirm. A .json file downloads. 5. Move that file to this NVR. Paste the file's full path below. EOF echo while true; do ask "Path to Firebase service-account JSON file: " read -r FB_KEY_PATH FB_KEY_PATH="${FB_KEY_PATH/#\~/$HOME}" if [ -z "$FB_KEY_PATH" ]; then err "Path cannot be empty." continue fi if [ ! -f "$FB_KEY_PATH" ]; then err "File not found: $FB_KEY_PATH" continue fi if ! grep -q '"type"' "$FB_KEY_PATH" 2>/dev/null || \ ! grep -q '"private_key"' "$FB_KEY_PATH" 2>/dev/null; then err "That doesn't look like a Firebase service-account JSON." continue fi break done cp "$FB_KEY_PATH" ./firebase-key.json chmod 600 ./firebase-key.json ok "Copied to ./firebase-key.json (chmod 600)." echo # --- Step 2: relay port + public URL ------------------------------------- printf "${BOLD}Step 2 of 5 — Network${RESET}\n" hr PREFERRED_PORT=8080 if is_port_free $PREFERRED_PORT; then DEFAULT_PORT=$PREFERRED_PORT ok "Port $PREFERRED_PORT is free on this machine." else SUGGESTIONS=() p=$((PREFERRED_PORT + 1)) while [ ${#SUGGESTIONS[@]} -lt 5 ] && [ $p -le 65535 ]; do if is_port_free $p; then SUGGESTIONS+=("$p") fi p=$((p + 1)) done if [ ${#SUGGESTIONS[@]} -eq 0 ]; then err "Could not find any free port at or above $PREFERRED_PORT." exit 1 fi warn "Port $PREFERRED_PORT is already in use on this machine." echo "Available nearby ports: ${SUGGESTIONS[*]}" DEFAULT_PORT=${SUGGESTIONS[0]} fi echo cat <})" echo # --- Step 4: pairing code ------------------------------------------------ printf "${BOLD}Step 4 of 5 — Pairing code${RESET}\n" hr PAIRING_CODE=$(openssl rand -hex 3) ok "Pairing code generated." echo # --- Step 5: write config + build + start -------------------------------- printf "${BOLD}Step 5 of 5 — Write config and start${RESET}\n" hr if [ -f .env ]; then warn "Existing .env will be backed up to .env.bak." mv .env .env.bak fi cat > .env < docker-compose.yml </dev/null 2>&1; then echo " — ok" break fi echo -n "." sleep 1 if [ "$i" -eq 30 ]; then echo err "Relay did not become healthy in 30 seconds." err "Check logs: $DC logs relay" exit 1 fi done # --- Done ----------------------------------------------------------------- echo printf "${GREEN}========================================${RESET}\n" printf "${GREEN}Relay is running and connected to Frigate.${RESET}\n" printf "${GREEN}========================================${RESET}\n" echo echo "One thing left to do — pair your phone:" echo printf "${BOLD}In the BirdWatch Android app${RESET}\n" echo " Settings → \"Battery-saver push notifications\" → toggle ON." echo " When asked, paste:" echo " Relay URL: $RELAY_BASE_URL" echo " Pairing code: $PAIRING_CODE" echo echo "(No Frigate config edit is needed. The relay logs into Frigate as $FRIGATE_USER" echo "and subscribes to its event stream directly.)" echo hr echo "Useful commands:" echo " Watch logs: $DC logs -f relay" echo " Restart relay: $DC restart relay" echo " Stop relay: $DC down" echo " Re-run setup: ./setup.sh" echo echo "Files written by this setup (keep them private — never commit them):" echo " ./firebase-key.json — your Firebase service-account key" echo " ./.env — pairing code + Frigate credentials" echo " ./docker-compose.yml — runtime config" echo " ./data/ — SQLite database of registered devices" echo