#!/bin/bash # ============================================================================ # Register a DEDICATED Signal number for the bot. # # The bot becomes its own PRIMARY Signal device: it is a separate identity, it # cannot read your personal DMs, and it never posts as you. No phone app, no # QR scan, no work profile. Shelter cannot help here anyway -- this phone has a # Device Owner, so no_add_managed_profile is enabled and no work profile can # be created at all. # # You need a number that can receive ONE SMS or voice call. Two manual steps: # solving a captcha in the browser, and reading the code off that number. # # Usage: scripts/register_bot.sh +48123456789 [--voice] # ============================================================================ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_DIR REPO_ROOT="$(dirname "$SCRIPT_DIR")" readonly REPO_ROOT readonly CONTAINER="${CONTAINER:-signal-api}" readonly PORT="${PORT:-9922}" readonly API="${API:-http://localhost:${PORT}}" readonly DATA_DIR="${DATA_DIR:-$HOME/signal-api}" readonly IMAGE="${IMAGE:-bbernhard/signal-cli-rest-api:0.100}" readonly CAPTCHA_URL="https://signalcaptchas.org/registration/generate.html" # Registering is exactly the operation that punishes a stale client. On # 2026-03-06 Signal made the post-quantum ratchet mandatory and # unregistered every account whose client lacked it; 0.14.7 additionally # carries "Update to new registration/provisioning API". The stable # container tag lags, so this is checked rather than hoped for. readonly MIN_SIGNAL_CLI="0.14.7" readonly CI_IMAGE="bbernhard/signal-cli-rest-api:1787779383-ci" NUMBER="" USE_VOICE="false" # shellcheck source-path=SCRIPTDIR source=scripts/lib_signal.sh . "${SCRIPT_DIR}/lib_signal.sh" usage() { echo "Usage: $(basename "$0") + [--voice]" echo " --voice Verify by voice call instead of SMS (for landlines)." exit 0 } validate_requirements() { if [[ -z "$NUMBER" ]]; then echo "Error: a phone number in +E.164 form is required" >&2 exit 1 fi if ! [[ "$NUMBER" =~ ^\+[1-9][0-9]{6,14}$ ]]; then echo "Error: '$NUMBER' is not a +E.164 number (e.g. +48123456789)" >&2 exit 1 fi for tool in docker curl jq; do command -v "$tool" >/dev/null || { echo "Error: $tool is required" >&2 exit 1 } done } start_container() { if curl -sf "${API}/v1/about" >/dev/null 2>&1; then echo "== reusing the container already listening on :${PORT}" return 0 fi mkdir -p "$DATA_DIR" docker rm -f "$CONTAINER" >/dev/null 2>&1 || true 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 signal_wait_for_url "${API}/v1/about" "signal-cli-rest-api" } # Refuse to register with a client Signal may drop. A long-lived bot that # silently de-registers looks like a broken bot, not an outdated one. verify_signal_cli_version() { local version oldest version="$(docker exec "$CONTAINER" signal-cli --version 2>/dev/null | awk '{print $2}')" if [[ -z "$version" ]]; then echo "Error: could not read the signal-cli version from $CONTAINER" >&2 return 1 fi oldest="$(printf '%s\n%s\n' "$MIN_SIGNAL_CLI" "$version" | sort -V | head -1)" if [[ "$oldest" != "$MIN_SIGNAL_CLI" ]]; then echo "Error: this container bundles signal-cli ${version}, older than ${MIN_SIGNAL_CLI}." >&2 echo " Registering with a stale client risks the account being" >&2 echo " de-registered server-side later. Re-run against a newer image:" >&2 echo >&2 echo " docker rm -f ${CONTAINER}" >&2 echo " IMAGE=${CI_IMAGE} $0 ${NUMBER}" >&2 return 1 fi echo "== signal-cli ${version} (>= ${MIN_SIGNAL_CLI})" } already_registered() { curl -sf "${API}/v1/accounts" | jq -e --arg n "$NUMBER" 'index($n)' >/dev/null 2>&1 } # The captcha is the one step that cannot be automated: it exists precisely to # stop automated registration. Open the page and take the token by hand. collect_captcha() { echo "== solving the captcha (the only step I cannot do for you)" >&2 echo " 1. Solve the captcha in the browser." >&2 echo " 2. Right-click 'Open Signal' -> Copy link address." >&2 echo " 3. Paste it below. It looks like: signalcaptcha://signal-hcaptcha..." >&2 if command -v xdg-open >/dev/null; then xdg-open "$CAPTCHA_URL" >/dev/null 2>&1 & else echo " Open this URL yourself: $CAPTCHA_URL" >&2 fi local token read -r -p "Captcha link: " token <"$SIGNAL_TTY" >&2 if [[ -z "$token" ]]; then echo "Error: no captcha token given" >&2 return 1 fi printf '%s' "$token" } request_code() { local captcha="$1" response response="$(curl -sS -X POST "${API}/v1/register/${NUMBER}" \ -H 'Content-Type: application/json' \ -d "$(jq -n --arg c "$captcha" --argjson v "$USE_VOICE" \ '{captcha: $c, use_voice: $v}')")" if [[ -n "$response" ]] && printf '%s' "$response" | jq -e '.error' >/dev/null 2>&1; then echo "Error from Signal:" >&2 printf '%s\n' "$response" | jq -r '.error' >&2 return 1 fi echo "== verification code sent to ${NUMBER}" } submit_code() { local code response read -r -p "Verification code from ${NUMBER} (digits only): " code <"$SIGNAL_TTY" >&2 code="${code//[^0-9]/}" if [[ -z "$code" ]]; then echo "Error: no code given" >&2 return 1 fi response="$(curl -sS -X POST "${API}/v1/register/${NUMBER}/verify/${code}")" if [[ -n "$response" ]] && printf '%s' "$response" | jq -e '.error' >/dev/null 2>&1; then echo "Error from Signal:" >&2 printf '%s\n' "$response" | jq -r '.error' >&2 return 1 fi } confirm_registered() { if ! already_registered; then echo "Error: ${NUMBER} still is not in /v1/accounts after verifying." >&2 return 1 fi echo "== registered: ${NUMBER} is now its own primary Signal device" } main() { validate_requirements start_container signal_verify_mode verify_signal_cli_version if already_registered; then echo "== ${NUMBER} is already registered; skipping to group selection" else local captcha captcha="$(collect_captcha)" request_code "$captcha" submit_code confirm_registered fi echo echo "== NEXT: from your personal Signal, add ${NUMBER} to the private group," echo " then press Enter. The bot must be a member before it can post." read -r -p "Press Enter once the bot is in the group: " _ <"$SIGNAL_TTY" >&2 local group group="$(signal_choose_group "$NUMBER")" signal_write_env "$REPO_ROOT" "$NUMBER" "$group" echo "== starting the bot. Send '!cat' in that group." exec "$REPO_ROOT/run.sh" } while [[ $# -gt 0 ]]; do case $1 in --voice) USE_VOICE="true" shift ;; -h | --help) usage ;; *) NUMBER="$1" shift ;; esac done main "$@"