2026-05-13 21:04:45 +07:00
|
|
|
import type { Client, Message } from "discord.js-selfbot-v13";
|
2026-05-13 19:34:14 +07:00
|
|
|
import { createChildLogger } from "../logger";
|
2026-05-13 19:47:44 +07:00
|
|
|
import type { SqliteDatabase } from "../muxer-queue";
|
2026-05-13 19:34:14 +07:00
|
|
|
import { config } from "../config";
|
|
|
|
|
import { insertMessage, insertAttachment } from "./messageStore";
|
|
|
|
|
import { processAttachmentUpload } from "./attachmentUploader";
|
2026-05-13 21:04:45 +07:00
|
|
|
import { getDisplayContent, getMessageLocation, getMessageMetadata } from "./messageMetadata";
|
2026-05-14 02:31:16 +07:00
|
|
|
import { queueMessageAnalysis } from "./aiAnalyzer";
|
2026-05-13 19:34:14 +07:00
|
|
|
import type { MessageRecord, AttachmentRecord } from "./types";
|
|
|
|
|
|
|
|
|
|
const logger = createChildLogger("message-capture");
|
|
|
|
|
|
2026-05-13 21:04:45 +07:00
|
|
|
export async function captureMessage(
|
2026-05-13 19:47:44 +07:00
|
|
|
db: SqliteDatabase,
|
2026-05-13 19:34:14 +07:00
|
|
|
message: Message,
|
|
|
|
|
type: "text" | "edited" | "deleted",
|
|
|
|
|
): Promise<void> {
|
2026-05-13 20:52:37 +07:00
|
|
|
const location = getMessageLocation(message);
|
2026-05-13 21:04:45 +07:00
|
|
|
const metadata = getMessageMetadata(message);
|
2026-05-13 19:34:14 +07:00
|
|
|
|
|
|
|
|
const messageRecord: MessageRecord = {
|
|
|
|
|
id: message.id,
|
|
|
|
|
guild_id: message.guildId!,
|
2026-05-13 20:52:37 +07:00
|
|
|
channel_id: location.channelId,
|
|
|
|
|
thread_id: location.threadId,
|
2026-05-13 19:34:14 +07:00
|
|
|
user_id: message.author!.id,
|
|
|
|
|
username: message.author!.username,
|
|
|
|
|
avatar_url: message.author!.avatarURL() || null,
|
2026-05-13 20:52:37 +07:00
|
|
|
content: getDisplayContent(message),
|
2026-05-13 19:34:14 +07:00
|
|
|
edited_content: null,
|
|
|
|
|
created_at: message.createdTimestamp,
|
|
|
|
|
edited_at: null,
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
type,
|
2026-05-13 20:52:37 +07:00
|
|
|
metadata: JSON.stringify(metadata),
|
2026-05-13 19:34:14 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
insertMessage(db, messageRecord);
|
2026-05-14 02:31:16 +07:00
|
|
|
queueMessageAnalysis(db, message.id);
|
2026-05-13 19:34:14 +07:00
|
|
|
|
|
|
|
|
const broadcaster = globalThis as any;
|
|
|
|
|
if (broadcaster.broadcastMessageCreated) {
|
|
|
|
|
broadcaster.broadcastMessageCreated({
|
2026-05-13 20:52:37 +07:00
|
|
|
...messageRecord,
|
2026-05-13 19:34:14 +07:00
|
|
|
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!,
|
2026-05-13 20:52:37 +07:00
|
|
|
channel_id: location.channelId,
|
|
|
|
|
thread_id: location.threadId,
|
2026-05-13 19:34:14 +07:00
|
|
|
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",
|
2026-05-13 20:52:37 +07:00
|
|
|
channel_id: location.channelId,
|
2026-05-13 19:34:14 +07:00
|
|
|
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",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 19:47:44 +07:00
|
|
|
export function registerMessageCapture(client: Client, db: SqliteDatabase): void {
|
2026-05-13 19:34:14 +07:00
|
|
|
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();
|
2026-05-13 20:52:37 +07:00
|
|
|
updateMessageAsEdited(db, newMessage.id, getDisplayContent(newMessage as Message), editedAt);
|
2026-05-14 02:31:16 +07:00
|
|
|
queueMessageAnalysis(db, newMessage.id);
|
2026-05-13 19:34:14 +07:00
|
|
|
|
|
|
|
|
const broadcaster = globalThis as any;
|
|
|
|
|
if (broadcaster.broadcastMessageUpdated) {
|
|
|
|
|
broadcaster.broadcastMessageUpdated({
|
|
|
|
|
id: newMessage.id,
|
2026-05-13 20:52:37 +07:00
|
|
|
edited_content: getDisplayContent(newMessage as Message),
|
2026-05-13 19:34:14 +07:00
|
|
|
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");
|
|
|
|
|
}
|