enhance logging and error handling for Discord Audit Bot
This commit is contained in:
parent
63f9607942
commit
4655614f0f
63
src/index.ts
63
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);
|
const MAX_EVENT_AGE_SEC = Number(process.env.MAX_EVENT_AGE_SEC || 300);
|
||||||
|
|
||||||
if (!BOT_TOKEN || !APP_ID || !PUBLIC_KEY || !ADMIN_KEY) {
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[BOOT] Starting Discord Audit Bot…");
|
||||||
|
|
||||||
/** ====== Helpers ====== */
|
/** ====== Helpers ====== */
|
||||||
const API = "https://discord.com/api/v10";
|
const API = "https://discord.com/api/v10";
|
||||||
|
|
||||||
@ -29,18 +31,24 @@ const hexToUint8 = (hex: string) => {
|
|||||||
async function verifySignature(req: Request, rawBody: string) {
|
async function verifySignature(req: Request, rawBody: string) {
|
||||||
const sig = req.headers.get("X-Signature-Ed25519") || "";
|
const sig = req.headers.get("X-Signature-Ed25519") || "";
|
||||||
const ts = req.headers.get("X-Signature-Timestamp") || "";
|
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 msg = new TextEncoder().encode(ts + rawBody);
|
||||||
const sigBin = hexToUint8(sig);
|
const sigBin = hexToUint8(sig);
|
||||||
const pub = hexToUint8(PUBLIC_KEY);
|
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) =>
|
const snowflakeToMs = (id: string) =>
|
||||||
Number((BigInt(id) >> BigInt(22)) + BigInt(1420070400000)); // Discord epoch
|
Number((BigInt(id) >> BigInt(22)) + BigInt(1420070400000)); // Discord epoch
|
||||||
|
|
||||||
async function discordFetch(path: string, init?: RequestInit) {
|
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,
|
...init,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bot ${BOT_TOKEN}`,
|
Authorization: `Bot ${BOT_TOKEN}`,
|
||||||
@ -48,9 +56,12 @@ async function discordFetch(path: string, init?: RequestInit) {
|
|||||||
...(init?.headers || {}),
|
...(init?.headers || {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log(`[DISCORD] ← ${res.status} ${path}`);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function interactionCallback(id: string, token: string, body: any) {
|
async function interactionCallback(id: string, token: string, body: any) {
|
||||||
|
console.log(`[INTERACTION] Responding to ${id}`);
|
||||||
return fetch(`${API}/interactions/${id}/${token}/callback`, {
|
return fetch(`${API}/interactions/${id}/${token}/callback`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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 */
|
/** Find most recent MEMBER_UPDATE with deaf=true within window */
|
||||||
function findRecentDeafen(audit: any): { entry: any } | null {
|
function findRecentDeafen(audit: any): { entry: any } | null {
|
||||||
const entries = audit?.audit_log_entries ?? [];
|
const entries = audit?.audit_log_entries ?? [];
|
||||||
|
console.log(`[AUDIT] Fetched ${entries.length} entries`);
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
if (e.action_type !== 24) continue; // MEMBER_UPDATE
|
if (e.action_type !== 24) continue; // MEMBER_UPDATE
|
||||||
const changes = e.changes || [];
|
const changes = e.changes || [];
|
||||||
const deafChange = changes.find((c: any) => c.key === "deaf");
|
const deafChange = changes.find((c: any) => c.key === "deaf");
|
||||||
if (!deafChange || deafChange.new_value !== true) continue;
|
if (!deafChange || deafChange.new_value !== true) continue;
|
||||||
const ageSec = (Date.now() - snowflakeToMs(e.id)) / 1000;
|
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 };
|
if (ageSec <= MAX_EVENT_AGE_SEC) return { entry: e };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPoll(channelId: string, content: string) {
|
async function createPoll(channelId: string, content: string) {
|
||||||
|
console.log("[POLL] Creating poll");
|
||||||
const body = {
|
const body = {
|
||||||
content,
|
content,
|
||||||
poll: {
|
poll: {
|
||||||
@ -90,7 +106,9 @@ async function createPoll(channelId: string, content: string) {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
if (!r.ok) {
|
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`, {
|
await discordFetch(`/channels/${channelId}/messages`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -102,6 +120,7 @@ async function createPoll(channelId: string, content: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage(channelId: string, content: string) {
|
async function sendMessage(channelId: string, content: string) {
|
||||||
|
console.log(`[MSG] Sending message to channel ${channelId}`);
|
||||||
await discordFetch(`/channels/${channelId}/messages`, {
|
await discordFetch(`/channels/${channelId}/messages`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ content }),
|
body: JSON.stringify({ content }),
|
||||||
@ -116,9 +135,12 @@ app.get("/healthz", (c) => c.text("ok"));
|
|||||||
|
|
||||||
/** Register or upsert the /checkaudit command for a guild */
|
/** Register or upsert the /checkaudit command for a guild */
|
||||||
app.post("/admin/register", async (c) => {
|
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);
|
return c.text("forbidden", 403);
|
||||||
|
}
|
||||||
const { guildId = DEFAULT_GUILD_ID } = await c.req.json().catch(() => ({}));
|
const { guildId = DEFAULT_GUILD_ID } = await c.req.json().catch(() => ({}));
|
||||||
|
console.log(`[ADMIN] Registering /checkaudit for guild ${guildId}`);
|
||||||
const body = {
|
const body = {
|
||||||
name: "checkaudit",
|
name: "checkaudit",
|
||||||
description: "Scan recent audit logs for deafen events",
|
description: "Scan recent audit logs for deafen events",
|
||||||
@ -132,6 +154,7 @@ app.post("/admin/register", async (c) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const j = await r.json().catch(() => ({}));
|
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 });
|
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 interaction = JSON.parse(raw);
|
||||||
const { type } = interaction;
|
const { type } = interaction;
|
||||||
|
|
||||||
|
console.log(`[INTERACTION] type=${type}`);
|
||||||
|
|
||||||
// Ping
|
// Ping
|
||||||
if (type === 1) return c.json({ type: 1 });
|
if (type === 1) {
|
||||||
|
console.log("[INTERACTION] Received PING");
|
||||||
|
return c.json({ type: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
// Application Command
|
// Application Command
|
||||||
if (type === 2) {
|
if (type === 2) {
|
||||||
@ -155,19 +183,24 @@ app.post("/interactions", async (c) => {
|
|||||||
const guildId = interaction.guild_id as string;
|
const guildId = interaction.guild_id as string;
|
||||||
const channelId = interaction.channel_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, {
|
await interactionCallback(id, token, {
|
||||||
type: 4, // CHANNEL_MESSAGE_WITH_SOURCE
|
type: 4,
|
||||||
data: { content: "Scanning audit logs…", flags: 64 }, // ephemeral
|
data: { content: "Scanning audit logs…", flags: 64 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Background work
|
// Background work
|
||||||
queueMicrotask(async () => {
|
queueMicrotask(async () => {
|
||||||
// Fetch recent MEMBER_UPDATE logs
|
console.log("[WORK] Fetching audit logs…");
|
||||||
const logsResp = await discordFetch(
|
const logsResp = await discordFetch(
|
||||||
`/guilds/${guildId}/audit-logs?action_type=24&limit=10`
|
`/guilds/${guildId}/audit-logs?action_type=24&limit=10`
|
||||||
);
|
);
|
||||||
if (!logsResp.ok) {
|
if (!logsResp.ok) {
|
||||||
|
console.error("[WORK] Failed to fetch audit logs", logsResp.status);
|
||||||
await sendMessage(channelId, "Failed to read audit logs.");
|
await sendMessage(channelId, "Failed to read audit logs.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -175,6 +208,7 @@ app.post("/interactions", async (c) => {
|
|||||||
|
|
||||||
const hit = findRecentDeafen(logs);
|
const hit = findRecentDeafen(logs);
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
|
console.log("[WORK] No deafen event found in recent logs");
|
||||||
await sendMessage(channelId, "No recent deafen event found.");
|
await sendMessage(channelId, "No recent deafen event found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -183,17 +217,18 @@ app.post("/interactions", async (c) => {
|
|||||||
const executorId = e.user_id;
|
const executorId = e.user_id;
|
||||||
const targetId = e.target_id;
|
const targetId = e.target_id;
|
||||||
|
|
||||||
// Create poll
|
console.log(
|
||||||
|
`[WORK] Deafen event confirmed. Executor=${executorId}, Target=${targetId}`
|
||||||
|
);
|
||||||
|
|
||||||
await createPoll(
|
await createPoll(
|
||||||
channelId,
|
channelId,
|
||||||
`Detected deafen event. Executor: <@${executorId}> → Target: <@${targetId}>`
|
`Detected deafen event. Executor: <@${executorId}> → Target: <@${targetId}>`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Taunt
|
|
||||||
await sendMessage(channelId, "Justice incoming.");
|
await sendMessage(channelId, "Justice incoming.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// We already responded above
|
|
||||||
return c.body(null, 204);
|
return c.body(null, 204);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user