feat: implement punishment process management and integrate with Discord interactions
This commit is contained in:
parent
fee40b10a2
commit
9943ea51f9
6
bun.lock
6
bun.lock
@ -4,10 +4,12 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "police-discord-bot",
|
"name": "police-discord-bot",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"hono": "^4.9.4",
|
"hono": "^4.9.4",
|
||||||
"pino": "^9.9.0",
|
"pino": "^9.9.0",
|
||||||
"pino-pretty": "^13.1.1",
|
"pino-pretty": "^13.1.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
|
"ws": "^8.18.3",
|
||||||
"zod": "^4.1.5",
|
"zod": "^4.1.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -22,6 +24,8 @@
|
|||||||
|
|
||||||
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
|
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
|
||||||
|
|
||||||
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
||||||
@ -86,6 +90,8 @@
|
|||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
"zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="],
|
"zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,10 +5,12 @@
|
|||||||
"dev": "bun run --hot src/index.ts"
|
"dev": "bun run --hot src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"hono": "^4.9.4",
|
"hono": "^4.9.4",
|
||||||
"pino": "^9.9.0",
|
"pino": "^9.9.0",
|
||||||
"pino-pretty": "^13.1.1",
|
"pino-pretty": "^13.1.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
|
"ws": "^8.18.3",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -2,5 +2,6 @@ export * from "./application";
|
|||||||
export * from "./audit-log";
|
export * from "./audit-log";
|
||||||
export * from "./core";
|
export * from "./core";
|
||||||
export * from "./guild";
|
export * from "./guild";
|
||||||
|
export * from "./interaction";
|
||||||
export * from "./message";
|
export * from "./message";
|
||||||
export * from "./poll";
|
export * from "./poll";
|
||||||
|
|||||||
23
src/discord/interaction.ts
Normal file
23
src/discord/interaction.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { fetchDiscord } from "./core";
|
||||||
|
import { Config } from "../config";
|
||||||
|
|
||||||
|
export const editInteractionResponse = async (token: string, data: any) => {
|
||||||
|
return fetchDiscord(
|
||||||
|
`/webhooks/${Config.APP_ID}/${token}/messages/@original`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendInteractionCallback = async (
|
||||||
|
id: string,
|
||||||
|
token: string,
|
||||||
|
body: any,
|
||||||
|
) => {
|
||||||
|
return fetchDiscord(`/interactions/${id}/${token}/callback`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -217,3 +217,18 @@ export const getReactions = async (
|
|||||||
const res = await fetchDiscord(path);
|
const res = await fetchDiscord(path);
|
||||||
return res.json();
|
return res.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const listReactionUsersMulti = async (
|
||||||
|
channelId: string,
|
||||||
|
messageId: string,
|
||||||
|
emojis: string[],
|
||||||
|
) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
emojis.map((e) => getReactions(channelId, messageId, e)),
|
||||||
|
);
|
||||||
|
const byId = new Map<string, any>();
|
||||||
|
for (const arr of results) {
|
||||||
|
for (const u of arr) byId.set(u.id, u); // de-dupe across variants
|
||||||
|
}
|
||||||
|
return Array.from(byId.values());
|
||||||
|
};
|
||||||
|
|||||||
261
src/gateway.ts
Normal file
261
src/gateway.ts
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import { WebSocket } from "ws";
|
||||||
|
import { Config } from "./config";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
import * as Discord from "./discord";
|
||||||
|
import {
|
||||||
|
getActiveProcesses,
|
||||||
|
getPunishmentProcess,
|
||||||
|
updatePunishmentProcess,
|
||||||
|
endPunishmentProcess,
|
||||||
|
PunishmentState,
|
||||||
|
} from "./store";
|
||||||
|
|
||||||
|
let ws: WebSocket;
|
||||||
|
let sessionId: string | null = null;
|
||||||
|
let sequence: number | null = null;
|
||||||
|
let resumeGatewayUrl: string | null = null;
|
||||||
|
let heartbeatInterval: number | null = null;
|
||||||
|
let botUserId: string | null = null;
|
||||||
|
|
||||||
|
const INTENTS =
|
||||||
|
(1 << 0) | // GUILDS
|
||||||
|
(1 << 9) | // GUILD_MESSAGES
|
||||||
|
(1 << 10) | // GUILD_MESSAGE_REACTIONS
|
||||||
|
(1 << 15); // MESSAGE_CONTENT
|
||||||
|
|
||||||
|
const send = (op: number, d: any) => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ op, d }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const heartbeat = () => {
|
||||||
|
if (heartbeatInterval) {
|
||||||
|
setInterval(() => {
|
||||||
|
send(1, sequence);
|
||||||
|
logger.info("Sent heartbeat");
|
||||||
|
}, heartbeatInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const identify = () => {
|
||||||
|
send(2, {
|
||||||
|
token: Config.BOT_TOKEN,
|
||||||
|
intents: INTENTS,
|
||||||
|
properties: {
|
||||||
|
os: "linux",
|
||||||
|
browser: "my_library",
|
||||||
|
device: "my_library",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReactionAdd = async (data: any) => {
|
||||||
|
const { message_id, user_id, emoji, channel_id, guild_id } = data;
|
||||||
|
const process = getPunishmentProcess(message_id);
|
||||||
|
|
||||||
|
if (!process || process.chosen || user_id === botUserId) return;
|
||||||
|
|
||||||
|
const RENAME_EMOJIS = ["✏️", "✏"];
|
||||||
|
const ROLE_EMOJIS = ["😭"];
|
||||||
|
|
||||||
|
const isRenameReaction = RENAME_EMOJIS.includes(emoji.name);
|
||||||
|
const isRoleReaction = ROLE_EMOJIS.includes(emoji.name);
|
||||||
|
|
||||||
|
if (!isRenameReaction && !isRoleReaction) return;
|
||||||
|
|
||||||
|
const [renameUsers, roleUsers] = await Promise.all([
|
||||||
|
Discord.listReactionUsersMulti(channel_id, message_id, RENAME_EMOJIS),
|
||||||
|
Discord.listReactionUsersMulti(channel_id, message_id, ROLE_EMOJIS),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DECIDER_USER_ID = "311380871901085707";
|
||||||
|
const deciderRename = renameUsers.some((u) => u.id === DECIDER_USER_ID);
|
||||||
|
const deciderRole = roleUsers.some((u) => u.id === DECIDER_USER_ID);
|
||||||
|
|
||||||
|
let chosen: "rename" | "role" | null = null;
|
||||||
|
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) {
|
||||||
|
updatePunishmentProcess(message_id, { chosen });
|
||||||
|
executePunishment(chosen, process, message_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageCreate = async (data: any) => {
|
||||||
|
const { channel_id, author, content, guild_id } = data;
|
||||||
|
|
||||||
|
if (author.bot) return;
|
||||||
|
|
||||||
|
for (const [messageId, process] of Array.from(getActiveProcesses())) {
|
||||||
|
if (
|
||||||
|
process.channelId === channel_id &&
|
||||||
|
process.askMessageId &&
|
||||||
|
!process.chosen
|
||||||
|
) {
|
||||||
|
if (process.renameDeadline && Date.now() > process.renameDeadline) {
|
||||||
|
Discord.createMessage(channel_id, {
|
||||||
|
content: "หมดเวลาการลงชื่อใหม่ ไม่เปลี่ยนแม่งละชื่อ",
|
||||||
|
});
|
||||||
|
Discord.editInteractionResponse(process.interactionToken, {
|
||||||
|
content: "No decision taken.",
|
||||||
|
});
|
||||||
|
endPunishmentProcess(messageId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNick = content.trim().slice(0, 32);
|
||||||
|
if (newNick) {
|
||||||
|
endPunishmentProcess(messageId);
|
||||||
|
try {
|
||||||
|
await Discord.modifyGuildMember(guild_id, process.executorId, {
|
||||||
|
nick: newNick,
|
||||||
|
});
|
||||||
|
await Discord.createMessage(channel_id, {
|
||||||
|
content: `ตั้งชื่อใหม่ให้ <@${process.executorId}> เป็น **${newNick}** แล้วนะคราฟ`,
|
||||||
|
});
|
||||||
|
await Discord.editInteractionResponse(process.interactionToken, {
|
||||||
|
content: "Done.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to execute rename punishment");
|
||||||
|
await Discord.editInteractionResponse(process.interactionToken, {
|
||||||
|
content: "Failed to execute punishment.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const executePunishment = async (
|
||||||
|
chosen: "rename" | "role",
|
||||||
|
process: PunishmentState,
|
||||||
|
messageId: string,
|
||||||
|
) => {
|
||||||
|
if (chosen === "rename") {
|
||||||
|
const ask = await Discord.createMessage(process.channelId, {
|
||||||
|
content: `เลือก: เปลี่ยนชื่อเล่นให้ <@${process.executorId}>\nพิมพ์ชื่อใหม่ (≤32 ตัวอักษร) ภายใน ${Config.RENAME_TIMEOUT_SEC} วินาที`,
|
||||||
|
});
|
||||||
|
updatePunishmentProcess(messageId, {
|
||||||
|
askMessageId: ask.id,
|
||||||
|
renameDeadline: Date.now() + Config.RENAME_TIMEOUT_SEC * 1000,
|
||||||
|
});
|
||||||
|
} else if (chosen === "role") {
|
||||||
|
endPunishmentProcess(messageId);
|
||||||
|
try {
|
||||||
|
if (!Config.PUNISH_ROLE_ID) {
|
||||||
|
await Discord.createMessage(process.channelId, {
|
||||||
|
content: "ยังไม่ได้ตั้งค่า PUNISH_ROLE_ID จึงลบ role ไม่ได้",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await Discord.removeGuildMemberRole(
|
||||||
|
process.guildId,
|
||||||
|
process.executorId,
|
||||||
|
Config.PUNISH_ROLE_ID,
|
||||||
|
);
|
||||||
|
await Discord.createMessage(process.channelId, {
|
||||||
|
content: `ลบ role ออกจาก <@${process.executorId}> แล้วนะคราฟ`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await Discord.createMessage(process.channelId, {
|
||||||
|
content: "โดนซะบ้าง 🙂",
|
||||||
|
});
|
||||||
|
await Discord.editInteractionResponse(process.interactionToken, {
|
||||||
|
content: "Done.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to execute role punishment");
|
||||||
|
await Discord.editInteractionResponse(process.interactionToken, {
|
||||||
|
content: "Failed to execute punishment.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [messageId, process] of Array.from(getActiveProcesses())) {
|
||||||
|
if (now > process.deadline && !process.chosen) {
|
||||||
|
Discord.createMessage(process.channelId, {
|
||||||
|
content: "หมดเวลา ไม่มีการเลือกลงโทษ รอดตัวไป",
|
||||||
|
});
|
||||||
|
Discord.editInteractionResponse(process.interactionToken, {
|
||||||
|
content: "No decision taken.",
|
||||||
|
});
|
||||||
|
endPunishmentProcess(messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
const handleEvent = (payload: any) => {
|
||||||
|
const { op, d, s, t } = payload;
|
||||||
|
|
||||||
|
if (s) {
|
||||||
|
sequence = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case 0: // Dispatch
|
||||||
|
logger.info({ type: t }, "Received dispatch event");
|
||||||
|
if (t === "READY") {
|
||||||
|
botUserId = d.user.id;
|
||||||
|
sessionId = d.session_id;
|
||||||
|
resumeGatewayUrl = d.resume_gateway_url;
|
||||||
|
}
|
||||||
|
if (t === "MESSAGE_REACTION_ADD") {
|
||||||
|
handleReactionAdd(d);
|
||||||
|
}
|
||||||
|
if (t === "MESSAGE_CREATE") {
|
||||||
|
handleMessageCreate(d);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 10: // Hello
|
||||||
|
heartbeatInterval = d.heartbeat_interval;
|
||||||
|
heartbeat();
|
||||||
|
identify();
|
||||||
|
break;
|
||||||
|
case 11: // Heartbeat ACK
|
||||||
|
logger.info("Received heartbeat ACK");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.info({ op, d, s, t }, "Received unhandled opcode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connect = async () => {
|
||||||
|
try {
|
||||||
|
const res = await Discord.fetchDiscord("/gateway/bot");
|
||||||
|
const data = await res.json();
|
||||||
|
const gatewayUrl = data.url;
|
||||||
|
resumeGatewayUrl = gatewayUrl;
|
||||||
|
|
||||||
|
ws = new WebSocket(`${gatewayUrl}/?v=10&encoding=json`);
|
||||||
|
|
||||||
|
ws.on("open", () => {
|
||||||
|
logger.info("Connected to Gateway");
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("message", (data) => {
|
||||||
|
const payload = JSON.parse(data.toString());
|
||||||
|
handleEvent(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", (code) => {
|
||||||
|
logger.warn({ code }, "Gateway connection closed. Reconnecting...");
|
||||||
|
setTimeout(connect, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("error", (err) => {
|
||||||
|
logger.error(err, "Gateway error");
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to connect to Gateway");
|
||||||
|
}
|
||||||
|
};
|
||||||
158
src/index.ts
158
src/index.ts
@ -4,6 +4,8 @@ import { logger } from "./logger";
|
|||||||
import * as nacl from "tweetnacl";
|
import * as nacl from "tweetnacl";
|
||||||
import * as Discord from "./discord";
|
import * as Discord from "./discord";
|
||||||
import { registerCommand } from "./commands";
|
import { registerCommand } from "./commands";
|
||||||
|
import { connect } from "./gateway";
|
||||||
|
import { startPunishmentProcess } from "./store";
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{ port: Config.PORT, guildId: Config.DEFAULT_GUILD_ID },
|
{ port: Config.PORT, guildId: Config.DEFAULT_GUILD_ID },
|
||||||
@ -13,27 +15,11 @@ logger.info(
|
|||||||
registerCommand().catch((err) =>
|
registerCommand().catch((err) =>
|
||||||
logger.error(err, "Failed to register commands"),
|
logger.error(err, "Failed to register commands"),
|
||||||
);
|
);
|
||||||
|
connect();
|
||||||
|
|
||||||
const snowflakeToMs = (id: string) =>
|
const snowflakeToMs = (id: string) =>
|
||||||
Number((BigInt(id) >> BigInt(22)) + BigInt(1420070400000));
|
Number((BigInt(id) >> BigInt(22)) + BigInt(1420070400000));
|
||||||
|
|
||||||
async function interactionCallback(id: string, token: string, body: any) {
|
|
||||||
return Discord.fetchDiscord(`/interactions/${id}/${token}/callback`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function editOriginal(token: string, content: string) {
|
|
||||||
return Discord.fetchDiscord(
|
|
||||||
`/webhooks/${Config.APP_ID}/${token}/messages/@original`,
|
|
||||||
{
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify({ content }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findRecentVoicePunish(audit: Discord.AuditLog): {
|
function findRecentVoicePunish(audit: Discord.AuditLog): {
|
||||||
entry: Discord.AuditLogEntry;
|
entry: Discord.AuditLogEntry;
|
||||||
kind: "deafen" | "mute";
|
kind: "deafen" | "mute";
|
||||||
@ -52,21 +38,6 @@ function findRecentVoicePunish(audit: Discord.AuditLog): {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listReactionUsersMulti(
|
|
||||||
channelId: string,
|
|
||||||
messageId: string,
|
|
||||||
emojis: string[],
|
|
||||||
) {
|
|
||||||
const results = await Promise.all(
|
|
||||||
emojis.map((e) => Discord.getReactions(channelId, messageId, e)),
|
|
||||||
);
|
|
||||||
const byId = new Map<string, any>();
|
|
||||||
for (const arr of results) {
|
|
||||||
for (const u of arr) byId.set(u.id, u); // de-dupe across variants
|
|
||||||
}
|
|
||||||
return Array.from(byId.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
const randomSystem = () => {
|
const randomSystem = () => {
|
||||||
const pool = ["คุณเป็นคนพูดจาเกรียนๆ"];
|
const pool = ["คุณเป็นคนพูดจาเกรียนๆ"];
|
||||||
return pool[Math.floor(Math.random() * pool.length)];
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
@ -132,7 +103,7 @@ app.post("/interactions", async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const interaction = JSON.parse(raw);
|
const interaction = JSON.parse(raw);
|
||||||
logger.info("Received interaction");
|
logger.info({ interaction }, "Received interaction");
|
||||||
const { type } = interaction;
|
const { type } = interaction;
|
||||||
|
|
||||||
if (type === 1) {
|
if (type === 1) {
|
||||||
@ -145,7 +116,10 @@ app.post("/interactions", async (c) => {
|
|||||||
const commandName = data.name;
|
const commandName = data.name;
|
||||||
|
|
||||||
if (commandName === "checkaudit") {
|
if (commandName === "checkaudit") {
|
||||||
await interactionCallback(id, token, { type: 5, data: { flags: 64 } });
|
await Discord.sendInteractionCallback(id, token, {
|
||||||
|
type: 5,
|
||||||
|
data: { flags: 64 },
|
||||||
|
});
|
||||||
|
|
||||||
queueMicrotask(async () => {
|
queueMicrotask(async () => {
|
||||||
try {
|
try {
|
||||||
@ -157,7 +131,9 @@ app.post("/interactions", async (c) => {
|
|||||||
|
|
||||||
const hit = findRecentVoicePunish(auditLogs);
|
const hit = findRecentVoicePunish(auditLogs);
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
await editOriginal(token, "No recent mute/deafen found.");
|
await Discord.editInteractionResponse(token, {
|
||||||
|
content: "No recent mute/deafen found.",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,94 +155,23 @@ app.post("/interactions", async (c) => {
|
|||||||
for (const e of ROLE_EMOJIS)
|
for (const e of ROLE_EMOJIS)
|
||||||
await Discord.addReaction(channel_id, sent.id, e);
|
await Discord.addReaction(channel_id, sent.id, e);
|
||||||
|
|
||||||
const botUserId = String(application_id);
|
startPunishmentProcess(sent.id, {
|
||||||
const deadline = Date.now() + Config.REACTION_TIMEOUT_SEC * 1000;
|
interactionToken: token,
|
||||||
let chosen: "rename" | "role" | null = null;
|
channelId: channel_id,
|
||||||
|
guildId: guild_id,
|
||||||
|
executorId: executorId,
|
||||||
|
timeout: Config.REACTION_TIMEOUT_SEC,
|
||||||
|
});
|
||||||
|
|
||||||
while (Date.now() < deadline && !chosen) {
|
await Discord.editInteractionResponse(token, {
|
||||||
const [renameUsers, roleUsers] = await Promise.all([
|
content:
|
||||||
listReactionUsersMulti(channel_id, sent.id, RENAME_EMOJIS),
|
"Punishment process initiated. Please react to the message above.",
|
||||||
listReactionUsersMulti(channel_id, sent.id, ROLE_EMOJIS),
|
});
|
||||||
]);
|
|
||||||
|
|
||||||
const DECIDER_USER_ID = "311380871901085707";
|
|
||||||
const deciderRename = renameUsers.some(
|
|
||||||
(u) => u.id === DECIDER_USER_ID,
|
|
||||||
);
|
|
||||||
const deciderRole = roleUsers.some((u) => u.id === DECIDER_USER_ID);
|
|
||||||
|
|
||||||
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 (!chosen) {
|
|
||||||
await Discord.createMessage(channel_id, {
|
|
||||||
content: "หมดเวลา ไม่มีการเลือกลงโทษ รอดตัวไป",
|
|
||||||
});
|
|
||||||
await editOriginal(token, "No decision taken.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chosen === "rename") {
|
|
||||||
const ask = await Discord.createMessage(channel_id, {
|
|
||||||
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 {
|
|
||||||
await Discord.modifyGuildMember(guild_id, executorId, {
|
|
||||||
nick: newNick,
|
|
||||||
});
|
|
||||||
await Discord.createMessage(channel_id, {
|
|
||||||
content: `ตั้งชื่อใหม่ให้ <@${executorId}> เป็น **${newNick}** แล้วนะคราฟ`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (chosen === "role") {
|
|
||||||
if (!Config.PUNISH_ROLE_ID) {
|
|
||||||
await Discord.createMessage(channel_id, {
|
|
||||||
content: "ยังไม่ได้ตั้งค่า PUNISH_ROLE_ID จึงลบ role ไม่ได้",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await Discord.removeGuildMemberRole(
|
|
||||||
guild_id,
|
|
||||||
executorId,
|
|
||||||
Config.PUNISH_ROLE_ID,
|
|
||||||
);
|
|
||||||
await Discord.createMessage(channel_id, {
|
|
||||||
content: `ลบ role ออกจาก <@${executorId}> แล้วนะคราฟ`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Discord.createMessage(channel_id, { content: "โดนซะบ้าง 🙂" });
|
|
||||||
await editOriginal(token, "Done.");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "Error in checkaudit command");
|
logger.error(error, "Error in checkaudit command");
|
||||||
await editOriginal(token, "An error occurred.");
|
await Discord.editInteractionResponse(token, {
|
||||||
|
content: "An error occurred.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -276,22 +181,27 @@ app.post("/interactions", async (c) => {
|
|||||||
if (commandName === "talk") {
|
if (commandName === "talk") {
|
||||||
const prompt =
|
const prompt =
|
||||||
data.options?.find((o: any) => o.name === "prompt")?.value || "";
|
data.options?.find((o: any) => o.name === "prompt")?.value || "";
|
||||||
await interactionCallback(id, token, { type: 5, data: { flags: 64 } });
|
await Discord.sendInteractionCallback(id, token, {
|
||||||
|
type: 5,
|
||||||
|
data: { flags: 64 },
|
||||||
|
});
|
||||||
|
|
||||||
queueMicrotask(async () => {
|
queueMicrotask(async () => {
|
||||||
try {
|
try {
|
||||||
const reply = await callOpenRouterChat(prompt);
|
const reply = await callOpenRouterChat(prompt);
|
||||||
await editOriginal(token, reply);
|
await Discord.editInteractionResponse(token, { content: reply });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "Error in talk command");
|
logger.error(error, "Error in talk command");
|
||||||
await editOriginal(token, "An error occurred.");
|
await Discord.editInteractionResponse(token, {
|
||||||
|
content: "An error occurred.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.body(null, 204);
|
return c.body(null, 204);
|
||||||
}
|
}
|
||||||
|
|
||||||
await interactionCallback(id, token, {
|
await Discord.sendInteractionCallback(id, token, {
|
||||||
type: 4,
|
type: 4,
|
||||||
data: { content: "Unknown command", flags: 64 },
|
data: { content: "Unknown command", flags: 64 },
|
||||||
});
|
});
|
||||||
|
|||||||
42
src/store.ts
Normal file
42
src/store.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
export interface PunishmentState {
|
||||||
|
interactionToken: string;
|
||||||
|
channelId: string;
|
||||||
|
guildId: string;
|
||||||
|
executorId: string;
|
||||||
|
deadline: number;
|
||||||
|
chosen: "rename" | "role" | null;
|
||||||
|
askMessageId?: string;
|
||||||
|
renameDeadline?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const punishmentProcesses = new Map<string, PunishmentState>(); // Key: messageId
|
||||||
|
|
||||||
|
export const startPunishmentProcess = (
|
||||||
|
messageId: string,
|
||||||
|
state: Omit<PunishmentState, "chosen" | "deadline"> & { timeout: number },
|
||||||
|
) => {
|
||||||
|
const deadline = Date.now() + state.timeout * 1000;
|
||||||
|
punishmentProcesses.set(messageId, { ...state, chosen: null, deadline });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPunishmentProcess = (messageId: string) => {
|
||||||
|
return punishmentProcesses.get(messageId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePunishmentProcess = (
|
||||||
|
messageId: string,
|
||||||
|
updates: Partial<PunishmentState>,
|
||||||
|
) => {
|
||||||
|
const process = punishmentProcesses.get(messageId);
|
||||||
|
if (process) {
|
||||||
|
punishmentProcesses.set(messageId, { ...process, ...updates });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const endPunishmentProcess = (messageId: string) => {
|
||||||
|
punishmentProcesses.delete(messageId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActiveProcesses = () => {
|
||||||
|
return punishmentProcesses.entries();
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user