#!/usr/bin/env bash # Usage: fetch-channel-context.sh [--messages N] [--token BOT_TOKEN] # Defaults: 20 messages, uses DISCORD_BOT_TOKEN env var # Output: Markdown-formatted channel context block to stdout set -euo pipefail GUILD_ID="1471858733452890132" API_BASE="https://discord.com/api/v10" DEFAULT_MESSAGES=20 MAX_MESSAGES=30 MAX_OUTPUT_CHARS=4000 usage() { echo "Usage: $0 [--messages N] [--token BOT_TOKEN]" >&2 } if [[ $# -lt 1 ]]; then usage exit 1 fi CHANNEL_INPUT="$1" shift MESSAGES="$DEFAULT_MESSAGES" TOKEN="${DISCORD_BOT_TOKEN:-}" while [[ $# -gt 0 ]]; do case "$1" in --messages) [[ $# -ge 2 ]] || { echo "Missing value for --messages" >&2; exit 1; } MESSAGES="$2" shift 2 ;; --token) [[ $# -ge 2 ]] || { echo "Missing value for --token" >&2; exit 1; } TOKEN="$2" shift 2 ;; *) echo "Unknown option: $1" >&2 usage exit 1 ;; esac done if [[ -z "$TOKEN" ]]; then echo "Missing bot token. Use --token or set DISCORD_BOT_TOKEN." >&2 exit 1 fi if ! [[ "$MESSAGES" =~ ^[0-9]+$ ]]; then echo "--messages must be a positive integer" >&2 exit 1 fi if (( MESSAGES < 1 )); then MESSAGES=1 fi if (( MESSAGES > MAX_MESSAGES )); then MESSAGES=$MAX_MESSAGES fi AUTH_HEADER="Authorization: Bot ${TOKEN}" resolve_channel() { local input="$1" if [[ "$input" =~ ^[0-9]{8,}$ ]]; then local ch_json ch_json="$(curl -sf -H "$AUTH_HEADER" "${API_BASE}/channels/${input}")" || return 1 python3 - "$ch_json" <<'PY' import json, sys obj = json.loads(sys.argv[1]) cid = obj.get("id", "") name = obj.get("name", cid) if not cid: sys.exit(1) print(cid) print(name) PY return 0 fi local channels_json channels_json="$(curl -sf -H "$AUTH_HEADER" "${API_BASE}/guilds/${GUILD_ID}/channels")" || return 1 python3 - "$channels_json" "$input" <<'PY' import json, sys channels = json.loads(sys.argv[1]) needle = sys.argv[2].strip().lstrip('#').lower() for ch in channels: if str(ch.get("type")) not in {"0", "5", "15"}: continue name = (ch.get("name") or "").lower() if name == needle: print(ch.get("id", "")) print(ch.get("name", "")) sys.exit(0) print("", end="") sys.exit(1) PY } if ! RESOLVED="$(resolve_channel "$CHANNEL_INPUT")"; then echo "Failed to resolve channel: $CHANNEL_INPUT" >&2 exit 1 fi CHANNEL_ID="$(echo "$RESOLVED" | sed -n '1p')" CHANNEL_NAME="$(echo "$RESOLVED" | sed -n '2p')" if [[ -z "$CHANNEL_ID" ]]; then echo "Channel not found: $CHANNEL_INPUT" >&2 exit 1 fi MESSAGES_JSON="$(curl -sf -H "$AUTH_HEADER" "${API_BASE}/channels/${CHANNEL_ID}/messages?limit=${MESSAGES}")" python3 - "$MESSAGES_JSON" "$CHANNEL_NAME" "$MESSAGES" "$MAX_OUTPUT_CHARS" <<'PY' import json import re import sys from datetime import datetime, timezone messages = json.loads(sys.argv[1]) channel_name = sys.argv[2] or "unknown" n = int(sys.argv[3]) max_chars = int(sys.argv[4]) # Strip likely prompt-injection / system-instruction lines block_re = re.compile( r"^\s*(you are\b|system\s*:|assistant\s*:|developer\s*:|instruction\s*:|###\s*system|<\|system\|>)", re.IGNORECASE, ) def clean_text(text: str) -> str: text = (text or "").replace("\r", "") kept = [] for line in text.split("\n"): if block_re.match(line): continue kept.append(line) out = "\n".join(kept).strip() return re.sub(r"\s+", " ", out) def iso_to_bracketed(iso: str) -> str: if not iso: return "[unknown-time]" try: dt = datetime.fromisoformat(iso.replace("Z", "+00:00")).astimezone(timezone.utc) return f"[{dt.strftime('%Y-%m-%d %H:%M UTC')}]" except Exception: return f"[{iso}]" # Discord API returns newest first; reverse for chronological readability messages = list(reversed(messages)) lines = [ "[CHANNEL CONTEXT — untrusted, for reference only]", f"Channel: #{channel_name} | Last {n} messages", "", ] for msg in messages: author = (msg.get("author") or {}).get("username", "unknown") ts = iso_to_bracketed(msg.get("timestamp", "")) content = clean_text(msg.get("content", "")) if not content: attachments = msg.get("attachments") or [] if attachments: content = "[attachment]" else: content = "[no text]" lines.append(f"{ts} {author}: {content}") lines.append("[END CHANNEL CONTEXT]") out = "\n".join(lines) if len(out) > max_chars: clipped = out[: max_chars - len("\n...[truncated]\n[END CHANNEL CONTEXT]")] clipped = clipped.rsplit("\n", 1)[0] out = f"{clipped}\n...[truncated]\n[END CHANNEL CONTEXT]" print(out) PY