ci: fix lint errors for CI
Deploy to VPS / deploy (push) Successful in 1m47s

- Fix noImplicitAnyLet: add type to let match variable
- Fix noAssignInExpressions: use matchAll() + for-of instead of while
- Suppress useExhaustiveDependencies in mascot scroll effect
- Suppress useSemanticElements for message card click handler
This commit is contained in:
asepharyana
2026-07-26 12:01:51 +07:00
parent 42595a75f3
commit cedce85087
22 changed files with 109 additions and 87 deletions
+6 -6
View File
@@ -14,12 +14,12 @@
"packages/**/*.ts", "packages/**/*.ts",
"*.json", "*.json",
"*.ts", "*.ts",
"!vendor/**", "!vendor",
"!.claude/**", "!.claude",
"!services/**/dist/**", "!services/**/dist",
"!packages/**/dist/**", "!packages/**/dist",
"!services/**/.next/**", "!services/**/.next",
"!services/**/out/**" "!services/**/out"
] ]
}, },
"formatter": { "formatter": {
@@ -4,7 +4,7 @@ import {
COMMAND_VOICE_CHANNELS, COMMAND_VOICE_CHANNELS,
COMMAND_VOICE_CONNECT, COMMAND_VOICE_CONNECT,
COMMAND_VOICE_DISCONNECT, COMMAND_VOICE_DISCONNECT,
CommandReply, type CommandReply,
VOICE_STATUS_KEY, VOICE_STATUS_KEY,
} from "@bete/shared"; } from "@bete/shared";
import { import {
@@ -1,7 +1,4 @@
import { import { AppError, ValidationError } from "@bete/shared/errors";
AppError,
ValidationError,
} from "@bete/shared/errors";
import { createChildLogger } from "@bete/shared/logger"; import { createChildLogger } from "@bete/shared/logger";
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
+1 -1
View File
@@ -2,7 +2,7 @@ import type { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13"; import type { Client } from "discord.js-selfbot-v13";
import type { CommandHandler } from "../modules/command-handler/commandHandler.js"; import type { CommandHandler } from "../modules/command-handler/commandHandler.js";
import type { EventBroadcaster } from "../modules/event-broadcaster/index.js"; import type { EventBroadcaster } from "../modules/event-broadcaster/index.js";
import { stopMetricsServer } from "../modules/gateway-metrics/index.js"; import type { stopMetricsServer } from "../modules/gateway-metrics/index.js";
import type { VoicePcmWsClient } from "../modules/voice-pcm-ws/index.js"; import type { VoicePcmWsClient } from "../modules/voice-pcm-ws/index.js";
import { stopMuxerWorker } from "../modules/voice-recording/muxer.js"; import { stopMuxerWorker } from "../modules/voice-recording/muxer.js";
import type { VoiceController } from "../modules/voice-recording/voiceController.js"; import type { VoiceController } from "../modules/voice-recording/voiceController.js";
@@ -2,7 +2,7 @@ import { createChildLogger } from "@bete/shared/logger";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { getDatabase } from "../../shared/database/drizzle.js"; import { getDatabase } from "../../shared/database/drizzle.js";
import { import {
ChannelCulture, type ChannelCulture,
channelCulturesTable, channelCulturesTable,
} from "../../shared/database/schema.js"; } from "../../shared/database/schema.js";
@@ -133,7 +133,7 @@ Pedoman ini mencerminkan nilai-nilai yang dijunjung server. Terapkan dengan bija
Prioritas tertinggi (ANCAMAN KESELAMATAN): Prioritas tertinggi (ANCAMAN KESELAMATAN):
- child_safety, violence, illegal_content — flag jika ada indikasi nyata - child_safety, violence, illegal_content — flag jika ada indikasi nyata
- self_harm: BEDAKAN antara ancaman klinis nyata dengan hiperbola stres remaja (mis. "mati aja gua ngerjain tugas", "bunuh aku sekarang"). Hiperbola ekspresi stres adalah AMAN dan JANGAN di-flag sebagai self_harm. - self_harm: BEDAKAN antara ancaman klinis nyata dengan hiperbola stres remaja (mis. "mati aja gua ngerjain tugas", "bunuh aku sekarang"). Hiperbola ekspresi stres adalah AMAN dan JANGAN di-flag sebagai self_harm.
- Pornografi/NSFW, ajakan seksual, roleplay seksual → "sexual_content". PENGECUALIAN: Roleplay aksi kasual/RPG non-seksual (misal \*memeluk teman karena sedih\*, \*menebas naga\*) adalah AMAN dan bukan sexual_content. - Pornografi/NSFW, ajakan seksual, roleplay seksual → "sexual_content". PENGECUALIAN: Roleplay aksi kasual/RPG non-seksual (misal *memeluk teman karena sedih*, *menebas naga*) adalah AMAN dan bukan sexual_content.
- Judi/promosi judi → "gambling" - Judi/promosi judi → "gambling"
- Narkoba/promosi → "drugs" - Narkoba/promosi → "drugs"
@@ -425,7 +425,7 @@ const ALL_EXAMPLES: ExampleDef[] = [
id: "13", id: "13",
title: "Obfuscation / Zalgo Text (Evasion)", title: "Obfuscation / Zalgo Text (Evasion)",
input: input:
'[target] id=14141 user=hater: ++++++K1[[ your $€/F" "~\`| \\\\0ve $ 1F ¥°U |}iE ®©', '[target] id=14141 user=hater: ++++++K1[[ your $€/F" "~`| \\\\0ve $ 1F ¥°U |}iE ®©',
output: output:
'{"results":[{"message_id":"14141","status":"flagged","flags":["harassment","hate_speech"],"score":0.95,"categories":["harassment","hate_speech"],"severity":"critical","confidence":0.95,"recommended_action":"delete","policy_version":"default-2026-05-30","evidence":["++++++K1[[ your $€/F","\\\\\\\\0ve $ 1F ¥°U |}iE"],"analysis":"Pesan menggunakan teknik obfuscation/simbol untuk menyembunyikan frasa \'Kill yourself I love if you die\'. Ini adalah ancaman dan pelecehan berat yang disamarkan."}]}', '{"results":[{"message_id":"14141","status":"flagged","flags":["harassment","hate_speech"],"score":0.95,"categories":["harassment","hate_speech"],"severity":"critical","confidence":0.95,"recommended_action":"delete","policy_version":"default-2026-05-30","evidence":["++++++K1[[ your $€/F","\\\\\\\\0ve $ 1F ¥°U |}iE"],"analysis":"Pesan menggunakan teknik obfuscation/simbol untuk menyembunyikan frasa \'Kill yourself I love if you die\'. Ini adalah ancaman dan pelecehan berat yang disamarkan."}]}',
modes: ["text", "media", "mixed"], modes: ["text", "media", "mixed"],
@@ -522,7 +522,7 @@ const ALL_EXAMPLES: ExampleDef[] = [
}, },
{ {
id: "24", id: "24",
title: "Nabi palsu (SARA agama, Ba\'al)", title: "Nabi palsu (SARA agama, Ba'al)",
input: input:
"[target] id=24245 user=provokator: nabi palsu ba'al, sembah aja patung", "[target] id=24245 user=provokator: nabi palsu ba'al, sembah aja patung",
output: output:
@@ -1,6 +1,6 @@
import { createChildLogger } from "@bete/shared/logger"; import { createChildLogger } from "@bete/shared/logger";
import type { z } from "zod"; import type { z } from "zod";
import { import type {
RecommendedActionSchema, RecommendedActionSchema,
SeveritySchema, SeveritySchema,
} from "./moderationSchemas.js"; } from "./moderationSchemas.js";
@@ -2,7 +2,7 @@ import { createChildLogger } from "@bete/shared/logger";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { getDatabase } from "../../shared/database/drizzle.js"; import { getDatabase } from "../../shared/database/drizzle.js";
import { import {
UserProfile, type UserProfile,
userProfilesTable, userProfilesTable,
} from "../../shared/database/schema.js"; } from "../../shared/database/schema.js";
@@ -3,7 +3,7 @@ import { and, desc, eq } from "drizzle-orm";
import { getDatabase } from "../../shared/database/drizzle.js"; import { getDatabase } from "../../shared/database/drizzle.js";
import { import {
messagesTable, messagesTable,
UserReputation, type UserReputation,
userReputationsTable, userReputationsTable,
} from "../../shared/database/schema.js"; } from "../../shared/database/schema.js";
@@ -1,4 +1,4 @@
import { type CommandMessage, type CommandReply } from "@bete/shared"; import type { CommandMessage, CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger"; import { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13"; import type { Client } from "discord.js-selfbot-v13";
@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { type CommandMessage, type CommandReply } from "@bete/shared"; import type { CommandMessage, CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger"; import { createChildLogger } from "@bete/shared/logger";
import { StreamType } from "@discordjs/voice"; import { StreamType } from "@discordjs/voice";
import { import {
@@ -1,4 +1,4 @@
import { type CommandMessage, type CommandReply } from "@bete/shared"; import type { CommandMessage, CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger"; import { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13"; import type { Client } from "discord.js-selfbot-v13";
import { createModerationAction } from "../message-capture/messageStore.js"; import { createModerationAction } from "../message-capture/messageStore.js";
@@ -153,8 +153,8 @@ export function getCustomEmojiMetadata(
): RichMessageMetadata["customEmojis"] { ): RichMessageMetadata["customEmojis"] {
const CUSTOM_EMOJI_PATTERN = /<(a)?:([a-zA-Z0-9_]+):(\d+)>/g; const CUSTOM_EMOJI_PATTERN = /<(a)?:([a-zA-Z0-9_]+):(\d+)>/g;
const emojis: CustomEmojiEvidence[] = []; const emojis: CustomEmojiEvidence[] = [];
let match; const matches = [...message.content.matchAll(CUSTOM_EMOJI_PATTERN)];
while ((match = CUSTOM_EMOJI_PATTERN.exec(message.content)) !== null) { for (const match of matches) {
const [, animated, name, id] = match; const [, animated, name, id] = match;
const ext = animated ? "gif" : "png"; const ext = animated ? "gif" : "png";
emojis.push({ emojis.push({
@@ -1,5 +1,5 @@
import { type ChildProcess, spawn } from "node:child_process"; import { type ChildProcess, spawn } from "node:child_process";
import { PassThrough, Readable } from "node:stream"; import { PassThrough, type Readable } from "node:stream";
import { createChildLogger } from "@bete/shared/logger"; import { createChildLogger } from "@bete/shared/logger";
import { StreamType } from "@discordjs/voice"; import { StreamType } from "@discordjs/voice";
@@ -1,4 +1,4 @@
import { Transform, TransformCallback } from "node:stream"; import { Transform, type TransformCallback } from "node:stream";
import { createChildLogger } from "@bete/shared/logger"; import { createChildLogger } from "@bete/shared/logger";
const logger = createChildLogger("packet-filter"); const logger = createChildLogger("packet-filter");
@@ -1,13 +1,13 @@
import { Readable } from "node:stream"; import type { Readable } from "node:stream";
import { createChildLogger } from "@bete/shared/logger"; import { createChildLogger } from "@bete/shared/logger";
import { import {
AudioPlayer, type AudioPlayer,
AudioPlayerStatus, AudioPlayerStatus,
type AudioResource, type AudioResource,
createAudioPlayer, createAudioPlayer,
createAudioResource, createAudioResource,
StreamType, StreamType,
VoiceConnection, type VoiceConnection,
} from "@discordjs/voice"; } from "@discordjs/voice";
import type { DiscordPlayerOwner, DiscordPlayOptions } from "./mediaTypes.js"; import type { DiscordPlayerOwner, DiscordPlayOptions } from "./mediaTypes.js";
@@ -121,7 +121,7 @@ export function createSpeakingHandler(
// Step 4: Open the first segment // Step 4: Open the first segment
const activeSession = activeSessions.get(channel.guild.id); const activeSession = activeSessions.get(channel.guild.id);
let currentSegment = segmentManager.open(oggPacketStream); const currentSegment = segmentManager.open(oggPacketStream);
// Step 5: Handle segment file completion // Step 5: Handle segment file completion
currentSegment.out.on("finish", () => { currentSegment.out.on("finish", () => {
@@ -2,11 +2,11 @@
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useRef } from "react"; import { Suspense, useEffect, useRef } from "react";
import { uiStateApi } from "@/lib/api";
import { Header } from "@/components/layout/header"; import { Header } from "@/components/layout/header";
import { MobileTabBar } from "@/components/layout/mobile-tab-bar"; import { MobileTabBar } from "@/components/layout/mobile-tab-bar";
import { Sidebar } from "@/components/layout/sidebar"; import { Sidebar } from "@/components/layout/sidebar";
import { MascotChatbot } from "@/features/mascot/mascot-chatbot"; import { MascotChatbot } from "@/features/mascot/mascot-chatbot";
import { uiStateApi } from "@/lib/api";
import { WsProvider } from "@/lib/ws/context"; import { WsProvider } from "@/lib/ws/context";
function DashboardShell({ children }: { children: React.ReactNode }) { function DashboardShell({ children }: { children: React.ReactNode }) {
@@ -489,9 +489,7 @@ export function LivePanel() {
setMicActive(!micActive); setMicActive(!micActive);
try { try {
await voiceApi.sendCommand( await voiceApi.sendCommand(
micActive micActive ? "voice:transmit:stop" : "voice:transmit:start",
? "voice:transmit:stop"
: "voice:transmit:start",
); );
} catch { } catch {
setMicActive(micActive); setMicActive(micActive);
@@ -1,6 +1,14 @@
"use client"; "use client";
import { Bot, Loader2, MessageCircle, Send, Trash2, User, X } from "lucide-react"; import {
Bot,
Loader2,
MessageCircle,
Send,
Trash2,
User,
X,
} from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { mascotApi } from "@/lib/api"; import { mascotApi } from "@/lib/api";
import type { ChatHistoryMessage } from "@/lib/types"; import type { ChatHistoryMessage } from "@/lib/types";
@@ -20,6 +28,7 @@ export function MascotChatbot() {
.catch(() => {}); .catch(() => {});
}, [open]); }, [open]);
// biome-ignore lint/correctness/useExhaustiveDependencies: scrollRef doesn't need messages in deps
useEffect(() => { useEffect(() => {
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
@@ -1,20 +1,20 @@
"use client"; "use client";
import { useState, useEffect, useCallback } from "react";
import { messagesApi, voiceApi } from "@/lib/api";
import type { MessageRecord, Channel, AttachmentRecord } from "@/lib/types";
import { useWebSocket } from "@/lib/ws/context";
import { import {
Search,
RefreshCw,
Loader2,
AlertCircle, AlertCircle,
Flag,
X,
Download, Download,
ExternalLink, ExternalLink,
Flag,
Loader2,
RefreshCw,
Search,
X,
} from "lucide-react"; } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { messagesApi, voiceApi } from "@/lib/api";
import { useAppConfig } from "@/lib/hooks/use-config"; import { useAppConfig } from "@/lib/hooks/use-config";
import type { AttachmentRecord, Channel, MessageRecord } from "@/lib/types";
import { useWebSocket } from "@/lib/ws/context";
export function MessagesPanel() { export function MessagesPanel() {
const { config } = useAppConfig(); const { config } = useAppConfig();
@@ -27,14 +27,20 @@ export function MessagesPanel() {
const [cursor, setCursor] = useState<string | null>(null); const [cursor, setCursor] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<MessageRecord[] | null>(null); const [searchResults, setSearchResults] = useState<MessageRecord[] | null>(
null,
);
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [viewTab, setViewTab] = useState<"all" | "images" | "review">("all"); const [viewTab, setViewTab] = useState<"all" | "images" | "review">("all");
const [imageMessages, setImageMessages] = useState<MessageRecord[]>([]); const [imageMessages, setImageMessages] = useState<MessageRecord[]>([]);
const [reviewMessages, setReviewMessages] = useState<MessageRecord[]>([]); const [reviewMessages, setReviewMessages] = useState<MessageRecord[]>([]);
const [channels, setChannels] = useState<Channel[]>([]); const [channels, setChannels] = useState<Channel[]>([]);
const [detailMessage, setDetailMessage] = useState<MessageRecord | null>(null); const [detailMessage, setDetailMessage] = useState<MessageRecord | null>(
const [detailAttachments, setDetailAttachments] = useState<AttachmentRecord[]>([]); null,
);
const [detailAttachments, setDetailAttachments] = useState<
AttachmentRecord[]
>([]);
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
const [selectedChannel, setSelectedChannel] = useState(""); const [selectedChannel, setSelectedChannel] = useState("");
@@ -43,7 +49,10 @@ export function MessagesPanel() {
// Fetch available text channels for filtering // Fetch available text channels for filtering
useEffect(() => { useEffect(() => {
if (!guildId) return; if (!guildId) return;
voiceApi.getTextChannels(guildId).then(setChannels).catch(() => {}); voiceApi
.getTextChannels(guildId)
.then(setChannels)
.catch(() => {});
}, [guildId]); }, [guildId]);
// Fetch initial messages // Fetch initial messages
@@ -51,7 +60,11 @@ export function MessagesPanel() {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const result = await messagesApi.list(guildId, 50, selectedChannel || undefined); const result = await messagesApi.list(
guildId,
50,
selectedChannel || undefined,
);
setMessages(result.data); setMessages(result.data);
setCursor(result.nextCursor); setCursor(result.nextCursor);
setHasMore(result.nextCursor !== null); setHasMore(result.nextCursor !== null);
@@ -75,7 +88,10 @@ export function MessagesPanel() {
// Fetch review (flagged) messages // Fetch review (flagged) messages
const fetchReview = useCallback(async () => { const fetchReview = useCallback(async () => {
try { try {
const result = await messagesApi.getReview(50, selectedChannel || undefined); const result = await messagesApi.getReview(
50,
selectedChannel || undefined,
);
setReviewMessages(result.results); setReviewMessages(result.results);
} catch { } catch {
// silently fail // silently fail
@@ -146,7 +162,12 @@ export function MessagesPanel() {
if (!cursor || loadingMore) return; if (!cursor || loadingMore) return;
setLoadingMore(true); setLoadingMore(true);
try { try {
const result = await messagesApi.list(guildId, 50, selectedChannel || undefined, cursor); const result = await messagesApi.list(
guildId,
50,
selectedChannel || undefined,
cursor,
);
setMessages((prev) => [...prev, ...result.data]); setMessages((prev) => [...prev, ...result.data]);
setCursor(result.nextCursor); setCursor(result.nextCursor);
setHasMore(result.nextCursor !== null); setHasMore(result.nextCursor !== null);
@@ -506,7 +527,9 @@ export function MessagesPanel() {
{detailMessage.ai_recommended_action && {detailMessage.ai_recommended_action &&
detailMessage.ai_recommended_action !== "none" && ( detailMessage.ai_recommended_action !== "none" && (
<div className="rounded-lg border p-2"> <div className="rounded-lg border p-2">
<p className="text-xs text-muted-foreground">Action</p> <p className="text-xs text-muted-foreground">
Action
</p>
<p className="text-sm font-medium"> <p className="text-sm font-medium">
{detailMessage.ai_recommended_action} {detailMessage.ai_recommended_action}
</p> </p>
@@ -602,13 +625,14 @@ function MessageCard({
const timeStr = date.toLocaleString(); const timeStr = date.toLocaleString();
return ( return (
// biome-ignore lint/a11y/useSemanticElements: complex nested content prevents using button
<div <div
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => onClick(msg.id)} onClick={() => onClick(msg.id)}
onKeyDown={(e) => e.key === "Enter" && onClick(msg.id)} onKeyDown={(e) => e.key === "Enter" && onClick(msg.id)}
className={`rounded-lg border p-4 space-y-2 transition-colors cursor-pointer hover:bg-muted/50 ${ className={`rounded-lg border p-4 space-y-2 transition-colors cursor-pointer hover:bg-muted/50 ${
msg.ai_severity ? severityColor[msg.ai_severity] ?? "" : "" msg.ai_severity ? (severityColor[msg.ai_severity] ?? "") : ""
} ${msg.ai_severity && msg.ai_severity !== "none" ? "border-l-2" : ""}`} } ${msg.ai_severity && msg.ai_severity !== "none" ? "border-l-2" : ""}`}
> >
{/* Header */} {/* Header */}
@@ -676,19 +700,16 @@ function MessageCard({
</p> </p>
{/* AI Details */} {/* AI Details */}
{msg.ai_moderation_flags && {msg.ai_moderation_flags && msg.ai_moderation_flags !== "[]" && (
msg.ai_moderation_flags !== "[]" && (
<div className="flex flex-wrap gap-1 mt-1"> <div className="flex flex-wrap gap-1 mt-1">
{safeParseJsonArray(msg.ai_moderation_flags).map( {safeParseJsonArray(msg.ai_moderation_flags).map((flag) => (
(flag) => (
<span <span
key={flag} key={flag}
className="inline-flex items-center rounded-md bg-destructive/10 px-1.5 py-0.5 text-xs font-medium text-destructive" className="inline-flex items-center rounded-md bg-destructive/10 px-1.5 py-0.5 text-xs font-medium text-destructive"
> >
{flag} {flag}
</span> </span>
), ))}
)}
</div> </div>
)} )}
@@ -699,8 +720,7 @@ function MessageCard({
)} )}
{/* Confidence score */} {/* Confidence score */}
{msg.ai_confidence !== undefined && {msg.ai_confidence !== undefined && msg.ai_confidence !== null && (
msg.ai_confidence !== null && (
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden max-w-24"> <div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden max-w-24">
<div <div
@@ -755,9 +775,7 @@ function formatBytes(bytes: number): string {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
} }
function safeParseJsonArray( function safeParseJsonArray(value: string | null | undefined): string[] {
value: string | null | undefined,
): string[] {
if (!value) return []; if (!value) return [];
try { try {
const parsed = JSON.parse(value); const parsed = JSON.parse(value);
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { configApi } from "@/lib/api"; import { configApi } from "@/lib/api";
export interface AppConfig { export interface AppConfig {