diff --git a/src/index.ts b/src/index.ts index 9551e0a..e47f4ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,10 +11,12 @@ const PORT = Number(process.env.PORT || 3000); const MAX_EVENT_AGE_SEC = Number(process.env.MAX_EVENT_AGE_SEC || 300); if (!BOT_TOKEN || !APP_ID || !PUBLIC_KEY || !ADMIN_KEY) { - console.error("Missing env: BOT_TOKEN, APP_ID, PUBLIC_KEY, ADMIN_KEY"); + console.error("[BOOT] Missing env: BOT_TOKEN, APP_ID, PUBLIC_KEY, ADMIN_KEY"); process.exit(1); } +console.log("[BOOT] Starting Discord Audit Bot…"); + /** ====== Helpers ====== */ const API = "https://discord.com/api/v10"; @@ -29,18 +31,24 @@ const hexToUint8 = (hex: string) => { async function verifySignature(req: Request, rawBody: string) { const sig = req.headers.get("X-Signature-Ed25519") || ""; const ts = req.headers.get("X-Signature-Timestamp") || ""; - if (!sig || !ts) return false; + if (!sig || !ts) { + console.warn("[VERIFY] Missing signature or timestamp header"); + return false; + } const msg = new TextEncoder().encode(ts + rawBody); const sigBin = hexToUint8(sig); const pub = hexToUint8(PUBLIC_KEY); - return nacl.sign.detached.verify(msg, sigBin, pub); + const ok = nacl.sign.detached.verify(msg, sigBin, pub); + if (!ok) console.warn("[VERIFY] Invalid Discord signature"); + return ok; } const snowflakeToMs = (id: string) => Number((BigInt(id) >> BigInt(22)) + BigInt(1420070400000)); // Discord epoch async function discordFetch(path: string, init?: RequestInit) { - return fetch(`${API}${path}`, { + console.log(`[DISCORD] → ${init?.method || "GET"} ${path}`); + const res = await fetch(`${API}${path}`, { ...init, headers: { Authorization: `Bot ${BOT_TOKEN}`, @@ -48,9 +56,12 @@ async function discordFetch(path: string, init?: RequestInit) { ...(init?.headers || {}), }, }); + console.log(`[DISCORD] ← ${res.status} ${path}`); + return res; } async function interactionCallback(id: string, token: string, body: any) { + console.log(`[INTERACTION] Responding to ${id}`); return fetch(`${API}/interactions/${id}/${token}/callback`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -61,18 +72,23 @@ async function interactionCallback(id: string, token: string, body: any) { /** Find most recent MEMBER_UPDATE with deaf=true within window */ function findRecentDeafen(audit: any): { entry: any } | null { const entries = audit?.audit_log_entries ?? []; + console.log(`[AUDIT] Fetched ${entries.length} entries`); for (const e of entries) { if (e.action_type !== 24) continue; // MEMBER_UPDATE const changes = e.changes || []; const deafChange = changes.find((c: any) => c.key === "deaf"); if (!deafChange || deafChange.new_value !== true) continue; const ageSec = (Date.now() - snowflakeToMs(e.id)) / 1000; + console.log( + `[AUDIT] Found deafen event: executor=${e.user_id}, target=${e.target_id}, age=${ageSec}s` + ); if (ageSec <= MAX_EVENT_AGE_SEC) return { entry: e }; } return null; } async function createPoll(channelId: string, content: string) { + console.log("[POLL] Creating poll"); const body = { content, poll: { @@ -90,7 +106,9 @@ async function createPoll(channelId: string, content: string) { body: JSON.stringify(body), }); if (!r.ok) { - // Fallback to message + reactions if Poll API not available + console.warn( + `[POLL] Poll creation failed (${r.status}), falling back to text + reactions` + ); await discordFetch(`/channels/${channelId}/messages`, { method: "POST", body: JSON.stringify({ @@ -102,6 +120,7 @@ async function createPoll(channelId: string, content: string) { } async function sendMessage(channelId: string, content: string) { + console.log(`[MSG] Sending message to channel ${channelId}`); await discordFetch(`/channels/${channelId}/messages`, { method: "POST", body: JSON.stringify({ content }), @@ -116,9 +135,12 @@ app.get("/healthz", (c) => c.text("ok")); /** Register or upsert the /checkaudit command for a guild */ app.post("/admin/register", async (c) => { - if (c.req.header("X-Admin-Key") !== ADMIN_KEY) + if (c.req.header("X-Admin-Key") !== ADMIN_KEY) { + console.warn("[ADMIN] Unauthorized attempt to register command"); return c.text("forbidden", 403); + } const { guildId = DEFAULT_GUILD_ID } = await c.req.json().catch(() => ({})); + console.log(`[ADMIN] Registering /checkaudit for guild ${guildId}`); const body = { name: "checkaudit", description: "Scan recent audit logs for deafen events", @@ -132,6 +154,7 @@ app.post("/admin/register", async (c) => { } ); const j = await r.json().catch(() => ({})); + console.log(`[ADMIN] Registration status=${r.status}`, j); return c.json({ ok: r.ok, status: r.status, resp: j }); }); @@ -145,8 +168,13 @@ app.post("/interactions", async (c) => { const interaction = JSON.parse(raw); const { type } = interaction; + console.log(`[INTERACTION] type=${type}`); + // Ping - if (type === 1) return c.json({ type: 1 }); + if (type === 1) { + console.log("[INTERACTION] Received PING"); + return c.json({ type: 1 }); + } // Application Command if (type === 2) { @@ -155,19 +183,24 @@ app.post("/interactions", async (c) => { const guildId = interaction.guild_id as string; const channelId = interaction.channel_id as string; - // Immediate ephemeral ack to avoid 3s timeout + console.log( + `[INTERACTION] Slash command invoked: guild=${guildId}, channel=${channelId}, id=${id}` + ); + + // Immediate ephemeral ack await interactionCallback(id, token, { - type: 4, // CHANNEL_MESSAGE_WITH_SOURCE - data: { content: "Scanning audit logs…", flags: 64 }, // ephemeral + type: 4, + data: { content: "Scanning audit logs…", flags: 64 }, }); // Background work queueMicrotask(async () => { - // Fetch recent MEMBER_UPDATE logs + console.log("[WORK] Fetching audit logs…"); const logsResp = await discordFetch( `/guilds/${guildId}/audit-logs?action_type=24&limit=10` ); if (!logsResp.ok) { + console.error("[WORK] Failed to fetch audit logs", logsResp.status); await sendMessage(channelId, "Failed to read audit logs."); return; } @@ -175,6 +208,7 @@ app.post("/interactions", async (c) => { const hit = findRecentDeafen(logs); if (!hit) { + console.log("[WORK] No deafen event found in recent logs"); await sendMessage(channelId, "No recent deafen event found."); return; } @@ -183,17 +217,18 @@ app.post("/interactions", async (c) => { const executorId = e.user_id; const targetId = e.target_id; - // Create poll + console.log( + `[WORK] Deafen event confirmed. Executor=${executorId}, Target=${targetId}` + ); + await createPoll( channelId, `Detected deafen event. Executor: <@${executorId}> → Target: <@${targetId}>` ); - // Taunt await sendMessage(channelId, "Justice incoming."); }); - // We already responded above return c.body(null, 204); }