refactor: break monorepo into 3 standalone services (gateway, backend, frontend)
Build & Deploy / build-and-push (backend) (push) Failing after 35s
Build & Deploy / build-and-push (discord-gateway) (push) Failing after 25s
Build & Deploy / build-and-push (proxy) (push) Failing after 25s

- Remove pnpm workspace, moon repo, and all monorepo tooling
- Delete packages/shared/, embed shared code directly into each service
- Copy packages/shared/src/* -> services/backend/src/shared/ and services/discord-gateway/src/shared/
- Replace all @bete/shared imports with @/shared/ path alias
- Remove @bete/shared workspace dependency from both services
- Update root package.json scripts from --filter to --prefix
- Rewrite Dockerfiles to build each service standalone
- Clean up biome.json, .gitignore, remove root drizzle.config.ts
This commit is contained in:
Developer
2026-07-30 11:50:48 +07:00
parent 8eb7fa49e4
commit dcd13482c2
167 changed files with 1886 additions and 533 deletions
@@ -1,5 +1,5 @@
import { ConfigError, DatabaseError } from "@bete/shared/errors";
import { createChildLogger } from "@bete/shared/logger";
import { ConfigError, DatabaseError } from "@/shared/errors/index";
import { createChildLogger } from "@/shared/logger/index";
import { Client } from "discord.js-selfbot-v13";
import { inArray, lt } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { inArray, lt } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { config } from "../shared/config/config.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import type { createChildLogger } from "@bete/shared/logger";
import type { createChildLogger } from "@/shared/logger/index";
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";
+1 -1
View File
@@ -1,7 +1,7 @@
import "libsodium-wrappers";
import "@snazzah/davey";
import "dotenv/config";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { initializeDiscordGateway } from "./app/bootstrap.js";
const logger = createChildLogger("discord-gateway");
@@ -17,7 +17,7 @@
* simpler, unified worker that combines both layers.
*/
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import { initializeDatabase } from "../../shared/database/drizzle.js";
import { messageStore } from "../message-capture/messageStore.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Client } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
import type { EventBroadcaster } from "../event-broadcaster/index.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import { initializeDatabase } from "../../shared/database/drizzle.js";
import { extractMessageMediaEvidence } from "../message-capture/messageMetadata.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import type {
AnalysisResult,
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Guild } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
import type { MessageRecord } from "../message-capture/types.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Client, PermissionString } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
import { messageStore } from "../message-capture/messageStore.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Client } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
import type { MessageRecord } from "../message-capture/types.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import { isAgeRestrictedMetadata } from "../message-capture/messageMetadata.js";
import { messageStore } from "../message-capture/messageStore.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import { messageStore } from "../message-capture/messageStore.js";
import type { MessageRecord } from "../message-capture/types.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { eq } from "drizzle-orm";
import { getDatabase } from "../../shared/database/drizzle.js";
import {
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { encoding_for_model as encodingForModel } from "tiktoken";
import { formatMediaEvidenceForPrompt } from "../message-capture/messageMetadata.js";
import type { MessageRecord } from "../message-capture/types.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { LRUCache } from "lru-cache";
import { config } from "../../shared/config/config.js";
import { LAST_ERROR } from "./moderationState.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { and, desc, eq, sql } from "drizzle-orm";
import { config } from "../../shared/config/config.js";
import { getDatabase } from "../../shared/database/drizzle.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { LRUCache } from "lru-cache";
import { config } from "../../shared/config/config.js";
import { messageStore } from "../message-capture/messageStore.js";
@@ -9,8 +9,8 @@
*
* Both sides now import from this module instead.
*/
import { createChildLogger } from "@bete/shared/logger";
import { delay, retryWithBackoff } from "@bete/shared/utils";
import { createChildLogger } from "@/shared/logger/index";
import { delay, retryWithBackoff } from "@/shared/utils/index";
import type { ChatCompletion } from "openai/resources/chat/completions";
import { config } from "../../shared/config/config.js";
import type { AnalysisResult } from "../message-capture/types.js";
@@ -6,8 +6,8 @@
* defaults are maintained in one place.
*/
import { createChildLogger } from "@bete/shared/logger";
import { retryWithBackoff } from "@bete/shared/utils";
import { createChildLogger } from "@/shared/logger/index";
import { retryWithBackoff } from "@/shared/utils/index";
import OpenAI from "openai";
import pLimit from "p-limit";
import { config } from "../../shared/config/config.js";
@@ -5,7 +5,7 @@
* analysis, and calls the LLM for a batched moderation response. Extracted from
* moderationOrchestrator.ts.
*/
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import type {
AnalysisResult,
@@ -10,8 +10,8 @@ import { mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { createChildLogger } from "@bete/shared/logger";
import { createAbortControllerWithTimeout } from "@bete/shared/utils";
import { createChildLogger } from "@/shared/logger/index";
import { createAbortControllerWithTimeout } from "@/shared/utils/index";
import { resizeImageForVision } from "../attachment-upload/imageResizer.js";
import type { MessageMediaEvidence } from "../message-capture/messageMetadata.js";
import type { AttachmentRecord } from "../message-capture/types.js";
@@ -4,7 +4,7 @@
* Orchestrates LLM-based moderation analysis — manages batch splitting,
* parallel text+media analysis, LLM calls with retry, and cache handling.
*/
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import { extractMessageMediaEvidence } from "../message-capture/messageMetadata.js";
import type {
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { z } from "zod";
import type { AnalysisResult } from "../message-capture/types.js";
import type {
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { z } from "zod";
const log = createChildLogger("moderationSchemas");
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Client } from "discord.js-selfbot-v13";
import { LRUCache } from "lru-cache";
import { config } from "../../shared/config/config.js";
@@ -10,7 +10,7 @@
* Enables full audit trail and debugging of moderation decisions.
*/
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { AnalysisResult } from "../message-capture/types.js";
const logger = createChildLogger("response-logger");
@@ -1,5 +1,5 @@
import { createChildLogger } from "@bete/shared/logger";
import { createAbortControllerWithTimeout } from "@bete/shared/utils";
import { createChildLogger } from "@/shared/logger/index";
import { createAbortControllerWithTimeout } from "@/shared/utils/index";
import Redis from "ioredis";
const log = createChildLogger("searxng-search");
@@ -6,7 +6,7 @@
* Step 2: Real analysis text (only if not clean).
* Extracted from moderationOrchestrator.ts.
*/
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type {
AnalysisResult,
MessageRecord,
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import { executeAll, executeGet } from "../../shared/database/drizzle.js";
import { uploadToTele } from "../../shared/uploader.js";
@@ -5,7 +5,7 @@
* searches, deduplicates short messages, splits into sub-batches, and calls
* the LLM for analysis. Extracted from moderationOrchestrator.ts.
*/
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import type {
AnalysisResult,
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { executeAll, executeGet } from "../../shared/database/drizzle.js";
const logger = createChildLogger("text-cache-store");
@@ -1,7 +1,7 @@
import { resolve } from "node:dns/promises";
import { isIP } from "node:net";
import { createChildLogger } from "@bete/shared/logger";
import { createAbortControllerWithTimeout } from "@bete/shared/utils";
import { createChildLogger } from "@/shared/logger/index";
import { createAbortControllerWithTimeout } from "@/shared/utils/index";
const _log = createChildLogger("urlFetcher");
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { and, desc, eq, sql } from "drizzle-orm";
import { config } from "../../shared/config/config.js";
import { getDatabase } from "../../shared/database/drizzle.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { eq } from "drizzle-orm";
import { getDatabase } from "../../shared/database/drizzle.js";
import {
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { and, desc, eq } from "drizzle-orm";
import { getDatabase } from "../../shared/database/drizzle.js";
import {
@@ -5,8 +5,8 @@
* moderation pipeline, runs single-image vision LLM analysis with
* multi-layer caching, and detects whether a message has media content.
*/
import { createChildLogger } from "@bete/shared/logger";
import { delay } from "@bete/shared/utils";
import { createChildLogger } from "@/shared/logger/index";
import { delay } from "@/shared/utils/index";
import { config } from "../../shared/config/config.js";
import { extractMessageMediaEvidence } from "../message-capture/messageMetadata.js";
import type {
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import { uploadToTele } from "../../shared/uploader.js";
import { messageStore } from "../message-capture/messageStore.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import sharp from "sharp";
const log = createChildLogger("imageResizer");
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Client, TextChannel } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
import type { EventBroadcaster } from "../event-broadcaster/eventBroadcaster.js";
@@ -4,8 +4,8 @@ import {
type CommandReply,
MEDIA_STATUS_KEY,
VOICE_STATUS_KEY,
} from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
} from "../../shared/index.js";
import { createChildLogger } from "../../shared/logger/index.js";
import type { Client } from "discord.js-selfbot-v13";
import Redis from "ioredis";
import { config } from "../../shared/config/config.js";
@@ -1,5 +1,5 @@
import type { CommandMessage, CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import type { CommandMessage, CommandReply } from "../../shared/index.js";
import { createChildLogger } from "../../shared/logger/index.js";
import type { Client } from "discord.js-selfbot-v13";
// ---------------------------------------------------------------------------
@@ -14,7 +14,7 @@ import {
COMMAND_VOICE_TRANSMIT_STOP,
type CommandMessage,
type CommandReply,
} from "@bete/shared";
} from "../../shared/index.js";
import type { GuildHandler } from "./guild.handler.js";
import type { MediaHandler } from "./media.handler.js";
import type { ModerationHandler } from "./moderation.handler.js";
@@ -1,6 +1,6 @@
import { randomUUID } from "node:crypto";
import type { CommandMessage, CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import type { CommandMessage, CommandReply } from "../../shared/index.js";
import { createChildLogger } from "../../shared/logger/index.js";
import { StreamType } from "@discordjs/voice";
import {
extractMediaInfo,
@@ -1,5 +1,5 @@
import type { CommandMessage, CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import type { CommandMessage, CommandReply } from "../../shared/index.js";
import { createChildLogger } from "../../shared/logger/index.js";
import type { Client } from "discord.js-selfbot-v13";
import { messageStore } from "../message-capture/messageStore.js";
@@ -1,5 +1,5 @@
import type { CommandMessage, CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import type { CommandMessage, CommandReply } from "../../shared/index.js";
import { createChildLogger } from "../../shared/logger/index.js";
import type { Client } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
import { discordPlayer } from "../voice-recording/player.js";
@@ -1,5 +1,5 @@
import type { AttachmentRecord, MessageRecord } from "@bete/shared";
import { type CustomLogger, createChildLogger } from "@bete/shared/logger";
import type { AttachmentRecord, MessageRecord } from "../../shared/index.js";
import { type CustomLogger, createChildLogger } from "../../shared/logger/index.js";
import Redis from "ioredis";
import { type DiscordGatewayEvent, EventChannels } from "./eventTypes.js";
@@ -22,7 +22,7 @@ import {
DISCORD_VOICE_STOPPED,
DISCORD_VOICE_UPLOADED,
type DiscordGatewayEvent,
} from "@bete/shared";
} from "../../shared/index.js";
export type { DiscordGatewayEvent };
@@ -1,5 +1,5 @@
import http from "node:http";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
const logger = createChildLogger("gateway-metrics");
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type {
Client,
GuildMember,
@@ -1,4 +1,4 @@
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { createChildLogger, type Logger } from "@/shared/logger/index";
import { and, desc, eq, inArray, type SQL } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Client, Message } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
import { queueMessageAnalysis } from "../ai-moderation/aiAnalyzer.js";
@@ -1,4 +1,4 @@
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { createChildLogger, type Logger } from "@/shared/logger/index";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { getDatabase } from "../../shared/database/drizzle.js";
import type * as schema from "../../shared/database/schema.js";
@@ -1,4 +1,4 @@
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { createChildLogger, type Logger } from "@/shared/logger/index";
import { and, asc, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
@@ -1,4 +1,4 @@
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { createChildLogger, type Logger } from "@/shared/logger/index";
import { and, eq, isNull, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
@@ -1,4 +1,4 @@
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { createChildLogger, type Logger } from "@/shared/logger/index";
import { and, desc, eq, or, type SQL } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
@@ -1,4 +1,4 @@
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { createChildLogger, type Logger } from "@/shared/logger/index";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
import type {
@@ -1,5 +1,5 @@
import { buildCursorCondition, pageResult } from "@bete/shared";
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { buildCursorCondition, pageResult } from "../../shared/index.js";
import { createChildLogger, type Logger } from "../../shared/logger/index.js";
import { and, desc, eq, inArray, type SQL, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
@@ -1,4 +1,4 @@
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { createChildLogger, type Logger } from "@/shared/logger/index";
import { and, desc, eq, isNull, or, type SQL, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
@@ -1,5 +1,5 @@
import { buildCursorCondition, pageResult } from "@bete/shared";
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { buildCursorCondition, pageResult } from "../../shared/index.js";
import { createChildLogger, type Logger } from "../../shared/logger/index.js";
import { and, desc, eq, inArray, type SQL, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
@@ -1,4 +1,4 @@
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { createChildLogger, type Logger } from "@/shared/logger/index";
import { and, eq, isNull, or } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
@@ -1,5 +1,5 @@
import { buildCursorCondition, pageResult } from "@bete/shared";
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { buildCursorCondition, pageResult } from "../../shared/index.js";
import { createChildLogger, type Logger } from "../../shared/logger/index.js";
import { and, desc, eq, inArray, type SQL, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
@@ -5,7 +5,7 @@ import type {
MessageRecord,
UserMetadata,
VoiceRecordingUploadData,
} from "@bete/shared";
} from "../../shared/index.js";
import type prism from "prism-media";
// Re-export all shared types for backward compatibility
@@ -31,7 +31,7 @@ export type {
UserMetadata,
VoiceRecordingUploadData,
VoiceSegmentRecord,
} from "@bete/shared";
} from "../../shared/index.js";
// Types that are LOCAL ONLY (not in shared) — keep here
export interface SegmentState {
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type {
Client,
MessageReaction,
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Client, ThreadChannel } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
import type { EventBroadcaster } from "../event-broadcaster/eventBroadcaster.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Client, Presence } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
import type { EventBroadcaster } from "../event-broadcaster/eventBroadcaster.js";
@@ -7,7 +7,7 @@
* The backend forwards these unchanged to frontend clients.
*/
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import WebSocket from "ws";
const logger = createChildLogger("voice-pcm-ws");
@@ -1,5 +1,5 @@
import { spawn } from "node:child_process";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
const logger = createChildLogger("ffmpeg-process");
@@ -1,6 +1,6 @@
import { type ChildProcess, spawn } from "node:child_process";
import { PassThrough, type Readable } from "node:stream";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { StreamType } from "@discordjs/voice";
const logger = createChildLogger("media-source");
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { eq } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { getDatabase } from "../../shared/database/drizzle.js";
@@ -1,5 +1,5 @@
import { Transform, type TransformCallback } from "node:stream";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
const _logger = createChildLogger("packet-filter");
@@ -1,5 +1,5 @@
import type { Readable } from "node:stream";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import {
type AudioPlayer,
AudioPlayerStatus,
@@ -1,6 +1,6 @@
import { promises as fsPromises } from "node:fs";
import { createChildLogger } from "@bete/shared/logger";
import { retryWithBackoff } from "@bete/shared/utils";
import { createChildLogger } from "@/shared/logger/index";
import { retryWithBackoff } from "@/shared/utils/index";
import {
type DiscordGatewayAdapterCreator,
entersState,
@@ -1,5 +1,5 @@
import { createRequire } from "node:module";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import * as prism from "prism-media";
import { config } from "../../../shared/config/config.js";
@@ -1,5 +1,5 @@
import path from "node:path";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { Client, VoiceChannel } from "discord.js-selfbot-v13";
import { config } from "../../../shared/config/config.js";
import type {
@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import * as prism from "prism-media";
import type { SegmentState } from "../../message-capture/types.js";
@@ -1,5 +1,5 @@
import { promises as fsPromises } from "node:fs";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../../shared/config/config.js";
import type {
SegmentState,
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { UserMetadata } from "../../message-capture/types.js";
const logger = createChildLogger("recording-session");
@@ -1,6 +1,6 @@
import { promises as fsPromises } from "node:fs";
import path from "node:path";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { VoiceConnection } from "@discordjs/voice";
import type { Client, VoiceChannel } from "discord.js-selfbot-v13";
import type { EventBroadcaster } from "../../event-broadcaster/eventBroadcaster.js";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import type { VoiceConnection } from "@discordjs/voice";
import { EndBehaviorType } from "@discordjs/voice";
import { config } from "../../../shared/config/config.js";
@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../../shared/config/config.js";
import {
insertVoiceRecording,
@@ -1,7 +1,7 @@
import { spawn } from "node:child_process";
import { PassThrough } from "node:stream";
import { BACKEND_VOICE_TRANSMIT } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import { BACKEND_VOICE_TRANSMIT } from "../../shared/index.js";
import { createChildLogger } from "../../shared/logger/index.js";
import { StreamType } from "@discordjs/voice";
import type Redis from "ioredis";
import { discordPlayer } from "./player.js";
@@ -1,5 +1,5 @@
import { AppError } from "@bete/shared/errors";
import { createChildLogger } from "@bete/shared/logger";
import { AppError } from "@/shared/errors/index";
import { createChildLogger } from "@/shared/logger/index";
import type { VoiceConnection } from "@discordjs/voice";
import type { Client, Guild, VoiceChannel } from "discord.js-selfbot-v13";
import { discordPlayer } from "./player.js";
@@ -1,7 +1,7 @@
// ─── Voice Transcription — AI-powered speech-to-text for voice recordings ────
import { createReadStream } from "node:fs";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import OpenAI from "openai";
import { config } from "../../shared/config/config.js";
@@ -1,6 +1,6 @@
import "dotenv/config";
import type { AppConfig } from "@bete/shared/config";
import { loadConfig as sharedLoadConfig } from "@bete/shared/config";
import type { AppConfig } from "@/shared/config/index";
import { loadConfig as sharedLoadConfig } from "@/shared/config/index";
// Re-export the unified config — all EFFECTIVE_* fields are already
// computed by the shared loadConfig().
@@ -0,0 +1,311 @@
/**
* Unified configuration schema shared by all services.
*
* This is the single source of truth for all environment variables.
* Individual services re-export from here; they do NOT define their own schemas.
*/
import { z } from "zod";
import { ConfigError } from "../errors/index.js";
export const configSchema = z
.object({
// ── Discord ──────────────────────────────────────────────────────────
DISCORD_TOKEN: z
.string()
.min(1, "DISCORD_TOKEN is required")
.transform((value) => value.replace(/^("|')|(?:("|'))$/g, "")),
MONITOR_GUILD_IDS: z
.string()
.default("")
.transform((v) => v.split(",").filter(Boolean)),
MONITOR_GUILD_ID: z.string().min(1).optional(),
TEXT_GUILD_ID: z.string().min(1).optional(),
TEXT_CHANNEL_ID: z.string().min(1).optional(),
EXCLUDED_CHANNEL_IDS: z
.string()
.default("")
.transform((v) => v.split(",").filter(Boolean))
.describe("Channel IDs to exclude from capture"),
EXCLUDED_THREAD_IDS: z
.string()
.default("")
.transform((v) => v.split(",").filter(Boolean))
.describe("Thread IDs to exclude from capture"),
// ── Legacy voice ─────────────────────────────────────────────────────
VOICE_GUILD_ID: z.string().min(1).optional(),
VOICE_CHANNEL_ID: z.string().min(1).optional(),
// ── Recording ────────────────────────────────────────────────────────
RECORDINGS_DIR: z.string().default("./recordings"),
RECORDING_SEGMENT_MS: z.coerce.number().positive().default(5000),
// ── Decoder ──────────────────────────────────────────────────────────
DECODER_ROTATE_MS: z.coerce.number().positive().default(5000),
DECODER_COOLDOWN_MS: z.coerce.number().positive().default(30000),
// ── Audio ────────────────────────────────────────────────────────────
AUDIO_STREAM_SILENCE_DURATION_MS: z.coerce
.number()
.positive()
.default(3000),
PACKET_FILTER_MIN_SIZE: z.coerce.number().positive().default(8),
OPUS_FRAME_SIZE: z.coerce.number().positive().default(960),
AUDIO_SAMPLE_RATE: z.coerce.number().positive().default(48000),
AUDIO_CHANNELS: z.coerce.number().positive().default(2),
AVATAR_SIZE: z.coerce.number().positive().default(64),
// ── Server ───────────────────────────────────────────────────────────
WEBSERVER_PORT: z.coerce.number().positive().default(3001),
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
LOG_LEVEL: z
.enum(["error", "warn", "info", "http", "verbose", "debug", "silly"])
.default("info"),
VERBOSE: z
.string()
.optional()
.transform((v) => v === "true")
.default(false),
ADMIN_PASSWORD: z.string().default("admin123"),
WEBHOOK_URLS: z
.string()
.default("")
.transform((v) => v.split(",").filter(Boolean)),
WEBHOOK_EVENTS: z
.string()
.default("message_flagged,auto_deleted,high_severity")
.transform((v) => v.split(",").filter(Boolean)),
METRICS_PORT: z.coerce.number().positive().default(9090),
// ── Database (PostgreSQL) ────────────────────────────────────────────
DATABASE_URL: z.string().optional(),
POSTGRES_HOST: z.string().default("localhost"),
POSTGRES_PORT: z.coerce.number().int().positive().default(5432),
POSTGRES_USER: z.string().optional(),
POSTGRES_PASSWORD: z.string().optional(),
POSTGRES_DB: z.string().optional(),
POSTGRES_POOL_MIN: z.coerce.number().int().positive().default(2),
POSTGRES_POOL_MAX: z.coerce.number().int().positive().default(10),
// ── Redis ────────────────────────────────────────────────────────────
REDIS_URL: z.string().default("redis://localhost:6379"),
// ── Voice PCM WebSocket (direct gateway→backend, bypasses Redis) ────
VOICE_PCM_WS_ENABLED: z
.string()
.optional()
.transform((v) => v === "true")
.default(true),
BACKEND_WS_URL: z.string().default("ws://backend:3000/ws"),
BACKEND_WS_TOKEN: z.string().optional().default(""),
// ── Connection ───────────────────────────────────────────────────────
VOICE_CONNECTION_TIMEOUT_MS: z.coerce.number().positive().default(15000),
RECONNECT_TIMEOUT_MS: z.coerce.number().positive().default(5000),
// ── Attachments ─────────────────────────────────────────────────────
TELE_UPLOAD_URL: z
.string()
.url()
.default("https://upload.asepharyana.my.id/api/upload"),
ATTACHMENT_UPLOAD_TIMEOUT_MS: z.coerce.number().positive().default(30000),
ATTACHMENT_MAX_SIZE_MB: z.coerce.number().positive().default(100),
ATTACHMENT_RETRY_ATTEMPTS: z.coerce.number().positive().default(3),
BACKLOG_SYNC_HOURS: z.coerce.number().positive().default(24),
BACKLOG_SYNC_BATCH_SIZE: z.coerce
.number()
.int()
.positive()
.max(100)
.default(100),
// ── AI Analysis ─────────────────────────────────────────────────────
AI_ANALYSIS_ENABLED: z
.string()
.optional()
.transform((v) => v === "true")
.default(false),
AI_LLM_API_KEY: z.string().optional(),
AI_LLM_BASE_URL: z
.string()
.url()
.default("https://9router.asepharyana.my.id/v1"),
AI_LLM_MODEL: z.string().default("text"),
AI_LLM_VISION_MODEL: z.string().optional(),
AI_LLM_MAX_CONCURRENT: z.coerce.number().int().positive().default(5),
AI_LLM_IMAGE_MAX_DIMENSION: z.coerce
.number()
.int()
.positive()
.default(1024),
AI_LLM_TEXT_BATCH_SIZE: z.coerce.number().int().positive().default(20),
AI_LLM_MEDIA_ANALYSIS_TIMEOUT_MS: z.coerce
.number()
.int()
.positive()
.default(60000),
// ── AI Model (new unified keys) ───────────────────────────────────
AI_MODEL_FAST_CLASSIFIER_ENABLED: z
.string()
.optional()
.transform((v) => v === "true")
.default(true)
.describe("Enable Layer 1 fast heuristic classifier"),
AI_MODEL_LLM_TIMEOUT_MS: z.coerce
.number()
.int()
.positive()
.default(30000)
.describe("Timeout for individual LLM moderation calls"),
// ── AI Analysis Timing ──────────────────────────────────────────────
AI_ANALYSIS_DEBOUNCE_MS: z.coerce.number().positive().default(500),
AI_ANALYSIS_RECOVERY_INTERVAL_MS: z.coerce
.number()
.positive()
.default(15000),
AI_ANALYSIS_ERROR_COOLDOWN_MS: z.coerce.number().positive().default(30000),
// ── AI Analysis Batch ───────────────────────────────────────────────
AI_ANALYSIS_MAX_BATCH_SIZE: z.coerce.number().int().positive().default(200),
AI_ANALYSIS_MAX_CONTEXT_TOKENS: z.coerce.number().positive().default(8000),
AI_ANALYSIS_MAX_TARGET_TOKENS: z.coerce.number().positive().default(4000),
AI_ANALYSIS_CONTEXT_MESSAGE_LIMIT: z.coerce
.number()
.int()
.positive()
.default(20),
AI_ANALYSIS_PROCESSING_TIMEOUT_MS: z.coerce
.number()
.positive()
.default(120000),
AI_ANALYSIS_INDIVIDUAL_MAX_CONCURRENT: z.coerce
.number()
.int()
.positive()
.default(50),
AI_ANALYSIS_INDIVIDUAL_CB_THRESHOLD: z.coerce
.number()
.int()
.positive()
.default(50),
PISCINA_MAX_THREADS: z.coerce.number().int().positive().optional(),
// ── Voice Transcription ────────────────────────────────────────────────
AI_VOICE_TRANSCRIPTION_ENABLED: z
.string()
.optional()
.transform((v) => v === "true")
.default(false),
// ── OpenAI Moderation ───────────────────────────────────────────────
OPENAI_MODERATION_API_KEY: z.string().optional(),
OPENAI_MODERATION_BASE_URL: z
.string()
.url()
.default("https://api.openai.com/v1"),
OPENAI_MODERATION_MODEL: z.string().default("omni-moderation-latest"),
// ── Auto Delete ─────────────────────────────────────────────────────
AUTO_DELETE_FLAGGED_ENABLED: z
.string()
.optional()
.transform((v) => v === "true")
.default(true),
AUTO_DELETE_FLAGGED_DRY_RUN: z
.string()
.optional()
.transform((v) => v === "true")
.default(false),
AUTO_DELETE_FLAGGED_DELAY_MS: z.coerce.number().min(0).default(0),
AUTO_DELETE_MIN_CONFIDENCE: z.coerce.number().min(0).max(1).default(0.5),
AUTO_DELETE_ALLOWED_SEVERITIES: z
.string()
.default("critical,high,medium,low"),
AUTO_DELETE_ALLOWED_CATEGORIES: z.string().default(""),
AUTO_DELETE_EXCLUDED_CHANNEL_IDS: z.string().default(""),
AUTO_DELETE_EXCLUDED_USER_IDS: z.string().default(""),
AUTO_DELETE_NOTIFY_USER: z
.string()
.optional()
.transform((v) => v === "true")
.default(false),
AUTO_DELETE_LOG_CHANNEL_ID: z.string().default(""),
// ── Retention ───────────────────────────────────────────────────────
RETENTION_MESSAGES_DAYS: z.coerce.number().int().min(0).default(0),
RETENTION_ATTACHMENTS_DAYS: z.coerce.number().int().min(0).default(0),
RETENTION_VOICE_DAYS: z.coerce.number().int().min(0).default(0),
RETENTION_CLEANUP_INTERVAL_MS: z.coerce
.number()
.positive()
.default(24 * 60 * 60 * 1000),
RETENTION_DRY_RUN: z
.string()
.optional()
.transform((v) => v === "true")
.default(true),
AUTO_MIGRATE_ON_STARTUP: z
.string()
.optional()
.transform((v) => v === "true")
.default(true),
})
.superRefine((value, ctx) => {
if (!value.AI_ANALYSIS_ENABLED) {
// skip: AI analysis not enabled
} else if (!value.AI_LLM_API_KEY) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["AI_LLM_API_KEY"],
message: "AI_LLM_API_KEY is required when AI_ANALYSIS_ENABLED=true",
});
}
// Validate database configuration
if (!value.DATABASE_URL && !value.POSTGRES_HOST) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["DATABASE_URL"],
message: "Either DATABASE_URL or POSTGRES_HOST must be provided",
});
}
});
export type AppConfig = z.infer<typeof configSchema> & {
EFFECTIVE_TEXT_GUILD_ID?: string;
EFFECTIVE_VOICE_GUILD_ID?: string;
EFFECTIVE_MONITOR_GUILD_IDS: string[];
};
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
try {
const parsed = configSchema.parse(env);
return {
...parsed,
EFFECTIVE_TEXT_GUILD_ID: parsed.TEXT_GUILD_ID ?? parsed.MONITOR_GUILD_ID,
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID,
EFFECTIVE_MONITOR_GUILD_IDS:
parsed.MONITOR_GUILD_IDS.length > 0
? parsed.MONITOR_GUILD_IDS
: parsed.MONITOR_GUILD_ID
? [parsed.MONITOR_GUILD_ID]
: [],
};
} catch (error) {
if (error instanceof z.ZodError) {
const messages = error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join("\n");
throw new ConfigError(`Configuration validation failed:\n${messages}`);
}
throw error;
}
}
/** Singleton config loaded from process.env at import time. */
export const config = loadConfig();
@@ -5,8 +5,8 @@ import {
getDatabase as sharedGetDb,
initializeDatabase as sharedInit,
withDatabaseClient as sharedWithClient,
} from "@bete/shared/database/init";
import { createChildLogger } from "@bete/shared/logger";
} from "@/shared/database/init";
import { createChildLogger } from "@/shared/logger/index";
import { config } from "../../shared/config/config.js";
import * as schema from "./schema.js";
@@ -0,0 +1,131 @@
import { createChildLogger } from "../logger/index.js";
import { drizzle } from "drizzle-orm/node-postgres";
import type { Pool, PoolClient } from "pg";
import { closePool, createPoolFromConfig } from "./pool.js";
const logger = createChildLogger("database.init");
let db: ReturnType<typeof drizzle> | null = null;
let rawPool: Pool | null = null;
export interface DatabaseConfig {
DATABASE_URL?: string;
POSTGRES_HOST?: string;
POSTGRES_PORT?: number;
POSTGRES_USER?: string;
POSTGRES_PASSWORD?: string;
POSTGRES_DB?: string;
POSTGRES_POOL_MIN?: number;
POSTGRES_POOL_MAX?: number;
}
export async function initializeDatabase(
cfg: DatabaseConfig,
schema?: Record<string, unknown>,
) {
if (db !== null) return db;
const pool = cfg.DATABASE_URL
? createPoolFromConfig({
url: cfg.DATABASE_URL,
min: cfg.POSTGRES_POOL_MIN,
max: cfg.POSTGRES_POOL_MAX,
})
: createPoolFromConfig({
host: cfg.POSTGRES_HOST,
port: cfg.POSTGRES_PORT,
user: cfg.POSTGRES_USER,
password: cfg.POSTGRES_PASSWORD,
database: cfg.POSTGRES_DB,
min: cfg.POSTGRES_POOL_MIN,
max: cfg.POSTGRES_POOL_MAX,
});
rawPool = pool;
if (schema) {
db = drizzle(pool, { schema });
} else {
db = drizzle(pool);
}
try {
const client = await pool.connect();
client.release();
logger.info("Database connection successful");
} catch (err) {
logger.error({ err }, "Failed to connect to database");
throw err;
}
return db;
}
export function getDatabase() {
if (db === null) {
throw new Error(
"Database not initialized. Call initializeDatabase() first.",
);
}
return db;
}
export function getPool() {
if (!rawPool) {
throw new Error(
"Database not initialized. Call initializeDatabase() first.",
);
}
return rawPool;
}
export async function closeDatabase() {
if (rawPool !== null) {
await closePool(rawPool);
}
rawPool = null;
db = null;
logger.info("Database connection closed");
}
function convertPlaceholdersForPostgres(sql: string) {
let i = 0;
return sql.replace(/\?/g, () => `$${++i}`);
}
export async function executeAll(sql: string, params?: unknown[]) {
if (!rawPool) {
throw new Error(
"Database not initialized. Call initializeDatabase() first.",
);
}
const query = convertPlaceholdersForPostgres(sql);
const result = await rawPool.query(query, params || []);
return result.rows;
}
export async function executeGet(sql: string, params?: unknown[]) {
if (!rawPool) {
throw new Error(
"Database not initialized. Call initializeDatabase() first.",
);
}
const query = convertPlaceholdersForPostgres(sql);
const result = await rawPool.query(query, params || []);
return result.rows[0] ?? null;
}
export async function withDatabaseClient<T>(
callback: (client: PoolClient) => Promise<T>,
): Promise<T> {
if (!rawPool) {
throw new Error(
"Database not initialized. Call initializeDatabase() first.",
);
}
const client = await rawPool.connect();
try {
return await callback(client);
} finally {
client.release();
}
}
@@ -1,7 +1,7 @@
import "dotenv/config";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
import { migrate as migratePostgres } from "drizzle-orm/node-postgres/migrator";
import type { PoolClient } from "pg";
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { runMigrations } from "./migrate.js";
const logger = createChildLogger("migrate-cli");
@@ -0,0 +1,36 @@
import { type PoolConfig as PgPoolConfig, Pool } from "pg";
export interface PoolConfig {
url?: string;
host?: string;
port?: number;
user?: string;
password?: string;
database?: string;
min?: number;
max?: number;
}
export function createPoolFromConfig(cfg: PoolConfig): Pool {
const opts: PgPoolConfig = {
min: cfg.min ?? 2,
max: cfg.max ?? 10,
};
if (cfg.url) {
opts.connectionString = cfg.url;
} else {
opts.host = cfg.host;
opts.port = cfg.port;
opts.user = cfg.user;
opts.password = cfg.password;
opts.database = cfg.database;
}
return new Pool(opts);
}
export function closePool(pool: Pool | null): Promise<void> {
if (!pool) return Promise.resolve();
return pool.end();
}
@@ -1,13 +1,596 @@
// ---------------------------------------------------------------------------
// Database Schema — barrel re-export
//
// All table definitions have been split into domain files under schema/.
// This barrel preserves backward compatibility for existing imports.
// New code can import from the specific domain file (e.g., schema/messages.js).
// ---------------------------------------------------------------------------
import {
bigint as pgBigint,
boolean as pgBoolean,
foreignKey as pgForeignKey,
index as pgIndex,
integer as pgInteger,
jsonb as pgJsonb,
real as pgReal,
pgTable,
text as pgText,
timestamp as pgTimestamp,
uuid as pgUuid,
} from "drizzle-orm/pg-core";
export * from "./schema/analytics.js";
export * from "./schema/cache.js";
export * from "./schema/messages.js";
export * from "./schema/meta.js";
export * from "./schema/voice.js";
// =============================================================================
// Messages
// =============================================================================
/**
* Messages Table (PostgreSQL)
* Stores text messages with AI moderation analysis
*/
export const pgMessagesTable = pgTable(
"messages",
{
id: pgText("id").primaryKey(),
guild_id: pgText("guild_id").notNull(),
channel_id: pgText("channel_id").notNull(),
thread_id: pgText("thread_id"),
user_id: pgText("user_id").notNull(),
username: pgText("username").notNull(),
avatar_url: pgText("avatar_url"),
content: pgText("content").notNull(),
edited_content: pgText("edited_content"),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
edited_at: pgBigint("edited_at", { mode: "number" }),
deleted_at: pgBigint("deleted_at", { mode: "number" }),
type: pgText("type", { enum: ["text", "edited", "deleted"] })
.notNull()
.default("text"),
is_reply: pgBoolean("is_reply"),
is_forward: pgBoolean("is_forward"),
is_crosspost: pgBoolean("is_crosspost"),
reference_message_id: pgText("reference_message_id"),
reference_channel_id: pgText("reference_channel_id"),
reference_guild_id: pgText("reference_guild_id"),
metadata: pgText("metadata"),
ai_status: pgText("ai_status", {
enum: ["pending", "processing", "clean", "warn", "flagged", "error"],
})
.notNull()
.default("pending"),
ai_moderation_flags: pgText("ai_moderation_flags"),
ai_moderation_score: pgReal("ai_moderation_score"),
ai_analysis: pgText("ai_analysis"),
ai_categories: pgText("ai_categories"),
ai_severity: pgText("ai_severity", {
enum: ["none", "low", "medium", "high", "critical"],
}),
ai_confidence: pgReal("ai_confidence"),
ai_recommended_action: pgText("ai_recommended_action", {
enum: ["none", "monitor", "warn", "review", "delete", "escalate"],
}),
ai_analyzed_at: pgBigint("ai_analyzed_at", { mode: "number" }),
ai_error: pgText("ai_error"),
},
(table) => ({
channelIdx: pgIndex("idx_messages_channel").on(table.channel_id),
userIdx: pgIndex("idx_messages_user").on(table.user_id),
createdIdx: pgIndex("idx_messages_created").on(table.created_at),
threadIdx: pgIndex("idx_messages_thread").on(table.thread_id),
channelCreatedIdx: pgIndex("idx_messages_channel_created").on(
table.channel_id,
table.created_at,
table.id,
),
threadCreatedIdx: pgIndex("idx_messages_thread_created").on(
table.thread_id,
table.created_at,
table.id,
),
aiStatusCreatedIdx: pgIndex("idx_messages_ai_status_created").on(
table.ai_status,
table.created_at,
table.id,
),
guildAiStatusCreatedIdx: pgIndex("idx_messages_guild_ai_status_created").on(
table.guild_id,
table.ai_status,
table.created_at,
table.id,
),
guildAiStatusAnalyzedIdx: pgIndex(
"idx_messages_guild_ai_status_analyzed",
).on(table.guild_id, table.ai_status, table.ai_analyzed_at, table.id),
guildCreatedDeletedIdx: pgIndex("idx_messages_guild_created_deleted").on(
table.guild_id,
table.created_at,
table.deleted_at,
table.id,
),
channelAiStatusCreatedIdx: pgIndex(
"idx_messages_channel_ai_status_created",
).on(table.channel_id, table.ai_status, table.created_at, table.id),
threadAiStatusCreatedIdx: pgIndex(
"idx_messages_thread_ai_status_created",
).on(table.thread_id, table.ai_status, table.created_at, table.id),
}),
);
export const messagesTable = pgMessagesTable;
/**
* Attachments Table (PostgreSQL)
* Stores attachment metadata with upload status tracking
*/
export const pgAttachmentsTable = pgTable(
"attachments",
{
id: pgText("id").primaryKey(),
message_id: pgText("message_id").notNull(),
guild_id: pgText("guild_id").notNull(),
channel_id: pgText("channel_id").notNull(),
thread_id: pgText("thread_id"),
user_id: pgText("user_id").notNull(),
filename: pgText("filename").notNull(),
size: pgInteger("size").notNull(),
type: pgText("type").notNull(),
discord_url: pgText("discord_url").notNull(),
uploaded_url: pgText("uploaded_url"),
upload_status: pgText("upload_status", {
enum: ["pending", "uploaded", "failed"],
})
.notNull()
.default("pending"),
upload_error: pgText("upload_error"),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
uploaded_at: pgBigint("uploaded_at", { mode: "number" }),
},
(table) => ({
channelIdx: pgIndex("idx_attachments_channel").on(table.channel_id),
messageIdx: pgIndex("idx_attachments_message").on(table.message_id),
statusIdx: pgIndex("idx_attachments_status").on(table.upload_status),
channelCreatedIdx: pgIndex("idx_attachments_channel_created").on(
table.channel_id,
table.created_at,
table.id,
),
threadCreatedIdx: pgIndex("idx_attachments_thread_created").on(
table.thread_id,
table.created_at,
table.id,
),
messageFk: pgForeignKey({
columns: [table.message_id],
foreignColumns: [pgMessagesTable.id],
name: "fk_attachments_message_id",
}).onDelete("cascade"),
}),
);
export const attachmentsTable = pgAttachmentsTable;
/**
* Message Reviews Table (PostgreSQL)
* Tracks manual reviews of messages flagged by AI moderation
*/
export const pgMessageReviewsTable = pgTable(
"message_reviews",
{
id: pgText("id").primaryKey(),
message_id: pgText("message_id").notNull(),
guild_id: pgText("guild_id").notNull(),
channel_id: pgText("channel_id").notNull(),
reviewer_id: pgText("reviewer_id"),
status: pgText("status", {
enum: ["pending", "approved", "rejected", "escalated"],
})
.notNull()
.default("pending"),
notes: pgText("notes"),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
reviewed_at: pgBigint("reviewed_at", { mode: "number" }),
},
(table) => ({
messageIdIdx: pgIndex("idx_message_reviews_message_id").on(
table.message_id,
),
statusIdx: pgIndex("idx_message_reviews_status").on(table.status),
createdAtIdx: pgIndex("idx_message_reviews_created_at").on(
table.created_at,
),
guildStatusIdx: pgIndex("idx_message_reviews_guild_status").on(
table.guild_id,
table.status,
table.created_at,
),
}),
);
export const messageReviewsTable = pgMessageReviewsTable;
// =============================================================================
// Moderation / Corrections
// =============================================================================
/**
* Corrected Moderations Table (PostgreSQL)
* Stores manual corrections of AI moderation false positives
* for few-shot injection into LLM moderation prompts.
*/
export const pgCorrectedModerationsTable = pgTable(
"corrected_moderations",
{
id: pgText("id").primaryKey(),
message_id: pgText("message_id").notNull(),
original_flags: pgText("original_flags").notNull(),
corrected_flags: pgText("corrected_flags").notNull(),
correction_notes: pgText("correction_notes"),
content_snippet: pgText("content_snippet").notNull(),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
},
(table) => ({
createdAtIdx: pgIndex("idx_corrected_moderations_created_at").on(
table.created_at,
),
messageIdIdx: pgIndex("idx_corrected_moderations_message_id").on(
table.message_id,
),
}),
);
export const correctedModerationsTable = pgCorrectedModerationsTable;
// =============================================================================
// Voice Recordings
// =============================================================================
/**
* Voice Recordings Table (PostgreSQL)
* Stores voice recording segment metadata and upload status
*/
export const pgVoiceRecordingsTable = pgTable(
"voice_recordings",
{
id: pgText("id").primaryKey(),
user_id: pgText("user_id").notNull(),
username: pgText("username").notNull(),
avatar_url: pgText("avatar_url"),
guild_id: pgText("guild_id"),
channel_id: pgText("channel_id"),
channel_name: pgText("channel_name"),
filename: pgText("filename").notNull(),
size_bytes: pgInteger("size_bytes").notNull(),
download_url: pgText("download_url"),
upload_status: pgText("upload_status", {
enum: ["pending", "uploaded", "failed"],
})
.notNull()
.default("pending"),
upload_error: pgText("upload_error"),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
uploaded_at: pgBigint("uploaded_at", { mode: "number" }),
transcription: pgText("transcription"),
},
(table) => ({
userIdIdx: pgIndex("idx_voice_recordings_user_id").on(table.user_id),
channelIdIdx: pgIndex("idx_voice_recordings_channel_id").on(
table.channel_id,
),
createdIdx: pgIndex("idx_voice_recordings_created_at").on(table.created_at),
}),
);
export const voiceRecordingsTable = pgVoiceRecordingsTable;
// =============================================================================
// AI Analysis / Analytics
// =============================================================================
/**
* AI Analysis Runs Table (PostgreSQL)
* Tracks AI analysis batch runs for conversation-level moderation
*/
export const pgAIAnalysisRunsTable = pgTable(
"ai_analysis_runs",
{
id: pgText("id").primaryKey(),
conversation_key: pgText("conversation_key").notNull(),
target_message_ids: pgText("target_message_ids").notNull(),
model: pgText("model").notNull(),
request_tokens_estimate: pgInteger("request_tokens_estimate"),
response_raw: pgText("response_raw"),
status: pgText("status", {
enum: ["pending", "processing", "completed", "failed"],
})
.notNull()
.default("pending"),
error: pgText("error"),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
completed_at: pgBigint("completed_at", { mode: "number" }),
},
(table) => ({
conversationKeyIdx: pgIndex("idx_ai_analysis_runs_conversation_key").on(
table.conversation_key,
),
statusIdx: pgIndex("idx_ai_analysis_runs_status").on(table.status),
createdAtIdx: pgIndex("idx_ai_analysis_runs_created_at").on(
table.created_at,
),
}),
);
export const aiAnalysisRunsTable = pgAIAnalysisRunsTable;
/**
* User Profiles Table (PostgreSQL)
* Stores AI-generated summaries of user behavior patterns.
*/
export const pgUserProfilesTable = pgTable(
"user_profiles",
{
user_id: pgText("user_id").primaryKey(),
guild_id: pgText("guild_id").notNull(),
profile_summary: pgText("profile_summary").notNull(),
last_analyzed_at: pgBigint("last_analyzed_at", {
mode: "number",
}).notNull(),
},
(table) => ({
guildIdx: pgIndex("idx_user_profiles_guild_id").on(table.guild_id),
}),
);
export const userProfilesTable = pgUserProfilesTable;
/**
* User Reputations Table (PostgreSQL)
* Tracks user trust score and infractions to provide context to AI.
*/
export const pgUserReputationsTable = pgTable(
"user_reputations",
{
user_id: pgText("user_id").primaryKey(),
guild_id: pgText("guild_id").notNull(),
trust_score: pgInteger("trust_score").notNull().default(50),
clean_message_streak: pgInteger("clean_message_streak")
.notNull()
.default(0),
total_infractions: pgInteger("total_infractions").notNull().default(0),
last_infraction_at: pgBigint("last_infraction_at", { mode: "number" }),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
updated_at: pgBigint("updated_at", { mode: "number" }).notNull(),
},
(table) => ({
guildIdx: pgIndex("idx_user_reputations_guild_id").on(table.guild_id),
scoreIdx: pgIndex("idx_user_reputations_trust_score").on(table.trust_score),
}),
);
export const userReputationsTable = pgUserReputationsTable;
/**
* Channel Cultures Table (PostgreSQL)
* Stores AI-generated summaries of channel norms and slang to inject as context.
*/
export const pgChannelCulturesTable = pgTable(
"channel_cultures",
{
channel_id: pgText("channel_id").primaryKey(),
guild_id: pgText("guild_id").notNull(),
culture_summary: pgText("culture_summary").notNull(),
last_analyzed_at: pgBigint("last_analyzed_at", {
mode: "number",
}).notNull(),
},
(table) => ({
guildIdx: pgIndex("idx_channel_cultures_guild_id").on(table.guild_id),
}),
);
export const channelCulturesTable = pgChannelCulturesTable;
// =============================================================================
// Cache (text analysis + stickers)
// =============================================================================
/**
* Text Analysis Cache Table (PostgreSQL)
* Caches per-normalized-text moderation analysis results.
*/
export const pgTextAnalysisCacheTable = pgTable(
"text_analysis_cache",
{
text: pgText("text").primaryKey(),
flags: pgText("flags").notNull().default("[]"),
source: pgText("source", {
enum: ["local", "primary_ai", "vision_llm"],
})
.notNull()
.default("local"),
analyzed_at: pgBigint("analyzed_at", { mode: "number" }).notNull(),
expires_at: pgBigint("expires_at", { mode: "number" }).notNull(),
hit_count: pgInteger("hit_count").notNull().default(0),
},
(table) => ({
expiresAtIdx: pgIndex("idx_text_analysis_cache_expires_at").on(
table.expires_at,
),
sourceIdx: pgIndex("idx_text_analysis_cache_source").on(table.source),
}),
);
export const textAnalysisCacheTable = pgTextAnalysisCacheTable;
/**
* Sticker Cache Table (PostgreSQL)
* Stores uploaded sticker image URLs for vision analysis.
*/
export const pgStickerCacheTable = pgTable(
"sticker_cache",
{
name: pgText("name").primaryKey(),
imageUrl: pgText("image_url").notNull().default(""),
mime_type: pgText("mime_type").notNull(),
fetched_at: pgBigint("fetched_at", { mode: "number" }).notNull(),
},
(table) => ({
fetchedAtIdx: pgIndex("idx_sticker_cache_fetched_at").on(table.fetched_at),
}),
);
export const stickerCacheTable = pgStickerCacheTable;
// =============================================================================
// Meta / System
// =============================================================================
/**
* Muxer Jobs Table (PostgreSQL)
* Tracks audio post-processing jobs with status and retry logic
*/
export const pgMuxerJobsTable = pgTable(
"muxer_jobs",
{
id: pgText("id").primaryKey(),
data: pgText("data").notNull(),
status: pgText("status", {
enum: ["pending", "processing", "completed", "failed"],
})
.notNull()
.default("pending"),
attempts: pgInteger("attempts").notNull().default(0),
maxAttempts: pgInteger("maxAttempts").notNull().default(3),
createdAt: pgBigint("createdAt", { mode: "number" }).notNull(),
updatedAt: pgBigint("updatedAt", { mode: "number" }).notNull(),
error: pgText("error"),
},
(table) => ({
statusIdx: pgIndex("idx_muxer_jobs_status").on(table.status),
createdAtIdx: pgIndex("idx_muxer_jobs_createdAt").on(table.createdAt),
}),
);
export const muxerJobsTable = pgMuxerJobsTable;
/**
* UI State Table (PostgreSQL)
* Stores persistent UI state (e.g., selected channel, filter preferences)
*/
export const pgUIStateTable = pgTable("ui_state", {
key: pgText("key").primaryKey(),
value: pgText("value").notNull(),
updated_at: pgBigint("updated_at", { mode: "number" }).notNull(),
});
export const uiStateTable = pgUIStateTable;
/**
* Retention Policies Table (PostgreSQL)
* Defines data retention rules per guild/channel
*/
export const pgRetentionPoliciesTable = pgTable(
"retention_policies",
{
id: pgText("id").primaryKey(),
guild_id: pgText("guild_id").notNull(),
channel_id: pgText("channel_id"),
retention_days: pgInteger("retention_days").notNull().default(90),
apply_to_media: pgBoolean("apply_to_media").notNull().default(true),
apply_to_voice: pgBoolean("apply_to_voice").notNull().default(true),
enabled: pgBoolean("enabled").notNull().default(true),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
updated_at: pgBigint("updated_at", { mode: "number" }).notNull(),
},
(table) => ({
guildIdIdx: pgIndex("idx_retention_policies_guild_id").on(table.guild_id),
enabledIdx: pgIndex("idx_retention_policies_enabled").on(table.enabled),
}),
);
export const retentionPoliciesTable = pgRetentionPoliciesTable;
/**
* Chatbot Messages Table (PostgreSQL)
* Stores AI chat conversation history
*/
export const pgChatbotMessagesTable = pgTable(
"chatbot_messages",
{
id: pgUuid("id").defaultRandom().primaryKey(),
user_id: pgText("user_id").notNull(),
user_message: pgText("user_message").notNull(),
bot_response: pgText("bot_response").notNull(),
context: pgJsonb("context").notNull().default("{}"),
created_at: pgTimestamp("created_at", { withTimezone: true, mode: "date" })
.notNull()
.defaultNow(),
},
(table) => ({
userCreatedIdx: pgIndex("idx_chatbot_messages_user_created").on(
table.user_id,
table.created_at.desc(),
),
}),
);
export const chatbotMessagesTable = pgChatbotMessagesTable;
// =============================================================================
// Type Exports
// =============================================================================
// Messages
export type Message = typeof messagesTable.$inferSelect;
export type MessageInsert = typeof messagesTable.$inferInsert;
// Attachments
export type Attachment = typeof attachmentsTable.$inferSelect;
export type AttachmentInsert = typeof attachmentsTable.$inferInsert;
// Message Reviews
export type DbMessageReview = typeof messageReviewsTable.$inferSelect;
export type DbMessageReviewInsert = typeof messageReviewsTable.$inferInsert;
// Corrected Moderations
export type CorrectedModeration = typeof correctedModerationsTable.$inferSelect;
export type CorrectedModerationInsert =
typeof correctedModerationsTable.$inferInsert;
// Voice Recordings
export type VoiceRecording = typeof voiceRecordingsTable.$inferSelect;
export type VoiceRecordingInsert = typeof voiceRecordingsTable.$inferInsert;
// AI Analysis Runs
export type AIAnalysisRun = typeof aiAnalysisRunsTable.$inferSelect;
export type AIAnalysisRunInsert = typeof aiAnalysisRunsTable.$inferInsert;
// User Profiles
export type UserProfile = typeof userProfilesTable.$inferSelect;
export type UserProfileInsert = typeof userProfilesTable.$inferInsert;
// User Reputations
export type UserReputation = typeof userReputationsTable.$inferSelect;
export type UserReputationInsert = typeof userReputationsTable.$inferInsert;
// Channel Cultures
export type ChannelCulture = typeof channelCulturesTable.$inferSelect;
export type ChannelCultureInsert = typeof channelCulturesTable.$inferInsert;
// Text Analysis Cache
export type TextAnalysisCache = typeof textAnalysisCacheTable.$inferSelect;
export type TextAnalysisCacheInsert =
typeof textAnalysisCacheTable.$inferInsert;
// Sticker Cache
export type StickerCacheRecord = typeof stickerCacheTable.$inferSelect;
export type StickerCacheInsert = typeof stickerCacheTable.$inferInsert;
// Muxer Jobs
export type MuxerJob = typeof muxerJobsTable.$inferSelect;
export type MuxerJobInsert = typeof muxerJobsTable.$inferInsert;
// UI State
export type UIState = typeof uiStateTable.$inferSelect;
export type UIStateInsert = typeof uiStateTable.$inferInsert;
// Retention Policies
export type DbRetentionPolicy = typeof retentionPoliciesTable.$inferSelect;
export type DbRetentionPolicyInsert =
typeof retentionPoliciesTable.$inferInsert;
// Chatbot Messages
export type ChatbotMessage = typeof chatbotMessagesTable.$inferSelect;
export type ChatbotMessageInsert =
typeof chatbotMessagesTable.$inferInsert;
@@ -3,7 +3,7 @@ import {
pgChannelCulturesTable,
pgUserProfilesTable,
pgUserReputationsTable,
} from "@bete/shared";
} from "../../../shared/index.js";
// Re-export shared tables
export {
@@ -2,7 +2,7 @@ import {
pgCorrectedModerationsTable,
pgStickerCacheTable,
pgTextAnalysisCacheTable,
} from "@bete/shared";
} from "../../../shared/index.js";
// Re-export shared tables
export {
@@ -2,7 +2,7 @@ import {
pgAttachmentsTable,
pgMessageReviewsTable,
pgMessagesTable,
} from "@bete/shared";
} from "../../../shared/index.js";
import {
bigint as pgBigint,
boolean as pgBoolean,
@@ -3,7 +3,7 @@ import {
pgMuxerJobsTable,
pgRetentionPoliciesTable,
pgUIStateTable,
} from "@bete/shared";
} from "../../../shared/index.js";
// Re-export shared tables
export {
@@ -1,4 +1,4 @@
import { pgVoiceRecordingsTable } from "@bete/shared";
import { pgVoiceRecordingsTable } from "../../../shared/index.js";
// Re-export shared table
export { pgVoiceRecordingsTable };
@@ -1,4 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import { createChildLogger } from "@/shared/logger/index";
import { desc, eq } from "drizzle-orm";
import { getDatabase } from "./drizzle.js";
import {
@@ -0,0 +1,48 @@
// Custom error classes for all services
export class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500,
public details?: Record<string, unknown>,
) {
super(message);
this.name = "AppError";
}
}
export class ValidationError extends AppError {
constructor(message: string, details?: Record<string, unknown>) {
super(message, "VALIDATION_ERROR", 400, details);
this.name = "ValidationError";
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id?: string) {
super(`${resource} not found${id ? `: ${id}` : ""}`, "NOT_FOUND", 404);
this.name = "NotFoundError";
}
}
export class UnauthorizedError extends AppError {
constructor(message = "Unauthorized") {
super(message, "UNAUTHORIZED", 401);
this.name = "UnauthorizedError";
}
}
export class DatabaseError extends AppError {
constructor(message: string, details?: Record<string, unknown>) {
super(message, "DATABASE_ERROR", 500, details);
this.name = "DatabaseError";
}
}
export class ConfigError extends AppError {
constructor(message: string) {
super(message, "CONFIG_ERROR", 500);
this.name = "ConfigError";
}
}
@@ -0,0 +1,9 @@
export * from "./config/index.js";
export * from "./database/init.js";
export * from "./database/pool.js";
export * from "./database/schema.js";
export * from "./errors/index.js";
export * from "./logger/index.js";
export * from "./moderation-types.js";
export * from "./redis-channels.js";
export * from "./utils/index.js";

Some files were not shown because too many files have changed in this diff Show More