feat: restructure configuration and command registration for improved clarity and functionality

This commit is contained in:
Sosokker 2025-09-02 20:51:05 +07:00
parent 6272bb62ff
commit fdbcfe3887
4 changed files with 259 additions and 395 deletions

View File

@ -1,11 +1,29 @@
To install dependencies: # Police Discord Bot
```sh
bun install
```
To run: ## Setup
```sh
bun run dev
```
open http://localhost:3000 1. Install dependencies:
```sh
bun install
```
2. Create a `.env` file in the root of the project and add the following environment variables:
```
BOT_TOKEN=
APP_ID=
PUBLIC_KEY=
DEFAULT_GUILD_ID=
ADMIN_KEY=
OPENROUTER_API_KEY=
PUNISH_ROLE_ID=
```
3. Run the bot:
```sh
bun run dev
```
4. The bot will be running at `http://localhost:3000`. You will need to use a tool like `ngrok` to expose it to the internet for Discord to send interactions.
5. Register the slash commands:
The bot registers the commands on startup. Check the logs for confirmation.

40
src/commands.ts Normal file
View File

@ -0,0 +1,40 @@
import { Config } from "./config";
import { logger } from "./logger";
import { fetchDiscord } from "./discord/core";
const COMMANDS = [
{
name: "checkaudit",
description: "Scan recent audit logs for mute/deafen",
type: 1,
},
{
name: "talk",
description: "Chat with the AI",
type: 1,
options: [
{
name: "prompt",
description: "Your message",
type: 3,
required: false,
},
],
},
];
export const registerCommand = async () => {
logger.info(
{ command: COMMANDS, guildId: Config.DEFAULT_GUILD_ID },
"Registering commands to discord server",
);
for (const commmand of COMMANDS) {
const response = await fetchDiscord(
`/applications/${Config.APP_ID}/guilds/${Config.DEFAULT_GUILD_ID}/commands`,
{
method: "POST",
body: JSON.stringify(commmand),
},
);
}
};

20
src/config.ts Normal file
View File

@ -0,0 +1,20 @@
import { z } from "zod";
const EnvSchema = z.object({
BOT_TOKEN: z.string(),
APP_ID: z.string(),
PUBLIC_KEY: z.string(),
DEFAULT_GUILD_ID: z.string(),
ADMIN_KEY: z.string(),
OPENROUTER_API_KEY: z.string(),
PUNISH_ROLE_ID: z.string().default(""),
PORT: z.coerce.number().default(3000),
MAX_EVENT_AGE_SEC: z.coerce.number().default(300),
REACTION_TIMEOUT_SEC: z.coerce.number().default(120),
RENAME_TIMEOUT_SEC: z.coerce.number().default(60),
DISCORD_API: z.string().default("https://discord.com/api/v10"),
});
type EnvConfig = z.infer<typeof EnvSchema>;
export const Config: EnvConfig = EnvSchema.parse(process.env);

View File

@ -1,113 +1,38 @@
import { Hono } from "hono"; import { Hono } from "hono";
import { Config } from "./config";
import { logger } from "./logger";
import * as nacl from "tweetnacl"; import * as nacl from "tweetnacl";
import * as Discord from "./discord";
/** ====== Env ====== */ logger.info(
const BOT_TOKEN = process.env.BOT_TOKEN!; { port: Config.PORT, guildId: Config.DEFAULT_GUILD_ID },
const APP_ID = process.env.APP_ID!; "Discord police bot is starting",
const PUBLIC_KEY = process.env.PUBLIC_KEY!; );
const DEFAULT_GUILD_ID = process.env.DEFAULT_GUILD_ID!;
const ADMIN_KEY = process.env.ADMIN_KEY!;
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY!;
const PUNISH_ROLE_ID = process.env.PUNISH_ROLE_ID || "";
const PORT = Number(process.env.PORT || 3000);
const MAX_EVENT_AGE_SEC = Number(process.env.MAX_EVENT_AGE_SEC || 300);
const REACTION_TIMEOUT_SEC = Number(process.env.REACTION_TIMEOUT_SEC || 120);
const RENAME_TIMEOUT_SEC = Number(process.env.RENAME_TIMEOUT_SEC || 60);
if (!BOT_TOKEN || !APP_ID || !PUBLIC_KEY || !ADMIN_KEY) {
console.error("[BOOT] Missing required envs");
process.exit(1);
}
console.log("[BOOT] Discord Audit Bot starting…");
/** ====== Constants ====== */
const API = "https://discord.com/api/v10";
const DECIDER_USER_ID = "311380871901085707";
const RENAME_EMOJIS = ["✏️", "✏"]; // VS16 + no-VS16
const ROLE_EMOJIS = ["😭"];
/** ====== Helpers ====== */
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const hexToUint8 = (hex: string) => {
if (hex.length % 2 !== 0) throw new Error("Invalid hex");
const u = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2)
u[i / 2] = parseInt(hex.slice(i, i + 2), 16);
return u;
};
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;
const msg = new TextEncoder().encode(ts + rawBody);
return nacl.sign.detached.verify(
hexToUint8(Buffer.from(msg).toString("hex")),
hexToUint8(sig),
hexToUint8(PUBLIC_KEY)
);
}
// Use direct uint8 approach (safer in Bun)
async function verifySignatureSafe(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;
const enc = new TextEncoder();
const msg = enc.encode(ts + rawBody);
return nacl.sign.detached.verify(
msg,
hexToUint8(sig),
hexToUint8(PUBLIC_KEY)
);
}
const snowflakeToMs = (id: string) => const snowflakeToMs = (id: string) =>
Number((BigInt(id) >> BigInt(22)) + BigInt(1420070400000)); Number((BigInt(id) >> BigInt(22)) + BigInt(1420070400000));
async function discordFetch(
path: string,
init?: RequestInit
): Promise<Response> {
const r = await fetch(`${API}${path}`, {
...init,
headers: {
Authorization: `Bot ${BOT_TOKEN}`,
"Content-Type": "application/json",
...(init?.headers || {}),
},
});
if (r.status === 429) {
const j = await r.json().catch(() => ({} as any));
const retry = (j?.retry_after ? Number(j.retry_after) : 1) * 1000;
await sleep(retry);
}
return r;
}
async function interactionCallback(id: string, token: string, body: any) { async function interactionCallback(id: string, token: string, body: any) {
return fetch(`${API}/interactions/${id}/${token}/callback`, { return Discord.fetchDiscord(`/interactions/${id}/${token}/callback`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
} }
async function editOriginal(token: string, content: string) { async function editOriginal(token: string, content: string) {
return fetch(`${API}/webhooks/${APP_ID}/${token}/messages/@original`, { return Discord.fetchDiscord(
method: "PATCH", `/webhooks/${Config.APP_ID}/${token}/messages/@original`,
headers: { "Content-Type": "application/json" }, {
body: JSON.stringify({ content }), method: "PATCH",
}); body: JSON.stringify({ content }),
},
);
} }
/** ====== Audit utils ====== */ function findRecentVoicePunish(audit: Discord.AuditLog): {
/** Find most recent MEMBER_UPDATE with deaf=true or mute=true within window */ entry: Discord.AuditLogEntry;
function findRecentVoicePunish( kind: "deafen" | "mute";
audit: any } | null {
): { entry: any; kind: "deafen" | "mute" } | null {
const entries = audit?.audit_log_entries ?? []; const entries = audit?.audit_log_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
@ -116,61 +41,19 @@ function findRecentVoicePunish(
const mute = changes.find((c: any) => c.key === "mute")?.new_value === true; const mute = changes.find((c: any) => c.key === "mute")?.new_value === true;
if (!deaf && !mute) continue; if (!deaf && !mute) continue;
const ageSec = (Date.now() - snowflakeToMs(e.id)) / 1000; const ageSec = (Date.now() - snowflakeToMs(e.id)) / 1000;
if (ageSec <= MAX_EVENT_AGE_SEC) if (ageSec <= Config.MAX_EVENT_AGE_SEC)
return { entry: e, kind: deaf ? "deafen" : "mute" }; return { entry: e, kind: deaf ? "deafen" : "mute" };
} }
return null; return null;
} }
/** ====== Message + Reaction helpers (REST polling) ====== */
const encEmoji = (e: string) => encodeURIComponent(e);
async function sendMessage(channelId: string, content: string) {
const r = await discordFetch(`/channels/${channelId}/messages`, {
method: "POST",
body: JSON.stringify({ content }),
});
const j = await r.json().catch(() => ({}));
return { ok: r.ok, status: r.status, data: j };
}
async function addReaction(
channelId: string,
messageId: string,
emoji: string
) {
// PUT /channels/{channel.id}/messages/{message.id}/reactions/{emoji}/@me
const r = await discordFetch(
`/channels/${channelId}/messages/${messageId}/reactions/${encEmoji(
emoji
)}/@me`,
{ method: "PUT", headers: { "Content-Type": "application/json" } }
);
return r.ok;
}
async function listReactionUsers(
channelId: string,
messageId: string,
emoji: string
) {
const r = await discordFetch(
`/channels/${channelId}/messages/${messageId}/reactions/${encEmoji(
emoji
)}?limit=100`
);
if (!r.ok) return [];
const users = await r.json().catch(() => []);
return Array.isArray(users) ? users : [];
}
async function listReactionUsersMulti( async function listReactionUsersMulti(
channelId: string, channelId: string,
messageId: string, messageId: string,
emojis: string[] emojis: string[],
) { ) {
const results = await Promise.all( const results = await Promise.all(
emojis.map((e) => listReactionUsers(channelId, messageId, e)) emojis.map((e) => Discord.getReactions(channelId, messageId, e)),
); );
const byId = new Map<string, any>(); const byId = new Map<string, any>();
for (const arr of results) { for (const arr of results) {
@ -179,39 +62,6 @@ async function listReactionUsersMulti(
return Array.from(byId.values()); return Array.from(byId.values());
} }
async function fetchNewMessagesAfter(
channelId: string,
afterMessageId: string
) {
const r = await discordFetch(
`/channels/${channelId}/messages?after=${afterMessageId}&limit=50`
);
if (!r.ok) return [];
const arr = await r.json().catch(() => []);
if (!Array.isArray(arr)) return [];
// Newest first; reverse to oldest→newest
return arr.reverse();
}
async function setNickname(guildId: string, userId: string, nick: string) {
const r = await discordFetch(`/guilds/${guildId}/members/${userId}`, {
method: "PATCH",
body: JSON.stringify({ nick }),
});
return r.ok;
}
async function removeRole(guildId: string, userId: string, roleId: string) {
const r = await discordFetch(
`/guilds/${guildId}/members/${userId}/roles/${roleId}`,
{
method: "DELETE",
}
);
return r.ok;
}
/** ====== OpenRouter /talk ====== */
const randomSystem = () => { const randomSystem = () => {
const pool = ["คุณเป็นคนพูดจาเกรียนๆ"]; const pool = ["คุณเป็นคนพูดจาเกรียนๆ"];
return pool[Math.floor(Math.random() * pool.length)]; return pool[Math.floor(Math.random() * pool.length)];
@ -221,7 +71,7 @@ async function callOpenRouterChat(userPrompt: string) {
const r = await fetch("https://openrouter.ai/api/v1/chat/completions", { const r = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${OPENROUTER_API_KEY}`, Authorization: `Bearer ${Config.OPENROUTER_API_KEY}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
@ -244,262 +94,198 @@ async function callOpenRouterChat(userPrompt: string) {
return j?.choices?.[0]?.message?.content || "(no content)"; return j?.choices?.[0]?.message?.content || "(no content)";
} }
/** ====== Hono app ====== */
const app = new Hono(); const app = new Hono();
/** Health */ app.get("/health", (c) => c.text("ok"));
app.get("/healthz", (c) => c.text("ok"));
/** Admin: register both commands */ const hexToUint8 = (hex: string) => {
app.post("/admin/register", async (c) => { if (hex.length % 2 !== 0) throw new Error("Invalid hex");
if (c.req.header("X-Admin-Key") !== ADMIN_KEY) const u = new Uint8Array(hex.length / 2);
return c.text("forbidden", 403); for (let i = 0; i < hex.length; i += 2)
const { guildId = DEFAULT_GUILD_ID } = await c.req.json().catch(() => ({})); u[i / 2] = parseInt(hex.slice(i, i + 2), 16);
return u;
};
const cmds = [ async function verifySignatureSafe(req: Request, rawBody: string) {
{ const sig = req.headers.get("X-Signature-Ed25519") || "";
name: "checkaudit", const ts = req.headers.get("X-Signature-Timestamp") || "";
description: "Scan recent audit logs for mute/deafen", if (!sig || !ts) return false;
type: 1, const enc = new TextEncoder();
}, const msg = enc.encode(ts + rawBody);
{ return nacl.sign.detached.verify(
name: "talk", msg,
description: "Chat with the AI", hexToUint8(sig),
type: 1, hexToUint8(Config.PUBLIC_KEY),
options: [ );
{ }
name: "prompt",
description: "Your message",
type: 3,
required: false,
},
],
},
];
const results: any[] = [];
for (const body of cmds) {
const r = await discordFetch(
`/applications/${APP_ID}/guilds/${guildId}/commands`,
{
method: "POST",
body: JSON.stringify(body),
}
);
results.push({
ok: r.ok,
status: r.status,
body: await r.json().catch(() => ({})),
});
}
return c.json({ results });
});
/** Interactions endpoint */
app.post("/interactions", async (c) => { app.post("/interactions", async (c) => {
const raw = await c.req.text(); const raw = await c.req.text();
if (!(await verifySignatureSafe(c.req.raw, raw))) if (!(await verifySignatureSafe(c.req.raw, raw))) {
logger.warn("Unauthorized request");
return c.text("unauthorized", 401); return c.text("unauthorized", 401);
}
const interaction = JSON.parse(raw); const interaction = JSON.parse(raw);
logger.info({ interaction }, "Received interaction");
const { type } = interaction; const { type } = interaction;
// PING if (type === 1) {
if (type === 1) return c.json({ type: 1 }); return c.json({ type: 1 });
}
// APPLICATION_COMMAND
if (type === 2) { if (type === 2) {
const name = interaction?.data?.name; const { data, id, token, guild_id, channel_id, application_id } =
const id = interaction.id as string; interaction;
const token = interaction.token as string; const commandName = data.name;
const guildId = interaction.guild_id as string;
const channelId = interaction.channel_id as string;
if (name === "checkaudit") { if (commandName === "checkaudit") {
// Immediate ephemeral ACK
await interactionCallback(id, token, { type: 5, data: { flags: 64 } }); await interactionCallback(id, token, { type: 5, data: { flags: 64 } });
// Background job
queueMicrotask(async () => { queueMicrotask(async () => {
// Fetch audit logs try {
const logsResp = await discordFetch( const auditLogs = await Discord.getAuditLog({
`/guilds/${guildId}/audit-logs?action_type=24&limit=10` guildId: Config.DEFAULT_GUILD_ID,
); actionType: 24,
if (!logsResp.ok) { limit: 10,
await editOriginal(token, "Failed to read audit logs."); });
return;
}
const logs = await logsResp.json();
const hit = findRecentVoicePunish(logs);
if (!hit) {
await editOriginal(token, "No recent mute/deafen found.");
return;
}
const e = hit.entry; const hit = findRecentVoicePunish(auditLogs);
const executorId = e.user_id as string; // the user who did the action if (!hit) {
const targetId = e.target_id as string; // the member affected await editOriginal(token, "No recent mute/deafen found.");
const kind = hit.kind === "deafen" ? "ปิดหู" : "ปิดไมค์";
// Decision message
const intro = `มีคนโดนทำร้าย😭😭 ${kind}\nคนทำ!: <@${executorId}> → คนโดน: <@${targetId}>\nเลือกการลงโทษโดยการกดส่งอีโมจิ:\n${RENAME_EMOJIS[0]} = เปลี่ยนชื่อเล่น, ${ROLE_EMOJIS[0]} = ลบ Role`;
const sent = await sendMessage(channelId, intro);
if (!sent.ok) {
await editOriginal(token, "Cannot post decision message.");
return;
}
const msgId = sent.data.id as string;
// Seed reactions so users can tap
for (const e of RENAME_EMOJIS) await addReaction(channelId, msgId, e);
for (const e of ROLE_EMOJIS) await addReaction(channelId, msgId, e);
// Identify the bot user ID (same as application_id for bot apps)
const botUserId = String(interaction.application_id);
// Poll reactions up to timeout
const deadline = Date.now() + REACTION_TIMEOUT_SEC * 1000;
let chosen: "rename" | "role" | null = null;
while (Date.now() < deadline && !chosen) {
const [renameUsersAll, roleUsersAll] = await Promise.all([
listReactionUsersMulti(channelId, msgId, RENAME_EMOJIS),
listReactionUsersMulti(channelId, msgId, ROLE_EMOJIS),
]);
// Exclude the bot itself from counts
const renameUsers = renameUsersAll.filter(
(u: any) => u?.id !== botUserId
);
const roleUsers = roleUsersAll.filter(
(u: any) => u?.id !== botUserId
);
// Priority: specific decider
const deciderRename = renameUsers.some(
(u: any) => u?.id === DECIDER_USER_ID
);
const deciderRole = roleUsers.some(
(u: any) => u?.id === DECIDER_USER_ID
);
console.log(
`[VOTE] rename=${renameUsers.length} role=${roleUsers.length} deciderRename=${deciderRename} deciderRole=${deciderRole}`
);
if (deciderRename) chosen = "rename";
else if (deciderRole) chosen = "role";
else {
// Otherwise: first path to reach 2 human votes
if (renameUsers.length >= 2) chosen = "rename";
else if (roleUsers.length >= 2) chosen = "role";
}
if (!chosen) await sleep(2000);
}
if (!chosen) {
await sendMessage(channelId, "หมดเวลา ไม่มีการเลือกลงโทษ รอดตัวไป");
await editOriginal(token, "No decision taken.");
return;
}
if (chosen === "rename") {
// Ask for new nickname
const ask = await sendMessage(
channelId,
`เลือก: เปลี่ยนชื่อเล่นให้ <@${executorId}>\nพิมพ์ชื่อใหม่ (≤32 ตัวอักษร) ภายใน ${RENAME_TIMEOUT_SEC} วินาที`
);
if (!ask.ok) {
await sendMessage(channelId, "ถามชื่อไม่ได้ ทำไมอ่า");
return; return;
} }
const askId = ask.data.id as string;
const renameDeadline = Date.now() + RENAME_TIMEOUT_SEC * 1000;
let newNick: string | null = null;
while (Date.now() < renameDeadline && !newNick) { const { entry, kind } = hit;
const news = await fetchNewMessagesAfter(channelId, askId); const executorId = entry.user_id!;
const firstUserMsg = news.find( const targetId = entry.target_id!;
(m: any) => const kindText = kind === "deafen" ? "ปิดหู" : "ปิดไมค์";
!m?.author?.bot &&
typeof m?.content === "string" && const RENAME_EMOJIS = ["✏️", "✏"];
m.content.trim().length > 0 const ROLE_EMOJIS = ["😭"];
const intro = `มีคนโดนทำร้าย😭😭 ${kindText}\nคนทำ!: <@${executorId}> → คนโดน: <@${targetId}>\nเลือกการลงโทษโดยการกดส่งอีโมจิ:\n${RENAME_EMOJIS[0]} = เปลี่ยนชื่อเล่น, ${ROLE_EMOJIS[0]} = ลบ Role`;
const sent = await Discord.createMessage(channel_id, {
content: intro,
});
for (const e of RENAME_EMOJIS)
await Discord.addReaction(channel_id, sent.id, e);
for (const e of ROLE_EMOJIS)
await Discord.addReaction(channel_id, sent.id, e);
const botUserId = String(application_id);
const deadline = Date.now() + Config.REACTION_TIMEOUT_SEC * 1000;
let chosen: "rename" | "role" | null = null;
while (Date.now() < deadline && !chosen) {
const [renameUsers, roleUsers] = await Promise.all([
listReactionUsersMulti(channel_id, sent.id, RENAME_EMOJIS),
listReactionUsersMulti(channel_id, sent.id, ROLE_EMOJIS),
]);
const DECIDER_USER_ID = "311380871901085707";
const deciderRename = renameUsers.some(
(u) => u.id === DECIDER_USER_ID,
); );
if (firstUserMsg) const deciderRole = roleUsers.some((u) => u.id === DECIDER_USER_ID);
newNick = firstUserMsg.content.trim().slice(0, 32);
if (!newNick) await sleep(2000); if (deciderRename) chosen = "rename";
else if (deciderRole) chosen = "role";
else if (renameUsers.filter((u) => u.id !== botUserId).length >= 2)
chosen = "rename";
else if (roleUsers.filter((u) => u.id !== botUserId).length >= 2)
chosen = "role";
if (!chosen) await new Promise((r) => setTimeout(r, 2000));
} }
if (!newNick) { if (!chosen) {
await sendMessage( await Discord.createMessage(channel_id, {
channelId, content: "หมดเวลา ไม่มีการเลือกลงโทษ รอดตัวไป",
"หมดเวลาการลงชื่อใหม่ ไม่เปลี่ยนแม่งละชื่อ" });
); await editOriginal(token, "No decision taken.");
} else { return;
const ok = await setNickname(guildId, executorId, newNick); }
if (ok) {
await sendMessage( if (chosen === "rename") {
channelId, const ask = await Discord.createMessage(channel_id, {
`ตั้งชื่อใหม่ให้ <@${executorId}> เป็น **${newNick}** แล้วนะคราฟ` content: `เลือก: เปลี่ยนชื่อเล่นให้ <@${executorId}>\nพิมพ์ชื่อใหม่ (≤32 ตัวอักษร) ภายใน ${Config.RENAME_TIMEOUT_SEC} วินาที`,
});
const renameDeadline =
Date.now() + Config.RENAME_TIMEOUT_SEC * 1000;
let newNick: string | null = null;
while (Date.now() < renameDeadline && !newNick) {
const newMessages = await Discord.getChannelMessages(channel_id, {
after: ask.id,
});
const firstUserMsg = newMessages.find(
(m) => !m.author.bot && m.content?.trim().length > 0,
); );
if (firstUserMsg)
newNick = firstUserMsg.content.trim().slice(0, 32);
if (!newNick) await new Promise((r) => setTimeout(r, 2000));
}
if (!newNick) {
await Discord.createMessage(channel_id, {
content: "หมดเวลาการลงชื่อใหม่ ไม่เปลี่ยนแม่งละชื่อ",
});
} else { } else {
await sendMessage( await Discord.modifyGuildMember(guild_id, executorId, {
channelId, nick: newNick,
"เปลี่ยนชื่อไม่สำเร็จ (สิทธิ์ไม่พอครับพี่)" });
); await Discord.createMessage(channel_id, {
} content: `ตั้งชื่อใหม่ให้ <@${executorId}> เป็น **${newNick}** แล้วนะคราฟ`,
} });
} else if (chosen === "role") { }
if (!PUNISH_ROLE_ID) { } else if (chosen === "role") {
await sendMessage( if (!Config.PUNISH_ROLE_ID) {
channelId, await Discord.createMessage(channel_id, {
"ยังไม่ได้ตั้งค่า PUNISH_ROLE_ID จึงลบ role ไม่ได้" content: "ยังไม่ได้ตั้งค่า PUNISH_ROLE_ID จึงลบ role ไม่ได้",
); });
} else { } else {
const ok = await removeRole(guildId, executorId, PUNISH_ROLE_ID); await Discord.removeGuildMemberRole(
if (ok) { guild_id,
await sendMessage( executorId,
channelId, Config.PUNISH_ROLE_ID,
`ลบ role ออกจาก <@${executorId}> แล้วนะคราฟ`
);
} else {
await sendMessage(
channelId,
"ลบ role ไม่สำเร็จ (สิทธิ์ไม่พอครับนาย)"
); );
await Discord.createMessage(channel_id, {
content: `ลบ role ออกจาก <@${executorId}> แล้วนะคราฟ`,
});
} }
} }
await Discord.createMessage(channel_id, { content: "โดนซะบ้าง 🙂" });
await editOriginal(token, "Done.");
} catch (error) {
logger.error(error, "Error in checkaudit command");
await editOriginal(token, "An error occurred.");
} }
// Taunt
await sendMessage(channelId, "โดนซะบ้าง 🙂");
await editOriginal(token, "Done.");
}); });
// We already deferred
return c.body(null, 204); return c.body(null, 204);
} }
if (name === "talk") { if (commandName === "talk") {
// Read optional prompt const prompt =
const promptOpt = interaction?.data?.options?.find( data.options?.find((o: any) => o.name === "prompt")?.value || "";
(o: any) => o.name === "prompt"
);
const prompt = promptOpt?.value || "";
// Deferred ephemeral
await interactionCallback(id, token, { type: 5, data: { flags: 64 } }); await interactionCallback(id, token, { type: 5, data: { flags: 64 } });
queueMicrotask(async () => { queueMicrotask(async () => {
const reply = await callOpenRouterChat(prompt); try {
await editOriginal(token, reply); const reply = await callOpenRouterChat(prompt);
await editOriginal(token, reply);
} catch (error) {
logger.error(error, "Error in talk command");
await editOriginal(token, "An error occurred.");
}
}); });
return c.body(null, 204); return c.body(null, 204);
} }
// Unknown command
await interactionCallback(id, token, { await interactionCallback(id, token, {
type: 4, type: 4,
data: { content: "Unknown command", flags: 64 }, data: { content: "Unknown command", flags: 64 },
@ -511,6 +297,6 @@ app.post("/interactions", async (c) => {
}); });
export default { export default {
port: PORT, port: Config.PORT,
fetch: app.fetch, fetch: app.fetch,
}; };