From dcd13482c20b6bbbd3962976ca522d65b61648e9 Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 30 Jul 2026 11:50:48 +0700 Subject: [PATCH] refactor: break monorepo into 3 standalone services (gateway, backend, frontend) - 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 --- .gitignore | 3 - .moon/tasks.yml | 34 - .moon/workspace.yml | 10 - biome.json | 2 - drizzle.config.ts | 18 - infra/docker/Dockerfile.backend | 27 +- infra/docker/Dockerfile.discord-gateway | 34 +- infra/docker/Dockerfile.proxy | 18 +- package.json | 29 +- packages/shared/moon.yml | 12 - packages/shared/package.json | 36 -- packages/shared/tsconfig.json | 19 - pnpm-workspace.yaml | 44 -- services/backend/moon.yml | 48 -- services/backend/package.json | 1 - services/backend/src/http/app.ts | 2 +- services/backend/src/http/server.ts | 2 +- services/backend/src/index.ts | 2 +- .../modules/analysis/analysis.repository.ts | 4 +- .../src/modules/analysis/analysis.routes.ts | 2 +- .../src/modules/analysis/analysis.service.ts | 2 +- .../src/modules/chatbot/chatbot.controller.ts | 2 +- .../src/modules/chatbot/chatbot.repository.ts | 4 +- .../src/modules/chatbot/chatbot.service.ts | 2 +- .../modules/dashboard/dashboard.repository.ts | 2 +- .../src/modules/dashboard/dashboard.routes.ts | 2 +- .../modules/dashboard/dashboard.service.ts | 2 +- .../src/modules/health/health.repository.ts | 2 +- .../backend/src/modules/media/media.routes.ts | 2 +- .../src/modules/media/media.service.ts | 2 +- .../modules/messages/messages.controller.ts | 2 +- .../modules/messages/messages.repository.ts | 6 +- .../src/modules/messages/messages.routes.ts | 2 +- .../src/modules/messages/messages.service.ts | 4 +- .../modules/recordings/recordings.routes.ts | 2 +- .../modules/recordings/recordings.service.ts | 4 +- .../src/modules/ui-state/ui-state.routes.ts | 2 +- .../src/modules/ui-state/ui-state.service.ts | 2 +- .../src/modules/voice/voice.controller.ts | 2 +- .../backend/src/modules/voice/voice.routes.ts | 2 +- .../src/modules/voice/voice.service.ts | 2 +- services/backend/src/shared/commandHelper.ts | 4 +- services/backend/src/shared/config/index.ts | 313 ++++++++- services/backend/src/shared/database/index.ts | 4 +- .../backend/src/shared}/database/init.ts | 2 +- .../backend/src/shared}/database/pool.ts | 0 .../backend/src/shared}/database/schema.ts | 0 .../backend/src/shared}/errors/index.ts | 0 .../backend/src/shared}/index.ts | 0 .../backend/src/shared}/logger/index.ts | 0 .../backend/src/shared/middlewares/index.ts | 4 +- .../backend/src/shared}/moderation-types.ts | 0 .../backend/src/shared}/redis-channels.ts | 0 services/backend/src/shared/redis/index.ts | 4 +- .../backend/src/shared}/utils/index.ts | 0 .../backend/src/shared}/utils/pagination.ts | 0 services/backend/src/ws/broadcast.ts | 2 +- services/backend/src/ws/redis-bridge.ts | 4 +- services/backend/src/ws/server.ts | 4 +- services/discord-gateway/moon.yml | 48 -- services/discord-gateway/package.json | 1 - services/discord-gateway/src/app/bootstrap.ts | 4 +- services/discord-gateway/src/app/retention.ts | 2 +- services/discord-gateway/src/app/shutdown.ts | 2 +- services/discord-gateway/src/index.ts | 2 +- .../ai-moderation/ai-analysis-worker.ts | 2 +- .../src/modules/ai-moderation/aiAnalyzer.ts | 2 +- .../ai-moderation/archive/aiAnalysisWorker.ts | 2 +- .../ai-moderation/autoDeleteEligibility.ts | 2 +- .../modules/ai-moderation/autoDeleteLogger.ts | 2 +- .../ai-moderation/autoDeleteManager.ts | 2 +- .../modules/ai-moderation/autoDeleteNotify.ts | 2 +- .../modules/ai-moderation/batchProcessor.ts | 2 +- .../modules/ai-moderation/batchScheduler.ts | 2 +- .../ai-moderation/channelCultureStore.ts | 2 +- .../ai-moderation/conversationContext.ts | 2 +- .../ai-moderation/conversationState.ts | 2 +- .../modules/ai-moderation/cultureLearner.ts | 2 +- .../individualFallbackProcessor.ts | 2 +- .../src/modules/ai-moderation/llmCaller.ts | 4 +- .../src/modules/ai-moderation/llmClient.ts | 4 +- .../ai-moderation/mediaBatchProcessor.ts | 2 +- .../modules/ai-moderation/mediaDownloader.ts | 4 +- .../ai-moderation/moderationOrchestrator.ts | 2 +- .../ai-moderation/moderationResponseParser.ts | 2 +- .../ai-moderation/moderationSchemas.ts | 2 +- .../modules/ai-moderation/moderationState.ts | 2 +- .../modules/ai-moderation/responseLogger.ts | 2 +- .../modules/ai-moderation/searxngSearch.ts | 4 +- .../modules/ai-moderation/simpleFallback.ts | 2 +- .../src/modules/ai-moderation/stickerCache.ts | 2 +- .../ai-moderation/textBatchProcessor.ts | 2 +- .../modules/ai-moderation/textCacheStore.ts | 2 +- .../src/modules/ai-moderation/urlFetcher.ts | 4 +- .../ai-moderation/userProfileLearner.ts | 2 +- .../modules/ai-moderation/userProfileStore.ts | 2 +- .../ai-moderation/userReputationStore.ts | 2 +- .../modules/ai-moderation/visionAnalyzer.ts | 4 +- .../attachment-upload/attachmentUploader.ts | 2 +- .../modules/attachment-upload/imageResizer.ts | 2 +- .../channel-topic/channelTopicCapture.ts | 2 +- .../modules/command-handler/commandHandler.ts | 4 +- .../modules/command-handler/guild.handler.ts | 4 +- .../command-handler/handler-registry.ts | 2 +- .../modules/command-handler/media.handler.ts | 4 +- .../command-handler/moderation.handler.ts | 4 +- .../modules/command-handler/voice.handler.ts | 4 +- .../event-broadcaster/eventBroadcaster.ts | 4 +- .../modules/event-broadcaster/eventTypes.ts | 2 +- .../src/modules/gateway-metrics/metrics.ts | 2 +- .../guild-member-events/memberEvents.ts | 2 +- .../modules/message-capture/attachmentsDb.ts | 2 +- .../modules/message-capture/messageCapture.ts | 2 +- .../modules/message-capture/messageStore.ts | 2 +- .../message-capture/messagesAnalysis.ts | 2 +- .../message-capture/messagesCleanup.ts | 2 +- .../modules/message-capture/messagesCrud.ts | 2 +- .../src/modules/message-capture/messagesDb.ts | 2 +- .../message-capture/messagesPagination.ts | 4 +- .../modules/message-capture/messagesSearch.ts | 2 +- .../message-capture/moderationActionsDb.ts | 4 +- .../modules/message-capture/retentionDb.ts | 2 +- .../src/modules/message-capture/reviewsDb.ts | 4 +- .../src/modules/message-capture/types.ts | 4 +- .../reaction-tracking/reactionCapture.ts | 2 +- .../modules/thread-tracking/threadCapture.ts | 2 +- .../modules/user-presence/presenceCapture.ts | 2 +- .../modules/voice-pcm-ws/voicePcmWsClient.ts | 2 +- .../modules/voice-recording/ffmpegProcess.ts | 2 +- .../modules/voice-recording/mediaSource.ts | 2 +- .../src/modules/voice-recording/muxer.ts | 2 +- .../modules/voice-recording/packetFilter.ts | 2 +- .../src/modules/voice-recording/player.ts | 2 +- .../src/modules/voice-recording/recorder.ts | 4 +- .../voice-recording/recorder/decoder.ts | 2 +- .../voice-recording/recorder/metadata.ts | 2 +- .../voice-recording/recorder/segment.ts | 2 +- .../recorder/segmentFinalizer.ts | 2 +- .../recorder/sessionRecording.ts | 2 +- .../recorder/speakingHandler.ts | 2 +- .../voice-recording/recorder/streamSetup.ts | 2 +- .../voice-recording/recorder/uploader.ts | 2 +- .../modules/voice-recording/transmitter.ts | 4 +- .../voice-recording/voiceController.ts | 4 +- .../voice-recording/voiceTranscriber.ts | 2 +- .../src/shared/config/config.ts | 4 +- .../src/shared}/config/index.ts | 0 .../src/shared/database/drizzle.ts | 4 +- .../src/shared/database/init.ts | 131 ++++ .../src/shared/database/migrate.ts | 2 +- .../src/shared/database/migrateCli.ts | 2 +- .../src/shared/database/pool.ts | 36 ++ .../src/shared/database/schema.ts | 607 +++++++++++++++++- .../src/shared/database/schema/analytics.ts | 2 +- .../src/shared/database/schema/cache.ts | 2 +- .../src/shared/database/schema/messages.ts | 2 +- .../src/shared/database/schema/meta.ts | 2 +- .../src/shared/database/schema/voice.ts | 2 +- .../src/shared/database/voiceRecordingRepo.ts | 2 +- .../src/shared/errors/index.ts | 48 ++ services/discord-gateway/src/shared/index.ts | 9 + .../src/shared/logger/index.ts | 31 + .../src/shared/moderation-types.ts | 227 +++++++ .../src/shared/redis-channels.ts | 128 ++++ .../discord-gateway/src/shared/uploader.ts | 4 +- .../discord-gateway/src/shared/utils/index.ts | 112 ++++ .../src/shared/utils/pagination.ts | 65 ++ 167 files changed, 1886 insertions(+), 533 deletions(-) delete mode 100644 .moon/tasks.yml delete mode 100644 .moon/workspace.yml delete mode 100644 drizzle.config.ts delete mode 100644 packages/shared/moon.yml delete mode 100644 packages/shared/package.json delete mode 100644 packages/shared/tsconfig.json delete mode 100644 pnpm-workspace.yaml delete mode 100644 services/backend/moon.yml rename {packages/shared/src => services/backend/src/shared}/database/init.ts (98%) rename {packages/shared/src => services/backend/src/shared}/database/pool.ts (100%) rename {packages/shared/src => services/backend/src/shared}/database/schema.ts (100%) rename {packages/shared/src => services/backend/src/shared}/errors/index.ts (100%) rename {packages/shared/src => services/backend/src/shared}/index.ts (100%) rename {packages/shared/src => services/backend/src/shared}/logger/index.ts (100%) rename {packages/shared/src => services/backend/src/shared}/moderation-types.ts (100%) rename {packages/shared/src => services/backend/src/shared}/redis-channels.ts (100%) rename {packages/shared/src => services/backend/src/shared}/utils/index.ts (100%) rename {packages/shared/src => services/backend/src/shared}/utils/pagination.ts (100%) delete mode 100644 services/discord-gateway/moon.yml rename {packages/shared/src => services/discord-gateway/src/shared}/config/index.ts (100%) create mode 100644 services/discord-gateway/src/shared/database/init.ts create mode 100644 services/discord-gateway/src/shared/database/pool.ts create mode 100644 services/discord-gateway/src/shared/errors/index.ts create mode 100644 services/discord-gateway/src/shared/index.ts create mode 100644 services/discord-gateway/src/shared/logger/index.ts create mode 100644 services/discord-gateway/src/shared/moderation-types.ts create mode 100644 services/discord-gateway/src/shared/redis-channels.ts create mode 100644 services/discord-gateway/src/shared/utils/index.ts create mode 100644 services/discord-gateway/src/shared/utils/pagination.ts diff --git a/.gitignore b/.gitignore index 1970838..5f437d5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,6 @@ public/app/ .env.test logs/ .codegraph/ -# moon -.moon/cache -.moon/docker worktrees/ .worktrees/ services/frontend/frontend/dist/ diff --git a/.moon/tasks.yml b/.moon/tasks.yml deleted file mode 100644 index 4afd2a1..0000000 --- a/.moon/tasks.yml +++ /dev/null @@ -1,34 +0,0 @@ -$schema: 'https://moonrepo.dev/schema/config/v2.json' - -# Global task templates — each project must explicitly inherit via workspace.inheritedTasks -# In moon v2, these define reusable task templates -tasks: - lint: - command: biome - args: - - check - - '--diagnostic-level=error' - - . - options: - runFromWorkspaceRoot: false - inputs: - - 'src/**/*' - - '*.js' - - '*.mjs' - - '*.ts' - - '*.mts' - - format: - command: biome - args: - - format - - '--write' - - . - options: - runFromWorkspaceRoot: false - inputs: - - 'src/**/*' - - '*.js' - - '*.mjs' - - '*.ts' - - '*.mts' diff --git a/.moon/workspace.yml b/.moon/workspace.yml deleted file mode 100644 index 62f3559..0000000 --- a/.moon/workspace.yml +++ /dev/null @@ -1,10 +0,0 @@ -$schema: 'https://moonrepo.dev/schema/config/v2.json' - -# Moon Workspace Configuration -projects: - - 'services/*' - - 'packages/*' - -vcs: - defaultBranch: main - provider: github diff --git a/biome.json b/biome.json index 8671327..e9d10fb 100644 --- a/biome.json +++ b/biome.json @@ -11,13 +11,11 @@ "tests/**/*.ts", "services/**/*.ts", "services/**/*.tsx", - "packages/**/*.ts", "*.json", "*.ts", "!vendor", "!.claude", "!services/**/dist", - "!packages/**/dist", "!services/**/.next", "!services/**/out" ] diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100644 index 5a33091..0000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -const databaseUrl = process.env.DATABASE_URL; - -export default defineConfig({ - schema: "./src/database/schema.ts", - out: "./drizzle/migrations", - dialect: "postgresql", - dbCredentials: databaseUrl - ? { url: databaseUrl } - : { - host: process.env.POSTGRES_HOST || "localhost", - port: parseInt(process.env.POSTGRES_PORT || "5432", 10), - user: process.env.POSTGRES_USER || "postgres", - password: process.env.POSTGRES_PASSWORD || "", - database: process.env.POSTGRES_DB || "moderation_bot", - }, -}); diff --git a/infra/docker/Dockerfile.backend b/infra/docker/Dockerfile.backend index dab37da..79aaf1e 100644 --- a/infra/docker/Dockerfile.backend +++ b/infra/docker/Dockerfile.backend @@ -2,12 +2,6 @@ FROM node:22-slim WORKDIR /app -# Build args for frontend API URLs -ARG VITE_BE_API_URL -ARG VITE_BE_WS_URL -ENV VITE_BE_API_URL=${VITE_BE_API_URL} -ENV VITE_BE_WS_URL=${VITE_BE_WS_URL} - # Install pnpm RUN npm install -g pnpm @@ -18,30 +12,21 @@ RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends python3 RUN groupadd --system app && useradd --system -g app app # Copy dependency definition files first for better caching -COPY pnpm-workspace.yaml . -COPY pnpm-lock.yaml . -COPY package.json . +COPY services/backend/package.json services/backend/pnpm-lock.yaml* ./services/backend/ # Copy patches (pnpm patchedDependencies) COPY patches ./patches -# Copy packages (workspace dependencies) -COPY packages/shared ./packages/shared +# Copy service source (shared code is embedded in src/shared/) +COPY services/backend/ ./services/backend/ -# Copy service -COPY services/backend ./services/backend - -# Install dependencies with build cache +# Install dependencies RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm install --frozen-lockfile - -# Build shared workspace dependency first -RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm --filter './packages/shared' run build + cd services/backend && pnpm install --frozen-lockfile # Build backend RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm --filter './services/backend' run build + cd services/backend && pnpm run build # Own dist + node_modules by non-root user RUN chown -R app:app /app diff --git a/infra/docker/Dockerfile.discord-gateway b/infra/docker/Dockerfile.discord-gateway index f94e0f5..1fe9bd1 100644 --- a/infra/docker/Dockerfile.discord-gateway +++ b/infra/docker/Dockerfile.discord-gateway @@ -13,40 +13,23 @@ RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ ENV PATH="/root/.cargo/bin:${PATH}" # Dependency manifests -COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY services/discord-gateway/package.json services/discord-gateway/pnpm-lock.yaml* ./services/discord-gateway/ COPY patches ./patches -COPY packages/shared/package.json ./packages/shared/package.json -COPY services/discord-gateway/package.json ./services/discord-gateway/package.json # Install ALL deps (including devDeps needed for build) RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm install --frozen-lockfile + cd services/discord-gateway && pnpm install --frozen-lockfile -# Source code -COPY packages/shared ./packages/shared +# Source code (shared code is embedded in src/shared/) COPY services/discord-gateway ./services/discord-gateway COPY services/discord-gateway/drizzle ./drizzle -# Build (combined to reduce layers) +# Build RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm --filter './packages/shared' run build \ - && pnpm --filter './services/discord-gateway' run build \ + cd services/discord-gateway && pnpm run build \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -# Create production-only deployment (no devDeps) -RUN pnpm deploy --legacy --filter '@bete/discord-gateway' /tmp/deploy \ - && rm -rf /tmp/deploy/node_modules/.pnpm/@biomejs+biome@* \ - /tmp/deploy/node_modules/.pnpm/@biomejs+cli-linux-x64@* \ - /tmp/deploy/node_modules/.pnpm/typescript@* \ - /tmp/deploy/node_modules/.pnpm/drizzle-kit@* \ - /tmp/deploy/node_modules/.pnpm/vitest@* \ - /tmp/deploy/node_modules/.pnpm/esbuild@* \ - /tmp/deploy/node_modules/.pnpm/tsx@* \ - /tmp/deploy/node_modules/.pnpm/@types* \ - /tmp/deploy/node_modules/.pnpm/@rolldown* \ - /tmp/deploy/node_modules/.pnpm/@vitest* - # ---- Stage 2: Runtime (minimal) ---- FROM node:22-slim @@ -57,11 +40,10 @@ RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ ffmpeg ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Copy production-only artifacts from deploy -COPY --from=builder /tmp/deploy/node_modules ./node_modules -COPY --from=builder /tmp/deploy/package.json ./package.json -COPY --from=builder /app/packages/shared/dist ./packages/shared/dist +# Copy built artifacts COPY --from=builder /app/services/discord-gateway/dist ./services/discord-gateway/dist +COPY --from=builder /app/services/discord-gateway/node_modules ./services/discord-gateway/node_modules +COPY --from=builder /app/services/discord-gateway/package.json ./services/discord-gateway/package.json COPY --from=builder /app/drizzle ./drizzle # Create recordings dir and set ownership diff --git a/infra/docker/Dockerfile.proxy b/infra/docker/Dockerfile.proxy index cd54413..2f1c2ed 100644 --- a/infra/docker/Dockerfile.proxy +++ b/infra/docker/Dockerfile.proxy @@ -11,20 +11,14 @@ RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ python3 make g++ && rm -rf /var/lib/apt/lists/* # Copy dependency manifests first for layer caching -COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ -COPY packages/shared/package.json ./packages/shared/package.json -COPY services/frontend/package.json ./services/frontend/package.json +COPY services/frontend/package.json services/frontend/pnpm-lock.yaml* ./services/frontend/ COPY services/frontend/tsconfig.json ./services/frontend/tsconfig.json -# Copy patches (pnpm patchedDependencies) -COPY patches/ ./patches/ - -# Install dependencies (frontend + shared) +# Install frontend dependencies (standalone, no shared workspace dep) RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm install --no-frozen-lockfile --filter './packages/shared' --filter './services/frontend' + cd services/frontend && pnpm install --no-frozen-lockfile # Copy source code -COPY packages/shared/ ./packages/shared/ COPY services/frontend/ ./services/frontend/ # Pass API/WS URLs as build args @@ -33,11 +27,9 @@ ARG VITE_BE_WS_URL ENV VITE_BE_API_URL=${VITE_BE_API_URL} ENV VITE_BE_WS_URL=${VITE_BE_WS_URL} -# Build shared lib first, then frontend static export +# Build frontend static export RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm --filter './packages/shared' run build -RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm --filter frontend run build + cd services/frontend && pnpm run build # ---- Stage 2: Nginx ---- FROM nginx:alpine diff --git a/package.json b/package.json index 0791db8..ecf3a15 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,24 @@ { - "name": "discord-voice-recorder", + "name": "bete-monorepo", "version": "1.0.0", - "description": "Discord bot that joins a voice channel and records audio", - "type": "module", - "packageManager": "pnpm@11.1.3", + "private": true, + "description": "Bete Discord Moderation Services — standalone services in one repo", "scripts": { - "dev:backend": "pnpm --filter './services/backend' run dev", - "dev:discord-gateway": "pnpm --filter './services/discord-gateway' run dev", - "dev:web": "pnpm --filter './services/frontend' run dev", - "build:web": "pnpm --filter './services/frontend' run build", - "typecheck:web": "pnpm --filter './services/frontend' run build", - "build:backend": "pnpm --filter './services/backend' run build", - "build:discord-gateway": "pnpm --filter './services/discord-gateway' run build", - "typecheck": "pnpm -r run typecheck", + "dev:backend": "pnpm --prefix services/backend dev", + "dev:discord-gateway": "pnpm --prefix services/discord-gateway dev", + "dev:web": "pnpm --prefix services/frontend dev", + "build:web": "pnpm --prefix services/frontend build", + "build:backend": "pnpm --prefix services/backend build", + "build:discord-gateway": "pnpm --prefix services/discord-gateway build", + "typecheck": "pnpm --prefix services/backend typecheck && pnpm --prefix services/discord-gateway typecheck", "lint": "biome check --diagnostic-level=error .", "format": "biome format --write .", - "test": "pnpm -r run test", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio", + "test": "pnpm --prefix services/backend test && pnpm --prefix services/discord-gateway test", "db:truncate": "psql \"$DATABASE_URL\" -f scripts/truncate-all.sql", "install:yt-dlp": "sh scripts/install-yt-dlp.sh" }, "devDependencies": { "@biomejs/biome": "^2.5.5", - "drizzle-kit": "^0.31.10", - "tsx": "^4.22.2", "typescript": "^5.9.3" } } diff --git a/packages/shared/moon.yml b/packages/shared/moon.yml deleted file mode 100644 index ec95f01..0000000 --- a/packages/shared/moon.yml +++ /dev/null @@ -1,12 +0,0 @@ -$schema: 'https://moonrepo.dev/schema/config/v2.json' - -language: typescript - -tasks: - typecheck: - command: tsc --noEmit - options: - runFromWorkspaceRoot: false - inputs: - - src - - tsconfig.json diff --git a/packages/shared/package.json b/packages/shared/package.json deleted file mode 100644 index 4731dd6..0000000 --- a/packages/shared/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@bete/shared", - "version": "1.0.0", - "description": "Shared utilities, types, and errors for Bete microservices", - "type": "module", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": "./dist/index.js", - "./config": "./dist/config/index.js", - "./database/pool": "./dist/database/pool.js", - "./database/schema": "./dist/database/schema.js", - "./database/init": "./dist/database/init.js", - "./errors": "./dist/errors/index.js", - "./logger": "./dist/logger/index.js", - "./moderation-types": "./dist/moderation-types.js", - "./redis-channels": "./dist/redis-channels.js", - "./utils": "./dist/utils/index.js" - }, - "scripts": { - "build": "tsc", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "drizzle-orm": "^0.45.2", - "pg": "^8.13.0", - "pino": "^9.0.0", - "zod": "^4.4.3" - }, - "devDependencies": { - "@types/node": "^25.9.0", - "@types/pg": "^8.11.0", - "pino-pretty": "^13.1.3", - "typescript": "^5.9.3" - } -} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json deleted file mode 100644 index 70cf646..0000000 --- a/packages/shared/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "lib": ["ES2020"], - "declaration": true, - "declarationMap": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "moduleResolution": "bundler" - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] -} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 7139c93..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,44 +0,0 @@ -packages: - - services/* - - packages/* - -allowBuilds: - '@discordjs/opus': true - '@lng2004/node-datachannel': true - esbuild: true - node-av: true - node-crc: true - sharp: true - zeromq: true - -minimumReleaseAgeExclude: - - '@types/node@25.9.0' - - pg-cloudflare@1.4.0 - - pg-connection-string@2.13.0 - - pg-pool@3.14.0 - - pg-protocol@1.14.0 - - pg@8.21.0 - - tsx@4.22.2 - - '@next/env@16.2.12' - - '@next/swc-darwin-arm64@16.2.12' - - '@next/swc-darwin-x64@16.2.12' - - '@next/swc-linux-arm64-gnu@16.2.12' - - '@next/swc-linux-arm64-musl@16.2.12' - - '@next/swc-linux-x64-gnu@16.2.12' - - '@next/swc-linux-x64-musl@16.2.12' - - '@next/swc-win32-arm64-msvc@16.2.12' - - '@next/swc-win32-x64-msvc@16.2.12' - - lucide-react@1.27.0 - - next@16.2.12 - - shadcn@4.15.0 - -onlyBuiltDependencies: - - '@discordjs/opus' - - '@lng2004/node-datachannel' - - esbuild - - node-av - - sharp - - zeromq - -patchedDependencies: - node-crc@4.0.0: patches/node-crc@4.0.0.patch diff --git a/services/backend/moon.yml b/services/backend/moon.yml deleted file mode 100644 index 4986705..0000000 --- a/services/backend/moon.yml +++ /dev/null @@ -1,48 +0,0 @@ -# Backend service - Discord moderation monitoring -language: typescript - -tasks: - dev: - command: tsx watch src/index.ts - preset: server - - build: - command: tsc - options: - runFromWorkspaceRoot: false - inputs: - - src - - tsconfig.json - outputs: - - dist - - typecheck: - command: tsc --noEmit - options: - runFromWorkspaceRoot: false - inputs: - - src - - tsconfig.json - - lint: - command: biome check --diagnostic-level=error . - options: - runFromWorkspaceRoot: false - inputs: - - src - - format: - command: biome format --write . - options: - runFromWorkspaceRoot: false - inputs: - - src - - test: - command: vitest run - options: - runFromWorkspaceRoot: false - inputs: - - src - - tests - - tsconfig.json diff --git a/services/backend/package.json b/services/backend/package.json index 96e1540..64654d4 100644 --- a/services/backend/package.json +++ b/services/backend/package.json @@ -14,7 +14,6 @@ "test": "vitest run" }, "dependencies": { - "@bete/shared": "workspace:*", "@discordjs/voice": "^0.19.2", "axios": "^1.16.1", "dotenv": "^17.4.2", diff --git a/services/backend/src/http/app.ts b/services/backend/src/http/app.ts index 73b1419..3f5a562 100644 --- a/services/backend/src/http/app.ts +++ b/services/backend/src/http/app.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import express, { type Express, type NextFunction, diff --git a/services/backend/src/http/server.ts b/services/backend/src/http/server.ts index 732e4ae..554bd5f 100644 --- a/services/backend/src/http/server.ts +++ b/services/backend/src/http/server.ts @@ -1,5 +1,5 @@ import { createServer, type Server } from "node:http"; -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import { config } from "../shared/config/index.js"; import { initializeDatabase } from "../shared/database/index.js"; import { startRedisBridge } from "../ws/redis-bridge.js"; diff --git a/services/backend/src/index.ts b/services/backend/src/index.ts index b3ed69c..d3f70f1 100644 --- a/services/backend/src/index.ts +++ b/services/backend/src/index.ts @@ -1,5 +1,5 @@ import type { Server } from "node:http"; -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import { startHttpServer } from "./http/server.js"; import { closeDatabase } from "./shared/database/index.js"; import { stopCommandBridge } from "./shared/redis/index.js"; diff --git a/services/backend/src/modules/analysis/analysis.repository.ts b/services/backend/src/modules/analysis/analysis.repository.ts index 5601176..432245e 100644 --- a/services/backend/src/modules/analysis/analysis.repository.ts +++ b/services/backend/src/modules/analysis/analysis.repository.ts @@ -1,5 +1,5 @@ -import { pgMessagesTable } from "@bete/shared"; -import { createChildLogger } from "@bete/shared/logger"; +import { pgMessagesTable } from "../../shared/index.js"; +import { createChildLogger } from "../../shared/logger/index.js"; import { and, desc, eq, ilike, type SQL } from "drizzle-orm"; import { getDatabase } from "../../shared/database/index.js"; import { diff --git a/services/backend/src/modules/analysis/analysis.routes.ts b/services/backend/src/modules/analysis/analysis.routes.ts index 92ac45e..3b4556f 100644 --- a/services/backend/src/modules/analysis/analysis.routes.ts +++ b/services/backend/src/modules/analysis/analysis.routes.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response, Router } from "express"; import express from "express"; import { asyncHandler } from "../../shared/middlewares/index.js"; diff --git a/services/backend/src/modules/analysis/analysis.service.ts b/services/backend/src/modules/analysis/analysis.service.ts index 97fca20..38235c6 100644 --- a/services/backend/src/modules/analysis/analysis.service.ts +++ b/services/backend/src/modules/analysis/analysis.service.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import { config } from "../../shared/config/index.js"; import type { AnalysisSearchQuery } from "./analysis.repository.js"; import { analysisRepository } from "./analysis.repository.js"; diff --git a/services/backend/src/modules/chatbot/chatbot.controller.ts b/services/backend/src/modules/chatbot/chatbot.controller.ts index aac08aa..6c3cf17 100644 --- a/services/backend/src/modules/chatbot/chatbot.controller.ts +++ b/services/backend/src/modules/chatbot/chatbot.controller.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response } from "express"; import { asyncHandler } from "../../shared/middlewares/index.js"; import { chatbotService } from "./chatbot.service.js"; diff --git a/services/backend/src/modules/chatbot/chatbot.repository.ts b/services/backend/src/modules/chatbot/chatbot.repository.ts index e2c3039..c5b9c2a 100644 --- a/services/backend/src/modules/chatbot/chatbot.repository.ts +++ b/services/backend/src/modules/chatbot/chatbot.repository.ts @@ -1,5 +1,5 @@ -import { pgChatbotMessagesTable, pgMessagesTable } from "@bete/shared"; -import { createChildLogger } from "@bete/shared/logger"; +import { pgChatbotMessagesTable, pgMessagesTable } from "../../shared/index.js"; +import { createChildLogger } from "../../shared/logger/index.js"; import { and, desc, eq, type SQL, sql } from "drizzle-orm"; import { getDatabase } from "../../shared/database/index.js"; diff --git a/services/backend/src/modules/chatbot/chatbot.service.ts b/services/backend/src/modules/chatbot/chatbot.service.ts index ab31015..c9263a3 100644 --- a/services/backend/src/modules/chatbot/chatbot.service.ts +++ b/services/backend/src/modules/chatbot/chatbot.service.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import { config } from "../../shared/config/index.js"; import type { ChatbotContext, diff --git a/services/backend/src/modules/dashboard/dashboard.repository.ts b/services/backend/src/modules/dashboard/dashboard.repository.ts index 2a07cee..49945be 100644 --- a/services/backend/src/modules/dashboard/dashboard.repository.ts +++ b/services/backend/src/modules/dashboard/dashboard.repository.ts @@ -4,7 +4,7 @@ import { pgUserProfilesTable, pgUserReputationsTable, pgVoiceRecordingsTable, -} from "@bete/shared"; +} from "../../shared/index.js"; import type { SQL } from "drizzle-orm"; import { sql } from "drizzle-orm"; import { getDatabase } from "../../shared/database/index.js"; diff --git a/services/backend/src/modules/dashboard/dashboard.routes.ts b/services/backend/src/modules/dashboard/dashboard.routes.ts index 12b6c59..a519e75 100644 --- a/services/backend/src/modules/dashboard/dashboard.routes.ts +++ b/services/backend/src/modules/dashboard/dashboard.routes.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response, Router } from "express"; import express from "express"; import { asyncHandler } from "../../shared/middlewares/index.js"; diff --git a/services/backend/src/modules/dashboard/dashboard.service.ts b/services/backend/src/modules/dashboard/dashboard.service.ts index 5987098..ea39a29 100644 --- a/services/backend/src/modules/dashboard/dashboard.service.ts +++ b/services/backend/src/modules/dashboard/dashboard.service.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import { dashboardRepository } from "./dashboard.repository.js"; const logger = createChildLogger("dashboard.service"); diff --git a/services/backend/src/modules/health/health.repository.ts b/services/backend/src/modules/health/health.repository.ts index 7a0418c..7f2819f 100644 --- a/services/backend/src/modules/health/health.repository.ts +++ b/services/backend/src/modules/health/health.repository.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import { sql } from "drizzle-orm"; import { getDatabase } from "../../shared/database/index.js"; diff --git a/services/backend/src/modules/media/media.routes.ts b/services/backend/src/modules/media/media.routes.ts index 6cf1d13..0a68458 100644 --- a/services/backend/src/modules/media/media.routes.ts +++ b/services/backend/src/modules/media/media.routes.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response, Router } from "express"; import express from "express"; import { asyncHandler, validateBody } from "../../shared/middlewares/index.js"; diff --git a/services/backend/src/modules/media/media.service.ts b/services/backend/src/modules/media/media.service.ts index 0049de5..034c228 100644 --- a/services/backend/src/modules/media/media.service.ts +++ b/services/backend/src/modules/media/media.service.ts @@ -4,7 +4,7 @@ import { COMMAND_MEDIA_STOP, COMMAND_MEDIA_VOLUME, MEDIA_STATUS_KEY, -} from "@bete/shared"; +} from "../../shared/index.js"; import { createChildLogger, tryCommandThenFallback, diff --git a/services/backend/src/modules/messages/messages.controller.ts b/services/backend/src/modules/messages/messages.controller.ts index 94c77fd..9638555 100644 --- a/services/backend/src/modules/messages/messages.controller.ts +++ b/services/backend/src/modules/messages/messages.controller.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response } from "express"; import { asyncHandler } from "../../shared/middlewares/index.js"; import { messageQuerySchema } from "./messages.schema.js"; diff --git a/services/backend/src/modules/messages/messages.repository.ts b/services/backend/src/modules/messages/messages.repository.ts index 7c327f4..a080d0d 100644 --- a/services/backend/src/modules/messages/messages.repository.ts +++ b/services/backend/src/modules/messages/messages.repository.ts @@ -1,6 +1,6 @@ -import type { PageResult } from "@bete/shared"; -import { pgAttachmentsTable, pgMessagesTable } from "@bete/shared"; -import { createChildLogger } from "@bete/shared/logger"; +import type { PageResult } from "../../shared/index.js"; +import { pgAttachmentsTable, pgMessagesTable } from "../../shared/index.js"; +import { createChildLogger } from "../../shared/logger/index.js"; import { and, desc, diff --git a/services/backend/src/modules/messages/messages.routes.ts b/services/backend/src/modules/messages/messages.routes.ts index 93ced0d..a14926f 100644 --- a/services/backend/src/modules/messages/messages.routes.ts +++ b/services/backend/src/modules/messages/messages.routes.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response, Router } from "express"; import express from "express"; import { asyncHandler, validateBody } from "../../shared/middlewares/index.js"; diff --git a/services/backend/src/modules/messages/messages.service.ts b/services/backend/src/modules/messages/messages.service.ts index a7dc56f..5a6496d 100644 --- a/services/backend/src/modules/messages/messages.service.ts +++ b/services/backend/src/modules/messages/messages.service.ts @@ -1,5 +1,5 @@ -import { NotFoundError, ValidationError } from "@bete/shared/errors"; -import { createChildLogger } from "@bete/shared/logger"; +import { NotFoundError, ValidationError } from "@/shared/errors/index"; +import { createChildLogger } from "@/shared/logger/index"; import { messagesRepository } from "./messages.repository.js"; import type { MessageQuery } from "./messages.schema.js"; diff --git a/services/backend/src/modules/recordings/recordings.routes.ts b/services/backend/src/modules/recordings/recordings.routes.ts index b30efc9..32c636a 100644 --- a/services/backend/src/modules/recordings/recordings.routes.ts +++ b/services/backend/src/modules/recordings/recordings.routes.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response, Router } from "express"; import express from "express"; import { asyncHandler } from "../../shared/middlewares/index.js"; diff --git a/services/backend/src/modules/recordings/recordings.service.ts b/services/backend/src/modules/recordings/recordings.service.ts index 1e41602..4bb818f 100644 --- a/services/backend/src/modules/recordings/recordings.service.ts +++ b/services/backend/src/modules/recordings/recordings.service.ts @@ -1,5 +1,5 @@ -import { pgVoiceRecordingsTable } from "@bete/shared"; -import { createChildLogger } from "@bete/shared/logger"; +import { pgVoiceRecordingsTable } from "../../shared/index.js"; +import { createChildLogger } from "../../shared/logger/index.js"; import { and, desc, eq, lt, type SQL } from "drizzle-orm"; import { getDatabase } from "../../shared/database/index.js"; diff --git a/services/backend/src/modules/ui-state/ui-state.routes.ts b/services/backend/src/modules/ui-state/ui-state.routes.ts index 389e80d..d25bb66 100644 --- a/services/backend/src/modules/ui-state/ui-state.routes.ts +++ b/services/backend/src/modules/ui-state/ui-state.routes.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response, Router } from "express"; import express from "express"; import { asyncHandler } from "../../shared/middlewares/index.js"; diff --git a/services/backend/src/modules/ui-state/ui-state.service.ts b/services/backend/src/modules/ui-state/ui-state.service.ts index bb75508..8632923 100644 --- a/services/backend/src/modules/ui-state/ui-state.service.ts +++ b/services/backend/src/modules/ui-state/ui-state.service.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import { sql } from "drizzle-orm"; import { getDatabase } from "../../shared/database/index.js"; diff --git a/services/backend/src/modules/voice/voice.controller.ts b/services/backend/src/modules/voice/voice.controller.ts index 6a8dee1..a827a9d 100644 --- a/services/backend/src/modules/voice/voice.controller.ts +++ b/services/backend/src/modules/voice/voice.controller.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response } from "express"; import { asyncHandler } from "../../shared/middlewares/index.js"; import { publishCommandNoReply } from "../../shared/redis/index.js"; diff --git a/services/backend/src/modules/voice/voice.routes.ts b/services/backend/src/modules/voice/voice.routes.ts index 3ca54ab..1ed9412 100644 --- a/services/backend/src/modules/voice/voice.routes.ts +++ b/services/backend/src/modules/voice/voice.routes.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Request, Response, Router } from "express"; import express from "express"; import { asyncHandler, validateBody } from "../../shared/middlewares/index.js"; diff --git a/services/backend/src/modules/voice/voice.service.ts b/services/backend/src/modules/voice/voice.service.ts index e43f1e6..0dec772 100644 --- a/services/backend/src/modules/voice/voice.service.ts +++ b/services/backend/src/modules/voice/voice.service.ts @@ -7,7 +7,7 @@ import { type CommandReply, pgMessagesTable, VOICE_STATUS_KEY, -} from "@bete/shared"; +} from "../../shared/index.js"; import { eq } from "drizzle-orm"; import { createChildLogger, diff --git a/services/backend/src/shared/commandHelper.ts b/services/backend/src/shared/commandHelper.ts index 09e3b3a..79d091b 100644 --- a/services/backend/src/shared/commandHelper.ts +++ b/services/backend/src/shared/commandHelper.ts @@ -1,5 +1,5 @@ -import type { CommandReply } from "@bete/shared"; -import { createChildLogger } from "@bete/shared/logger"; +import type { CommandReply } from "./index.js"; +import { createChildLogger } from "./logger/index.js"; export { createChildLogger }; diff --git a/services/backend/src/shared/config/index.ts b/services/backend/src/shared/config/index.ts index 911c463..e1e6f02 100644 --- a/services/backend/src/shared/config/index.ts +++ b/services/backend/src/shared/config/index.ts @@ -1,4 +1,311 @@ -import "dotenv/config"; -import { config as sharedConfig } from "@bete/shared/config"; +/** + * 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. + */ -export const config = sharedConfig; +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 & { + 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(); diff --git a/services/backend/src/shared/database/index.ts b/services/backend/src/shared/database/index.ts index 9916c31..9439b0b 100644 --- a/services/backend/src/shared/database/index.ts +++ b/services/backend/src/shared/database/index.ts @@ -3,8 +3,8 @@ import { getDatabase as sharedGetDb, getPool as sharedGetPool, initializeDatabase as sharedInit, -} from "@bete/shared/database/init"; -import { createChildLogger } from "@bete/shared/logger"; +} from "@/shared/database/init"; +import { createChildLogger } from "@/shared/logger/index"; import { config } from "../config/index.js"; const logger = createChildLogger("database"); diff --git a/packages/shared/src/database/init.ts b/services/backend/src/shared/database/init.ts similarity index 98% rename from packages/shared/src/database/init.ts rename to services/backend/src/shared/database/init.ts index eb02c78..ca91bbb 100644 --- a/packages/shared/src/database/init.ts +++ b/services/backend/src/shared/database/init.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +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"; diff --git a/packages/shared/src/database/pool.ts b/services/backend/src/shared/database/pool.ts similarity index 100% rename from packages/shared/src/database/pool.ts rename to services/backend/src/shared/database/pool.ts diff --git a/packages/shared/src/database/schema.ts b/services/backend/src/shared/database/schema.ts similarity index 100% rename from packages/shared/src/database/schema.ts rename to services/backend/src/shared/database/schema.ts diff --git a/packages/shared/src/errors/index.ts b/services/backend/src/shared/errors/index.ts similarity index 100% rename from packages/shared/src/errors/index.ts rename to services/backend/src/shared/errors/index.ts diff --git a/packages/shared/src/index.ts b/services/backend/src/shared/index.ts similarity index 100% rename from packages/shared/src/index.ts rename to services/backend/src/shared/index.ts diff --git a/packages/shared/src/logger/index.ts b/services/backend/src/shared/logger/index.ts similarity index 100% rename from packages/shared/src/logger/index.ts rename to services/backend/src/shared/logger/index.ts diff --git a/services/backend/src/shared/middlewares/index.ts b/services/backend/src/shared/middlewares/index.ts index 2bfabe9..d2eafae 100644 --- a/services/backend/src/shared/middlewares/index.ts +++ b/services/backend/src/shared/middlewares/index.ts @@ -1,5 +1,5 @@ -import { AppError, ValidationError } from "@bete/shared/errors"; -import { createChildLogger } from "@bete/shared/logger"; +import { AppError, ValidationError } from "@/shared/errors/index"; +import { createChildLogger } from "@/shared/logger/index"; import type { NextFunction, Request, Response } from "express"; import type { ZodSchema } from "zod"; diff --git a/packages/shared/src/moderation-types.ts b/services/backend/src/shared/moderation-types.ts similarity index 100% rename from packages/shared/src/moderation-types.ts rename to services/backend/src/shared/moderation-types.ts diff --git a/packages/shared/src/redis-channels.ts b/services/backend/src/shared/redis-channels.ts similarity index 100% rename from packages/shared/src/redis-channels.ts rename to services/backend/src/shared/redis-channels.ts diff --git a/services/backend/src/shared/redis/index.ts b/services/backend/src/shared/redis/index.ts index 341d68c..3866f07 100644 --- a/services/backend/src/shared/redis/index.ts +++ b/services/backend/src/shared/redis/index.ts @@ -4,8 +4,8 @@ import { BACKEND_COMMAND_REPLY_PREFIX, type CommandMessage, type CommandReply, -} from "@bete/shared"; -import { createChildLogger } from "@bete/shared/logger"; +} from "../index.js"; +import { createChildLogger } from "../logger/index.js"; import Redis from "ioredis"; import { config } from "../config/index.js"; diff --git a/packages/shared/src/utils/index.ts b/services/backend/src/shared/utils/index.ts similarity index 100% rename from packages/shared/src/utils/index.ts rename to services/backend/src/shared/utils/index.ts diff --git a/packages/shared/src/utils/pagination.ts b/services/backend/src/shared/utils/pagination.ts similarity index 100% rename from packages/shared/src/utils/pagination.ts rename to services/backend/src/shared/utils/pagination.ts diff --git a/services/backend/src/ws/broadcast.ts b/services/backend/src/ws/broadcast.ts index 6f0a6fc..687828c 100644 --- a/services/backend/src/ws/broadcast.ts +++ b/services/backend/src/ws/broadcast.ts @@ -9,7 +9,7 @@ * broadcastEvent("message_created", messageData); */ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; const logger = createChildLogger("broadcast"); diff --git a/services/backend/src/ws/redis-bridge.ts b/services/backend/src/ws/redis-bridge.ts index 8b5ab07..260f567 100644 --- a/services/backend/src/ws/redis-bridge.ts +++ b/services/backend/src/ws/redis-bridge.ts @@ -1,5 +1,5 @@ -import { DISCORD_CHANNEL_TO_WS_EVENT, DISCORD_VOICE_PCM } from "@bete/shared"; -import { createChildLogger } from "@bete/shared/logger"; +import { DISCORD_CHANNEL_TO_WS_EVENT, DISCORD_VOICE_PCM } from "../shared/index.js"; +import { createChildLogger } from "../shared/logger/index.js"; import Redis from "ioredis"; import { config } from "../shared/config/index.js"; import { broadcastBinary, broadcastEvent } from "./broadcast.js"; diff --git a/services/backend/src/ws/server.ts b/services/backend/src/ws/server.ts index 45f2aa8..873ec06 100644 --- a/services/backend/src/ws/server.ts +++ b/services/backend/src/ws/server.ts @@ -1,6 +1,6 @@ import type { Server } from "node:http"; -import { BACKEND_COMMAND, BACKEND_VOICE_TRANSMIT } from "@bete/shared"; -import { createChildLogger } from "@bete/shared/logger"; +import { BACKEND_COMMAND, BACKEND_VOICE_TRANSMIT } from "../shared/index.js"; +import { createChildLogger } from "../shared/logger/index.js"; import { WebSocket, WebSocketServer } from "ws"; import { config } from "../shared/config/index.js"; import { setBroadcastFunctions } from "./broadcast.js"; diff --git a/services/discord-gateway/moon.yml b/services/discord-gateway/moon.yml deleted file mode 100644 index 20fed03..0000000 --- a/services/discord-gateway/moon.yml +++ /dev/null @@ -1,48 +0,0 @@ -# Discord Gateway service - handles message capture, voice recording, and AI moderation -language: typescript - -tasks: - dev: - command: tsx watch src/index.ts - preset: server - - build: - command: tsc - options: - runFromWorkspaceRoot: false - inputs: - - src - - tsconfig.json - outputs: - - dist - - typecheck: - command: tsc --noEmit - options: - runFromWorkspaceRoot: false - inputs: - - src - - tsconfig.json - - lint: - command: biome check --diagnostic-level=error . - options: - runFromWorkspaceRoot: false - inputs: - - src - - format: - command: biome format --write . - options: - runFromWorkspaceRoot: false - inputs: - - src - - test: - command: vitest run - options: - runFromWorkspaceRoot: false - inputs: - - src - - tests - - tsconfig.json diff --git a/services/discord-gateway/package.json b/services/discord-gateway/package.json index e04483e..f913f5a 100644 --- a/services/discord-gateway/package.json +++ b/services/discord-gateway/package.json @@ -15,7 +15,6 @@ "test": "vitest run" }, "dependencies": { - "@bete/shared": "workspace:*", "@discordjs/opus": "^0.10.0", "@discordjs/voice": "^0.19.2", "@snazzah/davey": "^0.1.11", diff --git a/services/discord-gateway/src/app/bootstrap.ts b/services/discord-gateway/src/app/bootstrap.ts index c779c0f..065c15b 100644 --- a/services/discord-gateway/src/app/bootstrap.ts +++ b/services/discord-gateway/src/app/bootstrap.ts @@ -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"; diff --git a/services/discord-gateway/src/app/retention.ts b/services/discord-gateway/src/app/retention.ts index 4736874..96f8cc1 100644 --- a/services/discord-gateway/src/app/retention.ts +++ b/services/discord-gateway/src/app/retention.ts @@ -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"; diff --git a/services/discord-gateway/src/app/shutdown.ts b/services/discord-gateway/src/app/shutdown.ts index ddb1e25..ea5e0ec 100644 --- a/services/discord-gateway/src/app/shutdown.ts +++ b/services/discord-gateway/src/app/shutdown.ts @@ -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"; diff --git a/services/discord-gateway/src/index.ts b/services/discord-gateway/src/index.ts index b34bd06..6d5737d 100644 --- a/services/discord-gateway/src/index.ts +++ b/services/discord-gateway/src/index.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/ai-moderation/ai-analysis-worker.ts b/services/discord-gateway/src/modules/ai-moderation/ai-analysis-worker.ts index f12d3b8..0f3c221 100644 --- a/services/discord-gateway/src/modules/ai-moderation/ai-analysis-worker.ts +++ b/services/discord-gateway/src/modules/ai-moderation/ai-analysis-worker.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/aiAnalyzer.ts b/services/discord-gateway/src/modules/ai-moderation/aiAnalyzer.ts index 05c157d..93fd969 100644 --- a/services/discord-gateway/src/modules/ai-moderation/aiAnalyzer.ts +++ b/services/discord-gateway/src/modules/ai-moderation/aiAnalyzer.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/archive/aiAnalysisWorker.ts b/services/discord-gateway/src/modules/ai-moderation/archive/aiAnalysisWorker.ts index 4bf6cb3..0fe7f4d 100644 --- a/services/discord-gateway/src/modules/ai-moderation/archive/aiAnalysisWorker.ts +++ b/services/discord-gateway/src/modules/ai-moderation/archive/aiAnalysisWorker.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/autoDeleteEligibility.ts b/services/discord-gateway/src/modules/ai-moderation/autoDeleteEligibility.ts index f669d32..5e048d9 100644 --- a/services/discord-gateway/src/modules/ai-moderation/autoDeleteEligibility.ts +++ b/services/discord-gateway/src/modules/ai-moderation/autoDeleteEligibility.ts @@ -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, diff --git a/services/discord-gateway/src/modules/ai-moderation/autoDeleteLogger.ts b/services/discord-gateway/src/modules/ai-moderation/autoDeleteLogger.ts index 53762b2..d56f379 100644 --- a/services/discord-gateway/src/modules/ai-moderation/autoDeleteLogger.ts +++ b/services/discord-gateway/src/modules/ai-moderation/autoDeleteLogger.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/autoDeleteManager.ts b/services/discord-gateway/src/modules/ai-moderation/autoDeleteManager.ts index 6563f24..26354ad 100644 --- a/services/discord-gateway/src/modules/ai-moderation/autoDeleteManager.ts +++ b/services/discord-gateway/src/modules/ai-moderation/autoDeleteManager.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/autoDeleteNotify.ts b/services/discord-gateway/src/modules/ai-moderation/autoDeleteNotify.ts index a77c373..fff8ec9 100644 --- a/services/discord-gateway/src/modules/ai-moderation/autoDeleteNotify.ts +++ b/services/discord-gateway/src/modules/ai-moderation/autoDeleteNotify.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/batchProcessor.ts b/services/discord-gateway/src/modules/ai-moderation/batchProcessor.ts index f5d787d..f27327c 100644 --- a/services/discord-gateway/src/modules/ai-moderation/batchProcessor.ts +++ b/services/discord-gateway/src/modules/ai-moderation/batchProcessor.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/batchScheduler.ts b/services/discord-gateway/src/modules/ai-moderation/batchScheduler.ts index 567394a..51f9978 100644 --- a/services/discord-gateway/src/modules/ai-moderation/batchScheduler.ts +++ b/services/discord-gateway/src/modules/ai-moderation/batchScheduler.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/channelCultureStore.ts b/services/discord-gateway/src/modules/ai-moderation/channelCultureStore.ts index 593c460..b8a226d 100644 --- a/services/discord-gateway/src/modules/ai-moderation/channelCultureStore.ts +++ b/services/discord-gateway/src/modules/ai-moderation/channelCultureStore.ts @@ -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 { diff --git a/services/discord-gateway/src/modules/ai-moderation/conversationContext.ts b/services/discord-gateway/src/modules/ai-moderation/conversationContext.ts index a7c0bf4..93e45ca 100644 --- a/services/discord-gateway/src/modules/ai-moderation/conversationContext.ts +++ b/services/discord-gateway/src/modules/ai-moderation/conversationContext.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/conversationState.ts b/services/discord-gateway/src/modules/ai-moderation/conversationState.ts index 0836330..8680b7b 100644 --- a/services/discord-gateway/src/modules/ai-moderation/conversationState.ts +++ b/services/discord-gateway/src/modules/ai-moderation/conversationState.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/cultureLearner.ts b/services/discord-gateway/src/modules/ai-moderation/cultureLearner.ts index 5e18b2f..e71d537 100644 --- a/services/discord-gateway/src/modules/ai-moderation/cultureLearner.ts +++ b/services/discord-gateway/src/modules/ai-moderation/cultureLearner.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/individualFallbackProcessor.ts b/services/discord-gateway/src/modules/ai-moderation/individualFallbackProcessor.ts index 756a909..aa271e4 100644 --- a/services/discord-gateway/src/modules/ai-moderation/individualFallbackProcessor.ts +++ b/services/discord-gateway/src/modules/ai-moderation/individualFallbackProcessor.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/llmCaller.ts b/services/discord-gateway/src/modules/ai-moderation/llmCaller.ts index ea492aa..719a1c1 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmCaller.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmCaller.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts index 1011922..c7af90e 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/mediaBatchProcessor.ts b/services/discord-gateway/src/modules/ai-moderation/mediaBatchProcessor.ts index 449af87..1a57005 100644 --- a/services/discord-gateway/src/modules/ai-moderation/mediaBatchProcessor.ts +++ b/services/discord-gateway/src/modules/ai-moderation/mediaBatchProcessor.ts @@ -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, diff --git a/services/discord-gateway/src/modules/ai-moderation/mediaDownloader.ts b/services/discord-gateway/src/modules/ai-moderation/mediaDownloader.ts index 8489861..049ef3d 100644 --- a/services/discord-gateway/src/modules/ai-moderation/mediaDownloader.ts +++ b/services/discord-gateway/src/modules/ai-moderation/mediaDownloader.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/moderationOrchestrator.ts b/services/discord-gateway/src/modules/ai-moderation/moderationOrchestrator.ts index 8f74c30..3930bce 100644 --- a/services/discord-gateway/src/modules/ai-moderation/moderationOrchestrator.ts +++ b/services/discord-gateway/src/modules/ai-moderation/moderationOrchestrator.ts @@ -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 { diff --git a/services/discord-gateway/src/modules/ai-moderation/moderationResponseParser.ts b/services/discord-gateway/src/modules/ai-moderation/moderationResponseParser.ts index 3032631..6504b8a 100644 --- a/services/discord-gateway/src/modules/ai-moderation/moderationResponseParser.ts +++ b/services/discord-gateway/src/modules/ai-moderation/moderationResponseParser.ts @@ -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 { diff --git a/services/discord-gateway/src/modules/ai-moderation/moderationSchemas.ts b/services/discord-gateway/src/modules/ai-moderation/moderationSchemas.ts index a7b4bf4..c4ac4fd 100644 --- a/services/discord-gateway/src/modules/ai-moderation/moderationSchemas.ts +++ b/services/discord-gateway/src/modules/ai-moderation/moderationSchemas.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import { z } from "zod"; const log = createChildLogger("moderationSchemas"); diff --git a/services/discord-gateway/src/modules/ai-moderation/moderationState.ts b/services/discord-gateway/src/modules/ai-moderation/moderationState.ts index b798411..129cbc4 100644 --- a/services/discord-gateway/src/modules/ai-moderation/moderationState.ts +++ b/services/discord-gateway/src/modules/ai-moderation/moderationState.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/responseLogger.ts b/services/discord-gateway/src/modules/ai-moderation/responseLogger.ts index ab36afa..986573a 100644 --- a/services/discord-gateway/src/modules/ai-moderation/responseLogger.ts +++ b/services/discord-gateway/src/modules/ai-moderation/responseLogger.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/ai-moderation/searxngSearch.ts b/services/discord-gateway/src/modules/ai-moderation/searxngSearch.ts index c519b56..71c261d 100644 --- a/services/discord-gateway/src/modules/ai-moderation/searxngSearch.ts +++ b/services/discord-gateway/src/modules/ai-moderation/searxngSearch.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/ai-moderation/simpleFallback.ts b/services/discord-gateway/src/modules/ai-moderation/simpleFallback.ts index a10a2f6..af2ace2 100644 --- a/services/discord-gateway/src/modules/ai-moderation/simpleFallback.ts +++ b/services/discord-gateway/src/modules/ai-moderation/simpleFallback.ts @@ -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, diff --git a/services/discord-gateway/src/modules/ai-moderation/stickerCache.ts b/services/discord-gateway/src/modules/ai-moderation/stickerCache.ts index eec87f9..487d2eb 100644 --- a/services/discord-gateway/src/modules/ai-moderation/stickerCache.ts +++ b/services/discord-gateway/src/modules/ai-moderation/stickerCache.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts b/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts index 9890f35..45d227a 100644 --- a/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts +++ b/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts @@ -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, diff --git a/services/discord-gateway/src/modules/ai-moderation/textCacheStore.ts b/services/discord-gateway/src/modules/ai-moderation/textCacheStore.ts index 4149970..e027d1e 100644 --- a/services/discord-gateway/src/modules/ai-moderation/textCacheStore.ts +++ b/services/discord-gateway/src/modules/ai-moderation/textCacheStore.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/ai-moderation/urlFetcher.ts b/services/discord-gateway/src/modules/ai-moderation/urlFetcher.ts index fb4ec7e..d71fd14 100644 --- a/services/discord-gateway/src/modules/ai-moderation/urlFetcher.ts +++ b/services/discord-gateway/src/modules/ai-moderation/urlFetcher.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/ai-moderation/userProfileLearner.ts b/services/discord-gateway/src/modules/ai-moderation/userProfileLearner.ts index 0a76321..18e4400 100644 --- a/services/discord-gateway/src/modules/ai-moderation/userProfileLearner.ts +++ b/services/discord-gateway/src/modules/ai-moderation/userProfileLearner.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/ai-moderation/userProfileStore.ts b/services/discord-gateway/src/modules/ai-moderation/userProfileStore.ts index b2654e7..77582df 100644 --- a/services/discord-gateway/src/modules/ai-moderation/userProfileStore.ts +++ b/services/discord-gateway/src/modules/ai-moderation/userProfileStore.ts @@ -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 { diff --git a/services/discord-gateway/src/modules/ai-moderation/userReputationStore.ts b/services/discord-gateway/src/modules/ai-moderation/userReputationStore.ts index c1a702a..89263ae 100644 --- a/services/discord-gateway/src/modules/ai-moderation/userReputationStore.ts +++ b/services/discord-gateway/src/modules/ai-moderation/userReputationStore.ts @@ -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 { diff --git a/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts b/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts index 8462517..f02f757 100644 --- a/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts +++ b/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts @@ -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 { diff --git a/services/discord-gateway/src/modules/attachment-upload/attachmentUploader.ts b/services/discord-gateway/src/modules/attachment-upload/attachmentUploader.ts index 47b3fd5..33aa17e 100644 --- a/services/discord-gateway/src/modules/attachment-upload/attachmentUploader.ts +++ b/services/discord-gateway/src/modules/attachment-upload/attachmentUploader.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/attachment-upload/imageResizer.ts b/services/discord-gateway/src/modules/attachment-upload/imageResizer.ts index b6ca203..c015ec8 100644 --- a/services/discord-gateway/src/modules/attachment-upload/imageResizer.ts +++ b/services/discord-gateway/src/modules/attachment-upload/imageResizer.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import sharp from "sharp"; const log = createChildLogger("imageResizer"); diff --git a/services/discord-gateway/src/modules/channel-topic/channelTopicCapture.ts b/services/discord-gateway/src/modules/channel-topic/channelTopicCapture.ts index d5746ea..f6daad6 100644 --- a/services/discord-gateway/src/modules/channel-topic/channelTopicCapture.ts +++ b/services/discord-gateway/src/modules/channel-topic/channelTopicCapture.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/command-handler/commandHandler.ts b/services/discord-gateway/src/modules/command-handler/commandHandler.ts index 46273ed..7b46637 100644 --- a/services/discord-gateway/src/modules/command-handler/commandHandler.ts +++ b/services/discord-gateway/src/modules/command-handler/commandHandler.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/command-handler/guild.handler.ts b/services/discord-gateway/src/modules/command-handler/guild.handler.ts index 37af046..740282b 100644 --- a/services/discord-gateway/src/modules/command-handler/guild.handler.ts +++ b/services/discord-gateway/src/modules/command-handler/guild.handler.ts @@ -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"; // --------------------------------------------------------------------------- diff --git a/services/discord-gateway/src/modules/command-handler/handler-registry.ts b/services/discord-gateway/src/modules/command-handler/handler-registry.ts index 52fc1e0..c4c51bf 100644 --- a/services/discord-gateway/src/modules/command-handler/handler-registry.ts +++ b/services/discord-gateway/src/modules/command-handler/handler-registry.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/command-handler/media.handler.ts b/services/discord-gateway/src/modules/command-handler/media.handler.ts index 4a526b8..d6ecdf8 100644 --- a/services/discord-gateway/src/modules/command-handler/media.handler.ts +++ b/services/discord-gateway/src/modules/command-handler/media.handler.ts @@ -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, diff --git a/services/discord-gateway/src/modules/command-handler/moderation.handler.ts b/services/discord-gateway/src/modules/command-handler/moderation.handler.ts index 2302545..f41a881 100644 --- a/services/discord-gateway/src/modules/command-handler/moderation.handler.ts +++ b/services/discord-gateway/src/modules/command-handler/moderation.handler.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/command-handler/voice.handler.ts b/services/discord-gateway/src/modules/command-handler/voice.handler.ts index f7fa7fe..e6d7c24 100644 --- a/services/discord-gateway/src/modules/command-handler/voice.handler.ts +++ b/services/discord-gateway/src/modules/command-handler/voice.handler.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/event-broadcaster/eventBroadcaster.ts b/services/discord-gateway/src/modules/event-broadcaster/eventBroadcaster.ts index 93cea2d..27ac261 100644 --- a/services/discord-gateway/src/modules/event-broadcaster/eventBroadcaster.ts +++ b/services/discord-gateway/src/modules/event-broadcaster/eventBroadcaster.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/event-broadcaster/eventTypes.ts b/services/discord-gateway/src/modules/event-broadcaster/eventTypes.ts index adbeb4f..0d0afd6 100644 --- a/services/discord-gateway/src/modules/event-broadcaster/eventTypes.ts +++ b/services/discord-gateway/src/modules/event-broadcaster/eventTypes.ts @@ -22,7 +22,7 @@ import { DISCORD_VOICE_STOPPED, DISCORD_VOICE_UPLOADED, type DiscordGatewayEvent, -} from "@bete/shared"; +} from "../../shared/index.js"; export type { DiscordGatewayEvent }; diff --git a/services/discord-gateway/src/modules/gateway-metrics/metrics.ts b/services/discord-gateway/src/modules/gateway-metrics/metrics.ts index 4004ed9..7e7b672 100644 --- a/services/discord-gateway/src/modules/gateway-metrics/metrics.ts +++ b/services/discord-gateway/src/modules/gateway-metrics/metrics.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/guild-member-events/memberEvents.ts b/services/discord-gateway/src/modules/guild-member-events/memberEvents.ts index 6bc9152..acbba56 100644 --- a/services/discord-gateway/src/modules/guild-member-events/memberEvents.ts +++ b/services/discord-gateway/src/modules/guild-member-events/memberEvents.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Client, GuildMember, diff --git a/services/discord-gateway/src/modules/message-capture/attachmentsDb.ts b/services/discord-gateway/src/modules/message-capture/attachmentsDb.ts index 0aeb01b..3d76ff7 100644 --- a/services/discord-gateway/src/modules/message-capture/attachmentsDb.ts +++ b/services/discord-gateway/src/modules/message-capture/attachmentsDb.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/messageCapture.ts b/services/discord-gateway/src/modules/message-capture/messageCapture.ts index 47e1092..3ba6fd0 100644 --- a/services/discord-gateway/src/modules/message-capture/messageCapture.ts +++ b/services/discord-gateway/src/modules/message-capture/messageCapture.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/messageStore.ts b/services/discord-gateway/src/modules/message-capture/messageStore.ts index d83345a..0d61a12 100644 --- a/services/discord-gateway/src/modules/message-capture/messageStore.ts +++ b/services/discord-gateway/src/modules/message-capture/messageStore.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/messagesAnalysis.ts b/services/discord-gateway/src/modules/message-capture/messagesAnalysis.ts index 08ced7d..178485f 100644 --- a/services/discord-gateway/src/modules/message-capture/messagesAnalysis.ts +++ b/services/discord-gateway/src/modules/message-capture/messagesAnalysis.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/messagesCleanup.ts b/services/discord-gateway/src/modules/message-capture/messagesCleanup.ts index 5cd48df..a13108d 100644 --- a/services/discord-gateway/src/modules/message-capture/messagesCleanup.ts +++ b/services/discord-gateway/src/modules/message-capture/messagesCleanup.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/messagesCrud.ts b/services/discord-gateway/src/modules/message-capture/messagesCrud.ts index 2a32c19..405df85 100644 --- a/services/discord-gateway/src/modules/message-capture/messagesCrud.ts +++ b/services/discord-gateway/src/modules/message-capture/messagesCrud.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/messagesDb.ts b/services/discord-gateway/src/modules/message-capture/messagesDb.ts index 5a8ef2b..16a1734 100644 --- a/services/discord-gateway/src/modules/message-capture/messagesDb.ts +++ b/services/discord-gateway/src/modules/message-capture/messagesDb.ts @@ -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 { diff --git a/services/discord-gateway/src/modules/message-capture/messagesPagination.ts b/services/discord-gateway/src/modules/message-capture/messagesPagination.ts index 353d936..1e5fe48 100644 --- a/services/discord-gateway/src/modules/message-capture/messagesPagination.ts +++ b/services/discord-gateway/src/modules/message-capture/messagesPagination.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/messagesSearch.ts b/services/discord-gateway/src/modules/message-capture/messagesSearch.ts index a2ba6b5..96af0f3 100644 --- a/services/discord-gateway/src/modules/message-capture/messagesSearch.ts +++ b/services/discord-gateway/src/modules/message-capture/messagesSearch.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/moderationActionsDb.ts b/services/discord-gateway/src/modules/message-capture/moderationActionsDb.ts index c782ec0..61ff70e 100644 --- a/services/discord-gateway/src/modules/message-capture/moderationActionsDb.ts +++ b/services/discord-gateway/src/modules/message-capture/moderationActionsDb.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/retentionDb.ts b/services/discord-gateway/src/modules/message-capture/retentionDb.ts index efacfd5..ba12f4f 100644 --- a/services/discord-gateway/src/modules/message-capture/retentionDb.ts +++ b/services/discord-gateway/src/modules/message-capture/retentionDb.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/reviewsDb.ts b/services/discord-gateway/src/modules/message-capture/reviewsDb.ts index 5a96a15..e13c8ef 100644 --- a/services/discord-gateway/src/modules/message-capture/reviewsDb.ts +++ b/services/discord-gateway/src/modules/message-capture/reviewsDb.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/message-capture/types.ts b/services/discord-gateway/src/modules/message-capture/types.ts index d524dd5..d63a74a 100644 --- a/services/discord-gateway/src/modules/message-capture/types.ts +++ b/services/discord-gateway/src/modules/message-capture/types.ts @@ -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 { diff --git a/services/discord-gateway/src/modules/reaction-tracking/reactionCapture.ts b/services/discord-gateway/src/modules/reaction-tracking/reactionCapture.ts index 11feec0..90c8711 100644 --- a/services/discord-gateway/src/modules/reaction-tracking/reactionCapture.ts +++ b/services/discord-gateway/src/modules/reaction-tracking/reactionCapture.ts @@ -1,4 +1,4 @@ -import { createChildLogger } from "@bete/shared/logger"; +import { createChildLogger } from "@/shared/logger/index"; import type { Client, MessageReaction, diff --git a/services/discord-gateway/src/modules/thread-tracking/threadCapture.ts b/services/discord-gateway/src/modules/thread-tracking/threadCapture.ts index 2dca10d..0c60a8e 100644 --- a/services/discord-gateway/src/modules/thread-tracking/threadCapture.ts +++ b/services/discord-gateway/src/modules/thread-tracking/threadCapture.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/user-presence/presenceCapture.ts b/services/discord-gateway/src/modules/user-presence/presenceCapture.ts index 437caba..10b218b 100644 --- a/services/discord-gateway/src/modules/user-presence/presenceCapture.ts +++ b/services/discord-gateway/src/modules/user-presence/presenceCapture.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/voice-pcm-ws/voicePcmWsClient.ts b/services/discord-gateway/src/modules/voice-pcm-ws/voicePcmWsClient.ts index c5d5a31..1d8bbda 100644 --- a/services/discord-gateway/src/modules/voice-pcm-ws/voicePcmWsClient.ts +++ b/services/discord-gateway/src/modules/voice-pcm-ws/voicePcmWsClient.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/voice-recording/ffmpegProcess.ts b/services/discord-gateway/src/modules/voice-recording/ffmpegProcess.ts index d4f7d66..70d7eff 100644 --- a/services/discord-gateway/src/modules/voice-recording/ffmpegProcess.ts +++ b/services/discord-gateway/src/modules/voice-recording/ffmpegProcess.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/voice-recording/mediaSource.ts b/services/discord-gateway/src/modules/voice-recording/mediaSource.ts index ecf474a..0b1e50c 100644 --- a/services/discord-gateway/src/modules/voice-recording/mediaSource.ts +++ b/services/discord-gateway/src/modules/voice-recording/mediaSource.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/voice-recording/muxer.ts b/services/discord-gateway/src/modules/voice-recording/muxer.ts index 7608f4b..e1c8d45 100644 --- a/services/discord-gateway/src/modules/voice-recording/muxer.ts +++ b/services/discord-gateway/src/modules/voice-recording/muxer.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/voice-recording/packetFilter.ts b/services/discord-gateway/src/modules/voice-recording/packetFilter.ts index 643d4d1..cdd8f77 100644 --- a/services/discord-gateway/src/modules/voice-recording/packetFilter.ts +++ b/services/discord-gateway/src/modules/voice-recording/packetFilter.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/voice-recording/player.ts b/services/discord-gateway/src/modules/voice-recording/player.ts index f201c6d..a559571 100644 --- a/services/discord-gateway/src/modules/voice-recording/player.ts +++ b/services/discord-gateway/src/modules/voice-recording/player.ts @@ -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, diff --git a/services/discord-gateway/src/modules/voice-recording/recorder.ts b/services/discord-gateway/src/modules/voice-recording/recorder.ts index e78ec92..a7219ff 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder.ts @@ -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, diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/decoder.ts b/services/discord-gateway/src/modules/voice-recording/recorder/decoder.ts index d8d7eae..38c10f9 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder/decoder.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder/decoder.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/metadata.ts b/services/discord-gateway/src/modules/voice-recording/recorder/metadata.ts index f30542b..07f9fdf 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder/metadata.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder/metadata.ts @@ -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 { diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/segment.ts b/services/discord-gateway/src/modules/voice-recording/recorder/segment.ts index 6279617..e14bbc6 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder/segment.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder/segment.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/segmentFinalizer.ts b/services/discord-gateway/src/modules/voice-recording/recorder/segmentFinalizer.ts index 224a29e..7c892a3 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder/segmentFinalizer.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder/segmentFinalizer.ts @@ -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, diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/sessionRecording.ts b/services/discord-gateway/src/modules/voice-recording/recorder/sessionRecording.ts index 779ce16..2ba8e65 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder/sessionRecording.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder/sessionRecording.ts @@ -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"); diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/speakingHandler.ts b/services/discord-gateway/src/modules/voice-recording/recorder/speakingHandler.ts index 12ed2c8..5634cc3 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder/speakingHandler.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder/speakingHandler.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/streamSetup.ts b/services/discord-gateway/src/modules/voice-recording/recorder/streamSetup.ts index 28babea..be8b016 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder/streamSetup.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder/streamSetup.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/voice-recording/recorder/uploader.ts b/services/discord-gateway/src/modules/voice-recording/recorder/uploader.ts index d7040d7..5dc8346 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder/uploader.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder/uploader.ts @@ -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, diff --git a/services/discord-gateway/src/modules/voice-recording/transmitter.ts b/services/discord-gateway/src/modules/voice-recording/transmitter.ts index 1759a6d..5800f4f 100644 --- a/services/discord-gateway/src/modules/voice-recording/transmitter.ts +++ b/services/discord-gateway/src/modules/voice-recording/transmitter.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/voice-recording/voiceController.ts b/services/discord-gateway/src/modules/voice-recording/voiceController.ts index efb42f5..e3e5d7b 100644 --- a/services/discord-gateway/src/modules/voice-recording/voiceController.ts +++ b/services/discord-gateway/src/modules/voice-recording/voiceController.ts @@ -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"; diff --git a/services/discord-gateway/src/modules/voice-recording/voiceTranscriber.ts b/services/discord-gateway/src/modules/voice-recording/voiceTranscriber.ts index 558c6b9..f6d1371 100644 --- a/services/discord-gateway/src/modules/voice-recording/voiceTranscriber.ts +++ b/services/discord-gateway/src/modules/voice-recording/voiceTranscriber.ts @@ -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"; diff --git a/services/discord-gateway/src/shared/config/config.ts b/services/discord-gateway/src/shared/config/config.ts index 9dd739d..065b9f9 100644 --- a/services/discord-gateway/src/shared/config/config.ts +++ b/services/discord-gateway/src/shared/config/config.ts @@ -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(). diff --git a/packages/shared/src/config/index.ts b/services/discord-gateway/src/shared/config/index.ts similarity index 100% rename from packages/shared/src/config/index.ts rename to services/discord-gateway/src/shared/config/index.ts diff --git a/services/discord-gateway/src/shared/database/drizzle.ts b/services/discord-gateway/src/shared/database/drizzle.ts index 0d9bb61..24a6d73 100644 --- a/services/discord-gateway/src/shared/database/drizzle.ts +++ b/services/discord-gateway/src/shared/database/drizzle.ts @@ -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"; diff --git a/services/discord-gateway/src/shared/database/init.ts b/services/discord-gateway/src/shared/database/init.ts new file mode 100644 index 0000000..ca91bbb --- /dev/null +++ b/services/discord-gateway/src/shared/database/init.ts @@ -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 | 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, +) { + 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( + callback: (client: PoolClient) => Promise, +): Promise { + if (!rawPool) { + throw new Error( + "Database not initialized. Call initializeDatabase() first.", + ); + } + const client = await rawPool.connect(); + try { + return await callback(client); + } finally { + client.release(); + } +} diff --git a/services/discord-gateway/src/shared/database/migrate.ts b/services/discord-gateway/src/shared/database/migrate.ts index a264151..9974d27 100644 --- a/services/discord-gateway/src/shared/database/migrate.ts +++ b/services/discord-gateway/src/shared/database/migrate.ts @@ -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"; diff --git a/services/discord-gateway/src/shared/database/migrateCli.ts b/services/discord-gateway/src/shared/database/migrateCli.ts index cc7c6d8..2710cd2 100644 --- a/services/discord-gateway/src/shared/database/migrateCli.ts +++ b/services/discord-gateway/src/shared/database/migrateCli.ts @@ -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"); diff --git a/services/discord-gateway/src/shared/database/pool.ts b/services/discord-gateway/src/shared/database/pool.ts new file mode 100644 index 0000000..79884d8 --- /dev/null +++ b/services/discord-gateway/src/shared/database/pool.ts @@ -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 { + if (!pool) return Promise.resolve(); + return pool.end(); +} diff --git a/services/discord-gateway/src/shared/database/schema.ts b/services/discord-gateway/src/shared/database/schema.ts index 5c0c108..5a7bc2f 100644 --- a/services/discord-gateway/src/shared/database/schema.ts +++ b/services/discord-gateway/src/shared/database/schema.ts @@ -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; diff --git a/services/discord-gateway/src/shared/database/schema/analytics.ts b/services/discord-gateway/src/shared/database/schema/analytics.ts index 8a99302..d950528 100644 --- a/services/discord-gateway/src/shared/database/schema/analytics.ts +++ b/services/discord-gateway/src/shared/database/schema/analytics.ts @@ -3,7 +3,7 @@ import { pgChannelCulturesTable, pgUserProfilesTable, pgUserReputationsTable, -} from "@bete/shared"; +} from "../../../shared/index.js"; // Re-export shared tables export { diff --git a/services/discord-gateway/src/shared/database/schema/cache.ts b/services/discord-gateway/src/shared/database/schema/cache.ts index 41a7b67..81de785 100644 --- a/services/discord-gateway/src/shared/database/schema/cache.ts +++ b/services/discord-gateway/src/shared/database/schema/cache.ts @@ -2,7 +2,7 @@ import { pgCorrectedModerationsTable, pgStickerCacheTable, pgTextAnalysisCacheTable, -} from "@bete/shared"; +} from "../../../shared/index.js"; // Re-export shared tables export { diff --git a/services/discord-gateway/src/shared/database/schema/messages.ts b/services/discord-gateway/src/shared/database/schema/messages.ts index 608c2d4..3974d6f 100644 --- a/services/discord-gateway/src/shared/database/schema/messages.ts +++ b/services/discord-gateway/src/shared/database/schema/messages.ts @@ -2,7 +2,7 @@ import { pgAttachmentsTable, pgMessageReviewsTable, pgMessagesTable, -} from "@bete/shared"; +} from "../../../shared/index.js"; import { bigint as pgBigint, boolean as pgBoolean, diff --git a/services/discord-gateway/src/shared/database/schema/meta.ts b/services/discord-gateway/src/shared/database/schema/meta.ts index aabc48b..bc0931f 100644 --- a/services/discord-gateway/src/shared/database/schema/meta.ts +++ b/services/discord-gateway/src/shared/database/schema/meta.ts @@ -3,7 +3,7 @@ import { pgMuxerJobsTable, pgRetentionPoliciesTable, pgUIStateTable, -} from "@bete/shared"; +} from "../../../shared/index.js"; // Re-export shared tables export { diff --git a/services/discord-gateway/src/shared/database/schema/voice.ts b/services/discord-gateway/src/shared/database/schema/voice.ts index f4f7fd9..799a22d 100644 --- a/services/discord-gateway/src/shared/database/schema/voice.ts +++ b/services/discord-gateway/src/shared/database/schema/voice.ts @@ -1,4 +1,4 @@ -import { pgVoiceRecordingsTable } from "@bete/shared"; +import { pgVoiceRecordingsTable } from "../../../shared/index.js"; // Re-export shared table export { pgVoiceRecordingsTable }; diff --git a/services/discord-gateway/src/shared/database/voiceRecordingRepo.ts b/services/discord-gateway/src/shared/database/voiceRecordingRepo.ts index 0ccead7..3da5f46 100644 --- a/services/discord-gateway/src/shared/database/voiceRecordingRepo.ts +++ b/services/discord-gateway/src/shared/database/voiceRecordingRepo.ts @@ -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 { diff --git a/services/discord-gateway/src/shared/errors/index.ts b/services/discord-gateway/src/shared/errors/index.ts new file mode 100644 index 0000000..0ddb7ee --- /dev/null +++ b/services/discord-gateway/src/shared/errors/index.ts @@ -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, + ) { + super(message); + this.name = "AppError"; + } +} + +export class ValidationError extends AppError { + constructor(message: string, details?: Record) { + 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) { + 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"; + } +} diff --git a/services/discord-gateway/src/shared/index.ts b/services/discord-gateway/src/shared/index.ts new file mode 100644 index 0000000..c5d5d99 --- /dev/null +++ b/services/discord-gateway/src/shared/index.ts @@ -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"; diff --git a/services/discord-gateway/src/shared/logger/index.ts b/services/discord-gateway/src/shared/logger/index.ts new file mode 100644 index 0000000..37371b8 --- /dev/null +++ b/services/discord-gateway/src/shared/logger/index.ts @@ -0,0 +1,31 @@ +import pino from "pino"; + +const rootLogger = pino({ + level: process.env.LOG_LEVEL || "info", + transport: + process.env.NODE_ENV === "development" + ? { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "SYS:standard", + ignore: "pid,hostname", + }, + } + : undefined, +} as pino.LoggerOptions); + +export type Logger = ReturnType; + +/** + * Alias for backwards compatibility (discord-gateway uses this name). + */ +export type CustomLogger = Logger; + +/** + * Returns a child logger bound to the root singleton via pino's .child(). + * Preserves parent context and is efficient (no transport re-init per call). + */ +export function createChildLogger(context: string) { + return rootLogger.child({ context }); +} diff --git a/services/discord-gateway/src/shared/moderation-types.ts b/services/discord-gateway/src/shared/moderation-types.ts new file mode 100644 index 0000000..ab33481 --- /dev/null +++ b/services/discord-gateway/src/shared/moderation-types.ts @@ -0,0 +1,227 @@ +// Shared moderation types for all services +// Source of truth — snake_case + number (matching PostgreSQL schema) + +export type AIStatus = + | "pending" + | "processing" + | "clean" + | "warn" + | "flagged" + | "error"; +export type AISeverity = "none" | "low" | "medium" | "high" | "critical"; +export type AIRecommendedAction = + | "none" + | "monitor" + | "warn" + | "review" + | "delete" + | "escalate"; + +export interface BroadcasterClient { + messageCreated: (data: unknown) => void; + messageUpdated: (data: unknown) => void; + messageDeleted: (data: unknown) => void; + messageAnalyzed: (data: unknown) => void; + attachmentCreated: (data: unknown) => void; + attachmentUploaded: (data: unknown) => void; + voiceRecordingStarted: (data: unknown) => void; + voiceRecordingStopped: (data: unknown) => void; + voiceRecordingUploaded: (data: unknown) => void; + analysisQueueStatus: (data: unknown) => void; +} + +export type ModerationBroadcaster = BroadcasterClient; + +export interface RoleMetadata { + id: string; + name: string; + position: number; +} + +export interface UserMetadata { + userId: string; + username: string; + tag: string; + displayName: string; + avatarUrl: string; + bot: boolean; + roles: RoleMetadata[]; + highestRole: RoleMetadata | null; + joinedTimestamp: number | null; +} + +export interface MessageRecord { + id: string; + guild_id: string; + channel_id: string; + thread_id: string | null; + user_id: string; + username: string; + avatar_url: string | null; + content: string; + edited_content: string | null; + created_at: number; + edited_at: number | null; + deleted_at: number | null; + type: "text" | "edited" | "deleted"; + is_reply: boolean | null; + is_forward: boolean | null; + is_crosspost: boolean | null; + reference_message_id: string | null; + reference_channel_id: string | null; + reference_guild_id: string | null; + metadata: string | null; + ai_status?: AIStatus | null; + ai_moderation_flags?: string | null; + ai_moderation_score?: number | null; + ai_analysis?: string | null; + ai_categories?: string | null; + ai_severity?: AISeverity | null; + ai_confidence?: number | null; + ai_recommended_action?: AIRecommendedAction | null; + ai_analyzed_at?: number | null; + ai_error?: string | null; +} + +export interface AttachmentRecord { + id: string; + message_id: string; + guild_id: string; + channel_id: string; + thread_id: string | null; + user_id: string; + filename: string; + size: number; + type: string; + discord_url: string; + uploaded_url: string | null; + upload_status: "pending" | "uploaded" | "failed"; + upload_error: string | null; + created_at: number; + uploaded_at: number | null; +} + +export interface VoiceSegmentRecord { + id: string; + user_id: string; + session_id: string; + guild_id: string; + channel_id: string; + filename: string; + duration_ms: number; + created_at: number; +} + +export interface DashboardMessage { + id: string; + channel_id: string; + user_id: string; + username: string; + avatar_url: string | null; + content: string; + created_at: number; + type: "text" | "image" | "voice"; +} + +export interface MessageQuery { + guildId?: string; + channelId?: string; + threadId?: string; + status?: AIStatus[]; + userId?: string; + q?: string; + cursor?: string; + limit: number; +} + +export interface PageResult { + data: T[]; + nextCursor: string | null; +} + +export interface AnalysisResult { + messageId: string; + status: Exclude; + flags: string[]; + score: number; + analysis: string; + categories?: string[]; + severity?: AISeverity; + confidence?: number; + recommendedAction?: AIRecommendedAction; + policyVersion?: string; + evidence?: string[]; +} + +export interface VoiceRecordingUploadData { + id: string; + user_id: string; + username: string; + avatar_url: string | null; + guild_id: string | null; + channel_id: string | null; + channel_name: string | null; + filename: string; + size_bytes: number; + download_url: string; + upload_status: string; + created_at: number; + uploaded_at: number; + transcription?: string | null; +} + +export interface AnalysisQueueStatus { + queuedConversations: number; + activeRequests: number; + activeIndividualRequests: number; + individualInFlightCount: number; + individualCircuitBreakerActive: boolean; + lastError: string | null; +} + +export type ReviewStatus = "pending" | "approved" | "rejected" | "escalated"; + +export interface MessageReview { + id: string; + message_id: string; + guild_id: string; + channel_id: string; + reviewer_id: string | null; + status: ReviewStatus; + notes: string | null; + created_at: number; + reviewed_at: number | null; +} + +export type ModerationActionType = + | "delete_message" + | "mute_user" + | "warn_user" + | "kick_user" + | "ban_user"; + +export interface ModerationAction { + id: string; + message_id: string | null; + user_id: string | null; + guild_id: string; + action_type: ModerationActionType; + reason: string | null; + executed_by: string | null; + status: "pending" | "executed" | "failed"; + error: string | null; + created_at: number; + executed_at: number | null; +} + +export interface RetentionPolicy { + id: string; + guild_id: string; + channel_id: string | null; + retention_days: number; + apply_to_media: boolean; + apply_to_voice: boolean; + enabled: boolean; + created_at: number; + updated_at: number; +} diff --git a/services/discord-gateway/src/shared/redis-channels.ts b/services/discord-gateway/src/shared/redis-channels.ts new file mode 100644 index 0000000..0b70c05 --- /dev/null +++ b/services/discord-gateway/src/shared/redis-channels.ts @@ -0,0 +1,128 @@ +// --------------------------------------------------------------------------- +// Redis Channel Constants — single source of truth +// +// All Redis channel names, status keys, and command types used for +// inter-service communication between discord-gateway and backend. +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Event channels (discord-gateway -> backend via pub/sub) +// --------------------------------------------------------------------------- + +export const DISCORD_MESSAGE_CREATED = "discord:message:created"; +export const DISCORD_MESSAGE_UPDATED = "discord:message:updated"; +export const DISCORD_MESSAGE_DELETED = "discord:message:deleted"; +export const DISCORD_MESSAGE_ANALYZED = "discord:message:analyzed"; +export const DISCORD_ATTACHMENT_CREATED = "discord:attachment:created"; +export const DISCORD_ATTACHMENT_UPLOADED = "discord:attachment:uploaded"; +export const DISCORD_VOICE_STARTED = "discord:voice:started"; +export const DISCORD_VOICE_STOPPED = "discord:voice:stopped"; +export const DISCORD_VOICE_UPLOADED = "discord:voice:uploaded"; +export const DISCORD_VOICE_ACTIVE_USER = "discord:voice:active_user"; +export const DISCORD_VOICE_PCM = "discord:voice:pcm"; +export const DISCORD_ANALYSIS_QUEUE_STATUS = "discord:analysis:queue_status"; +export const DISCORD_REACTION_ADDED = "discord:reaction:added"; +export const DISCORD_REACTION_REMOVED = "discord:reaction:removed"; +export const DISCORD_THREAD_CREATED = "discord:thread:created"; +export const DISCORD_THREAD_DELETED = "discord:thread:deleted"; +export const DISCORD_THREAD_UPDATED = "discord:thread:updated"; +export const DISCORD_CHANNEL_TOPIC_UPDATED = "discord:channel:topic_updated"; +export const DISCORD_PRESENCE_UPDATED = "discord:presence:updated"; +export const DISCORD_GUILD_MEMBER_ADDED = "discord:guild_member:added"; +export const DISCORD_GUILD_MEMBER_REMOVED = "discord:guild_member:removed"; + +// --------------------------------------------------------------------------- +// Command channels (backend -> discord-gateway) +// --------------------------------------------------------------------------- + +export const BACKEND_COMMAND = "backend:command"; +export const BACKEND_VOICE_TRANSMIT = "backend:voice:transmit"; +export const BACKEND_COMMAND_REPLY_PREFIX = "backend:command:reply:"; + +// --------------------------------------------------------------------------- +// Status keys (set by discord-gateway, read by backend via Redis GET) +// --------------------------------------------------------------------------- + +export const VOICE_STATUS_KEY = "voice:status"; +export const MEDIA_STATUS_KEY = "media:status"; + +// --------------------------------------------------------------------------- +// Command types (used as the `type` field in CommandMessage envelopes) +// --------------------------------------------------------------------------- + +export const COMMAND_VOICE_CONNECT = "voice:connect"; +export const COMMAND_VOICE_DISCONNECT = "voice:disconnect"; +export const COMMAND_VOICE_DISCONNECT_GUILD = "voice:disconnect:guild"; +export const COMMAND_VOICE_CHANNELS = "voice:channels"; +export const COMMAND_VOICE_TRANSMIT_START = "voice:transmit:start"; +export const COMMAND_VOICE_TRANSMIT_STOP = "voice:transmit:stop"; +export const COMMAND_GUILDS_LIST = "guilds:list"; +export const COMMAND_GUILDS_TEXT_CHANNELS = "guilds:text-channels"; +export const COMMAND_MEDIA_QUEUE = "media:queue"; +export const COMMAND_MEDIA_SKIP = "media:skip"; +export const COMMAND_MEDIA_STOP = "media:stop"; +export const COMMAND_MEDIA_VOLUME = "media:volume"; +export const COMMAND_MODERATION_ACTION = "moderation:action"; +export const DISCORD_VOICE_ANALYZED = "discord:voice:analyzed"; + +// --------------------------------------------------------------------------- +// Event envelope — used by discord-gateway when publishing to Redis +// --------------------------------------------------------------------------- + +export interface DiscordGatewayEvent { + type: string; + data: unknown; + timestamp: number; + source: string; +} + +// --------------------------------------------------------------------------- +// Command envelope — used by backend when publishing to backend:command +// --------------------------------------------------------------------------- + +export interface CommandMessage { + id: string; + type: string; + payload: Record; + replyChannel: string; +} + +export interface CommandReply { + id: string; + success: boolean; + data?: T; + error?: string; +} + +// --------------------------------------------------------------------------- +// Discord Redis channel → WebSocket event type mapping (single source of truth) +// --------------------------------------------------------------------------- + +/** + * Maps each Discord Redis channel to its corresponding WebSocket event type. + * Used by the backend Redis bridge to dispatch events to frontend WS clients. + */ +export const DISCORD_CHANNEL_TO_WS_EVENT: Record = { + [DISCORD_MESSAGE_CREATED]: "message_created", + [DISCORD_MESSAGE_UPDATED]: "message_updated", + [DISCORD_MESSAGE_DELETED]: "message_deleted", + [DISCORD_MESSAGE_ANALYZED]: "message_analyzed", + [DISCORD_ATTACHMENT_CREATED]: "attachment_created", + [DISCORD_ATTACHMENT_UPLOADED]: "attachment_uploaded", + [DISCORD_VOICE_STARTED]: "voice_recording_started", + [DISCORD_VOICE_STOPPED]: "voice_recording_stopped", + [DISCORD_VOICE_UPLOADED]: "voice_recording_uploaded", + [DISCORD_ANALYSIS_QUEUE_STATUS]: "analysis_queue_status", + [DISCORD_VOICE_ACTIVE_USER]: "voice_active_user", + [DISCORD_VOICE_PCM]: "voice_pcm_data", + [DISCORD_VOICE_ANALYZED]: "voice_analyzed", + [DISCORD_REACTION_ADDED]: "reaction_added", + [DISCORD_REACTION_REMOVED]: "reaction_removed", + [DISCORD_THREAD_CREATED]: "thread_created", + [DISCORD_THREAD_DELETED]: "thread_deleted", + [DISCORD_THREAD_UPDATED]: "thread_updated", + [DISCORD_CHANNEL_TOPIC_UPDATED]: "channel_topic_updated", + [DISCORD_PRESENCE_UPDATED]: "presence_updated", + [DISCORD_GUILD_MEMBER_ADDED]: "guild_member_added", + [DISCORD_GUILD_MEMBER_REMOVED]: "guild_member_removed", +}; diff --git a/services/discord-gateway/src/shared/uploader.ts b/services/discord-gateway/src/shared/uploader.ts index 8bedafe..a31256e 100644 --- a/services/discord-gateway/src/shared/uploader.ts +++ b/services/discord-gateway/src/shared/uploader.ts @@ -1,5 +1,5 @@ -import { createChildLogger } from "@bete/shared/logger"; -import { retryWithBackoff } from "@bete/shared/utils"; +import { createChildLogger } from "@/shared/logger/index"; +import { retryWithBackoff } from "@/shared/utils/index"; const logger = createChildLogger("tele-upload"); diff --git a/services/discord-gateway/src/shared/utils/index.ts b/services/discord-gateway/src/shared/utils/index.ts new file mode 100644 index 0000000..83ceba9 --- /dev/null +++ b/services/discord-gateway/src/shared/utils/index.ts @@ -0,0 +1,112 @@ +// Utility functions shared across services + +export function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export * from "./pagination.js"; + +// --------------------------------------------------------------------------- +// Centralized AbortController with guaranteed cleanup +// --------------------------------------------------------------------------- + +/** + * Creates an AbortController with a timeout that is ALWAYS cleaned up, + * even if the caller throws or returns early without calling clear(). + * + * Returns both the controller and a cleanup handle. + * + * Usage: + * const { controller, clear } = createAbortControllerWithTimeout(8000); + * try { + * const res = await fetch(url, { signal: controller.signal }); + * // ... work ... + * } finally { + * clear(); // guaranteed to clear the timeout + * } + */ +export function createAbortControllerWithTimeout(timeoutMs: number): { + controller: AbortController; + clear: () => void; +} { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + // Unref so the timeout doesn't keep the process alive + timeoutId?.unref?.(); + return { + controller, + clear: () => { + clearTimeout(timeoutId); + }, + }; +} + +// --------------------------------------------------------------------------- +// Retry with exponential backoff +// --------------------------------------------------------------------------- + +export async function retryWithBackoff( + fn: () => Promise, + options: { + /** Number of retry attempts (default: 3) */ + retries?: number; + /** Initial delay in ms (default: 1000) */ + minTimeout?: number; + /** Maximum delay in ms (default: 30000) */ + maxTimeout?: number; + /** Multiplication factor for each retry (default: 2) */ + factor?: number; + /** Optional AbortSignal to cancel retries */ + signal?: AbortSignal; + } = {}, +): Promise { + const { + retries = 3, + minTimeout = 1_000, + maxTimeout = 30_000, + factor = 2, + signal, + } = options; + + let lastError: Error | undefined; + for (let attempt = 0; attempt <= retries; attempt++) { + if (signal?.aborted) { + const err = new Error("Aborted"); + err.name = "AbortError"; + throw err; + } + + try { + return await fn(); + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)); + if (lastError.name === "AbortError") { + throw lastError; + } + if (attempt === retries) break; + const backoff = Math.min( + minTimeout * factor ** attempt + Math.random() * 100, + maxTimeout, + ); + + await new Promise((resolve, reject) => { + let timeoutId: NodeJS.Timeout; + const onAbort = () => { + clearTimeout(timeoutId); + const abortErr = new Error("Aborted"); + abortErr.name = "AbortError"; + reject(abortErr); + }; + if (signal?.aborted) return onAbort(); + + timeoutId = setTimeout(() => { + if (signal) signal.removeEventListener("abort", onAbort); + resolve(); + }, backoff); + + if (signal) signal.addEventListener("abort", onAbort, { once: true }); + }); + } + } + throw lastError!; +} diff --git a/services/discord-gateway/src/shared/utils/pagination.ts b/services/discord-gateway/src/shared/utils/pagination.ts new file mode 100644 index 0000000..d0686d9 --- /dev/null +++ b/services/discord-gateway/src/shared/utils/pagination.ts @@ -0,0 +1,65 @@ +// Shared cursor-based pagination utilities + +export interface CursorData { + created_at: number; + id: string; +} + +/** + * Encode a cursor to a base64 string. + */ +export function encodeCursor(data: CursorData): string { + return Buffer.from(JSON.stringify(data)).toString("base64"); +} + +/** + * Decode a cursor from a base64 string. Returns null on invalid input. + */ +export function decodeCursor(cursor?: string): CursorData | null { + if (!cursor) return null; + try { + const data = JSON.parse(Buffer.from(cursor, "base64").toString("utf-8")); + if (typeof data.created_at === "number" && typeof data.id === "string") { + return data; + } + return null; + } catch { + return null; + } +} + +/** + * Build a `PageResult` from a slice of rows (limit + 1) using cursor-based pagination. + */ +export function pageResult( + rows: unknown[], + limit: number, +): { data: T[]; nextCursor: string | null } { + const hasMore = rows.length > limit; + const data = rows.slice(0, limit) as T[]; + const lastItem = data[data.length - 1]; + const nextCursor = + hasMore && lastItem + ? encodeCursor({ created_at: lastItem.created_at, id: lastItem.id }) + : null; + + return { data, nextCursor }; +} + +/** + * Build a Drizzle cursor condition expression. + * Used in WHERE clauses: `(created_at < cursor.created_at OR (created_at = cursor.created_at AND id < cursor.id))` + * + * Returns the SQL expression or undefined when cursor is absent. + */ +import { type SQL, sql } from "drizzle-orm"; + +export function buildCursorCondition( + created_at_col: SQL | unknown, + id_col: SQL | unknown, + cursor?: string, +): SQL | undefined { + const data = decodeCursor(cursor); + if (!data) return undefined; + return sql`(${created_at_col} < ${data.created_at} or (${created_at_col} = ${data.created_at} and ${id_col} < ${data.id}))`; +}