mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 20:37:03 +01:00
Some checks are pending
CI / check (push) Waiting to run
Shelter cannot work on this phone, and it would not have helped anyway.
It cannot work: setting a Device Owner makes Android enable
no_add_managed_profile, no_add_private_profile and no_add_clone_profile on
every user, which kills Shelter, Island, Private Space and app cloning
alike. Verified on-device rather than inferred -- `pm create-user
--profileOf 0 --managed` returns "Cannot add user: no_add_managed_profile is
enabled". This is the Android 11 COMP deprecation, enforced in three places
in DevicePolicyManagerService and unchanged through Android 16.
It would not have helped: a second Signal *app* is not a second Signal
*account*. Every account needs its own phone number, so the second number
was always the real requirement -- and once you have one, signal-cli can
register it as a PRIMARY device with no phone app, no QR scan and no work
profile at all. That is strictly better than both the linked-device setup
and the Shelter plan: the bot cannot read personal DMs and never posts as
the account owner.
scripts/register_bot.sh drives that flow. It gates on signal-cli >= 0.14.7
rather than hoping: stable container tag 0.100 bundles 0.14.5, and on
2026-03-06 Signal made the post-quantum ratchet mandatory and de-registered
every account whose client lacked it -- a stale client is how a bot dies
silently months later. Verified that the recommended CI tag really does
bundle 0.14.7 instead of assuming it.
Rehearsed offline, not written blind. fake_signal grew /v1/accounts,
/v1/register/{number}, /v1/register/{number}/verify/{token} and
/v1/groups/{number}, so the whole runbook including every error path
(missing captcha, stray verify, wrong code, unregistered number, empty group
list, out-of-range and non-numeric picks) runs against the fake. The picker
was proven to resolve the group the user actually pointed at.
Prompts now read from $SIGNAL_TTY instead of /dev/tty directly. A hard-coded
/dev/tty made the interactive path untestable without a real terminal; the
seam is the same idea as the injected clock and transport on the Python side,
and is what let the picker be rehearsed at all.
live_smoke.sh and register_bot.sh share scripts/lib_signal.sh so the two
cannot drift, and shellcheck now runs with -x so the sourced lib is actually
linted from its callers.
201 tests, 100% branch coverage, all 23 gates green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yaBT45KhHqxTKVCTA87iy
88 lines
2.9 KiB
Bash
88 lines
2.9 KiB
Bash
# shellcheck shell=bash
|
|
|
|
# ============================================================================
|
|
# Shared helpers for the scripts that drive a signal-cli-rest-api container.
|
|
#
|
|
# Sourced, never executed. Callers must define API before sourcing.
|
|
#
|
|
# Prompts read from $SIGNAL_TTY, not /dev/tty directly. The redirect is
|
|
# needed because these functions' stdout is captured by the caller, but a
|
|
# hard-coded /dev/tty makes the interactive path impossible to rehearse
|
|
# without a real terminal -- so it is a seam, exactly like the injected
|
|
# clock and transport on the Python side.
|
|
# ============================================================================
|
|
|
|
: "${SIGNAL_TTY:=/dev/tty}"
|
|
|
|
# Wait until a URL answers, or give up with a named error.
|
|
signal_wait_for_url() {
|
|
local url="$1" name="$2" attempts="${3:-150}" attempt=0
|
|
until curl -sf "$url" >/dev/null 2>&1; do
|
|
attempt=$((attempt + 1))
|
|
if [[ $attempt -ge $attempts ]]; then
|
|
echo "Error: $name did not come up at $url" >&2
|
|
return 1
|
|
fi
|
|
sleep 0.4
|
|
done
|
|
}
|
|
|
|
# Refuse to continue unless the container exposes a receive WebSocket.
|
|
signal_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
|
|
return 1
|
|
fi
|
|
echo "== container mode: $mode"
|
|
}
|
|
|
|
# Print the group id the user picks. Every prompt goes to stderr because this
|
|
# function's stdout is captured by the caller.
|
|
signal_choose_group() {
|
|
local number="$1" groups choice id
|
|
# Fetched ONCE: two calls could return a different order, so the index the
|
|
# user picks would resolve against a different list than the one shown.
|
|
groups="$(curl -sf "${API}/v1/groups/${number}")"
|
|
if [[ -z "$groups" || "$groups" == "[]" ]]; then
|
|
echo "Error: no groups on ${number}." >&2
|
|
echo " Add the bot to a private group from your phone first," >&2
|
|
echo " then re-run this script." >&2
|
|
return 1
|
|
fi
|
|
|
|
echo "== groups on this account:" >&2
|
|
printf '%s' "$groups" | 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 <"$SIGNAL_TTY" >&2
|
|
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
|
|
echo "Error: '$choice' is not a number" >&2
|
|
return 1
|
|
fi
|
|
|
|
id="$(printf '%s' "$groups" | jq -r --argjson n "$choice" '.[$n - 1].id // empty')"
|
|
if [[ -z "$id" ]]; then
|
|
echo "Error: no group at position $choice" >&2
|
|
return 1
|
|
fi
|
|
printf '%s' "$id"
|
|
}
|
|
|
|
# Write .env for the bot. GROUP_ID == GROUP_ID_SEND so replies land in the
|
|
# same group that triggered them.
|
|
signal_write_env() {
|
|
local repo_root="$1" number="$2" group="$3"
|
|
cat > "${repo_root}/.env" <<ENV
|
|
PHONE_NUMBER=${number}
|
|
GROUP_ID=${group}
|
|
GROUP_ID_SEND=${group}
|
|
API_BASE_URL=${API}
|
|
DATABASE_PATH=${repo_root}/signal_bot.sqlite3
|
|
ENV
|
|
echo "== wrote ${repo_root}/.env"
|
|
}
|