From 596fadac827dbeb5d6170301421205dcf37cd940 Mon Sep 17 00:00:00 2001 From: Bruno Bornsztein Date: Tue, 9 Jun 2026 10:09:32 -0500 Subject: [PATCH] feat(slack): one-paste app manifest + guided `setup.sh slack` wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cuts Slack onboarding from a ~10-step manual checklist to: paste a manifest, copy two tokens, run one command, /invite the bot. - modules/slack/manifest.yaml — Slack app manifest. "Create New App → From an app manifest → paste" pre-sets Socket Mode, bot scopes, and the app_mention/message.im event subscriptions in one shot. - setup.sh `slack` mode — `./setup.sh slack `: prompts for the bot + app tokens, verifies the bot token (auth.test), resolves the operator's Slack user id from their email (users.lookupByEmail) or takes a member id, writes the SLACK_* block into that GM's config.env (idempotent; re-run to update), and installs the bridge on the right target (local / server / exe). Blank app token = outbound-only. - Refactors the local install into setup_slack_local() (shared by the local server path and the wizard). - README: manifest + wizard flow, manual path kept as a fallback. Wizard is non-interactive-testable via WIZ_ overrides. Verified: config block written + bridge installed; re-run updates in place (one block); manifest is valid YAML. Co-Authored-By: Claude Opus 4.8 --- modules/slack/README.md | 41 +++++--- modules/slack/manifest.yaml | 40 ++++++++ setup.sh | 183 ++++++++++++++++++++++++++++++------ 3 files changed, 225 insertions(+), 39 deletions(-) create mode 100644 modules/slack/manifest.yaml diff --git a/modules/slack/README.md b/modules/slack/README.md index 1d40c29..cf2d5fe 100644 --- a/modules/slack/README.md +++ b/modules/slack/README.md @@ -58,18 +58,35 @@ Because classification spends tokens, the bridge is bounded on every axis: ## Setup -1. Create a Slack app (https://api.slack.com/apps). - - **Socket Mode:** on. Generate an app-level token with `connections:write` - → `SLACK_APP_TOKEN` (`xapp-…`). - - **Bot token scopes:** `chat:write`, `app_mentions:read`, `im:history`, - `channels:read` → install to workspace → `SLACK_BOT_TOKEN` (`xoxb-…`). - - **Event Subscriptions:** subscribe to bot events `app_mention` and - `message.im`. - - Invite the bot to the channel(s) you want it to watch / post in. -2. Fill in the Slack section of `config.env` (see `config.example.env`). -3. `./setup.sh server ` (or `exe`). This installs the bridge to - `~/scripts/slack/`, writes `~/scripts/slack/.env`, and enables the - `ty-slack` systemd service. +1. **Create the app from the manifest.** https://api.slack.com/apps → *Create + New App* → *From an app manifest* → pick your workspace → paste + [`manifest.yaml`](manifest.yaml) → *Create*. (Pre-sets Socket Mode, the bot + scopes, and the event subscriptions — no manual toggles.) +2. **Mint two tokens** (a manifest can't): + - *Basic Information → App-Level Tokens → Generate* with `connections:write` + → `xapp-…` (`SLACK_APP_TOKEN`). + - *OAuth & Permissions → Install to Workspace* → copy the **Bot User OAuth + Token** `xoxb-…` (`SLACK_BOT_TOKEN`). +3. **Run the wizard** for your GM: + ```bash + ./setup.sh slack ~/Projects/gms/ + ``` + It takes the two tokens, resolves your Slack user from your email (via the + bot token), writes the `SLACK_*` block into that GM's `config.env`, and + installs + enables the bridge (systemd on a server/exe.dev; rendered files on + a local Mac). Leave the app token blank for **outbound-only** (pings, no + inbound control). +4. **Invite the bot** to your notify channel in Slack: `/invite @taskyou`. + +A few minutes, no hand-editing config or hunting for member IDs. + +
+Manual setup (without the wizard) + +Do steps 1–2, then put the `SLACK_*` block (see `config.example.env`) into the +GM's `config.env` yourself and run `./setup.sh server ` (or `exe`). +`SLACK_ALLOWED_USERS` takes Slack **member IDs** (profile → More → Copy member ID). +
## Config diff --git a/modules/slack/manifest.yaml b/modules/slack/manifest.yaml new file mode 100644 index 0000000..679559d --- /dev/null +++ b/modules/slack/manifest.yaml @@ -0,0 +1,40 @@ +# TaskYou Slack app manifest. +# Create the app in one step: https://api.slack.com/apps → Create New App → +# "From an app manifest" → pick your workspace → paste this file → Create. +# +# This pre-configures Socket Mode, the bot scopes, and the event subscriptions +# the bridge needs. Two things a manifest can't mint for you, so do them after: +# 1. Basic Information → App-Level Tokens → Generate (scope: connections:write) +# → this is your SLACK_APP_TOKEN (xapp-…). +# 2. OAuth & Permissions → Install to Workspace → copy the Bot User OAuth +# Token → this is your SLACK_BOT_TOKEN (xoxb-…). +# Then run: ./setup.sh slack +display_information: + name: TaskYou + description: Manage TaskYou tasks from Slack + background_color: "#1a1a1a" +features: + bot_user: + display_name: taskyou + always_online: true + app_home: + home_tab_enabled: false + messages_tab_enabled: true + messages_tab_read_only_enabled: false +oauth_config: + scopes: + bot: + - chat:write + - app_mentions:read + - im:history + - channels:read +settings: + event_subscriptions: + bot_events: + - app_mention + - message.im + interactivity: + is_enabled: false + org_deploy_enabled: false + socket_mode_enabled: true + token_rotation_enabled: false diff --git a/setup.sh b/setup.sh index c4436d8..ad0cc6c 100755 --- a/setup.sh +++ b/setup.sh @@ -20,6 +20,7 @@ usage() { echo " server — Provision the remote server only" echo " exe — Deploy GM to an exe.dev VM" echo " all — Both local and server" + echo " slack — Guided Slack setup wizard for an existing GM" echo "" echo "The project-dir must contain a config.env file." echo "See config.example.env for all available variables." @@ -195,8 +196,8 @@ MODE="$1" PROJECT_DIR="$(cd "$2" && pwd 2>/dev/null || echo "$2")" CONFIG_FILE="$PROJECT_DIR/config.env" -if [[ "$MODE" != "local" && "$MODE" != "server" && "$MODE" != "exe" && "$MODE" != "all" ]]; then - echo "Error: mode must be local, server, exe, or all" +if [[ "$MODE" != "local" && "$MODE" != "server" && "$MODE" != "exe" && "$MODE" != "all" && "$MODE" != "slack" ]]; then + echo "Error: mode must be local, server, exe, all, or slack" usage fi @@ -593,6 +594,155 @@ EOF fi } +# Install the Slack bridge when the daemon is on THIS machine (local/macOS): +# render the script + .env; no systemd (operator starts it or adds a launchd +# agent). $1 = home dir. +setup_slack_local() { + local home_dir="$1" + + log "Setting up Slack integration (local)" + local slack_dir="$home_dir/scripts/slack" + mkdir -p "$slack_dir" + cp "$MODULES_DIR/slack/slack-bridge.mjs" "$slack_dir/slack-bridge.mjs" + chmod +x "$slack_dir/slack-bridge.mjs" + + local project_map_json="$SLACK_PROJECT_MAP" + [[ -z "$project_map_json" ]] && project_map_json="{}" + local default_project + default_project=$(echo "$PROJECTS" | cut -d',' -f1 | xargs) + cat > "$slack_dir/.env" <) ─────────────────────── + +# Prompt for a value. Default shown is the current value (from config on a +# re-run) so pressing enter keeps it and typing a new value updates it. A +# WIZ_ env var pre-answers non-interactively (for tests/scripts) without +# colliding with the sourced config; a closed stdin keeps the current/default. +ask() { + local __var="$1" __prompt="$2" + local __override="WIZ_${__var}" + if [[ -n "${!__override:-}" ]]; then printf -v "$__var" '%s' "${!__override}"; return; fi + local __default="${!__var:-${3:-}}" __val + if [[ -t 0 ]]; then + read -r -p " $__prompt${__default:+ [$__default]}: " __val || true + printf -v "$__var" '%s' "${__val:-$__default}" + else + printf -v "$__var" '%s' "$__default" + fi +} + +# Resolve a Slack email → user id via the bot token; pass through a bare U… id. +resolve_slack_user() { + local token="$1" who="$2" + [[ -z "$who" ]] && { echo ""; return; } + if [[ "$who" != *@* ]]; then echo "$who"; return; fi + local resp + resp=$(curl -sS -G --data-urlencode "email=$who" \ + -H "Authorization: Bearer $token" \ + "https://slack.com/api/users.lookupByEmail" 2>/dev/null || true) + echo "$resp" | python3 -c 'import sys,json +try: + d=json.load(sys.stdin); print(d.get("user",{}).get("id","") if d.get("ok") else "") +except Exception: + print("")' 2>/dev/null || echo "" +} + +# Rewrite the SLACK_* block in the GM's config.env (idempotent). +write_slack_config() { + local tmp="$CONFIG_FILE.slacktmp" + # Strip any prior managed block (vars + our comment marker) so re-runs replace + # rather than accumulate. + grep -vE '^(export )?SLACK_(ENABLED|BOT_TOKEN|APP_TOKEN|NOTIFY_CHANNEL|ALLOWED_USERS|PROJECT_MAP)=|^# === Slack integration \(managed by' "$CONFIG_FILE" > "$tmp" || true + { + echo "" + echo "# === Slack integration (managed by 'setup.sh slack') ===" + echo "SLACK_ENABLED=\"true\"" + echo "SLACK_BOT_TOKEN=\"$SLACK_BOT_TOKEN\"" + echo "SLACK_APP_TOKEN=\"$SLACK_APP_TOKEN\"" + echo "SLACK_NOTIFY_CHANNEL=\"$SLACK_NOTIFY_CHANNEL\"" + echo "SLACK_ALLOWED_USERS=\"$SLACK_ALLOWED_USERS\"" + echo "SLACK_PROJECT_MAP='$SLACK_PROJECT_MAP'" + } >> "$tmp" + mv "$tmp" "$CONFIG_FILE" + ok "wrote Slack config to $CONFIG_FILE" +} + +setup_slack_wizard() { + echo "" + log "Slack setup wizard for $PROJECT_DISPLAY_NAME" + echo " 1. Create the app: https://api.slack.com/apps → Create New App →" + echo " From an app manifest → paste modules/slack/manifest.yaml → Create." + echo " 2. Basic Information → App-Level Tokens → Generate (scope connections:write)" + echo " → copy the xapp-… token. Then OAuth & Permissions → Install to Workspace" + echo " → copy the Bot User OAuth Token xoxb-…." + echo " (Leave the app token blank for outbound-only — pings, no inbound control.)" + echo "" + + ask SLACK_BOT_TOKEN "Bot User OAuth Token (xoxb-…)" + ask SLACK_APP_TOKEN "App-Level Token (xapp-…, blank = outbound-only)" + ask SLACK_NOTIFY_CHANNEL "Channel for task pings" "#taskyou" + ask SLACK_OWNER "Your Slack email or member ID (to allow-list)" + ask SLACK_PROJECT_MAP "Channel→project map JSON (optional)" "{}" + + if [[ -z "$SLACK_BOT_TOKEN" ]]; then + echo "Error: a bot token is required." + exit 1 + fi + + # Verify the bot token and resolve the allow-listed user. + local auth + auth=$(curl -sS -X POST -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + "https://slack.com/api/auth.test" 2>/dev/null || true) + if echo "$auth" | grep -q '"ok":true'; then + local team botname + team=$(echo "$auth" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("team",""))' 2>/dev/null || echo "") + botname=$(echo "$auth" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("user",""))' 2>/dev/null || echo "") + ok "bot @$botname authenticated in ${team:-workspace}" + else + warn "couldn't verify the bot token (auth.test). Continuing — fix it in $CONFIG_FILE if Slack errors." + fi + + SLACK_ALLOWED_USERS=$(resolve_slack_user "$SLACK_BOT_TOKEN" "$SLACK_OWNER") + if [[ -z "$SLACK_ALLOWED_USERS" ]]; then + warn "couldn't resolve '$SLACK_OWNER' to a Slack user id — set SLACK_ALLOWED_USERS in $CONFIG_FILE before anyone can drive ty." + else + ok "allow-listed: $SLACK_ALLOWED_USERS" + fi + + write_slack_config + export SLACK_ENABLED="true" SLACK_BOT_TOKEN SLACK_APP_TOKEN SLACK_NOTIFY_CHANNEL \ + SLACK_ALLOWED_USERS SLACK_PROJECT_MAP + + # Install on whichever target this GM uses. + if is_local_server; then + export SERVER_HOME="${SERVER_HOME:-$HOME}" + setup_slack_local "$SERVER_HOME" + elif [[ "$EXE_DEV_ENABLED" == "true" && -n "$EXE_DEV_VM_NAME" ]]; then + setup_slack_remote "exedev@${EXE_DEV_VM_NAME}.exe.xyz" "/home/exedev" + else + setup_slack_remote "$SERVER_HOST" "$SERVER_HOME" + fi + + echo "" + ok "Slack wired up. Final step: invite the bot in Slack → /invite @ in $SLACK_NOTIFY_CHANNEL" + echo " Then try: @ what's on the board?" +} + # ── Daemon systemd service ──────────────────────────────────────────────────── install_daemon_service() { @@ -717,31 +867,7 @@ setup_server_local() { # Slack module (local mode: render files + .env; no systemd on macOS, so the # operator starts it — directly or via a launchd agent). if [[ "$SLACK_ENABLED" == "true" ]]; then - log "Setting up Slack integration (local)" - local slack_dir="$home_dir/scripts/slack" - mkdir -p "$slack_dir" - cp "$MODULES_DIR/slack/slack-bridge.mjs" "$slack_dir/slack-bridge.mjs" - chmod +x "$slack_dir/slack-bridge.mjs" - - local project_map_json="$SLACK_PROJECT_MAP" - [[ -z "$project_map_json" ]] && project_map_json="{}" - local default_project - default_project=$(echo "$PROJECTS" | cut -d',' -f1 | xargs) - cat > "$slack_dir/.env" <