mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 21:14:04 +01:00
CI failed on shellcheck where the local hook passed: the runner's shellcheck reports SC2015 (`A && B || C` is not if-then-else) and mine did not. Both now run with --severity=style so a version skew cannot pass locally and fail in CI. The cleanup traps are plain ifs, which is what the finding was about -- `kill` on an already-dead pid makes B fail and C run while A is true. live_smoke.sh choose_group had two bugs that only surface when it is actually run, neither reachable from the offline suite: - it fetched /v1/groups twice, once to display and once to resolve the picked index, so an unstable order would hand back a different group than the one shown. Fetched once into a variable now. - `read -r -p` wrote its prompt to stdout, which the caller captures with $(...), so the prompt text would have been swallowed into the group id. Prompt and listing both go to stderr; stdout carries only the id. Also validates that the pick is a number and in range. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yaBT45KhHqxTKVCTA87iy
169 lines
5.5 KiB
Bash
Executable File
169 lines
5.5 KiB
Bash
Executable File
#!/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() {
|
|
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
|
|
rm -rf "$TEMP_DIR"
|
|
fi
|
|
}
|
|
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" groups choice id
|
|
# Fetched ONCE: two separate calls could return a different order, so the
|
|
# index the user picks would resolve against a different list.
|
|
groups="$(curl -sf "${API}/v1/groups/${number}")"
|
|
|
|
# Every prompt goes to stderr: this function's stdout is captured by $(...)
|
|
# and must contain nothing but the chosen group id.
|
|
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 </dev/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() {
|
|
local number="$1" group="$2"
|
|
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 (GROUP_ID == GROUP_ID_SEND, replies land in the same group)"
|
|
}
|
|
|
|
main() {
|
|
validate_requirements
|
|
TEMP_DIR="$(mktemp -d)"
|
|
|
|
start_container "$IMAGE"
|
|
verify_mode
|
|
show_qr_code
|
|
wait_for_link
|
|
|
|
local number group
|
|
number="$(curl -sf "${API}/v1/accounts" | jq -r '.[0]')"
|
|
echo "== linked as ${number}"
|
|
group="$(choose_group "$number")"
|
|
[[ -n "$group" && "$group" != "null" ]] || { echo "Error: no group chosen" >&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 "$@"
|