#!/bin/bash # ============================================================================ # Live verification against a real Signal account. # # Starts the signal-cli-rest-api container, opens the linking QR code, waits # for the link, lists your groups so you can pick a private one, writes .env # and starts the bot. The only manual step is scanning the QR code. # # Run scripts/e2e.sh first: this costs a device link, that costs nothing. # ============================================================================ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_DIR REPO_ROOT="$(dirname "$SCRIPT_DIR")" readonly REPO_ROOT readonly PYTHON="${PYTHON:-$REPO_ROOT/.venv/bin/python}" readonly CONTAINER="${CONTAINER:-signal-api}" readonly PORT="${PORT:-9922}" readonly API="http://localhost:${PORT}" readonly DATA_DIR="${DATA_DIR:-$HOME/signal-api}" # Stable tag. The dev series counts separately, so 0.167-dev is OLDER than this. readonly IMAGE="${IMAGE:-bbernhard/signal-cli-rest-api:0.100}" # Fallback when linking fails on client deprecation: tracks a newer signal-cli. readonly FALLBACK_IMAGE="${FALLBACK_IMAGE:-bbernhard/signal-cli-rest-api:1787779383-ci}" TEMP_DIR="" cleanup() { [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR" return 0 } trap cleanup EXIT validate_requirements() { command -v docker >/dev/null || { echo "Error: docker is required" >&2; exit 1; } command -v curl >/dev/null || { echo "Error: curl is required" >&2; exit 1; } command -v jq >/dev/null || { echo "Error: jq is required" >&2; exit 1; } [[ -x "$PYTHON" ]] || { echo "Error: no interpreter at $PYTHON" >&2 echo " Run: uv venv .venv && uv pip install -e '.[dev]'" >&2 exit 1 } } start_container() { local image="$1" docker rm -f "$CONTAINER" >/dev/null 2>&1 || true mkdir -p "$DATA_DIR" echo "== starting $image on :${PORT} (MODE=json-rpc-native)" docker run -d --name "$CONTAINER" --restart=always -p "${PORT}:8080" \ -v "${DATA_DIR}:/home/.local/share/signal-cli" \ -e 'MODE=json-rpc-native' "$image" >/dev/null local attempt=0 until curl -sf "${API}/v1/about" >/dev/null 2>&1; do attempt=$((attempt + 1)) if [[ $attempt -ge 150 ]]; then echo "Error: container never became ready. Logs:" >&2 docker logs --tail 30 "$CONTAINER" >&2 return 1 fi sleep 0.4 done } verify_mode() { local mode mode="$(curl -sf "${API}/v1/about" | jq -r '.mode // "unknown"')" if [[ "$mode" != *json-rpc* ]]; then echo "Error: container mode is '$mode'." >&2 echo " /v1/receive is only a WebSocket in a json-rpc mode." >&2 exit 1 fi echo "== container mode: $mode" } show_qr_code() { local png="${TEMP_DIR}/link.png" curl -sf "${API}/v1/qrcodelink?device_name=${CONTAINER}" -o "$png" echo "== scan this QR code: Signal > Settings > Linked devices > +" # The endpoint returns a PNG, so it has to be opened, not printed. if command -v xdg-open >/dev/null; then xdg-open "$png" >/dev/null 2>&1 & else echo " open this file: $png" fi } wait_for_link() { echo "== waiting for the link (Ctrl-C to abort)" local attempt=0 until [[ "$(curl -sf "${API}/v1/accounts" | jq -r 'length // 0')" != "0" ]]; do attempt=$((attempt + 1)) if [[ $attempt -ge 300 ]]; then echo "Error: no account linked after 5 minutes." >&2 echo " If linking failed with a connection error, retry with:" >&2 echo " IMAGE=$FALLBACK_IMAGE $0" >&2 exit 1 fi sleep 1 done } choose_group() { local number="$1" echo "== groups on this account:" >&2 curl -sf "${API}/v1/groups/${number}" \ | jq -r '.[] | "\(.id)\t\(.name // "(unnamed)")"' \ | nl -w3 -s' ' >&2 echo >&2 read -r -p "Pick the number of the PRIVATE group to use: " choice curl -sf "${API}/v1/groups/${number}" \ | jq -r --argjson n "$choice" '.[$n - 1].id' } write_env() { local number="$1" group="$2" cat > "${REPO_ROOT}/.env" <&2; exit 1; } write_env "$number" "$group" echo "== starting the bot. Send '!cat' in that group; you should get a cat." exec "$REPO_ROOT/run.sh" } main "$@"