diff --git a/infra/docker/Dockerfile.backend b/infra/docker/Dockerfile.backend index 2ecd0fb..2ad79fb 100644 --- a/infra/docker/Dockerfile.backend +++ b/infra/docker/Dockerfile.backend @@ -1,53 +1,41 @@ -# ---- Builder stage: install deps + compile ---- +# ---- Builder stage ---- FROM node:22-slim AS builder WORKDIR /build -# Install pnpm and allow postinstall scripts for native addons -RUN npm install -g pnpm \ - && pnpm config set onlyBuiltDependencies '*' --location project +RUN npm install -g pnpm@10 -# Build-time system deps (python3 for node-gyp, g++ for native addons) +# Build tools for native addons (discordjs/voice may need node-gyp) RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ python3 make g++ \ && rm -rf /var/lib/apt/lists/* -# Copy manifest files first for layer caching -COPY services/backend/package.json ./ -COPY patches/ ./patches/ +# Copy source + manifests +COPY services/backend/ ./ -# Install all deps (devDeps included for build) +# Install ALL deps (devDeps needed for TypeScript build) RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ pnpm install --no-frozen-lockfile -# Copy source (shared code embedded in src/shared/) -COPY services/backend/ ./ - # Build TypeScript -RUN pnpm run build +RUN ./node_modules/.bin/tsc -# Strip devDependencies — production runtime only -RUN pnpm prune --prod +# Switch to production-only deps (strips devDeps + cleans .pnpm store) +RUN rm -rf node_modules && pnpm install --prod --no-frozen-lockfile -# ---- Runtime stage: minimal ---- -FROM node:22-slim +# ---- Runtime stage ---- +FROM node:22-alpine WORKDIR /app -# Create non-root user -RUN groupadd --system app && useradd --system -g app app - -# Only production artifacts from builder -COPY --from=builder /build/dist ./dist -COPY --from=builder /build/node_modules ./node_modules -COPY --from=builder /build/package.json ./ - -RUN chown -R app:app /app -USER app +COPY --from=builder --chown=node:node /build/dist ./dist +COPY --from=builder --chown=node:node /build/node_modules ./node_modules +COPY --from=builder --chown=node:node /build/package.json ./ +USER node EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ - CMD wget -qO- http://localhost:3000/api/health || exit 1 + CMD node -e "require('http').get('http://localhost:3000/api/health',r=>process.exit(r.statusCode===200?0:1))" CMD ["node", "dist/index.js"] diff --git a/infra/docker/Dockerfile.discord-gateway b/infra/docker/Dockerfile.discord-gateway index 1aad2f8..a57c547 100644 --- a/infra/docker/Dockerfile.discord-gateway +++ b/infra/docker/Dockerfile.discord-gateway @@ -1,60 +1,52 @@ -# ---- Builder stage: native addons + TypeScript ---- +# ---- Builder stage ---- FROM node:22-slim AS builder WORKDIR /build -# Install pnpm and allow postinstall scripts for native addons -RUN corepack enable \ - && pnpm config set onlyBuiltDependencies '*' --location project +RUN npm install -g pnpm@10 -# Build tools + Rust (required by @dank074/discord-video-stream) +# Build tools + Rust (required by napi-rs native addons) RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ python3 make g++ curl ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Install Rust toolchain (separate RUN so the layer can cache if apt changes) +# Install Rust toolchain RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable ENV PATH="/root/.cargo/bin:${PATH}" -# Manifest files first for layer caching -COPY services/discord-gateway/package.json ./ -COPY patches/ ./patches/ +COPY services/discord-gateway/ ./ -# Install all deps (devDeps needed for TypeScript build) RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ pnpm install --no-frozen-lockfile -# Source code (shared code embedded in src/shared/) -COPY services/discord-gateway/ ./ -COPY services/discord-gateway/drizzle ./drizzle +RUN ./node_modules/.bin/tsc -# Build TypeScript -RUN pnpm run build +# Strip debug symbols from native addons +RUN find node_modules -name '*.node' -exec strip -s {} \; 2>/dev/null; \ + find node_modules -name '*.so' -exec strip -s {} \; 2>/dev/null; \ + echo "Stripped native binaries" -# Strip devDependencies — only production deps in final image -RUN pnpm prune --prod +# Switch to production-only deps +RUN rm -rf node_modules && pnpm install --prod --no-frozen-lockfile -# ---- Runtime stage: minimal ---- +# ---- Runtime stage ---- FROM node:22-slim +# Install ffmpeg with minimal dependencies +RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ + ffmpeg ca-certificates && rm -rf /var/lib/apt/lists/* + WORKDIR /app -# Runtime system deps only (ffmpeg for audio processing) -RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ - ffmpeg ca-certificates \ - && rm -rf /var/lib/apt/lists/* +COPY --from=builder --chown=node:node /build/dist ./dist +COPY --from=builder --chown=node:node /build/node_modules ./node_modules +COPY --from=builder --chown=node:node /build/package.json ./ +COPY --from=builder --chown=node:node /build/drizzle ./drizzle -# Only production artifacts from builder -COPY --from=builder /build/dist ./dist -COPY --from=builder /build/node_modules ./node_modules -COPY --from=builder /build/package.json ./ -COPY --from=builder /build/drizzle ./drizzle - -# Create recordings dir -RUN mkdir -p /app/recordings && chown -R node:node /app +RUN mkdir -p /app/recordings USER node HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD sh -c "kill -0 1" + CMD node -e "process.exit(0)" CMD ["node", "dist/index.js"] diff --git a/infra/docker/Dockerfile.proxy b/infra/docker/Dockerfile.proxy index b0b0d7b..f9bb432 100644 --- a/infra/docker/Dockerfile.proxy +++ b/infra/docker/Dockerfile.proxy @@ -3,34 +3,22 @@ FROM node:22-slim AS builder WORKDIR /build -RUN corepack enable +RUN npm install -g pnpm@10 -# Build essentials +# Build essentials (native deps like sharp need g++) RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ make g++ \ && rm -rf /var/lib/apt/lists/* -# Manifest files first -COPY services/frontend/package.json ./ -COPY services/frontend/tsconfig.json ./ -COPY services/frontend/next.config.ts ./ - -# Install all deps, skip postinstall (no native addons needed for static export) -RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm install --ignore-scripts --no-frozen-lockfile - -# Source code +# Copy source + manifests COPY services/frontend/ ./ -# Build args -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 all deps +RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ + pnpm install --no-frozen-lockfile # Build static export -RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - npx next build +RUN pnpm run build # ---- Runtime stage: nginx ---- FROM nginx:alpine diff --git a/services/discord-gateway/package.json b/services/discord-gateway/package.json index f913f5a..f67c35c 100644 --- a/services/discord-gateway/package.json +++ b/services/discord-gateway/package.json @@ -4,7 +4,20 @@ "description": "Discord Gateway service - handles message capture, voice recording, and AI moderation", "type": "module", "main": "dist/index.js", - "packageManager": "pnpm@11.1.3", + "pnpm": { + "onlyBuiltDependencies": [ + "@discordjs/opus", + "@lng2004/node-datachannel", + "esbuild", + "node-av", + "node-crc", + "sharp", + "zeromq" + ], + "patchedDependencies": { + "node-crc@4.0.0": "./patches/node-crc@4.0.0.patch" + } + }, "scripts": { "dev": "tsx watch src/index.ts", "start": "node dist/index.js", diff --git a/services/discord-gateway/patches/node-crc@4.0.0.patch b/services/discord-gateway/patches/node-crc@4.0.0.patch new file mode 100644 index 0000000..0597d7f --- /dev/null +++ b/services/discord-gateway/patches/node-crc@4.0.0.patch @@ -0,0 +1,13 @@ +diff --git a/Cargo.toml b/Cargo.toml +index a967508960d8b6b686b23401ea14333b858a675c..d0282a30ee931563a65d774ed783c83d6d2fdd5e 100644 +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -3,7 +3,7 @@ name = "node-crc" + version = "4.0.0" + authors = ["Magic Len "] + edition = "2021" +-rust-version = "1.65" ++rust-version = "1.77.0" + repository = "https://github.com/magiclen/node-crc" + homepage = "https://magiclen.org/node-js-crc/" + keywords = ["nodejs", "crc8", "crc16", "crc32", "crc64"] diff --git a/services/discord-gateway/src/shared/database/schema.ts b/services/discord-gateway/src/shared/database/schema.ts index 5a7bc2f..f143189 100644 --- a/services/discord-gateway/src/shared/database/schema.ts +++ b/services/discord-gateway/src/shared/database/schema.ts @@ -594,3 +594,103 @@ export type DbRetentionPolicyInsert = export type ChatbotMessage = typeof chatbotMessagesTable.$inferSelect; export type ChatbotMessageInsert = typeof chatbotMessagesTable.$inferInsert; + +// ============================================================================= +// Moderation Actions (gateway-local) +// ============================================================================= + +export const pgModerationActionsTable = pgTable( + "moderation_actions", + { + id: pgText("id").primaryKey(), + message_id: pgText("message_id"), + user_id: pgText("user_id"), + guild_id: pgText("guild_id").notNull(), + action_type: pgText("action_type", { + enum: [ + "delete_message", + "mute_user", + "warn_user", + "kick_user", + "ban_user", + ], + }).notNull(), + reason: pgText("reason"), + executed_by: pgText("executed_by"), + status: pgText("status", { + enum: ["pending", "executed", "failed"], + }) + .notNull() + .default("pending"), + error: pgText("error"), + created_at: pgBigint("created_at", { mode: "number" }).notNull(), + executed_at: pgBigint("executed_at", { mode: "number" }), + }, + (table) => ({ + messageIdIdx: pgIndex("idx_moderation_actions_message_id").on( + table.message_id, + ), + userIdIdx: pgIndex("idx_moderation_actions_user_id").on(table.user_id), + statusIdx: pgIndex("idx_moderation_actions_status").on(table.status), + guildStatusIdx: pgIndex("idx_moderation_actions_guild_status").on( + table.guild_id, + table.status, + table.created_at, + ), + }), +); + +export const moderationActionsTable = pgModerationActionsTable; + +// ============================================================================= +// Message Edits (gateway-local) +// ============================================================================= + +export const pgMessageEditsTable = pgTable( + "message_edits", + { + id: pgUuid("id").defaultRandom().primaryKey(), + message_id: pgText("message_id").notNull(), + old_content: pgText("old_content").notNull(), + edited_at: pgBigint("edited_at", { mode: "number" }).notNull(), + }, + (table) => ({ + messageIdIdx: pgIndex("idx_message_edits_message_id").on(table.message_id), + editedAtIdx: pgIndex("idx_message_edits_edited_at").on(table.edited_at), + }), +); + +export const messageEditsTable = pgMessageEditsTable; + +// ============================================================================= +// Reactions (gateway-local) +// ============================================================================= + +export const pgReactionsTable = pgTable( + "message_reactions", + { + id: pgText("id").primaryKey(), + message_id: pgText("message_id").notNull(), + channel_id: pgText("channel_id").notNull(), + guild_id: pgText("guild_id").notNull(), + user_id: pgText("user_id").notNull(), + username: pgText("username").notNull(), + emoji: pgText("emoji").notNull(), + emoji_id: pgText("emoji_id"), + animated: pgBoolean("animated").notNull().default(false), + reaction_type: pgText("reaction_type", { + enum: ["add", "remove"], + }).notNull(), + created_at: pgBigint("created_at", { mode: "number" }).notNull(), + }, + (table) => ({ + messageIdIdx: pgIndex("idx_reactions_message_id").on(table.message_id), + userIdIdx: pgIndex("idx_reactions_user_id").on(table.user_id), + guildCreatedIdx: pgIndex("idx_reactions_guild_created").on( + table.guild_id, + table.created_at, + ), + }), +); + +export const reactionsTable = pgReactionsTable;