#!/usr/bin/env bash # register-matrix-bot.sh — provision the Matrix bot account the maintenance # agent uses to talk to the operator, and a private room shared with the admin. # # Idempotent-ish: re-running re-uses the existing account (login still works if # the password is unchanged) and creates a fresh room only if one isn't set. # # Requirements: run on the core-stack host, with the `user-synapse` container # running. Uses the registration shared secret already in the synapse config. # # Usage: # ./register-matrix-bot.sh # uses config.json next to this script # CONFIG=/path/config.json ./register-matrix-bot.sh set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG="${CONFIG:-$DIR/config.json}" SECRETS="$DIR/.bot-secrets" # gitignored: stores the bot password if [ ! -f "$CONFIG" ]; then echo "ERROR: $CONFIG not found. Copy config.example.json to config.json first." exit 1 fi jq_get() { python3 -c "import json,sys; print(json.load(open('$CONFIG'))$1)"; } HOMESERVER="$(jq_get "['matrix']['homeserver']")" BOT_USER_ID="$(jq_get "['matrix']['user_id']")" ADMIN_USER_ID="$(jq_get "['matrix']['admin_user_id']")" SYNAPSE_CONTAINER="${SYNAPSE_CONTAINER:-user-synapse}" # localpart from @maintainer:semprini.me -> maintainer BOT_LOCAL="${BOT_USER_ID#@}"; BOT_LOCAL="${BOT_LOCAL%%:*}" echo "→ Homeserver : $HOMESERVER" echo "→ Bot : $BOT_USER_ID" echo "→ Admin : $ADMIN_USER_ID" # 1. Password — reuse if we created one before, else generate and persist. if [ -f "$SECRETS" ]; then # shellcheck disable=SC1090 source "$SECRETS" fi if [ -z "${BOT_PASSWORD:-}" ]; then BOT_PASSWORD="$(openssl rand -hex 32)" umask 077; echo "BOT_PASSWORD=$BOT_PASSWORD" > "$SECRETS" echo "→ Generated new bot password (saved to $SECRETS)" fi # 2. Create the account (no-op if it already exists). echo "→ Registering account in $SYNAPSE_CONTAINER…" if docker exec "$SYNAPSE_CONTAINER" register_new_matrix_user \ -u "$BOT_LOCAL" -p "$BOT_PASSWORD" --no-admin \ -c /data/homeserver.yaml http://localhost:8008 2>&1 | tee /tmp/reg.out; then : fi grep -qiE "already taken|User ID already" /tmp/reg.out && \ echo " (account already exists — continuing)" # 3. Log in to obtain an access token. echo "→ Logging in…" LOGIN_JSON="$(curl -fsS -X POST "$HOMESERVER/_matrix/client/v3/login" \ -H 'Content-Type: application/json' \ -d "{\"type\":\"m.login.password\", \"identifier\":{\"type\":\"m.id.user\",\"user\":\"$BOT_LOCAL\"}, \"password\":\"$BOT_PASSWORD\", \"initial_device_display_name\":\"semprini-maintainer\"}")" ACCESS_TOKEN="$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['access_token'])" "$LOGIN_JSON")" [ -n "$ACCESS_TOKEN" ] || { echo "ERROR: no access token"; exit 1; } echo " got access token" # 4. Ensure a private room exists with the admin invited. ROOM_ID="$(jq_get "['matrix'].get('room_id','')" 2>/dev/null || echo "")" case "$ROOM_ID" in ""|FILLED_BY_*|!*) : ;; esac if [[ "$ROOM_ID" != \!* ]]; then echo "→ Creating maintenance room and inviting $ADMIN_USER_ID…" ROOM_JSON="$(curl -fsS -X POST "$HOMESERVER/_matrix/client/v3/createRoom" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d "{\"name\":\"Stack Maintenance\", \"topic\":\"semprini-core autonomous stack maintenance\", \"preset\":\"trusted_private_chat\", \"is_direct\":true, \"invite\":[\"$ADMIN_USER_ID\"]}")" ROOM_ID="$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['room_id'])" "$ROOM_JSON")" echo " room: $ROOM_ID" else echo "→ Re-using existing room $ROOM_ID" fi # 5. Write token + room back into config.json. python3 - "$CONFIG" "$ACCESS_TOKEN" "$ROOM_ID" <<'PY' import json, sys path, token, room = sys.argv[1], sys.argv[2], sys.argv[3] cfg = json.load(open(path)) cfg.setdefault("matrix", {}) cfg["matrix"]["access_token"] = token cfg["matrix"]["room_id"] = room json.dump(cfg, open(path, "w"), indent=2) print(f" wrote access_token + room_id to {path}") PY echo echo "✓ Done." echo " ACTION REQUIRED (one-time): in Element (chat.semprini.me) as" echo " $ADMIN_USER_ID, accept the invite to the 'Stack Maintenance' room."