diff --git a/package.json b/package.json index 79c3bbb0..3306fb04 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "env-var": "7.4.1", "keyv": "4.5.3", "open-graph-scraper": "6.2.2", - "param-case": "3.0.4" + "param-case": "3.0.4", + "zod": "3.22.2" }, "devDependencies": { "@types/node": "20.5.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01476c3c..3b07e314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ dependencies: param-case: specifier: 3.0.4 version: 3.0.4 + zod: + specifier: 3.22.2 + version: 3.22.2 devDependencies: '@types/node': @@ -3229,3 +3232,7 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + + /zod@3.22.2: + resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} + dev: false diff --git a/src/config.ts b/src/config.ts index ecdbeb85..249c0ea8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,7 +5,6 @@ export const config = { token: env.get('DISCORD_TOKEN').required().asString(), clientId: env.get('DISCORD_CLIENT_ID').required().asString(), guildId: env.get('DISCORD_GUILD_ID').required().asString(), - coolLinksChannelId: env.get('COOL_LINKS_CHANNEL_ID').required().asString(), }, redis: { url: env.get('REDIS_URL').required().asString(), diff --git a/src/core/env.ts b/src/core/env.ts new file mode 100644 index 00000000..393c690d --- /dev/null +++ b/src/core/env.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import type { BotModule } from '../types/bot'; + +export let allEnvSchemas = z.object({}); +export const getEnv = () => process.env as z.infer; + +export const loadEnvirontmentVariables = (modules: Record): void => { + const modulesEnv = Object.values(modules) + .map((module) => 'env' in module && module.env) + .filter((env: T): env is Exclude => Boolean(env)) + .reduce((acc, currentEnv) => { + Object.keys(currentEnv).forEach((key) => { + if (Object.keys(acc).includes(key)) { + throw new Error(`Duplicate environment variable found: ${key}`); + } + }); + return { ...acc, ...currentEnv }; + }); + + Object.entries(modulesEnv).forEach(([key, schema]) => { + allEnvSchemas = allEnvSchemas.extend({ [key]: schema }); + schema.parse(process.env[key]); + }); +}; diff --git a/src/modules/coolLinksManagement/coolLinksManagement.module.ts b/src/modules/coolLinksManagement/coolLinksManagement.module.ts index e4cedb58..c0180860 100644 --- a/src/modules/coolLinksManagement/coolLinksManagement.module.ts +++ b/src/modules/coolLinksManagement/coolLinksManagement.module.ts @@ -1,7 +1,8 @@ import { MessageType, ThreadAutoArchiveDuration } from 'discord.js'; import ogs from 'open-graph-scraper'; +import { z } from 'zod'; -import { config } from '../../config'; +import { getEnv } from '../../core/env'; import { isASocialNetworkUrl } from '../../helpers/regex.helper'; import type { BotModule } from '../../types/bot'; import { getPageSummary } from './summarizeCoolPages'; @@ -33,12 +34,16 @@ const getThreadNameFromOpenGraph = async (url: string): Promise = const youtubeUrlRegex = new RegExp('^(https?)?(://)?(www.)?(m.)?((youtube.com)|(youtu.be))'); export const coolLinksManagement: BotModule = { + env: { + COOL_LINKS_CHANNEL_ID: z.string().nonempty(), + }, eventHandlers: { messageCreate: async (message) => { + const env = getEnv(); if ( message.author.bot || message.type !== MessageType.Default || - message.channelId !== config.discord.coolLinksChannelId + message.channelId !== env['COOL_LINKS_CHANNEL_ID'] ) { return; } diff --git a/src/types/bot.ts b/src/types/bot.ts index a26a2e57..1c2dc10d 100644 --- a/src/types/bot.ts +++ b/src/types/bot.ts @@ -3,6 +3,7 @@ import type { ClientEvents, RESTPostAPIChatInputApplicationCommandsJSONBody, } from 'discord.js'; +import type { ZodTypeAny } from 'zod'; type slashCommandHandler = ( interaction: ChatInputCommandInteraction<'cached' | 'raw'>, @@ -18,6 +19,9 @@ export type BotCommand = { }; export type BotModule = { + env?: { + [key: string]: ZodTypeAny; + }; slashCommands?: Array; eventHandlers?: { [key in keyof ClientEvents]?: EventHandler;