diff --git a/.gitignore b/.gitignore index d0b5440..e6584ce 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules recordings .env dist/ +.muxer-queue.db \ No newline at end of file diff --git a/src/moderation/attachmentUploader.ts b/src/moderation/attachmentUploader.ts index 5f7597f..9e59d9b 100644 --- a/src/moderation/attachmentUploader.ts +++ b/src/moderation/attachmentUploader.ts @@ -1,7 +1,7 @@ import { createChildLogger } from "../logger"; import { config } from "../config"; import { retryWithBackoff } from "../retry"; -import type Database from "better-sqlite3"; +import type { SqliteDatabase } from "../muxer-queue"; import { updateAttachmentAsUploaded, updateAttachmentAsFailedUpload } from "./messageStore"; const logger = createChildLogger("attachment-uploader"); @@ -109,7 +109,7 @@ export async function downloadDiscordAttachment(url: string): Promise { } export async function processAttachmentUpload( - db: Database.Database, + db: SqliteDatabase, attachmentId: string, discordUrl: string, filename: string, diff --git a/src/moderation/messageCapture.ts b/src/moderation/messageCapture.ts index 74f1888..c046ffa 100644 --- a/src/moderation/messageCapture.ts +++ b/src/moderation/messageCapture.ts @@ -1,6 +1,6 @@ import type { Client, Message, TextChannel, ThreadChannel } from "discord.js-selfbot-v13"; -import type Database from "better-sqlite3"; import { createChildLogger } from "../logger"; +import type { SqliteDatabase } from "../muxer-queue"; import { config } from "../config"; import { insertMessage, insertAttachment } from "./messageStore"; import { processAttachmentUpload } from "./attachmentUploader"; @@ -9,7 +9,7 @@ import type { MessageRecord, AttachmentRecord } from "./types"; const logger = createChildLogger("message-capture"); async function captureMessage( - db: Database.Database, + db: SqliteDatabase, message: Message, type: "text" | "edited" | "deleted", ): Promise { @@ -101,7 +101,7 @@ async function captureMessage( ); } -export function registerMessageCapture(client: Client, db: Database.Database): void { +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; diff --git a/src/moderation/messageStore.ts b/src/moderation/messageStore.ts index d90c808..5b0b377 100644 --- a/src/moderation/messageStore.ts +++ b/src/moderation/messageStore.ts @@ -1,10 +1,10 @@ -import type Database from "better-sqlite3"; import { createChildLogger } from "../logger"; +import type { SqliteDatabase } from "../muxer-queue"; import type { MessageRecord, AttachmentRecord } from "./types"; const logger = createChildLogger("message-store"); -export function insertMessage(db: Database.Database, message: MessageRecord): void { +export function insertMessage(db: SqliteDatabase, message: MessageRecord): void { try { const stmt = db.prepare(` INSERT INTO messages ( @@ -41,7 +41,7 @@ export function insertMessage(db: Database.Database, message: MessageRecord): vo } export function updateMessageAsEdited( - db: Database.Database, + db: SqliteDatabase, messageId: string, editedContent: string, editedAt: number, @@ -65,7 +65,7 @@ export function updateMessageAsEdited( } export function updateMessageAsDeleted( - db: Database.Database, + db: SqliteDatabase, messageId: string, deletedAt: number, ): void { @@ -88,7 +88,7 @@ export function updateMessageAsDeleted( } export function getMessagesByChannel( - db: Database.Database, + db: SqliteDatabase, channelId: string, limit: number = 50, offset: number = 0, @@ -112,7 +112,7 @@ export function getMessagesByChannel( } } -export function insertAttachment(db: Database.Database, attachment: AttachmentRecord): void { +export function insertAttachment(db: SqliteDatabase, attachment: AttachmentRecord): void { try { const stmt = db.prepare(` INSERT INTO attachments ( @@ -149,7 +149,7 @@ export function insertAttachment(db: Database.Database, attachment: AttachmentRe } export function getAttachmentsByChannel( - db: Database.Database, + db: SqliteDatabase, channelId: string, limit: number = 50, offset: number = 0, @@ -174,7 +174,7 @@ export function getAttachmentsByChannel( } export function updateAttachmentAsUploaded( - db: Database.Database, + db: SqliteDatabase, attachmentId: string, uploadedUrl: string, uploadedAt: number, @@ -198,7 +198,7 @@ export function updateAttachmentAsUploaded( } export function updateAttachmentAsFailedUpload( - db: Database.Database, + db: SqliteDatabase, attachmentId: string, error: string, ): void { diff --git a/src/muxer-queue.ts b/src/muxer-queue.ts index a362137..b9d241e 100644 --- a/src/muxer-queue.ts +++ b/src/muxer-queue.ts @@ -1,9 +1,21 @@ import path from "node:path"; -import Database from "better-sqlite3"; +import { Database } from "bun:sqlite"; import { createChildLogger } from "./logger"; const logger = createChildLogger("muxer-queue"); +export interface SqliteStatement { + run: (...params: unknown[]) => { changes: number }; + all: (...params: unknown[]) => unknown[]; + get: (...params: unknown[]) => unknown; +} + +export interface SqliteDatabase { + prepare: (sql: string) => SqliteStatement; + exec: (sql: string) => void; + close: () => void; +} + export interface MuxerJobData { userId: string; sessionId: string; @@ -23,13 +35,14 @@ interface StoredJob { } const dbPath = path.join(process.cwd(), ".muxer-queue.db"); -let db: Database.Database | null = null; +let db: SqliteDatabase | null = null; -function initializeDatabase(): Database.Database { - const database = new Database(dbPath); - database.pragma("journal_mode = WAL"); +function initializeDatabase(): SqliteDatabase { + const database = new Database(dbPath) as SqliteDatabase; database.exec(` + PRAGMA journal_mode = WAL; + CREATE TABLE IF NOT EXISTS muxer_jobs ( id TEXT PRIMARY KEY, data TEXT NOT NULL, @@ -43,12 +56,56 @@ function initializeDatabase(): Database.Database { CREATE INDEX IF NOT EXISTS idx_status ON muxer_jobs(status); CREATE INDEX IF NOT EXISTS idx_createdAt ON muxer_jobs(createdAt); + + CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + thread_id TEXT, + user_id TEXT NOT NULL, + username TEXT NOT NULL, + avatar_url TEXT, + content TEXT NOT NULL, + edited_content TEXT, + created_at INTEGER NOT NULL, + edited_at INTEGER, + deleted_at INTEGER, + type TEXT NOT NULL DEFAULT 'text', + metadata TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id); + CREATE INDEX IF NOT EXISTS idx_messages_user ON messages(user_id); + CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id); + + CREATE TABLE IF NOT EXISTS attachments ( + id TEXT PRIMARY KEY, + message_id TEXT NOT NULL, + guild_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + user_id TEXT NOT NULL, + filename TEXT NOT NULL, + size INTEGER NOT NULL, + type TEXT NOT NULL, + discord_url TEXT NOT NULL, + uploaded_url TEXT, + upload_status TEXT NOT NULL DEFAULT 'pending', + upload_error TEXT, + created_at INTEGER NOT NULL, + uploaded_at INTEGER, + FOREIGN KEY (message_id) REFERENCES messages(id) + ); + + CREATE INDEX IF NOT EXISTS idx_attachments_channel ON attachments(channel_id); + CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id); + CREATE INDEX IF NOT EXISTS idx_attachments_status ON attachments(upload_status); `); return database; } -function getDatabase(): Database.Database { +function getDatabase(): SqliteDatabase { if (!db) { db = initializeDatabase(); }