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