fix: adapt code to modernized libraries

- Migrate validation.ts from class-transformer/class-validator to Zod
- Apply Biome auto-fixes (import sorting, nodejs protocol)
- Fix duplicate type identifier in validation.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-05-15 04:25:06 +07:00
parent 203aa9a589
commit 29fcde69e4
10 changed files with 74 additions and 49 deletions

View File

@@ -1,13 +1,22 @@
import "dotenv/config"; import "dotenv/config";
import Database from "better-sqlite3"; 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 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 { config } from "../config";
import { createChildLogger } from "../logger"; import { createChildLogger } from "../logger";
import { initializeDatabase } from "./drizzle"; import { initializeDatabase } from "./drizzle";
const logger = createChildLogger("migrate"); 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<void> { export async function runMigrations(): Promise<void> {
try { try {
logger.info("Starting database migrations"); logger.info("Starting database migrations");
@@ -15,14 +24,16 @@ export async function runMigrations(): Promise<void> {
if (config.DATABASE_TYPE === "postgres") { if (config.DATABASE_TYPE === "postgres") {
logger.info("Running PostgreSQL migrations"); logger.info("Running PostgreSQL migrations");
const db = await initializeDatabase(); 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"); logger.info("PostgreSQL migrations completed successfully");
} else { } else {
logger.info("Running SQLite migrations"); logger.info("Running SQLite migrations");
const sqlite = new Database(".muxer-queue.db"); const { sqlite, db } = await initializeMigrationSqliteDatabase();
sqlite.pragma("journal_mode = WAL");
const db = require("drizzle-orm/better-sqlite3").drizzle(sqlite);
migrateSqlite(db, { migrationsFolder: "./drizzle/migrations" }); migrateSqlite(db, { migrationsFolder: "./drizzle/migrations" });
// Ensure the SQLite connection is closed after migrations
sqlite.close();
logger.info("SQLite migrations completed successfully"); logger.info("SQLite migrations completed successfully");
} }
} catch (error) { } catch (error) {
@@ -35,7 +46,7 @@ export async function runMigrations(): Promise<void> {
} }
// Run migrations if called directly // Run migrations if called directly
if (require.main === module) { if (import.meta.url === `file://${process.argv[1]}`) {
runMigrations() runMigrations()
.then(() => { .then(() => {
logger.info("Migrations completed"); logger.info("Migrations completed");

View File

@@ -8,7 +8,7 @@ for (let i = 0; i < 256; i++) {
CRC_TABLE[i] = r >>> 0; CRC_TABLE[i] = r >>> 0;
} }
const Module = require("module"); const Module = require("node:module");
const originalRequire = Module.prototype.require; const originalRequire = Module.prototype.require;
Module.prototype.require = function (id: string) { Module.prototype.require = function (id: string) {
if (id === "node-crc") { if (id === "node-crc") {

View File

@@ -34,9 +34,9 @@ export async function captureMessage(
guild_id: message.guildId!, guild_id: message.guildId!,
channel_id: location.channelId, channel_id: location.channelId,
thread_id: location.threadId, thread_id: location.threadId,
user_id: message.author!.id, user_id: message.author?.id,
username: message.author!.username, username: message.author?.username,
avatar_url: message.author!.avatarURL() || null, avatar_url: message.author?.avatarURL() || null,
content: getDisplayContent(message), content: getDisplayContent(message),
edited_content: null, edited_content: null,
created_at: message.createdTimestamp, created_at: message.createdTimestamp,
@@ -67,7 +67,7 @@ export async function captureMessage(
guild_id: message.guildId!, guild_id: message.guildId!,
channel_id: location.channelId, channel_id: location.channelId,
thread_id: location.threadId, thread_id: location.threadId,
user_id: message.author!.id, user_id: message.author?.id,
filename: attachment.name || "unknown", filename: attachment.name || "unknown",
size: attachment.size, size: attachment.size,
type: attachment.contentType || "application/octet-stream", type: attachment.contentType || "application/octet-stream",

View File

@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import ffmpeg from "fluent-ffmpeg"; import ffmpeg from "fluent-ffmpeg";
import fs from "fs";
import path from "path";
const recordingsDir = process.env.RECORDINGS_DIR ?? "./recordings"; const recordingsDir = process.env.RECORDINGS_DIR ?? "./recordings";

View File

@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import ffmpeg from "fluent-ffmpeg"; import ffmpeg from "fluent-ffmpeg";
import fs from "fs";
import path from "path";
const recordingsDir = process.env.RECORDINGS_DIR ?? "./recordings"; const recordingsDir = process.env.RECORDINGS_DIR ?? "./recordings";

View File

@@ -1,4 +1,4 @@
import { Transform, TransformCallback } from "stream"; import { Transform, TransformCallback } from "node:stream";
/** /**
* Transform stream untuk memfilter audio packets yang terlalu kecil * Transform stream untuk memfilter audio packets yang terlalu kecil

View File

@@ -1,3 +1,4 @@
import { Readable } from "node:stream";
import { import {
AudioPlayer, AudioPlayer,
AudioPlayerStatus, AudioPlayerStatus,
@@ -6,7 +7,6 @@ import {
StreamType, StreamType,
VoiceConnection, VoiceConnection,
} from "@discordjs/voice"; } from "@discordjs/voice";
import { Readable } from "stream";
export class DiscordPlayer { export class DiscordPlayer {
private player: AudioPlayer; private player: AudioPlayer;

View File

@@ -1,38 +1,22 @@
import { plainToClass } from "class-transformer"; import { z } from "zod";
import { IsBoolean, IsString, validate } from "class-validator";
export class UserStateUpdate { export const userStateUpdateSchema = z.object({
@IsString() userId: z.string(),
userId!: string; username: z.string(),
avatar: z.string(),
speaking: z.boolean(),
});
@IsString() export type UserStateUpdate = z.infer<typeof userStateUpdateSchema>;
username!: string;
@IsString() export interface AudioMessage {
avatar!: string; data: Buffer;
userId: string;
@IsBoolean()
speaking!: boolean;
}
export class AudioMessage {
data!: Buffer;
userId!: string;
} }
export async function validateUserStateUpdate( export async function validateUserStateUpdate(
data: unknown, data: unknown,
): Promise<UserStateUpdate | null> { ): Promise<UserStateUpdate | null> {
if (typeof data !== "object" || data === null) { const result = userStateUpdateSchema.safeParse(data);
return null; return result.success ? result.data : null;
}
const obj = plainToClass(UserStateUpdate, data);
const errors = await validate(obj);
if (errors.length > 0) {
return null;
}
return obj;
} }

View File

@@ -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 type { Client } from "discord.js-selfbot-v13";
import express from "express"; import express from "express";
import fs from "fs";
import helmet from "helmet"; import helmet from "helmet";
import http from "http";
import path from "path";
import * as prism from "prism-media"; import * as prism from "prism-media";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
import { AppError } from "./errors"; import { AppError } from "./errors";

30
tests/validation.test.ts Normal file
View File

@@ -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();
});