diff --git a/.gitignore b/.gitignore index 9d70124..34ec744 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ mkmf.log -bbctl* \ No newline at end of file +bbctl* + +# Generated by setup-tailscale-serve.sh — contains secrets, never commit +config.env +run-bridge.sh diff --git a/README.md b/README.md index 155eb1a..bba2b80 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,157 @@ # sh-imessage-setup -Script to setup or upgrade [sh-imessage](https://github.com/mautrix/imessage) Beeper bridge with BlueBubbles connector + +Scripts to set up and maintain the [sh-imessage](https://github.com/mautrix/imessage) Beeper bridge +with a [BlueBubbles](https://bluebubbles.app) connector on macOS. + +Two setup paths are available depending on your use case: + +| Script | Proxy method | Best for | +|---|---|---| +| `setup-tailscale-serve.sh` | Tailscale Serve (HTTPS, tailnet-only) | Recommended — no port forwarding, auto-restart via LaunchAgent | +| `setup.sh` | Manual URL / localhost | Legacy — interactive, tmux-optional, cron-based restart | + +--- + +## Recommended: Tailscale Serve setup ### Prerequisites -Brew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -Xcode CLI Tools: xcode-select --install +All of the following must already be installed and running on the Mac that will serve iMessage: + +- **macOS Monterey 12+** (Ventura recommended) +- **[BlueBubbles Server](https://github.com/BlueBubblesApp/bluebubbles-server/releases/latest)** — running on the default port `1234` +- **[Tailscale](https://tailscale.com/download/mac)** — connected to your tailnet (standalone install recommended) +- **[Homebrew](https://brew.sh)** +- **A Beeper account** — signed into the Beeper app on this Mac + +### Usage -Blue Bubbles Server setup and running: brew install --cask bluebubbles or https://github.com/BlueBubblesApp/bluebubbles-server/releases/latest +```bash +# Clone (if you haven't already) +git clone https://github.com/ngencokamin/sh-imessage-setup.git +cd sh-imessage-setup -### Installation +# First-time setup or reconfigure +bash setup-tailscale-serve.sh -##### Required +# Force re-prompt all settings (new password, fresh Tailscale URL, etc.) +bash setup-tailscale-serve.sh --reset -- A computer running MacOS which will be left always running with the following software: - - MacOS Catalina minimum, Ventura recommended (more recent versions enable more features, reference the [BlueBubbles documentation](https://docs.bluebubbles.app/server#supported-mac-devices)). - - [BlueBubbles Server](https://github.com/BlueBubblesApp/bluebubbles-server/releases/latest) installed and running. +# Tear everything down cleanly +bash uninstall.sh +``` -##### Optional -- [tmux](https://github.com/tmux/tmux?tab=readme-ov-file#installation) - - This is not *required* to use this script, but provides additional benefits in launching the bridge post-install. +### What the script does +1. **Checks prerequisites** — BlueBubbles on port 1234, Tailscale connected, Homebrew present +2. **Installs `bbctl`** (Beeper Bridge Manager) to `~/.local/bin/` — downloads from nightly.link, auto-selects Apple Silicon (`arm64`) or Intel (`amd64`), clears Gatekeeper quarantine +3. **Logs in to Beeper** via `bbctl login` (browser popup; skipped if already authenticated) +4. **Configures Tailscale Serve** — exposes `https://..ts.net` → `localhost:1234` (HTTPS, tailnet-only, no port forwarding). This URL is for **remote clients** (phone, other devices). The bridge itself runs on the same Mac as BlueBubbles and always connects via `localhost:1234` directly. +5. **Prompts for your BlueBubbles password** and saves it to `~/.config/bb-beeper/config.env` (chmod 600). The password is passed to the BlueBubbles API as a query parameter (`?password=`). +6. **Generates a bridge runner script** at `~/.config/bb-beeper/run-bridge.sh` — includes startup diagnostics (bbctl auth, Tailscale status, BlueBubbles health check) logged before handing off to `bbctl run` +7. **Installs a LaunchAgent** (`~/Library/LaunchAgents/com.user.bb-beeper-bridge.plist`) — starts at login, auto-restarts with 30 s back-off on crash +8. **Smoke-tests** the setup and prints a summary -#### Upgrade Unsupported Mac +Configuration is saved so re-running the script is idempotent — it updates rather than rebuilding from scratch. -If your Mac does not officially support upgrading to Ventura or higher, you can check out OCLP (Opencore Legacy Patcher) [here](https://dortania.github.io/OpenCore-Legacy-Patcher/) to see how you can upgrade. Do note that upgrading with OCLP and then changing hardware can cause issues with iMessage. If you have trouble sending and receiving iMessages after an upgrade and hardware change, check out [this guide](https://gist.github.com/ngencokamin/6643b0253c49817ff20b7d9458fcfe06) to try and resolve it. This is what worked for me when I encountered a hardware ban. +### Useful commands -#### BlueBubbles +```bash +# Full status diagnostic (health check all components) +bash debug-status.sh -For initial BlueBubbles setup, see [this guide](https://bluebubbles.app/install/). Additionally, some features also require you disable SIP to enable BlueBubbles’ Private API features. See [this page](https://docs.bluebubbles.app/private-api/) for Private API features, as well as [this page](https://docs.bluebubbles.app/private-api/installation) for a guide on how to disable SIP and enable Private API. +# Show recent errors only +bash debug-status.sh --errors -### Setup +# Follow live bridge logs +tail -f ~/Library/Logs/bb-beeper-bridge.log -1. Open a new terminal window on your Mac -2. Run `git clone https://github.com/ngencokamin/sh-imessage-setup.git` to clone this repo to your device -3. Navigate into the cloned folder with `cd sh-imessage-setup` -4. Add run permissions with `chmod +x setup.sh` -5. Run `./setup.sh` and follow the prompts from the script +# Restart the bridge +launchctl unload ~/Library/LaunchAgents/com.user.bb-beeper-bridge.plist +launchctl load ~/Library/LaunchAgents/com.user.bb-beeper-bridge.plist -### Automating Startup +# Check Tailscale Serve status +tailscale serve status -The setup script includes a function, `create_cron_job`, which automates the startup of the Bridge script. This function creates a new bash script, `check_and_run.sh`, that checks if the `bbctl` process is running. If it's not, the script sources the `~/.bashrc` file and starts the server using the `start-bb-server` command. +# Verify Beeper login and bridge state +bbctl whoami +``` -The `create_cron_job` function also adds a new job to the crontab to run `check_and_run.sh` at system startup and every hour thereafter. This ensures that if the Bridge script encounters an issue and stops running, it will automatically restart. The cron job is set up as follows: +### File layout -- At system reboot, the `check_and_run.sh` script is executed. -- Every hour, on the hour, the `check_and_run.sh` script is executed again. +``` +# Repo +debug-status.sh # on-demand diagnostic snapshot +setup-tailscale-serve.sh # main setup script (recommended) +setup.sh # legacy interactive setup +uninstall.sh # tear everything down cleanly -This self-recovery mechanism ensures the continuous operation of the Bridge script. +# Generated at runtime +~/.config/bb-beeper/ +├── config.env # saved settings (chmod 600 — contains password) +└── run-bridge.sh # generated runner invoked by the LaunchAgent -### Fuction by Function breakdown: +~/Library/LaunchAgents/ +└── com.user.bb-beeper-bridge.plist # LaunchAgent (auto-start + keep-alive) + +~/Library/Logs/ +└── bb-beeper-bridge.log # stdout + stderr from the bridge +``` + +--- + +## Legacy: interactive setup (`setup.sh`) + +The original script from this repo. Supports tmux, optional shell alias, and cron-based restart. +Does **not** configure Tailscale — you supply the BlueBubbles URL yourself. + +### Prerequisites -Functions -install_xcode_tools() -This function checks if Xcode command line tools are installed on the system. If not, it installs them. This is necessary for some of the operations in the script. +- macOS Catalina minimum, Ventura recommended +- [BlueBubbles Server](https://github.com/BlueBubblesApp/bluebubbles-server/releases/latest) installed and running +- [Homebrew](https://brew.sh) +- Xcode CLI Tools: `xcode-select --install` +- *(optional)* [tmux](https://github.com/tmux/tmux) -check_macos_version() -This function checks the version of macOS on the system. If the version is less than the required version for BlueBubbles, it recommends the user to upgrade their macOS version. +### Usage -backup_bbctl() -This function finds the path to the bbctl binary, if it exists, and backs it up to the home directory as bbctl.bak. +```bash +chmod +x setup.sh +./setup.sh +``` -download_bbctl() -This function downloads the latest bbctl binary for the system's OS and architecture, unzips it, makes it executable, and moves it to /usr/local/bin. It also checks if the bbctl command works after installation. +Follow the interactive prompts. The script will: +- Install or update `bbctl` +- Log in to Beeper if needed +- Ask for your BlueBubbles URL and password +- Optionally set up a `start-bb-server` shell alias +- Optionally use tmux for a detached session +- Create a cron job (`~/check_and_run.sh`) for auto-restart on reboot and hourly -add_alias() -This function adds an alias to the user's shell (either .zshrc or .bashrc) to start the BlueBubbles server. The alias is start-bb-server. +--- -build_command() -This function builds the command to start the BlueBubbles server. It asks the user for their BlueBubbles URL and password, and builds the command accordingly. If the user chooses to use tmux, it includes tmux in the command. +## Upgrading an unsupported Mac -ping_bluebubbles_server() -This function pings the BlueBubbles server at the provided URL to check if it's running and responding to requests. +If your Mac doesn't officially support Ventura or later, see +[OpenCore Legacy Patcher](https://dortania.github.io/OpenCore-Legacy-Patcher/). +After an OCLP upgrade + hardware change, if iMessage breaks, see +[this guide](https://gist.github.com/ngencokamin/6643b0253c49817ff20b7d9458fcfe06). -create_cron_job() -This function creates a cron job that checks if the bbctl process is running at system startup and every hour thereafter. If it's not running, it starts it. This ensures that the server will automatically restart if it stops for any reason. +## BlueBubbles Private API -### Credits +Some features (typing indicators, read receipts, reactions) require disabling SIP and enabling +BlueBubbles' Private API. See the [Private API docs](https://docs.bluebubbles.app/private-api/) +and [installation guide](https://docs.bluebubbles.app/private-api/installation). -None of this would be possible without the recent hard work of Donovon Simpson and Christian Nuss, which built upon the foundational work of Tulir (Beeper Lead Architect and creator of mautrix-imessage ) and the BlueBubbles team who supported this project, along with many community members who tested and reported their findings. To acknowledge and support these incredible folks and their continued efforts, you can donate at the following links: +--- -- Nix Genco-Kamin (oh hey, it's me): https://www.buymeacoffee.com/ngencokamin +## Credits -- Tulir: https://github.com/sponsors/tulir -- Donovon Simpson: https://www.buymeacoffee.com/trek.boldly.go or https://github.com/sponsors/trek-boldly-go -- BlueBubbles Team: https://bluebubbles.app/donate -- Christian Nuss: https://github.com/cnuss (awaiting sponsor link) -- Cameron Aaron: https://www.buymeacoffee.com/cameronaaron or -https://github.com/sponsors/cameronaaron +None of this would be possible without the work of: +- **Nix Genco-Kamin** (original script author) — https://www.buymeacoffee.com/ngencokamin +- **Tulir** (Beeper Lead Architect, mautrix-imessage) — https://github.com/sponsors/tulir +- **Donovon Simpson** — https://www.buymeacoffee.com/trek.boldly.go +- **BlueBubbles Team** — https://bluebubbles.app/donate +- **Christian Nuss** — https://github.com/cnuss +- **Cameron Aaron** — https://www.buymeacoffee.com/cameronaaron diff --git a/debug-status.sh b/debug-status.sh new file mode 100755 index 0000000..928287f --- /dev/null +++ b/debug-status.sh @@ -0,0 +1,266 @@ +#!/usr/bin/env bash +# ============================================================================= +# debug-status.sh +# +# On-demand diagnostic snapshot for the BlueBubbles + Beeper + Tailscale stack. +# Run at any time to see the current state of all components. +# +# Usage: +# bash debug-status.sh # summary +# bash debug-status.sh --log N # also print last N lines of bridge log (default 50) +# bash debug-status.sh --errors # print only ERR/WRN lines from bridge log +# ============================================================================= + +set -uo pipefail + +# ── Constants (must match setup-tailscale-serve.sh) ─────────────────────────── +CONFIG_DIR="$HOME/.config/bb-beeper" +CONFIG_FILE="$CONFIG_DIR/config.env" +BRIDGE_RUNNER="$CONFIG_DIR/run-bridge.sh" +LOG_FILE="$HOME/Library/Logs/bb-beeper-bridge.log" +LAUNCH_LABEL="com.user.bb-beeper-bridge" +LAUNCH_PLIST="$HOME/Library/LaunchAgents/${LAUNCH_LABEL}.plist" +BBCTL_BIN="$HOME/.local/bin/bbctl" +BB_PORT=1234 + +# ── Colours ─────────────────────────────────────────────────────────────────── +R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'; B='\033[0;34m' +BOLD='\033[1m'; N='\033[0m' + +ok() { printf "${G}[✓]${N} %s\n" "$*"; } +warn() { printf "${Y}[!]${N} %s\n" "$*"; } +err() { printf "${R}[✗]${N} %s\n" "$*"; } +info() { printf "${B}[→]${N} %s\n" "$*"; } +sep() { printf "${BOLD}${B}────────────────────────────────────────${N}\n"; } + +# ── Args ────────────────────────────────────────────────────────────────────── +SHOW_LOG=false +LOG_LINES=50 +ERRORS_ONLY=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --log) SHOW_LOG=true; [[ "${2:-}" =~ ^[0-9]+$ ]] && { LOG_LINES="$2"; shift; }; shift ;; + --errors) ERRORS_ONLY=true; SHOW_LOG=true; shift ;; + *) shift ;; + esac +done + +# ── Load config ─────────────────────────────────────────────────────────────── +BB_LOCAL_URL="http://localhost:${BB_PORT}" +BB_PASSWORD="" +TAILSCALE_URL="" + +if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck source=/dev/null + source "$CONFIG_FILE" +fi + +export PATH="$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH" + +echo +printf "${BOLD}${B} BlueBubbles + Beeper + Tailscale — Debug Status${N}\n" +printf " $(date '+%Y-%m-%d %H:%M:%S %Z')\n" + +# ============================================================================= +sep +printf "${BOLD}1. LaunchAgent${N}\n" +# ============================================================================= + +if [[ -f "$LAUNCH_PLIST" ]]; then + ok "Plist exists: $LAUNCH_PLIST" +else + err "Plist missing: $LAUNCH_PLIST" +fi + +launchctl_entry="$(launchctl list 2>/dev/null | grep "$LAUNCH_LABEL" || true)" +if [[ -n "$launchctl_entry" ]]; then + pid="$(echo "$launchctl_entry" | awk '{print $1}')" + exit_code="$(echo "$launchctl_entry" | awk '{print $2}')" + if [[ "$pid" == "-" ]]; then + if [[ "$exit_code" == "0" ]]; then + warn "LaunchAgent registered but not running (last exit: 0)" + else + err "LaunchAgent registered but not running (last exit code: $exit_code)" + fi + else + ok "LaunchAgent running (PID: $pid)" + fi +else + err "LaunchAgent not registered with launchd" +fi + +# ============================================================================= +sep +printf "${BOLD}2. bbctl / Bridge process${N}\n" +# ============================================================================= + +if [[ -x "$BBCTL_BIN" ]]; then + ok "bbctl binary: $BBCTL_BIN ($(${BBCTL_BIN} version 2>/dev/null | head -1 || echo 'version unknown'))" +else + err "bbctl binary not found or not executable: $BBCTL_BIN" +fi + +bbctl_pids="$(pgrep -f 'bbctl run' 2>/dev/null | tr '\n' ' ' || true)" +if [[ -n "$bbctl_pids" ]]; then + ok "bbctl run process(es): PID $bbctl_pids" +else + err "No 'bbctl run' process found" +fi + +mautrix_pids="$(pgrep -f 'mautrix-imessage' 2>/dev/null | tr '\n' ' ' || true)" +if [[ -n "$mautrix_pids" ]]; then + ok "mautrix-imessage process(es): PID $mautrix_pids" +else + warn "No mautrix-imessage process found" +fi + +# ============================================================================= +sep +printf "${BOLD}3. Beeper login (bbctl whoami)${N}\n" +# ============================================================================= + +if [[ -x "$BBCTL_BIN" ]]; then + whoami_out="$("$BBCTL_BIN" whoami 2>&1 || true)" + if echo "$whoami_out" | grep -qi "you're not logged in"; then + err "bbctl not logged in — run: $BBCTL_BIN login" + echo "$whoami_out" | sed 's/^/ /' + elif [[ -z "$whoami_out" ]]; then + warn "bbctl whoami returned no output" + else + ok "bbctl whoami:" + echo "$whoami_out" | sed 's/^/ /' + fi +fi + +# ============================================================================= +sep +printf "${BOLD}4. BlueBubbles Server${N}\n" +# ============================================================================= + +if nc -z localhost "$BB_PORT" 2>/dev/null; then + ok "Port $BB_PORT: open" +else + err "Port $BB_PORT: not reachable — is BlueBubbles Server running?" +fi + +# Try the health/info endpoint +if [[ -n "$BB_PASSWORD" ]]; then + http_code="$(curl -sf --max-time 5 \ + "${BB_LOCAL_URL}/api/v1/server/info?password=${BB_PASSWORD}" \ + -o /dev/null -w "%{http_code}" 2>/dev/null || echo "ERR")" + if [[ "$http_code" == "200" ]]; then + ok "BlueBubbles API /api/v1/server/info → HTTP $http_code" + elif [[ "$http_code" == "ERR" ]]; then + err "BlueBubbles API request failed (curl error)" + else + warn "BlueBubbles API /api/v1/server/info → HTTP $http_code (check password in config)" + fi +else + warn "No BB_PASSWORD in config — skipping API health check" +fi + +# ============================================================================= +sep +printf "${BOLD}5. Tailscale${N}\n" +# ============================================================================= + +if command -v tailscale &>/dev/null; then + ts_status="$(tailscale status 2>&1 | head -3)" + ok "Tailscale CLI found: $(tailscale version | head -1)" + echo "$ts_status" | sed 's/^/ /' + + echo + info "Tailscale serve status:" + tailscale serve status 2>&1 | sed 's/^/ /' || warn "tailscale serve status failed" + + ts_url="$(python3 - 2>/dev/null <<'PYEOF' +import subprocess, json, sys +try: + raw = subprocess.check_output(["tailscale", "status", "--json"]) + d = json.loads(raw) + dns = d["Self"]["DNSName"].rstrip(".") + print(f"https://{dns}") +except Exception: + pass +PYEOF + )" + if [[ -n "$ts_url" ]]; then + ok "MagicDNS URL: $ts_url" + if [[ "$TAILSCALE_URL" != "$ts_url" ]]; then + info "Config TAILSCALE_URL ($TAILSCALE_URL) differs from current MagicDNS URL ($ts_url)" + info "Bridge uses localhost — this only matters for remote clients (phone/tablet)." + info "Run setup-tailscale-serve.sh to update if needed." + fi + fi +else + err "tailscale CLI not found in PATH" +fi + +# ============================================================================= +sep +printf "${BOLD}6. Config file${N}\n" +# ============================================================================= + +if [[ -f "$CONFIG_FILE" ]]; then + ok "Config: $CONFIG_FILE" + # Print config but mask the password + while IFS= read -r line; do + if [[ "$line" =~ ^BB_PASSWORD ]]; then + echo " BB_PASSWORD=" + else + echo " $line" + fi + done < "$CONFIG_FILE" +else + err "Config file missing: $CONFIG_FILE" +fi + +# ============================================================================= +sep +printf "${BOLD}7. Log file${N}\n" +# ============================================================================= + +if [[ -f "$LOG_FILE" ]]; then + log_size="$(du -sh "$LOG_FILE" 2>/dev/null | cut -f1)" + log_modified="$(stat -f '%Sm' -t '%Y-%m-%d %H:%M:%S' "$LOG_FILE" 2>/dev/null || date -r "$LOG_FILE" 2>/dev/null || echo 'unknown')" + ok "Log: $LOG_FILE (${log_size}, last modified: ${log_modified})" + + # Always show last error/warning lines (log uses ANSI codes, match on text content) + err_count="$(grep -c ' ERR ' "$LOG_FILE" 2>/dev/null || true)" + err_count="${err_count:-0}" + if [[ "$err_count" -gt 0 ]]; then + warn "Found ${err_count} ERR lines in log. Most recent:" + grep ' ERR ' "$LOG_FILE" | tail -10 | sed 's/^/ /' + else + ok "No ERR lines in log" + fi + + if $SHOW_LOG; then + echo + if $ERRORS_ONLY; then + info "All ERR lines from log:" + grep ' ERR ' "$LOG_FILE" | sed 's/^/ /' || echo " (none)" + else + info "Last ${LOG_LINES} lines of log:" + tail -"${LOG_LINES}" "$LOG_FILE" | sed 's/^/ /' + fi + else + info "Tip: run with --log to print recent log lines, or --errors for errors only" + fi +else + warn "Log file not found: $LOG_FILE (bridge may not have started yet)" +fi + +# ============================================================================= +sep +printf "${BOLD}8. Quick fixes${N}\n" +# ============================================================================= + +echo " Restart bridge: launchctl unload $LAUNCH_PLIST && launchctl load $LAUNCH_PLIST" +echo " Tail logs: tail -f $LOG_FILE" +echo " Errors only: bash debug-status.sh --errors" +echo " Re-login Beeper: $BBCTL_BIN login" +echo " Reconfigure: bash setup-tailscale-serve.sh" +echo " Force reset: bash setup-tailscale-serve.sh --reset" +echo diff --git a/setup-tailscale-serve.sh b/setup-tailscale-serve.sh new file mode 100755 index 0000000..efb00bb --- /dev/null +++ b/setup-tailscale-serve.sh @@ -0,0 +1,477 @@ +#!/usr/bin/env bash +# ============================================================================= +# setup-tailscale-serve.sh +# +# Reproducible setup for: +# BlueBubbles Server → Tailscale Serve (HTTPS) → bbctl sh-imessage bridge +# (sends messages through Beeper) +# +# The bridge (mautrix-imessage via bbctl) runs on the same Mac as BlueBubbles +# and connects to it via localhost:1234 — not via the Tailscale URL. +# The Tailscale Serve URL is for remote clients (phone, other devices) only. +# +# Reference: https://github.com/ngencokamin/sh-imessage-setup +# +# Prerequisites (already installed): +# • BlueBubbles Server running on this Mac +# • Tailscale connected (standalone install recommended) +# • Homebrew +# • Beeper account +# +# Usage: +# bash setup-tailscale-serve.sh # first-time setup or update +# bash setup-tailscale-serve.sh --reset # force re-prompt of all config +# ============================================================================= + +set -euo pipefail + +# ── Constants ────────────────────────────────────────────────────────────────── +readonly SCRIPT_VERSION="1.1.0" +readonly BBCTL_BIN="$HOME/.local/bin/bbctl" +readonly CONFIG_DIR="$HOME/.config/bb-beeper" +readonly CONFIG_FILE="$CONFIG_DIR/config.env" +readonly BRIDGE_RUNNER="$CONFIG_DIR/run-bridge.sh" +readonly LOG_FILE="$HOME/Library/Logs/bb-beeper-bridge.log" +readonly LAUNCH_LABEL="com.user.bb-beeper-bridge" +readonly LAUNCH_PLIST="$HOME/Library/LaunchAgents/${LAUNCH_LABEL}.plist" +readonly BB_PORT=1234 +readonly ARCH="$(uname -m)" + +# ── Colours ──────────────────────────────────────────────────────────────────── +R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'; B='\033[0;34m' +BOLD='\033[1m'; N='\033[0m' + +info() { printf "${B}[→]${N} %s\n" "$*"; } +ok() { printf "${G}[✓]${N} %s\n" "$*"; } +warn() { printf "${Y}[!]${N} %s\n" "$*"; } +die() { printf "${R}[✗]${N} %s\n" "$*" >&2; exit 1; } +sep() { printf "${BOLD}${B}────────────────────────────────────────${N}\n"; } + +# ── Globals set during run ───────────────────────────────────────────────────── +TAILSCALE_URL="" +BB_PASSWORD="" +RESET=false + +# ── Parse args ───────────────────────────────────────────────────────────────── +for arg in "$@"; do + [[ "$arg" == "--reset" ]] && RESET=true +done + +# ============================================================================= +# STEP 1 — Prerequisites +# ============================================================================= +check_prereqs() { + sep + info "Checking prerequisites..." + + # Must be macOS + [[ "$(uname -s)" == "Darwin" ]] || die "This script requires macOS." + ok "macOS detected: $(sw_vers -productVersion)" + + # Homebrew + command -v brew &>/dev/null || die "Homebrew not found. Install from https://brew.sh" + ok "Homebrew: $(brew --version | head -1)" + + # Tailscale CLI in PATH + if ! command -v tailscale &>/dev/null; then + # Try common locations for standalone install + for ts_path in /usr/local/bin/tailscale /opt/homebrew/bin/tailscale \ + /Applications/Tailscale.app/Contents/MacOS/tailscale; do + if [[ -x "$ts_path" ]]; then + export PATH="$(dirname "$ts_path"):$PATH" + break + fi + done + fi + command -v tailscale &>/dev/null || die "tailscale CLI not found. Install via: brew install tailscale" + ok "Tailscale CLI: $(tailscale version | head -1)" + + # Tailscale connected + if ! tailscale status &>/dev/null; then + die "Tailscale is not connected. Run 'tailscale up' first." + fi + ok "Tailscale: connected" + + # BlueBubbles running on expected port + if ! nc -z localhost "$BB_PORT" 2>/dev/null; then + die "Cannot reach BlueBubbles Server on localhost:${BB_PORT}.\nPlease open BlueBubbles Server and ensure it is running." + fi + ok "BlueBubbles Server: listening on port ${BB_PORT}" +} + +# ============================================================================= +# STEP 2 — Install bbctl +# ============================================================================= +install_bbctl() { + sep + info "Checking bbctl (Beeper Bridge Manager)..." + + if [[ -x "$BBCTL_BIN" ]] && ! $RESET; then + ok "bbctl already installed: $("$BBCTL_BIN" version 2>/dev/null | head -1 || echo 'unknown version')" + return + fi + + info "Installing latest bbctl binary..." + mkdir -p "$(dirname "$BBCTL_BIN")" + + # Builds are hosted on nightly.link as zips. + # uname -p returns "arm" on Apple Silicon, "i386" on Intel. + local arch_tag + [[ "$(uname -p)" == "arm" ]] && arch_tag="arm64" || arch_tag="amd64" + + local zip_url="https://nightly.link/beeper/bridge-manager/workflows/go.yaml/main/bbctl-macos-${arch_tag}.zip" + local tmp_zip + tmp_zip="$(mktemp /tmp/bbctl-XXXXXX.zip)" + + info "Downloading from: $zip_url" + curl -fsSL "$zip_url" -o "$tmp_zip" \ + || die "Failed to download bbctl. Check your internet connection." + + # Unzip the single binary out of the archive + unzip -o "$tmp_zip" "bbctl-macos-${arch_tag}" -d "$(dirname "$BBCTL_BIN")" \ + || die "Failed to unzip bbctl." + mv "$(dirname "$BBCTL_BIN")/bbctl-macos-${arch_tag}" "$BBCTL_BIN" + rm -f "$tmp_zip" + + chmod +x "$BBCTL_BIN" + # macOS Gatekeeper: clear quarantine flag if present + xattr -d com.apple.quarantine "$BBCTL_BIN" 2>/dev/null || true + ok "bbctl installed → $BBCTL_BIN" + + # Ensure ~/.local/bin is on PATH for this session + export PATH="$HOME/.local/bin:$PATH" + + # Remind user to persist this + if ! grep -q 'local/bin' "${HOME}/.zshrc" 2>/dev/null && \ + ! grep -q 'local/bin' "${HOME}/.bashrc" 2>/dev/null; then + warn "Add ~/.local/bin to your shell PATH:" + warn " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.zshrc" + fi +} + +# ============================================================================= +# STEP 3 — Login to Beeper via bbctl +# ============================================================================= +login_bbctl() { + sep + info "Verifying Beeper login..." + + if "$BBCTL_BIN" whoami &>/dev/null; then + local user + user="$("$BBCTL_BIN" whoami 2>/dev/null | head -1)" + ok "Logged in to Beeper as: ${user}" + return + fi + + info "Opening Beeper login (browser)..." + "$BBCTL_BIN" login \ + || die "Beeper login failed. Ensure you have a Beeper account and retry." + ok "Logged in to Beeper" +} + +# ============================================================================= +# STEP 4 — Configure Tailscale Serve +# ============================================================================= +setup_tailscale_serve() { + sep + info "Configuring Tailscale Serve (HTTPS → localhost:${BB_PORT})..." + + # Tear down any existing serve on this port to avoid conflicts + tailscale serve --https=443 off 2>/dev/null || true + + # Set up HTTPS serve: external HTTPS:443 → local HTTP:BB_PORT + # Try without sudo first (standalone install), then with sudo (system install) + if ! tailscale serve --bg "${BB_PORT}" 2>/dev/null; then + if ! sudo tailscale serve --bg "${BB_PORT}" 2>/dev/null; then + # Older tailscale syntax fallback + tailscale serve --bg "http://localhost:${BB_PORT}" 2>/dev/null \ + || sudo tailscale serve --bg "http://localhost:${BB_PORT}" 2>/dev/null \ + || die "Could not configure Tailscale Serve. Ensure Tailscale is up to date." + fi + fi + + sleep 2 # give serve a moment to initialise + + # Derive the HTTPS URL from tailscale status + TAILSCALE_URL="$(_get_tailscale_url)" + ok "Tailscale Serve URL: ${TAILSCALE_URL}" +} + +_get_tailscale_url() { + # Parse the machine's MagicDNS name from tailscale status JSON + # Strips trailing dot, produces: https://hostname.tailnet.ts.net + python3 - <<'PYEOF' +import subprocess, json, sys +try: + raw = subprocess.check_output(["tailscale", "status", "--json"]) + d = json.loads(raw) + dns = d["Self"]["DNSName"].rstrip(".") + print(f"https://{dns}") +except Exception as e: + print("", file=sys.stderr) + sys.exit(1) +PYEOF +} + +# ============================================================================= +# STEP 5 — Collect BlueBubbles password +# ============================================================================= +get_bb_password() { + sep + info "BlueBubbles Server password..." + + # Load from existing config unless --reset + if [[ -f "$CONFIG_FILE" ]] && ! $RESET; then + # shellcheck source=/dev/null + source "$CONFIG_FILE" + if [[ -n "${BB_PASSWORD:-}" ]]; then + ok "Using saved password (run with --reset to change)" + return + fi + fi + + printf "${BOLD}Enter your BlueBubbles Server password:${N} " + read -rs BB_PASSWORD + echo + [[ -n "$BB_PASSWORD" ]] || die "Password cannot be empty." + ok "Password accepted" +} + +# ============================================================================= +# STEP 6 — Save config +# ============================================================================= +save_config() { + sep + info "Saving configuration..." + + mkdir -p "$CONFIG_DIR" + # Write config safely — use printf for the password so special characters + # (especially @) are never interpreted by the heredoc or shell expansion. + { + printf '# ── bb-beeper bridge config ──────────────────────────────\n' + printf '# Generated by setup-tailscale-serve.sh v%s on %s\n' "$SCRIPT_VERSION" "$(date)" + printf '# Re-run setup-tailscale-serve.sh to update.\n\n' + printf 'BB_LOCAL_URL=%s\n' "http://localhost:${BB_PORT}" + printf 'TAILSCALE_URL=%s\n' "${TAILSCALE_URL}" + printf 'BB_PORT=%s\n' "${BB_PORT}" + printf 'BBCTL_BIN=%s\n' "${BBCTL_BIN}" + # Password last — printf %q safely shell-quotes any special characters + printf 'BB_PASSWORD=%q\n' "${BB_PASSWORD}" + } > "$CONFIG_FILE" + chmod 600 "$CONFIG_FILE" + ok "Config → ${CONFIG_FILE}" +} + +# ============================================================================= +# STEP 7 — Create bridge runner script +# ============================================================================= +create_runner() { + sep + info "Creating bridge runner script..." + + mkdir -p "$CONFIG_DIR" + mkdir -p "$(dirname "$LOG_FILE")" + + cat > "$BRIDGE_RUNNER" <&1 | sed "s/^/\$LOG_PREFIX [diag] /" || \\ + echo "\$LOG_PREFIX [diag] ERROR: bbctl whoami failed (may not be logged in)" + +# ── Diagnostic: Tailscale status ────────────────────────────────────────────── +echo "\$LOG_PREFIX [diag] Tailscale status:" +if command -v tailscale &>/dev/null; then + tailscale status 2>&1 | head -5 | sed "s/^/\$LOG_PREFIX [diag] /" || \\ + echo "\$LOG_PREFIX [diag] ERROR: tailscale status failed" + echo "\$LOG_PREFIX [diag] Tailscale serve status:" + tailscale serve status 2>&1 | sed "s/^/\$LOG_PREFIX [diag] /" || \\ + echo "\$LOG_PREFIX [diag] ERROR: tailscale serve status failed" +else + echo "\$LOG_PREFIX [diag] tailscale not found in PATH" +fi + +# ── Diagnostic: BlueBubbles connectivity ────────────────────────────────────── +echo "\$LOG_PREFIX [diag] BlueBubbles health check (\${BB_LOCAL_URL}):" +if curl -sf --max-time 5 "\${BB_LOCAL_URL}/api/v1/server/info?password=\${BB_PASSWORD}" \\ + -o /dev/null -w "HTTP %{http_code}\n" 2>&1 \\ + | sed "s/^/\$LOG_PREFIX [diag] /"; then + echo "\$LOG_PREFIX [diag] BlueBubbles reachable" +else + echo "\$LOG_PREFIX [diag] WARNING: BlueBubbles health check failed" +fi + +echo "\$LOG_PREFIX [diag] Done. Handing off to bbctl run..." +echo "\$LOG_PREFIX ============================================" + +exec "\${BBCTL_BIN}" run \\ + --param "bluebubbles_url=\${BB_LOCAL_URL}" \\ + --param "bluebubbles_password=\${BB_PASSWORD}" \\ + --param "imessage_platform=bluebubbles" \\ + sh-imessage +RUNNER + + chmod +x "$BRIDGE_RUNNER" + ok "Runner → ${BRIDGE_RUNNER}" +} + +# ============================================================================= +# STEP 8 — LaunchAgent (auto-start + keep-alive) +# ============================================================================= +setup_launch_agent() { + sep + info "Installing LaunchAgent (auto-start at login, keep-alive)..." + + # Gracefully unload if already running + if launchctl list 2>/dev/null | grep -q "$LAUNCH_LABEL"; then + launchctl unload "$LAUNCH_PLIST" 2>/dev/null || true + info "Unloaded existing LaunchAgent" + fi + + mkdir -p "$HOME/Library/LaunchAgents" + + cat > "$LAUNCH_PLIST" < + + + + + Label + ${LAUNCH_LABEL} + + + ProgramArguments + + /bin/bash + ${BRIDGE_RUNNER} + + + + RunAtLoad + + + + KeepAlive + + + + ThrottleInterval + 30 + + + StandardOutPath + ${LOG_FILE} + StandardErrorPath + ${LOG_FILE} + + + EnvironmentVariables + + HOME + ${HOME} + PATH + /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:${HOME}/.local/bin + LANG + en_US.UTF-8 + + + + +PLIST + + launchctl load "$LAUNCH_PLIST" + ok "LaunchAgent loaded: ${LAUNCH_LABEL}" +} + +# ============================================================================= +# STEP 9 — Smoke test +# ============================================================================= +smoke_test() { + sep + info "Waiting for bridge to start (15 s)..." + sleep 15 + + if launchctl list 2>/dev/null | grep -q "$LAUNCH_LABEL"; then + ok "LaunchAgent is registered with launchd" + else + warn "LaunchAgent not found in launchctl list — check logs." + fi + + if [[ -f "$LOG_FILE" ]]; then + info "Last 5 log lines:" + tail -5 "$LOG_FILE" | sed 's/^/ /' + fi +} + +# ============================================================================= +# Summary +# ============================================================================= +print_summary() { + echo + printf "${BOLD}${G}╔══════════════════════════════════════════════╗${N}\n" + printf "${BOLD}${G}║ BlueBubbles + Beeper bridge is running! ║${N}\n" + printf "${BOLD}${G}╚══════════════════════════════════════════════╝${N}\n" + echo + printf " ${BOLD}Tailscale Serve URL${N} %s\n" "$TAILSCALE_URL" + printf " ${BOLD}Bridge type${N} sh-imessage (BlueBubbles connector)\n" + echo + printf " ${BOLD}Config file${N} %s\n" "$CONFIG_FILE" + printf " ${BOLD}Bridge runner${N} %s\n" "$BRIDGE_RUNNER" + printf " ${BOLD}LaunchAgent plist${N} %s\n" "$LAUNCH_PLIST" + printf " ${BOLD}Log file${N} %s\n" "$LOG_FILE" + echo + printf " ${BOLD}Useful commands${N}\n" + printf " Tail logs: tail -f %s\n" "$LOG_FILE" + printf " Stop bridge: launchctl unload %s\n" "$LAUNCH_PLIST" + printf " Start bridge: launchctl load %s\n" "$LAUNCH_PLIST" + printf " Status check: bash debug-status.sh\n" + printf " Reconfigure: bash setup-tailscale-serve.sh\n" + printf " Force reset: bash setup-tailscale-serve.sh --reset\n" + printf " Uninstall: bash uninstall.sh\n" + echo + printf " ${BOLD}${Y}Tip:${N} Open Beeper and your iMessage conversations\n" + printf " should appear within a minute or two.\n" + echo +} + +# ============================================================================= +# Main +# ============================================================================= +main() { + echo + printf "${BOLD}${B} BlueBubbles + Beeper + Tailscale Serve Setup${N}\n" + printf " v%s — based on github.com/ngencokamin/sh-imessage-setup\n" "$SCRIPT_VERSION" + echo + + check_prereqs + install_bbctl + login_bbctl + setup_tailscale_serve + get_bb_password + save_config + create_runner + setup_launch_agent + smoke_test + print_summary +} + +main "$@" diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..24a1a24 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# ============================================================================= +# uninstall-bb-beeper.sh +# +# Cleanly removes the BlueBubbles + Beeper bridge setup created by +# setup-bb-beeper.sh — including the LaunchAgent, config, runner script, +# and Tailscale Serve configuration. +# +# bbctl itself is NOT removed (you may use it for other bridges). +# ============================================================================= + +set -euo pipefail + +# ── Constants (must match setup-bb-beeper.sh) ────────────────────────────────── +readonly CONFIG_DIR="$HOME/.config/bb-beeper" +readonly LOG_FILE="$HOME/Library/Logs/bb-beeper-bridge.log" +readonly LAUNCH_LABEL="com.user.bb-beeper-bridge" +readonly LAUNCH_PLIST="$HOME/Library/LaunchAgents/${LAUNCH_LABEL}.plist" +readonly BB_PORT=1234 + +# ── Colours ──────────────────────────────────────────────────────────────────── +R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'; B='\033[0;34m' +BOLD='\033[1m'; N='\033[0m' +info() { printf "${B}[→]${N} %s\n" "$*"; } +ok() { printf "${G}[✓]${N} %s\n" "$*"; } +warn() { printf "${Y}[!]${N} %s\n" "$*"; } + +# ── Confirm ──────────────────────────────────────────────────────────────────── +echo +printf "${BOLD}${R} Uninstall BlueBubbles + Beeper bridge?${N}\n" +printf " This will stop the bridge and remove its config files.\n\n" +printf "${BOLD}Continue? [y/N]:${N} " +read -r confirm +[[ "${confirm,,}" == "y" ]] || { echo "Aborted."; exit 0; } +echo + +# ── 1. Stop & unload LaunchAgent ────────────────────────────────────────────── +info "Stopping LaunchAgent..." +if launchctl list 2>/dev/null | grep -q "$LAUNCH_LABEL"; then + launchctl unload "$LAUNCH_PLIST" 2>/dev/null && ok "LaunchAgent unloaded" \ + || warn "Could not unload LaunchAgent (may already be stopped)" +else + warn "LaunchAgent not currently loaded — skipping" +fi + +if [[ -f "$LAUNCH_PLIST" ]]; then + rm -f "$LAUNCH_PLIST" + ok "Removed: $LAUNCH_PLIST" +fi + +# ── 2. Remove Tailscale Serve config ────────────────────────────────────────── +info "Removing Tailscale Serve on port ${BB_PORT}..." +tailscale serve --https=443 off 2>/dev/null \ + || sudo tailscale serve --https=443 off 2>/dev/null \ + || warn "Could not remove Tailscale Serve (may already be inactive)" +ok "Tailscale Serve removed" + +# ── 3. Remove config directory ──────────────────────────────────────────────── +if [[ -d "$CONFIG_DIR" ]]; then + rm -rf "$CONFIG_DIR" + ok "Removed: $CONFIG_DIR" +fi + +# ── 4. Optionally remove log file ───────────────────────────────────────────── +if [[ -f "$LOG_FILE" ]]; then + printf "${BOLD}Remove log file %s? [y/N]:${N} " "$LOG_FILE" + read -r rm_logs + if [[ "${rm_logs,,}" == "y" ]]; then + rm -f "$LOG_FILE" + ok "Removed: $LOG_FILE" + else + warn "Keeping log file: $LOG_FILE" + fi +fi + +echo +printf "${BOLD}${G} Uninstall complete.${N}\n" +printf " Note: bbctl itself was not removed (~/.local/bin/bbctl).\n" +printf " To also remove it: rm ~/.local/bin/bbctl\n\n"