diff --git a/src/database/migrate.ts b/src/database/migrate.ts index 9570451..ed53e81 100644 --- a/src/database/migrate.ts +++ b/src/database/migrate.ts @@ -1,13 +1,22 @@ import "dotenv/config"; import Database from "better-sqlite3"; +import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3"; import { migrate as migrateSqlite } from "drizzle-orm/better-sqlite3/migrator"; -import { migrate } from "drizzle-orm/node-postgres/migrator"; +import { migrate as migratePostgres } from "drizzle-orm/node-postgres/migrator"; import { config } from "../config"; import { createChildLogger } from "../logger"; import { initializeDatabase } from "./drizzle"; const logger = createChildLogger("migrate"); +export async function initializeMigrationSqliteDatabase( + path = ".muxer-queue.db", +) { + const sqlite = new Database(path); + sqlite.pragma("journal_mode = WAL"); + return { sqlite, db: drizzleSqlite(sqlite) }; +} + export async function runMigrations(): Promise { try { logger.info("Starting database migrations"); @@ -15,14 +24,16 @@ export async function runMigrations(): Promise { if (config.DATABASE_TYPE === "postgres") { logger.info("Running PostgreSQL migrations"); const db = await initializeDatabase(); - await migrate(db as any, { migrationsFolder: "./drizzle/migrations" }); + await migratePostgres(db as any, { + migrationsFolder: "./drizzle/migrations", + }); logger.info("PostgreSQL migrations completed successfully"); } else { logger.info("Running SQLite migrations"); - const sqlite = new Database(".muxer-queue.db"); - sqlite.pragma("journal_mode = WAL"); - const db = require("drizzle-orm/better-sqlite3").drizzle(sqlite); + const { sqlite, db } = await initializeMigrationSqliteDatabase(); migrateSqlite(db, { migrationsFolder: "./drizzle/migrations" }); + // Ensure the SQLite connection is closed after migrations + sqlite.close(); logger.info("SQLite migrations completed successfully"); } } catch (error) { @@ -35,7 +46,7 @@ export async function runMigrations(): Promise { } // Run migrations if called directly -if (require.main === module) { +if (import.meta.url === `file://${process.argv[1]}`) { runMigrations() .then(() => { logger.info("Migrations completed"); diff --git a/src/mock-crc.ts b/src/mock-crc.ts index ddda4a4..e73b8ed 100644 --- a/src/mock-crc.ts +++ b/src/mock-crc.ts @@ -8,7 +8,7 @@ for (let i = 0; i < 256; i++) { CRC_TABLE[i] = r >>> 0; } -const Module = require("module"); +const Module = require("node:module"); const originalRequire = Module.prototype.require; Module.prototype.require = function (id: string) { if (id === "node-crc") { diff --git a/src/moderation/messageCapture.ts b/src/moderation/messageCapture.ts index ddaa452..5473fc9 100644 --- a/src/moderation/messageCapture.ts +++ b/src/moderation/messageCapture.ts @@ -34,9 +34,9 @@ export async function captureMessage( 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, + 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, @@ -67,7 +67,7 @@ export async function captureMessage( guild_id: message.guildId!, channel_id: location.channelId, thread_id: location.threadId, - user_id: message.author!.id, + user_id: message.author?.id, filename: attachment.name || "unknown", size: attachment.size, type: attachment.contentType || "application/octet-stream", diff --git a/src/muxer-aup3.ts b/src/muxer-aup3.ts index b7f0fb9..99039cc 100644 --- a/src/muxer-aup3.ts +++ b/src/muxer-aup3.ts @@ -1,6 +1,6 @@ +import fs from "node:fs"; +import path from "node:path"; import ffmpeg from "fluent-ffmpeg"; -import fs from "fs"; -import path from "path"; const recordingsDir = process.env.RECORDINGS_DIR ?? "./recordings"; diff --git a/src/muxer.ts b/src/muxer.ts index ada832d..8eeb2cd 100644 --- a/src/muxer.ts +++ b/src/muxer.ts @@ -1,6 +1,6 @@ +import fs from "node:fs"; +import path from "node:path"; import ffmpeg from "fluent-ffmpeg"; -import fs from "fs"; -import path from "path"; const recordingsDir = process.env.RECORDINGS_DIR ?? "./recordings"; diff --git a/src/packetFilter.ts b/src/packetFilter.ts index aa698d9..20d680b 100644 --- a/src/packetFilter.ts +++ b/src/packetFilter.ts @@ -1,4 +1,4 @@ -import { Transform, TransformCallback } from "stream"; +import { Transform, TransformCallback } from "node:stream"; /** * Transform stream untuk memfilter audio packets yang terlalu kecil diff --git a/src/player.ts b/src/player.ts index 7b15854..648e235 100644 --- a/src/player.ts +++ b/src/player.ts @@ -1,3 +1,4 @@ +import { Readable } from "node:stream"; import { AudioPlayer, AudioPlayerStatus, @@ -6,7 +7,6 @@ import { StreamType, VoiceConnection, } from "@discordjs/voice"; -import { Readable } from "stream"; export class DiscordPlayer { private player: AudioPlayer; diff --git a/src/validation.ts b/src/validation.ts index 3fb2bd6..46c7353 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -1,38 +1,22 @@ -import { plainToClass } from "class-transformer"; -import { IsBoolean, IsString, validate } from "class-validator"; +import { z } from "zod"; -export class UserStateUpdate { - @IsString() - userId!: string; +export const userStateUpdateSchema = z.object({ + userId: z.string(), + username: z.string(), + avatar: z.string(), + speaking: z.boolean(), +}); - @IsString() - username!: string; +export type UserStateUpdate = z.infer; - @IsString() - avatar!: string; - - @IsBoolean() - speaking!: boolean; -} - -export class AudioMessage { - data!: Buffer; - userId!: string; +export interface AudioMessage { + data: Buffer; + userId: string; } export async function validateUserStateUpdate( data: unknown, ): Promise { - if (typeof data !== "object" || data === null) { - return null; - } - - const obj = plainToClass(UserStateUpdate, data); - const errors = await validate(obj); - - if (errors.length > 0) { - return null; - } - - return obj; + const result = userStateUpdateSchema.safeParse(data); + return result.success ? result.data : null; } diff --git a/src/webserver.ts b/src/webserver.ts index fd71d66..1d97e39 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -1,9 +1,9 @@ +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; import type { Client } from "discord.js-selfbot-v13"; import express from "express"; -import fs from "fs"; import helmet from "helmet"; -import http from "http"; -import path from "path"; import * as prism from "prism-media"; import { WebSocketServer } from "ws"; import { AppError } from "./errors"; diff --git a/tests/validation.test.ts b/tests/validation.test.ts new file mode 100644 index 0000000..1228677 --- /dev/null +++ b/tests/validation.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "vitest"; +import { UserStateUpdate, validateUserStateUpdate } from "../src/validation"; + +test("valid object returns typed object", async () => { + const input = { + userId: "123", + username: "testuser", + avatar: "avatar.png", + speaking: true, + }; + const result = await validateUserStateUpdate(input); + expect(result).toEqual(input as UserStateUpdate); +}); + +test("non-object input returns null", async () => { + // @ts-expect-error testing invalid input + const result = await validateUserStateUpdate("not an object"); + expect(result).toBeNull(); +}); + +test("invalid field types return null", async () => { + const input = { + userId: "123", + username: "testuser", + avatar: "avatar.png", + speaking: "true", // invalid type + }; + const result = await validateUserStateUpdate(input as unknown); + expect(result).toBeNull(); +});