#!/usr/bin/env bash # discord-dm.sh — send a Discord message to an org member (DM) or a team channel, # using the ClaudeTools bot. Built for copy-paste-friendly delivery of wrapped # command lines (consent links, long one-liners) straight to a person's DMs. # # Usage: # discord-dm.sh "message text" # echo "message text" | discord-dm.sh # discord-dm.sh list # show known users + channels # # is one of: # - a user name: mike | howard | rob | winter -> opens a DM # - a channel: #bot-alerts | #dev-alerts | bot | dev -> posts to the channel # - a raw snowflake: 17-19 digit ID (treated as a USER DM; prefix chan: for a channel) # # Token resolution (mirrors post-bot-alert.sh, first hit wins): # 1. SOPS vault: projects/discord-bot/bot-token.sops.yaml field credentials.bot_token # 2. projects/discord-bot/.env key DISCORD_TOKEN # # Exit codes: 0 success; 1 usage/arg error; 2 token missing; 3 Discord API error. # Unlike post-bot-alert.sh this does NOT soft-fail — a DM is usually load-bearing # (you asked for it on purpose), so a failure is surfaced with a non-zero exit. set -u ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" API="https://discord.com/api/v10" UA="ClaudeToolsBot (claudetools, 1.0)" # --- prepopulated org directory (from .claude/users.json + post-bot-alert.sh) --- declare -A USERS=( [mike]="264814939619721216" [howard]="624667664501178379" [rob]="261978810713505792" [winter]="624666486362996755" ) declare -A CHANNELS=( [bot-alerts]="624710699771232265" # whole team (Syncro + general) [dev-alerts]="1509998508198068484" # private RMM/Dev (Howard + Mike) ) print_dir() { echo "Users (DM):" for u in "${!USERS[@]}"; do printf " %-8s %s\n" "$u" "${USERS[$u]}"; done echo "Channels:" for c in "${!CHANNELS[@]}"; do printf " #%-12s %s\n" "$c" "${CHANNELS[$c]}"; done } RECIPIENT="${1:-}" if [ -z "$RECIPIENT" ] || [ "$RECIPIENT" = "-h" ] || [ "$RECIPIENT" = "--help" ]; then sed -n '2,30p' "$0"; exit 1 fi if [ "$RECIPIENT" = "list" ]; then print_dir; exit 0; fi shift # --- message (remaining args, else stdin) --- if [ "$#" -gt 0 ]; then MSG="$*"; elif [ ! -t 0 ]; then MSG="$(cat)"; else MSG=""; fi if [ -z "$MSG" ]; then echo "[ERROR] empty message — nothing sent" >&2; exit 1; fi # --- resolve recipient -> mode (dm|channel) + target id --- MODE=""; TARGET=""; LABEL="" key="$(printf '%s' "$RECIPIENT" | tr '[:upper:]' '[:lower:]')" case "$key" in bot|bot-alerts|"#bot-alerts") MODE=channel; TARGET="${CHANNELS[bot-alerts]}"; LABEL="#bot-alerts" ;; dev|dev-alerts|"#dev-alerts") MODE=channel; TARGET="${CHANNELS[dev-alerts]}"; LABEL="#dev-alerts" ;; chan:*) MODE=channel; TARGET="${key#chan:}"; LABEL="channel ${TARGET}" ;; \#*) c="${key#\#}"; if [ -n "${CHANNELS[$c]:-}" ]; then MODE=channel; TARGET="${CHANNELS[$c]}"; LABEL="#$c"; else echo "[ERROR] unknown channel: $RECIPIENT (try: discord-dm.sh list)" >&2; exit 1; fi ;; *) if [ -n "${USERS[$key]:-}" ]; then MODE=dm; TARGET="${USERS[$key]}"; LABEL="$key (DM)" elif printf '%s' "$key" | grep -qE '^[0-9]{17,19}$'; then MODE=dm; TARGET="$key"; LABEL="user ${key} (DM)" else echo "[ERROR] unknown recipient: $RECIPIENT (try: discord-dm.sh list)" >&2; exit 1; fi ;; esac # --- bot token: vault first, then .env --- TOKEN="$(bash "$ROOT/.claude/scripts/vault.sh" get-field \ projects/discord-bot/bot-token.sops.yaml credentials.bot_token 2>/dev/null)" if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then ENV_FILE="$ROOT/projects/discord-bot/.env" [ -f "$ENV_FILE" ] && TOKEN="$(grep -iE '^[[:space:]]*DISCORD_TOKEN[[:space:]]*=' "$ENV_FILE" | head -1 | sed -E 's/^[^=]*=[[:space:]]*//; s/^["'"'"']//; s/["'"'"'][[:space:]]*$//')" fi if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then echo "[ERROR] no bot token (vault + .env both empty)" >&2 bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "no Discord bot token (vault projects/discord-bot/bot-token + .env both empty)" >/dev/null 2>&1 exit 2 fi auth=(-H "Authorization: Bot ${TOKEN}" -H "Content-Type: application/json" -H "User-Agent: ${UA}") # --- for a DM, open (or fetch) the user's DM channel --- if [ "$MODE" = "dm" ]; then DM="$(printf '%s' "$(jq -nc --arg r "$TARGET" '{recipient_id:$r}')" | \ curl -s -m 15 "${auth[@]}" -X POST "$API/users/@me/channels" --data-binary @-)" CHID="$(printf '%s' "$DM" | jq -r '.id // empty' 2>/dev/null)" if [ -z "$CHID" ]; then echo "[ERROR] could not open DM channel for $LABEL: $DM" >&2 bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "failed to open DM channel for $LABEL" --context "resp=${DM:0:80}" >/dev/null 2>&1 exit 3 fi TARGET="$CHID" fi # --- post the message (printf | --data-binary @- — direct -d mangles multiline JSON) --- RESP="$(printf '%s' "$(jq -nc --arg c "$MSG" '{content:$c}')" | \ curl -s -m 15 -w $'\n%{http_code}' "${auth[@]}" \ -X POST "$API/channels/${TARGET}/messages" --data-binary @-)" HTTP="$(printf '%s' "$RESP" | tail -n1)" BODY="$(printf '%s' "$RESP" | sed '$d')" if [ "$HTTP" = "200" ]; then echo "[OK] discord-dm: sent to ${LABEL} (message_id=$(printf '%s' "$BODY" | jq -r '.id // empty'))" exit 0 fi echo "[ERROR] discord-dm: Discord returned ${HTTP:-no-response} — ${BODY}" >&2 bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "Discord send to $LABEL failed" --context "http=${HTTP:-none} resp=${BODY:0:80}" >/dev/null 2>&1 exit 3