Initial public release of birdwatch-relay
This commit is contained in:
commit
c176f2ad24
17 changed files with 2025 additions and 0 deletions
342
setup.sh
Normal file
342
setup.sh
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
#!/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 <<EOF
|
||||
Which port should the relay listen on?
|
||||
- Press ENTER to accept the suggested port: $DEFAULT_PORT
|
||||
- Or type a different number (1-65535) and press ENTER.
|
||||
EOF
|
||||
ask "Port [$DEFAULT_PORT]: "
|
||||
read -r RELAY_PORT
|
||||
RELAY_PORT="${RELAY_PORT:-$DEFAULT_PORT}"
|
||||
if ! [[ "$RELAY_PORT" =~ ^[0-9]+$ ]] || [ "$RELAY_PORT" -lt 1 ] || [ "$RELAY_PORT" -gt 65535 ]; then
|
||||
err "Invalid port: $RELAY_PORT (must be a number between 1 and 65535)."
|
||||
exit 1
|
||||
fi
|
||||
if ! is_port_free "$RELAY_PORT"; then
|
||||
err "Port $RELAY_PORT is already in use. Pick another and re-run."
|
||||
exit 1
|
||||
fi
|
||||
ok "Will use port $RELAY_PORT."
|
||||
echo
|
||||
|
||||
cat <<EOF
|
||||
What URL will the BirdWatch app on your phone use to reach the relay?
|
||||
|
||||
Examples:
|
||||
https://relay.example.com if you have a TLS reverse proxy in front
|
||||
http://192.168.1.50:$RELAY_PORT if it's LAN-only with no proxy
|
||||
|
||||
Type the FULL URL with scheme (http or https), no trailing slash. If you have
|
||||
a proxy in front (recommended for any remote access), the proxy's URL is what
|
||||
phones use — not this machine's port-mapped URL.
|
||||
|
||||
EOF
|
||||
|
||||
DEFAULT_LANIP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
DEFAULT_PUB_URL="http://${DEFAULT_LANIP:-192.168.1.x}:$RELAY_PORT"
|
||||
ask "Public URL [$DEFAULT_PUB_URL]: "
|
||||
read -r RELAY_BASE_URL
|
||||
RELAY_BASE_URL="${RELAY_BASE_URL:-$DEFAULT_PUB_URL}"
|
||||
if [[ ! "$RELAY_BASE_URL" =~ ^https?://[^[:space:]]+$ ]]; then
|
||||
err "Public URL must start with http:// or https://"
|
||||
exit 1
|
||||
fi
|
||||
RELAY_BASE_URL="${RELAY_BASE_URL%/}"
|
||||
ok "Phones will connect to: $RELAY_BASE_URL"
|
||||
echo
|
||||
|
||||
# --- Step 3: Frigate connection ------------------------------------------
|
||||
printf "${BOLD}Step 3 of 5 — Frigate${RESET}\n"
|
||||
hr
|
||||
cat <<EOF
|
||||
The relay subscribes to Frigate's /ws WebSocket as a long-lived event consumer.
|
||||
It needs your Frigate base URL and (if Frigate has auth enabled) a username
|
||||
and password to log in.
|
||||
|
||||
Recommended: create a dedicated Frigate user just for the relay (e.g.
|
||||
"birdwatch-relay") in Frigate's Auth UI. Limits blast radius if these
|
||||
credentials are exposed.
|
||||
|
||||
EOF
|
||||
|
||||
DEFAULT_FRIGATE_URL="http://localhost:5000"
|
||||
ask "Frigate base URL [$DEFAULT_FRIGATE_URL]: "
|
||||
read -r FRIGATE_URL
|
||||
FRIGATE_URL="${FRIGATE_URL:-$DEFAULT_FRIGATE_URL}"
|
||||
if [[ ! "$FRIGATE_URL" =~ ^https?://[^[:space:]]+$ ]]; then
|
||||
err "Frigate URL must start with http:// or https://"
|
||||
exit 1
|
||||
fi
|
||||
FRIGATE_URL="${FRIGATE_URL%/}"
|
||||
|
||||
ask "Frigate username (leave blank if Frigate auth is disabled): "
|
||||
read -r FRIGATE_USER
|
||||
|
||||
FRIGATE_PASSWORD=""
|
||||
if [ -n "$FRIGATE_USER" ]; then
|
||||
ask "Frigate password (input hidden): "
|
||||
read -rs FRIGATE_PASSWORD
|
||||
echo
|
||||
if [ -z "$FRIGATE_PASSWORD" ]; then
|
||||
err "Password cannot be empty when a username is set."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
ok "Frigate URL: $FRIGATE_URL (user: ${FRIGATE_USER:-<none>})"
|
||||
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 <<EOF
|
||||
# Generated by setup.sh on $(date -u +"%Y-%m-%dT%H:%M:%SZ").
|
||||
# Keep this file out of git and off public storage.
|
||||
RELAY_PAIRING_CODE=$PAIRING_CODE
|
||||
FRIGATE_URL=$FRIGATE_URL
|
||||
FRIGATE_USER=$FRIGATE_USER
|
||||
FRIGATE_PASSWORD=$FRIGATE_PASSWORD
|
||||
EOF
|
||||
chmod 600 .env
|
||||
|
||||
if [ -f docker-compose.yml ]; then
|
||||
warn "Existing docker-compose.yml will be backed up to docker-compose.yml.bak."
|
||||
mv docker-compose.yml docker-compose.yml.bak
|
||||
fi
|
||||
|
||||
HOST_UID=$(id -u)
|
||||
HOST_GID=$(id -g)
|
||||
|
||||
cat > docker-compose.yml <<EOF
|
||||
services:
|
||||
relay:
|
||||
build: .
|
||||
container_name: birdwatch-relay
|
||||
restart: unless-stopped
|
||||
user: "$HOST_UID:$HOST_GID"
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./firebase-key.json:/secrets/firebase-key.json:ro
|
||||
environment:
|
||||
RELAY_FIREBASE_KEY_PATH: /secrets/firebase-key.json
|
||||
RELAY_PAIRING_CODE: \${RELAY_PAIRING_CODE}
|
||||
RELAY_DB_PATH: /data/relay.db
|
||||
RELAY_PORT: "$RELAY_PORT"
|
||||
FRIGATE_URL: \${FRIGATE_URL}
|
||||
FRIGATE_USER: \${FRIGATE_USER}
|
||||
FRIGATE_PASSWORD: \${FRIGATE_PASSWORD}
|
||||
EOF
|
||||
ok "Wrote .env and docker-compose.yml."
|
||||
|
||||
mkdir -p ./data
|
||||
echo
|
||||
echo "Building image (first build downloads Go and dependencies, ~1 minute)..."
|
||||
$DC up -d --build
|
||||
|
||||
echo
|
||||
echo -n "Waiting for relay to come up"
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf "http://localhost:$RELAY_PORT/v1/health" >/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
|
||||
Loading…
Add table
Add a link
Reference in a new issue