diff --git a/src/moderation/aiAnalyzer.ts b/src/moderation/aiAnalyzer.ts index c3534ec..bf0cf8f 100644 --- a/src/moderation/aiAnalyzer.ts +++ b/src/moderation/aiAnalyzer.ts @@ -9,10 +9,22 @@ import { getPendingMessagesByConversation, updateMessageAIAnalysis, } from "./messageStore"; -import type { AnalysisQueueStatus, MessageRecord } from "./types"; +import type { + AnalysisQueueStatus, + MessageRecord, + ModerationBroadcaster, +} from "./types"; const logger = createChildLogger("ai-analyzer"); +type ModerationGlobal = typeof globalThis & { + moderationBroadcaster?: ModerationBroadcaster; +}; + +function getModerationBroadcaster(): ModerationBroadcaster | undefined { + return (globalThis as ModerationGlobal).moderationBroadcaster; +} + // Debounce state per conversation key const conversationDebounceTimers = new Map(); // Track conversations currently being processed @@ -117,7 +129,7 @@ async function processBatch( // Broadcast analyzed messages for (const row of analyzedRows) { - (globalThis as any).moderationBroadcaster?.messageAnalyzed(row); + getModerationBroadcaster()?.messageAnalyzed(row); } // Clear error cooldown on success @@ -147,7 +159,7 @@ async function processBatch( error: lastError, }); if (row) { - (globalThis as any).moderationBroadcaster?.messageAnalyzed(row); + getModerationBroadcaster()?.messageAnalyzed(row); } } diff --git a/src/moderation/backlogSync.ts b/src/moderation/backlogSync.ts index c208d03..a4785b6 100644 --- a/src/moderation/backlogSync.ts +++ b/src/moderation/backlogSync.ts @@ -1,12 +1,25 @@ -import type { Client, Message } from "discord.js-selfbot-v13"; +import type { Channel, Client, Message } from "discord.js-selfbot-v13"; import { config } from "../config"; import { createChildLogger } from "../logger"; import { captureMessage } from "./messageCapture"; const logger = createChildLogger("backlog-sync"); +type BacklogChannel = Channel & { + messages: { + fetch(options: { limit: number; before?: string }): Promise<{ + size: number; + values(): IterableIterator; + }>; + }; +}; + +function hasMessageBacklog(channel: Channel): channel is BacklogChannel { + return "messages" in channel; +} + async function syncChannelMessages( - channel: any, + channel: BacklogChannel, cutoffTime: number, ): Promise { let before: string | undefined; @@ -77,6 +90,10 @@ export async function syncSelectedChannelBacklog( logger.warn({ guildId, channelId }, "Channel not found for backlog sync"); return 0; } + if (!hasMessageBacklog(channel)) { + logger.warn({ guildId, channelId }, "Channel cannot fetch message backlog"); + return 0; + } const cutoffTime = Date.now() - config.BACKLOG_SYNC_HOURS * 60 * 60 * 1000; logger.info( @@ -85,7 +102,7 @@ export async function syncSelectedChannelBacklog( ); try { - const count = await syncChannelMessages(channel as any, cutoffTime); + const count = await syncChannelMessages(channel, cutoffTime); logger.info( { channelId, count }, "Backlog sync completed for selected channel", diff --git a/src/moderation/broadcaster.ts b/src/moderation/broadcaster.ts index 641ad41..4190670 100644 --- a/src/moderation/broadcaster.ts +++ b/src/moderation/broadcaster.ts @@ -7,11 +7,14 @@ import type { ModerationWsEvent, } from "./types"; -type ClientLike = Pick; +export type BroadcasterClient = Pick; const log = createChildLogger("broadcaster"); -function sendJson(clients: Set, event: ModerationWsEvent): void { +function sendJson( + clients: Set, + event: ModerationWsEvent, +): void { const payload = JSON.stringify({ ...event, timestamp: Date.now() }); for (const client of clients) { if (client.readyState === 1) { @@ -28,14 +31,14 @@ function sendJson(clients: Set, event: ModerationWsEvent): void { } export function createBroadcaster() { - const clients = new Set(); + const clients = new Set(); return { - addClient(client: ClientLike) { + addClient(client: BroadcasterClient) { clients.add(client); log.debug({ clientCount: clients.size }, "Client added"); }, - removeClient(client: ClientLike) { + removeClient(client: BroadcasterClient) { clients.delete(client); log.debug({ clientCount: clients.size }, "Client removed"); }, diff --git a/src/moderation/messageStore.ts b/src/moderation/messageStore.ts index 3c4dc3d..9cd2601 100644 --- a/src/moderation/messageStore.ts +++ b/src/moderation/messageStore.ts @@ -1,4 +1,4 @@ -import { and, asc, desc, eq, isNull, or, sql } from "drizzle-orm"; +import { and, asc, desc, eq, isNull, or, type SQL, sql } from "drizzle-orm"; import { getDatabase } from "../database/drizzle"; import { attachmentsTable, messagesTable } from "../database/schema"; import { createChildLogger } from "../logger"; @@ -11,6 +11,29 @@ import type { const logger = createChildLogger("message-store"); +interface QueryBuilder extends PromiseLike { + from(...args: unknown[]): QueryBuilder; + where(...args: unknown[]): QueryBuilder; + orderBy(...args: unknown[]): QueryBuilder; + limit(...args: unknown[]): QueryBuilder; + offset(...args: unknown[]): QueryBuilder; + values(...args: unknown[]): QueryBuilder; + onConflictDoNothing(...args: unknown[]): QueryBuilder; + returning(...args: unknown[]): QueryBuilder; + set(...args: unknown[]): QueryBuilder; +} + +interface MessageDatabase { + select(...args: unknown[]): QueryBuilder; + selectDistinct(...args: unknown[]): QueryBuilder; + insert(...args: unknown[]): QueryBuilder; + update(...args: unknown[]): QueryBuilder; +} + +function db(): MessageDatabase { + return getDatabase() as unknown as MessageDatabase; +} + // Cursor helpers for pagination interface CursorData { created_at: number; @@ -36,8 +59,8 @@ export function decodeCursor(cursor?: string): CursorData | null { export async function insertMessage(message: MessageRecord): Promise { try { - const db = getDatabase() as any; - await db.insert(messagesTable).values(message).onConflictDoNothing(); + const database = db(); + await database.insert(messagesTable).values(message).onConflictDoNothing(); logger.debug( { messageId: message.id, channelId: message.channel_id }, @@ -59,14 +82,14 @@ export async function upsertMessageForCapture( message: MessageRecord, ): Promise { try { - const db = getDatabase() as any; + const database = db(); const messageWithAIStatus = { ...message, ai_status: "pending" as const, }; - const rows = await db - .insert(messagesTable) + const rows = await database + .insert>(messagesTable) .values(messageWithAIStatus) .onConflictDoNothing() .returning({ id: messagesTable.id }); @@ -95,8 +118,8 @@ export async function updateMessageAsEdited( editedAt: number, ): Promise { try { - const db = getDatabase() as any; - await db + const database = db(); + await database .update(messagesTable) .set({ edited_content: editedContent, @@ -130,8 +153,8 @@ export async function updateMessageAsDeleted( deletedAt: number, ): Promise { try { - const db = getDatabase() as any; - await db + const database = db(); + await database .update(messagesTable) .set({ deleted_at: deletedAt, @@ -158,8 +181,8 @@ export async function getMessagesByChannel( offset: number = 0, ): Promise { try { - const db = getDatabase() as any; - const rows = await db + const database = db(); + const rows = await database .select() .from(messagesTable) .where( @@ -189,8 +212,11 @@ export async function insertAttachment( attachment: AttachmentRecord, ): Promise { try { - const db = getDatabase() as any; - await db.insert(attachmentsTable).values(attachment).onConflictDoNothing(); + const database = db(); + await database + .insert(attachmentsTable) + .values(attachment) + .onConflictDoNothing(); logger.debug( { attachmentId: attachment.id, messageId: attachment.message_id }, @@ -214,8 +240,8 @@ export async function getAttachmentsByChannel( offset: number = 0, ): Promise { try { - const db = getDatabase() as any; - const rows = await db + const database = db(); + const rows = await database .select() .from(attachmentsTable) .where( @@ -247,8 +273,8 @@ export async function updateAttachmentAsUploaded( uploadedAt: number, ): Promise { try { - const db = getDatabase() as any; - await db + const database = db(); + await database .update(attachmentsTable) .set({ uploaded_url: uploadedUrl, @@ -278,8 +304,8 @@ export async function updateAttachmentAsFailedUpload( error: string, ): Promise { try { - const db = getDatabase() as any; - await db + const database = db(); + await database .update(attachmentsTable) .set({ upload_status: "failed", @@ -315,8 +341,8 @@ export async function updateMessageAIAnalysis( result: AIAnalysisUpdate, ): Promise { try { - const db = getDatabase() as any; - await db + const database = db(); + await database .update(messagesTable) .set({ ai_status: result.status, @@ -329,7 +355,7 @@ export async function updateMessageAIAnalysis( }) .where(eq(messagesTable.id, messageId)); - const rows = await db + const rows = await database .select() .from(messagesTable) .where(eq(messagesTable.id, messageId)); @@ -351,8 +377,8 @@ export async function getPendingAIAnalysisMessages( limit: number = 25, ): Promise { try { - const db = getDatabase() as any; - const rows = await db + const database = db(); + const rows = await database .select() .from(messagesTable) .where( @@ -378,8 +404,8 @@ export async function getMessageById( messageId: string, ): Promise { try { - const db = getDatabase() as any; - const rows = await db + const database = db(); + const rows = await database .select() .from(messagesTable) .where(eq(messagesTable.id, messageId)); @@ -401,8 +427,8 @@ export async function listMessages( query: MessageQuery, ): Promise> { try { - const db = getDatabase() as any; - const conditions: any[] = []; + const database = db(); + const conditions: SQL[] = []; // Apply filters if (query.guildId) { @@ -411,10 +437,7 @@ export async function listMessages( if (query.channelId) { conditions.push( - or( - eq(messagesTable.channel_id, query.channelId), - eq(messagesTable.thread_id, query.channelId), - ), + sql`(${messagesTable.channel_id} = ${query.channelId} or ${messagesTable.thread_id} = ${query.channelId})`, ); } @@ -427,11 +450,7 @@ export async function listMessages( } if (query.status && query.status.length > 0) { - conditions.push( - or( - ...query.status.map((status) => eq(messagesTable.ai_status, status)), - ), - ); + conditions.push(sql`${messagesTable.ai_status} in ${query.status}`); } // Text search @@ -445,20 +464,14 @@ export async function listMessages( const cursorData = decodeCursor(query.cursor); if (cursorData) { conditions.push( - or( - sql`${messagesTable.created_at} < ${cursorData.created_at}`, - and( - eq(messagesTable.created_at, cursorData.created_at), - sql`${messagesTable.id} < ${cursorData.id}`, - ), - ), + sql`(${messagesTable.created_at} < ${cursorData.created_at} or (${messagesTable.created_at} = ${cursorData.created_at} and ${messagesTable.id} < ${cursorData.id}))`, ); } } // Fetch limit + 1 to determine if there's a next page const fetchLimit = query.limit + 1; - const rows = await db + const rows = await database .select() .from(messagesTable) .where(conditions.length > 0 ? and(...conditions) : undefined) @@ -506,7 +519,7 @@ export async function getConversationContextBefore(input: { limit: number; }): Promise { try { - const db = getDatabase() as any; + const database = db(); const { channelId, threadId, beforeCreatedAt, limit } = input; // Query same thread if threadId exists, otherwise channelId @@ -514,7 +527,7 @@ export async function getConversationContextBefore(input: { ? eq(messagesTable.thread_id, threadId) : eq(messagesTable.channel_id, channelId); - const rows = await db + const rows = await database .select() .from(messagesTable) .where( @@ -547,11 +560,11 @@ export async function getPendingMessagesByConversation( limit: number = 25, ): Promise { try { - const db = getDatabase() as any; + const database = db(); // conversationKey is either thread_id or channel_id // Query both to safely handle the key - const rows = await db + const rows = await database .select() .from(messagesTable) .where( @@ -584,11 +597,11 @@ export async function getPendingConversationKeys( limit: number = 100, ): Promise { try { - const db = getDatabase() as any; + const database = db(); // Get distinct conversation keys (thread_id or channel_id) for pending messages - const rows = await db - .selectDistinct({ + const rows = await database + .selectDistinct>({ thread_id: messagesTable.thread_id, channel_id: messagesTable.channel_id, }) @@ -602,7 +615,7 @@ export async function getPendingConversationKeys( .limit(limit); const keys: string[] = []; - for (const row of rows as any[]) { + for (const row of rows) { const key = row.thread_id || row.channel_id; if (key && !keys.includes(key)) { keys.push(key); diff --git a/src/moderation/types.ts b/src/moderation/types.ts index c27a68a..2fafa72 100644 --- a/src/moderation/types.ts +++ b/src/moderation/types.ts @@ -1,8 +1,8 @@ -import type { ModerationBroadcaster } from "./broadcaster"; +import type { BroadcasterClient, ModerationBroadcaster } from "./broadcaster"; export type AIStatus = "pending" | "clean" | "warn" | "flagged" | "error"; -export type { ModerationBroadcaster }; +export type { BroadcasterClient, ModerationBroadcaster }; export interface MessageRecord { id: string; diff --git a/src/muxer-queue.ts b/src/muxer-queue.ts index 58a5de5..3c49144 100644 --- a/src/muxer-queue.ts +++ b/src/muxer-queue.ts @@ -8,8 +8,28 @@ import { createChildLogger } from "./logger"; const logger = createChildLogger("muxer-queue"); -// Type alias for backward compatibility -export type SqliteDatabase = any; +interface QueryBuilder extends PromiseLike { + from(...args: unknown[]): QueryBuilder; + where(...args: unknown[]): QueryBuilder; + orderBy(...args: unknown[]): QueryBuilder; + limit(...args: unknown[]): QueryBuilder; + values(...args: unknown[]): QueryBuilder; + onConflictDoNothing(...args: unknown[]): QueryBuilder; + onConflictDoUpdate(...args: unknown[]): QueryBuilder; + set(...args: unknown[]): QueryBuilder; + groupBy(...args: unknown[]): QueryBuilder; +} + +export interface SqliteDatabase { + select(...args: unknown[]): QueryBuilder; + insert(...args: unknown[]): QueryBuilder; + update(...args: unknown[]): QueryBuilder; + delete(...args: unknown[]): QueryBuilder; +} + +function db(): SqliteDatabase { + return getDrizzleDatabase() as unknown as SqliteDatabase; +} export interface MuxerJobData { userId: string; @@ -18,6 +38,22 @@ export interface MuxerJobData { outputDir: string; } +interface StoredJobRow { + id: string; + data: string; + status: "pending" | "processing" | "completed" | "failed"; + attempts: number; + maxAttempts: number; + createdAt: number; + updatedAt: number; + error: string | null; +} + +interface JobStatsRow { + status: "pending" | "processing" | "completed" | "failed"; + count: number | string | { count: number | string }; +} + interface StoredJob { id: string; data: string; @@ -31,7 +67,7 @@ interface StoredJob { // Export getDatabase for backward compatibility with webserver.ts export function getDatabase(): SqliteDatabase { - return getDrizzleDatabase() as any; + return db(); } export async function getPersistedValue( @@ -39,10 +75,10 @@ export async function getPersistedValue( fallback: T, ): Promise { await initializeDatabase(); - const db = getDrizzleDatabase() as any; + const database = db(); - const row = await db - .select() + const row = await database + .select>() .from(uiStateTable) .where(eq(uiStateTable.key, key)) .limit(1); @@ -61,9 +97,9 @@ export async function setPersistedValue( value: unknown, ): Promise { await initializeDatabase(); - const db = getDrizzleDatabase() as any; + const database = db(); - await db + await database .insert(uiStateTable) .values({ key, @@ -82,12 +118,12 @@ export async function setPersistedValue( export async function enqueueMuxerJob(data: MuxerJobData): Promise { try { await initializeDatabase(); - const db = getDrizzleDatabase() as any; + const database = db(); const jobId = `${data.userId}-${data.sessionId}`; const now = Date.now(); - await db + await database .insert(muxerJobsTable) .values({ id: jobId, @@ -120,16 +156,16 @@ export async function enqueueMuxerJob(data: MuxerJobData): Promise { export async function getPendingJobs(): Promise { await initializeDatabase(); - const db = getDrizzleDatabase() as any; + const database = db(); - const rows = await db - .select() + const rows = await database + .select() .from(muxerJobsTable) .where(eq(muxerJobsTable.status, "pending")) .orderBy(asc(muxerJobsTable.createdAt)) .limit(10); - return rows.map((row: any) => ({ + return rows.map((row) => ({ id: row.id, data: row.data, status: row.status as "pending" | "processing" | "completed" | "failed", @@ -147,11 +183,11 @@ export async function updateJobStatus( error?: string, ): Promise { await initializeDatabase(); - const db = getDrizzleDatabase() as any; + const database = db(); const now = Date.now(); if (status === "failed") { - await db + await database .update(muxerJobsTable) .set({ status, @@ -161,7 +197,7 @@ export async function updateJobStatus( }) .where(eq(muxerJobsTable.id, jobId)); } else { - await db + await database .update(muxerJobsTable) .set({ status, @@ -175,10 +211,10 @@ export async function updateJobStatus( export async function retryFailedJob(jobId: string): Promise { await initializeDatabase(); - const db = getDrizzleDatabase() as any; + const database = db(); - const jobs = await db - .select() + const jobs = await database + .select() .from(muxerJobsTable) .where(eq(muxerJobsTable.id, jobId)) .limit(1); @@ -198,7 +234,7 @@ export async function retryFailedJob(jobId: string): Promise { return false; } - await db + await database .update(muxerJobsTable) .set({ status: "pending", @@ -215,10 +251,10 @@ export async function cleanupCompletedJobs( olderThanMs: number = 24 * 60 * 60 * 1000, ): Promise { await initializeDatabase(); - const db = getDrizzleDatabase() as any; + const database = db(); const cutoffTime = Date.now() - olderThanMs; - const result = await db + const result = await database .delete(muxerJobsTable) .where( and( @@ -228,8 +264,8 @@ export async function cleanupCompletedJobs( ); const deletedCount = - typeof result === "object" && "rowsAffected" in result - ? result.rowsAffected + typeof result === "object" && result !== null && "rowsAffected" in result + ? Number(result.rowsAffected) : 0; logger.info({ deletedCount }, "Cleaned up completed jobs"); @@ -244,10 +280,10 @@ export async function getJobStats(): Promise<{ failed: number; }> { await initializeDatabase(); - const db = getDrizzleDatabase() as any; + const database = db(); - const rows = await db - .select({ + const rows = await database + .select({ status: muxerJobsTable.status, count: sql`COUNT(*)`, }) @@ -264,7 +300,7 @@ export async function getJobStats(): Promise<{ for (const row of rows) { const count = typeof row.count === "object" && "count" in row.count - ? (row.count as any).count + ? Number((row.count as { count: number | string }).count) : Number(row.count); if (row.status === "pending") stats.pending = count; else if (row.status === "processing") stats.processing = count; diff --git a/src/recorder.ts b/src/recorder.ts index 8c9b0ef..693cddb 100644 --- a/src/recorder.ts +++ b/src/recorder.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { + type DiscordGatewayAdapterCreator, EndBehaviorType, entersState, getVoiceConnection, @@ -41,7 +42,8 @@ export async function startRecording( const connection = joinVoiceChannel({ channelId: channel.id, guildId: channel.guild.id, - adapterCreator: channel.guild.voiceAdapterCreator as any, + adapterCreator: channel.guild + .voiceAdapterCreator as DiscordGatewayAdapterCreator, selfDeaf: false, selfMute: false, debug: true, diff --git a/src/voiceController.ts b/src/voiceController.ts index 58917c6..a0d308d 100644 --- a/src/voiceController.ts +++ b/src/voiceController.ts @@ -88,13 +88,16 @@ export class VoiceController { await guild.channels.fetch().catch(() => null); const threads: ChannelSummary[] = []; + type ThreadFetchResult = { + threads: Map; + }; for (const channel of guild.channels.cache.values()) { const threadParent = channel as typeof channel & { threads?: { fetch: (options: { archived: boolean; limit: number; - }) => Promise; + }) => Promise; }; }; if (!threadParent.threads?.fetch) continue; diff --git a/src/webserver.ts b/src/webserver.ts index 1d97e39..fa96c23 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -10,6 +10,7 @@ import { AppError } from "./errors"; import { createChildLogger, logger } from "./logger"; import { getMetrics, uptimeGauge } from "./metrics"; import { createBroadcaster } from "./moderation/broadcaster"; +import type { ModerationBroadcaster } from "./moderation/types"; import { getPersistedValue, setPersistedValue } from "./muxer-queue"; import { discordPlayer } from "./player"; import { createAnalysisRoutes } from "./routes/analysisRoutes"; @@ -26,6 +27,15 @@ const activeUsers = new Map< { username: string; avatar: string; speaking: boolean } >(); +type VoiceGlobals = typeof globalThis & { + moderationBroadcaster?: ModerationBroadcaster; + broadcastPcmToWeb?: (chunk: Buffer, userId: string) => void; + updateActiveUser?: ( + userId: string, + data: { username: string; avatar: string; speaking: boolean }, + ) => void; +}; + interface SharedUIState { selectedGuild: string; selectedVoiceChannel: string; @@ -118,7 +128,7 @@ export async function startWebserver( // Create broadcaster instance const broadcaster = createBroadcaster(); - (globalThis as any).moderationBroadcaster = broadcaster; + (globalThis as VoiceGlobals).moderationBroadcaster = broadcaster; // Security headers. CSP disabled because the current static UI uses inline scripts/styles. app.use( @@ -196,7 +206,10 @@ export async function startWebserver( app.use("/api", createSyncRoutes(_client)); // Inbound: Discord PCM → tagged chunks → browser - (global as any).broadcastPcmToWeb = (chunk: Buffer, userId: string) => { + (globalThis as VoiceGlobals).broadcastPcmToWeb = ( + chunk: Buffer, + userId: string, + ) => { let hash = 0; for (let i = 0; i < userId.length; i++) { hash = (hash << 5) - hash + userId.charCodeAt(i); @@ -210,7 +223,7 @@ export async function startWebserver( } }; - (global as any).updateActiveUser = ( + (globalThis as VoiceGlobals).updateActiveUser = ( userId: string, data: { username: string; avatar: string; speaking: boolean }, ) => { @@ -327,7 +340,7 @@ export async function startWebserver( ); ws.send(JSON.stringify({ type: "ui_state", state: getSharedUIState() })); - ws.on("message", (data: any) => { + ws.on("message", (data: Buffer | ArrayBuffer | Buffer[]) => { if (!Buffer.isBuffer(data)) return; lastBrowserAudioTime = Date.now(); diff --git a/tests/database.test.ts b/tests/database.test.ts index ace7b23..969ec01 100644 --- a/tests/database.test.ts +++ b/tests/database.test.ts @@ -4,9 +4,9 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; const originalEnv = process.env; describe("Drizzle ORM Database", () => { - let config: any; - let drizzle: any; - let logger: any; + let config: typeof import("../src/config").config; + let drizzle: typeof import("../src/database/drizzle"); + let logger: ReturnType; beforeAll(async () => { // Set up environment for config loading diff --git a/tests/moderation/broadcaster.test.ts b/tests/moderation/broadcaster.test.ts index 4869122..d5a9337 100644 --- a/tests/moderation/broadcaster.test.ts +++ b/tests/moderation/broadcaster.test.ts @@ -1,17 +1,70 @@ +import type { Mock } from "vitest"; import { describe, expect, it, vi } from "vitest"; -import { createBroadcaster } from "../../src/moderation/broadcaster"; +import { + type BroadcasterClient, + createBroadcaster, +} from "../../src/moderation/broadcaster"; +import type { + AttachmentRecord, + MessageRecord, +} from "../../src/moderation/types"; -function client() { +type TestClient = BroadcasterClient & { send: Mock }; + +function client(): TestClient { return { readyState: 1, send: vi.fn() }; } +function messageRecord(overrides: Partial = {}): MessageRecord { + return { + id: "m1", + guild_id: "guild-1", + channel_id: "channel-1", + thread_id: null, + user_id: "user-1", + username: "alice", + avatar_url: null, + content: "test", + edited_content: null, + created_at: 1, + edited_at: null, + deleted_at: null, + type: "text", + metadata: null, + ...overrides, + }; +} + +function attachmentRecord( + overrides: Partial = {}, +): AttachmentRecord { + return { + id: "a1", + message_id: "m1", + guild_id: "guild-1", + channel_id: "channel-1", + thread_id: null, + user_id: "user-1", + filename: "image.png", + size: 1, + type: "image/png", + discord_url: "https://example.com/image.png", + uploaded_url: null, + upload_status: "pending", + upload_error: null, + created_at: 1, + uploaded_at: null, + ...overrides, + }; +} + describe("createBroadcaster", () => { it("sends JSON events to open clients", () => { const ws = client(); const broadcaster = createBroadcaster(); - broadcaster.addClient(ws as any); - broadcaster.messageAnalyzed({ id: "m1", ai_status: "clean" } as any); + broadcaster.addClient(ws); + broadcaster.messageAnalyzed(messageRecord({ ai_status: "clean" })); expect(ws.send).toHaveBeenCalledTimes(1); expect(JSON.parse(ws.send.mock.calls[0][0])).toMatchObject({ @@ -21,10 +74,10 @@ describe("createBroadcaster", () => { }); it("skips closed clients", () => { - const ws = { readyState: 3, send: vi.fn() }; + const ws: TestClient = { readyState: 3, send: vi.fn() }; const broadcaster = createBroadcaster(); - broadcaster.addClient(ws as any); + broadcaster.addClient(ws); broadcaster.messageDeleted({ id: "m1", deleted_at: 123 }); expect(ws.send).not.toHaveBeenCalled(); @@ -36,14 +89,11 @@ describe("createBroadcaster", () => { const ws3 = client(); const broadcaster = createBroadcaster(); - broadcaster.addClient(ws1 as any); - broadcaster.addClient(ws2 as any); - broadcaster.addClient(ws3 as any); + broadcaster.addClient(ws1); + broadcaster.addClient(ws2); + broadcaster.addClient(ws3); - broadcaster.messageCreated({ - id: "m1", - content: "test", - } as any); + broadcaster.messageCreated(messageRecord()); expect(ws1.send).toHaveBeenCalledTimes(1); expect(ws2.send).toHaveBeenCalledTimes(1); @@ -61,14 +111,14 @@ describe("createBroadcaster", () => { throw new Error("Send failed"); }); - broadcaster.addClient(ws1 as any); - broadcaster.addClient(ws2 as any); - broadcaster.addClient(ws3 as any); + broadcaster.addClient(ws1); + broadcaster.addClient(ws2); + broadcaster.addClient(ws3); broadcaster.messageUpdated({ id: "m1", content: "updated", - } as any); + }); // ws1 attempted send (threw) expect(ws1.send).toHaveBeenCalledTimes(1); @@ -84,16 +134,16 @@ describe("createBroadcaster", () => { expect(broadcaster.clientCount()).toBe(0); - broadcaster.addClient(ws1 as any); + broadcaster.addClient(ws1); expect(broadcaster.clientCount()).toBe(1); - broadcaster.addClient(ws2 as any); + broadcaster.addClient(ws2); expect(broadcaster.clientCount()).toBe(2); - broadcaster.removeClient(ws1 as any); + broadcaster.removeClient(ws1); expect(broadcaster.clientCount()).toBe(1); - broadcaster.removeClient(ws2 as any); + broadcaster.removeClient(ws2); expect(broadcaster.clientCount()).toBe(0); }); @@ -101,11 +151,8 @@ describe("createBroadcaster", () => { const ws = client(); const broadcaster = createBroadcaster(); - broadcaster.addClient(ws as any); - broadcaster.attachmentCreated({ - id: "a1", - message_id: "m1", - } as any); + broadcaster.addClient(ws); + broadcaster.attachmentCreated(attachmentRecord()); expect(ws.send).toHaveBeenCalledTimes(1); const payload = JSON.parse(ws.send.mock.calls[0][0]); diff --git a/tests/moderation/messageStoreQueries.test.ts b/tests/moderation/messageStoreQueries.test.ts index 1dce619..034a643 100644 --- a/tests/moderation/messageStoreQueries.test.ts +++ b/tests/moderation/messageStoreQueries.test.ts @@ -16,6 +16,14 @@ import { } from "../../src/moderation/messageStore"; import type { MessageRecord } from "../../src/moderation/types"; +interface TestDatabase { + run(sql: string): void; +} + +function getTestDatabase(): TestDatabase { + return getDatabase() as unknown as TestDatabase; +} + const logger = createChildLogger("messageStoreQueries.test"); describe("message cursor helpers", () => { @@ -36,7 +44,7 @@ describe("message query integration tests", () => { beforeAll(async () => { await initializeDatabase(); // Create tables using Drizzle schema (SQLite doesn't support migrations with PostgreSQL syntax) - const db = getDatabase() as any; + const db = getTestDatabase(); try { // Create messages table await db.run(` @@ -75,7 +83,7 @@ describe("message query integration tests", () => { beforeEach(async () => { // Clear messages table before each test try { - const db = getDatabase() as any; + const db = getTestDatabase(); await db.run(`DELETE FROM "messages"`); } catch (error) { logger.debug({ error }, "Could not clear messages table");