Files
GMW/src/moderation/messageCapture.ts
T
MythEclipse d55b56c897 feat(moderation): enhance message capture and storage with thread support
- Added functions to retrieve message location, sticker metadata, and display content in messageCapture.ts.
- Updated captureMessage function to store thread information and sticker metadata in the database.
- Modified messageStore.ts to support querying messages and attachments by thread ID.
- Updated types.ts to include thread_id in AttachmentRecord.
- Altered database schema in muxer-queue.ts to add thread_id column to attachments.
- Introduced ChannelSummary interface and listWatchableChannels method in voiceController.ts to fetch watchable channels.
- Added API endpoint in webserver.ts to retrieve channels for a given guild.
2026-05-13 20:52:37 +07:00

220 lines
6.6 KiB
TypeScript

import type { Client, Message, TextChannel, ThreadChannel } from "discord.js-selfbot-v13";
import { createChildLogger } from "../logger";
import type { SqliteDatabase } from "../muxer-queue";
import { config } from "../config";
import { insertMessage, insertAttachment } from "./messageStore";
import { processAttachmentUpload } from "./attachmentUploader";
import type { MessageRecord, AttachmentRecord } from "./types";
const logger = createChildLogger("message-capture");
function getMessageLocation(message: Message): {
channelId: string;
threadId: string | null;
threadName: string | null;
} {
const channel = message.channel as TextChannel | ThreadChannel;
if (!channel.isThread?.()) {
return { channelId: message.channelId, threadId: null, threadName: null };
}
return {
channelId: channel.parentId ?? message.channelId,
threadId: channel.id,
threadName: channel.name,
};
}
function getStickerMetadata(message: Message): Array<{
id: string;
name: string;
url: string;
}> {
return Array.from(message.stickers.values()).map((sticker) => ({
id: sticker.id,
name: sticker.name,
url: sticker.url,
}));
}
function getDisplayContent(message: Message): string {
if (message.content.trim().length > 0) return message.content;
const stickers = getStickerMetadata(message);
if (stickers.length > 0) {
return stickers.map((sticker) => `[Sticker: ${sticker.name}]`).join(" ");
}
return "";
}
async function captureMessage(
db: SqliteDatabase,
message: Message,
type: "text" | "edited" | "deleted",
): Promise<void> {
const location = getMessageLocation(message);
const stickers = getStickerMetadata(message);
const metadata = {
stickers,
threadName: location.threadName,
};
const messageRecord: MessageRecord = {
id: message.id,
guild_id: message.guildId!,
channel_id: location.channelId,
thread_id: location.threadId,
user_id: message.author!.id,
username: message.author!.username,
avatar_url: message.author!.avatarURL() || null,
content: getDisplayContent(message),
edited_content: null,
created_at: message.createdTimestamp,
edited_at: null,
deleted_at: null,
type,
metadata: JSON.stringify(metadata),
};
insertMessage(db, messageRecord);
const broadcaster = globalThis as any;
if (broadcaster.broadcastMessageCreated) {
broadcaster.broadcastMessageCreated({
...messageRecord,
type: "text",
});
}
if (message.attachments.size > 0) {
for (const [, attachment] of message.attachments) {
const attachmentRecord: AttachmentRecord = {
id: attachment.id,
message_id: message.id,
guild_id: message.guildId!,
channel_id: location.channelId,
thread_id: location.threadId,
user_id: message.author!.id,
filename: attachment.name || "unknown",
size: attachment.size,
type: attachment.contentType || "application/octet-stream",
discord_url: attachment.url,
uploaded_url: null,
upload_status: "pending",
upload_error: null,
created_at: Date.now(),
uploaded_at: null,
};
insertAttachment(db, attachmentRecord);
processAttachmentUpload(db, attachment.id, attachment.url, attachment.name || "unknown")
.then(() => {
if (broadcaster.broadcastAttachmentUploaded) {
broadcaster.broadcastAttachmentUploaded({
id: attachment.id,
message_id: message.id,
filename: attachment.name || "unknown",
channel_id: location.channelId,
created_at: Date.now(),
});
}
})
.catch((error) => {
logger.error(
{ attachmentId: attachment.id, error: error instanceof Error ? error.message : String(error) },
"Background attachment upload failed",
);
});
}
}
logger.info(
{
messageId: message.id,
channelId: message.channelId,
attachmentCount: message.attachments.size,
},
"Message captured",
);
}
export function registerMessageCapture(client: Client, db: SqliteDatabase): void {
client.on("messageCreate", async (message) => {
if (!message.guildId || message.guildId !== config.MONITOR_GUILD_ID) return;
if (message.author?.bot) return;
try {
await captureMessage(db, message, "text");
} catch (error) {
logger.error(
{ messageId: message.id, error: error instanceof Error ? error.message : String(error) },
"Failed to capture message",
);
}
});
client.on("messageUpdate", async (_oldMessage, newMessage) => {
if (!newMessage.guildId || newMessage.guildId !== config.MONITOR_GUILD_ID) return;
if (newMessage.author?.bot) return;
try {
const { updateMessageAsEdited } = await import("./messageStore");
const existing = db
.prepare("SELECT id FROM messages WHERE id = ?")
.get(newMessage.id) as { id: string } | undefined;
if (existing) {
const editedAt = Date.now();
updateMessageAsEdited(db, newMessage.id, getDisplayContent(newMessage as Message), editedAt);
const broadcaster = globalThis as any;
if (broadcaster.broadcastMessageUpdated) {
broadcaster.broadcastMessageUpdated({
id: newMessage.id,
edited_content: getDisplayContent(newMessage as Message),
edited_at: editedAt,
});
}
} else if (newMessage.author) {
await captureMessage(db, newMessage as Message, "text");
}
} catch (error) {
logger.error(
{ messageId: newMessage.id, error: error instanceof Error ? error.message : String(error) },
"Failed to capture message update",
);
}
});
client.on("messageDelete", async (message) => {
if (!message.guildId || message.guildId !== config.MONITOR_GUILD_ID) return;
if (!message.author) return;
try {
const { updateMessageAsDeleted } = await import("./messageStore");
const deletedAt = Date.now();
updateMessageAsDeleted(db, message.id, deletedAt);
const broadcaster = globalThis as any;
if (broadcaster.broadcastMessageDeleted) {
broadcaster.broadcastMessageDeleted({
id: message.id,
deleted_at: deletedAt,
});
}
logger.info({ messageId: message.id }, "Message deletion captured");
} catch (error) {
logger.error(
{ messageId: message.id, error: error instanceof Error ? error.message : String(error) },
"Failed to capture message deletion",
);
}
});
logger.info("Message capture handlers registered");
}