#!/bin/bash # ============================================================================ # Install, start and verify the signal-bot user service. # # A *user* unit, not a system one: the bot runs as whoever registered the # Signal account and reads that user's .env. Lingering is what makes it start # at boot with nobody logged in -- without it, "survives reboots" is a lie the # unit file cannot tell you about, so this script checks and fails loudly. # ============================================================================ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" readonly REPO_ROOT SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME readonly UNIT_NAME="signal-bot.service" readonly UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" readonly TEMPLATE="${REPO_ROOT}/scripts/${UNIT_NAME}" readonly WATCHDOG_UNITS=("signal-api-watchdog.service" "signal-api-watchdog.timer") readonly WATCHDOG_TIMER="signal-api-watchdog.timer" readonly DEFAULT_PORT=8137 VERIFY_ONLY="false" # shellcheck source=scripts/lib_signal.sh source "${REPO_ROOT}/scripts/lib_signal.sh" usage() { echo "Usage: $SCRIPT_NAME [--verify-only]" echo " --verify-only Check the running service without reinstalling it." exit 0 } die() { echo "Error: $1" >&2 exit 1 } # The port the bot will actually bind: .env wins, code default otherwise. bot_port() { local port="" if [[ -f "${REPO_ROOT}/.env" ]]; then port="$(sed -n 's/^BIND_PORT=[[:space:]]*//p' "${REPO_ROOT}/.env" | tail -1)" fi printf '%s' "${port:-$DEFAULT_PORT}" } # exiftool is what !add_photo strips uploads with, and the strip is not # optional: without it every upload is refused rather than stored dirty. # Installed here rather than documented, per the repo's own rule that a # script owns its dependencies. ensure_exiftool() { if ! command -v exiftool >/dev/null; then echo "Installing perl-image-exiftool (needed by !add_photo)..." if ! sudo pacman -S --needed --noconfirm perl-image-exiftool; then die "could not install perl-image-exiftool; run: sudo pacman -S perl-image-exiftool" fi fi # `command -v` answers for *this shell*, which is not who has to find it. # Arch installs to /usr/bin/vendor_perl, on an interactive PATH and not on # a systemd unit's -- so on 2026-08-31 the bot refused every upload while # exiftool sat plainly on the installer's PATH. Ask the bot's own resolver. if ! env -i "PATH=/usr/local/bin:/usr/bin" "${REPO_ROOT}/.venv/bin/python" -c \ 'import sys; from signal_bot.media_strip import locate; sys.exit(0 if locate() else 1)'; then die "exiftool is installed but signal_bot.media_strip.locate() cannot find it under the unit's PATH" fi } validate_requirements() { command -v systemctl >/dev/null || die "systemctl not found; this needs systemd" ensure_exiftool [[ -f "${REPO_ROOT}/.env" ]] || die "no ${REPO_ROOT}/.env -- run scripts/register_bot.sh first" [[ -x "${REPO_ROOT}/run.sh" ]] || die "no executable ${REPO_ROOT}/run.sh" [[ -f "$TEMPLATE" ]] || die "no unit template at $TEMPLATE" [[ -x "${REPO_ROOT}/scripts/signal_api_watchdog.sh" ]] || die "no executable ${REPO_ROOT}/scripts/signal_api_watchdog.sh" local unit for unit in "${WATCHDOG_UNITS[@]}"; do [[ -f "${REPO_ROOT}/scripts/${unit}" ]] || die "no unit template for ${unit}" done } # Without linger, a user unit dies at logout and never starts at boot. require_linger() { if [[ "$(loginctl show-user "$USER" --property=Linger --value)" == "yes" ]]; then echo "== linger enabled: the bot starts at boot with nobody logged in" return 0 fi echo "== enabling linger for $USER" if ! sudo -n loginctl enable-linger "$USER" 2>/dev/null; then echo "Error: linger is off and sudo needs a password here." >&2 echo " Without it the bot will NOT come back after a reboot." >&2 echo " Run this, then re-run with --verify-only:" >&2 echo >&2 echo " sudo loginctl enable-linger $USER" >&2 return 1 fi } install_unit() { mkdir -p "$UNIT_DIR" sed "s|@REPO_ROOT@|${REPO_ROOT}|g" "$TEMPLATE" > "${UNIT_DIR}/${UNIT_NAME}" systemctl --user daemon-reload systemctl --user enable "$UNIT_NAME" >/dev/null systemctl --user restart "$UNIT_NAME" echo "== installed and started ${UNIT_DIR}/${UNIT_NAME}" } # The bot cannot restart the container it depends on from inside a wedged # event loop, so recovery from a dead Signal connection lives in a timer. install_watchdog() { local unit for unit in "${WATCHDOG_UNITS[@]}"; do sed "s|@REPO_ROOT@|${REPO_ROOT}|g" "${REPO_ROOT}/scripts/${unit}" >"${UNIT_DIR}/${unit}" done systemctl --user daemon-reload systemctl --user enable --now "$WATCHDOG_TIMER" >/dev/null echo "== installed and armed ${UNIT_DIR}/${WATCHDOG_TIMER}" } # Report what actually happened, not that the commands returned 0. verify() { local port="$1" if ! systemctl --user is-active --quiet "$UNIT_NAME"; then echo "Error: ${UNIT_NAME} is not active." >&2 systemctl --user --no-pager --lines=20 status "$UNIT_NAME" >&2 || true return 1 fi signal_wait_for_url "http://127.0.0.1:${port}/healthz" "signal-bot" 60 echo "== active, and answering on http://127.0.0.1:${port}/healthz" echo "== enabled at boot: $(systemctl --user is-enabled "$UNIT_NAME")" if ! systemctl --user is-active --quiet "$WATCHDOG_TIMER"; then echo "Error: ${WATCHDOG_TIMER} is not active; nothing will recover a dead" >&2 echo " connection to Signal automatically." >&2 return 1 fi systemctl --user list-timers --no-pager "$WATCHDOG_TIMER" | sed -n '2p' } main() { validate_requirements local port port="$(bot_port)" if [[ "$VERIFY_ONLY" != "true" ]]; then require_linger install_unit install_watchdog fi verify "$port" echo "== logs: journalctl --user -u ${UNIT_NAME} -f" } while [[ $# -gt 0 ]]; do case $1 in --verify-only) VERIFY_ONLY="true" shift ;; -h | --help) usage ;; *) echo "Unknown option: $1" >&2 exit 1 ;; esac done main "$@"