# Shared Discord delivery for bin/vanguard-alerts.sh and bin/vanguard-deadman.sh. Sourced, never executed. # # . /usr/local/bin/vgnotify.sh # vg_post error "disk is nearly full" # 0 = delivered, 1 = not (no webhook, bad URL, or HTTP failure) # # WHY MENTIONS NEED allowed_mentions # Putting <@&ROLE_ID> in a webhook's content is not enough on its own: Discord will render it but will not # reliably ping, because a webhook has no permission to mention roles unless the request says which mentions # are allowed. So every post sends an explicit allowed_mentions. It also sets parse: [] always, which means # an @everyone appearing inside an alert's text (a log line, a filename) can never ping the whole server. # # ALERT_MENTION is written for humans rather than for Discord's syntax: # ALERT_MENTION='role:1387247501387628585,role:1566623319027880086' -> pings those roles # ALERT_MENTION='user:123456789' -> pings that person # Raw '<@&123>' / '<@123>' is accepted too and passed through unchanged. # # ALERT_MENTION_LEVELS decides which alarms are worth a ping; the default is errors only, so warnings still # arrive in the channel without waking anyone. VG_WEBHOOK_FILE="${ALERT_WEBHOOK_FILE:-/etc/vanguard/alert-webhook}" VG_MENTION="${ALERT_MENTION:-}" VG_MENTION_LEVELS="${ALERT_MENTION_LEVELS:-error}" # The webhook URL, or nothing. A value that is not https is treated as absent and complained about once: # posting to it would fail anyway, and silently "succeeding" would be worse. vg_webhook() { local url [[ -r $VG_WEBHOOK_FILE ]] || return 1 url=$(tr -d '[:space:]' < "$VG_WEBHOOK_FILE" 2>/dev/null) || return 1 case "$url" in https://*) printf '%s' "$url" ;; "") return 1 ;; *) echo "vgnotify: the configured webhook is not an https URL; ignoring it" >&2; return 1 ;; esac } vg_post() { local level="$1" text="$2" url payload code url=$(vg_webhook) || return 1 payload=$(VG_LEVEL="$level" VG_TEXT="$text" VG_MENTION="$VG_MENTION" VG_LEVELS="$VG_MENTION_LEVELS" python3 - <<'PY' import json, os, re level = os.environ.get("VG_LEVEL", "") text = os.environ.get("VG_TEXT", "") raw = os.environ.get("VG_MENTION", "").strip() levels = [l.strip() for l in os.environ.get("VG_LEVELS", "error").split(",") if l.strip()] users, roles, parts = [], [], [] for entry in [e.strip() for e in raw.replace(" ", ",").split(",") if e.strip()]: if entry.startswith("role:"): roles.append(entry[5:]); parts.append(f"<@&{entry[5:]}>") elif entry.startswith("user:"): users.append(entry[5:]); parts.append(f"<@{entry[5:]}>") else: # already Discord syntax parts.append(entry) roles += re.findall(r"<@&(\d+)>", entry) users += re.findall(r"<@!?(\d+)>", entry) ping = bool(parts) and level in levels prefix = (" ".join(parts) + " ") if ping else "" # parse: [] unconditionally — an @everyone inside alert text must never ping the server. body = {"content": (prefix + text)[:1900], "allowed_mentions": {"parse": [], "users": sorted(set(users)) if ping else [], "roles": sorted(set(roles)) if ping else []}} print(json.dumps(body)) PY ) code=$(curl -s -o /dev/null -w '%{http_code}' -m 15 -X POST \ -H 'Content-Type: application/json' -d "$payload" "$url" || true) case "${code:-000}" in 2*) return 0 ;; *) echo "vgnotify: webhook post failed, HTTP ${code:-000}" >&2; return 1 ;; esac }