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
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:
@@ -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,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,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
Reference in New Issue
Block a user