From 0d55fe59789443963b73154a23faba643b30f77b Mon Sep 17 00:00:00 2001 From: Puechberty Arthur Date: Thu, 23 Apr 2026 23:39:18 +0200 Subject: [PATCH] nettoyage du code --- .github/workflows/deploy.yml | 2 +- AGENT.md | 28 +- README.md | 44 +- apps/api/scripts/migrate.mjs | 45 +- apps/api/src/auth/discordOAuth.ts | 13 +- apps/api/src/auth/jwt.ts | 8 +- apps/api/src/config/env.ts | 21 +- apps/api/src/db/pool.ts | 4 +- apps/api/src/db/repositories.ts | 10 +- apps/api/src/index.ts | 13 +- apps/api/src/routes/authRoutes.ts | 18 +- apps/api/src/routes/botRoutes.ts | 50 +- apps/api/tsconfig.json | 5 +- apps/bot/locales/ar.json | 2 +- apps/bot/locales/bn.json | 2 +- apps/bot/locales/de.json | 2 +- apps/bot/locales/en.json | 2 +- apps/bot/locales/es.json | 2 +- apps/bot/locales/fr.json | 2 +- apps/bot/locales/hi.json | 2 +- apps/bot/locales/id.json | 2 +- apps/bot/locales/it.json | 2 +- apps/bot/locales/ja.json | 2 +- apps/bot/locales/nl.json | 2 +- apps/bot/locales/pl.json | 2 +- apps/bot/locales/pt.json | 2 +- apps/bot/locales/ru.json | 2 +- apps/bot/locales/tr.json | 2 +- apps/bot/locales/zh.json | 2 +- apps/bot/scripts/copyLegacyLocales.mjs | 25 +- apps/bot/src/config/env.ts | 45 +- apps/bot/src/db/pool.ts | 6 +- apps/bot/src/db/repositories.ts | 12 +- apps/bot/src/index.ts | 34 +- apps/bot/src/legacy/app/bootstrap.ts | 67 +- apps/bot/src/legacy/app/container.ts | 12 +- apps/bot/src/legacy/commands/goodbye.ts | 36 +- apps/bot/src/legacy/commands/index.ts | 5 +- apps/bot/src/legacy/commands/kiss.ts | 4 +- apps/bot/src/legacy/commands/logs.ts | 29 +- apps/bot/src/legacy/commands/presence.ts | 29 +- apps/bot/src/legacy/commands/welcome.ts | 36 +- apps/bot/src/legacy/config/env.ts | 51 +- .../bot/src/legacy/core/commands/argParser.ts | 121 +- .../src/legacy/core/commands/defineCommand.ts | 5 +- apps/bot/src/legacy/core/commands/deploy.ts | 14 +- apps/bot/src/legacy/core/commands/registry.ts | 15 +- .../src/legacy/core/commands/slashBuilder.ts | 67 +- apps/bot/src/legacy/core/commands/usage.ts | 29 +- .../legacy/core/execution/CommandExecutor.ts | 64 +- .../legacy/core/execution/cooldownStore.ts | 10 +- .../bot/src/legacy/core/execution/dispatch.ts | 32 +- .../core/execution/globalRateLimitStore.ts | 64 +- .../execution/redisCommandJobPublisher.ts | 5 +- .../legacy/core/runtime/leaderCoordinator.ts | 22 +- .../legacy/database/stores/logEventStore.ts | 17 +- .../database/stores/memberMessageStore.ts | 26 +- .../legacy/database/stores/presenceStore.ts | 24 +- apps/bot/src/legacy/events/guildDelete.ts | 5 +- apps/bot/src/legacy/events/guildMemberAdd.ts | 92 +- .../src/legacy/events/guildMemberRemove.ts | 34 +- apps/bot/src/legacy/events/index.ts | 15 +- .../src/legacy/events/interactionCreate.ts | 10 +- apps/bot/src/legacy/events/logsRuntime.ts | 361 ++++-- apps/bot/src/legacy/events/messageCreate.ts | 5 +- apps/bot/src/legacy/events/ready.ts | 62 +- apps/bot/src/legacy/features/logs/catalog.ts | 35 +- .../src/legacy/features/logs/commandPanel.ts | 114 +- .../src/legacy/features/logs/repository.ts | 15 +- apps/bot/src/legacy/features/logs/service.ts | 147 ++- .../features/memberMessages/commandPanel.ts | 170 ++- .../features/memberMessages/imageRenderer.ts | 74 +- .../features/memberMessages/repository.ts | 13 +- .../legacy/features/memberMessages/service.ts | 118 +- .../legacy/features/presence/commandPanel.ts | 126 +- .../src/legacy/features/presence/service.ts | 82 +- .../features/presence/templateVariables.ts | 42 +- .../handlers/commandExecutionContext.ts | 10 +- .../handlers/discordPermissionResolver.ts | 21 +- apps/bot/src/legacy/handlers/prefixHandler.ts | 39 +- apps/bot/src/legacy/handlers/replyAdapter.ts | 52 +- apps/bot/src/legacy/handlers/slashHandler.ts | 4 +- apps/bot/src/legacy/i18n/index.ts | 343 ++--- apps/bot/src/legacy/modules/help/service.ts | 128 +- apps/bot/src/legacy/modules/logs/index.ts | 2 +- apps/bot/src/legacy/types/argParser.ts | 2 +- apps/bot/src/legacy/types/command.ts | 32 +- apps/bot/src/legacy/types/deploy.ts | 2 +- apps/bot/src/legacy/types/handlers.ts | 2 +- apps/bot/src/legacy/types/i18n.ts | 2 +- apps/bot/src/legacy/types/logs.ts | 2 +- apps/bot/src/legacy/types/memberMessages.ts | 8 +- apps/bot/src/legacy/types/presence.ts | 17 +- apps/bot/src/legacy/types/reply.ts | 2 +- .../bot/src/legacy/types/templateVariables.ts | 2 +- .../bot/src/legacy/utils/templateVariables.ts | 21 +- apps/bot/src/legacy/validators/logs.ts | 10 +- .../src/legacy/validators/memberMessages.ts | 27 +- apps/bot/src/legacy/validators/presence.ts | 54 +- apps/bot/src/manager/BotManager.ts | 37 +- apps/bot/src/queue/worker.ts | 5 +- .../src/runtime/initializeLegacyBotRuntime.ts | 70 +- .../src/runtime/stores/TenantLogEventStore.ts | 7 +- .../stores/TenantMemberMessageStore.ts | 16 +- .../src/runtime/stores/TenantPresenceStore.ts | 24 +- apps/bot/tsconfig.json | 4 +- apps/web/app/[locale]/dashboard/page.tsx | 3 +- apps/web/app/[locale]/layout.tsx | 10 +- apps/web/app/[locale]/login/page.tsx | 11 +- apps/web/app/globals.css | 30 +- apps/web/components/FeatureCard.tsx | 8 +- apps/web/components/Features.tsx | 151 ++- apps/web/components/Hero.tsx | 21 +- apps/web/components/HowItWorks.tsx | 8 +- apps/web/components/Logo.tsx | 2 +- apps/web/components/dashboard-client.tsx | 72 +- apps/web/components/ui/Badge.tsx | 2 +- apps/web/components/ui/Button.tsx | 2 +- apps/web/components/ui/Card.tsx | 22 +- apps/web/components/ui/Container.tsx | 12 +- apps/web/components/ui/Input.tsx | 2 +- apps/web/components/ui/Navbar.tsx | 8 +- apps/web/components/ui/PageWrapper.tsx | 6 +- apps/web/components/ui/Switch.tsx | 2 +- apps/web/components/ui/cn.ts | 2 +- apps/web/i18n/client.ts | 2 +- apps/web/i18n/navigation.ts | 2 +- apps/web/i18n/next-intl.d.ts | 2 +- apps/web/i18n/request.ts | 2 +- apps/web/i18n/routing.ts | 2 +- apps/web/i18n/server.ts | 2 +- apps/web/locales/ar.json | 2 +- apps/web/locales/bn.json | 2 +- apps/web/locales/de.json | 2 +- apps/web/locales/en.json | 2 +- apps/web/locales/es.json | 2 +- apps/web/locales/hi.json | 2 +- apps/web/locales/id.json | 2 +- apps/web/locales/it.json | 2 +- apps/web/locales/ja.json | 2 +- apps/web/locales/nl.json | 2 +- apps/web/locales/pl.json | 2 +- apps/web/locales/pt.json | 2 +- apps/web/locales/ru.json | 2 +- apps/web/locales/tr.json | 2 +- apps/web/locales/zh.json | 2 +- apps/web/middleware.ts | 6 +- apps/web/next.config.mjs | 15 + apps/web/public/manifest.json | 2 +- apps/web/tsconfig.json | 17 +- docker-compose.yml | 5 +- eslint.config.js | 92 ++ package-lock.json | 1106 ++++++++++++++++- package.json | 10 +- packages/shared/src/constants.ts | 8 +- packages/shared/src/redisKeys.ts | 14 +- packages/shared/src/tokenCrypto.ts | 21 +- packages/shared/tsconfig.json | 3 +- scripts/i18n/sync-locales.mjs | 17 +- tsconfig.base.json | 6 +- 160 files changed, 4165 insertions(+), 1237 deletions(-) create mode 100644 eslint.config.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bcbdf61..95c4a31 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -42,4 +42,4 @@ jobs: echo "Cleaning unused Docker images..." docker image prune -f - echo "Deployment finished successfully" \ No newline at end of file + echo "Deployment finished successfully" diff --git a/AGENT.md b/AGENT.md index 2bb8a0e..4b8c13f 100644 --- a/AGENT.md +++ b/AGENT.md @@ -2,19 +2,19 @@ Version: 3.0 -But ---- +## But + Decrire l'architecture cible du monorepo SaaS pour bot Discord multi-tenant. -Vue d'ensemble --------------- +## Vue d'ensemble + - Stack backend: Node.js + TypeScript + Express + PostgreSQL + Redis + BullMQ. - Bot runtime: Discord.js avec gestion dynamique multi-instance. - Frontend: Next.js App Router. - Structure: monorepo `apps/*` + `packages/*`. -Organisation des dossiers -------------------------- +## Organisation des dossiers + - `apps/api/` - OAuth2 Discord, JWT cookie, routes multi-tenant, validation token bot, publication jobs BullMQ. - `apps/bot/` @@ -26,8 +26,8 @@ Organisation des dossiers - `database/migrations/` - SQL versionne (`schema_migrations`) avec schema multi-tenant. -Principes d'architecture ------------------------- +## Principes d'architecture + - Multi-tenant strict: - toutes les requetes de lecture/ecriture passent par `tenant_id`. - tables critiques liees a `tenant_id` et/ou `owner_user_id`. @@ -40,15 +40,15 @@ Principes d'architecture - Bot: runtime Discord. - Web: UX dashboard. -Conventions de code -------------------- +## Conventions de code + - TypeScript strict. - Exports nommes. - Erreurs API explicites et codes HTTP coherents. - Logs sans fuite de secrets. -Workflow recommande -------------------- +## Workflow recommande + 1. Ajouter/adapter migration SQL dans `database/migrations`. 2. Adapter repositories API/Bot avec filtre tenant. 3. Ajouter endpoint API + validation zod. @@ -56,8 +56,8 @@ Workflow recommande 5. Mettre a jour le dashboard web. 6. Valider `npm run typecheck` puis `docker compose up -d --build`. -Securite --------- +## Securite + - Ne jamais committer `.env`. - Garder `TOKEN_ENCRYPTION_KEY` hors depot. - Appliquer `httpOnly` + `sameSite` sur cookie session. diff --git a/README.md b/README.md index 3210390..d3d1d7d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ docker-compose.yml - Maintient une map en mémoire: ```ts -Map +Map; ``` - Worker BullMQ: consomme les jobs `start|stop|restart` @@ -55,32 +55,32 @@ Map ### 3) Web (`apps/web`) - Pages principales: - - `/login` - - `/dashboard` + - `/login` + - `/dashboard` - Dashboard utilisateur: - - Ajouter un bot (token) - - Voir la liste des bots du tenant - - `start / stop / restart` + - Ajouter un bot (token) + - Voir la liste des bots du tenant + - `start / stop / restart` ## Schéma PostgreSQL (core SaaS) Migration: `database/migrations/0004_saas_multitenant.sql` - `tenants` - - `id` (UUID) - - `owner_user_id` + - `id` (UUID) + - `owner_user_id` - `users` - - `tenant_id` FK - - `discord_user_id` (unique) - - `role` + - `tenant_id` FK + - `discord_user_id` (unique) + - `role` - `bots` - - `tenant_id` FK - - `owner_user_id` FK - - `discord_bot_id` (unique global) - - `token_ciphertext`, `token_iv`, `token_tag` - - `status`, `last_error` + - `tenant_id` FK + - `owner_user_id` FK + - `discord_bot_id` (unique global) + - `token_ciphertext`, `token_iv`, `token_tag` + - `status`, `last_error` - `bot_runtime_events` - - logs runtime par bot + tenant + - logs runtime par bot + tenant Les tables legacy de configuration (`bot_presence_states`, `bot_member_message_configs`, `bot_log_event_configs`) sont enrichies avec `tenant_id` et `owner_user_id` pour respecter l'isolation multi-tenant stricte. @@ -97,7 +97,7 @@ Les tables legacy de configuration (`bot_presence_states`, `bot_member_message_c - `GET /api/bots` - `POST /api/bots` - - body: `{ token: string, displayName?: string }` + - body: `{ token: string, displayName?: string }` - `POST /api/bots/:botId/start` - `POST /api/bots/:botId/stop` - `POST /api/bots/:botId/restart` @@ -107,7 +107,7 @@ Tous ces endpoints sont tenant-scopés via la session. ## Sécurité - Tokens bot jamais stockés en clair - - AES-256-GCM avec `TOKEN_ENCRYPTION_KEY` (32 bytes en base64) + - AES-256-GCM avec `TOKEN_ENCRYPTION_KEY` (32 bytes en base64) - Validation token côté Discord avant insertion - Session auth via JWT httpOnly cookie - Filtrage systématique des requêtes par `tenant_id` @@ -168,6 +168,6 @@ Bot manager health: - Une instance bot unique peut gérer des dizaines/centaines de bots selon ressources. - Pour monter en charge horizontalement: - - scaler `apps/api` - - scaler `apps/bot` avec coordination queue/locks - - conserver Redis + PostgreSQL managés + - scaler `apps/api` + - scaler `apps/bot` avec coordination queue/locks + - conserver Redis + PostgreSQL managés diff --git a/apps/api/scripts/migrate.mjs b/apps/api/scripts/migrate.mjs index c4d917c..e2f27d9 100644 --- a/apps/api/scripts/migrate.mjs +++ b/apps/api/scripts/migrate.mjs @@ -10,12 +10,21 @@ const LOCK_CLASS_ID = 7813; const LOCK_OBJECT_ID = 4312; const parseBoolean = (value, fallback = false) => { - if (value === undefined || value === null || String(value).trim().length === 0) { + if ( + value === undefined || + value === null || + String(value).trim().length === 0 + ) { return fallback; } const normalized = String(value).trim().toLowerCase(); - return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on"; + return ( + normalized === "true" || + normalized === "1" || + normalized === "yes" || + normalized === "on" + ); }; const sha256 = (content) => createHash("sha256").update(content).digest("hex"); @@ -33,7 +42,9 @@ const loadMigrations = async (migrationsDir) => { const files = await listMigrationFiles(migrationsDir); if (files.length === 0) { - throw new Error(`[migrate] no SQL migration files found in ${migrationsDir}`); + throw new Error( + `[migrate] no SQL migration files found in ${migrationsDir}`, + ); } const migrations = []; @@ -63,9 +74,13 @@ const ensureMigrationTable = async (client) => { }; const applyMigrations = async (client, migrations) => { - const applied = await client.query("SELECT version, checksum FROM schema_migrations ORDER BY version ASC"); + const applied = await client.query( + "SELECT version, checksum FROM schema_migrations ORDER BY version ASC", + ); - const appliedByVersion = new Map(applied.rows.map((row) => [String(row.version), String(row.checksum)])); + const appliedByVersion = new Map( + applied.rows.map((row) => [String(row.version), String(row.checksum)]), + ); for (const migration of migrations) { const existingChecksum = appliedByVersion.get(migration.fileName); @@ -91,7 +106,9 @@ const applyMigrations = async (client, migrations) => { console.log(`[migrate] applied ${migration.fileName}`); } catch (error) { await client.query("ROLLBACK").catch(() => undefined); - throw new Error(`[migrate] failed while applying ${migration.fileName}`, { cause: error }); + throw new Error(`[migrate] failed while applying ${migration.fileName}`, { + cause: error, + }); } } }; @@ -111,8 +128,8 @@ const main = async () => { connectionString: process.env.DATABASE_URL, ssl: parseBoolean(process.env.DATABASE_SSL, false) ? { - rejectUnauthorized: true, - } + rejectUnauthorized: true, + } : undefined, }); @@ -121,12 +138,20 @@ const main = async () => { try { const migrations = await loadMigrations(migrationsDir); - await client.query("SELECT pg_advisory_lock($1, $2)", [LOCK_CLASS_ID, LOCK_OBJECT_ID]); + await client.query("SELECT pg_advisory_lock($1, $2)", [ + LOCK_CLASS_ID, + LOCK_OBJECT_ID, + ]); await ensureMigrationTable(client); await applyMigrations(client, migrations); console.log("[migrate] completed"); } finally { - await client.query("SELECT pg_advisory_unlock($1, $2)", [LOCK_CLASS_ID, LOCK_OBJECT_ID]).catch(() => undefined); + await client + .query("SELECT pg_advisory_unlock($1, $2)", [ + LOCK_CLASS_ID, + LOCK_OBJECT_ID, + ]) + .catch(() => undefined); client.release(); await pool.end(); } diff --git a/apps/api/src/auth/discordOAuth.ts b/apps/api/src/auth/discordOAuth.ts index 962cc4b..f06fee9 100644 --- a/apps/api/src/auth/discordOAuth.ts +++ b/apps/api/src/auth/discordOAuth.ts @@ -53,7 +53,10 @@ export const buildDiscordLoginUrl = (state: string): string => { return `https://discord.com/oauth2/authorize?${params.toString()}`; }; -const createAvatarUrl = (discordUserId: string, avatarHash: string | null): string | null => { +const createAvatarUrl = ( + discordUserId: string, + avatarHash: string | null, +): string | null => { if (!avatarHash) { return null; } @@ -61,7 +64,9 @@ const createAvatarUrl = (discordUserId: string, avatarHash: string | null): stri return `https://cdn.discordapp.com/avatars/${discordUserId}/${avatarHash}.png`; }; -export const exchangeCodeForDiscordIdentity = async (code: string): Promise => { +export const exchangeCodeForDiscordIdentity = async ( + code: string, +): Promise => { const payload = new URLSearchParams({ client_id: env.DISCORD_CLIENT_ID, client_secret: env.DISCORD_CLIENT_SECRET, @@ -103,7 +108,9 @@ export const exchangeCodeForDiscordIdentity = async (code: string): Promise => { +export const validateDiscordBotToken = async ( + botToken: string, +): Promise => { const response = await fetch(`${DISCORD_API_BASE}/users/@me`, { headers: { Authorization: `Bot ${botToken}`, diff --git a/apps/api/src/auth/jwt.ts b/apps/api/src/auth/jwt.ts index 1853adc..9fef2ad 100644 --- a/apps/api/src/auth/jwt.ts +++ b/apps/api/src/auth/jwt.ts @@ -12,7 +12,9 @@ interface SessionJwtPayload { const secret = new TextEncoder().encode(env.JWT_SECRET); -export const issueSessionToken = async (session: AuthSession): Promise => { +export const issueSessionToken = async ( + session: AuthSession, +): Promise => { return new SignJWT({ tenantId: session.tenantId, discordUserId: session.discordUserId, @@ -25,7 +27,9 @@ export const issueSessionToken = async (session: AuthSession): Promise = .sign(secret); }; -export const verifySessionToken = async (token: string): Promise => { +export const verifySessionToken = async ( + token: string, +): Promise => { const verified = await jwtVerify(token, secret, { algorithms: ["HS256"], }); diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts index d32a4ce..5b7f7fd 100644 --- a/apps/api/src/config/env.ts +++ b/apps/api/src/config/env.ts @@ -5,18 +5,27 @@ loadEnv(); const parseBoolean = (value: string): boolean => { const normalized = value.trim().toLowerCase(); - return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on"; + return ( + normalized === "true" || + normalized === "1" || + normalized === "yes" || + normalized === "on" + ); }; const envSchema = z.object({ - NODE_ENV: z.enum(["development", "test", "production"]).default("development"), + NODE_ENV: z + .enum(["development", "test", "production"]) + .default("development"), PORT: z.coerce.number().int().positive().default(4000), DATABASE_URL: z.string().url("DATABASE_URL must be a valid URL"), DATABASE_SSL: z.string().default("false").transform(parseBoolean), REDIS_URL: z.string().url("REDIS_URL must be a valid URL"), DISCORD_CLIENT_ID: z.string().min(1, "DISCORD_CLIENT_ID is required"), DISCORD_CLIENT_SECRET: z.string().min(1, "DISCORD_CLIENT_SECRET is required"), - DISCORD_REDIRECT_URI: z.string().url("DISCORD_REDIRECT_URI must be a valid URL"), + DISCORD_REDIRECT_URI: z + .string() + .url("DISCORD_REDIRECT_URI must be a valid URL"), WEB_URL: z.string().url("WEB_URL must be a valid URL"), API_BASE_URL: z.string().url("API_BASE_URL must be a valid URL"), JWT_SECRET: z.string().min(32, "JWT_SECRET must be at least 32 characters"), @@ -25,7 +34,11 @@ const envSchema = z.object({ COOKIE_SECURE: z.string().default("false").transform(parseBoolean), TOKEN_ENCRYPTION_KEY: z.string().min(10, "TOKEN_ENCRYPTION_KEY is required"), TENANT_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(120), - TENANT_RATE_LIMIT_WINDOW_SECONDS: z.coerce.number().int().positive().default(60), + TENANT_RATE_LIMIT_WINDOW_SECONDS: z.coerce + .number() + .int() + .positive() + .default(60), }); export const env = envSchema.parse(process.env); diff --git a/apps/api/src/db/pool.ts b/apps/api/src/db/pool.ts index a04cf42..b67b475 100644 --- a/apps/api/src/db/pool.ts +++ b/apps/api/src/db/pool.ts @@ -7,8 +7,8 @@ export const createPgPool = (): Pool => { connectionString: env.DATABASE_URL, ssl: env.DATABASE_SSL ? { - rejectUnauthorized: true, - } + rejectUnauthorized: true, + } : undefined, }); }; diff --git a/apps/api/src/db/repositories.ts b/apps/api/src/db/repositories.ts index 9c53f70..615506b 100644 --- a/apps/api/src/db/repositories.ts +++ b/apps/api/src/db/repositories.ts @@ -151,7 +151,10 @@ export const getUserByIdAndTenant = async ( return mapUser(result.rows[0] as Record); }; -export const listBotsForTenant = async (pool: Pool, tenantId: string): Promise => { +export const listBotsForTenant = async ( + pool: Pool, + tenantId: string, +): Promise => { const result = await pool.query( ` SELECT id, tenant_id, discord_bot_id, display_name, status, last_error, created_at, updated_at @@ -206,7 +209,10 @@ export const createOrUpdateBotForTenant = async ( [input.discordBotId], ); - if (existingByDiscordBot.rows[0] && existingByDiscordBot.rows[0].tenant_id !== input.tenantId) { + if ( + existingByDiscordBot.rows[0] && + existingByDiscordBot.rows[0].tenant_id !== input.tenantId + ) { throw new Error("BOT_ALREADY_CLAIMED"); } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 537cc11..3c6c1a8 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -56,9 +56,15 @@ const bootstrap = async (): Promise => { return; } - const user = await getUserByIdAndTenant(pgPool, req.auth.userId, req.auth.tenantId); + const user = await getUserByIdAndTenant( + pgPool, + req.auth.userId, + req.auth.tenantId, + ); if (!user) { - res.status(401).json({ error: "Session does not match an existing user" }); + res + .status(401) + .json({ error: "Session does not match an existing user" }); return; } @@ -93,12 +99,10 @@ const bootstrap = async (): Promise => { ); const server = app.listen(env.PORT, () => { - // eslint-disable-next-line no-console console.log(`[api] listening on :${env.PORT}`); }); const shutdown = async (signal: string): Promise => { - // eslint-disable-next-line no-console console.log(`[api] shutdown requested (${signal})`); server.close(async () => { @@ -119,7 +123,6 @@ const bootstrap = async (): Promise => { }; bootstrap().catch((error) => { - // eslint-disable-next-line no-console console.error("[api] fatal startup error", error); process.exit(1); }); diff --git a/apps/api/src/routes/authRoutes.ts b/apps/api/src/routes/authRoutes.ts index 8e1cb5f..fbfe112 100644 --- a/apps/api/src/routes/authRoutes.ts +++ b/apps/api/src/routes/authRoutes.ts @@ -2,7 +2,11 @@ import type { Router } from "express"; import { Router as expressRouter } from "express"; import type { Pool } from "pg"; -import { buildDiscordLoginUrl, createOauthState, exchangeCodeForDiscordIdentity } from "../auth/discordOAuth.js"; +import { + buildDiscordLoginUrl, + createOauthState, + exchangeCodeForDiscordIdentity, +} from "../auth/discordOAuth.js"; import { issueSessionToken } from "../auth/jwt.js"; import { env } from "../config/env.js"; import { upsertUserFromDiscord } from "../db/repositories.js"; @@ -46,7 +50,9 @@ export const createAuthRouter = ({ pool }: AuthRouterDependencies): Router => { const cookieState = req.cookies?.[OAUTH_STATE_COOKIE_NAME]; if (!code || !state || !cookieState || state !== cookieState) { - res.clearCookie(OAUTH_STATE_COOKIE_NAME, { path: "/auth/discord/callback" }); + res.clearCookie(OAUTH_STATE_COOKIE_NAME, { + path: "/auth/discord/callback", + }); res.redirect(`${env.WEB_URL}/login?error=oauth_state_invalid`); return; } @@ -66,11 +72,15 @@ export const createAuthRouter = ({ pool }: AuthRouterDependencies): Router => { username: user.username, }); - res.clearCookie(OAUTH_STATE_COOKIE_NAME, { path: "/auth/discord/callback" }); + res.clearCookie(OAUTH_STATE_COOKIE_NAME, { + path: "/auth/discord/callback", + }); res.cookie(env.SESSION_COOKIE_NAME, sessionToken, sessionCookieConfig); res.redirect(`${env.WEB_URL}/dashboard`); } catch { - res.clearCookie(OAUTH_STATE_COOKIE_NAME, { path: "/auth/discord/callback" }); + res.clearCookie(OAUTH_STATE_COOKIE_NAME, { + path: "/auth/discord/callback", + }); res.redirect(`${env.WEB_URL}/login?error=oauth_failed`); } }); diff --git a/apps/api/src/routes/botRoutes.ts b/apps/api/src/routes/botRoutes.ts index d88cd48..45c9a92 100644 --- a/apps/api/src/routes/botRoutes.ts +++ b/apps/api/src/routes/botRoutes.ts @@ -50,13 +50,23 @@ const queueBotAction = ( return; } - const bot = await getBotForTenant(deps.pool, req.auth.tenantId, parsedParams.data.botId); + const bot = await getBotForTenant( + deps.pool, + req.auth.tenantId, + parsedParams.data.botId, + ); if (!bot) { res.status(404).json({ error: "Bot not found" }); return; } - await setBotStatusForTenant(deps.pool, req.auth.tenantId, parsedParams.data.botId, statusDuringAction, null); + await setBotStatusForTenant( + deps.pool, + req.auth.tenantId, + parsedParams.data.botId, + statusDuringAction, + null, + ); await insertBotRuntimeEvent(deps.pool, { tenantId: req.auth.tenantId, @@ -105,13 +115,18 @@ export const createBotRouter = (deps: BotRouterDependencies): Router => { const parsedBody = addBotSchema.safeParse(req.body); if (!parsedBody.success) { - res.status(400).json({ error: parsedBody.error.issues[0]?.message ?? "Invalid body" }); + res + .status(400) + .json({ error: parsedBody.error.issues[0]?.message ?? "Invalid body" }); return; } try { const botIdentity = await validateDiscordBotToken(parsedBody.data.token); - const encryptedToken = encryptToken(parsedBody.data.token, deps.tokenEncryptionKey); + const encryptedToken = encryptToken( + parsedBody.data.token, + deps.tokenEncryptionKey, + ); const bot = await createOrUpdateBotForTenant(deps.pool, { tenantId: req.auth.tenantId, @@ -135,11 +150,16 @@ export const createBotRouter = (deps: BotRouterDependencies): Router => { res.status(201).json({ bot }); } catch (error) { if (error instanceof Error && error.message === "BOT_ALREADY_CLAIMED") { - res.status(409).json({ error: "This Discord bot is already claimed by another tenant" }); + res.status(409).json({ + error: "This Discord bot is already claimed by another tenant", + }); return; } - if (error instanceof Error && error.message === "Invalid Discord bot token") { + if ( + error instanceof Error && + error.message === "Invalid Discord bot token" + ) { res.status(400).json({ error: error.message }); return; } @@ -148,9 +168,21 @@ export const createBotRouter = (deps: BotRouterDependencies): Router => { } }); - router.post("/:botId/start", deps.tenantControlRateLimit, queueBotAction("start", "starting", deps)); - router.post("/:botId/stop", deps.tenantControlRateLimit, queueBotAction("stop", "stopping", deps)); - router.post("/:botId/restart", deps.tenantControlRateLimit, queueBotAction("restart", "starting", deps)); + router.post( + "/:botId/start", + deps.tenantControlRateLimit, + queueBotAction("start", "starting", deps), + ); + router.post( + "/:botId/stop", + deps.tenantControlRateLimit, + queueBotAction("stop", "stopping", deps), + ); + router.post( + "/:botId/restart", + deps.tenantControlRateLimit, + queueBotAction("restart", "starting", deps), + ); return router; }; diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index b0f4e56..1df6b45 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -4,7 +4,10 @@ "composite": true, "rootDir": "src", "outDir": "dist", - "types": ["node"] + "types": ["node"], + "declaration": true, + "noUnusedLocals": true, + "noUnusedParameters": true }, "include": ["src/**/*.ts"] } diff --git a/apps/bot/locales/ar.json b/apps/bot/locales/ar.json index 5a0802e..cbfc328 100644 --- a/apps/bot/locales/ar.json +++ b/apps/bot/locales/ar.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/bn.json b/apps/bot/locales/bn.json index 39061fb..8c203c3 100644 --- a/apps/bot/locales/bn.json +++ b/apps/bot/locales/bn.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/de.json b/apps/bot/locales/de.json index 3bac467..38e094e 100644 --- a/apps/bot/locales/de.json +++ b/apps/bot/locales/de.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/en.json b/apps/bot/locales/en.json index e27fdbe..66e5818 100644 --- a/apps/bot/locales/en.json +++ b/apps/bot/locales/en.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/es.json b/apps/bot/locales/es.json index 47c126a..f09768a 100644 --- a/apps/bot/locales/es.json +++ b/apps/bot/locales/es.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/fr.json b/apps/bot/locales/fr.json index 1c889dd..48aaa69 100644 --- a/apps/bot/locales/fr.json +++ b/apps/bot/locales/fr.json @@ -465,4 +465,4 @@ "inviter": "inviteur={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/hi.json b/apps/bot/locales/hi.json index a116f8b..51debc9 100644 --- a/apps/bot/locales/hi.json +++ b/apps/bot/locales/hi.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/id.json b/apps/bot/locales/id.json index 8daaaeb..5bb5de2 100644 --- a/apps/bot/locales/id.json +++ b/apps/bot/locales/id.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/it.json b/apps/bot/locales/it.json index 00181c7..82040b7 100644 --- a/apps/bot/locales/it.json +++ b/apps/bot/locales/it.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/ja.json b/apps/bot/locales/ja.json index 847ecd6..52f33a6 100644 --- a/apps/bot/locales/ja.json +++ b/apps/bot/locales/ja.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/nl.json b/apps/bot/locales/nl.json index 80aa294..1c6ba54 100644 --- a/apps/bot/locales/nl.json +++ b/apps/bot/locales/nl.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/pl.json b/apps/bot/locales/pl.json index e43fb63..55a3daa 100644 --- a/apps/bot/locales/pl.json +++ b/apps/bot/locales/pl.json @@ -465,4 +465,4 @@ "inviter": "zapraszający={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/pt.json b/apps/bot/locales/pt.json index 2e38c53..f8362a1 100644 --- a/apps/bot/locales/pt.json +++ b/apps/bot/locales/pt.json @@ -465,4 +465,4 @@ "inviter": "convidador={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/ru.json b/apps/bot/locales/ru.json index 78aad79..a5f219d 100644 --- a/apps/bot/locales/ru.json +++ b/apps/bot/locales/ru.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/tr.json b/apps/bot/locales/tr.json index 065419c..bfc9f5f 100644 --- a/apps/bot/locales/tr.json +++ b/apps/bot/locales/tr.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/locales/zh.json b/apps/bot/locales/zh.json index 0fa2734..8157529 100644 --- a/apps/bot/locales/zh.json +++ b/apps/bot/locales/zh.json @@ -465,4 +465,4 @@ "inviter": "inviter={{value}}" } } -} \ No newline at end of file +} diff --git a/apps/bot/scripts/copyLegacyLocales.mjs b/apps/bot/scripts/copyLegacyLocales.mjs index b4a56b0..ddccbd1 100644 --- a/apps/bot/scripts/copyLegacyLocales.mjs +++ b/apps/bot/scripts/copyLegacyLocales.mjs @@ -6,24 +6,27 @@ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const BOT_DIR = path.resolve(SCRIPT_DIR, ".."); const SOURCE_DIR = path.join(BOT_DIR, "locales"); const TARGET_DIRECTORIES = [ - path.join(BOT_DIR, "dist", "locales"), - path.join(BOT_DIR, "dist", "legacy", "i18n"), + path.join(BOT_DIR, "dist", "locales"), + path.join(BOT_DIR, "dist", "legacy", "i18n"), ]; if (!existsSync(SOURCE_DIR)) { - throw new Error(`[i18n] source locale directory not found: ${SOURCE_DIR}`); + throw new Error(`[i18n] source locale directory not found: ${SOURCE_DIR}`); } for (const targetDirectory of TARGET_DIRECTORIES) { - mkdirSync(targetDirectory, { recursive: true }); + mkdirSync(targetDirectory, { recursive: true }); } for (const fileName of readdirSync(SOURCE_DIR)) { - if (!fileName.endsWith(".json")) { - continue; - } + if (!fileName.endsWith(".json")) { + continue; + } - for (const targetDirectory of TARGET_DIRECTORIES) { - copyFileSync(path.join(SOURCE_DIR, fileName), path.join(targetDirectory, fileName)); - } -} \ No newline at end of file + for (const targetDirectory of TARGET_DIRECTORIES) { + copyFileSync( + path.join(SOURCE_DIR, fileName), + path.join(targetDirectory, fileName), + ); + } +} diff --git a/apps/bot/src/config/env.ts b/apps/bot/src/config/env.ts index e79a591..dd6c0b4 100644 --- a/apps/bot/src/config/env.ts +++ b/apps/bot/src/config/env.ts @@ -7,7 +7,12 @@ loadEnv(); const parseBoolean = (value: string): boolean => { const normalized = value.trim().toLowerCase(); - return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on"; + return ( + normalized === "true" || + normalized === "1" || + normalized === "yes" || + normalized === "on" + ); }; const optionalString = (value?: string): string | undefined => { @@ -33,7 +38,9 @@ const parseOptionalUrl = (value: unknown): string | undefined => { }; const envSchema = z.object({ - NODE_ENV: z.enum(["development", "test", "production"]).default("development"), + NODE_ENV: z + .enum(["development", "test", "production"]) + .default("development"), PORT: z.coerce.number().int().positive().default(4100), DATABASE_URL: z.string().url("DATABASE_URL must be a valid URL"), DATABASE_SSL: z.string().default("false").transform(parseBoolean), @@ -58,7 +65,10 @@ const envSchema = z.object({ .optional() .default("false") .transform(parseBoolean), - REDIS_URL: z.preprocess(parseOptionalUrl, z.string().url("REDIS_URL must be a valid URL").optional()), + REDIS_URL: z.preprocess( + parseOptionalUrl, + z.string().url("REDIS_URL must be a valid URL").optional(), + ), TOKEN_ENCRYPTION_KEY: z.string().min(10, "TOKEN_ENCRYPTION_KEY is required"), PRESENCE_STREAM_URL: z .string() @@ -67,7 +77,10 @@ const envSchema = z.object({ .default("https://twitch.tv/discord"), PREFIX: z.string().min(1).max(5).default("+"), DEFAULT_LANG: z.enum(SUPPORTED_LANGS).default("en"), - DEV_GUILD_ID: z.string().optional().transform((value) => value && value.length > 0 ? value : undefined), + DEV_GUILD_ID: z + .string() + .optional() + .transform((value) => (value && value.length > 0 ? value : undefined)), AUTO_DEPLOY_SLASH: z .string() .optional() @@ -76,9 +89,21 @@ const envSchema = z.object({ LOG_LEVEL: z.string().trim().min(1).default("info"), STATE_BACKEND: z.enum(["memory", "redis"]).default("memory"), COMMAND_DISPATCH_MODE: z.enum(["local", "worker"]).default("local"), - COMMAND_QUEUE_NAME: z.string().trim().min(1).default("bot:${botId}:command-jobs"), - GLOBAL_RATE_LIMIT_MAX_REQUESTS: z.coerce.number().int().positive().default(20), - GLOBAL_RATE_LIMIT_WINDOW_SECONDS: z.coerce.number().int().positive().default(10), + COMMAND_QUEUE_NAME: z + .string() + .trim() + .min(1) + .default("bot:${botId}:command-jobs"), + GLOBAL_RATE_LIMIT_MAX_REQUESTS: z.coerce + .number() + .int() + .positive() + .default(20), + GLOBAL_RATE_LIMIT_WINDOW_SECONDS: z.coerce + .number() + .int() + .positive() + .default(10), RATE_LIMIT_FAIL_OPEN: z .string() .optional() @@ -93,7 +118,11 @@ const envSchema = z.object({ const parsed = envSchema.parse(process.env); -if (parsed.DATABASE_SSL && !parsed.DATABASE_SSL_REJECT_UNAUTHORIZED && !parsed.ALLOW_INSECURE_DB_SSL) { +if ( + parsed.DATABASE_SSL && + !parsed.DATABASE_SSL_REJECT_UNAUTHORIZED && + !parsed.ALLOW_INSECURE_DB_SSL +) { throw new Error( "Insecure DATABASE_SSL configuration detected: rejectUnauthorized=false is blocked by default. Set DATABASE_SSL_REJECT_UNAUTHORIZED=true or explicitly set ALLOW_INSECURE_DB_SSL=true for non-production environments.", ); diff --git a/apps/bot/src/db/pool.ts b/apps/bot/src/db/pool.ts index f7fac61..d439698 100644 --- a/apps/bot/src/db/pool.ts +++ b/apps/bot/src/db/pool.ts @@ -5,9 +5,9 @@ import { env } from "../config/env.js"; export const createPgPool = (): Pool => { const ssl = env.DATABASE_SSL ? { - rejectUnauthorized: env.DATABASE_SSL_REJECT_UNAUTHORIZED, - ca: env.DATABASE_SSL_CA, - } + rejectUnauthorized: env.DATABASE_SSL_REJECT_UNAUTHORIZED, + ca: env.DATABASE_SSL_CA, + } : undefined; return new Pool({ diff --git a/apps/bot/src/db/repositories.ts b/apps/bot/src/db/repositories.ts index 9ee570e..4e6b802 100644 --- a/apps/bot/src/db/repositories.ts +++ b/apps/bot/src/db/repositories.ts @@ -13,7 +13,9 @@ export interface StoredBotCredentials { status: BotStatus; } -const mapBotCredentials = (row: Record): StoredBotCredentials => { +const mapBotCredentials = ( + row: Record, +): StoredBotCredentials => { return { id: String(row.id), tenantId: String(row.tenant_id), @@ -27,7 +29,9 @@ const mapBotCredentials = (row: Record): StoredBotCredentials = }; }; -export const listBotsToRestore = async (pool: Pool): Promise => { +export const listBotsToRestore = async ( + pool: Pool, +): Promise => { const result = await pool.query( ` SELECT id, tenant_id, owner_user_id, discord_bot_id, display_name, token_ciphertext, token_iv, token_tag, status @@ -37,7 +41,9 @@ export const listBotsToRestore = async (pool: Pool): Promise mapBotCredentials(row as Record)); + return result.rows.map((row) => + mapBotCredentials(row as Record), + ); }; export const getBotCredentialsById = async ( diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts index f239c9e..fed487a 100644 --- a/apps/bot/src/index.ts +++ b/apps/bot/src/index.ts @@ -13,9 +13,9 @@ const bootstrap = async (): Promise => { const redis = env.REDIS_URL ? new Redis(env.REDIS_URL, { - maxRetriesPerRequest: null, - enableReadyCheck: true, - }) + maxRetriesPerRequest: null, + enableReadyCheck: true, + }) : null; await pgPool.query("SELECT 1"); @@ -23,24 +23,33 @@ const bootstrap = async (): Promise => { await redis.ping(); } - const manager = new BotManager(pgPool, parseTokenEncryptionKey(env.TOKEN_ENCRYPTION_KEY)); + const manager = new BotManager( + pgPool, + parseTokenEncryptionKey(env.TOKEN_ENCRYPTION_KEY), + ); await manager.loadAndStartPersistedBots(); - const worker = redis ? createBotControlWorker(redis.duplicate(), manager) : null; + const worker = redis + ? createBotControlWorker(redis.duplicate(), manager) + : null; if (worker) { worker.on("completed", (job) => { - // eslint-disable-next-line no-console - console.log(`[bot] completed ${job.data.action} for bot ${job.data.botId}`); + console.log( + `[bot] completed ${job.data.action} for bot ${job.data.botId}`, + ); }); worker.on("failed", (job, error) => { - // eslint-disable-next-line no-console - console.error(`[bot] failed ${job?.data.action ?? "unknown"} for bot ${job?.data.botId ?? "unknown"}`, error); + console.error( + `[bot] failed ${job?.data.action ?? "unknown"} for bot ${job?.data.botId ?? "unknown"}`, + error, + ); }); } else { - // eslint-disable-next-line no-console - console.warn("[bot] REDIS_URL is not configured, bot control queue worker is disabled"); + console.warn( + "[bot] REDIS_URL is not configured, bot control queue worker is disabled", + ); } const healthServer = createServer((req, res) => { @@ -65,12 +74,10 @@ const bootstrap = async (): Promise => { }); healthServer.listen(env.PORT, () => { - // eslint-disable-next-line no-console console.log(`[bot] health endpoint available on :${env.PORT}`); }); const shutdown = async (signal: string): Promise => { - // eslint-disable-next-line no-console console.log(`[bot] shutdown requested (${signal})`); healthServer.close(async () => { @@ -92,7 +99,6 @@ const bootstrap = async (): Promise => { }; bootstrap().catch((error) => { - // eslint-disable-next-line no-console console.error("[bot] fatal startup error", error); process.exit(1); }); diff --git a/apps/bot/src/legacy/app/bootstrap.ts b/apps/bot/src/legacy/app/bootstrap.ts index 5a60896..2091d38 100644 --- a/apps/bot/src/legacy/app/bootstrap.ts +++ b/apps/bot/src/legacy/app/bootstrap.ts @@ -32,20 +32,16 @@ import { registerEvents } from "../events/index.js"; import { createPrefixHandler } from "../handlers/prefixHandler.js"; import { createSlashHandler } from "../handlers/slashHandler.js"; import { I18nService } from "../i18n/index.js"; -import { - LogEventService, -} from "../modules/logs/index.js"; -import { - MemberMessageService, -} from "../modules/memberMessages/index.js"; -import { - PresenceService, -} from "../modules/presence/index.js"; +import { LogEventService } from "../modules/logs/index.js"; +import { MemberMessageService } from "../modules/memberMessages/index.js"; +import { PresenceService } from "../modules/presence/index.js"; const SHUTDOWN_TIMEOUT_MS = 10_000; const log = createScopedLogger("bootstrap"); -const bindGracefulShutdown = (shutdown: (signal: string) => Promise): void => { +const bindGracefulShutdown = ( + shutdown: (signal: string) => Promise, +): void => { process.once("SIGINT", () => { void shutdown("SIGINT"); }); @@ -56,7 +52,11 @@ const bindGracefulShutdown = (shutdown: (signal: string) => Promise): void }; const bindFatalProcessHandlers = ( - shutdown: (signal: string, exitCode?: number, error?: unknown) => Promise, + shutdown: ( + signal: string, + exitCode?: number, + error?: unknown, + ) => Promise, ): void => { process.once("uncaughtException", (error) => { log.error({ err: error }, "process uncaught exception"); @@ -72,16 +72,18 @@ const bindFatalProcessHandlers = ( export const bootstrap = async (): Promise => { const dispatchMode: CommandDispatchMode = env.COMMAND_DISPATCH_MODE; if (dispatchMode === "worker") { - throw new Error("Worker mode is not implemented and must not be used in production"); + throw new Error( + "Worker mode is not implemented and must not be used in production", + ); } const pool = new Pool({ connectionString: env.DATABASE_URL, ssl: env.DATABASE_SSL ? { - rejectUnauthorized: env.DATABASE_SSL_REJECT_UNAUTHORIZED, - ...(env.DATABASE_SSL_CA ? { ca: env.DATABASE_SSL_CA } : {}), - } + rejectUnauthorized: env.DATABASE_SSL_REJECT_UNAUTHORIZED, + ...(env.DATABASE_SSL_CA ? { ca: env.DATABASE_SSL_CA } : {}), + } : undefined, }); @@ -129,18 +131,23 @@ export const bootstrap = async (): Promise => { ); const services: AppFeatureServices = { - presenceService: new PresenceService(presenceStore, env.PRESENCE_STREAM_URL), + presenceService: new PresenceService( + presenceStore, + env.PRESENCE_STREAM_URL, + ), memberMessageService: new MemberMessageService(memberMessageStore), logEventService: new LogEventService(logEventStore), }; - const cooldownStore = env.STATE_BACKEND === "redis" && redis - ? new RedisCooldownStore(redis) - : new MemoryCooldownStore(); + const cooldownStore = + env.STATE_BACKEND === "redis" && redis + ? new RedisCooldownStore(redis) + : new MemoryCooldownStore(); - const globalRateLimitStore = env.STATE_BACKEND === "redis" && redis - ? new RedisGlobalRateLimitStore(redis) - : new MemoryGlobalRateLimitStore(); + const globalRateLimitStore = + env.STATE_BACKEND === "redis" && redis + ? new RedisGlobalRateLimitStore(redis) + : new MemoryGlobalRateLimitStore(); const executor = new CommandExecutor({ cooldownStore, @@ -162,7 +169,11 @@ export const bootstrap = async (): Promise => { let shuttingDown = false; let client: Client | null = null; - const shutdown = async (signal: string, exitCode = 0, error?: unknown): Promise => { + const shutdown = async ( + signal: string, + exitCode = 0, + error?: unknown, + ): Promise => { if (shuttingDown) { return; } @@ -176,7 +187,10 @@ export const bootstrap = async (): Promise => { const forcedExitCode = exitCode === 0 ? 1 : exitCode; const forceExitTimer = setTimeout(() => { - log.error({ signal, forcedExitCode, timeoutMs: SHUTDOWN_TIMEOUT_MS }, "forcing process exit"); + log.error( + { signal, forcedExitCode, timeoutMs: SHUTDOWN_TIMEOUT_MS }, + "forcing process exit", + ); process.exit(forcedExitCode); }, SHUTDOWN_TIMEOUT_MS); @@ -224,7 +238,10 @@ export const bootstrap = async (): Promise => { }); const i18n = new I18nService(env.DEFAULT_LANG); - const registry = new CommandRegistry(createCommandList(services, i18n), i18n); + const registry = new CommandRegistry( + createCommandList(services, i18n), + i18n, + ); const onPrefixMessage = createPrefixHandler({ registry, diff --git a/apps/bot/src/legacy/app/container.ts b/apps/bot/src/legacy/app/container.ts index 55e0221..34d1823 100644 --- a/apps/bot/src/legacy/app/container.ts +++ b/apps/bot/src/legacy/app/container.ts @@ -1,12 +1,6 @@ -import type { - MemberMessageService, -} from "../modules/memberMessages/index.js"; -import type { - LogEventService, -} from "../modules/logs/index.js"; -import type { - PresenceService, -} from "../modules/presence/index.js"; +import type { MemberMessageService } from "../modules/memberMessages/index.js"; +import type { LogEventService } from "../modules/logs/index.js"; +import type { PresenceService } from "../modules/presence/index.js"; export interface AppFeatureServices { presenceService: PresenceService; diff --git a/apps/bot/src/legacy/commands/goodbye.ts b/apps/bot/src/legacy/commands/goodbye.ts index 097dfbb..ceb851e 100644 --- a/apps/bot/src/legacy/commands/goodbye.ts +++ b/apps/bot/src/legacy/commands/goodbye.ts @@ -14,18 +14,26 @@ import { } from "../modules/memberMessages/index.js"; /** Commande `goodbye` — ouvre le panneau de configuration des messages 'goodbye'. */ -export const createGoodbyeCommand = (memberMessageService: MemberMessageService, i18n: I18nService) => defineCommand({ - meta: { - name: "goodbye", - category: "utility", - }, - permissions: [PermissionFlagsBits.ManageGuild], - sensitive: true, - examples: [ - { - source: "slash", - descriptionKey: "examples.slash", +export const createGoodbyeCommand = ( + memberMessageService: MemberMessageService, + i18n: I18nService, +) => + defineCommand({ + meta: { + name: "goodbye", + category: "utility", }, - ], - execute: createMemberMessagePanelExecute("goodbye", memberMessageService, i18n), -}); + permissions: [PermissionFlagsBits.ManageGuild], + sensitive: true, + examples: [ + { + source: "slash", + descriptionKey: "examples.slash", + }, + ], + execute: createMemberMessagePanelExecute( + "goodbye", + memberMessageService, + i18n, + ), + }); diff --git a/apps/bot/src/legacy/commands/index.ts b/apps/bot/src/legacy/commands/index.ts index 6d2146b..2fdfa4c 100644 --- a/apps/bot/src/legacy/commands/index.ts +++ b/apps/bot/src/legacy/commands/index.ts @@ -17,7 +17,10 @@ import type { I18nService } from "../i18n/index.js"; import type { BotCommand } from "../types/command.js"; /** CommandList: tableau ordonné des commandes disponibles. */ -export const createCommandList = (services: AppFeatureServices, i18n: I18nService): BotCommand[] => [ +export const createCommandList = ( + services: AppFeatureServices, + i18n: I18nService, +): BotCommand[] => [ kissCommand, pingCommand, createWelcomeCommand(services.memberMessageService, i18n), diff --git a/apps/bot/src/legacy/commands/kiss.ts b/apps/bot/src/legacy/commands/kiss.ts index eff2e16..5f632df 100644 --- a/apps/bot/src/legacy/commands/kiss.ts +++ b/apps/bot/src/legacy/commands/kiss.ts @@ -29,7 +29,9 @@ export const kissCommand = defineCommand({ execute: async (ctx) => { const target = ctx.args.user; if (!target || typeof target !== "object" || !("id" in target)) { - await ctx.reply(ctx.t("errors.args.invalidUser", { value: String(target) })); + await ctx.reply( + ctx.t("errors.args.invalidUser", { value: String(target) }), + ); return; } diff --git a/apps/bot/src/legacy/commands/logs.ts b/apps/bot/src/legacy/commands/logs.ts index b51d13a..41e118e 100644 --- a/apps/bot/src/legacy/commands/logs.ts +++ b/apps/bot/src/legacy/commands/logs.ts @@ -6,18 +6,19 @@ import { type LogEventService, } from "../modules/logs/index.js"; -export const createLogsCommand = (logEventService: LogEventService) => defineCommand({ - meta: { - name: "logs", - category: "utility", - }, - permissions: [PermissionFlagsBits.ManageGuild], - sensitive: true, - examples: [ - { - source: "slash", - descriptionKey: "examples.slash", +export const createLogsCommand = (logEventService: LogEventService) => + defineCommand({ + meta: { + name: "logs", + category: "utility", }, - ], - execute: createLogsCommandExecute(logEventService), -}); \ No newline at end of file + permissions: [PermissionFlagsBits.ManageGuild], + sensitive: true, + examples: [ + { + source: "slash", + descriptionKey: "examples.slash", + }, + ], + execute: createLogsCommandExecute(logEventService), + }); diff --git a/apps/bot/src/legacy/commands/presence.ts b/apps/bot/src/legacy/commands/presence.ts index 753397d..a8265f0 100644 --- a/apps/bot/src/legacy/commands/presence.ts +++ b/apps/bot/src/legacy/commands/presence.ts @@ -6,18 +6,19 @@ import { type PresenceService, } from "../modules/presence/index.js"; -export const createPresenceCommand = (presenceService: PresenceService) => defineCommand({ - meta: { - name: "presence", - category: "utility", - }, - permissions: [PermissionFlagsBits.ManageGuild], - sensitive: true, - examples: [ - { - source: "slash", - descriptionKey: "examples.slash", +export const createPresenceCommand = (presenceService: PresenceService) => + defineCommand({ + meta: { + name: "presence", + category: "utility", }, - ], - execute: createPresenceCommandExecute(presenceService), -}); + permissions: [PermissionFlagsBits.ManageGuild], + sensitive: true, + examples: [ + { + source: "slash", + descriptionKey: "examples.slash", + }, + ], + execute: createPresenceCommandExecute(presenceService), + }); diff --git a/apps/bot/src/legacy/commands/welcome.ts b/apps/bot/src/legacy/commands/welcome.ts index a28451e..92a09c6 100644 --- a/apps/bot/src/legacy/commands/welcome.ts +++ b/apps/bot/src/legacy/commands/welcome.ts @@ -14,18 +14,26 @@ import { } from "../modules/memberMessages/index.js"; /** Commande `welcome` — ouvre le panneau de configuration des messages 'welcome'. */ -export const createWelcomeCommand = (memberMessageService: MemberMessageService, i18n: I18nService) => defineCommand({ - meta: { - name: "welcome", - category: "utility", - }, - permissions: [PermissionFlagsBits.ManageGuild], - sensitive: true, - examples: [ - { - source: "slash", - descriptionKey: "examples.slash", +export const createWelcomeCommand = ( + memberMessageService: MemberMessageService, + i18n: I18nService, +) => + defineCommand({ + meta: { + name: "welcome", + category: "utility", }, - ], - execute: createMemberMessagePanelExecute("welcome", memberMessageService, i18n), -}); + permissions: [PermissionFlagsBits.ManageGuild], + sensitive: true, + examples: [ + { + source: "slash", + descriptionKey: "examples.slash", + }, + ], + execute: createMemberMessagePanelExecute( + "welcome", + memberMessageService, + i18n, + ), + }); diff --git a/apps/bot/src/legacy/config/env.ts b/apps/bot/src/legacy/config/env.ts index 0043aa7..7ba08ff 100644 --- a/apps/bot/src/legacy/config/env.ts +++ b/apps/bot/src/legacy/config/env.ts @@ -7,7 +7,12 @@ loadEnv(); const toBoolean = (value: string): boolean => { const normalized = value.trim().toLowerCase(); - return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on"; + return ( + normalized === "true" || + normalized === "1" || + normalized === "yes" || + normalized === "on" + ); }; const optionalString = (value?: string): string | undefined => { @@ -40,11 +45,7 @@ const envSchema = z.object({ .trim() .min(1, "DATABASE_URL is required") .url("DATABASE_URL must be a valid URL"), - DATABASE_SSL: z - .string() - .optional() - .default("false") - .transform(toBoolean), + DATABASE_SSL: z.string().optional().default("false").transform(toBoolean), DATABASE_SSL_REJECT_UNAUTHORIZED: z .string() .optional() @@ -73,7 +74,10 @@ const envSchema = z.object({ .default("https://twitch.tv/discord"), PREFIX: z.string().min(1).max(5).default("+"), DEFAULT_LANG: z.enum(SUPPORTED_LANGS).default("en"), - DEV_GUILD_ID: z.string().optional().transform((value) => value && value.length > 0 ? value : undefined), + DEV_GUILD_ID: z + .string() + .optional() + .transform((value) => (value && value.length > 0 ? value : undefined)), AUTO_DEPLOY_SLASH: z .string() .optional() @@ -81,11 +85,26 @@ const envSchema = z.object({ .transform(toBoolean), LOG_LEVEL: z.string().trim().min(1).default("info"), STATE_BACKEND: z.enum(["memory", "redis"]).default("memory"), - REDIS_URL: z.preprocess(parseOptionalUrl, z.string().url("REDIS_URL must be a valid URL").optional()), + REDIS_URL: z.preprocess( + parseOptionalUrl, + z.string().url("REDIS_URL must be a valid URL").optional(), + ), COMMAND_DISPATCH_MODE: z.enum(["local", "worker"]).default("local"), - COMMAND_QUEUE_NAME: z.string().trim().min(1).default("bot:${botId}:command-jobs"), - GLOBAL_RATE_LIMIT_MAX_REQUESTS: z.coerce.number().int().positive().default(20), - GLOBAL_RATE_LIMIT_WINDOW_SECONDS: z.coerce.number().int().positive().default(10), + COMMAND_QUEUE_NAME: z + .string() + .trim() + .min(1) + .default("bot:${botId}:command-jobs"), + GLOBAL_RATE_LIMIT_MAX_REQUESTS: z.coerce + .number() + .int() + .positive() + .default(20), + GLOBAL_RATE_LIMIT_WINDOW_SECONDS: z.coerce + .number() + .int() + .positive() + .default(10), RATE_LIMIT_FAIL_OPEN: z .string() .optional() @@ -100,16 +119,18 @@ const envSchema = z.object({ const parsed = envSchema.parse(process.env); -if (parsed.DATABASE_SSL && !parsed.DATABASE_SSL_REJECT_UNAUTHORIZED && !parsed.ALLOW_INSECURE_DB_SSL) { +if ( + parsed.DATABASE_SSL && + !parsed.DATABASE_SSL_REJECT_UNAUTHORIZED && + !parsed.ALLOW_INSECURE_DB_SSL +) { throw new Error( "Insecure DATABASE_SSL configuration detected: rejectUnauthorized=false is blocked by default. Set DATABASE_SSL_REJECT_UNAUTHORIZED=true or explicitly set ALLOW_INSECURE_DB_SSL=true for non-production environments.", ); } if (parsed.STATE_BACKEND === "redis" && !parsed.REDIS_URL) { - throw new Error( - "REDIS_URL is required when STATE_BACKEND=redis.", - ); + throw new Error("REDIS_URL is required when STATE_BACKEND=redis."); } export const env = parsed; diff --git a/apps/bot/src/legacy/core/commands/argParser.ts b/apps/bot/src/legacy/core/commands/argParser.ts index 35d0fc3..46cc04c 100644 --- a/apps/bot/src/legacy/core/commands/argParser.ts +++ b/apps/bot/src/legacy/core/commands/argParser.ts @@ -1,6 +1,13 @@ -import type { ChatInputCommandInteraction, GuildBasedChannel, Message } from "discord.js"; +import type { + ChatInputCommandInteraction, + GuildBasedChannel, + Message, +} from "discord.js"; -import type { ArgumentParseError, ParsedArgumentsResult } from "../../types/argParser.js"; +import type { + ArgumentParseError, + ParsedArgumentsResult, +} from "../../types/argParser.js"; import type { CommandArgValue, CommandArgument } from "../../types/command.js"; const USER_MENTION_PATTERN = /^<@!?(\d{16,22})>$/; @@ -12,7 +19,11 @@ const BOOLEAN_TRUE = new Set(["true", "1", "yes", "y", "on"]); const BOOLEAN_FALSE = new Set(["false", "0", "no", "n", "off"]); const isGuildBasedChannel = (value: unknown): value is GuildBasedChannel => { - return Boolean(value) && typeof value === "object" && "guild" in (value as Record); + return ( + Boolean(value) && + typeof value === "object" && + "guild" in (value as Record) + ); }; export const tokenizePrefixInput = (raw: string): string[] => { @@ -21,7 +32,9 @@ export const tokenizePrefixInput = (raw: string): string[] => { let match: RegExpExecArray | null; while ((match = regex.exec(raw)) !== null) { - tokens.push((match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["'])/g, "$1")); + tokens.push( + (match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["'])/g, "$1"), + ); } return tokens; @@ -41,7 +54,10 @@ export const parsePrefixArgs = async ( if (!token) { if (definition.required) { - errors.push({ key: "errors.args.missing", vars: { arg: definition.name } }); + errors.push({ + key: "errors.args.missing", + vars: { arg: definition.name }, + }); } values[definition.name] = undefined; continue; @@ -81,7 +97,10 @@ export const parseSlashArgs = ( values[definition.name] = parseByTypeFromSlash(interaction, definition); } catch { if (definition.required) { - errors.push({ key: "errors.args.missing", vars: { arg: definition.name } }); + errors.push({ + key: "errors.args.missing", + vars: { arg: definition.name }, + }); } values[definition.name] = undefined; } @@ -94,14 +113,20 @@ const parseByTypeFromPrefix = async ( message: Message, type: CommandArgument["type"], token: string, -): Promise<{ ok: true; value: CommandArgValue } | { ok: false; error: ArgumentParseError }> => { +): Promise< + | { ok: true; value: CommandArgValue } + | { ok: false; error: ArgumentParseError } +> => { switch (type) { case "string": return { ok: true, value: token }; case "int": { if (!/^-?\d+$/.test(token)) { - return { ok: false, error: { key: "errors.args.invalidInt", vars: { value: token } } }; + return { + ok: false, + error: { key: "errors.args.invalidInt", vars: { value: token } }, + }; } const value = Number.parseInt(token, 10); @@ -111,7 +136,10 @@ const parseByTypeFromPrefix = async ( case "number": { const value = Number(token); if (!Number.isFinite(value)) { - return { ok: false, error: { key: "errors.args.invalidNumber", vars: { value: token } } }; + return { + ok: false, + error: { key: "errors.args.invalidNumber", vars: { value: token } }, + }; } return { ok: true, value }; } @@ -124,7 +152,10 @@ const parseByTypeFromPrefix = async ( if (BOOLEAN_FALSE.has(normalized)) { return { ok: true, value: false }; } - return { ok: false, error: { key: "errors.args.invalidBoolean", vars: { value: token } } }; + return { + ok: false, + error: { key: "errors.args.invalidBoolean", vars: { value: token } }, + }; } case "user": { @@ -133,7 +164,10 @@ const parseByTypeFromPrefix = async ( const userId = mentionMatch?.[1] ?? idMatch?.[1]; if (!userId) { - return { ok: false, error: { key: "errors.args.invalidUser", vars: { value: token } } }; + return { + ok: false, + error: { key: "errors.args.invalidUser", vars: { value: token } }, + }; } const fromMention = message.mentions.users.get(userId); @@ -145,7 +179,10 @@ const parseByTypeFromPrefix = async ( const fetched = await message.client.users.fetch(userId); return { ok: true, value: fetched }; } catch { - return { ok: false, error: { key: "errors.args.invalidUser", vars: { value: token } } }; + return { + ok: false, + error: { key: "errors.args.invalidUser", vars: { value: token } }, + }; } } @@ -155,7 +192,10 @@ const parseByTypeFromPrefix = async ( const channelId = mentionMatch?.[1] ?? idMatch?.[1]; if (!channelId) { - return { ok: false, error: { key: "errors.args.invalidChannel", vars: { value: token } } }; + return { + ok: false, + error: { key: "errors.args.invalidChannel", vars: { value: token } }, + }; } const fromMention = message.mentions.channels.get(channelId); @@ -186,7 +226,10 @@ const parseByTypeFromPrefix = async ( // Falls through to invalid channel error. } - return { ok: false, error: { key: "errors.args.invalidChannel", vars: { value: token } } }; + return { + ok: false, + error: { key: "errors.args.invalidChannel", vars: { value: token } }, + }; } case "role": { @@ -195,7 +238,10 @@ const parseByTypeFromPrefix = async ( const roleId = mentionMatch?.[1] ?? idMatch?.[1]; if (!roleId || !message.guild) { - return { ok: false, error: { key: "errors.args.invalidRole", vars: { value: token } } }; + return { + ok: false, + error: { key: "errors.args.invalidRole", vars: { value: token } }, + }; } const fromMention = message.mentions.roles.get(roleId); @@ -217,7 +263,10 @@ const parseByTypeFromPrefix = async ( // Falls through to invalid role error. } - return { ok: false, error: { key: "errors.args.invalidRole", vars: { value: token } } }; + return { + ok: false, + error: { key: "errors.args.invalidRole", vars: { value: token } }, + }; } default: @@ -231,27 +280,51 @@ const parseByTypeFromSlash = ( ): CommandArgValue => { switch (definition.type) { case "string": - return interaction.options.getString(definition.name, definition.required) ?? undefined; + return ( + interaction.options.getString(definition.name, definition.required) ?? + undefined + ); case "int": - return interaction.options.getInteger(definition.name, definition.required) ?? undefined; + return ( + interaction.options.getInteger(definition.name, definition.required) ?? + undefined + ); case "number": - return interaction.options.getNumber(definition.name, definition.required) ?? undefined; + return ( + interaction.options.getNumber(definition.name, definition.required) ?? + undefined + ); case "boolean": - return interaction.options.getBoolean(definition.name, definition.required) ?? undefined; + return ( + interaction.options.getBoolean(definition.name, definition.required) ?? + undefined + ); case "user": - return interaction.options.getUser(definition.name, definition.required) ?? undefined; + return ( + interaction.options.getUser(definition.name, definition.required) ?? + undefined + ); case "channel": - return (interaction.options.getChannel(definition.name, definition.required) ?? undefined) as CommandArgValue; + return (interaction.options.getChannel( + definition.name, + definition.required, + ) ?? undefined) as CommandArgValue; case "role": - return (interaction.options.getRole(definition.name, definition.required) ?? undefined) as CommandArgValue; + return (interaction.options.getRole( + definition.name, + definition.required, + ) ?? undefined) as CommandArgValue; default: - return interaction.options.getString(definition.name, definition.required) ?? undefined; + return ( + interaction.options.getString(definition.name, definition.required) ?? + undefined + ); } }; diff --git a/apps/bot/src/legacy/core/commands/defineCommand.ts b/apps/bot/src/legacy/core/commands/defineCommand.ts index 546806d..57d252e 100644 --- a/apps/bot/src/legacy/core/commands/defineCommand.ts +++ b/apps/bot/src/legacy/core/commands/defineCommand.ts @@ -32,7 +32,10 @@ const normalizeCooldown = (input: BotCommandInput): number | undefined => { return input.cooldown; }; -const resolveSensitive = (input: BotCommandInput, permissions: readonly unknown[]): boolean => { +const resolveSensitive = ( + input: BotCommandInput, + permissions: readonly unknown[], +): boolean => { if (input.sensitive === true && permissions.length === 0) { throw new Error( `Invalid security config for command "${input.meta.name}": sensitive commands must declare at least one required permission.`, diff --git a/apps/bot/src/legacy/core/commands/deploy.ts b/apps/bot/src/legacy/core/commands/deploy.ts index 8d747d0..5e17e58 100644 --- a/apps/bot/src/legacy/core/commands/deploy.ts +++ b/apps/bot/src/legacy/core/commands/deploy.ts @@ -1,14 +1,22 @@ import { REST, Routes } from "discord.js"; import { buildSlashPayload } from "./slashBuilder.js"; -import type { DeployCommandsOptions, DeployCommandsResult } from "../../types/deploy.js"; +import type { + DeployCommandsOptions, + DeployCommandsResult, +} from "../../types/deploy.js"; -export const deployApplicationCommands = async (options: DeployCommandsOptions): Promise => { +export const deployApplicationCommands = async ( + options: DeployCommandsOptions, +): Promise => { const body = buildSlashPayload(options.registry.getAll(), options.i18n); const rest = new REST({ version: "10" }).setToken(options.token); if (options.guildId) { - await rest.put(Routes.applicationGuildCommands(options.clientId, options.guildId), { body }); + await rest.put( + Routes.applicationGuildCommands(options.clientId, options.guildId), + { body }, + ); return { scope: "guild", count: body.length }; } diff --git a/apps/bot/src/legacy/core/commands/registry.ts b/apps/bot/src/legacy/core/commands/registry.ts index f4bc16a..806ea76 100644 --- a/apps/bot/src/legacy/core/commands/registry.ts +++ b/apps/bot/src/legacy/core/commands/registry.ts @@ -26,7 +26,9 @@ export class CommandRegistry { return this.commandsByName.get(normalize(name)); } - public findByAnyPrefixTrigger(trigger: string): PrefixTriggerMatch | undefined { + public findByAnyPrefixTrigger( + trigger: string, + ): PrefixTriggerMatch | undefined { return this.prefixTriggers.get(normalize(trigger)); } @@ -43,7 +45,9 @@ export class CommandRegistry { this.commandsByName.set(name, command); for (const lang of SUPPORTED_LANGS) { - const trigger = normalize(this.i18n.commandTrigger(lang, command.meta.name)); + const trigger = normalize( + this.i18n.commandTrigger(lang, command.meta.name), + ); const existing = this.prefixTriggers.get(trigger); if (existing) { @@ -60,7 +64,12 @@ export class CommandRegistry { }); } - const slashKeys = [command.meta.name, ...SUPPORTED_LANGS.map((lang) => this.i18n.commandName(lang, command.meta.name))]; + const slashKeys = [ + command.meta.name, + ...SUPPORTED_LANGS.map((lang) => + this.i18n.commandName(lang, command.meta.name), + ), + ]; for (const keyRaw of slashKeys) { const key = normalize(keyRaw); diff --git a/apps/bot/src/legacy/core/commands/slashBuilder.ts b/apps/bot/src/legacy/core/commands/slashBuilder.ts index b94dfcd..079907e 100644 --- a/apps/bot/src/legacy/core/commands/slashBuilder.ts +++ b/apps/bot/src/legacy/core/commands/slashBuilder.ts @@ -4,7 +4,12 @@ import { type RESTPostAPIChatInputApplicationCommandsJSONBody, } from "discord.js"; -import { SUPPORTED_LANGS, type BotCommand, type CommandArgument, type SupportedLang } from "../../types/command.js"; +import { + SUPPORTED_LANGS, + type BotCommand, + type CommandArgument, + type SupportedLang, +} from "../../types/command.js"; import type { I18nService } from "../../i18n/index.js"; const LANG_TO_DISCORD_LOCALE: Partial> = { @@ -24,7 +29,9 @@ const LANG_TO_DISCORD_LOCALE: Partial> = { tr: "tr", }; -const toLocalizationMap = (source: Partial>): Record => { +const toLocalizationMap = ( + source: Partial>, +): Record => { const entries = Object.entries(source) .map(([lang, value]) => { const discordLocale = LANG_TO_DISCORD_LOCALE[lang as SupportedLang]; @@ -39,7 +46,10 @@ const toLocalizationMap = (source: Partial>): Reco return Object.fromEntries(entries); }; -const buildCommandNameLocalizationSource = (command: BotCommand, i18n: I18nService): Partial> => { +const buildCommandNameLocalizationSource = ( + command: BotCommand, + i18n: I18nService, +): Partial> => { const localizations: Partial> = {}; for (const lang of SUPPORTED_LANGS) { @@ -53,7 +63,10 @@ const buildCommandNameLocalizationSource = (command: BotCommand, i18n: I18nServi return localizations; }; -const buildDescriptionLocalizationSource = (descriptionKey: string, i18n: I18nService): Partial> => { +const buildDescriptionLocalizationSource = ( + descriptionKey: string, + i18n: I18nService, +): Partial> => { const localizations: Partial> = {}; for (const lang of SUPPORTED_LANGS) { @@ -67,7 +80,10 @@ const buildDescriptionLocalizationSource = (descriptionKey: string, i18n: I18nSe return localizations; }; -const argDescriptionKey = (command: BotCommand, arg: CommandArgument): string => { +const argDescriptionKey = ( + command: BotCommand, + arg: CommandArgument, +): string => { return `commands.${command.meta.name}.${arg.descriptionKey}`; }; @@ -75,7 +91,10 @@ const commandDescriptionKey = (command: BotCommand): string => { return `commands.${command.meta.name}.description`; }; -const buildHelpCommandChoices = (commands: readonly BotCommand[], i18n: I18nService): Array<{ name: string; value: string }> => { +const buildHelpCommandChoices = ( + commands: readonly BotCommand[], + i18n: I18nService, +): Array<{ name: string; value: string }> => { return commands .slice() .sort((a, b) => a.meta.name.localeCompare(b.meta.name)) @@ -95,7 +114,9 @@ const applyOption = ( ): void => { const descriptionKey = argDescriptionKey(command, arg); const descriptionEn = i18n.t("en", descriptionKey); - const descriptionLocalizations = toLocalizationMap(buildDescriptionLocalizationSource(descriptionKey, i18n)); + const descriptionLocalizations = toLocalizationMap( + buildDescriptionLocalizationSource(descriptionKey, i18n), + ); if (arg.type === "user") { builder.addUserOption((opt) => @@ -163,24 +184,22 @@ const applyOption = ( return; } - builder.addStringOption((opt) => - { - const configured = opt + builder.addStringOption((opt) => { + const configured = opt .setName(arg.name) .setDescription(descriptionEn) .setDescriptionLocalizations(descriptionLocalizations) .setRequired(arg.required); - if (command.meta.name === "help" && arg.name === "command") { - const choices = buildHelpCommandChoices(allCommands, i18n); - if (choices.length > 0) { - configured.addChoices(...choices); - } + if (command.meta.name === "help" && arg.name === "command") { + const choices = buildHelpCommandChoices(allCommands, i18n); + if (choices.length > 0) { + configured.addChoices(...choices); } + } - return configured; - }, - ); + return configured; + }); }; export const buildSlashPayload = ( @@ -190,8 +209,12 @@ export const buildSlashPayload = ( return commands.map((command) => { const descriptionKey = commandDescriptionKey(command); - const slashLocalizations = toLocalizationMap(buildCommandNameLocalizationSource(command, i18n)); - const descriptionLocalizations = toLocalizationMap(buildDescriptionLocalizationSource(descriptionKey, i18n)); + const slashLocalizations = toLocalizationMap( + buildCommandNameLocalizationSource(command, i18n), + ); + const descriptionLocalizations = toLocalizationMap( + buildDescriptionLocalizationSource(descriptionKey, i18n), + ); const slashBuilder = new SlashCommandBuilder() .setName(command.meta.name) @@ -207,7 +230,9 @@ export const buildSlashPayload = ( }); }; -export const commandOptionType = (type: CommandArgument["type"]): ApplicationCommandOptionType => { +export const commandOptionType = ( + type: CommandArgument["type"], +): ApplicationCommandOptionType => { switch (type) { case "user": return ApplicationCommandOptionType.User; diff --git a/apps/bot/src/legacy/core/commands/usage.ts b/apps/bot/src/legacy/core/commands/usage.ts index 6f0d1d7..5ed72dd 100644 --- a/apps/bot/src/legacy/core/commands/usage.ts +++ b/apps/bot/src/legacy/core/commands/usage.ts @@ -1,6 +1,11 @@ -import type { BotCommand, CommandI18nTools, SupportedLang } from "../../types/command.js"; +import type { + BotCommand, + CommandI18nTools, + SupportedLang, +} from "../../types/command.js"; -const formatArgToken = (name: string, required: boolean): string => required ? `<${name}>` : `[${name}]`; +const formatArgToken = (name: string, required: boolean): string => + required ? `<${name}>` : `[${name}]`; export const resolvePrefixTrigger = ( command: BotCommand, @@ -21,7 +26,11 @@ export const resolvePrefixTrigger = ( return command.meta.name; }; -export const resolveSlashName = (command: BotCommand, lang: SupportedLang, i18n: CommandI18nTools): string => { +export const resolveSlashName = ( + command: BotCommand, + lang: SupportedLang, + i18n: CommandI18nTools, +): string => { return i18n.commandName(lang, command.meta.name); }; @@ -33,12 +42,20 @@ export const buildPrefixUsage = ( i18n: CommandI18nTools, ): string => { const trigger = resolvePrefixTrigger(command, lang, defaultLang, i18n); - const args = command.args.map((arg) => formatArgToken(arg.name, arg.required)).join(" "); + const args = command.args + .map((arg) => formatArgToken(arg.name, arg.required)) + .join(" "); return `${prefix}${trigger}${args.length > 0 ? ` ${args}` : ""}`; }; -export const buildSlashUsage = (command: BotCommand, lang: SupportedLang, i18n: CommandI18nTools): string => { +export const buildSlashUsage = ( + command: BotCommand, + lang: SupportedLang, + i18n: CommandI18nTools, +): string => { const slashName = resolveSlashName(command, lang, i18n); - const args = command.args.map((arg) => formatArgToken(arg.name, arg.required)).join(" "); + const args = command.args + .map((arg) => formatArgToken(arg.name, arg.required)) + .join(" "); return `/${slashName}${args.length > 0 ? ` ${args}` : ""}`; }; diff --git a/apps/bot/src/legacy/core/execution/CommandExecutor.ts b/apps/bot/src/legacy/core/execution/CommandExecutor.ts index 8f7b457..9a9df10 100644 --- a/apps/bot/src/legacy/core/execution/CommandExecutor.ts +++ b/apps/bot/src/legacy/core/execution/CommandExecutor.ts @@ -1,9 +1,9 @@ -import { - PermissionsBitField, - type PermissionResolvable, -} from "discord.js"; +import { PermissionsBitField, type PermissionResolvable } from "discord.js"; -import type { BotCommand, CommandExecutionContext } from "../../types/command.js"; +import type { + BotCommand, + CommandExecutionContext, +} from "../../types/command.js"; import type { AppLogger } from "../logging/logger.js"; import type { CooldownStore } from "./cooldownStore.js"; import type { @@ -22,7 +22,10 @@ export interface CommandExecutorDeps { export class CommandExecutor { public constructor(private readonly deps: CommandExecutorDeps) {} - public async run(command: BotCommand, ctx: CommandExecutionContext): Promise { + public async run( + command: BotCommand, + ctx: CommandExecutionContext, + ): Promise { if (this.isSensitiveCommand(command) && !ctx.transport.guild) { await ctx.reply( ctx.t("errors.permissions.user", { @@ -34,20 +37,31 @@ export class CommandExecutor { const rateLimitRetryAfterSeconds = await this.consumeGlobalRateLimit(ctx); if (rateLimitRetryAfterSeconds > 0) { - await ctx.reply(ctx.t("errors.rateLimit", { seconds: rateLimitRetryAfterSeconds })); + await ctx.reply( + ctx.t("errors.rateLimit", { seconds: rateLimitRetryAfterSeconds }), + ); return; } const availablePermissions = await this.resolveMemberPermissions(ctx); - const missingUserPermissions = this.getMissingPermissions(command.permissions, availablePermissions); + const missingUserPermissions = this.getMissingPermissions( + command.permissions, + availablePermissions, + ); if (missingUserPermissions.length > 0) { - await ctx.reply(ctx.t("errors.permissions.user", { permissions: missingUserPermissions.join(", ") })); + await ctx.reply( + ctx.t("errors.permissions.user", { + permissions: missingUserPermissions.join(", "), + }), + ); return; } const remainingCooldownSeconds = await this.consumeCooldown(command, ctx); if (remainingCooldownSeconds > 0) { - await ctx.reply(ctx.t("errors.cooldown", { seconds: remainingCooldownSeconds })); + await ctx.reply( + ctx.t("errors.cooldown", { seconds: remainingCooldownSeconds }), + ); return; } @@ -96,7 +110,11 @@ export class CommandExecutor { } if (!available) { - return [...new Set(required.flatMap((permission) => this.permissionToLabels(permission)))]; + return [ + ...new Set( + required.flatMap((permission) => this.permissionToLabels(permission)), + ), + ]; } return [ @@ -111,7 +129,9 @@ export class CommandExecutor { private permissionToLabels(permission: PermissionResolvable): string[] { try { const resolved = PermissionsBitField.resolve(permission); - const labels = new PermissionsBitField(resolved).toArray().map(this.formatPermissionLabel); + const labels = new PermissionsBitField(resolved) + .toArray() + .map(this.formatPermissionLabel); if (labels.length > 0) { return labels; } @@ -129,7 +149,10 @@ export class CommandExecutor { .trim(); } - private async consumeCooldown(command: BotCommand, ctx: CommandExecutionContext): Promise { + private async consumeCooldown( + command: BotCommand, + ctx: CommandExecutionContext, + ): Promise { if (command.cooldown === undefined || command.cooldown <= 0) { return 0; } @@ -142,7 +165,12 @@ export class CommandExecutor { const userId = ctx.execution.actor.userId; try { - const result = await this.deps.cooldownStore.consume(botId, command.meta.name, userId, command.cooldown); + const result = await this.deps.cooldownStore.consume( + botId, + command.meta.name, + userId, + command.cooldown, + ); return result.allowed ? 0 : result.retryAfterSeconds; } catch (error) { if (this.deps.rateLimitFailOpen) { @@ -174,10 +202,14 @@ export class CommandExecutor { } } - private async consumeGlobalRateLimit(ctx: CommandExecutionContext): Promise { + private async consumeGlobalRateLimit( + ctx: CommandExecutionContext, + ): Promise { const botId = this.resolveBotId(ctx); if (!botId) { - return this.deps.rateLimitFailOpen ? 0 : Math.max(1, this.deps.globalRateLimitPolicy.windowSeconds); + return this.deps.rateLimitFailOpen + ? 0 + : Math.max(1, this.deps.globalRateLimitPolicy.windowSeconds); } try { diff --git a/apps/bot/src/legacy/core/execution/cooldownStore.ts b/apps/bot/src/legacy/core/execution/cooldownStore.ts index 598516d..32f1a4e 100644 --- a/apps/bot/src/legacy/core/execution/cooldownStore.ts +++ b/apps/bot/src/legacy/core/execution/cooldownStore.ts @@ -9,7 +9,12 @@ export interface CooldownConsumeResult { } export interface CooldownStore { - consume(botId: string, commandName: string, subjectId: string, cooldownSeconds: number): Promise; + consume( + botId: string, + commandName: string, + subjectId: string, + cooldownSeconds: number, + ): Promise; } export class MemoryCooldownStore implements CooldownStore { @@ -52,7 +57,8 @@ export class MemoryCooldownStore implements CooldownStore { } const shouldSweepBySize = this.cooldowns.size >= COOLDOWN_SWEEP_MIN_ENTRIES; - const shouldSweepByTime = now - this.lastSweepAt >= COOLDOWN_SWEEP_INTERVAL_MS; + const shouldSweepByTime = + now - this.lastSweepAt >= COOLDOWN_SWEEP_INTERVAL_MS; if (!shouldSweepBySize && !shouldSweepByTime) { return; } diff --git a/apps/bot/src/legacy/core/execution/dispatch.ts b/apps/bot/src/legacy/core/execution/dispatch.ts index cfbe2aa..8ffc89f 100644 --- a/apps/bot/src/legacy/core/execution/dispatch.ts +++ b/apps/bot/src/legacy/core/execution/dispatch.ts @@ -36,7 +36,10 @@ export interface CommandJobPublisher { export class LocalCommandDispatchPort implements CommandDispatchPort { public constructor(private readonly executor: CommandExecutor) {} - public async dispatch(command: BotCommand, ctx: CommandExecutionContext): Promise { + public async dispatch( + command: BotCommand, + ctx: CommandExecutionContext, + ): Promise { await this.executor.run(command, ctx); } } @@ -47,7 +50,10 @@ export class WorkerCommandDispatchPort implements CommandDispatchPort { private readonly logger: AppLogger, ) {} - public async dispatch(command: BotCommand, ctx: CommandExecutionContext): Promise { + public async dispatch( + command: BotCommand, + ctx: CommandExecutionContext, + ): Promise { const job = toExecutionJob(command, ctx); await this.publisher.publish(job); @@ -65,14 +71,20 @@ export class WorkerCommandDispatchPort implements CommandDispatchPort { } } -const toExecutionJob = (command: BotCommand, ctx: CommandExecutionContext): CommandExecutionJob => { +const toExecutionJob = ( + command: BotCommand, + ctx: CommandExecutionContext, +): CommandExecutionJob => { const botId = ctx.transport.client.user?.id; if (!botId) { throw new Error("runtime bot id unavailable for worker command dispatch"); } const serializedArgs = Object.fromEntries( - Object.entries(ctx.execution.args).map(([key, value]) => [key, serializeArgValue(value)]), + Object.entries(ctx.execution.args).map(([key, value]) => [ + key, + serializeArgValue(value), + ]), ); return { @@ -96,11 +108,19 @@ const serializeArgValue = (value: CommandArgValue): unknown => { return value; } - if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + if ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { return value; } - if (typeof value === "object" && "id" in value && typeof value.id === "string") { + if ( + typeof value === "object" && + "id" in value && + typeof value.id === "string" + ) { return { id: value.id, }; diff --git a/apps/bot/src/legacy/core/execution/globalRateLimitStore.ts b/apps/bot/src/legacy/core/execution/globalRateLimitStore.ts index 7338aa2..c04c6c5 100644 --- a/apps/bot/src/legacy/core/execution/globalRateLimitStore.ts +++ b/apps/bot/src/legacy/core/execution/globalRateLimitStore.ts @@ -13,7 +13,11 @@ export interface GlobalRateLimitDecision { } export interface GlobalRateLimitStore { - consume(botId: string, userId: string, policy: GlobalRateLimitPolicy): Promise; + consume( + botId: string, + userId: string, + policy: GlobalRateLimitPolicy, + ): Promise; } interface WindowCounterEntry { @@ -24,7 +28,11 @@ interface WindowCounterEntry { export class MemoryGlobalRateLimitStore implements GlobalRateLimitStore { private readonly counters = new Map(); - public async consume(botId: string, userId: string, policy: GlobalRateLimitPolicy): Promise { + public async consume( + botId: string, + userId: string, + policy: GlobalRateLimitPolicy, + ): Promise { const sanitized = sanitizePolicy(policy); const now = Date.now(); const key = this.key(botId, userId, sanitized.windowSeconds); @@ -47,7 +55,10 @@ export class MemoryGlobalRateLimitStore implements GlobalRateLimitStore { existing.count += 1; - const retryAfterSeconds = Math.max(1, Math.ceil((existing.resetAt - now) / 1000)); + const retryAfterSeconds = Math.max( + 1, + Math.ceil((existing.resetAt - now) / 1000), + ); const allowed = existing.count <= sanitized.limit; return { @@ -92,7 +103,11 @@ export class RedisGlobalRateLimitStore implements GlobalRateLimitStore { private readonly keyPrefix = "bot", ) {} - public async consume(botId: string, userId: string, policy: GlobalRateLimitPolicy): Promise { + public async consume( + botId: string, + userId: string, + policy: GlobalRateLimitPolicy, + ): Promise { const sanitized = sanitizePolicy(policy); const key = this.key(botId, userId); @@ -102,7 +117,10 @@ export class RedisGlobalRateLimitStore implements GlobalRateLimitStore { key, String(sanitized.windowSeconds), ); - const { count, retryAfterSeconds } = parseScriptResult(rawResult, sanitized.windowSeconds); + const { count, retryAfterSeconds } = parseScriptResult( + rawResult, + sanitized.windowSeconds, + ); const allowed = count <= sanitized.limit; return { @@ -118,11 +136,17 @@ export class RedisGlobalRateLimitStore implements GlobalRateLimitStore { } } -const sanitizePolicy = (policy: GlobalRateLimitPolicy): GlobalRateLimitPolicy => { - const limit = Number.isFinite(policy.limit) && policy.limit > 0 ? Math.floor(policy.limit) : 1; - const windowSeconds = Number.isFinite(policy.windowSeconds) && policy.windowSeconds > 0 - ? Math.floor(policy.windowSeconds) - : 1; +const sanitizePolicy = ( + policy: GlobalRateLimitPolicy, +): GlobalRateLimitPolicy => { + const limit = + Number.isFinite(policy.limit) && policy.limit > 0 + ? Math.floor(policy.limit) + : 1; + const windowSeconds = + Number.isFinite(policy.windowSeconds) && policy.windowSeconds > 0 + ? Math.floor(policy.windowSeconds) + : 1; return { limit, @@ -130,14 +154,19 @@ const sanitizePolicy = (policy: GlobalRateLimitPolicy): GlobalRateLimitPolicy => }; }; -const parseScriptResult = (rawResult: unknown, fallbackWindowSeconds: number): { count: number; retryAfterSeconds: number } => { +const parseScriptResult = ( + rawResult: unknown, + fallbackWindowSeconds: number, +): { count: number; retryAfterSeconds: number } => { if (!Array.isArray(rawResult) || rawResult.length < 2) { throw new Error("Redis rate limit script returned an unexpected payload"); } const count = toPositiveInt(rawResult[0]); if (!count) { - throw new Error("Redis rate limit script returned an invalid counter value"); + throw new Error( + "Redis rate limit script returned an invalid counter value", + ); } const ttl = toPositiveInt(rawResult[1]) ?? fallbackWindowSeconds; @@ -149,11 +178,12 @@ const parseScriptResult = (rawResult: unknown, fallbackWindowSeconds: number): { }; const toPositiveInt = (value: unknown): number | null => { - const numeric = typeof value === "number" - ? value - : typeof value === "string" - ? Number(value) - : Number.NaN; + const numeric = + typeof value === "number" + ? value + : typeof value === "string" + ? Number(value) + : Number.NaN; if (!Number.isFinite(numeric)) { return null; diff --git a/apps/bot/src/legacy/core/execution/redisCommandJobPublisher.ts b/apps/bot/src/legacy/core/execution/redisCommandJobPublisher.ts index 37ca7ca..4802e89 100644 --- a/apps/bot/src/legacy/core/execution/redisCommandJobPublisher.ts +++ b/apps/bot/src/legacy/core/execution/redisCommandJobPublisher.ts @@ -9,7 +9,10 @@ export class RedisCommandJobPublisher implements CommandJobPublisher { ) {} public async publish(job: CommandExecutionJob): Promise { - await this.redis.lpush(this.resolveQueueName(job.botId), JSON.stringify(job)); + await this.redis.lpush( + this.resolveQueueName(job.botId), + JSON.stringify(job), + ); } private resolveQueueName(botId: string): string { diff --git a/apps/bot/src/legacy/core/runtime/leaderCoordinator.ts b/apps/bot/src/legacy/core/runtime/leaderCoordinator.ts index 4d94277..7a6fa4a 100644 --- a/apps/bot/src/legacy/core/runtime/leaderCoordinator.ts +++ b/apps/bot/src/legacy/core/runtime/leaderCoordinator.ts @@ -1,11 +1,19 @@ import type { Pool } from "pg"; export interface LeaderCoordinator { - runIfLeader(lockName: string, botId: string, task: () => Promise): Promise; + runIfLeader( + lockName: string, + botId: string, + task: () => Promise, + ): Promise; } export class LocalLeaderCoordinator implements LeaderCoordinator { - public async runIfLeader(_lockName: string, _botId: string, task: () => Promise): Promise { + public async runIfLeader( + _lockName: string, + _botId: string, + task: () => Promise, + ): Promise { await task(); return true; } @@ -17,8 +25,14 @@ export class PostgresLeaderCoordinator implements LeaderCoordinator { private readonly namespace = "discord-bot", ) {} - public async runIfLeader(lockName: string, botId: string, task: () => Promise): Promise { - const advisoryLockKey = hashToInt32(`${this.namespace}:bot:${botId}:leader:${lockName}`); + public async runIfLeader( + lockName: string, + botId: string, + task: () => Promise, + ): Promise { + const advisoryLockKey = hashToInt32( + `${this.namespace}:bot:${botId}:leader:${lockName}`, + ); const client = await this.pool.connect(); try { diff --git a/apps/bot/src/legacy/database/stores/logEventStore.ts b/apps/bot/src/legacy/database/stores/logEventStore.ts index ba800f9..da92485 100644 --- a/apps/bot/src/legacy/database/stores/logEventStore.ts +++ b/apps/bot/src/legacy/database/stores/logEventStore.ts @@ -28,13 +28,16 @@ export class PostgresLogEventStore implements LogEventRepository { await this.pool.query(logEventSchemaProbeSql); } catch (error) { throw new Error( - "[db:init] missing or incompatible table \"bot_log_event_configs\". Run migrations with \"npm run migrate\".", + '[db:init] missing or incompatible table "bot_log_event_configs". Run migrations with "npm run migrate".', { cause: error }, ); } } - public async listByBotGuild(botId: string, guildId: string): Promise { + public async listByBotGuild( + botId: string, + guildId: string, + ): Promise { const result = await this.pool.query( ` SELECT event_key, enabled, channel_id @@ -107,7 +110,13 @@ export class PostgresLogEventStore implements LogEventRepository { channel_id = EXCLUDED.channel_id, updated_at = NOW() `, - [botId, guildId, entry.eventKey, entry.config.enabled, entry.config.channelId], + [ + botId, + guildId, + entry.eventKey, + entry.config.enabled, + entry.config.channelId, + ], ); } @@ -126,4 +135,4 @@ export class PostgresLogEventStore implements LogEventRepository { [botId, guildId], ); } -} \ No newline at end of file +} diff --git a/apps/bot/src/legacy/database/stores/memberMessageStore.ts b/apps/bot/src/legacy/database/stores/memberMessageStore.ts index 55843b6..c48b895 100644 --- a/apps/bot/src/legacy/database/stores/memberMessageStore.ts +++ b/apps/bot/src/legacy/database/stores/memberMessageStore.ts @@ -37,7 +37,9 @@ const parseRoleIds = (serialized: string | null): string[] => { return []; } - const roleIds = parsed.filter((value): value is string => typeof value === "string"); + const roleIds = parsed.filter( + (value): value is string => typeof value === "string", + ); return sanitizeMemberMessageRoleIds(roleIds); } catch { return []; @@ -50,7 +52,9 @@ const toConfig = (row: MemberMessageRow): MemberMessageConfig => { return { enabled: row.enabled, channelId: row.channel_id, - messageType: isMemberMessageRenderTypeValue(row.message_type) ? row.message_type : fallback.messageType, + messageType: isMemberMessageRenderTypeValue(row.message_type) + ? row.message_type + : fallback.messageType, autoRoleIds: parseRoleIds(row.auto_role_ids), }; }; @@ -63,13 +67,17 @@ export class PostgresMemberMessageStore implements MemberMessageRepository { await this.pool.query(memberMessageSchemaProbeSql); } catch (error) { throw new Error( - "[db:init] missing or incompatible table \"bot_member_message_configs\". Run migrations with \"npm run migrate\".", + '[db:init] missing or incompatible table "bot_member_message_configs". Run migrations with "npm run migrate".', { cause: error }, ); } } - public async getByBotGuildKind(botId: string, guildId: string, kind: MemberMessageKind): Promise { + public async getByBotGuildKind( + botId: string, + guildId: string, + kind: MemberMessageKind, + ): Promise { const result = await this.pool.query( ` SELECT enabled, channel_id, message_type, auto_role_ids @@ -117,7 +125,15 @@ export class PostgresMemberMessageStore implements MemberMessageRepository { auto_role_ids = EXCLUDED.auto_role_ids, updated_at = NOW() `, - [botId, guildId, kind, config.enabled, config.channelId, config.messageType, JSON.stringify(autoRoleIds)], + [ + botId, + guildId, + kind, + config.enabled, + config.channelId, + config.messageType, + JSON.stringify(autoRoleIds), + ], ); } diff --git a/apps/bot/src/legacy/database/stores/presenceStore.ts b/apps/bot/src/legacy/database/stores/presenceStore.ts index abbe08b..d3b6b96 100644 --- a/apps/bot/src/legacy/database/stores/presenceStore.ts +++ b/apps/bot/src/legacy/database/stores/presenceStore.ts @@ -30,7 +30,10 @@ FROM bot_presence_states LIMIT 0; `; -const parseStoredTexts = (rawTexts: string | null, fallbackText: string): string[] => { +const parseStoredTexts = ( + rawTexts: string | null, + fallbackText: string, +): string[] => { if (typeof rawTexts === "string" && rawTexts.trim().length > 0) { try { const parsed = JSON.parse(rawTexts) as unknown; @@ -53,7 +56,10 @@ const parseStoredTexts = (rawTexts: string | null, fallbackText: string): string }; const toPresenceState = (row: PresenceRow): PresenceState | null => { - if (!isPresenceStatusValue(row.status) || !isPresenceActivityTypeValue(row.activity_type)) { + if ( + !isPresenceStatusValue(row.status) || + !isPresenceActivityTypeValue(row.activity_type) + ) { return null; } @@ -81,7 +87,7 @@ export class PostgresPresenceStore implements PresenceRepository { await this.pool.query(presenceSchemaProbeSql); } catch (error) { throw new Error( - "[db:init] missing or incompatible table \"bot_presence_states\". Run migrations with \"npm run migrate\".", + '[db:init] missing or incompatible table "bot_presence_states". Run migrations with "npm run migrate".', { cause: error }, ); } @@ -101,10 +107,16 @@ export class PostgresPresenceStore implements PresenceRepository { return toPresenceState(row) ?? createDefaultPresenceState(); } - public async upsertByBotId(botId: string, state: PresenceState): Promise { + public async upsertByBotId( + botId: string, + state: PresenceState, + ): Promise { const activityTexts = sanitizeActivityTexts(state.activity.texts); - const primaryText = activityTexts[0] ?? sanitizeActivityText(state.activity.text); - const rotationIntervalSeconds = sanitizePresenceRotationIntervalSeconds(state.activity.rotationIntervalSeconds); + const primaryText = + activityTexts[0] ?? sanitizeActivityText(state.activity.text); + const rotationIntervalSeconds = sanitizePresenceRotationIntervalSeconds( + state.activity.rotationIntervalSeconds, + ); await this.pool.query( ` diff --git a/apps/bot/src/legacy/events/guildDelete.ts b/apps/bot/src/legacy/events/guildDelete.ts index d3c8351..b587d04 100644 --- a/apps/bot/src/legacy/events/guildDelete.ts +++ b/apps/bot/src/legacy/events/guildDelete.ts @@ -23,7 +23,10 @@ export const registerGuildDelete = ( memberMessageService.cleanupGuild(botId, guild.id), logEventService.cleanupGuild(botId, guild.id), ]).catch((error) => { - log.error({ guildId: guild.id, botId, err: error }, "failed to cleanup guild config"); + log.error( + { guildId: guild.id, botId, err: error }, + "failed to cleanup guild config", + ); }); }); }; diff --git a/apps/bot/src/legacy/events/guildMemberAdd.ts b/apps/bot/src/legacy/events/guildMemberAdd.ts index d96a9a4..be87e90 100644 --- a/apps/bot/src/legacy/events/guildMemberAdd.ts +++ b/apps/bot/src/legacy/events/guildMemberAdd.ts @@ -12,50 +12,58 @@ export const registerGuildMemberAdd = ( memberMessageService: MemberMessageService, ): void => { client.on(Events.GuildMemberAdd, (member) => { - void memberMessageService.assignWelcomeAutoRoles({ client, member }).then((result) => { - if (result.assigned) { - return; - } + void memberMessageService + .assignWelcomeAutoRoles({ client, member }) + .then((result) => { + if (result.assigned) { + return; + } - if (result.reason === "no_roles_configured" || result.reason === "no_assignable_roles") { - return; - } + if ( + result.reason === "no_roles_configured" || + result.reason === "no_assignable_roles" + ) { + return; + } - log.warn( - { - guildId: member.guild.id, - userId: member.user.id, - reason: result.reason, - configuredRoleIds: result.configuredRoleIds, - }, - "failed to assign welcome auto roles", - ); - }).catch((error) => { - log.error( - { - guildId: member.guild.id, - userId: member.user.id, - err: error, - }, - "welcome auto-role dispatch crashed", - ); - }); + log.warn( + { + guildId: member.guild.id, + userId: member.user.id, + reason: result.reason, + configuredRoleIds: result.configuredRoleIds, + }, + "failed to assign welcome auto roles", + ); + }) + .catch((error) => { + log.error( + { + guildId: member.guild.id, + userId: member.user.id, + err: error, + }, + "welcome auto-role dispatch crashed", + ); + }); - void memberMessageService.dispatch({ - client, - i18n, - guild: member.guild, - user: member.user, - kind: "welcome", - }).catch((error) => { - log.error( - { - guildId: member.guild.id, - userId: member.user.id, - err: error, - }, - "failed to send welcome message", - ); - }); + void memberMessageService + .dispatch({ + client, + i18n, + guild: member.guild, + user: member.user, + kind: "welcome", + }) + .catch((error) => { + log.error( + { + guildId: member.guild.id, + userId: member.user.id, + err: error, + }, + "failed to send welcome message", + ); + }); }); }; diff --git a/apps/bot/src/legacy/events/guildMemberRemove.ts b/apps/bot/src/legacy/events/guildMemberRemove.ts index 41c76fb..d82e9f0 100644 --- a/apps/bot/src/legacy/events/guildMemberRemove.ts +++ b/apps/bot/src/legacy/events/guildMemberRemove.ts @@ -12,21 +12,23 @@ export const registerGuildMemberRemove = ( memberMessageService: MemberMessageService, ): void => { client.on(Events.GuildMemberRemove, (member) => { - void memberMessageService.dispatch({ - client, - i18n, - guild: member.guild, - user: member.user, - kind: "goodbye", - }).catch((error) => { - log.error( - { - guildId: member.guild.id, - userId: member.user.id, - err: error, - }, - "failed to send goodbye message", - ); - }); + void memberMessageService + .dispatch({ + client, + i18n, + guild: member.guild, + user: member.user, + kind: "goodbye", + }) + .catch((error) => { + log.error( + { + guildId: member.guild.id, + userId: member.user.id, + err: error, + }, + "failed to send goodbye message", + ); + }); }); }; diff --git a/apps/bot/src/legacy/events/index.ts b/apps/bot/src/legacy/events/index.ts index cacd9ac..be2d3f5 100644 --- a/apps/bot/src/legacy/events/index.ts +++ b/apps/bot/src/legacy/events/index.ts @@ -34,7 +34,18 @@ export const registerEvents = ( registerGuildMemberRemove(client, i18n, services.memberMessageService); registerGuildCreate(client); - registerGuildDelete(client, services.memberMessageService, services.logEventService); + registerGuildDelete( + client, + services.memberMessageService, + services.logEventService, + ); - registerClientReady(client, registry, i18n, services.presenceService, leaderCoordinator, runtimeAuth); + registerClientReady( + client, + registry, + i18n, + services.presenceService, + leaderCoordinator, + runtimeAuth, + ); }; diff --git a/apps/bot/src/legacy/events/interactionCreate.ts b/apps/bot/src/legacy/events/interactionCreate.ts index 168022e..bf258cd 100644 --- a/apps/bot/src/legacy/events/interactionCreate.ts +++ b/apps/bot/src/legacy/events/interactionCreate.ts @@ -1,4 +1,8 @@ -import { Events, type Client, type ChatInputCommandInteraction } from "discord.js"; +import { + Events, + type Client, + type ChatInputCommandInteraction, +} from "discord.js"; import { createScopedLogger } from "../core/logging/logger.js"; const log = createScopedLogger("event:interactionCreate"); @@ -11,7 +15,9 @@ const log = createScopedLogger("event:interactionCreate"); */ export const registerInteractionCreate = ( client: Client, - onSlashInteraction: (interaction: ChatInputCommandInteraction) => Promise, + onSlashInteraction: ( + interaction: ChatInputCommandInteraction, + ) => Promise, ): void => { client.on(Events.InteractionCreate, (interaction) => { if (!interaction.isChatInputCommand()) { diff --git a/apps/bot/src/legacy/events/logsRuntime.ts b/apps/bot/src/legacy/events/logsRuntime.ts index 7990ad9..8ff2568 100644 --- a/apps/bot/src/legacy/events/logsRuntime.ts +++ b/apps/bot/src/legacy/events/logsRuntime.ts @@ -1,9 +1,4 @@ -import { - Events, - type Client, - type Guild, - type InviteGuild, -} from "discord.js"; +import { Events, type Client, type Guild, type InviteGuild } from "discord.js"; import { createScopedLogger } from "../core/logging/logger.js"; import type { I18nService } from "../i18n/index.js"; @@ -26,7 +21,10 @@ const tForGuild = ( key: string, vars: Record = {}, ): string => { - const preferredLocale = guild && "preferredLocale" in guild ? (guild.preferredLocale ?? null) : null; + const preferredLocale = + guild && "preferredLocale" in guild + ? (guild.preferredLocale ?? null) + : null; const lang = i18n.resolveLang(preferredLocale); return i18n.t(lang, key, vars); }; @@ -45,7 +43,11 @@ const formatContent = ( guild: Guild | InviteGuild | null | undefined, content: string | null | undefined, ): string => { - const noContent = tForGuild(i18n, guild, "logsRuntime.placeholders.noContent"); + const noContent = tForGuild( + i18n, + guild, + "logsRuntime.placeholders.noContent", + ); if (typeof content !== "string") { return noContent; @@ -82,19 +84,30 @@ const emitRuntimeLog = ( const title = tForGuild(i18n, input.guild, "logsRuntime.embed.title", { event: input.eventKey, }); - const detailsTitle = tForGuild(i18n, input.guild, "logsRuntime.embed.detailsField"); + const detailsTitle = tForGuild( + i18n, + input.guild, + "logsRuntime.embed.detailsField", + ); - void logEventService.dispatchEvent(client, { - eventKey: input.eventKey, - guildId, - summary: input.summary, - ...(input.details && input.details.length > 0 ? { details: input.details } : {}), - ...(input.color !== undefined ? { color: input.color } : {}), - title, - detailsTitle, - }).catch((error) => { - logger.error({ eventKey: input.eventKey, guildId, err: error }, "failed to dispatch runtime log event"); - }); + void logEventService + .dispatchEvent(client, { + eventKey: input.eventKey, + guildId, + summary: input.summary, + ...(input.details && input.details.length > 0 + ? { details: input.details } + : {}), + ...(input.color !== undefined ? { color: input.color } : {}), + title, + detailsTitle, + }) + .catch((error) => { + logger.error( + { eventKey: input.eventKey, guildId, err: error }, + "failed to dispatch runtime log event", + ); + }); }; export const registerLogRuntimeEvents = ( @@ -110,10 +123,15 @@ export const registerLogRuntimeEvents = ( emitRuntimeLog(client, logEventService, i18n, { eventKey: "messageCreate", guild: message.guild, - summary: tForGuild(i18n, message.guild, "logsRuntime.summaries.messageCreate", { - user: `<@${message.author.id}>`, - channel: `<#${message.channelId}>`, - }), + summary: tForGuild( + i18n, + message.guild, + "logsRuntime.summaries.messageCreate", + { + user: `<@${message.author.id}>`, + channel: `<#${message.channelId}>`, + }, + ), details: [ detailLine(i18n, message.guild, "messageId", { value: message.id }), detailLine(i18n, message.guild, "author", { @@ -187,9 +205,14 @@ export const registerLogRuntimeEvents = ( eventKey: "messageBulkDelete", guild, guildId: guild?.id, - summary: tForGuild(i18n, guild, "logsRuntime.summaries.messageBulkDelete", { - count: messages.size, - }), + summary: tForGuild( + i18n, + guild, + "logsRuntime.summaries.messageBulkDelete", + { + count: messages.size, + }, + ), details: [ detailLine(i18n, guild, "channelId", { value: first?.channelId ?? unknown, @@ -204,9 +227,14 @@ export const registerLogRuntimeEvents = ( emitRuntimeLog(client, logEventService, i18n, { eventKey: "guildMemberAdd", guild: member.guild, - summary: tForGuild(i18n, member.guild, "logsRuntime.summaries.guildMemberAdd", { - user: `<@${member.user.id}>`, - }), + summary: tForGuild( + i18n, + member.guild, + "logsRuntime.summaries.guildMemberAdd", + { + user: `<@${member.user.id}>`, + }, + ), details: [ detailLine(i18n, member.guild, "user", { tag: member.user.tag, @@ -221,9 +249,14 @@ export const registerLogRuntimeEvents = ( emitRuntimeLog(client, logEventService, i18n, { eventKey: "guildMemberRemove", guild: member.guild, - summary: tForGuild(i18n, member.guild, "logsRuntime.summaries.guildMemberRemove", { - user: `<@${member.user.id}>`, - }), + summary: tForGuild( + i18n, + member.guild, + "logsRuntime.summaries.guildMemberRemove", + { + user: `<@${member.user.id}>`, + }, + ), details: [ detailLine(i18n, member.guild, "user", { tag: member.user.tag, @@ -235,14 +268,23 @@ export const registerLogRuntimeEvents = ( }); client.on(Events.GuildMemberUpdate, (oldMember, newMember) => { - const none = tForGuild(i18n, newMember.guild, "logsRuntime.placeholders.none"); + const none = tForGuild( + i18n, + newMember.guild, + "logsRuntime.placeholders.none", + ); emitRuntimeLog(client, logEventService, i18n, { eventKey: "guildMemberUpdate", guild: newMember.guild, - summary: tForGuild(i18n, newMember.guild, "logsRuntime.summaries.guildMemberUpdate", { - user: `<@${newMember.user.id}>`, - }), + summary: tForGuild( + i18n, + newMember.guild, + "logsRuntime.summaries.guildMemberUpdate", + { + user: `<@${newMember.user.id}>`, + }, + ), details: [ detailLine(i18n, newMember.guild, "user", { tag: newMember.user.tag, @@ -267,27 +309,52 @@ export const registerLogRuntimeEvents = ( const guild = interaction.guild; const unknown = tForGuild(i18n, guild, "logsRuntime.placeholders.unknown"); - let summary = tForGuild(i18n, guild, "logsRuntime.summaries.interactionGeneric", { - user: `<@${interaction.user.id}>`, - }); + let summary = tForGuild( + i18n, + guild, + "logsRuntime.summaries.interactionGeneric", + { + user: `<@${interaction.user.id}>`, + }, + ); if (interaction.isChatInputCommand()) { - summary = tForGuild(i18n, guild, "logsRuntime.summaries.interactionSlash", { - command: interaction.commandName, - user: `<@${interaction.user.id}>`, - }); + summary = tForGuild( + i18n, + guild, + "logsRuntime.summaries.interactionSlash", + { + command: interaction.commandName, + user: `<@${interaction.user.id}>`, + }, + ); } else if (interaction.isButton()) { - summary = tForGuild(i18n, guild, "logsRuntime.summaries.interactionButton", { - user: `<@${interaction.user.id}>`, - }); + summary = tForGuild( + i18n, + guild, + "logsRuntime.summaries.interactionButton", + { + user: `<@${interaction.user.id}>`, + }, + ); } else if (interaction.isStringSelectMenu()) { - summary = tForGuild(i18n, guild, "logsRuntime.summaries.interactionSelect", { - user: `<@${interaction.user.id}>`, - }); + summary = tForGuild( + i18n, + guild, + "logsRuntime.summaries.interactionSelect", + { + user: `<@${interaction.user.id}>`, + }, + ); } else if (interaction.isModalSubmit()) { - summary = tForGuild(i18n, guild, "logsRuntime.summaries.interactionModal", { - user: `<@${interaction.user.id}>`, - }); + summary = tForGuild( + i18n, + guild, + "logsRuntime.summaries.interactionModal", + { + user: `<@${interaction.user.id}>`, + }, + ); } emitRuntimeLog(client, logEventService, i18n, { @@ -313,9 +380,14 @@ export const registerLogRuntimeEvents = ( emitRuntimeLog(client, logEventService, i18n, { eventKey: "channelCreate", guild: channel.guild, - summary: tForGuild(i18n, channel.guild, "logsRuntime.summaries.channelCreate", { - channel: `#${channel.name}`, - }), + summary: tForGuild( + i18n, + channel.guild, + "logsRuntime.summaries.channelCreate", + { + channel: `#${channel.name}`, + }, + ), details: [ detailLine(i18n, channel.guild, "channelId", { value: channel.id }), detailLine(i18n, channel.guild, "type", { value: channel.type }), @@ -332,9 +404,14 @@ export const registerLogRuntimeEvents = ( emitRuntimeLog(client, logEventService, i18n, { eventKey: "channelDelete", guild: channel.guild, - summary: tForGuild(i18n, channel.guild, "logsRuntime.summaries.channelDelete", { - channel: `#${channel.name}`, - }), + summary: tForGuild( + i18n, + channel.guild, + "logsRuntime.summaries.channelDelete", + { + channel: `#${channel.name}`, + }, + ), details: [ detailLine(i18n, channel.guild, "channelId", { value: channel.id }), detailLine(i18n, channel.guild, "type", { value: channel.type }), @@ -348,19 +425,32 @@ export const registerLogRuntimeEvents = ( return; } - const unknown = tForGuild(i18n, newChannel.guild, "logsRuntime.placeholders.unknown"); + const unknown = tForGuild( + i18n, + newChannel.guild, + "logsRuntime.placeholders.unknown", + ); const oldName = "name" in oldChannel ? oldChannel.name : unknown; emitRuntimeLog(client, logEventService, i18n, { eventKey: "channelUpdate", guild: newChannel.guild, - summary: tForGuild(i18n, newChannel.guild, "logsRuntime.summaries.channelUpdate", { - channel: `#${newChannel.name}`, - }), + summary: tForGuild( + i18n, + newChannel.guild, + "logsRuntime.summaries.channelUpdate", + { + channel: `#${newChannel.name}`, + }, + ), details: [ - detailLine(i18n, newChannel.guild, "channelId", { value: newChannel.id }), + detailLine(i18n, newChannel.guild, "channelId", { + value: newChannel.id, + }), detailLine(i18n, newChannel.guild, "oldName", { value: oldName }), - detailLine(i18n, newChannel.guild, "newName", { value: newChannel.name }), + detailLine(i18n, newChannel.guild, "newName", { + value: newChannel.name, + }), ], color: 0xfee75c, }); @@ -394,9 +484,14 @@ export const registerLogRuntimeEvents = ( emitRuntimeLog(client, logEventService, i18n, { eventKey: "roleUpdate", guild: newRole.guild, - summary: tForGuild(i18n, newRole.guild, "logsRuntime.summaries.roleUpdate", { - role: `@${newRole.name}`, - }), + summary: tForGuild( + i18n, + newRole.guild, + "logsRuntime.summaries.roleUpdate", + { + role: `@${newRole.name}`, + }, + ), details: [ detailLine(i18n, newRole.guild, "roleId", { value: newRole.id }), detailLine(i18n, newRole.guild, "oldName", { value: oldRole.name }), @@ -411,10 +506,19 @@ export const registerLogRuntimeEvents = ( eventKey: "threadCreate", guild: thread.guild ?? null, guildId: thread.guild?.id, - summary: tForGuild(i18n, thread.guild ?? null, "logsRuntime.summaries.threadCreate", { - thread: thread.name, - }), - details: [detailLine(i18n, thread.guild ?? null, "threadId", { value: thread.id })], + summary: tForGuild( + i18n, + thread.guild ?? null, + "logsRuntime.summaries.threadCreate", + { + thread: thread.name, + }, + ), + details: [ + detailLine(i18n, thread.guild ?? null, "threadId", { + value: thread.id, + }), + ], color: 0x57f287, }); }); @@ -424,10 +528,19 @@ export const registerLogRuntimeEvents = ( eventKey: "threadDelete", guild: thread.guild ?? null, guildId: thread.guild?.id, - summary: tForGuild(i18n, thread.guild ?? null, "logsRuntime.summaries.threadDelete", { - thread: thread.name, - }), - details: [detailLine(i18n, thread.guild ?? null, "threadId", { value: thread.id })], + summary: tForGuild( + i18n, + thread.guild ?? null, + "logsRuntime.summaries.threadDelete", + { + thread: thread.name, + }, + ), + details: [ + detailLine(i18n, thread.guild ?? null, "threadId", { + value: thread.id, + }), + ], color: 0xed4245, }); }); @@ -437,28 +550,48 @@ export const registerLogRuntimeEvents = ( eventKey: "threadUpdate", guild: newThread.guild ?? null, guildId: newThread.guild?.id, - summary: tForGuild(i18n, newThread.guild ?? null, "logsRuntime.summaries.threadUpdate", { - thread: newThread.name, - }), + summary: tForGuild( + i18n, + newThread.guild ?? null, + "logsRuntime.summaries.threadUpdate", + { + thread: newThread.name, + }, + ), details: [ - detailLine(i18n, newThread.guild ?? null, "threadId", { value: newThread.id }), - detailLine(i18n, newThread.guild ?? null, "oldName", { value: oldThread.name }), - detailLine(i18n, newThread.guild ?? null, "newName", { value: newThread.name }), + detailLine(i18n, newThread.guild ?? null, "threadId", { + value: newThread.id, + }), + detailLine(i18n, newThread.guild ?? null, "oldName", { + value: oldThread.name, + }), + detailLine(i18n, newThread.guild ?? null, "newName", { + value: newThread.name, + }), ], color: 0xfee75c, }); }); client.on(Events.GuildEmojiCreate, (emoji) => { - const unknown = tForGuild(i18n, emoji.guild ?? null, "logsRuntime.placeholders.unknown"); + const unknown = tForGuild( + i18n, + emoji.guild ?? null, + "logsRuntime.placeholders.unknown", + ); emitRuntimeLog(client, logEventService, i18n, { eventKey: "emojiCreate", guild: emoji.guild ?? null, guildId: emoji.guild?.id, - summary: tForGuild(i18n, emoji.guild ?? null, "logsRuntime.summaries.emojiCreate", { - emoji: emoji.name ?? unknown, - }), + summary: tForGuild( + i18n, + emoji.guild ?? null, + "logsRuntime.summaries.emojiCreate", + { + emoji: emoji.name ?? unknown, + }, + ), details: [ detailLine(i18n, emoji.guild ?? null, "emojiId", { value: emoji.id ?? unknown, @@ -469,15 +602,24 @@ export const registerLogRuntimeEvents = ( }); client.on(Events.GuildEmojiDelete, (emoji) => { - const unknown = tForGuild(i18n, emoji.guild ?? null, "logsRuntime.placeholders.unknown"); + const unknown = tForGuild( + i18n, + emoji.guild ?? null, + "logsRuntime.placeholders.unknown", + ); emitRuntimeLog(client, logEventService, i18n, { eventKey: "emojiDelete", guild: emoji.guild ?? null, guildId: emoji.guild?.id, - summary: tForGuild(i18n, emoji.guild ?? null, "logsRuntime.summaries.emojiDelete", { - emoji: emoji.name ?? unknown, - }), + summary: tForGuild( + i18n, + emoji.guild ?? null, + "logsRuntime.summaries.emojiDelete", + { + emoji: emoji.name ?? unknown, + }, + ), details: [ detailLine(i18n, emoji.guild ?? null, "emojiId", { value: emoji.id ?? unknown, @@ -488,15 +630,24 @@ export const registerLogRuntimeEvents = ( }); client.on(Events.GuildEmojiUpdate, (oldEmoji, newEmoji) => { - const unknown = tForGuild(i18n, newEmoji.guild ?? null, "logsRuntime.placeholders.unknown"); + const unknown = tForGuild( + i18n, + newEmoji.guild ?? null, + "logsRuntime.placeholders.unknown", + ); emitRuntimeLog(client, logEventService, i18n, { eventKey: "emojiUpdate", guild: newEmoji.guild ?? null, guildId: newEmoji.guild?.id, - summary: tForGuild(i18n, newEmoji.guild ?? null, "logsRuntime.summaries.emojiUpdate", { - emoji: newEmoji.name ?? unknown, - }), + summary: tForGuild( + i18n, + newEmoji.guild ?? null, + "logsRuntime.summaries.emojiUpdate", + { + emoji: newEmoji.name ?? unknown, + }, + ), details: [ detailLine(i18n, newEmoji.guild ?? null, "emojiId", { value: newEmoji.id ?? unknown, @@ -532,9 +683,14 @@ export const registerLogRuntimeEvents = ( emitRuntimeLog(client, logEventService, i18n, { eventKey: "guildUnavailable", guild, - summary: tForGuild(i18n, guild, "logsRuntime.summaries.guildUnavailable", { - guild: guild.name, - }), + summary: tForGuild( + i18n, + guild, + "logsRuntime.summaries.guildUnavailable", + { + guild: guild.name, + }, + ), details: [detailLine(i18n, guild, "guildId", { value: guild.id })], color: 0xed4245, }); @@ -562,9 +718,14 @@ export const registerLogRuntimeEvents = ( emitRuntimeLog(client, logEventService, i18n, { eventKey: "guildBanRemove", guild: ban.guild, - summary: tForGuild(i18n, ban.guild, "logsRuntime.summaries.guildBanRemove", { - user: `<@${ban.user.id}>`, - }), + summary: tForGuild( + i18n, + ban.guild, + "logsRuntime.summaries.guildBanRemove", + { + user: `<@${ban.user.id}>`, + }, + ), details: [ detailLine(i18n, ban.guild, "user", { tag: ban.user.tag, diff --git a/apps/bot/src/legacy/events/messageCreate.ts b/apps/bot/src/legacy/events/messageCreate.ts index ebccf6c..d702323 100644 --- a/apps/bot/src/legacy/events/messageCreate.ts +++ b/apps/bot/src/legacy/events/messageCreate.ts @@ -9,7 +9,10 @@ const log = createScopedLogger("event:messageCreate"); * @param client - instance du Client Discord * @param onPrefixMessage - fonction à appeler pour traiter les messages (préfixe) */ -export const registerMessageCreate = (client: Client, onPrefixMessage: (message: Message) => Promise): void => { +export const registerMessageCreate = ( + client: Client, + onPrefixMessage: (message: Message) => Promise, +): void => { client.on(Events.MessageCreate, (message: Message) => { void onPrefixMessage(message).catch((error) => { log.error({ err: error }, "handler failed"); diff --git a/apps/bot/src/legacy/events/ready.ts b/apps/bot/src/legacy/events/ready.ts index 5afdf03..4820bf5 100644 --- a/apps/bot/src/legacy/events/ready.ts +++ b/apps/bot/src/legacy/events/ready.ts @@ -28,17 +28,25 @@ export const registerClientReady = ( const botId = client.user?.id; if (!botId) { - log.error("client ready event received without bot id, leader-only tasks skipped"); + log.error( + "client ready event received without bot id, leader-only tasks skipped", + ); return; } try { - const restoredByLeader = await leaderCoordinator.runIfLeader("presence-restore", botId, async () => { - await presenceService.restoreFromStorage(client); - }); + const restoredByLeader = await leaderCoordinator.runIfLeader( + "presence-restore", + botId, + async () => { + await presenceService.restoreFromStorage(client); + }, + ); if (!restoredByLeader) { - log.info("presence restore skipped: leader lock already held by another instance"); + log.info( + "presence restore skipped: leader lock already held by another instance", + ); } } catch (error) { log.error({ err: error }, "failed to restore bot presence"); @@ -46,31 +54,39 @@ export const registerClientReady = ( if (env.AUTO_DEPLOY_SLASH) { if (!runtimeAuth) { - log.warn("slash sync skipped: runtime auth is missing for this bot instance"); + log.warn( + "slash sync skipped: runtime auth is missing for this bot instance", + ); return; } try { - const deployedByLeader = await leaderCoordinator.runIfLeader("slash-deploy", botId, async () => { - const result = await deployApplicationCommands({ - token: runtimeAuth.token, - clientId: runtimeAuth.clientId, - registry, - i18n, - ...(env.DEV_GUILD_ID ? { guildId: env.DEV_GUILD_ID } : {}), - }); + const deployedByLeader = await leaderCoordinator.runIfLeader( + "slash-deploy", + botId, + async () => { + const result = await deployApplicationCommands({ + token: runtimeAuth.token, + clientId: runtimeAuth.clientId, + registry, + i18n, + ...(env.DEV_GUILD_ID ? { guildId: env.DEV_GUILD_ID } : {}), + }); - log.info( - { - scope: result.scope, - count: result.count, - }, - "slash sync completed", - ); - }); + log.info( + { + scope: result.scope, + count: result.count, + }, + "slash sync completed", + ); + }, + ); if (!deployedByLeader) { - log.info("slash sync skipped: leader lock already held by another instance"); + log.info( + "slash sync skipped: leader lock already held by another instance", + ); } } catch (error) { log.error({ err: error }, "slash sync failed"); diff --git a/apps/bot/src/legacy/features/logs/catalog.ts b/apps/bot/src/legacy/features/logs/catalog.ts index 60636a4..40aeced 100644 --- a/apps/bot/src/legacy/features/logs/catalog.ts +++ b/apps/bot/src/legacy/features/logs/catalog.ts @@ -32,23 +32,28 @@ export const LOG_EVENT_DEFINITIONS: readonly LogEventDefinition[] = [ { key: "inviteDelete", category: "invite" }, ]; -export const LOG_EVENT_KEYS = LOG_EVENT_DEFINITIONS.map((definition) => definition.key); +export const LOG_EVENT_KEYS = LOG_EVENT_DEFINITIONS.map( + (definition) => definition.key, +); export const LOG_EVENT_KEYS_SET = new Set(LOG_EVENT_KEYS); -export const LOG_CHANNEL_NAME_BY_CATEGORY: Record = { - message: "logs-message", - member: "logs-member", - interaction: "logs-interaction", - channel: "logs-channel", - role: "logs-role", - thread: "logs-thread", - emoji: "logs-emoji", - guild: "logs-guild", - moderation: "logs-moderation", - invite: "logs-invite", -}; +export const LOG_CHANNEL_NAME_BY_CATEGORY: Record = + { + message: "logs-message", + member: "logs-member", + interaction: "logs-interaction", + channel: "logs-channel", + role: "logs-role", + thread: "logs-thread", + emoji: "logs-emoji", + guild: "logs-guild", + moderation: "logs-moderation", + invite: "logs-invite", + }; -export const LOG_EVENT_CATEGORY_KEYS = Object.keys(LOG_CHANNEL_NAME_BY_CATEGORY) as LogEventCategoryKey[]; +export const LOG_EVENT_CATEGORY_KEYS = Object.keys( + LOG_CHANNEL_NAME_BY_CATEGORY, +) as LogEventCategoryKey[]; -export const LOGS_PANEL_EVENTS_PER_PAGE = 15; \ No newline at end of file +export const LOGS_PANEL_EVENTS_PER_PAGE = 15; diff --git a/apps/bot/src/legacy/features/logs/commandPanel.ts b/apps/bot/src/legacy/features/logs/commandPanel.ts index 6809f8a..53e80fb 100644 --- a/apps/bot/src/legacy/features/logs/commandPanel.ts +++ b/apps/bot/src/legacy/features/logs/commandPanel.ts @@ -70,7 +70,11 @@ const clampPageIndex = (index: number, pageCount: number): number => { return index; }; -const pageCount = (): number => Math.max(1, Math.ceil(LOG_EVENT_DEFINITIONS.length / LOGS_PANEL_EVENTS_PER_PAGE)); +const pageCount = (): number => + Math.max( + 1, + Math.ceil(LOG_EVENT_DEFINITIONS.length / LOGS_PANEL_EVENTS_PER_PAGE), + ); const pageEvents = (uiState: LogPanelUiState): LogEventDefinition[] => { const totalPages = pageCount(); @@ -79,11 +83,17 @@ const pageEvents = (uiState: LogPanelUiState): LogEventDefinition[] => { return LOG_EVENT_DEFINITIONS.slice(start, start + LOGS_PANEL_EVENTS_PER_PAGE); }; -const eventStatusLabel = (ctx: CommandExecutionContext, enabled: boolean): string => { +const eventStatusLabel = ( + ctx: CommandExecutionContext, + enabled: boolean, +): string => { return enabled ? ctx.ct("ui.status.enabled") : ctx.ct("ui.status.disabled"); }; -const eventLabel = (ctx: CommandExecutionContext, eventKey: LogEventKey): string => { +const eventLabel = ( + ctx: CommandExecutionContext, + eventKey: LogEventKey, +): string => { return ctx.ct(`ui.events.${eventKey}`); }; @@ -94,22 +104,34 @@ const panelContent = ( ): string => { const totalPages = pageCount(); const currentEvents = pageEvents(uiState); - const enabledCount = LOG_EVENT_KEYS.filter((eventKey) => state[eventKey].enabled).length; + const enabledCount = LOG_EVENT_KEYS.filter( + (eventKey) => state[eventKey].enabled, + ).length; const lines = [ `## ${ctx.ct("ui.embed.title")}`, ctx.ct("ui.embed.description"), "", - ctx.ct("ui.pageLabel", { page: uiState.pageIndex + 1, pageCount: totalPages }), - ctx.ct("ui.enabledCountLabel", { enabledCount, totalCount: LOG_EVENT_KEYS.length }), + ctx.ct("ui.pageLabel", { + page: uiState.pageIndex + 1, + pageCount: totalPages, + }), + ctx.ct("ui.enabledCountLabel", { + enabledCount, + totalCount: LOG_EVENT_KEYS.length, + }), "", `${ctx.ct("ui.eventsHeader")}:`, ]; for (const definition of currentEvents) { const eventConfig = state[definition.key]; - const channelDisplay = eventConfig.channelId ? `<#${eventConfig.channelId}>` : ctx.ct("ui.channelNotConfigured"); - lines.push(`- ${eventLabel(ctx, definition.key)} | ${eventStatusLabel(ctx, eventConfig.enabled)} | ${ctx.ct("ui.channelLabel")}: ${channelDisplay}`); + const channelDisplay = eventConfig.channelId + ? `<#${eventConfig.channelId}>` + : ctx.ct("ui.channelNotConfigured"); + lines.push( + `- ${eventLabel(ctx, definition.key)} | ${eventStatusLabel(ctx, eventConfig.enabled)} | ${ctx.ct("ui.channelLabel")}: ${channelDisplay}`, + ); } if (uiState.feedback) { @@ -160,9 +182,15 @@ const buildContainer = ( .setDisabled(disabled || uiState.pageIndex >= totalPages - 1); const container = new ContainerBuilder(); - container.addTextDisplayComponents(new TextDisplayBuilder().setContent(panelContent(ctx, state, uiState))); + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent(panelContent(ctx, state, uiState)), + ); container.addActionRowComponents( - new ActionRowBuilder().addComponents(enableAllButton, disableAllButton, createChannelsButton), + new ActionRowBuilder().addComponents( + enableAllButton, + disableAllButton, + createChannelsButton, + ), ); for (const definitionChunk of chunk(currentEvents, 5)) { @@ -176,15 +204,22 @@ const buildContainer = ( return new ButtonBuilder() .setCustomId(customIds.toggleButtonsByEvent[definition.key]) .setLabel(label) - .setStyle(eventConfig.enabled ? ButtonStyle.Danger : ButtonStyle.Success) + .setStyle( + eventConfig.enabled ? ButtonStyle.Danger : ButtonStyle.Success, + ) .setDisabled(disabled); }); - container.addActionRowComponents(new ActionRowBuilder().addComponents(...rowButtons)); + container.addActionRowComponents( + new ActionRowBuilder().addComponents(...rowButtons), + ); } container.addActionRowComponents( - new ActionRowBuilder().addComponents(previousPageButton, nextPageButton), + new ActionRowBuilder().addComponents( + previousPageButton, + nextPageButton, + ), ); return container; @@ -196,15 +231,19 @@ const applyAllEnabled = (state: LogEventStateByKey, enabled: boolean): void => { } }; -const toToggleLookup = (customIds: LogPanelCustomIds): Map => { +const toToggleLookup = ( + customIds: LogPanelCustomIds, +): Map => { return new Map( - LOG_EVENT_KEYS.map((eventKey) => [customIds.toggleButtonsByEvent[eventKey], eventKey]), + LOG_EVENT_KEYS.map((eventKey) => [ + customIds.toggleButtonsByEvent[eventKey], + eventKey, + ]), ); }; export const createLogsCommandExecute = (logEventService: LogEventService) => { return async (ctx: CommandExecutionContext): Promise => { - if (!ctx.guild) { await ctx.reply(ctx.ct("responses.guildOnly")); return; @@ -232,7 +271,11 @@ export const createLogsCommandExecute = (logEventService: LogEventService) => { } const ownerId = ctx.user.id; - const sessionKey = logEventService.panelSessionKey(ctx.client, guild.id, ownerId); + const sessionKey = logEventService.panelSessionKey( + ctx.client, + guild.id, + ownerId, + ); const disablePanel = async (): Promise => { await replyMessage @@ -243,8 +286,13 @@ export const createLogsCommandExecute = (logEventService: LogEventService) => { .catch(() => undefined); }; - const collector = replyMessage.createMessageComponentCollector({ time: 15 * 60_000 }); - await logEventService.replacePanelSession(sessionKey, { collector, disable: disablePanel }); + const collector = replyMessage.createMessageComponentCollector({ + time: 15 * 60_000, + }); + await logEventService.replacePanelSession(sessionKey, { + collector, + disable: disablePanel, + }); collector.on("collect", async (interaction) => { if (interaction.user.id !== ownerId) { @@ -289,7 +337,11 @@ export const createLogsCommandExecute = (logEventService: LogEventService) => { if (interaction.customId === customIds.createChannelsButton) { await interaction.deferUpdate(); - const result = await logEventService.createCategoryChannels(ctx.client, guild, state); + const result = await logEventService.createCategoryChannels( + ctx.client, + guild, + state, + ); if (result.failedCategories.length === 0) { uiState.feedback = ctx.ct("responses.channelsCreated", { created: result.createdCount, @@ -356,17 +408,21 @@ export const createLogsCommandExecute = (logEventService: LogEventService) => { logger.error({ err: error }, "interaction failed"); if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: ctx.t("errors.execution"), - flags: [MessageFlags.Ephemeral], - }).catch(() => undefined); + await interaction + .reply({ + content: ctx.t("errors.execution"), + flags: [MessageFlags.Ephemeral], + }) + .catch(() => undefined); return; } - await interaction.followUp({ - content: ctx.t("errors.execution"), - flags: [MessageFlags.Ephemeral], - }).catch(() => undefined); + await interaction + .followUp({ + content: ctx.t("errors.execution"), + flags: [MessageFlags.Ephemeral], + }) + .catch(() => undefined); } }); @@ -375,4 +431,4 @@ export const createLogsCommandExecute = (logEventService: LogEventService) => { await disablePanel(); }); }; -}; \ No newline at end of file +}; diff --git a/apps/bot/src/legacy/features/logs/repository.ts b/apps/bot/src/legacy/features/logs/repository.ts index b72bb82..e456681 100644 --- a/apps/bot/src/legacy/features/logs/repository.ts +++ b/apps/bot/src/legacy/features/logs/repository.ts @@ -7,7 +7,16 @@ import type { export interface LogEventRepository { listByBotGuild(botId: string, guildId: string): Promise; - upsertByBotGuildEvent(botId: string, guildId: string, eventKey: LogEventKey, config: LogEventConfig): Promise; - upsertManyByBotGuildEvents(botId: string, guildId: string, entries: readonly LogEventRepositoryEntry[]): Promise; + upsertByBotGuildEvent( + botId: string, + guildId: string, + eventKey: LogEventKey, + config: LogEventConfig, + ): Promise; + upsertManyByBotGuildEvents( + botId: string, + guildId: string, + entries: readonly LogEventRepositoryEntry[], + ): Promise; deleteByBotGuild(botId: string, guildId: string): Promise; -} \ No newline at end of file +} diff --git a/apps/bot/src/legacy/features/logs/service.ts b/apps/bot/src/legacy/features/logs/service.ts index 93ef2d1..e4b3e12 100644 --- a/apps/bot/src/legacy/features/logs/service.ts +++ b/apps/bot/src/legacy/features/logs/service.ts @@ -34,22 +34,33 @@ const LOG_CHANNEL_CATEGORY_NAME = "logs"; const LOG_CHANNEL_NAME_PREFIX = ""; const LEGACY_LOG_CHANNEL_NAME_PREFIX = "📁・"; -const hasSendMethod = (value: unknown): value is { send: (payload: unknown) => Promise } => { +const hasSendMethod = ( + value: unknown, +): value is { send: (payload: unknown) => Promise } => { if (!value || typeof value !== "object") { return false; } - return "send" in value && typeof (value as { send?: unknown }).send === "function"; + return ( + "send" in value && typeof (value as { send?: unknown }).send === "function" + ); }; -const hasPermissionsFor = (value: unknown): value is { - permissionsFor: (member: unknown) => { has: (permission: unknown) => boolean } | null; +const hasPermissionsFor = ( + value: unknown, +): value is { + permissionsFor: ( + member: unknown, + ) => { has: (permission: unknown) => boolean } | null; } => { if (!value || typeof value !== "object") { return false; } - return "permissionsFor" in value && typeof (value as { permissionsFor?: unknown }).permissionsFor === "function"; + return ( + "permissionsFor" in value && + typeof (value as { permissionsFor?: unknown }).permissionsFor === "function" + ); }; const clampField = (value: string, maxLength: number): string => { @@ -66,7 +77,8 @@ const buildChannelName = (category: LogEventCategoryKey): string => { export class LogEventService { private readonly stateCache = new Map(); - private readonly panelSessions = new ComponentSessionRegistry(); + private readonly panelSessions = + new ComponentSessionRegistry(); public constructor(private readonly repository: LogEventRepository) {} @@ -74,19 +86,32 @@ export class LogEventService { return client.user?.id ?? null; } - public panelSessionKey(client: Client, guildId: string, userId: string): string { + public panelSessionKey( + client: Client, + guildId: string, + userId: string, + ): string { return `${this.resolveBotId(client) ?? "unbound"}:${guildId}:${userId}`; } - public async replacePanelSession(key: string, session: LogPanelSession): Promise { + public async replacePanelSession( + key: string, + session: LogPanelSession, + ): Promise { await this.panelSessions.replace(key, session); } - public deletePanelSessionIfCollectorMatch(key: string, collector: LogPanelSession["collector"]): void { + public deletePanelSessionIfCollectorMatch( + key: string, + collector: LogPanelSession["collector"], + ): void { this.panelSessions.deleteIfCollectorMatch(key, collector); } - public async loadGuildState(client: Client, guildId: string): Promise { + public async loadGuildState( + client: Client, + guildId: string, + ): Promise { const botId = this.resolveBotId(client); if (!botId) { return createDefaultLogEventState(); @@ -105,22 +130,31 @@ export class LogEventService { return cloneLogEventState(state); } - public async persistGuildState(client: Client, guildId: string, state: LogEventStateByKey): Promise { + public async persistGuildState( + client: Client, + guildId: string, + state: LogEventStateByKey, + ): Promise { const botId = this.resolveBotId(client); if (!botId) { return; } - const entries: LogEventRepositoryEntry[] = LOG_EVENT_KEYS.map((eventKey) => ({ - eventKey, - config: { - enabled: state[eventKey].enabled, - channelId: state[eventKey].channelId, - }, - })); + const entries: LogEventRepositoryEntry[] = LOG_EVENT_KEYS.map( + (eventKey) => ({ + eventKey, + config: { + enabled: state[eventKey].enabled, + channelId: state[eventKey].channelId, + }, + }), + ); await this.repository.upsertManyByBotGuildEvents(botId, guildId, entries); - this.stateCache.set(this.cacheKey(botId, guildId), cloneLogEventState(state)); + this.stateCache.set( + this.cacheKey(botId, guildId), + cloneLogEventState(state), + ); } public async createCategoryChannels( @@ -139,7 +173,10 @@ export class LogEventService { return false; } - return channel.type === ChannelType.GuildCategory && channel.name === LOG_CHANNEL_CATEGORY_NAME; + return ( + channel.type === ChannelType.GuildCategory && + channel.name === LOG_CHANNEL_CATEGORY_NAME + ); }); if (!logCategory) { @@ -149,7 +186,10 @@ export class LogEventService { type: ChannelType.GuildCategory, }); } catch (error) { - logger.warn({ guildId: guild.id, err: error }, "failed to create logs category"); + logger.warn( + { guildId: guild.id, err: error }, + "failed to create logs category", + ); return { createdCount, reusedCount, @@ -167,34 +207,45 @@ export class LogEventService { return false; } - return channel.type === ChannelType.GuildText && channel.name === expectedName; + return ( + channel.type === ChannelType.GuildText && + channel.name === expectedName + ); }); const existingByLegacyName = existingByExpectedName ? null : existingChannels.find((channel) => { - if (!channel) { - return false; - } + if (!channel) { + return false; + } - return channel.type === ChannelType.GuildText && ( - channel.name === legacyName - || channel.name === `${LEGACY_LOG_CHANNEL_NAME_PREFIX}${legacyName}` - ); - }); + return ( + channel.type === ChannelType.GuildText && + (channel.name === legacyName || + channel.name === + `${LEGACY_LOG_CHANNEL_NAME_PREFIX}${legacyName}`) + ); + }); const existing = existingByExpectedName ?? existingByLegacyName; if (existing) { if (existing.name !== expectedName) { await existing.setName(expectedName).catch((error) => { - logger.warn({ guildId: guild.id, category, err: error }, "failed to rename logs channel"); + logger.warn( + { guildId: guild.id, category, err: error }, + "failed to rename logs channel", + ); }); } if (existing.parentId !== logCategory.id) { await existing.setParent(logCategory.id).catch((error) => { - logger.warn({ guildId: guild.id, category, err: error }, "failed to move logs channel to category"); + logger.warn( + { guildId: guild.id, category, err: error }, + "failed to move logs channel to category", + ); }); } @@ -234,7 +285,10 @@ export class LogEventService { categoryToChannelId.set(category, created.id); createdCount += 1; } catch (error) { - logger.warn({ guildId: guild.id, category, err: error }, "failed to create logs channel"); + logger.warn( + { guildId: guild.id, category, err: error }, + "failed to create logs channel", + ); failedCategories.push(category); } } @@ -257,7 +311,10 @@ export class LogEventService { }; } - public async dispatchEvent(client: Client, input: LogRuntimeDispatchInput): Promise { + public async dispatchEvent( + client: Client, + input: LogRuntimeDispatchInput, + ): Promise { const botId = this.resolveBotId(client); if (!botId) { return; @@ -276,13 +333,16 @@ export class LogEventService { return; } - const guild = client.guilds.cache.get(input.guildId) ?? await client.guilds.fetch(input.guildId).catch(() => null); + const guild = + client.guilds.cache.get(input.guildId) ?? + (await client.guilds.fetch(input.guildId).catch(() => null)); if (!guild) { return; } - const channel = guild.channels.cache.get(eventConfig.channelId) - ?? await guild.channels.fetch(eventConfig.channelId).catch(() => null); + const channel = + guild.channels.cache.get(eventConfig.channelId) ?? + (await guild.channels.fetch(eventConfig.channelId).catch(() => null)); if (!channel || !hasSendMethod(channel)) { return; @@ -291,7 +351,11 @@ export class LogEventService { const me = guild.members.me; if (me && hasPermissionsFor(channel)) { const permissions = channel.permissionsFor(me); - if (!permissions || !permissions.has(PermissionFlagsBits.ViewChannel) || !permissions.has(PermissionFlagsBits.SendMessages)) { + if ( + !permissions || + !permissions.has(PermissionFlagsBits.ViewChannel) || + !permissions.has(PermissionFlagsBits.SendMessages) + ) { return; } } @@ -303,7 +367,10 @@ export class LogEventService { .setTimestamp(new Date()); if (input.details && input.details.length > 0) { - const detailsValue = clampField(input.details.map((line) => `- ${line}`).join("\n"), 1024); + const detailsValue = clampField( + input.details.map((line) => `- ${line}`).join("\n"), + 1024, + ); if (detailsValue.length > 0) { embed.addFields({ name: input.detailsTitle ?? "details", @@ -331,4 +398,4 @@ export class LogEventService { private cacheKey(botId: string, guildId: string): string { return `${botId}:${guildId}`; } -} \ No newline at end of file +} diff --git a/apps/bot/src/legacy/features/memberMessages/commandPanel.ts b/apps/bot/src/legacy/features/memberMessages/commandPanel.ts index 814a79a..000d5e5 100644 --- a/apps/bot/src/legacy/features/memberMessages/commandPanel.ts +++ b/apps/bot/src/legacy/features/memberMessages/commandPanel.ts @@ -35,7 +35,12 @@ const log = createScopedLogger("command:memberMessagesPanel"); const panelSessions = new ComponentSessionRegistry(); -const panelSessionKey = (kind: MemberMessageKind, botId: string, guildId: string, userId: string): string => { +const panelSessionKey = ( + kind: MemberMessageKind, + botId: string, + guildId: string, + userId: string, +): string => { return `${kind}:${botId}:${guildId}:${userId}`; }; @@ -56,15 +61,24 @@ const createCustomIds = (kind: MemberMessageKind): MemberMessageCustomIds => { }; }; -const statusLabel = (ctx: CommandExecutionContext, enabled: boolean): string => { +const statusLabel = ( + ctx: CommandExecutionContext, + enabled: boolean, +): string => { return enabled ? ctx.ct("ui.status.enabled") : ctx.ct("ui.status.disabled"); }; -const messageTypeLabel = (ctx: CommandExecutionContext, messageType: MemberMessageRenderType): string => { +const messageTypeLabel = ( + ctx: CommandExecutionContext, + messageType: MemberMessageRenderType, +): string => { return ctx.ct(`ui.type.options.${messageType}.label`); }; -const autoRolesLabel = (ctx: CommandExecutionContext, config: MemberMessageConfig): string => { +const autoRolesLabel = ( + ctx: CommandExecutionContext, + config: MemberMessageConfig, +): string => { const roleIds = sanitizeMemberMessageRoleIds(config.autoRoleIds); if (roleIds.length === 0) { return ctx.ct("ui.autoRolesNotConfigured"); @@ -79,7 +93,9 @@ const panelContent = ( config: MemberMessageConfig, uiState: MemberMessagePanelUiState, ): string => { - const channelDisplay = config.channelId ? `<#${config.channelId}>` : ctx.ct("ui.channelNotConfigured"); + const channelDisplay = config.channelId + ? `<#${config.channelId}>` + : ctx.ct("ui.channelNotConfigured"); const lines = [ `## ${ctx.ct("ui.embed.title")}`, ctx.ct("ui.embed.description"), @@ -90,15 +106,23 @@ const panelContent = ( ]; if (kind === "welcome") { - lines.push(`${ctx.ct("ui.embed.fields.autoRoles")}: ${autoRolesLabel(ctx, config)}`); + lines.push( + `${ctx.ct("ui.embed.fields.autoRoles")}: ${autoRolesLabel(ctx, config)}`, + ); } if (uiState.channelPickerOpen) { - lines.push("", `${ctx.ct("ui.embed.fields.channelPicker")}: ${ctx.ct("ui.channelPickerHint")}`); + lines.push( + "", + `${ctx.ct("ui.embed.fields.channelPicker")}: ${ctx.ct("ui.channelPickerHint")}`, + ); } if (kind === "welcome" && uiState.rolePickerOpen) { - lines.push("", `${ctx.ct("ui.embed.fields.autoRoles")}: ${ctx.ct("ui.rolePickerHint")}`); + lines.push( + "", + `${ctx.ct("ui.embed.fields.autoRoles")}: ${ctx.ct("ui.rolePickerHint")}`, + ); } return lines.join("\n"); @@ -119,14 +143,30 @@ const buildContainer = ( .setDisabled(disabled); const channelButton = new ButtonBuilder() - .setCustomId(uiState.channelPickerOpen ? customIds.channelCancelButton : customIds.channelButton) - .setLabel(uiState.channelPickerOpen ? ctx.ct("ui.buttons.channelCancel") : ctx.ct("ui.buttons.channel")) + .setCustomId( + uiState.channelPickerOpen + ? customIds.channelCancelButton + : customIds.channelButton, + ) + .setLabel( + uiState.channelPickerOpen + ? ctx.ct("ui.buttons.channelCancel") + : ctx.ct("ui.buttons.channel"), + ) .setStyle(ButtonStyle.Secondary) .setDisabled(disabled); const roleButton = new ButtonBuilder() - .setCustomId(uiState.rolePickerOpen ? customIds.roleCancelButton : customIds.roleButton) - .setLabel(uiState.rolePickerOpen ? ctx.ct("ui.buttons.rolesCancel") : ctx.ct("ui.buttons.roles")) + .setCustomId( + uiState.rolePickerOpen + ? customIds.roleCancelButton + : customIds.roleButton, + ) + .setLabel( + uiState.rolePickerOpen + ? ctx.ct("ui.buttons.rolesCancel") + : ctx.ct("ui.buttons.roles"), + ) .setStyle(ButtonStyle.Secondary) .setDisabled(disabled); @@ -159,7 +199,9 @@ const buildContainer = ( const container = new ContainerBuilder(); container.addTextDisplayComponents( - new TextDisplayBuilder().setContent(panelContent(ctx, kind, config, uiState)), + new TextDisplayBuilder().setContent( + panelContent(ctx, kind, config, uiState), + ), ); if (kind === "welcome") { @@ -174,7 +216,11 @@ const buildContainer = ( ); } else { container.addActionRowComponents( - new ActionRowBuilder().addComponents(toggleButton, channelButton, testButton), + new ActionRowBuilder().addComponents( + toggleButton, + channelButton, + testButton, + ), ); } @@ -192,7 +238,9 @@ const buildContainer = ( .setChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement); container.addActionRowComponents( - new ActionRowBuilder().addComponents(channelSelect), + new ActionRowBuilder().addComponents( + channelSelect, + ), ); } @@ -275,12 +323,16 @@ export const createMemberMessagePanelExecute = ( await replyMessage .edit({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, kind, customIds, config, uiState, true)], + components: [ + buildContainer(ctx, kind, customIds, config, uiState, true), + ], }) .catch(() => undefined); }; - const collector = replyMessage.createMessageComponentCollector({ time: 15 * 60_000 }); + const collector = replyMessage.createMessageComponentCollector({ + time: 15 * 60_000, + }); await panelSessions.replace(sessionKey, { collector, disable: disablePanel, @@ -302,7 +354,9 @@ export const createMemberMessagePanelExecute = ( await saveConfig(); await interaction.update({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, kind, customIds, config, uiState)], + components: [ + buildContainer(ctx, kind, customIds, config, uiState), + ], }); return; } @@ -312,7 +366,9 @@ export const createMemberMessagePanelExecute = ( uiState.rolePickerOpen = false; await interaction.update({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, kind, customIds, config, uiState)], + components: [ + buildContainer(ctx, kind, customIds, config, uiState), + ], }); return; } @@ -321,36 +377,53 @@ export const createMemberMessagePanelExecute = ( uiState.channelPickerOpen = false; await interaction.update({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, kind, customIds, config, uiState)], + components: [ + buildContainer(ctx, kind, customIds, config, uiState), + ], }); return; } - if (kind === "welcome" && interaction.customId === customIds.roleButton) { + if ( + kind === "welcome" && + interaction.customId === customIds.roleButton + ) { uiState.rolePickerOpen = true; uiState.channelPickerOpen = false; await interaction.update({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, kind, customIds, config, uiState)], + components: [ + buildContainer(ctx, kind, customIds, config, uiState), + ], }); return; } - if (kind === "welcome" && interaction.customId === customIds.roleCancelButton) { + if ( + kind === "welcome" && + interaction.customId === customIds.roleCancelButton + ) { uiState.rolePickerOpen = false; await interaction.update({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, kind, customIds, config, uiState)], + components: [ + buildContainer(ctx, kind, customIds, config, uiState), + ], }); return; } - if (kind === "welcome" && interaction.customId === customIds.roleClearButton) { + if ( + kind === "welcome" && + interaction.customId === customIds.roleClearButton + ) { config.autoRoleIds = []; await saveConfig(); await interaction.update({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, kind, customIds, config, uiState)], + components: [ + buildContainer(ctx, kind, customIds, config, uiState), + ], }); return; } @@ -369,7 +442,9 @@ export const createMemberMessagePanelExecute = ( if (testResult.sent) { await interaction.editReply({ content: ctx.ct("responses.testSuccess", { - channel: testResult.channelId ? `<#${testResult.channelId}>` : ctx.ct("ui.channelNotConfigured"), + channel: testResult.channelId + ? `<#${testResult.channelId}>` + : ctx.ct("ui.channelNotConfigured"), }), }); return; @@ -388,7 +463,10 @@ export const createMemberMessagePanelExecute = ( return; } - if (interaction.isStringSelectMenu() && interaction.customId === customIds.typeSelect) { + if ( + interaction.isStringSelectMenu() && + interaction.customId === customIds.typeSelect + ) { const nextType = interaction.values[0]; if (!nextType || !isMemberMessageRenderTypeValue(nextType)) { await interaction.reply({ @@ -407,8 +485,15 @@ export const createMemberMessagePanelExecute = ( return; } - if (kind === "welcome" && interaction.isRoleSelectMenu() && interaction.customId === customIds.roleSelect) { - config.autoRoleIds = sanitizeMemberMessageRoleIds([...config.autoRoleIds, ...interaction.values]); + if ( + kind === "welcome" && + interaction.isRoleSelectMenu() && + interaction.customId === customIds.roleSelect + ) { + config.autoRoleIds = sanitizeMemberMessageRoleIds([ + ...config.autoRoleIds, + ...interaction.values, + ]); uiState.rolePickerOpen = false; await saveConfig(); await interaction.update({ @@ -418,7 +503,10 @@ export const createMemberMessagePanelExecute = ( return; } - if (interaction.isChannelSelectMenu() && interaction.customId === customIds.channelSelect) { + if ( + interaction.isChannelSelectMenu() && + interaction.customId === customIds.channelSelect + ) { const channelId = interaction.values[0]; if (!channelId) { await interaction.reply({ @@ -446,17 +534,21 @@ export const createMemberMessagePanelExecute = ( log.error({ kind, err: error }, "interaction failed"); if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: ctx.t("errors.execution"), - flags: [MessageFlags.Ephemeral], - }).catch(() => undefined); + await interaction + .reply({ + content: ctx.t("errors.execution"), + flags: [MessageFlags.Ephemeral], + }) + .catch(() => undefined); return; } - await interaction.followUp({ - content: ctx.t("errors.execution"), - flags: [MessageFlags.Ephemeral], - }).catch(() => undefined); + await interaction + .followUp({ + content: ctx.t("errors.execution"), + flags: [MessageFlags.Ephemeral], + }) + .catch(() => undefined); } }); diff --git a/apps/bot/src/legacy/features/memberMessages/imageRenderer.ts b/apps/bot/src/legacy/features/memberMessages/imageRenderer.ts index 8b34242..1685438 100644 --- a/apps/bot/src/legacy/features/memberMessages/imageRenderer.ts +++ b/apps/bot/src/legacy/features/memberMessages/imageRenderer.ts @@ -39,7 +39,10 @@ const wrapText = ( maxWidth: number, maxLines: number, ): string[] => { - const words = text.trim().split(/\s+/g).filter((value) => value.length > 0); + const words = text + .trim() + .split(/\s+/g) + .filter((value) => value.length > 0); if (words.length === 0) { return [""]; } @@ -78,11 +81,15 @@ const wrapText = ( const lastLine = lines[maxLines - 1]; if (lastLine && lines.length === maxLines) { let value = lastLine; - while (context.measureText(`${value}...`).width > maxWidth && value.length > 0) { + while ( + context.measureText(`${value}...`).width > maxWidth && + value.length > 0 + ) { value = value.slice(0, -1); } - lines[maxLines - 1] = value.length === lastLine.length ? value : `${value}...`; + lines[maxLines - 1] = + value.length === lastLine.length ? value : `${value}...`; } return lines; @@ -95,7 +102,13 @@ const drawAvatar = async ( ): Promise => { context.fillStyle = "#1f2937"; context.beginPath(); - context.arc(AVATAR_X + AVATAR_SIZE / 2, AVATAR_Y + AVATAR_SIZE / 2, AVATAR_SIZE / 2, 0, Math.PI * 2); + context.arc( + AVATAR_X + AVATAR_SIZE / 2, + AVATAR_Y + AVATAR_SIZE / 2, + AVATAR_SIZE / 2, + 0, + Math.PI * 2, + ); context.closePath(); context.fill(); @@ -103,7 +116,13 @@ const drawAvatar = async ( const avatar = await loadImage(avatarUrl); context.save(); context.beginPath(); - context.arc(AVATAR_X + AVATAR_SIZE / 2, AVATAR_Y + AVATAR_SIZE / 2, AVATAR_SIZE / 2, 0, Math.PI * 2); + context.arc( + AVATAR_X + AVATAR_SIZE / 2, + AVATAR_Y + AVATAR_SIZE / 2, + AVATAR_SIZE / 2, + 0, + Math.PI * 2, + ); context.closePath(); context.clip(); context.drawImage(avatar, AVATAR_X, AVATAR_Y, AVATAR_SIZE, AVATAR_SIZE); @@ -114,7 +133,11 @@ const drawAvatar = async ( context.font = `700 92px ${FONT_FAMILY}`; context.textAlign = "center"; context.textBaseline = "middle"; - context.fillText(initial, AVATAR_X + AVATAR_SIZE / 2, AVATAR_Y + AVATAR_SIZE / 2); + context.fillText( + initial, + AVATAR_X + AVATAR_SIZE / 2, + AVATAR_Y + AVATAR_SIZE / 2, + ); context.textAlign = "left"; context.textBaseline = "alphabetic"; } @@ -122,12 +145,20 @@ const drawAvatar = async ( context.strokeStyle = "rgba(248, 250, 252, 0.9)"; context.lineWidth = 6; context.beginPath(); - context.arc(AVATAR_X + AVATAR_SIZE / 2, AVATAR_Y + AVATAR_SIZE / 2, AVATAR_SIZE / 2 - 3, 0, Math.PI * 2); + context.arc( + AVATAR_X + AVATAR_SIZE / 2, + AVATAR_Y + AVATAR_SIZE / 2, + AVATAR_SIZE / 2 - 3, + 0, + Math.PI * 2, + ); context.closePath(); context.stroke(); }; -export const renderMemberMessageImage = async (input: MemberMessageImageInput): Promise => { +export const renderMemberMessageImage = async ( + input: MemberMessageImageInput, +): Promise => { const canvas = createCanvas(CARD_WIDTH, CARD_HEIGHT); const context = canvas.getContext("2d"); @@ -153,12 +184,26 @@ export const renderMemberMessageImage = async (input: MemberMessageImageInput): await drawAvatar(context, input.username, input.avatarUrl); - const headlineSize = fitFontSize(context, input.title, 84, 52, TEXT_MAX_WIDTH, 700); + const headlineSize = fitFontSize( + context, + input.title, + 84, + 52, + TEXT_MAX_WIDTH, + 700, + ); context.font = `700 ${headlineSize}px ${FONT_FAMILY}`; context.fillStyle = "#f8fafc"; context.fillText(input.title, TEXT_X, 116); - const subtitleSize = fitFontSize(context, input.subtitle, 58, 30, TEXT_MAX_WIDTH, 500); + const subtitleSize = fitFontSize( + context, + input.subtitle, + 58, + 30, + TEXT_MAX_WIDTH, + 500, + ); context.font = `500 ${subtitleSize}px ${FONT_FAMILY}`; context.fillStyle = "#e5e7eb"; @@ -169,7 +214,14 @@ export const renderMemberMessageImage = async (input: MemberMessageImageInput): context.fillText(line, TEXT_X, subtitleStartY + index * subtitleLineHeight); }); - const usernameSize = fitFontSize(context, input.username, 54, 28, TEXT_MAX_WIDTH, 700); + const usernameSize = fitFontSize( + context, + input.username, + 54, + 28, + TEXT_MAX_WIDTH, + 700, + ); context.font = `700 ${usernameSize}px ${FONT_FAMILY}`; context.fillStyle = input.kind === "welcome" ? "#93c5fd" : "#fda4af"; context.fillText(input.username, TEXT_X, CARD_HEIGHT - 34); diff --git a/apps/bot/src/legacy/features/memberMessages/repository.ts b/apps/bot/src/legacy/features/memberMessages/repository.ts index 88f8dcb..06b01fa 100644 --- a/apps/bot/src/legacy/features/memberMessages/repository.ts +++ b/apps/bot/src/legacy/features/memberMessages/repository.ts @@ -4,7 +4,16 @@ import type { } from "../../types/memberMessages.js"; export interface MemberMessageRepository { - getByBotGuildKind(botId: string, guildId: string, kind: MemberMessageKind): Promise; - upsertByBotGuildKind(botId: string, guildId: string, kind: MemberMessageKind, config: MemberMessageConfig): Promise; + getByBotGuildKind( + botId: string, + guildId: string, + kind: MemberMessageKind, + ): Promise; + upsertByBotGuildKind( + botId: string, + guildId: string, + kind: MemberMessageKind, + config: MemberMessageConfig, + ): Promise; deleteByBotGuild(botId: string, guildId: string): Promise; } diff --git a/apps/bot/src/legacy/features/memberMessages/service.ts b/apps/bot/src/legacy/features/memberMessages/service.ts index 8ab2b36..e3b0095 100644 --- a/apps/bot/src/legacy/features/memberMessages/service.ts +++ b/apps/bot/src/legacy/features/memberMessages/service.ts @@ -38,7 +38,9 @@ const hasSendMethod = (value: unknown): value is SendableChannel => { return false; } - return "send" in value && typeof (value as { send?: unknown }).send === "function"; + return ( + "send" in value && typeof (value as { send?: unknown }).send === "function" + ); }; const hasCode = (error: unknown): error is { code: number } => { @@ -46,10 +48,15 @@ const hasCode = (error: unknown): error is { code: number } => { return false; } - return "code" in error && typeof (error as { code?: unknown }).code === "number"; + return ( + "code" in error && typeof (error as { code?: unknown }).code === "number" + ); }; -const messageTemplateVars = (guild: Guild, user: User): Record => ({ +const messageTemplateVars = ( + guild: Guild, + user: User, +): Record => ({ user: `<@${user.id}>`, username: user.username, guild: guild.name, @@ -87,12 +94,17 @@ const resolveNonEmptyTemplate = ( return fallback.length > 0 ? fallback : key; }; -const resolveAssignableRoleId = async (member: GuildMember, roleId: string): Promise => { +const resolveAssignableRoleId = async ( + member: GuildMember, + roleId: string, +): Promise => { if (roleId === member.guild.id) { return null; } - const role = member.guild.roles.cache.get(roleId) ?? await member.guild.roles.fetch(roleId).catch(() => null); + const role = + member.guild.roles.cache.get(roleId) ?? + (await member.guild.roles.fetch(roleId).catch(() => null)); if (!role || !role.editable) { return null; } @@ -109,10 +121,11 @@ const buildMemberMessagePayload = async ( user: User, ): Promise => { const vars = messageTemplateVars(guild, user); - const allowedMentions: NonNullable = { - parse: [], - users: [user.id], - }; + const allowedMentions: NonNullable = + { + parse: [], + users: [user.id], + }; if (renderType === "simple") { return { @@ -128,7 +141,9 @@ const buildMemberMessagePayload = async ( new EmbedBuilder() .setColor(messageColor(kind)) .setTitle(resolveTemplate(i18n, lang, kind, "embedTitle", vars)) - .setDescription(resolveTemplate(i18n, lang, kind, "embedDescription", vars)), + .setDescription( + resolveTemplate(i18n, lang, kind, "embedDescription", vars), + ), ], }; } @@ -148,8 +163,20 @@ const buildMemberMessagePayload = async ( }; } - const imageTitle = resolveNonEmptyTemplate(i18n, lang, kind, "imageTitle", vars); - const imageSubtitle = resolveNonEmptyTemplate(i18n, lang, kind, "imageDescription", vars); + const imageTitle = resolveNonEmptyTemplate( + i18n, + lang, + kind, + "imageTitle", + vars, + ); + const imageSubtitle = resolveNonEmptyTemplate( + i18n, + lang, + kind, + "imageDescription", + vars, + ); const imageBuffer = await renderMemberMessageImage({ kind, @@ -171,11 +198,17 @@ const buildMemberMessagePayload = async ( export class MemberMessageService { public constructor(private readonly repository: MemberMessageRepository) {} - public resolveBotId(client: DispatchMemberMessageInput["client"]): string | null { + public resolveBotId( + client: DispatchMemberMessageInput["client"], + ): string | null { return client.user?.id ?? null; } - public async getConfig(botId: string, guildId: string, kind: MemberMessageKind) { + public async getConfig( + botId: string, + guildId: string, + kind: MemberMessageKind, + ) { return this.repository.getByBotGuildKind(botId, guildId, kind); } @@ -192,7 +225,9 @@ export class MemberMessageService { await this.repository.deleteByBotGuild(botId, guildId); } - public async assignWelcomeAutoRoles(input: AssignWelcomeAutoRolesInput): Promise { + public async assignWelcomeAutoRoles( + input: AssignWelcomeAutoRolesInput, + ): Promise { const botId = this.resolveBotId(input.client); if (!botId) { return { @@ -204,7 +239,11 @@ export class MemberMessageService { }; } - const config = await this.repository.getByBotGuildKind(botId, input.member.guild.id, "welcome"); + const config = await this.repository.getByBotGuildKind( + botId, + input.member.guild.id, + "welcome", + ); const configuredRoleIds = sanitizeMemberMessageRoleIds(config.autoRoleIds); if (configuredRoleIds.length === 0) { @@ -239,11 +278,17 @@ export class MemberMessageService { } const resolvedRoleIds = await Promise.all( - configuredRoleIds.map((roleId) => resolveAssignableRoleId(input.member, roleId)), + configuredRoleIds.map((roleId) => + resolveAssignableRoleId(input.member, roleId), + ), ); - const appliedRoleIds = resolvedRoleIds.filter((roleId): roleId is string => roleId !== null); - const skippedRoleIds = configuredRoleIds.filter((roleId) => !appliedRoleIds.includes(roleId)); + const appliedRoleIds = resolvedRoleIds.filter( + (roleId): roleId is string => roleId !== null, + ); + const skippedRoleIds = configuredRoleIds.filter( + (roleId) => !appliedRoleIds.includes(roleId), + ); if (appliedRoleIds.length === 0) { return { @@ -285,7 +330,9 @@ export class MemberMessageService { } } - public async dispatch(input: DispatchMemberMessageInput): Promise { + public async dispatch( + input: DispatchMemberMessageInput, + ): Promise { const botId = this.resolveBotId(input.client); if (!botId) { return { @@ -295,7 +342,11 @@ export class MemberMessageService { }; } - const config = await this.repository.getByBotGuildKind(botId, input.guild.id, input.kind); + const config = await this.repository.getByBotGuildKind( + botId, + input.guild.id, + input.kind, + ); if (!input.ignoreEnabled && !config.enabled) { return { @@ -313,7 +364,9 @@ export class MemberMessageService { }; } - const channel = await input.guild.channels.fetch(config.channelId).catch(() => null); + const channel = await input.guild.channels + .fetch(config.channelId) + .catch(() => null); if (!channel) { return { sent: false, @@ -331,9 +384,17 @@ export class MemberMessageService { } const me = input.guild.members.me; - if (me && "permissionsFor" in channel && typeof channel.permissionsFor === "function") { + if ( + me && + "permissionsFor" in channel && + typeof channel.permissionsFor === "function" + ) { const permissions = channel.permissionsFor(me); - if (!permissions || !permissions.has(PermissionFlagsBits.ViewChannel) || !permissions.has(PermissionFlagsBits.SendMessages)) { + if ( + !permissions || + !permissions.has(PermissionFlagsBits.ViewChannel) || + !permissions.has(PermissionFlagsBits.SendMessages) + ) { return { sent: false, reason: "missing_permissions", @@ -343,7 +404,14 @@ export class MemberMessageService { } const lang = input.i18n.resolveLang(input.guild.preferredLocale ?? null); - const payload = await buildMemberMessagePayload(input.i18n, lang, input.kind, config.messageType, input.guild, input.user); + const payload = await buildMemberMessagePayload( + input.i18n, + lang, + input.kind, + config.messageType, + input.guild, + input.user, + ); try { await channel.send(payload); diff --git a/apps/bot/src/legacy/features/presence/commandPanel.ts b/apps/bot/src/legacy/features/presence/commandPanel.ts index 98b41fa..18e40e6 100644 --- a/apps/bot/src/legacy/features/presence/commandPanel.ts +++ b/apps/bot/src/legacy/features/presence/commandPanel.ts @@ -47,8 +47,10 @@ const createCustomIds = (): PresenceCustomIds => { const statusLabel = (ctx: CommandExecutionContext, status: string): string => ctx.ct(`ui.status.options.${status}.label`); -const activityLabel = (ctx: CommandExecutionContext, activityType: string): string => - ctx.ct(`ui.activity.options.${activityType}.label`); +const activityLabel = ( + ctx: CommandExecutionContext, + activityType: string, +): string => ctx.ct(`ui.activity.options.${activityType}.label`); const panelContent = ( ctx: CommandExecutionContext, @@ -60,8 +62,13 @@ const panelContent = ( const templateText = service.getActiveTemplateText(state, runtimeState); const activityPreview = service.renderPreview(ctx.client, templateText); - const activityTexts = state.activity.texts.map((text, index) => `${index + 1}. ${text}`).join(" | "); - const currentIndex = Math.min(runtimeState.activePresenceTextIndex + 1, state.activity.texts.length); + const activityTexts = state.activity.texts + .map((text, index) => `${index + 1}. ${text}`) + .join(" | "); + const currentIndex = Math.min( + runtimeState.activePresenceTextIndex + 1, + state.activity.texts.length, + ); const summary = ctx.ct("responses.panel", { status: statusLabel(ctx, state.status), @@ -138,16 +145,23 @@ const buildContainer = ( new ActionRowBuilder().addComponents(statusSelect), ); container.addActionRowComponents( - new ActionRowBuilder().addComponents(activitySelect), + new ActionRowBuilder().addComponents( + activitySelect, + ), ); container.addActionRowComponents( - new ActionRowBuilder().addComponents(textButton, intervalButton), + new ActionRowBuilder().addComponents( + textButton, + intervalButton, + ), ); return container; }; -export const createPresenceCommandExecute = (presenceService: PresenceService) => { +export const createPresenceCommandExecute = ( + presenceService: PresenceService, +) => { return async (ctx: CommandExecutionContext): Promise => { const state = await presenceService.loadState(ctx.client); presenceService.applyState(ctx.client, state); @@ -172,13 +186,20 @@ export const createPresenceCommandExecute = (presenceService: PresenceService) = await replyMessage .edit({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, presenceService, state, customIds, true)], + components: [ + buildContainer(ctx, presenceService, state, customIds, true), + ], }) .catch(() => undefined); }; - const collector = replyMessage.createMessageComponentCollector({ time: 15 * 60_000 }); - await presenceService.replacePanelSession(sessionKey, { collector, disable: disablePanel }); + const collector = replyMessage.createMessageComponentCollector({ + time: 15 * 60_000, + }); + await presenceService.replacePanelSession(sessionKey, { + collector, + disable: disablePanel, + }); collector.on("collect", async (interaction) => { if (interaction.user.id !== ownerId) { @@ -194,7 +215,10 @@ export const createPresenceCommandExecute = (presenceService: PresenceService) = if (interaction.customId === customIds.statusSelect) { const nextStatus = interaction.values[0]; if (!nextStatus || !isPresenceStatusValue(nextStatus)) { - await interaction.reply({ content: ctx.ct("responses.invalidSelection"), flags: [MessageFlags.Ephemeral] }); + await interaction.reply({ + content: ctx.ct("responses.invalidSelection"), + flags: [MessageFlags.Ephemeral], + }); return; } @@ -202,7 +226,9 @@ export const createPresenceCommandExecute = (presenceService: PresenceService) = await presenceService.persistAndApply(ctx.client, state); await interaction.update({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, presenceService, state, customIds)], + components: [ + buildContainer(ctx, presenceService, state, customIds), + ], }); return; } @@ -210,7 +236,10 @@ export const createPresenceCommandExecute = (presenceService: PresenceService) = if (interaction.customId === customIds.activitySelect) { const nextType = interaction.values[0]; if (!nextType || !isPresenceActivityTypeValue(nextType)) { - await interaction.reply({ content: ctx.ct("responses.invalidSelection"), flags: [MessageFlags.Ephemeral] }); + await interaction.reply({ + content: ctx.ct("responses.invalidSelection"), + flags: [MessageFlags.Ephemeral], + }); return; } @@ -218,12 +247,17 @@ export const createPresenceCommandExecute = (presenceService: PresenceService) = await presenceService.persistAndApply(ctx.client, state); await interaction.update({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, presenceService, state, customIds)], + components: [ + buildContainer(ctx, presenceService, state, customIds), + ], }); return; } - await interaction.reply({ content: ctx.ct("responses.invalidSelection"), flags: [MessageFlags.Ephemeral] }); + await interaction.reply({ + content: ctx.ct("responses.invalidSelection"), + flags: [MessageFlags.Ephemeral], + }); return; } @@ -251,12 +285,14 @@ export const createPresenceCommandExecute = (presenceService: PresenceService) = const submitted = await interaction.awaitModalSubmit({ time: 120_000, filter: (modalInteraction) => - modalInteraction.customId === customIds.textModal - && modalInteraction.user.id === ownerId, + modalInteraction.customId === customIds.textModal && + modalInteraction.user.id === ownerId, }); const nextTexts = sanitizeActivityTexts( - submitted.fields.getTextInputValue(customIds.textInput).split(/\r?\n/g), + submitted.fields + .getTextInputValue(customIds.textInput) + .split(/\r?\n/g), ); state.activity.texts = nextTexts; @@ -269,13 +305,17 @@ export const createPresenceCommandExecute = (presenceService: PresenceService) = await replyMessage.edit({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, presenceService, state, customIds)], + components: [ + buildContainer(ctx, presenceService, state, customIds), + ], }); } catch { - await interaction.followUp({ - content: ctx.ct("responses.modalTimeout"), - flags: [MessageFlags.Ephemeral], - }).catch(() => undefined); + await interaction + .followUp({ + content: ctx.ct("responses.modalTimeout"), + flags: [MessageFlags.Ephemeral], + }) + .catch(() => undefined); } return; @@ -304,11 +344,13 @@ export const createPresenceCommandExecute = (presenceService: PresenceService) = const submitted = await interaction.awaitModalSubmit({ time: 120_000, filter: (modalInteraction) => - modalInteraction.customId === customIds.intervalModal - && modalInteraction.user.id === ownerId, + modalInteraction.customId === customIds.intervalModal && + modalInteraction.user.id === ownerId, }); - const rawSeconds = submitted.fields.getTextInputValue(customIds.intervalInput).trim(); + const rawSeconds = submitted.fields + .getTextInputValue(customIds.intervalInput) + .trim(); if (!/^\d+$/.test(rawSeconds)) { await submitted.reply({ content: ctx.ct("responses.invalidInterval", { @@ -339,33 +381,47 @@ export const createPresenceCommandExecute = (presenceService: PresenceService) = await replyMessage.edit({ flags: MessageFlags.IsComponentsV2, - components: [buildContainer(ctx, presenceService, state, customIds)], + components: [ + buildContainer(ctx, presenceService, state, customIds), + ], }); } catch { - await interaction.followUp({ - content: ctx.ct("responses.modalTimeout"), - flags: [MessageFlags.Ephemeral], - }).catch(() => undefined); + await interaction + .followUp({ + content: ctx.ct("responses.modalTimeout"), + flags: [MessageFlags.Ephemeral], + }) + .catch(() => undefined); } return; } - await interaction.reply({ content: ctx.ct("responses.invalidSelection"), flags: [MessageFlags.Ephemeral] }); + await interaction.reply({ + content: ctx.ct("responses.invalidSelection"), + flags: [MessageFlags.Ephemeral], + }); return; } - await interaction.reply({ content: ctx.ct("responses.invalidSelection"), flags: [MessageFlags.Ephemeral] }); + await interaction.reply({ + content: ctx.ct("responses.invalidSelection"), + flags: [MessageFlags.Ephemeral], + }); } catch (error) { log.error({ err: error }, "interaction failed"); const fallback = ctx.t("errors.execution"); if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ content: fallback, flags: [MessageFlags.Ephemeral] }).catch(() => undefined); + await interaction + .reply({ content: fallback, flags: [MessageFlags.Ephemeral] }) + .catch(() => undefined); return; } - await interaction.followUp({ content: fallback, flags: [MessageFlags.Ephemeral] }).catch(() => undefined); + await interaction + .followUp({ content: fallback, flags: [MessageFlags.Ephemeral] }) + .catch(() => undefined); } }); diff --git a/apps/bot/src/legacy/features/presence/service.ts b/apps/bot/src/legacy/features/presence/service.ts index 867f62b..a733d45 100644 --- a/apps/bot/src/legacy/features/presence/service.ts +++ b/apps/bot/src/legacy/features/presence/service.ts @@ -20,14 +20,15 @@ import { import type { PresenceRepository } from "./repository.js"; import type { PresencePanelSession, PresenceRuntimeState } from "./types.js"; -const DISCORD_ACTIVITY_TYPES: Record = { - PLAYING: ActivityType.Playing, - STREAMING: ActivityType.Streaming, - WATCHING: ActivityType.Watching, - LISTENING: ActivityType.Listening, - COMPETING: ActivityType.Competing, - CUSTOM: ActivityType.Custom, -}; +const DISCORD_ACTIVITY_TYPES: Record = + { + PLAYING: ActivityType.Playing, + STREAMING: ActivityType.Streaming, + WATCHING: ActivityType.Watching, + LISTENING: ActivityType.Listening, + COMPETING: ActivityType.Competing, + CUSTOM: ActivityType.Custom, + }; const createRuntimeState = (): PresenceRuntimeState => ({ dynamicPresenceRefreshTimer: null, @@ -47,13 +48,16 @@ const clearRuntimeTimers = (runtimeState: PresenceRuntimeState): void => { } }; -const resolveDiscordStatus = (status: PresenceStatusValue): "online" | "idle" | "dnd" | "invisible" => { +const resolveDiscordStatus = ( + status: PresenceStatusValue, +): "online" | "idle" | "dnd" | "invisible" => { return status === "streaming" ? "online" : status; }; export class PresenceService { private readonly runtimeByBotId = new Map(); - private readonly panelSessions = new ComponentSessionRegistry(); + private readonly panelSessions = + new ComponentSessionRegistry(); public constructor( private readonly repository: PresenceRepository, @@ -76,20 +80,33 @@ export class PresenceService { return next; } - public normalizeState(state: PresenceState, runtimeState: PresenceRuntimeState): void { + public normalizeState( + state: PresenceState, + runtimeState: PresenceRuntimeState, + ): void { const activityTexts = sanitizeActivityTexts(state.activity.texts); state.activity.texts = activityTexts; - state.activity.text = activityTexts[0] ?? sanitizeActivityText(state.activity.text); - state.activity.rotationIntervalSeconds = sanitizePresenceRotationIntervalSeconds(state.activity.rotationIntervalSeconds); + state.activity.text = + activityTexts[0] ?? sanitizeActivityText(state.activity.text); + state.activity.rotationIntervalSeconds = + sanitizePresenceRotationIntervalSeconds( + state.activity.rotationIntervalSeconds, + ); if (runtimeState.activePresenceTextIndex >= activityTexts.length) { runtimeState.activePresenceTextIndex = 0; } } - public getActiveTemplateText(state: PresenceState, runtimeState: PresenceRuntimeState): string { + public getActiveTemplateText( + state: PresenceState, + runtimeState: PresenceRuntimeState, + ): string { this.normalizeState(state, runtimeState); - return state.activity.texts[runtimeState.activePresenceTextIndex] ?? state.activity.text; + return ( + state.activity.texts[runtimeState.activePresenceTextIndex] ?? + state.activity.text + ); } public renderPreview(client: Client, templateText: string): string { @@ -111,7 +128,10 @@ export class PresenceService { this.syncDynamicPresenceTimers(client, state, runtimeState); } - public async persistAndApply(client: Client, state: PresenceState): Promise { + public async persistAndApply( + client: Client, + state: PresenceState, + ): Promise { this.applyState(client, state); const botId = this.resolveBotId(client); @@ -134,11 +154,17 @@ export class PresenceService { return `${this.resolveBotId(client) ?? "unbound"}:${userId}`; } - public async replacePanelSession(key: string, session: PresencePanelSession): Promise { + public async replacePanelSession( + key: string, + session: PresencePanelSession, + ): Promise { await this.panelSessions.replace(key, session); } - public deletePanelSessionIfCollectorMatch(key: string, collector: PresencePanelSession["collector"]): void { + public deletePanelSessionIfCollectorMatch( + key: string, + collector: PresencePanelSession["collector"], + ): void { this.panelSessions.deleteIfCollectorMatch(key, collector); } @@ -151,7 +177,11 @@ export class PresenceService { await this.panelSessions.stopAll("shutdown"); } - private applyPresenceState(client: Client, state: PresenceState, runtimeState: PresenceRuntimeState): void { + private applyPresenceState( + client: Client, + state: PresenceState, + runtimeState: PresenceRuntimeState, + ): void { if (!client.user) { return; } @@ -201,7 +231,11 @@ export class PresenceService { }); } - private syncDynamicPresenceTimers(client: Client, state: PresenceState, runtimeState: PresenceRuntimeState): void { + private syncDynamicPresenceTimers( + client: Client, + state: PresenceState, + runtimeState: PresenceRuntimeState, + ): void { this.normalizeState(state, runtimeState); clearRuntimeTimers(runtimeState); @@ -213,14 +247,18 @@ export class PresenceService { return; } - runtimeState.activePresenceTextIndex = (runtimeState.activePresenceTextIndex + 1) % state.activity.texts.length; + runtimeState.activePresenceTextIndex = + (runtimeState.activePresenceTextIndex + 1) % + state.activity.texts.length; this.applyPresenceState(client, state, runtimeState); }, state.activity.rotationIntervalSeconds * 1_000); runtimeState.presenceRotationTimer.unref?.(); } - const hasKnownVariable = state.activity.texts.some((templateText) => containsPresenceTemplateVariables(templateText)); + const hasKnownVariable = state.activity.texts.some((templateText) => + containsPresenceTemplateVariables(templateText), + ); if (!hasKnownVariable) { return; } diff --git a/apps/bot/src/legacy/features/presence/templateVariables.ts b/apps/bot/src/legacy/features/presence/templateVariables.ts index 0fad004..8aa325c 100644 --- a/apps/bot/src/legacy/features/presence/templateVariables.ts +++ b/apps/bot/src/legacy/features/presence/templateVariables.ts @@ -1,7 +1,10 @@ import type { Client } from "discord.js"; import { env } from "../../config/env.js"; -import { hasTemplateVariable, renderTemplate } from "../../utils/templateVariables.js"; +import { + hasTemplateVariable, + renderTemplate, +} from "../../utils/templateVariables.js"; import { sanitizeActivityText } from "../../validators/presence.js"; export const PRESENCE_TEMPLATE_REFRESH_INTERVAL_MS = 60_000; @@ -53,11 +56,19 @@ const formatUptime = (totalSeconds: number): string => { return parts.join(" "); }; -const buildPresenceTemplateValues = (client: Client): Record => { +const buildPresenceTemplateValues = ( + client: Client, +): Record => { const guildCount = client.guilds.cache.size; - const memberCount = client.guilds.cache.reduce((total, guild) => total + (guild.memberCount ?? 0), 0); + const memberCount = client.guilds.cache.reduce( + (total, guild) => total + (guild.memberCount ?? 0), + 0, + ); const channelCount = client.channels.cache.size; - const uptimeSeconds = Math.max(0, Math.floor((client.uptime ?? process.uptime() * 1_000) / 1_000)); + const uptimeSeconds = Math.max( + 0, + Math.floor((client.uptime ?? process.uptime() * 1_000) / 1_000), + ); return { bot_name: client.user?.username ?? "bot", @@ -72,18 +83,29 @@ const buildPresenceTemplateValues = (client: Client): Record => }; }; -export const renderPresenceTemplate = (client: Client, template: string): string => { +export const renderPresenceTemplate = ( + client: Client, + template: string, +): string => { const sanitizedTemplate = sanitizeActivityText(template); - const rendered = renderTemplate(sanitizedTemplate, buildPresenceTemplateValues(client), { - aliases: PRESENCE_TEMPLATE_VARIABLE_ALIASES, - keepUnknown: true, - }); + const rendered = renderTemplate( + sanitizedTemplate, + buildPresenceTemplateValues(client), + { + aliases: PRESENCE_TEMPLATE_VARIABLE_ALIASES, + keepUnknown: true, + }, + ); return sanitizeActivityText(rendered); }; export const containsPresenceTemplateVariables = (template: string): boolean => - hasTemplateVariable(template, PRESENCE_KNOWN_TEMPLATE_VARIABLES, PRESENCE_TEMPLATE_VARIABLE_ALIASES); + hasTemplateVariable( + template, + PRESENCE_KNOWN_TEMPLATE_VARIABLES, + PRESENCE_TEMPLATE_VARIABLE_ALIASES, + ); export const getPresenceTemplateHelpText = (): string => PRESENCE_VISIBLE_TEMPLATE_VARIABLES.map((name) => `{{${name}}}`).join(", "); diff --git a/apps/bot/src/legacy/handlers/commandExecutionContext.ts b/apps/bot/src/legacy/handlers/commandExecutionContext.ts index e57719f..d70b8ac 100644 --- a/apps/bot/src/legacy/handlers/commandExecutionContext.ts +++ b/apps/bot/src/legacy/handlers/commandExecutionContext.ts @@ -6,7 +6,10 @@ import type { TransportContext, TranslationVars, } from "../types/command.js"; -import type { BuildExecutionContextInput, HandlerExecutionDeps } from "../types/handlers.js"; +import type { + BuildExecutionContextInput, + HandlerExecutionDeps, +} from "../types/handlers.js"; import type { I18nService } from "../i18n/index.js"; import { createDiscordMemberPermissionsResolver } from "./discordPermissionResolver.js"; @@ -14,7 +17,8 @@ export const createTranslator = ( i18n: I18nService, lang: SupportedLang, ): ((key: string, vars?: TranslationVars) => string) => { - return (key: string, vars?: TranslationVars): string => i18n.t(lang, key, vars); + return (key: string, vars?: TranslationVars): string => + i18n.t(lang, key, vars); }; export const buildCommandExecutionContext = ( @@ -86,4 +90,4 @@ export const buildCommandExecutionContext = ( prefix: i18nContext.prefix, defaultLang: i18nContext.defaultLang, }; -}; \ No newline at end of file +}; diff --git a/apps/bot/src/legacy/handlers/discordPermissionResolver.ts b/apps/bot/src/legacy/handlers/discordPermissionResolver.ts index 927442c..8c75d09 100644 --- a/apps/bot/src/legacy/handlers/discordPermissionResolver.ts +++ b/apps/bot/src/legacy/handlers/discordPermissionResolver.ts @@ -7,7 +7,10 @@ import { import type { BuildExecutionContextInput } from "../types/handlers.js"; -const resolveFromGuild = async (guild: Guild | null, userId: string): Promise | null> => { +const resolveFromGuild = async ( + guild: Guild | null, + userId: string, +): Promise | null> => { if (!guild) { return null; } @@ -30,13 +33,17 @@ const resolveFromInteractionMember = ( const member = interaction.member; if (member && typeof member === "object" && "permissions" in member) { - const rawPermissions = (member as { permissions?: string | number | bigint }).permissions; + const rawPermissions = ( + member as { permissions?: string | number | bigint } + ).permissions; if (rawPermissions !== undefined) { try { - const normalized = typeof rawPermissions === "string" || typeof rawPermissions === "number" - ? BigInt(rawPermissions) - : rawPermissions; + const normalized = + typeof rawPermissions === "string" || + typeof rawPermissions === "number" + ? BigInt(rawPermissions) + : rawPermissions; return new PermissionsBitField(normalized); } catch { @@ -72,7 +79,9 @@ export const createDiscordMemberPermissionsResolver = ( return directFromMessage; } - const fallbackFromGuildMember = message.guild?.members.resolve(message.author.id)?.permissions; + const fallbackFromGuildMember = message.guild?.members.resolve( + message.author.id, + )?.permissions; if (fallbackFromGuildMember) { return fallbackFromGuildMember; } diff --git a/apps/bot/src/legacy/handlers/prefixHandler.ts b/apps/bot/src/legacy/handlers/prefixHandler.ts index ced0752..0acdef6 100644 --- a/apps/bot/src/legacy/handlers/prefixHandler.ts +++ b/apps/bot/src/legacy/handlers/prefixHandler.ts @@ -23,13 +23,19 @@ const shouldThrottlePrefixPreParse = (userId: string): boolean => { const now = Date.now(); const lastSeenAt = prefixPreParseThrottle.get(userId); - if (lastSeenAt !== undefined && now - lastSeenAt < PREFIX_PRE_PARSE_THROTTLE_MS) { + if ( + lastSeenAt !== undefined && + now - lastSeenAt < PREFIX_PRE_PARSE_THROTTLE_MS + ) { return true; } prefixPreParseThrottle.set(userId, now); - if (now - prefixPreParseThrottleLastSweepAt >= PREFIX_PRE_PARSE_THROTTLE_SWEEP_INTERVAL_MS) { + if ( + now - prefixPreParseThrottleLastSweepAt >= + PREFIX_PRE_PARSE_THROTTLE_SWEEP_INTERVAL_MS + ) { for (const [trackedUserId, trackedAt] of prefixPreParseThrottle.entries()) { if (now - trackedAt >= PREFIX_PRE_PARSE_THROTTLE_MAX_AGE_MS) { prefixPreParseThrottle.delete(trackedUserId); @@ -49,9 +55,11 @@ const resolvePrefixLang = ( fallbackLang: SupportedLang, guildPreferredLocale?: string | null, ): SupportedLang => { - const contextualLang = deps.i18n.resolveLang(guildPreferredLocale); - const contextualTrigger = deps.i18n.commandTrigger(contextualLang, command.meta.name); + const contextualTrigger = deps.i18n.commandTrigger( + contextualLang, + command.meta.name, + ); if (contextualTrigger === trigger) { return contextualLang; @@ -72,8 +80,11 @@ export const createPrefixHandler = (deps: PrefixHandlerDeps) => { } const firstSpaceIndex = content.indexOf(" "); - const trigger = (firstSpaceIndex === -1 ? content : content.slice(0, firstSpaceIndex)).toLowerCase(); - const rawArgs = firstSpaceIndex === -1 ? "" : content.slice(firstSpaceIndex + 1); + const trigger = ( + firstSpaceIndex === -1 ? content : content.slice(0, firstSpaceIndex) + ).toLowerCase(); + const rawArgs = + firstSpaceIndex === -1 ? "" : content.slice(firstSpaceIndex + 1); const match = deps.registry.findByAnyPrefixTrigger(trigger); if (!match) { @@ -87,7 +98,13 @@ export const createPrefixHandler = (deps: PrefixHandlerDeps) => { const reply = createPrefixReply(message); const command = match.command; - const lang = resolvePrefixLang(deps, command, trigger, match.lang, message.guild?.preferredLocale ?? null); + const lang = resolvePrefixLang( + deps, + command, + trigger, + match.lang, + message.guild?.preferredLocale ?? null, + ); const t = createTranslator(deps.i18n, lang); const parsed = await parsePrefixArgs(message, command.args, rawArgs); @@ -97,7 +114,13 @@ export const createPrefixHandler = (deps: PrefixHandlerDeps) => { return; } - const usage = buildPrefixUsage(command, deps.prefix, lang, deps.defaultLang, deps.i18n); + const usage = buildPrefixUsage( + command, + deps.prefix, + lang, + deps.defaultLang, + deps.i18n, + ); await reply(t(firstError.key, { ...(firstError.vars ?? {}), usage })); return; } diff --git a/apps/bot/src/legacy/handlers/replyAdapter.ts b/apps/bot/src/legacy/handlers/replyAdapter.ts index e1af9db..a2ca897 100644 --- a/apps/bot/src/legacy/handlers/replyAdapter.ts +++ b/apps/bot/src/legacy/handlers/replyAdapter.ts @@ -10,12 +10,16 @@ import type { PrefixReplyObject } from "../types/reply.js"; const PREFIX_EPHEMERAL_DELETE_DELAY_MS = 10_000; -const PREFIX_ALLOWED_MENTIONS_DEFAULT: NonNullable = { +const PREFIX_ALLOWED_MENTIONS_DEFAULT: NonNullable< + MessageReplyOptions["allowedMentions"] +> = { parse: [], repliedUser: false, }; -const SLASH_ALLOWED_MENTIONS_DEFAULT: NonNullable = { +const SLASH_ALLOWED_MENTIONS_DEFAULT: NonNullable< + InteractionReplyOptions["allowedMentions"] +> = { parse: [], }; @@ -27,8 +31,14 @@ const FLAG_NAME_TO_BITS: Record = { IsComponentsV2: 32768n, }; -const hasBitfieldLike = (value: unknown): value is { bitfield: bigint | number } => { - return Boolean(value) && typeof value === "object" && "bitfield" in (value as Record); +const hasBitfieldLike = ( + value: unknown, +): value is { bitfield: bigint | number } => { + return ( + Boolean(value) && + typeof value === "object" && + "bitfield" in (value as Record) + ); }; const toFlagBits = (flags: unknown): bigint | null => { @@ -72,7 +82,9 @@ const hasEphemeralInFlags = (flags: unknown): boolean => { return bits !== null && (bits & EPHEMERAL_FLAG) !== 0n; }; -const sanitizePrefixFlags = (flags: unknown): MessageReplyOptions["flags"] | undefined => { +const sanitizePrefixFlags = ( + flags: unknown, +): MessageReplyOptions["flags"] | undefined => { const bits = toFlagBits(flags); if (bits === null) { return undefined; @@ -97,7 +109,9 @@ const scheduleDelete = (message: Message): void => { }, PREFIX_EPHEMERAL_DELETE_DELAY_MS); }; -const withPrefixAllowedMentions = (options: MessageReplyOptions): MessageReplyOptions => { +const withPrefixAllowedMentions = ( + options: MessageReplyOptions, +): MessageReplyOptions => { return { ...options, allowedMentions: { @@ -107,7 +121,9 @@ const withPrefixAllowedMentions = (options: MessageReplyOptions): MessageReplyOp }; }; -const withSlashAllowedMentions = (options: InteractionReplyOptions): InteractionReplyOptions => { +const withSlashAllowedMentions = ( + options: InteractionReplyOptions, +): InteractionReplyOptions => { return { ...options, allowedMentions: { @@ -117,9 +133,13 @@ const withSlashAllowedMentions = (options: InteractionReplyOptions): Interaction }; }; -const toMessageReplyOptions = (payload: Exclude): MessageReplyOptions => { +const toMessageReplyOptions = ( + payload: Exclude, +): MessageReplyOptions => { const rest = { ...(payload as Record) }; - const sanitizedFlags = sanitizePrefixFlags((payload as InteractionReplyOptions).flags); + const sanitizedFlags = sanitizePrefixFlags( + (payload as InteractionReplyOptions).flags, + ); // Drop interaction-only fields so prefix replies stay valid message payloads. delete rest.fetchReply; @@ -134,14 +154,18 @@ const toMessageReplyOptions = (payload: Exclude): MessageR return rest as MessageReplyOptions; }; -export const createPrefixReply = (message: Message): ((payload: ReplyPayload) => Promise) => { +export const createPrefixReply = ( + message: Message, +): ((payload: ReplyPayload) => Promise) => { return async (payload: ReplyPayload): Promise => { if (typeof payload === "string") { return message.reply(withPrefixAllowedMentions({ content: payload })); } const shouldDeleteAfterDelay = hasEphemeral(payload); - const sent = await message.reply(withPrefixAllowedMentions(toMessageReplyOptions(payload))); + const sent = await message.reply( + withPrefixAllowedMentions(toMessageReplyOptions(payload)), + ); if (shouldDeleteAfterDelay) { scheduleDelete(sent); } @@ -163,7 +187,9 @@ export const createSlashReply = ( return interaction.reply(options); } - const options = withSlashAllowedMentions(payload as InteractionReplyOptions); + const options = withSlashAllowedMentions( + payload as InteractionReplyOptions, + ); if (interaction.replied || interaction.deferred) { return interaction.followUp(options); @@ -171,4 +197,4 @@ export const createSlashReply = ( return interaction.reply(options); }; -}; \ No newline at end of file +}; diff --git a/apps/bot/src/legacy/handlers/slashHandler.ts b/apps/bot/src/legacy/handlers/slashHandler.ts index 89c3852..8149bec 100644 --- a/apps/bot/src/legacy/handlers/slashHandler.ts +++ b/apps/bot/src/legacy/handlers/slashHandler.ts @@ -18,7 +18,9 @@ export const createSlashHandler = (deps: SlashHandlerDeps) => { return; } - const lang = deps.i18n.resolveLang(interaction.locale ?? interaction.guildLocale); + const lang = deps.i18n.resolveLang( + interaction.locale ?? interaction.guildLocale, + ); const reply = createSlashReply(interaction); const t = createTranslator(deps.i18n, lang); diff --git a/apps/bot/src/legacy/i18n/index.ts b/apps/bot/src/legacy/i18n/index.ts index 1d5255d..d934dbe 100644 --- a/apps/bot/src/legacy/i18n/index.ts +++ b/apps/bot/src/legacy/i18n/index.ts @@ -2,206 +2,229 @@ import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { SUPPORTED_LANGS, type SupportedLang, type TranslationVars } from "../types/command.js"; +import { + SUPPORTED_LANGS, + type SupportedLang, + type TranslationVars, +} from "../types/command.js"; import type { JsonObject } from "../types/i18n.js"; const DISCORD_LOCALE_MAP: Record = { - en: "en", - "en-us": "en", - "en-gb": "en", - es: "es", - "es-es": "es", - "es-419": "es", - de: "de", - "de-de": "de", - ja: "ja", - "ja-jp": "ja", - fr: "fr", - "fr-fr": "fr", - pt: "pt", - "pt-br": "pt", - "pt-pt": "pt", - ru: "ru", - "ru-ru": "ru", - it: "it", - "it-it": "it", - nl: "nl", - "nl-nl": "nl", - pl: "pl", - "pl-pl": "pl", - zh: "zh", - "zh-cn": "zh", - "zh-hans": "zh", - "zh-tw": "zh", - "zh-hant": "zh", - hi: "hi", - "hi-in": "hi", - ar: "ar", - "ar-sa": "ar", - bn: "bn", - "bn-bd": "bn", - "bn-in": "bn", - id: "id", - "id-id": "id", - tr: "tr", - "tr-tr": "tr", + en: "en", + "en-us": "en", + "en-gb": "en", + es: "es", + "es-es": "es", + "es-419": "es", + de: "de", + "de-de": "de", + ja: "ja", + "ja-jp": "ja", + fr: "fr", + "fr-fr": "fr", + pt: "pt", + "pt-br": "pt", + "pt-pt": "pt", + ru: "ru", + "ru-ru": "ru", + it: "it", + "it-it": "it", + nl: "nl", + "nl-nl": "nl", + pl: "pl", + "pl-pl": "pl", + zh: "zh", + "zh-cn": "zh", + "zh-hans": "zh", + "zh-tw": "zh", + "zh-hant": "zh", + hi: "hi", + "hi-in": "hi", + ar: "ar", + "ar-sa": "ar", + bn: "bn", + "bn-bd": "bn", + "bn-in": "bn", + id: "id", + "id-id": "id", + tr: "tr", + "tr-tr": "tr", }; const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url)); const LOCALE_DIR_CANDIDATES = [ - path.resolve(CURRENT_DIR, "..", "..", "..", "locales"), - path.resolve(process.cwd(), "apps", "bot", "locales"), - path.resolve(process.cwd(), "locales"), - CURRENT_DIR, - path.resolve(process.cwd(), "src", "legacy", "i18n"), - path.resolve(process.cwd(), "dist", "legacy", "i18n"), + path.resolve(CURRENT_DIR, "..", "..", "..", "locales"), + path.resolve(process.cwd(), "apps", "bot", "locales"), + path.resolve(process.cwd(), "locales"), + CURRENT_DIR, + path.resolve(process.cwd(), "src", "legacy", "i18n"), + path.resolve(process.cwd(), "dist", "legacy", "i18n"), ]; const resolveLocaleFilePath = (lang: SupportedLang): string => { - for (const directory of LOCALE_DIR_CANDIDATES) { - const filePath = path.join(directory, `${lang}.json`); - if (existsSync(filePath)) { - return filePath; - } - } + for (const directory of LOCALE_DIR_CANDIDATES) { + const filePath = path.join(directory, `${lang}.json`); + if (existsSync(filePath)) { + return filePath; + } + } - throw new Error(`[i18n] missing locale file for "${lang}"`); + throw new Error(`[i18n] missing locale file for "${lang}"`); }; export class I18nService { - private readonly dictionaries: Record; - private readonly fallbackLang: SupportedLang = "en"; + private readonly dictionaries: Record; + private readonly fallbackLang: SupportedLang = "en"; - public constructor(private readonly defaultLang: SupportedLang) { - this.dictionaries = this.loadDictionaries(); - } + public constructor(private readonly defaultLang: SupportedLang) { + this.dictionaries = this.loadDictionaries(); + } - public resolveLang(input?: string | null): SupportedLang { - if (!input) { - return this.fallbackLang; - } + public resolveLang(input?: string | null): SupportedLang { + if (!input) { + return this.fallbackLang; + } - const normalized = input.toLowerCase(); + const normalized = input.toLowerCase(); - const direct = DISCORD_LOCALE_MAP[normalized]; - if (direct) { - return direct; - } + const direct = DISCORD_LOCALE_MAP[normalized]; + if (direct) { + return direct; + } - const short = normalized.split("-")[0]; - const fromShort = short ? DISCORD_LOCALE_MAP[short] : undefined; - if (fromShort) { - return fromShort; - } + const short = normalized.split("-")[0]; + const fromShort = short ? DISCORD_LOCALE_MAP[short] : undefined; + if (fromShort) { + return fromShort; + } - return this.fallbackLang; - } + return this.fallbackLang; + } - public t(lang: SupportedLang, key: string, vars: TranslationVars = {}): string { - const fromLang = this.lookup(this.dictionaries[lang], key); - const fromFallback = this.lookup(this.dictionaries[this.fallbackLang], key); - const fromDefault = this.lookup(this.dictionaries[this.defaultLang], key); + public t( + lang: SupportedLang, + key: string, + vars: TranslationVars = {}, + ): string { + const fromLang = this.lookup(this.dictionaries[lang], key); + const fromFallback = this.lookup(this.dictionaries[this.fallbackLang], key); + const fromDefault = this.lookup(this.dictionaries[this.defaultLang], key); - const template = - typeof fromLang === "string" - ? fromLang - : typeof fromFallback === "string" - ? fromFallback - : typeof fromDefault === "string" - ? fromDefault - : key; + const template = + typeof fromLang === "string" + ? fromLang + : typeof fromFallback === "string" + ? fromFallback + : typeof fromDefault === "string" + ? fromDefault + : key; - return this.format(template, vars); - } + return this.format(template, vars); + } - public commandT(lang: SupportedLang, commandName: string, relativeKey: string, vars: TranslationVars = {}): string { - return this.t(lang, `${this.commandBaseKey(commandName)}.${relativeKey}`, vars); - } + public commandT( + lang: SupportedLang, + commandName: string, + relativeKey: string, + vars: TranslationVars = {}, + ): string { + return this.t( + lang, + `${this.commandBaseKey(commandName)}.${relativeKey}`, + vars, + ); + } - public commandName(lang: SupportedLang, commandName: string): string { - const key = `${this.commandBaseKey(commandName)}.name`; - const fromLang = this.lookup(this.dictionaries[lang], key); - if (typeof fromLang === "string" && fromLang.length > 0) { - return fromLang; - } + public commandName(lang: SupportedLang, commandName: string): string { + const key = `${this.commandBaseKey(commandName)}.name`; + const fromLang = this.lookup(this.dictionaries[lang], key); + if (typeof fromLang === "string" && fromLang.length > 0) { + return fromLang; + } - const fromFallback = this.lookup(this.dictionaries[this.fallbackLang], key); - if (typeof fromFallback === "string" && fromFallback.length > 0) { - return fromFallback; - } + const fromFallback = this.lookup(this.dictionaries[this.fallbackLang], key); + if (typeof fromFallback === "string" && fromFallback.length > 0) { + return fromFallback; + } - const fromDefault = this.lookup(this.dictionaries[this.defaultLang], key); - if (typeof fromDefault === "string" && fromDefault.length > 0) { - return fromDefault; - } + const fromDefault = this.lookup(this.dictionaries[this.defaultLang], key); + if (typeof fromDefault === "string" && fromDefault.length > 0) { + return fromDefault; + } - return commandName; - } + return commandName; + } - public commandTrigger(lang: SupportedLang, commandName: string): string { - return this.commandName(lang, commandName).trim().toLowerCase(); - } + public commandTrigger(lang: SupportedLang, commandName: string): string { + return this.commandName(lang, commandName).trim().toLowerCase(); + } - public commandObject(lang: SupportedLang, commandName: string): Record { - const key = this.commandBaseKey(commandName); - const fromLang = this.lookup(this.dictionaries[lang], key); - if (this.isObject(fromLang)) { - return fromLang; - } + public commandObject( + lang: SupportedLang, + commandName: string, + ): Record { + const key = this.commandBaseKey(commandName); + const fromLang = this.lookup(this.dictionaries[lang], key); + if (this.isObject(fromLang)) { + return fromLang; + } - const fromFallback = this.lookup(this.dictionaries[this.fallbackLang], key); - if (this.isObject(fromFallback)) { - return fromFallback; - } + const fromFallback = this.lookup(this.dictionaries[this.fallbackLang], key); + if (this.isObject(fromFallback)) { + return fromFallback; + } - const fromDefault = this.lookup(this.dictionaries[this.defaultLang], key); - if (this.isObject(fromDefault)) { - return fromDefault; - } + const fromDefault = this.lookup(this.dictionaries[this.defaultLang], key); + if (this.isObject(fromDefault)) { + return fromDefault; + } - return {}; - } + return {}; + } - public format(template: string, vars: TranslationVars = {}): string { - return this.interpolate(template, vars); - } + public format(template: string, vars: TranslationVars = {}): string { + return this.interpolate(template, vars); + } - private loadDictionaries(): Record { - return SUPPORTED_LANGS.reduce>((acc, lang) => { - const filePath = resolveLocaleFilePath(lang); - const raw = readFileSync(filePath, "utf-8"); - acc[lang] = JSON.parse(raw) as JsonObject; - return acc; - }, {} as Record); - } + private loadDictionaries(): Record { + return SUPPORTED_LANGS.reduce>( + (acc, lang) => { + const filePath = resolveLocaleFilePath(lang); + const raw = readFileSync(filePath, "utf-8"); + acc[lang] = JSON.parse(raw) as JsonObject; + return acc; + }, + {} as Record, + ); + } - private lookup(source: JsonObject, key: string): unknown { - const parts = key.split("."); - let current: unknown = source; + private lookup(source: JsonObject, key: string): unknown { + const parts = key.split("."); + let current: unknown = source; - for (const part of parts) { - if (!current || typeof current !== "object" || Array.isArray(current)) { - return undefined; - } + for (const part of parts) { + if (!current || typeof current !== "object" || Array.isArray(current)) { + return undefined; + } - current = (current as JsonObject)[part]; - } + current = (current as JsonObject)[part]; + } - return current; - } + return current; + } - private commandBaseKey(commandName: string): string { - return `commands.${commandName}`; - } + private commandBaseKey(commandName: string): string { + return `commands.${commandName}`; + } - private isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); - } + private isObject(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); + } - private interpolate(template: string, vars: TranslationVars): string { - return template.replace(/\{\{(\w+)\}\}/g, (_, variable: string) => { - const value = vars[variable]; - return value === undefined || value === null ? "" : String(value); - }); - } + private interpolate(template: string, vars: TranslationVars): string { + return template.replace(/\{\{(\w+)\}\}/g, (_, variable: string) => { + const value = vars[variable]; + return value === undefined || value === null ? "" : String(value); + }); + } } diff --git a/apps/bot/src/legacy/modules/help/service.ts b/apps/bot/src/legacy/modules/help/service.ts index cfbf4bb..3c1721d 100644 --- a/apps/bot/src/legacy/modules/help/service.ts +++ b/apps/bot/src/legacy/modules/help/service.ts @@ -6,12 +6,17 @@ import { resolvePrefixTrigger, resolveSlashName, } from "../../core/commands/usage.js"; -import type { BotCommand, CommandExecutionContext } from "../../types/command.js"; +import type { + BotCommand, + CommandExecutionContext, +} from "../../types/command.js"; const categoryName = (command: BotCommand): string => command.meta.category; -const commandDescription = (ctx: CommandExecutionContext, command: BotCommand): string => - ctx.i18n.commandT(ctx.lang, command.meta.name, "description"); +const commandDescription = ( + ctx: CommandExecutionContext, + command: BotCommand, +): string => ctx.i18n.commandT(ctx.lang, command.meta.name, "description"); export const resolveCommandFromQuery = ( ctx: CommandExecutionContext, @@ -41,12 +46,23 @@ export const createGlobalHelpEmbed = ( grouped.get(key)?.push(command); } - const helpUsage = buildPrefixUsage(helpCommand, ctx.prefix, ctx.lang, ctx.defaultLang, ctx.i18n); + const helpUsage = buildPrefixUsage( + helpCommand, + ctx.prefix, + ctx.lang, + ctx.defaultLang, + ctx.i18n, + ); const embed = new EmbedBuilder() .setColor(0x2b6cb0) .setTitle(ctx.ct("embed.title")) - .setDescription(ctx.ct("embed.description", { prefix: ctx.prefix, usage: `${helpUsage} ` })); + .setDescription( + ctx.ct("embed.description", { + prefix: ctx.prefix, + usage: `${helpUsage} `, + }), + ); for (const [category, categoryCommands] of grouped.entries()) { const lines = categoryCommands @@ -70,45 +86,85 @@ export const createCommandDetailsEmbed = ( ctx: CommandExecutionContext, command: BotCommand, ): EmbedBuilder => { - const usagePrefix = buildPrefixUsage(command, ctx.prefix, ctx.lang, ctx.defaultLang, ctx.i18n); + const usagePrefix = buildPrefixUsage( + command, + ctx.prefix, + ctx.lang, + ctx.defaultLang, + ctx.i18n, + ); const usageSlash = buildSlashUsage(command, ctx.lang, ctx.i18n); - const prefixTrigger = resolvePrefixTrigger(command, ctx.lang, ctx.defaultLang, ctx.i18n); + const prefixTrigger = resolvePrefixTrigger( + command, + ctx.lang, + ctx.defaultLang, + ctx.i18n, + ); const slashTrigger = resolveSlashName(command, ctx.lang, ctx.i18n); - const localizedCommandName = ctx.i18n.commandName(ctx.lang, command.meta.name); + const localizedCommandName = ctx.i18n.commandName( + ctx.lang, + command.meta.name, + ); - const args = command.args.length === 0 - ? ctx.ct("labels.noArgs") - : command.args - .map((arg) => { - const description = ctx.i18n.commandT(ctx.lang, command.meta.name, arg.descriptionKey); - const requirement = arg.required ? ctx.ct("labels.required") : ctx.ct("labels.optional"); - return `- ${arg.name} (${arg.type}, ${requirement}): ${description}`; - }) - .join("\n"); + const args = + command.args.length === 0 + ? ctx.ct("labels.noArgs") + : command.args + .map((arg) => { + const description = ctx.i18n.commandT( + ctx.lang, + command.meta.name, + arg.descriptionKey, + ); + const requirement = arg.required + ? ctx.ct("labels.required") + : ctx.ct("labels.optional"); + return `- ${arg.name} (${arg.type}, ${requirement}): ${description}`; + }) + .join("\n"); - const examples = command.examples.length === 0 - ? ctx.ct("labels.noExamples") - : command.examples - .map((example) => { - const source = example.source ?? "prefix"; - const baseUsage = source === "slash" - ? buildSlashUsage(command, ctx.lang, ctx.i18n) - : buildPrefixUsage(command, ctx.prefix, ctx.lang, ctx.defaultLang, ctx.i18n); + const examples = + command.examples.length === 0 + ? ctx.ct("labels.noExamples") + : command.examples + .map((example) => { + const source = example.source ?? "prefix"; + const baseUsage = + source === "slash" + ? buildSlashUsage(command, ctx.lang, ctx.i18n) + : buildPrefixUsage( + command, + ctx.prefix, + ctx.lang, + ctx.defaultLang, + ctx.i18n, + ); - const baseCommand = source === "slash" - ? `/${slashTrigger}` - : `${ctx.prefix}${prefixTrigger}`; + const baseCommand = + source === "slash" + ? `/${slashTrigger}` + : `${ctx.prefix}${prefixTrigger}`; - const input = example.args ? `${baseCommand} ${example.args}` : baseUsage; - const description = ctx.i18n.commandT(ctx.lang, command.meta.name, example.descriptionKey); - return `- ${input}: ${description}`; - }) - .join("\n"); + const input = example.args + ? `${baseCommand} ${example.args}` + : baseUsage; + const description = ctx.i18n.commandT( + ctx.lang, + command.meta.name, + example.descriptionKey, + ); + return `- ${input}: ${description}`; + }) + .join("\n"); return new EmbedBuilder() .setColor(0x2f855a) .setTitle(ctx.ct("embed.detailsTitle", { name: localizedCommandName })) - .setDescription(ctx.ct("embed.detailsDescription", { description: commandDescription(ctx, command) })) + .setDescription( + ctx.ct("embed.detailsDescription", { + description: commandDescription(ctx, command), + }), + ) .addFields( { name: ctx.ct("embed.fields.usage"), @@ -123,5 +179,7 @@ export const createCommandDetailsEmbed = ( value: examples, }, ) - .setFooter({ text: ctx.ct("embed.footer", { source: command.meta.category }) }); + .setFooter({ + text: ctx.ct("embed.footer", { source: command.meta.category }), + }); }; diff --git a/apps/bot/src/legacy/modules/logs/index.ts b/apps/bot/src/legacy/modules/logs/index.ts index 3c7de07..061de2a 100644 --- a/apps/bot/src/legacy/modules/logs/index.ts +++ b/apps/bot/src/legacy/modules/logs/index.ts @@ -1,3 +1,3 @@ export { createLogsCommandExecute } from "../../features/logs/commandPanel.js"; export type { LogEventRepository } from "../../features/logs/repository.js"; -export { LogEventService } from "../../features/logs/service.js"; \ No newline at end of file +export { LogEventService } from "../../features/logs/service.js"; diff --git a/apps/bot/src/legacy/types/argParser.ts b/apps/bot/src/legacy/types/argParser.ts index 41b9593..4019c8e 100644 --- a/apps/bot/src/legacy/types/argParser.ts +++ b/apps/bot/src/legacy/types/argParser.ts @@ -8,4 +8,4 @@ export interface ArgumentParseError { export interface ParsedArgumentsResult { values: Record; errors: ArgumentParseError[]; -} \ No newline at end of file +} diff --git a/apps/bot/src/legacy/types/command.ts b/apps/bot/src/legacy/types/command.ts index 82db8ea..f7f7167 100644 --- a/apps/bot/src/legacy/types/command.ts +++ b/apps/bot/src/legacy/types/command.ts @@ -36,10 +36,24 @@ export const SUPPORTED_LANGS = [ export type SupportedLang = (typeof SUPPORTED_LANGS)[number]; export type CommandSource = "prefix" | "slash"; -export type CommandArgType = "string" | "user" | "int" | "boolean" | "number" | "channel" | "role"; +export type CommandArgType = + | "string" + | "user" + | "int" + | "boolean" + | "number" + | "channel" + | "role"; -export type TranslationVars = Record; -export type ReplyPayload = string | MessageCreateOptions | MessageReplyOptions | InteractionReplyOptions; +export type TranslationVars = Record< + string, + string | number | boolean | null | undefined +>; +export type ReplyPayload = + | string + | MessageCreateOptions + | MessageReplyOptions + | InteractionReplyOptions; export type CommandArgValue = | string @@ -85,8 +99,16 @@ export interface CommandRegistryReader { export interface CommandI18nTools { commandName: (lang: SupportedLang, commandName: string) => string; commandTrigger: (lang: SupportedLang, commandName: string) => string; - commandT: (lang: SupportedLang, commandName: string, relativeKey: string, vars?: TranslationVars) => string; - commandObject: (lang: SupportedLang, commandName: string) => Record; + commandT: ( + lang: SupportedLang, + commandName: string, + relativeKey: string, + vars?: TranslationVars, + ) => string; + commandObject: ( + lang: SupportedLang, + commandName: string, + ) => Record; format: (template: string, vars?: TranslationVars) => string; } diff --git a/apps/bot/src/legacy/types/deploy.ts b/apps/bot/src/legacy/types/deploy.ts index da28ae8..736cf83 100644 --- a/apps/bot/src/legacy/types/deploy.ts +++ b/apps/bot/src/legacy/types/deploy.ts @@ -12,4 +12,4 @@ export interface DeployCommandsOptions { export interface DeployCommandsResult { scope: "guild" | "global"; count: number; -} \ No newline at end of file +} diff --git a/apps/bot/src/legacy/types/handlers.ts b/apps/bot/src/legacy/types/handlers.ts index 2c73f17..ba34213 100644 --- a/apps/bot/src/legacy/types/handlers.ts +++ b/apps/bot/src/legacy/types/handlers.ts @@ -35,4 +35,4 @@ export interface PrefixHandlerDeps extends HandlerExecutionDeps { export interface SlashHandlerDeps extends HandlerExecutionDeps { dispatcher: CommandDispatchPort; -} \ No newline at end of file +} diff --git a/apps/bot/src/legacy/types/i18n.ts b/apps/bot/src/legacy/types/i18n.ts index fb1394a..84f1655 100644 --- a/apps/bot/src/legacy/types/i18n.ts +++ b/apps/bot/src/legacy/types/i18n.ts @@ -1 +1 @@ -export type JsonObject = Record; \ No newline at end of file +export type JsonObject = Record; diff --git a/apps/bot/src/legacy/types/logs.ts b/apps/bot/src/legacy/types/logs.ts index a8bd6bf..b23407d 100644 --- a/apps/bot/src/legacy/types/logs.ts +++ b/apps/bot/src/legacy/types/logs.ts @@ -91,4 +91,4 @@ export interface LogChannelProvisionResult { createdCount: number; reusedCount: number; failedCategories: LogEventCategoryKey[]; -} \ No newline at end of file +} diff --git a/apps/bot/src/legacy/types/memberMessages.ts b/apps/bot/src/legacy/types/memberMessages.ts index f15a607..56a253e 100644 --- a/apps/bot/src/legacy/types/memberMessages.ts +++ b/apps/bot/src/legacy/types/memberMessages.ts @@ -10,7 +10,11 @@ import type { import type { I18nService } from "../i18n/index.js"; export type MemberMessageKind = "welcome" | "goodbye"; -export type MemberMessageRenderType = "simple" | "embed" | "container" | "image"; +export type MemberMessageRenderType = + | "simple" + | "embed" + | "container" + | "image"; export interface MemberMessageConfig { enabled: boolean; @@ -113,4 +117,4 @@ export interface MemberMessagePanelUiState { export interface MemberMessagePanelSession { collector: ReturnType; disable: () => Promise; -} \ No newline at end of file +} diff --git a/apps/bot/src/legacy/types/presence.ts b/apps/bot/src/legacy/types/presence.ts index a8e3043..6a98324 100644 --- a/apps/bot/src/legacy/types/presence.ts +++ b/apps/bot/src/legacy/types/presence.ts @@ -1,7 +1,18 @@ import type { Message } from "discord.js"; -export type PresenceStatusValue = "online" | "idle" | "dnd" | "invisible" | "streaming"; -export type PresenceActivityTypeValue = "PLAYING" | "STREAMING" | "WATCHING" | "LISTENING" | "COMPETING" | "CUSTOM"; +export type PresenceStatusValue = + | "online" + | "idle" + | "dnd" + | "invisible" + | "streaming"; +export type PresenceActivityTypeValue = + | "PLAYING" + | "STREAMING" + | "WATCHING" + | "LISTENING" + | "COMPETING" + | "CUSTOM"; export interface PresenceActivityState { type: PresenceActivityTypeValue; @@ -46,4 +57,4 @@ export interface PresenceRuntimeState { activePanelsByUserId: Map; } -export type DiscordPresenceStatus = "online" | "idle" | "dnd" | "invisible"; \ No newline at end of file +export type DiscordPresenceStatus = "online" | "idle" | "dnd" | "invisible"; diff --git a/apps/bot/src/legacy/types/reply.ts b/apps/bot/src/legacy/types/reply.ts index c09c69d..e75ce5b 100644 --- a/apps/bot/src/legacy/types/reply.ts +++ b/apps/bot/src/legacy/types/reply.ts @@ -1,3 +1,3 @@ import type { ReplyPayload } from "./command.js"; -export type PrefixReplyObject = Exclude; \ No newline at end of file +export type PrefixReplyObject = Exclude; diff --git a/apps/bot/src/legacy/types/templateVariables.ts b/apps/bot/src/legacy/types/templateVariables.ts index 2a33846..4db141d 100644 --- a/apps/bot/src/legacy/types/templateVariables.ts +++ b/apps/bot/src/legacy/types/templateVariables.ts @@ -1,4 +1,4 @@ export interface TemplateRenderOptions { aliases?: Record; keepUnknown?: boolean; -} \ No newline at end of file +} diff --git a/apps/bot/src/legacy/utils/templateVariables.ts b/apps/bot/src/legacy/utils/templateVariables.ts index cae9818..a06aa00 100644 --- a/apps/bot/src/legacy/utils/templateVariables.ts +++ b/apps/bot/src/legacy/utils/templateVariables.ts @@ -2,20 +2,29 @@ import type { TemplateRenderOptions } from "../types/templateVariables.js"; const createTemplateTokenRegex = (): RegExp => /\{\{([a-zA-Z0-9_]+)\}\}/g; -const normalizeVariableName = (rawName: string): string => rawName.toLowerCase(); +const normalizeVariableName = (rawName: string): string => + rawName.toLowerCase(); -const normalizeAliases = (aliases: Record = {}): Record => +const normalizeAliases = ( + aliases: Record = {}, +): Record => Object.entries(aliases).reduce>((acc, [from, to]) => { acc[normalizeVariableName(from)] = normalizeVariableName(to); return acc; }, {}); -const resolveVariableName = (rawName: string, aliases: Record): string => { +const resolveVariableName = ( + rawName: string, + aliases: Record, +): string => { const normalized = normalizeVariableName(rawName); return aliases[normalized] ?? normalized; }; -export const extractTemplateVariables = (template: string, aliases: Record = {}): string[] => { +export const extractTemplateVariables = ( + template: string, + aliases: Record = {}, +): string[] => { const normalizedAliases = normalizeAliases(aliases); const found = new Set(); @@ -63,7 +72,9 @@ export const renderTemplate = ( ): string => { const normalizedAliases = normalizeAliases(options.aliases); const keepUnknown = options.keepUnknown ?? true; - const normalizedValues = Object.entries(values).reduce>((acc, [key, value]) => { + const normalizedValues = Object.entries(values).reduce< + Record + >((acc, [key, value]) => { acc[normalizeVariableName(key)] = value; return acc; }, {}); diff --git a/apps/bot/src/legacy/validators/logs.ts b/apps/bot/src/legacy/validators/logs.ts index 4c718a8..229d5da 100644 --- a/apps/bot/src/legacy/validators/logs.ts +++ b/apps/bot/src/legacy/validators/logs.ts @@ -24,7 +24,9 @@ export const createDefaultLogEventState = (): LogEventStateByKey => { return state; }; -export const cloneLogEventState = (state: LogEventStateByKey): LogEventStateByKey => { +export const cloneLogEventState = ( + state: LogEventStateByKey, +): LogEventStateByKey => { const next = {} as LogEventStateByKey; for (const eventKey of LOG_EVENT_KEYS) { @@ -42,7 +44,9 @@ export const isLogEventKey = (value: string): value is LogEventKey => { return LOG_EVENT_KEYS_SET.has(value as LogEventKey); }; -export const mergeLogEventRowsIntoState = (rows: readonly LogEventRow[]): LogEventStateByKey => { +export const mergeLogEventRowsIntoState = ( + rows: readonly LogEventRow[], +): LogEventStateByKey => { const state = createDefaultLogEventState(); for (const row of rows) { @@ -57,4 +61,4 @@ export const mergeLogEventRowsIntoState = (rows: readonly LogEventRow[]): LogEve } return state; -}; \ No newline at end of file +}; diff --git a/apps/bot/src/legacy/validators/memberMessages.ts b/apps/bot/src/legacy/validators/memberMessages.ts index d2bf99e..9b13757 100644 --- a/apps/bot/src/legacy/validators/memberMessages.ts +++ b/apps/bot/src/legacy/validators/memberMessages.ts @@ -6,13 +6,24 @@ import type { const DISCORD_SNOWFLAKE_REGEX = /^\d{17,20}$/; -export const MEMBER_MESSAGE_KINDS: readonly MemberMessageKind[] = ["welcome", "goodbye"]; +export const MEMBER_MESSAGE_KINDS: readonly MemberMessageKind[] = [ + "welcome", + "goodbye", +]; -export const MEMBER_MESSAGE_RENDER_TYPES: readonly MemberMessageRenderType[] = ["simple", "embed", "container", "image"]; +export const MEMBER_MESSAGE_RENDER_TYPES: readonly MemberMessageRenderType[] = [ + "simple", + "embed", + "container", + "image", +]; -export const DEFAULT_MEMBER_MESSAGE_RENDER_TYPE: MemberMessageRenderType = "simple"; +export const DEFAULT_MEMBER_MESSAGE_RENDER_TYPE: MemberMessageRenderType = + "simple"; -export const sanitizeMemberMessageRoleIds = (roleIds: readonly string[]): string[] => { +export const sanitizeMemberMessageRoleIds = ( + roleIds: readonly string[], +): string[] => { const uniqueRoleIds = new Set(); for (const rawRoleId of roleIds) { @@ -34,10 +45,14 @@ export const createDefaultMemberMessageConfig = (): MemberMessageConfig => ({ autoRoleIds: [], }); -export const isMemberMessageKindValue = (value: string): value is MemberMessageKind => { +export const isMemberMessageKindValue = ( + value: string, +): value is MemberMessageKind => { return MEMBER_MESSAGE_KINDS.includes(value as MemberMessageKind); }; -export const isMemberMessageRenderTypeValue = (value: string): value is MemberMessageRenderType => { +export const isMemberMessageRenderTypeValue = ( + value: string, +): value is MemberMessageRenderType => { return MEMBER_MESSAGE_RENDER_TYPES.includes(value as MemberMessageRenderType); }; diff --git a/apps/bot/src/legacy/validators/presence.ts b/apps/bot/src/legacy/validators/presence.ts index 2dc28b2..ed823e2 100644 --- a/apps/bot/src/legacy/validators/presence.ts +++ b/apps/bot/src/legacy/validators/presence.ts @@ -4,7 +4,13 @@ import type { PresenceStatusValue, } from "../types/presence.js"; -export const PRESENCE_STATUSES: readonly PresenceStatusValue[] = ["online", "idle", "dnd", "invisible", "streaming"]; +export const PRESENCE_STATUSES: readonly PresenceStatusValue[] = [ + "online", + "idle", + "dnd", + "invisible", + "streaming", +]; export const PRESENCE_ACTIVITY_TYPES: readonly PresenceActivityTypeValue[] = [ "PLAYING", "STREAMING", @@ -30,10 +36,14 @@ export const createDefaultPresenceState = (): PresenceState => ({ }, }); -export const isPresenceStatusValue = (value: string): value is PresenceStatusValue => +export const isPresenceStatusValue = ( + value: string, +): value is PresenceStatusValue => PRESENCE_STATUSES.includes(value as PresenceStatusValue); -export const isPresenceActivityTypeValue = (value: string): value is PresenceActivityTypeValue => +export const isPresenceActivityTypeValue = ( + value: string, +): value is PresenceActivityTypeValue => PRESENCE_ACTIVITY_TYPES.includes(value as PresenceActivityTypeValue); export const sanitizeActivityText = (value: string): string => { @@ -59,7 +69,9 @@ export const sanitizeActivityTexts = (values: readonly string[]): string[] => { return cleaned; }; -export const sanitizePresenceRotationIntervalSeconds = (value: number): number => { +export const sanitizePresenceRotationIntervalSeconds = ( + value: number, +): number => { if (!Number.isFinite(value)) { return DEFAULT_ACTIVITY_ROTATION_INTERVAL_SECONDS; } @@ -76,10 +88,12 @@ export const sanitizePresenceRotationIntervalSeconds = (value: number): number = return normalized; }; -export const isPresenceRotationIntervalSecondsValue = (value: number): boolean => - Number.isInteger(value) - && value >= MIN_ACTIVITY_ROTATION_INTERVAL_SECONDS - && value <= MAX_ACTIVITY_ROTATION_INTERVAL_SECONDS; +export const isPresenceRotationIntervalSecondsValue = ( + value: number, +): boolean => + Number.isInteger(value) && + value >= MIN_ACTIVITY_ROTATION_INTERVAL_SECONDS && + value <= MAX_ACTIVITY_ROTATION_INTERVAL_SECONDS; const isRecord = (value: unknown): value is Record => Boolean(value) && typeof value === "object" && !Array.isArray(value); @@ -92,12 +106,19 @@ export const parsePresenceState = (value: unknown): PresenceState | null => { const statusValue = value.status; const activityValue = value.activity; - if (typeof statusValue !== "string" || !isPresenceStatusValue(statusValue) || !isRecord(activityValue)) { + if ( + typeof statusValue !== "string" || + !isPresenceStatusValue(statusValue) || + !isRecord(activityValue) + ) { return null; } const activityType = activityValue.type; - if (typeof activityType !== "string" || !isPresenceActivityTypeValue(activityType)) { + if ( + typeof activityType !== "string" || + !isPresenceActivityTypeValue(activityType) + ) { return null; } @@ -106,7 +127,11 @@ export const parsePresenceState = (value: unknown): PresenceState | null => { const intervalValue = activityValue.rotationIntervalSeconds; const activityTexts = Array.isArray(activityTextsValue) - ? sanitizeActivityTexts(activityTextsValue.filter((entry): entry is string => typeof entry === "string")) + ? sanitizeActivityTexts( + activityTextsValue.filter( + (entry): entry is string => typeof entry === "string", + ), + ) : typeof legacyActivityTextValue === "string" ? sanitizeActivityTexts([legacyActivityTextValue]) : null; @@ -115,9 +140,10 @@ export const parsePresenceState = (value: unknown): PresenceState | null => { return null; } - const rotationIntervalSeconds = typeof intervalValue === "number" - ? sanitizePresenceRotationIntervalSeconds(intervalValue) - : DEFAULT_ACTIVITY_ROTATION_INTERVAL_SECONDS; + const rotationIntervalSeconds = + typeof intervalValue === "number" + ? sanitizePresenceRotationIntervalSeconds(intervalValue) + : DEFAULT_ACTIVITY_ROTATION_INTERVAL_SECONDS; return { status: statusValue, diff --git a/apps/bot/src/manager/BotManager.ts b/apps/bot/src/manager/BotManager.ts index 9ecc1fe..5028b25 100644 --- a/apps/bot/src/manager/BotManager.ts +++ b/apps/bot/src/manager/BotManager.ts @@ -54,20 +54,23 @@ export class BotManager { const persistedBots = await listBotsToRestore(this.pool); for (const bot of persistedBots) { - await this.startBot(bot.id, bot.tenantId).catch(async (error: unknown) => { - const message = error instanceof Error ? error.message : "Unknown startup error"; + await this.startBot(bot.id, bot.tenantId).catch( + async (error: unknown) => { + const message = + error instanceof Error ? error.message : "Unknown startup error"; - await setBotStatusById(this.pool, bot.id, "error", message); - await insertRuntimeEvent(this.pool, { - tenantId: bot.tenantId, - botId: bot.id, - level: "error", - message: "Bot failed to restore at startup", - metadata: { - error: message, - }, - }); - }); + await setBotStatusById(this.pool, bot.id, "error", message); + await insertRuntimeEvent(this.pool, { + tenantId: bot.tenantId, + botId: bot.id, + level: "error", + message: "Bot failed to restore at startup", + metadata: { + error: message, + }, + }); + }, + ); } } @@ -156,7 +159,8 @@ export class BotManager { client.destroy(); this.runningBots.delete(botId); - const message = error instanceof Error ? error.message : "Unknown login error"; + const message = + error instanceof Error ? error.message : "Unknown login error"; await setBotStatusById(this.pool, botId, "error", message); await insertRuntimeEvent(this.pool, { tenantId: bot.tenantId, @@ -215,7 +219,10 @@ export class BotManager { } } - private async withLock(botId: string, operation: () => Promise): Promise { + private async withLock( + botId: string, + operation: () => Promise, + ): Promise { const previous = this.operationLocks.get(botId) ?? Promise.resolve(); const next = previous diff --git a/apps/bot/src/queue/worker.ts b/apps/bot/src/queue/worker.ts index d1a3508..539c6ea 100644 --- a/apps/bot/src/queue/worker.ts +++ b/apps/bot/src/queue/worker.ts @@ -9,7 +9,10 @@ import { import { BotManager } from "../manager/BotManager.js"; -export const createBotControlWorker = (redis: Redis, manager: BotManager): Worker => { +export const createBotControlWorker = ( + redis: Redis, + manager: BotManager, +): Worker => { return new Worker( BOT_CONTROL_QUEUE, async (job) => { diff --git a/apps/bot/src/runtime/initializeLegacyBotRuntime.ts b/apps/bot/src/runtime/initializeLegacyBotRuntime.ts index f12a281..69c7a0b 100644 --- a/apps/bot/src/runtime/initializeLegacyBotRuntime.ts +++ b/apps/bot/src/runtime/initializeLegacyBotRuntime.ts @@ -6,12 +6,8 @@ import { createCommandList } from "../legacy/commands/index.js"; import { env } from "../legacy/config/env.js"; import { CommandRegistry } from "../legacy/core/commands/registry.js"; import { LocalCommandDispatchPort } from "../legacy/core/execution/dispatch.js"; -import { - MemoryCooldownStore, -} from "../legacy/core/execution/cooldownStore.js"; -import { - MemoryGlobalRateLimitStore, -} from "../legacy/core/execution/globalRateLimitStore.js"; +import { MemoryCooldownStore } from "../legacy/core/execution/cooldownStore.js"; +import { MemoryGlobalRateLimitStore } from "../legacy/core/execution/globalRateLimitStore.js"; import { CommandExecutor } from "../legacy/core/execution/CommandExecutor.js"; import { createScopedLogger } from "../legacy/core/logging/logger.js"; import { @@ -23,15 +19,9 @@ import { registerEvents } from "../legacy/events/index.js"; import { createPrefixHandler } from "../legacy/handlers/prefixHandler.js"; import { createSlashHandler } from "../legacy/handlers/slashHandler.js"; import { I18nService } from "../legacy/i18n/index.js"; -import { - LogEventService, -} from "../legacy/modules/logs/index.js"; -import { - MemberMessageService, -} from "../legacy/modules/memberMessages/index.js"; -import { - PresenceService, -} from "../legacy/modules/presence/index.js"; +import { LogEventService } from "../legacy/modules/logs/index.js"; +import { MemberMessageService } from "../legacy/modules/memberMessages/index.js"; +import { PresenceService } from "../legacy/modules/presence/index.js"; import { TenantLogEventStore } from "./stores/TenantLogEventStore.js"; import { TenantMemberMessageStore } from "./stores/TenantMemberMessageStore.js"; import { TenantPresenceStore } from "./stores/TenantPresenceStore.js"; @@ -54,9 +44,21 @@ export interface LegacyBotRuntime { export const initializeLegacyBotRuntime = async ( input: InitializeLegacyBotRuntimeInput, ): Promise => { - const presenceStore = new TenantPresenceStore(input.pool, input.tenantId, input.ownerUserId); - const memberMessageStore = new TenantMemberMessageStore(input.pool, input.tenantId, input.ownerUserId); - const logEventStore = new TenantLogEventStore(input.pool, input.tenantId, input.ownerUserId); + const presenceStore = new TenantPresenceStore( + input.pool, + input.tenantId, + input.ownerUserId, + ); + const memberMessageStore = new TenantMemberMessageStore( + input.pool, + input.tenantId, + input.ownerUserId, + ); + const logEventStore = new TenantLogEventStore( + input.pool, + input.tenantId, + input.ownerUserId, + ); const dbLifecycle = new DatabaseLifecycle( [ @@ -70,7 +72,10 @@ export const initializeLegacyBotRuntime = async ( await dbLifecycle.init(); const services: AppFeatureServices = { - presenceService: new PresenceService(presenceStore, env.PRESENCE_STREAM_URL), + presenceService: new PresenceService( + presenceStore, + env.PRESENCE_STREAM_URL, + ), memberMessageService: new MemberMessageService(memberMessageStore), logEventService: new LogEventService(logEventStore), }; @@ -137,15 +142,36 @@ export const initializeLegacyBotRuntime = async ( return { shutdown: async () => { await services.logEventService.shutdown().catch((error) => { - log.error({ err: error, tenantId: input.tenantId, botClientId: input.botClientId }, "logs service shutdown failed"); + log.error( + { + err: error, + tenantId: input.tenantId, + botClientId: input.botClientId, + }, + "logs service shutdown failed", + ); }); await services.presenceService.shutdown().catch((error) => { - log.error({ err: error, tenantId: input.tenantId, botClientId: input.botClientId }, "presence service shutdown failed"); + log.error( + { + err: error, + tenantId: input.tenantId, + botClientId: input.botClientId, + }, + "presence service shutdown failed", + ); }); await dbLifecycle.shutdown().catch((error) => { - log.error({ err: error, tenantId: input.tenantId, botClientId: input.botClientId }, "runtime db lifecycle shutdown failed"); + log.error( + { + err: error, + tenantId: input.tenantId, + botClientId: input.botClientId, + }, + "runtime db lifecycle shutdown failed", + ); }); }, }; diff --git a/apps/bot/src/runtime/stores/TenantLogEventStore.ts b/apps/bot/src/runtime/stores/TenantLogEventStore.ts index 68246c0..73cd8ad 100644 --- a/apps/bot/src/runtime/stores/TenantLogEventStore.ts +++ b/apps/bot/src/runtime/stores/TenantLogEventStore.ts @@ -34,13 +34,16 @@ export class TenantLogEventStore implements LogEventRepository { await this.pool.query(logEventSchemaProbeSql); } catch (error) { throw new Error( - "[db:init] missing or incompatible table \"bot_log_event_configs\". Run migrations with \"npm run migrate\".", + '[db:init] missing or incompatible table "bot_log_event_configs". Run migrations with "npm run migrate".', { cause: error }, ); } } - public async listByBotGuild(botId: string, guildId: string): Promise { + public async listByBotGuild( + botId: string, + guildId: string, + ): Promise { const result = await this.pool.query( ` SELECT event_key, enabled, channel_id diff --git a/apps/bot/src/runtime/stores/TenantMemberMessageStore.ts b/apps/bot/src/runtime/stores/TenantMemberMessageStore.ts index f621f98..a92c84a 100644 --- a/apps/bot/src/runtime/stores/TenantMemberMessageStore.ts +++ b/apps/bot/src/runtime/stores/TenantMemberMessageStore.ts @@ -39,7 +39,9 @@ const parseRoleIds = (serialized: string | null): string[] => { return []; } - const roleIds = parsed.filter((value): value is string => typeof value === "string"); + const roleIds = parsed.filter( + (value): value is string => typeof value === "string", + ); return sanitizeMemberMessageRoleIds(roleIds); } catch { return []; @@ -52,7 +54,9 @@ const toConfig = (row: MemberMessageRow): MemberMessageConfig => { return { enabled: row.enabled, channelId: row.channel_id, - messageType: isMemberMessageRenderTypeValue(row.message_type) ? row.message_type : fallback.messageType, + messageType: isMemberMessageRenderTypeValue(row.message_type) + ? row.message_type + : fallback.messageType, autoRoleIds: parseRoleIds(row.auto_role_ids), }; }; @@ -69,13 +73,17 @@ export class TenantMemberMessageStore implements MemberMessageRepository { await this.pool.query(memberMessageSchemaProbeSql); } catch (error) { throw new Error( - "[db:init] missing or incompatible table \"bot_member_message_configs\". Run migrations with \"npm run migrate\".", + '[db:init] missing or incompatible table "bot_member_message_configs". Run migrations with "npm run migrate".', { cause: error }, ); } } - public async getByBotGuildKind(botId: string, guildId: string, kind: MemberMessageKind): Promise { + public async getByBotGuildKind( + botId: string, + guildId: string, + kind: MemberMessageKind, + ): Promise { const result = await this.pool.query( ` SELECT enabled, channel_id, message_type, auto_role_ids diff --git a/apps/bot/src/runtime/stores/TenantPresenceStore.ts b/apps/bot/src/runtime/stores/TenantPresenceStore.ts index 76e5d45..b284c82 100644 --- a/apps/bot/src/runtime/stores/TenantPresenceStore.ts +++ b/apps/bot/src/runtime/stores/TenantPresenceStore.ts @@ -32,7 +32,10 @@ FROM bot_presence_states LIMIT 0; `; -const parseStoredTexts = (rawTexts: string | null, fallbackText: string): string[] => { +const parseStoredTexts = ( + rawTexts: string | null, + fallbackText: string, +): string[] => { if (typeof rawTexts === "string" && rawTexts.trim().length > 0) { try { const parsed = JSON.parse(rawTexts) as unknown; @@ -55,7 +58,10 @@ const parseStoredTexts = (rawTexts: string | null, fallbackText: string): string }; const toPresenceState = (row: PresenceRow): PresenceState | null => { - if (!isPresenceStatusValue(row.status) || !isPresenceActivityTypeValue(row.activity_type)) { + if ( + !isPresenceStatusValue(row.status) || + !isPresenceActivityTypeValue(row.activity_type) + ) { return null; } @@ -87,7 +93,7 @@ export class TenantPresenceStore implements PresenceRepository { await this.pool.query(presenceSchemaProbeSql); } catch (error) { throw new Error( - "[db:init] missing or incompatible table \"bot_presence_states\". Run migrations with \"npm run migrate\".", + '[db:init] missing or incompatible table "bot_presence_states". Run migrations with "npm run migrate".', { cause: error }, ); } @@ -112,10 +118,16 @@ export class TenantPresenceStore implements PresenceRepository { return toPresenceState(row) ?? createDefaultPresenceState(); } - public async upsertByBotId(botId: string, state: PresenceState): Promise { + public async upsertByBotId( + botId: string, + state: PresenceState, + ): Promise { const activityTexts = sanitizeActivityTexts(state.activity.texts); - const primaryText = activityTexts[0] ?? sanitizeActivityText(state.activity.text); - const rotationIntervalSeconds = sanitizePresenceRotationIntervalSeconds(state.activity.rotationIntervalSeconds); + const primaryText = + activityTexts[0] ?? sanitizeActivityText(state.activity.text); + const rotationIntervalSeconds = sanitizePresenceRotationIntervalSeconds( + state.activity.rotationIntervalSeconds, + ); await this.pool.query( ` diff --git a/apps/bot/tsconfig.json b/apps/bot/tsconfig.json index b0f4e56..47463ac 100644 --- a/apps/bot/tsconfig.json +++ b/apps/bot/tsconfig.json @@ -4,7 +4,9 @@ "composite": true, "rootDir": "src", "outDir": "dist", - "types": ["node"] + "types": ["node"], + "noUnusedLocals": true, + "noUnusedParameters": true }, "include": ["src/**/*.ts"] } diff --git a/apps/web/app/[locale]/dashboard/page.tsx b/apps/web/app/[locale]/dashboard/page.tsx index 66d99e8..de4d594 100644 --- a/apps/web/app/[locale]/dashboard/page.tsx +++ b/apps/web/app/[locale]/dashboard/page.tsx @@ -1,7 +1,8 @@ import { DashboardClient } from "../../../components/dashboard-client"; import PageWrapper from "../../../components/ui/PageWrapper"; -const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:4000"; +const apiBaseUrl = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:4000"; export const dynamic = "force-dynamic"; diff --git a/apps/web/app/[locale]/layout.tsx b/apps/web/app/[locale]/layout.tsx index 81fed1d..da77a03 100644 --- a/apps/web/app/[locale]/layout.tsx +++ b/apps/web/app/[locale]/layout.tsx @@ -137,19 +137,19 @@ export default async function LocaleLayout({ const messages = await getMessages(); - const direction = RTL_LOCALES.includes( - locale as (typeof RTL_LOCALES)[number], - ) + const direction = RTL_LOCALES.includes(locale as (typeof RTL_LOCALES)[number]) ? "rtl" : "ltr"; return ( - + {children} ); -} \ No newline at end of file +} diff --git a/apps/web/app/[locale]/login/page.tsx b/apps/web/app/[locale]/login/page.tsx index 7b901d9..c7d45cb 100644 --- a/apps/web/app/[locale]/login/page.tsx +++ b/apps/web/app/[locale]/login/page.tsx @@ -1,10 +1,17 @@ import { getT } from "../../../i18n/server"; import { Badge } from "../../../components/ui/Badge"; import { buttonClassName } from "../../../components/ui/Button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../../components/ui/Card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../../../components/ui/Card"; import PageWrapper from "../../../components/ui/PageWrapper"; -const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:4000"; +const apiBaseUrl = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:4000"; export default async function LoginPage() { const t = await getT(); diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 2fa5cca..22fcd18 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -72,9 +72,21 @@ a { inset: 0; z-index: -1; background: - radial-gradient(circle at 18% 10%, color-mix(in srgb, var(--primary) 28%, transparent), transparent 44%), - radial-gradient(circle at 82% 0%, color-mix(in srgb, var(--accent) 20%, transparent), transparent 36%), - linear-gradient(180deg, color-mix(in srgb, var(--bg-elevated) 86%, var(--bg)), var(--bg)); + radial-gradient( + circle at 18% 10%, + color-mix(in srgb, var(--primary) 28%, transparent), + transparent 44% + ), + radial-gradient( + circle at 82% 0%, + color-mix(in srgb, var(--accent) 20%, transparent), + transparent 36% + ), + linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-elevated) 86%, var(--bg)), + var(--bg) + ); } .ui-background::before { @@ -82,8 +94,16 @@ a { position: absolute; inset: 0; background-image: - linear-gradient(to right, color-mix(in srgb, var(--foreground-muted) 10%, transparent) 1px, transparent 1px), - linear-gradient(to bottom, color-mix(in srgb, var(--foreground-muted) 10%, transparent) 1px, transparent 1px); + linear-gradient( + to right, + color-mix(in srgb, var(--foreground-muted) 10%, transparent) 1px, + transparent 1px + ), + linear-gradient( + to bottom, + color-mix(in srgb, var(--foreground-muted) 10%, transparent) 1px, + transparent 1px + ); background-size: 42px 42px; mask-image: radial-gradient(circle at 55% 42%, black, transparent 78%); opacity: 0.2; diff --git a/apps/web/components/FeatureCard.tsx b/apps/web/components/FeatureCard.tsx index f54c28a..f74bf72 100644 --- a/apps/web/components/FeatureCard.tsx +++ b/apps/web/components/FeatureCard.tsx @@ -19,8 +19,12 @@ export default function FeatureCard({
-

{title}

-

{description}

+

+ {title} +

+

+ {description} +

diff --git a/apps/web/components/Features.tsx b/apps/web/components/Features.tsx index 204ba9b..213571b 100644 --- a/apps/web/components/Features.tsx +++ b/apps/web/components/Features.tsx @@ -9,9 +9,27 @@ export default async function Features() { title: t("features.items.autoCommands.title"), description: t("features.items.autoCommands.description"), icon: ( - - - + + + ), }, @@ -19,9 +37,27 @@ export default async function Features() { title: t("features.items.componentsV2.title"), description: t("features.items.componentsV2.description"), icon: ( - - - + + + ), }, @@ -29,9 +65,27 @@ export default async function Features() { title: t("features.items.autoHelp.title"), description: t("features.items.autoHelp.description"), icon: ( - - - + + + ), }, @@ -39,9 +93,27 @@ export default async function Features() { title: t("features.items.multiLanguage.title"), description: t("features.items.multiLanguage.description"), icon: ( - - - + + + ), }, @@ -49,9 +121,27 @@ export default async function Features() { title: t("features.items.dynamicVariables.title"), description: t("features.items.dynamicVariables.description"), icon: ( - - - + + + ), }, @@ -59,9 +149,27 @@ export default async function Features() { title: t("features.items.hosting.title"), description: t("features.items.hosting.description"), icon: ( - - - + + + ), }, @@ -76,7 +184,12 @@ export default async function Features() {
{items.map((it, idx) => ( - + ))}
diff --git a/apps/web/components/Hero.tsx b/apps/web/components/Hero.tsx index 08f6d49..882627b 100644 --- a/apps/web/components/Hero.tsx +++ b/apps/web/components/Hero.tsx @@ -3,7 +3,13 @@ import { getT } from "../i18n/server"; import { Badge } from "./ui/Badge"; import { buttonClassName } from "./ui/Button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/Card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "./ui/Card"; export default async function Hero() { const t = await getT(); @@ -24,7 +30,10 @@ export default async function Hero() {

- + {t("hero.ctaStart")} @@ -53,14 +62,18 @@ export default async function Hero() { key={bot.name} >
-

{bot.name}

+

+ {bot.name} +

{t("hero.card.instanceMetrics")}

- + {bot.status === "online" ? t("hero.status.online") : t("hero.status.stopped")} diff --git a/apps/web/components/HowItWorks.tsx b/apps/web/components/HowItWorks.tsx index 57cbfd9..90545ce 100644 --- a/apps/web/components/HowItWorks.tsx +++ b/apps/web/components/HowItWorks.tsx @@ -36,8 +36,12 @@ export default async function HowItWorks() { {`${i + 1}`}
-

{s.title}

-

{s.desc}

+

+ {s.title} +

+

+ {s.desc} +

diff --git a/apps/web/components/Logo.tsx b/apps/web/components/Logo.tsx index cb22d41..04ecc92 100644 --- a/apps/web/components/Logo.tsx +++ b/apps/web/components/Logo.tsx @@ -14,4 +14,4 @@ export default async function Logo({ className = "" }: { className?: string }) { ); -} \ No newline at end of file +} diff --git a/apps/web/components/dashboard-client.tsx b/apps/web/components/dashboard-client.tsx index f708238..1eb92dd 100644 --- a/apps/web/components/dashboard-client.tsx +++ b/apps/web/components/dashboard-client.tsx @@ -5,7 +5,13 @@ import { type FormEvent, useCallback, useEffect, useState } from "react"; import { useT } from "../i18n/client"; import { Badge } from "./ui/Badge"; import { Button, buttonClassName } from "./ui/Button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/Card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "./ui/Card"; import { Input } from "./ui/Input"; type User = { @@ -105,7 +111,10 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { const botsJson = await botsResponse.json(); setBots((botsJson.bots ?? []) as Bot[]); } catch (cause) { - const message = cause instanceof Error ? cause.message : t("dashboard.errors.unexpected"); + const message = + cause instanceof Error + ? cause.message + : t("dashboard.errors.unexpected"); setError(message); } finally { setLoading(false); @@ -153,7 +162,10 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { setDisplayName(""); await refreshData(); } catch (cause) { - const message = cause instanceof Error ? cause.message : t("dashboard.errors.unexpected"); + const message = + cause instanceof Error + ? cause.message + : t("dashboard.errors.unexpected"); setError(message); } finally { setSubmitting(false); @@ -164,19 +176,28 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { setError(null); try { - const response = await fetch(`${apiBaseUrl}/api/bots/${botId}/${action}`, { - method: "POST", - credentials: "include", - }); + const response = await fetch( + `${apiBaseUrl}/api/bots/${botId}/${action}`, + { + method: "POST", + credentials: "include", + }, + ); if (!response.ok) { const errorJson = await response.json().catch(() => ({})); - throw new Error(errorJson.error ?? t("dashboard.errors.actionFailed", { action: actionLabel[action] })); + throw new Error( + errorJson.error ?? + t("dashboard.errors.actionFailed", { action: actionLabel[action] }), + ); } await refreshData(); } catch (cause) { - const message = cause instanceof Error ? cause.message : t("dashboard.errors.unexpected"); + const message = + cause instanceof Error + ? cause.message + : t("dashboard.errors.unexpected"); setError(message); } }; @@ -185,7 +206,9 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { return ( -

{t("dashboard.loading")}

+

+ {t("dashboard.loading")} +

); @@ -223,7 +246,9 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) {
- {t("dashboard.tenant", { tenantId: user.tenantId })} + + {t("dashboard.tenant", { tenantId: user.tenantId })} +

{user.username}

@@ -239,7 +264,9 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { {error ? ( -

{error}

+

+ {error} +

) : null} @@ -249,7 +276,9 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { {t("dashboard.addBot.title")} - {t("dashboard.addBot.description")} + + {t("dashboard.addBot.description")} + @@ -280,7 +309,12 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { /> - @@ -330,7 +368,9 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { {bot.lastError ? (

- {t("dashboard.bots.lastError", { message: bot.lastError })} + {t("dashboard.bots.lastError", { + message: bot.lastError, + })}

) : null} diff --git a/apps/web/components/ui/Badge.tsx b/apps/web/components/ui/Badge.tsx index 8edf0f4..5521a29 100644 --- a/apps/web/components/ui/Badge.tsx +++ b/apps/web/components/ui/Badge.tsx @@ -36,4 +36,4 @@ export function Badge({ {...props} /> ); -} \ No newline at end of file +} diff --git a/apps/web/components/ui/Button.tsx b/apps/web/components/ui/Button.tsx index f5afc7f..8d7ea8b 100644 --- a/apps/web/components/ui/Button.tsx +++ b/apps/web/components/ui/Button.tsx @@ -73,4 +73,4 @@ export function Button({ {...props} /> ); -} \ No newline at end of file +} diff --git a/apps/web/components/ui/Card.tsx b/apps/web/components/ui/Card.tsx index b414c51..29aa0db 100644 --- a/apps/web/components/ui/Card.tsx +++ b/apps/web/components/ui/Card.tsx @@ -14,11 +14,17 @@ export function Card({ className, ...props }: HTMLAttributes) { ); } -export function CardHeader({ className, ...props }: HTMLAttributes) { +export function CardHeader({ + className, + ...props +}: HTMLAttributes) { return
; } -export function CardTitle({ className, ...props }: HTMLAttributes) { +export function CardTitle({ + className, + ...props +}: HTMLAttributes) { return (

) { return (

); } -export function CardContent({ className, ...props }: HTMLAttributes) { +export function CardContent({ + className, + ...props +}: HTMLAttributes) { return

; -} \ No newline at end of file +} diff --git a/apps/web/components/ui/Container.tsx b/apps/web/components/ui/Container.tsx index b82feab..5b23d65 100644 --- a/apps/web/components/ui/Container.tsx +++ b/apps/web/components/ui/Container.tsx @@ -2,11 +2,17 @@ import type { HTMLAttributes } from "react"; import { cn } from "./cn"; -export function Container({ className, ...props }: HTMLAttributes) { +export function Container({ + className, + ...props +}: HTMLAttributes) { return (
); -} \ No newline at end of file +} diff --git a/apps/web/components/ui/Input.tsx b/apps/web/components/ui/Input.tsx index a138022..50be4ce 100644 --- a/apps/web/components/ui/Input.tsx +++ b/apps/web/components/ui/Input.tsx @@ -18,4 +18,4 @@ export const Input = forwardRef(function Input( {...props} /> ); -}); \ No newline at end of file +}); diff --git a/apps/web/components/ui/Navbar.tsx b/apps/web/components/ui/Navbar.tsx index 9575c0f..e73d389 100644 --- a/apps/web/components/ui/Navbar.tsx +++ b/apps/web/components/ui/Navbar.tsx @@ -15,7 +15,11 @@ type NavbarProps = { export default async function Navbar({ currentPath }: NavbarProps) { const t = await getT(); - const links: Array<{ href: "/" | "/dashboard"; key: NavbarPath; label: string }> = [ + const links: Array<{ + href: "/" | "/dashboard"; + key: NavbarPath; + label: string; + }> = [ { href: "/", key: "home", label: t("nav.home") }, { href: "/dashboard", key: "dashboard", label: t("nav.dashboard") }, ]; @@ -60,4 +64,4 @@ export default async function Navbar({ currentPath }: NavbarProps) {

); -} \ No newline at end of file +} diff --git a/apps/web/components/ui/PageWrapper.tsx b/apps/web/components/ui/PageWrapper.tsx index 17ad580..263c5f6 100644 --- a/apps/web/components/ui/PageWrapper.tsx +++ b/apps/web/components/ui/PageWrapper.tsx @@ -27,7 +27,9 @@ export default async function PageWrapper({
- {children} + + {children} +
{footer ? ( @@ -37,4 +39,4 @@ export default async function PageWrapper({ ) : null}
); -} \ No newline at end of file +} diff --git a/apps/web/components/ui/Switch.tsx b/apps/web/components/ui/Switch.tsx index 72faa2a..77371f7 100644 --- a/apps/web/components/ui/Switch.tsx +++ b/apps/web/components/ui/Switch.tsx @@ -69,4 +69,4 @@ export function Switch({ /> ); -} \ No newline at end of file +} diff --git a/apps/web/components/ui/cn.ts b/apps/web/components/ui/cn.ts index b67f473..737ea7e 100644 --- a/apps/web/components/ui/cn.ts +++ b/apps/web/components/ui/cn.ts @@ -1,3 +1,3 @@ export function cn(...classes: Array) { return classes.filter(Boolean).join(" "); -} \ No newline at end of file +} diff --git a/apps/web/i18n/client.ts b/apps/web/i18n/client.ts index 954e200..3ad0bb3 100644 --- a/apps/web/i18n/client.ts +++ b/apps/web/i18n/client.ts @@ -2,4 +2,4 @@ import { useTranslations } from "next-intl"; export function useT() { return useTranslations(); -} \ No newline at end of file +} diff --git a/apps/web/i18n/navigation.ts b/apps/web/i18n/navigation.ts index 91d9571..fb5a1a1 100644 --- a/apps/web/i18n/navigation.ts +++ b/apps/web/i18n/navigation.ts @@ -3,4 +3,4 @@ import { createNavigation } from "next-intl/navigation"; import { routing } from "./routing"; export const { Link, redirect, usePathname, useRouter, getPathname } = - createNavigation(routing); \ No newline at end of file + createNavigation(routing); diff --git a/apps/web/i18n/next-intl.d.ts b/apps/web/i18n/next-intl.d.ts index 15e27ae..5edcc8c 100644 --- a/apps/web/i18n/next-intl.d.ts +++ b/apps/web/i18n/next-intl.d.ts @@ -9,4 +9,4 @@ declare module "next-intl" { Locale: (typeof routing.locales)[number]; Messages: typeof enMessages; } -} \ No newline at end of file +} diff --git a/apps/web/i18n/request.ts b/apps/web/i18n/request.ts index d92265d..53bde53 100644 --- a/apps/web/i18n/request.ts +++ b/apps/web/i18n/request.ts @@ -61,4 +61,4 @@ export default getRequestConfig(async ({ requestLocale }) => { ? defaultMessages : mergeMessages(defaultMessages, localeMessages), }; -}); \ No newline at end of file +}); diff --git a/apps/web/i18n/routing.ts b/apps/web/i18n/routing.ts index dbc26a3..489b81e 100644 --- a/apps/web/i18n/routing.ts +++ b/apps/web/i18n/routing.ts @@ -28,4 +28,4 @@ export const routing = defineRouting({ localeDetection: true, }); -export type AppLocale = (typeof routing.locales)[number]; \ No newline at end of file +export type AppLocale = (typeof routing.locales)[number]; diff --git a/apps/web/i18n/server.ts b/apps/web/i18n/server.ts index a99b9f5..e3ee80f 100644 --- a/apps/web/i18n/server.ts +++ b/apps/web/i18n/server.ts @@ -2,4 +2,4 @@ import { getTranslations } from "next-intl/server"; export async function getT() { return getTranslations(); -} \ No newline at end of file +} diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index 1862569..f21ceb9 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -164,4 +164,4 @@ "actionFailed": "فشل الإجراء: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/bn.json b/apps/web/locales/bn.json index 9107fd5..dff27af 100644 --- a/apps/web/locales/bn.json +++ b/apps/web/locales/bn.json @@ -164,4 +164,4 @@ "actionFailed": "অ্যাকশন ব্যর্থ: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index f75fe40..6055593 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -164,4 +164,4 @@ "actionFailed": "Aktion fehlgeschlagen: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index bc36066..ebd1296 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -164,4 +164,4 @@ "actionFailed": "Action failed: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index 10ba440..76c61fa 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -164,4 +164,4 @@ "actionFailed": "Acción fallida: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/hi.json b/apps/web/locales/hi.json index de60e64..dcd4d92 100644 --- a/apps/web/locales/hi.json +++ b/apps/web/locales/hi.json @@ -164,4 +164,4 @@ "actionFailed": "क्रिया असफल: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/id.json b/apps/web/locales/id.json index b6be653..4a32c62 100644 --- a/apps/web/locales/id.json +++ b/apps/web/locales/id.json @@ -164,4 +164,4 @@ "actionFailed": "Aksi gagal: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 05de15c..c97a83e 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -164,4 +164,4 @@ "actionFailed": "Azione impossibile: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/ja.json b/apps/web/locales/ja.json index 91de913..5c8c5cf 100644 --- a/apps/web/locales/ja.json +++ b/apps/web/locales/ja.json @@ -164,4 +164,4 @@ "actionFailed": "操作失敗: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index ad8a8b6..57606fc 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -164,4 +164,4 @@ "actionFailed": "Actie mislukt: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index adb30d2..18ad6c7 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -164,4 +164,4 @@ "actionFailed": "Akcja nie powiodła się: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index 535a5b0..2427629 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -164,4 +164,4 @@ "actionFailed": "Ação falhou: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index 0737304..782cdee 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -164,4 +164,4 @@ "actionFailed": "Ошибка действия: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/tr.json b/apps/web/locales/tr.json index f5391e3..42d6ec7 100644 --- a/apps/web/locales/tr.json +++ b/apps/web/locales/tr.json @@ -164,4 +164,4 @@ "actionFailed": "İşlem başarısız: {action}" } } -} \ No newline at end of file +} diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index b95c2e9..453ddc7 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -164,4 +164,4 @@ "actionFailed": "操作失败:{action}" } } -} \ No newline at end of file +} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index f50d147..8be00c6 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -4,7 +4,5 @@ import { routing } from "./i18n/routing"; export default createMiddleware(routing); export const config = { - matcher: [ - "/((?!api|auth|_next|_vercel|.*\\..*).*)" - ], -}; \ No newline at end of file + matcher: ["/((?!api|auth|_next|_vercel|.*\\..*).*)"], +}; diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 6259226..09adbcc 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -5,7 +5,22 @@ const withNextIntl = createNextIntlPlugin("./i18n/request.ts"); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + + // indispensable pour Docker + monorepo output: "standalone", + + // IMPORTANT pour éviter les problèmes de paths en monorepo + outputFileTracingRoot: process.cwd(), + + // optimise build (optionnel mais recommandé) + poweredByHeader: false, + compress: true, + + // évite erreurs turbopack / build tracing inutile + experimental: { + // utile en monorepo pour stabilité + externalDir: true, + }, }; export default withNextIntl(nextConfig); diff --git a/apps/web/public/manifest.json b/apps/web/public/manifest.json index d7d1777..23c3b8b 100644 --- a/apps/web/public/manifest.json +++ b/apps/web/public/manifest.json @@ -16,4 +16,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 8b4b9d5..1e439dc 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,11 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": false, "incremental": true, "module": "esnext", @@ -19,13 +15,6 @@ } ] }, - "include": [ - "**/*.ts", - "**/*.tsx", - "next-env.d.ts", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] + "include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", ".next/types/**/*.ts"], + "exclude": ["node_modules"] } diff --git a/docker-compose.yml b/docker-compose.yml index adfd335..098031a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,7 +57,8 @@ services: volumes: - redis_data:/data healthcheck: - test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep -q PONG"] + test: + ["CMD-SHELL", 'redis-cli -a "$${REDIS_PASSWORD}" ping | grep -q PONG'] interval: 10s timeout: 5s retries: 5 @@ -115,8 +116,6 @@ services: condition: service_healthy api: condition: service_started - ports: - - "${BOT_PORT}:4100" networks: - edge_net - backend_net diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..d610175 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,92 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default [ + // ========================= + // IGNORE GLOBAL + // ========================= + { + ignores: [ + "**/node_modules/**", + "**/dist/**", + "**/build/**", + "**/.next/**", + "**/coverage/**", + ], + }, + + // ========================= + // BASE JS RULES + // ========================= + js.configs.recommended, + + // ========================= + // TYPESCRIPT SUPPORT + // ========================= + ...tseslint.configs.recommended, + + // ========================= + // NODE (API / BOT / SCRIPTS / SHARED) + // ========================= + { + files: [ + "apps/api/**/*.{js,mjs,ts}", + "apps/bot/**/*.{js,mjs,ts}", + "scripts/**/*.{js,mjs,ts}", + "packages/shared/**/*.{js,ts}", + ], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.node, + }, + }, + rules: { + "no-unused-vars": "warn", + }, + }, + + // ========================= + // WEB (NEXT.JS) + // ========================= + { + files: ["apps/web/**/*.{ts,tsx,js,jsx}"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.browser, + }, + }, + rules: { + "no-unused-vars": "warn", + }, + }, + + // ========================= + // COMMONJS FILES (.cjs) + // ========================= + { + files: ["**/*.cjs"], + languageOptions: { + sourceType: "commonjs", + globals: { + module: "readonly", + require: "readonly", + exports: "readonly", + }, + }, + }, + + // ========================= + // LEGACY BOT (réduit le bruit) + // ========================= + { + files: ["apps/bot/src/legacy/**"], + rules: { + "no-unused-vars": "warn", + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 45c7f35..2ddd466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,16 @@ "packages/*" ], "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^22.10.7", "autoprefixer": "^10.4.14", + "eslint": "^10.2.1", + "globals": "^17.5.0", "postcss": "^8.4.21", "tailwindcss": "^3.4.7", "tsx": "^4.19.3", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "typescript-eslint": "^8.59.0" }, "engines": { "node": ">=20.0.0" @@ -682,6 +686,134 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@formatjs/fast-memoize": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.2.tgz", @@ -712,6 +844,72 @@ "@formatjs/fast-memoize": "3.1.2" } }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -2242,6 +2440,20 @@ "@types/node": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", @@ -2276,6 +2488,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2381,6 +2600,237 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vladfrangu/async_event_emitter": { "version": "2.4.7", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", @@ -2404,6 +2854,47 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -2484,6 +2975,16 @@ "postcss": "^8.1.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.20", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", @@ -2549,6 +3050,19 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2877,6 +3391,21 @@ "node": ">=12.0.0" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2914,6 +3443,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -3137,6 +3673,172 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", + "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3243,6 +3945,20 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3253,6 +3969,19 @@ "reusify": "^1.0.4" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3299,6 +4028,44 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3418,6 +4185,19 @@ "node": ">=10.13.0" } }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3501,6 +4281,26 @@ "@formatjs/icu-messageformat-parser": "^3.4.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3617,6 +4417,13 @@ "node": ">=0.12.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -3637,6 +4444,51 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3657,6 +4509,22 @@ "dev": true, "license": "MIT" }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -3789,6 +4657,22 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3856,6 +4740,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -4103,6 +4994,56 @@ "node": ">= 0.8" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4112,6 +5053,26 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -4499,6 +5460,16 @@ "node": ">=0.10.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -4528,6 +5499,16 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -4890,6 +5871,29 @@ "@img/sharp-win32-x64": "0.33.5" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -5254,6 +6258,19 @@ "node": ">=0.6" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -5294,6 +6311,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -5313,6 +6343,7 @@ "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5321,6 +6352,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", @@ -5376,6 +6431,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-intl": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.9.1.tgz", @@ -5435,6 +6500,32 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -5465,6 +6556,19 @@ "node": ">=0.4" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index e0ee84b..7ef7595 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,15 @@ "check": "npm run typecheck" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^22.10.7", + "autoprefixer": "^10.4.14", + "eslint": "^10.2.1", + "globals": "^17.5.0", + "postcss": "^8.4.21", + "tailwindcss": "^3.4.7", "tsx": "^4.19.3", "typescript": "^5.7.3", - "tailwindcss": "^3.4.7", - "postcss": "^8.4.21", - "autoprefixer": "^10.4.14" + "typescript-eslint": "^8.59.0" } } diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index b84a3ef..b36514e 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -1,5 +1,11 @@ export const BOT_CONTROL_QUEUE = "bot-control"; export const BOT_CONTROL_QUEUE_PREFIX = "discord-saas"; -export const BOT_STATUS = ["stopped", "starting", "running", "stopping", "error"] as const; +export const BOT_STATUS = [ + "stopped", + "starting", + "running", + "stopping", + "error", +] as const; export type BotStatus = (typeof BOT_STATUS)[number]; diff --git a/packages/shared/src/redisKeys.ts b/packages/shared/src/redisKeys.ts index 21b5db1..f64e5af 100644 --- a/packages/shared/src/redisKeys.ts +++ b/packages/shared/src/redisKeys.ts @@ -1,6 +1,10 @@ -const sanitizeSegment = (segment: string): string => segment.replace(/[^a-zA-Z0-9:_-]/g, "_"); +const sanitizeSegment = (segment: string): string => + segment.replace(/[^a-zA-Z0-9:_-]/g, "_"); -export const tenantRedisKey = (tenantId: string, ...parts: string[]): string => { +export const tenantRedisKey = ( + tenantId: string, + ...parts: string[] +): string => { const tail = parts.map(sanitizeSegment).join(":"); return tail.length > 0 ? `tenant:${sanitizeSegment(tenantId)}:${tail}` @@ -11,6 +15,10 @@ export const tenantRateLimitKey = (tenantId: string, scope: string): string => { return tenantRedisKey(tenantId, "ratelimit", scope); }; -export const tenantBotRedisKey = (tenantId: string, botId: string, ...parts: string[]): string => { +export const tenantBotRedisKey = ( + tenantId: string, + botId: string, + ...parts: string[] +): string => { return tenantRedisKey(tenantId, "bot", botId, ...parts); }; diff --git a/packages/shared/src/tokenCrypto.ts b/packages/shared/src/tokenCrypto.ts index a69d573..734172e 100644 --- a/packages/shared/src/tokenCrypto.ts +++ b/packages/shared/src/tokenCrypto.ts @@ -18,11 +18,17 @@ export const parseTokenEncryptionKey = (base64Key: string): Buffer => { return key; }; -export const encryptToken = (plainToken: string, key: Buffer): EncryptedToken => { +export const encryptToken = ( + plainToken: string, + key: Buffer, +): EncryptedToken => { const iv = randomBytes(IV_LENGTH); const cipher = createCipheriv(ALGORITHM, key, iv); - const ciphertext = Buffer.concat([cipher.update(plainToken, "utf8"), cipher.final()]); + const ciphertext = Buffer.concat([ + cipher.update(plainToken, "utf8"), + cipher.final(), + ]); const tag = cipher.getAuthTag(); return { @@ -32,8 +38,15 @@ export const encryptToken = (plainToken: string, key: Buffer): EncryptedToken => }; }; -export const decryptToken = (encryptedToken: EncryptedToken, key: Buffer): string => { - const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(encryptedToken.iv, "base64")); +export const decryptToken = ( + encryptedToken: EncryptedToken, + key: Buffer, +): string => { + const decipher = createDecipheriv( + ALGORITHM, + key, + Buffer.from(encryptedToken.iv, "base64"), + ); decipher.setAuthTag(Buffer.from(encryptedToken.tag, "base64")); const decrypted = Buffer.concat([ diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 7a46543..a66bf1b 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "composite": true, "rootDir": "src", - "outDir": "dist" + "outDir": "dist", + "declaration": true }, "include": ["src/**/*.ts"] } diff --git a/scripts/i18n/sync-locales.mjs b/scripts/i18n/sync-locales.mjs index 24100af..d924cef 100644 --- a/scripts/i18n/sync-locales.mjs +++ b/scripts/i18n/sync-locales.mjs @@ -88,7 +88,9 @@ for (const target of TARGETS) { const defaultLocalePath = path.join(targetDirectory, "en.json"); if (!existsSync(defaultLocalePath)) { - console.error(`[i18n:${target.name}] missing default locale file: ${defaultLocalePath}`); + console.error( + `[i18n:${target.name}] missing default locale file: ${defaultLocalePath}`, + ); hasError = true; continue; } @@ -102,7 +104,10 @@ for (const target of TARGETS) { const localePath = path.join(targetDirectory, `${locale}.json`); const localeExists = existsSync(localePath); const currentDictionary = localeExists ? readJsonFile(localePath) : {}; - const normalizedDictionary = shapeFromBase(baseDictionary, currentDictionary); + const normalizedDictionary = shapeFromBase( + baseDictionary, + currentDictionary, + ); const normalizedText = JSON.stringify(normalizedDictionary, null, 2); const currentText = JSON.stringify(currentDictionary, null, 2); @@ -115,8 +120,12 @@ for (const target of TARGETS) { if (CHECK_ONLY) { const localeLeafPaths = new Set(collectLeafPaths(currentDictionary)); - const missingPaths = [...baseLeafPaths].filter((key) => !localeLeafPaths.has(key)); - const extraPaths = [...localeLeafPaths].filter((key) => !baseLeafPaths.has(key)); + const missingPaths = [...baseLeafPaths].filter( + (key) => !localeLeafPaths.has(key), + ); + const extraPaths = [...localeLeafPaths].filter( + (key) => !baseLeafPaths.has(key), + ); if (missingPaths.length > 0 || extraPaths.length > 0) { hasError = true; diff --git a/tsconfig.base.json b/tsconfig.base.json index ddb8577..bc13516 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -3,14 +3,16 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "resolveJsonModule": true, + "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, - "declaration": true, - "sourceMap": true + "noFallthroughCasesInSwitch": true } }