chore(format): auto-format files with biome
This commit is contained in:
@@ -5,6 +5,8 @@ export default defineConfig({
|
||||
out: "./drizzle/migrations",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL || "postgresql://postgres:postgres@localhost:5432/bete",
|
||||
url:
|
||||
process.env.DATABASE_URL ||
|
||||
"postgresql://postgres:postgres@localhost:5432/bete",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { createChildLogger } from "@bete/shared/logger";
|
||||
import type { Client } from "discord.js-selfbot-v13";
|
||||
import type { CommandHandler } from "../modules/command-handler/commandHandler.js";
|
||||
import type { EventBroadcaster } from "../modules/event-broadcaster/index.js";
|
||||
import type { VoiceController } from "../modules/voice-recording/voiceController.js";
|
||||
import type { closeDatabase } from "../shared/database/drizzle.js";
|
||||
import type { createChildLogger } from "@bete/shared/logger";
|
||||
|
||||
type Logger = ReturnType<typeof createChildLogger>;
|
||||
type CloseDatabase = typeof closeDatabase;
|
||||
|
||||
@@ -2,8 +2,8 @@ import "./mock-crc.js";
|
||||
import "libsodium-wrappers";
|
||||
import "@snazzah/davey";
|
||||
import "dotenv/config";
|
||||
import { initializeDiscordGateway } from "./app/bootstrap.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { initializeDiscordGateway } from "./app/bootstrap.js";
|
||||
|
||||
const logger = createChildLogger("discord-gateway");
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { initializeDatabase } from "../../shared/database/drizzle.js";
|
||||
import { buildConversationContext } from "./conversationContext.js";
|
||||
import { runModerationAnalysis, runSimpleTextFallback } from "./llmModerationClient.js";
|
||||
import {
|
||||
getAttachmentsForMessages,
|
||||
getConversationContextBefore,
|
||||
updateMessagesAIAnalysisBulk,
|
||||
} from "../message-capture/messageStore.js";
|
||||
import type { MessageRecord, AnalysisResult } from "../message-capture/types.js";
|
||||
import type {
|
||||
AnalysisResult,
|
||||
MessageRecord,
|
||||
} from "../message-capture/types.js";
|
||||
import { buildConversationContext } from "./conversationContext.js";
|
||||
import {
|
||||
runModerationAnalysis,
|
||||
runSimpleTextFallback,
|
||||
} from "./llmModerationClient.js";
|
||||
|
||||
let dbInitialized = false;
|
||||
let dbInitPromise: Promise<any> | null = null;
|
||||
@@ -30,24 +36,44 @@ type WorkerJob =
|
||||
| { type: "batch"; conversationKey: string; messages: MessageRecord[] }
|
||||
| { type: "individual"; message: MessageRecord; skipNormalAnalysis: boolean };
|
||||
|
||||
type BatchOkResponse = { ok: true; conversationKey: string; rows: MessageRecord[] };
|
||||
type BatchErrorResponse = { ok: false; conversationKey: string; rows: MessageRecord[]; error: string };
|
||||
type BatchOkResponse = {
|
||||
ok: true;
|
||||
conversationKey: string;
|
||||
rows: MessageRecord[];
|
||||
};
|
||||
type BatchErrorResponse = {
|
||||
ok: false;
|
||||
conversationKey: string;
|
||||
rows: MessageRecord[];
|
||||
error: string;
|
||||
};
|
||||
type IndividualOkResponse = { ok: true; results: AnalysisResult[] };
|
||||
type IndividualErrorResponse = { ok: false; results: AnalysisResult[]; error: string };
|
||||
type IndividualErrorResponse = {
|
||||
ok: false;
|
||||
results: AnalysisResult[];
|
||||
error: string;
|
||||
};
|
||||
|
||||
type WorkerResponse = BatchOkResponse | BatchErrorResponse | IndividualOkResponse | IndividualErrorResponse;
|
||||
type WorkerResponse =
|
||||
| BatchOkResponse
|
||||
| BatchErrorResponse
|
||||
| IndividualOkResponse
|
||||
| IndividualErrorResponse;
|
||||
|
||||
/**
|
||||
* Default export — Piscina worker entry point.
|
||||
* Routes to the correct handler based on `type` field.
|
||||
*/
|
||||
export default async function workerRouter(job: WorkerJob): Promise<WorkerResponse> {
|
||||
export default async function workerRouter(
|
||||
job: WorkerJob,
|
||||
): Promise<WorkerResponse> {
|
||||
if (!config.AI_LLM_API_KEY) {
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
level: "FATAL",
|
||||
context: "aiAnalysisWorker",
|
||||
error: "AI_LLM_API_KEY is missing from environment. Force closing worker operation.",
|
||||
error:
|
||||
"AI_LLM_API_KEY is missing from environment. Force closing worker operation.",
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
@@ -59,7 +85,12 @@ export default async function workerRouter(job: WorkerJob): Promise<WorkerRespon
|
||||
} catch (dbError) {
|
||||
const msg = dbError instanceof Error ? dbError.message : String(dbError);
|
||||
if (job.type === "batch") {
|
||||
return { ok: false, conversationKey: job.conversationKey, rows: [], error: `Database init failed: ${msg}` };
|
||||
return {
|
||||
ok: false,
|
||||
conversationKey: job.conversationKey,
|
||||
rows: [],
|
||||
error: `Database init failed: ${msg}`,
|
||||
};
|
||||
}
|
||||
return { ok: false, results: [], error: `Database init failed: ${msg}` };
|
||||
}
|
||||
@@ -72,16 +103,23 @@ export default async function workerRouter(job: WorkerJob): Promise<WorkerRespon
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
console.error(JSON.stringify({
|
||||
level: "ERROR",
|
||||
context: "aiAnalysisWorker",
|
||||
type: job.type,
|
||||
error: errorMessage,
|
||||
stack: errorStack,
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
level: "ERROR",
|
||||
context: "aiAnalysisWorker",
|
||||
type: job.type,
|
||||
error: errorMessage,
|
||||
stack: errorStack,
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
if (job.type === "batch") {
|
||||
return { ok: false, conversationKey: job.conversationKey, rows: [], error: errorMessage };
|
||||
return {
|
||||
ok: false,
|
||||
conversationKey: job.conversationKey,
|
||||
rows: [],
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
return { ok: false, results: [], error: errorMessage };
|
||||
}
|
||||
@@ -91,7 +129,11 @@ export default async function workerRouter(job: WorkerJob): Promise<WorkerRespon
|
||||
// Batch handler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function processBatch(job: { type: "batch"; conversationKey: string; messages: MessageRecord[] }): Promise<BatchOkResponse | BatchErrorResponse> {
|
||||
async function processBatch(job: {
|
||||
type: "batch";
|
||||
conversationKey: string;
|
||||
messages: MessageRecord[];
|
||||
}): Promise<BatchOkResponse | BatchErrorResponse> {
|
||||
const { conversationKey, messages } = job;
|
||||
const firstMessage = messages[0];
|
||||
if (!firstMessage) return { ok: true, conversationKey, rows: [] };
|
||||
@@ -150,7 +192,11 @@ async function processBatch(job: { type: "batch"; conversationKey: string; messa
|
||||
// Individual fallback handler (offloaded from main thread)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function processIndividual(job: { type: "individual"; message: MessageRecord; skipNormalAnalysis: boolean }): Promise<IndividualOkResponse | IndividualErrorResponse> {
|
||||
async function processIndividual(job: {
|
||||
type: "individual";
|
||||
message: MessageRecord;
|
||||
skipNormalAnalysis: boolean;
|
||||
}): Promise<IndividualOkResponse | IndividualErrorResponse> {
|
||||
const { message, skipNormalAnalysis } = job;
|
||||
|
||||
const contextBefore = await getConversationContextBefore({
|
||||
@@ -167,7 +213,10 @@ async function processIndividual(job: { type: "individual"; message: MessageReco
|
||||
});
|
||||
|
||||
const contextIds = contextBefore.map((m) => m.id);
|
||||
const attachments = await getAttachmentsForMessages([message.id, ...contextIds]);
|
||||
const attachments = await getAttachmentsForMessages([
|
||||
message.id,
|
||||
...contextIds,
|
||||
]);
|
||||
|
||||
let results: AnalysisResult[];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { Client, PermissionString } from "discord.js-selfbot-v13";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { createModerationAction } from "../message-capture/messageStore.js";
|
||||
import type { MessageRecord } from "../message-capture/types.js";
|
||||
|
||||
@@ -323,17 +323,22 @@ export async function attemptAutoDeleteFlaggedMessage(
|
||||
try {
|
||||
const targetUser = await client.users.fetch(message.user_id);
|
||||
if (targetUser) {
|
||||
const reason = message.ai_categories ?? message.ai_moderation_flags ?? "(unknown)";
|
||||
const reason =
|
||||
message.ai_categories ?? message.ai_moderation_flags ?? "(unknown)";
|
||||
await targetUser.send(
|
||||
`Pesan Anda di **${guild.name}** telah dihapus oleh sistem moderasi otomatis.\n` +
|
||||
`Alasan: ${reason}\n` +
|
||||
`Jika Anda merasa ini adalah kesalahan, silakan hubungi admin server.`,
|
||||
`Alasan: ${reason}\n` +
|
||||
`Jika Anda merasa ini adalah kesalahan, silakan hubungi admin server.`,
|
||||
);
|
||||
}
|
||||
} catch (dmErr) {
|
||||
// DM might fail if user has DMs disabled — not critical
|
||||
logger.debug(
|
||||
{ messageId: message.id, userId: message.user_id, error: String(dmErr) },
|
||||
{
|
||||
messageId: message.id,
|
||||
userId: message.user_id,
|
||||
error: String(dmErr),
|
||||
},
|
||||
"Failed to send DM notification for auto-deleted message",
|
||||
);
|
||||
}
|
||||
@@ -342,18 +347,28 @@ export async function attemptAutoDeleteFlaggedMessage(
|
||||
// ── Log to moderation channel ──
|
||||
if (config.AUTO_DELETE_LOG_CHANNEL_ID) {
|
||||
try {
|
||||
const logChannel = guild.channels.cache.get(config.AUTO_DELETE_LOG_CHANNEL_ID);
|
||||
if (logChannel && "send" in logChannel && typeof (logChannel as any).send === "function") {
|
||||
const logChannel = guild.channels.cache.get(
|
||||
config.AUTO_DELETE_LOG_CHANNEL_ID,
|
||||
);
|
||||
if (
|
||||
logChannel &&
|
||||
"send" in logChannel &&
|
||||
typeof (logChannel as any).send === "function"
|
||||
) {
|
||||
const severity = message.ai_severity ?? "none";
|
||||
const categories = message.ai_categories ?? message.ai_moderation_flags ?? "—";
|
||||
const snippet = (message.edited_content ?? message.content).substring(0, 200);
|
||||
const categories =
|
||||
message.ai_categories ?? message.ai_moderation_flags ?? "—";
|
||||
const snippet = (message.edited_content ?? message.content).substring(
|
||||
0,
|
||||
200,
|
||||
);
|
||||
await (logChannel as any).send(
|
||||
`**🧹 Auto-Delete** — Pesan dari <@${message.user_id}> di <#${channelId}>\n` +
|
||||
`**Status:** ${message.ai_status}\n` +
|
||||
`**Severitas:** ${severity}\n` +
|
||||
`**Kategori:** ${categories}\n` +
|
||||
`**Isi:** ${snippet}\n` +
|
||||
`**Waktu:** <t:${Math.floor(Date.now() / 1000)}:R>`,
|
||||
`**Status:** ${message.ai_status}\n` +
|
||||
`**Severitas:** ${severity}\n` +
|
||||
`**Kategori:** ${categories}\n` +
|
||||
`**Isi:** ${snippet}\n` +
|
||||
`**Waktu:** <t:${Math.floor(Date.now() / 1000)}:R>`,
|
||||
);
|
||||
}
|
||||
} catch (logErr) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatModerationTextEvidenceForPrompt } from "./indonesianTextNormalizer.js";
|
||||
import { encoding_for_model as encodingForModel } from "tiktoken";
|
||||
import { formatMediaEvidenceForPrompt } from "../message-capture/messageMetadata.js";
|
||||
import type { MessageRecord } from "../message-capture/types.js";
|
||||
import { encoding_for_model as encodingForModel } from "tiktoken";
|
||||
import { formatModerationTextEvidenceForPrompt } from "./indonesianTextNormalizer.js";
|
||||
|
||||
export interface ConversationContextInput {
|
||||
contextBefore: MessageRecord[];
|
||||
@@ -59,13 +59,17 @@ export function buildConversationContext(
|
||||
const { contextBefore, targets, maxTokens } = input;
|
||||
|
||||
// Calculate tokens used by targets (parallel)
|
||||
const targetLines = targets.map((msg) => formatMessageForPrompt(msg, "target"));
|
||||
const targetLines = targets.map((msg) =>
|
||||
formatMessageForPrompt(msg, "target"),
|
||||
);
|
||||
let usedTokens = targetLines.reduce(
|
||||
(sum, line) => sum + estimateTokens(line),
|
||||
0,
|
||||
);
|
||||
|
||||
const contextLines = contextBefore.map((msg) => formatMessageForPrompt(msg, "context"));
|
||||
const contextLines = contextBefore.map((msg) =>
|
||||
formatMessageForPrompt(msg, "context"),
|
||||
);
|
||||
const selectedContextLines: string[] = [];
|
||||
|
||||
// Go backwards through context, taking most recent first
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export { startPendingAIAnalysisWorker } from "./aiAnalyzer.js";
|
||||
export {
|
||||
normalizeDiscordCustomEmoji,
|
||||
detectIndonesianBadwords,
|
||||
buildModerationTextEvidence,
|
||||
detectIndonesianBadwords,
|
||||
normalizeDiscordCustomEmoji,
|
||||
} from "./indonesianTextNormalizer.js";
|
||||
export { runModerationAnalysis } from "./llmModerationClient.js";
|
||||
export { buildSystemPrompt } from "./moderationPrompt.js";
|
||||
|
||||
@@ -19,17 +19,29 @@ const badwordCache = new Map<string, BadwordCacheEntry>();
|
||||
// Conservative: only returns true for patterns that CANNOT be violations.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SAFE_PATTERNS: Array<{ test: (text: string) => boolean; reason: string }> = [
|
||||
const SAFE_PATTERNS: Array<{
|
||||
test: (text: string) => boolean;
|
||||
reason: string;
|
||||
}> = [
|
||||
{
|
||||
test: (t) => /^(wkwk+|w+kw+k+|wkwkw+|haha+|hehe+|hihi+|huhu+|xixi+|wakak+|awkwa+)$/i.test(t),
|
||||
test: (t) =>
|
||||
/^(wkwk+|w+kw+k+|wkwkw+|haha+|hehe+|hihi+|huhu+|xixi+|wakak+|awkwa+)$/i.test(
|
||||
t,
|
||||
),
|
||||
reason: "laughter pattern",
|
||||
},
|
||||
{
|
||||
test: (t) => /^(ok|oke|okay|sip|siap|aman|mantap|gas|gass|gaskeun|santuy|gaskan|lah|wih|wah|eh|nah|loh|hmm|hm|heh)$/i.test(t),
|
||||
test: (t) =>
|
||||
/^(ok|oke|okay|sip|siap|aman|mantap|gas|gass|gaskeun|santuy|gaskan|lah|wih|wah|eh|nah|loh|hmm|hm|heh)$/i.test(
|
||||
t,
|
||||
),
|
||||
reason: "single-word affirmative",
|
||||
},
|
||||
{
|
||||
test: (t) => /^(hai|halo|hello|hi|oi|woy|woi|pagi|siang|sore|malam|mlm|p|w|L|F|gws|thx|thks|makasih|ty|thanks|yw|sama-sama|ok sip|ok bang|siap bang)$/i.test(t),
|
||||
test: (t) =>
|
||||
/^(hai|halo|hello|hi|oi|woy|woi|pagi|siang|sore|malam|mlm|p|w|L|F|gws|thx|thks|makasih|ty|thanks|yw|sama-sama|ok sip|ok bang|siap bang)$/i.test(
|
||||
t,
|
||||
),
|
||||
reason: "greeting/common expression",
|
||||
},
|
||||
{
|
||||
@@ -63,49 +75,148 @@ const BADWORD_CATEGORIES: BadwordEntry[] = [
|
||||
description: "vulgar genitalia / sexual terms",
|
||||
flag: "vulgar_language",
|
||||
words: [
|
||||
"kontol", "memek", "pepek", "tempik", "peler", "pelir", "pukimak", "pukima",
|
||||
"jancok", "jancuk", "cok", "cuk", "pantek", "palek", "ngentot", "ngewe",
|
||||
"entot", "ewe", "coli", "sange", "sangean", "ngocok", "bangkot",
|
||||
"nenen", "tete", "tetek", "dodot", "kentu", "perek", "bispak", "bangsat",
|
||||
"babi", "asu", "anjing", "anjir", "anjirt", "njing", "njir", "anjay",
|
||||
"kampret", "kampang", "brengsek", "brengus", "bejad", "bajingan",
|
||||
"goblok", "tolol", "bego", "dungu", "idiot", "beban", "keparat",
|
||||
"setan", "iblis", "sialan", "sial", "kacang", "edan", "gila",
|
||||
"kontol",
|
||||
"memek",
|
||||
"pepek",
|
||||
"tempik",
|
||||
"peler",
|
||||
"pelir",
|
||||
"pukimak",
|
||||
"pukima",
|
||||
"jancok",
|
||||
"jancuk",
|
||||
"cok",
|
||||
"cuk",
|
||||
"pantek",
|
||||
"palek",
|
||||
"ngentot",
|
||||
"ngewe",
|
||||
"entot",
|
||||
"ewe",
|
||||
"coli",
|
||||
"sange",
|
||||
"sangean",
|
||||
"ngocok",
|
||||
"bangkot",
|
||||
"nenen",
|
||||
"tete",
|
||||
"tetek",
|
||||
"dodot",
|
||||
"kentu",
|
||||
"perek",
|
||||
"bispak",
|
||||
"bangsat",
|
||||
"babi",
|
||||
"asu",
|
||||
"anjing",
|
||||
"anjir",
|
||||
"anjirt",
|
||||
"njing",
|
||||
"njir",
|
||||
"anjay",
|
||||
"kampret",
|
||||
"kampang",
|
||||
"brengsek",
|
||||
"brengus",
|
||||
"bejad",
|
||||
"bajingan",
|
||||
"goblok",
|
||||
"tolol",
|
||||
"bego",
|
||||
"dungu",
|
||||
"idiot",
|
||||
"beban",
|
||||
"keparat",
|
||||
"setan",
|
||||
"iblis",
|
||||
"sialan",
|
||||
"sial",
|
||||
"kacang",
|
||||
"edan",
|
||||
"gila",
|
||||
],
|
||||
},
|
||||
{
|
||||
description: "harassment / targeted insults",
|
||||
flag: "harassment",
|
||||
words: [
|
||||
"mampus", "mati", "bunuh", "bacot", "cupu", "geblek", "kere",
|
||||
"ngawur", "sembarangan", "nyampah", "nyampah", "sarap",
|
||||
"ke laut aja", "gila lu", "sinting", "editan", "mending mati",
|
||||
"monyet", "kuda", "unta", "bangke", "bangsat",
|
||||
"mampus",
|
||||
"mati",
|
||||
"bunuh",
|
||||
"bacot",
|
||||
"cupu",
|
||||
"geblek",
|
||||
"kere",
|
||||
"ngawur",
|
||||
"sembarangan",
|
||||
"nyampah",
|
||||
"nyampah",
|
||||
"sarap",
|
||||
"ke laut aja",
|
||||
"gila lu",
|
||||
"sinting",
|
||||
"editan",
|
||||
"mending mati",
|
||||
"monyet",
|
||||
"kuda",
|
||||
"unta",
|
||||
"bangke",
|
||||
"bangsat",
|
||||
],
|
||||
},
|
||||
{
|
||||
description: "SARA / racial slurs (non-exhaustive)",
|
||||
flag: "sara",
|
||||
words: [
|
||||
"cina", "tionghoa", "pribumi", "non-pribumi", "kaffir", "kafir",
|
||||
"murtad", "sesat", "liberal", "komunis", "komunisme", "pki",
|
||||
"cina",
|
||||
"tionghoa",
|
||||
"pribumi",
|
||||
"non-pribumi",
|
||||
"kaffir",
|
||||
"kafir",
|
||||
"murtad",
|
||||
"sesat",
|
||||
"liberal",
|
||||
"komunis",
|
||||
"komunisme",
|
||||
"pki",
|
||||
],
|
||||
},
|
||||
{
|
||||
description: "gambling / judi",
|
||||
flag: "gambling",
|
||||
words: [
|
||||
"judi", "slot", "togel", "toto gelap", "casino", "roulette",
|
||||
"poker", "domino", "gaple", "sabung ayam", "bola jalan",
|
||||
"maxwin", "gacor", "scatter", "bonanza", "olympus",
|
||||
"judi",
|
||||
"slot",
|
||||
"togel",
|
||||
"toto gelap",
|
||||
"casino",
|
||||
"roulette",
|
||||
"poker",
|
||||
"domino",
|
||||
"gaple",
|
||||
"sabung ayam",
|
||||
"bola jalan",
|
||||
"maxwin",
|
||||
"gacor",
|
||||
"scatter",
|
||||
"bonanza",
|
||||
"olympus",
|
||||
],
|
||||
},
|
||||
{
|
||||
description: "hate speech / extreme discrimination",
|
||||
flag: "hate_speech",
|
||||
words: [
|
||||
"bencina", "bencin", "bangsat", "dajjal", "laknat", "keparat",
|
||||
"dasar cina", "dasar tionghoa", "dasar pribumi",
|
||||
"bencina",
|
||||
"bencin",
|
||||
"bangsat",
|
||||
"dajjal",
|
||||
"laknat",
|
||||
"keparat",
|
||||
"dasar cina",
|
||||
"dasar tionghoa",
|
||||
"dasar pribumi",
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -294,9 +405,7 @@ export function buildModerationTextEvidence(
|
||||
};
|
||||
}
|
||||
|
||||
export function formatModerationTextEvidenceForPrompt(
|
||||
text: string,
|
||||
): string {
|
||||
export function formatModerationTextEvidenceForPrompt(text: string): string {
|
||||
const evidence = buildModerationTextEvidence(text);
|
||||
if (evidence.normalized === evidence.raw && evidence.notes.length === 0) {
|
||||
return "";
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
* defaults are maintained in one place.
|
||||
*/
|
||||
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { retryWithBackoff } from "@bete/shared/utils";
|
||||
import OpenAI from "openai";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { retryWithBackoff } from "@bete/shared/utils";
|
||||
import { withLlmConcurrency } from "./concurrencyLimiter.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
|
||||
const log = createChildLogger("llm-client");
|
||||
|
||||
|
||||
@@ -32,19 +32,17 @@ import {
|
||||
computeImagePhash,
|
||||
getCachedMediaAnalysis,
|
||||
getCachedMediaByPhash,
|
||||
getCachedUserModeration,
|
||||
getRecentCorrectedModerations,
|
||||
makeCustomEmojiCacheKey,
|
||||
makeImageCacheKey,
|
||||
makeStickerCacheKey,
|
||||
makeUserModerationCacheKey,
|
||||
setCachedUserModeration,
|
||||
upsertCachedMediaAnalysis,
|
||||
upsertCachedMediaByPhash,
|
||||
} from "./textCacheStore.js";
|
||||
import { extractUrlsFromText, fetchUrlSafely } from "./urlFetcher.js";
|
||||
import {
|
||||
getCachedUserModeration,
|
||||
makeUserModerationCacheKey,
|
||||
setCachedUserModeration,
|
||||
} from "./textCacheStore.js";
|
||||
|
||||
const SeveritySchema = z.enum(["none", "low", "medium", "high", "critical"]);
|
||||
const RecommendedActionSchema = z.enum([
|
||||
@@ -1690,7 +1688,11 @@ export async function runModerationAnalysis(
|
||||
// This guards against both legacy corrupt entries and any future write-path bugs.
|
||||
if (
|
||||
cached.flags.some((f) =>
|
||||
["analysis_api_failed", "analysis_parse_failed", "analysis_incomplete"].includes(f),
|
||||
[
|
||||
"analysis_api_failed",
|
||||
"analysis_parse_failed",
|
||||
"analysis_incomplete",
|
||||
].includes(f),
|
||||
)
|
||||
) {
|
||||
log.warn(
|
||||
@@ -1707,7 +1709,8 @@ export async function runModerationAnalysis(
|
||||
categories: cached.categories,
|
||||
severity: cached.severity as AnalysisResult["severity"],
|
||||
confidence: cached.confidence,
|
||||
recommendedAction: cached.recommendedAction as AnalysisResult["recommendedAction"],
|
||||
recommendedAction:
|
||||
cached.recommendedAction as AnalysisResult["recommendedAction"],
|
||||
policyVersion: "cached-user-moderation-2026-06",
|
||||
evidence: [],
|
||||
});
|
||||
@@ -1727,7 +1730,11 @@ export async function runModerationAnalysis(
|
||||
|
||||
if (cacheHits.length > 0) {
|
||||
log.info(
|
||||
{ cacheHits: cacheHits.length, uncached: uncachedTargets.length, total: targets.length },
|
||||
{
|
||||
cacheHits: cacheHits.length,
|
||||
uncached: uncachedTargets.length,
|
||||
total: targets.length,
|
||||
},
|
||||
"User moderation cache applied — skipping LLM call for cached targets",
|
||||
);
|
||||
}
|
||||
@@ -1953,15 +1960,11 @@ Kategori: spam`;
|
||||
}
|
||||
|
||||
// ── Parse category from "Kategori: xxx" line ──
|
||||
const categoryMatch = analysis.match(
|
||||
/[Kk]ategori:\s*(\w+)/i,
|
||||
);
|
||||
const categoryMatch = analysis.match(/[Kk]ategori:\s*(\w+)/i);
|
||||
if (categoryMatch) {
|
||||
const parsedCat = categoryMatch[1].toLowerCase();
|
||||
// Only accept known categories
|
||||
if (
|
||||
["harassment", "spam", "gambling", "sara"].includes(parsedCat)
|
||||
) {
|
||||
if (["harassment", "spam", "gambling", "sara"].includes(parsedCat)) {
|
||||
category = parsedCat;
|
||||
}
|
||||
// Strip the "Kategori:" line from the analysis text so it's cleaner
|
||||
@@ -1969,7 +1972,12 @@ Kategori: spam`;
|
||||
}
|
||||
|
||||
log.info(
|
||||
{ messageId: message.id, status, category, analysis: analysis.slice(0, 100) },
|
||||
{
|
||||
messageId: message.id,
|
||||
status,
|
||||
category,
|
||||
analysis: analysis.slice(0, 100),
|
||||
},
|
||||
"Simple fallback step 2 — reason + category",
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -1985,10 +1993,8 @@ Kategori: spam`;
|
||||
}
|
||||
|
||||
// Build the result fields using parsed category
|
||||
const flags: string[] =
|
||||
status === "clean" ? [] : [category];
|
||||
const categories: string[] =
|
||||
status === "clean" ? [] : [category];
|
||||
const flags: string[] = status === "clean" ? [] : [category];
|
||||
const categories: string[] = status === "clean" ? [] : [category];
|
||||
const score = status === "flagged" ? 0.7 : status === "warn" ? 0.4 : 0;
|
||||
const severity: "none" | "low" | "medium" | "high" | "critical" =
|
||||
status === "flagged" ? "medium" : status === "warn" ? "low" : "none";
|
||||
|
||||
@@ -83,7 +83,11 @@ export function logModerationAnalysis(
|
||||
model: string,
|
||||
results: AnalysisResult[],
|
||||
duration_ms: number,
|
||||
tokenUsage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number },
|
||||
tokenUsage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
},
|
||||
parseErrors: string[] = [],
|
||||
): void {
|
||||
const response: ModerationAnalysisResponse = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { executeAll, executeGet } from "../../shared/database/drizzle.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { executeAll, executeGet } from "../../shared/database/drizzle.js";
|
||||
|
||||
const logger = createChildLogger("text-cache-store");
|
||||
|
||||
@@ -255,10 +255,7 @@ export function makeUserModerationCacheKey(
|
||||
userId: string,
|
||||
content: string,
|
||||
): string {
|
||||
const hash = createHash("sha256")
|
||||
.update(content)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
const hash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
||||
return `user_mod:${userId}:${hash}`;
|
||||
}
|
||||
|
||||
@@ -266,9 +263,7 @@ export function makeUserModerationCacheKey(
|
||||
* Lookup a cached moderation result for a (user, content) pair.
|
||||
* Returns the stored result fields or null.
|
||||
*/
|
||||
export async function getCachedUserModeration(
|
||||
cacheKey: string,
|
||||
): Promise<{
|
||||
export async function getCachedUserModeration(cacheKey: string): Promise<{
|
||||
status: "clean" | "flagged";
|
||||
flags: string[];
|
||||
score: number;
|
||||
@@ -409,8 +404,9 @@ export async function computeImagePhash(
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
// Dynamic import — imghash is ESM with a default export containing { hash, hashRaw, ... }
|
||||
const imghashModule: { default?: { hash?: (buf: Buffer) => Promise<string> } } =
|
||||
await import("imghash");
|
||||
const imghashModule: {
|
||||
default?: { hash?: (buf: Buffer) => Promise<string> };
|
||||
} = await import("imghash");
|
||||
const hashFn = imghashModule.default?.hash;
|
||||
if (typeof hashFn !== "function") return null;
|
||||
const hash = await hashFn(buffer);
|
||||
@@ -451,13 +447,15 @@ export async function getRecentCorrectedModerations(
|
||||
|
||||
if (!rows || rows.length === 0) return [];
|
||||
|
||||
return (rows as Array<{
|
||||
id: string;
|
||||
original_flags: string;
|
||||
corrected_flags: string;
|
||||
correction_notes: string | null;
|
||||
content_snippet: string;
|
||||
}>).map((row) => ({
|
||||
return (
|
||||
rows as Array<{
|
||||
id: string;
|
||||
original_flags: string;
|
||||
corrected_flags: string;
|
||||
correction_notes: string | null;
|
||||
content_snippet: string;
|
||||
}>
|
||||
).map((row) => ({
|
||||
id: row.id,
|
||||
originalFlags: JSON.parse(row.original_flags) as string[],
|
||||
correctedFlags: JSON.parse(row.corrected_flags) as string[],
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { uploadToTele } from "./teleUpload.js";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import {
|
||||
updateAttachmentAsFailedUpload,
|
||||
updateAttachmentAsUploaded,
|
||||
updateAttachmentDiscordUrl,
|
||||
} from "../message-capture/messageStore.js";
|
||||
import { uploadToTele } from "./teleUpload.js";
|
||||
|
||||
const logger = createChildLogger("attachment-uploader");
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import sharp from "sharp";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import sharp from "sharp";
|
||||
|
||||
const log = createChildLogger("imageResizer");
|
||||
|
||||
|
||||
@@ -37,14 +37,8 @@ export async function uploadToTele(input: {
|
||||
timeoutMs?: number;
|
||||
retries: number;
|
||||
}): Promise<TeleUploadResult> {
|
||||
const {
|
||||
buffer,
|
||||
filename,
|
||||
contentType,
|
||||
uploadUrl,
|
||||
timeoutMs,
|
||||
retries,
|
||||
} = input;
|
||||
const { buffer, filename, contentType, uploadUrl, timeoutMs, retries } =
|
||||
input;
|
||||
|
||||
const response = await retryWithBackoff(
|
||||
async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { Client } from "discord.js-selfbot-v13";
|
||||
import Redis from "ioredis";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { discordPlayer } from "../voice-recording/player.js";
|
||||
import type { VoiceController } from "../voice-recording/voiceController.js";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Redis from "ioredis";
|
||||
import type { CustomLogger } from "@bete/shared/logger";
|
||||
import Redis from "ioredis";
|
||||
|
||||
export interface DiscordGatewayEvent {
|
||||
type: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { executeAll, executeGet } from "../../shared/database/drizzle.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { executeAll, executeGet } from "../../shared/database/drizzle.js";
|
||||
import type { MessageRecord } from "./types.js";
|
||||
|
||||
const logger = createChildLogger("analytics-store");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { WebSocket } from "ws";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { MediaState } from "../voice-recording/mediaTypes.js";
|
||||
import type { WebSocket } from "ws";
|
||||
import type {
|
||||
AnalysisQueueStatus,
|
||||
AttachmentRecord,
|
||||
MessageRecord,
|
||||
ModerationWsEvent,
|
||||
} from "../message-capture/types.js";
|
||||
import type { MediaState } from "../voice-recording/mediaTypes.js";
|
||||
|
||||
export type BroadcasterClient = Pick<WebSocket, "readyState" | "send">;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { registerMessageCapture } from "./messageCapture.js";
|
||||
export {
|
||||
getDisplayContent,
|
||||
getMessageLocation,
|
||||
@@ -19,3 +18,4 @@ export type {
|
||||
MessageRecord,
|
||||
VoiceSegmentRecord,
|
||||
} from "../message-capture/types.js";
|
||||
export { registerMessageCapture } from "./messageCapture.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { Client, Message } from "discord.js-selfbot-v13";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { queueMessageAnalysis } from "../ai-moderation/aiAnalyzer.js";
|
||||
import { processAttachmentUpload } from "../attachment-upload/attachmentUploader.js";
|
||||
import type { EventBroadcaster } from "../event-broadcaster/eventBroadcaster.js";
|
||||
@@ -16,7 +16,10 @@ import {
|
||||
updateMessageAsEdited,
|
||||
upsertMessageForCapture,
|
||||
} from "../message-capture/messageStore.js";
|
||||
import type { AttachmentRecord, MessageRecord } from "../message-capture/types.js";
|
||||
import type {
|
||||
AttachmentRecord,
|
||||
MessageRecord,
|
||||
} from "../message-capture/types.js";
|
||||
|
||||
const logger = createChildLogger("message-capture");
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
moderationActionsTable,
|
||||
retentionPoliciesTable,
|
||||
} from "../../shared/database/schema.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { decodeCursor, encodeCursor } from "../message-capture/pagination.js";
|
||||
import type {
|
||||
AttachmentRecord,
|
||||
|
||||
@@ -8,10 +8,7 @@ import {
|
||||
StreamType,
|
||||
VoiceConnection,
|
||||
} from "@discordjs/voice";
|
||||
import type {
|
||||
DiscordPlayerOwner,
|
||||
DiscordPlayOptions,
|
||||
} from "./mediaTypes.js";
|
||||
import type { DiscordPlayerOwner, DiscordPlayOptions } from "./mediaTypes.js";
|
||||
|
||||
export class DiscordPlayer {
|
||||
private player: AudioPlayer;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { retryWithBackoff } from "@bete/shared/utils";
|
||||
import {
|
||||
type DiscordGatewayAdapterCreator,
|
||||
EndBehaviorType,
|
||||
@@ -11,7 +13,7 @@ import {
|
||||
} from "@discordjs/voice";
|
||||
import type { Client, VoiceChannel } from "discord.js-selfbot-v13";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { PcmBroadcaster } from "../message-capture/types.js";
|
||||
import { PacketFilter } from "./packetFilter.js";
|
||||
import { subscribeToAudioStream } from "./recorder/audioStream.js";
|
||||
import { OpusDecoder } from "./recorder/decoder.js";
|
||||
@@ -26,8 +28,6 @@ import {
|
||||
type RecordingSession,
|
||||
} from "./recorder/sessionRecording.js";
|
||||
import { uploadRecordingSegment } from "./recorder/uploader.js";
|
||||
import { retryWithBackoff } from "@bete/shared/utils";
|
||||
import type { PcmBroadcaster } from "../message-capture/types.js";
|
||||
|
||||
const logger = createChildLogger("recorder");
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import path from "node:path";
|
||||
import type { Client, VoiceChannel } from "discord.js-selfbot-v13";
|
||||
import { config } from "../../../shared/config/config.js";
|
||||
import type { SegmentMetadata, SegmentState, UserMetadata } from "../../message-capture/types.js";
|
||||
import type {
|
||||
SegmentMetadata,
|
||||
SegmentState,
|
||||
UserMetadata,
|
||||
} from "../../message-capture/types.js";
|
||||
|
||||
export async function collectUserMetadata(
|
||||
client: Client,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { UserMetadata } from "../../message-capture/types.js";
|
||||
import {
|
||||
buildMuxFfmpegArgs,
|
||||
runFfmpeg as defaultRunFfmpeg,
|
||||
} from "../ffmpegProcess.js";
|
||||
import type { UserMetadata } from "../../message-capture/types.js";
|
||||
|
||||
export type SessionRecordingStatus =
|
||||
| "pending"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { config } from "../../../shared/config/config.js";
|
||||
import {
|
||||
insertVoiceRecording,
|
||||
updateVoiceRecordingAsFailed,
|
||||
updateVoiceRecordingAsUploaded,
|
||||
} from "../../../shared/database/voiceRecordingRepo.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { uploadToTele } from "../teleUpload.js";
|
||||
|
||||
const logger = createChildLogger("recording-uploader");
|
||||
|
||||
@@ -37,14 +37,8 @@ export async function uploadToTele(input: {
|
||||
timeoutMs?: number;
|
||||
retries: number;
|
||||
}): Promise<TeleUploadResult> {
|
||||
const {
|
||||
buffer,
|
||||
filename,
|
||||
contentType,
|
||||
uploadUrl,
|
||||
timeoutMs,
|
||||
retries,
|
||||
} = input;
|
||||
const { buffer, filename, contentType, uploadUrl, timeoutMs, retries } =
|
||||
input;
|
||||
|
||||
const response = await retryWithBackoff(
|
||||
async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getVoiceConnection, type VoiceConnection } from "@discordjs/voice";
|
||||
import type { Client, Guild, VoiceChannel } from "discord.js-selfbot-v13";
|
||||
import { AppError } from "@bete/shared/errors";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { getVoiceConnection, type VoiceConnection } from "@discordjs/voice";
|
||||
import type { Client, Guild, VoiceChannel } from "discord.js-selfbot-v13";
|
||||
import { discordPlayer } from "./player.js";
|
||||
import { startRecording, stopRecording } from "./recorder.js";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
|
||||
import type { PoolClient } from "pg";
|
||||
import { Pool } from "pg";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import * as schema from "./schema.js";
|
||||
|
||||
const logger = createChildLogger("drizzle");
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import "dotenv/config";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { PoolClient } from "pg";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
|
||||
import { migrate as migratePostgres } from "drizzle-orm/node-postgres/migrator";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { PoolClient } from "pg";
|
||||
import {
|
||||
closeDatabase,
|
||||
initializeDatabase,
|
||||
@@ -44,7 +44,9 @@ async function getFirstMigrationTag(): Promise<string> {
|
||||
const journal: MigrationJournal = JSON.parse(raw);
|
||||
|
||||
if (!journal.entries || journal.entries.length === 0) {
|
||||
throw new Error("Migration journal is empty — cannot determine first migration tag");
|
||||
throw new Error(
|
||||
"Migration journal is empty — cannot determine first migration tag",
|
||||
);
|
||||
}
|
||||
|
||||
// Entries are ordered by idx — the first entry is the initial migration.
|
||||
@@ -110,7 +112,10 @@ async function seedDrizzleHistory(client: PoolClient): Promise<void> {
|
||||
[firstMigrationTag, Date.now()],
|
||||
);
|
||||
}
|
||||
logger.info({ firstMigrationTag }, "Drizzle history seeded — first migration marked applied");
|
||||
logger.info(
|
||||
{ firstMigrationTag },
|
||||
"Drizzle history seeded — first migration marked applied",
|
||||
);
|
||||
}
|
||||
|
||||
export async function runMigrations(): Promise<void> {
|
||||
|
||||
@@ -497,4 +497,5 @@ export type StickerCacheRecord = typeof stickerCacheTable.$inferSelect;
|
||||
export type StickerCacheInsert = typeof stickerCacheTable.$inferInsert;
|
||||
|
||||
export type CorrectedModeration = typeof correctedModerationsTable.$inferSelect;
|
||||
export type CorrectedModerationInsert = typeof correctedModerationsTable.$inferInsert;
|
||||
export type CorrectedModerationInsert =
|
||||
typeof correctedModerationsTable.$inferInsert;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { getDatabase } from "./drizzle.js";
|
||||
import {
|
||||
type VoiceRecording,
|
||||
|
||||
Reference in New Issue
Block a user