From 08a4bed04d9099c1e2a8badc10404139e3daac09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Wed, 13 Sep 2023 21:01:26 +0200 Subject: [PATCH 01/11] temp --- package.json | 4 +- pnpm-lock.yaml | 30 +++++++++- src/__tests__/mocks/config.mock.ts | 15 ----- src/config.ts | 4 -- src/core/createModule.ts | 58 +++++++++++++++++++ .../coolLinksManagement.module.ts | 32 +++++++--- .../coolLinksManagement/summarizeCoolPages.ts | 7 +-- src/modules/fart/fart.module.ts | 9 +-- .../patternReplace/patternReplace.module.ts | 18 +++--- src/modules/quoiFeur/quoiFeur.module.ts | 13 +++-- .../recurringMessage.module.ts | 13 +++-- .../voiceOnDemand/voiceOnDemand.module.ts | 13 +++-- 12 files changed, 150 insertions(+), 66 deletions(-) delete mode 100644 src/__tests__/mocks/config.mock.ts create mode 100644 src/core/createModule.ts diff --git a/package.json b/package.json index 19978ee..3187e21 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,14 @@ "dependencies": { "@keyv/redis": "2.7.0", "cheerio": "1.0.0-rc.12", + "constant-case": "3.0.4", "cron": "2.4.3", "discord.js": "14.13.0", "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 5da5b43..e4537b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: cheerio: specifier: 1.0.0-rc.12 version: 1.0.0-rc.12 + constant-case: + specifier: 3.0.4 + version: 3.0.4 cron: specifier: 2.4.3 version: 2.4.3 @@ -29,6 +32,9 @@ dependencies: param-case: specifier: 3.0.4 version: 3.0.4 + zod: + specifier: 3.22.2 + version: 3.22.2 devDependencies: '@types/node': @@ -1184,6 +1190,14 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /constant-case@3.0.4: + resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.1 + upper-case: 2.0.2 + dev: false + /cron@2.4.3: resolution: {integrity: sha512-YBvExkQYF7w0PxyeFLRyr817YVDhGxaCi5/uRRMqa4aWD3IFKRd+uNbpW1VWMdqQy8PZ7CElc+accXJcauPKzQ==} dependencies: @@ -1345,7 +1359,7 @@ packages: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.5.0 + tslib: 2.6.1 dev: false /dotenv@16.3.1: @@ -2251,7 +2265,7 @@ packages: /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.5.0 + tslib: 2.6.1 dev: false /lru-cache@6.0.0: @@ -2343,7 +2357,7 @@ packages: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.5.0 + tslib: 2.6.1 dev: false /normalize-path@3.0.0: @@ -3028,6 +3042,12 @@ packages: busboy: 1.6.0 dev: false + /upper-case@2.0.2: + resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + dependencies: + tslib: 2.6.1 + dev: false + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: @@ -3248,3 +3268,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/__tests__/mocks/config.mock.ts b/src/__tests__/mocks/config.mock.ts deleted file mode 100644 index f7aeefb..0000000 --- a/src/__tests__/mocks/config.mock.ts +++ /dev/null @@ -1,15 +0,0 @@ -export default { - discord: { - token: 'token', - clientId: 'clientId', - guildId: 'guildId', - coolLinksChannelId: 'coolLinksChannelId', - }, - redis: { - url: 'redisUrl', - }, - thirdParties: { - // 🥷 - pageSummarizerBaseUrl: 'https://example.com', - }, -}; diff --git a/src/config.ts b/src/config.ts index ecdbeb8..29db2d1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,12 +5,8 @@ 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(), }, - thirdParties: { - pageSummarizerBaseUrl: env.get('PAGE_SUMMARIZER_BASE_URL').required().asString(), - }, }; diff --git a/src/core/createModule.ts b/src/core/createModule.ts new file mode 100644 index 0000000..9b1801c --- /dev/null +++ b/src/core/createModule.ts @@ -0,0 +1,58 @@ +import type { ClientEvents, ClientOptions } from 'discord.js'; +import type { ZodTypeAny } from 'zod'; +import { z } from 'zod'; + +import type { BotCommand, EventHandler } from '../types/bot'; + +type InferredZodShape> = { + [K in keyof Shape]: Shape[K]['_type']; +}; + +interface Context> { + env: InferredZodShape; +} + +type ModuleFunction, ReturnType> = ( + context: Context, +) => ReturnType; + +type EventHandlers = { + [K in keyof ClientEvents]?: EventHandler; +}; + +type BotModule> = { + name: string; + env?: Env; + intents?: ClientOptions['intents']; + slashCommands?: ModuleFunction>; + eventHandlers?: ModuleFunction; +}; + +interface CreatedModule { + intents: ClientOptions['intents']; + slashCommands: Array; + eventHandlers: EventHandlers; +} + +interface CreatedModuleInput { + env: unknown; + cache: unknown; +} + +export const createModule = >( + module: BotModule, +): ((input: CreatedModuleInput) => Promise) => { + return async (input) => { + const env = await z.object(module.env ?? ({} as Env)).parseAsync(input.env); + + const context = { + env, + }; + + return { + intents: module.intents ?? [], + slashCommands: module.slashCommands?.(context) ?? [], + eventHandlers: module.eventHandlers?.(context) ?? {}, + }; + }; +}; diff --git a/src/modules/coolLinksManagement/coolLinksManagement.module.ts b/src/modules/coolLinksManagement/coolLinksManagement.module.ts index e13f37f..a77542b 100644 --- a/src/modules/coolLinksManagement/coolLinksManagement.module.ts +++ b/src/modules/coolLinksManagement/coolLinksManagement.module.ts @@ -1,9 +1,9 @@ import { MessageType, ThreadAutoArchiveDuration } from 'discord.js'; import ogs from 'open-graph-scraper'; +import { z } from 'zod'; -import { config } from '../../config'; +import { createModule } from '../../core/createModule'; import { isASocialNetworkUrl } from '../../helpers/regex.helper'; -import type { BotModule } from '../../types/bot'; import { getPageSummary } from './summarizeCoolPages'; import { getVideoSummary } from './summarizeCoolVideos'; @@ -32,13 +32,18 @@ const getThreadNameFromOpenGraph = async (url: string): Promise = const youtubeUrlRegex = new RegExp('^(https?)?(://)?(www.)?(m.)?((youtube.com)|(youtu.be))'); -export const coolLinksManagement: BotModule = { - eventHandlers: { +export const coolLinksManagement = createModule({ + name: 'coolLinksManagement', + env: { + CHANNEL_ID: z.string().nonempty(), + PAGE_SUMMARIZER_BASE_URL: z.string().url(), + }, + eventHandlers: ({ env }) => ({ messageCreate: async (message) => { if ( message.author.bot || message.type !== MessageType.Default || - message.channelId !== config.discord.coolLinksChannelId + message.channelId !== env.CHANNEL_ID ) { return; } @@ -69,13 +74,24 @@ export const coolLinksManagement: BotModule = { } if (!youtubeUrlRegex.test(url) && !isASocialNetworkUrl(url)) { try { - const pageSummaryDiscordView = await getPageSummary(url); + // const parseBaseUrl = `${env.PAGE_SUMMARIZER_BASE_URL}/convert.php?type=expand&lang=en&langfrom=user&url=`; + const fullUrl = new URL('/convert.php', env.PAGE_SUMMARIZER_BASE_URL); + const searchParams = new URLSearchParams([ + ['type', 'expand'], + ['lang', 'en'], + ['langfrom', 'user'], + ['url', url], + ]); + + fullUrl.search = searchParams.toString(); + + const pageSummaryDiscordView = await getPageSummary(fullUrl.toString()); await thread.send(pageSummaryDiscordView); } catch (error) { console.error(error); } } }, - }, + }), intents: ['GuildMessages', 'MessageContent', 'GuildMessageReactions'], -}; +}); diff --git a/src/modules/coolLinksManagement/summarizeCoolPages.ts b/src/modules/coolLinksManagement/summarizeCoolPages.ts index f10399d..8499199 100644 --- a/src/modules/coolLinksManagement/summarizeCoolPages.ts +++ b/src/modules/coolLinksManagement/summarizeCoolPages.ts @@ -1,9 +1,6 @@ import { load } from 'cheerio'; -import { config } from '../../config'; import { resolveCatch } from '../../helpers/resolveCatch.helper'; -// langfrom can't be changed to another language, this result in a translation of the summary that throw an HTTP error because we are in "FREE PLAN" -const parseBaseUrl = `${config.thirdParties.pageSummarizerBaseUrl}/convert.php?type=expand&lang=en&langfrom=user&url=`; type PageSummary = { title: string; @@ -80,9 +77,7 @@ export const getPageSummaryDiscordView = (pageSummary: PageSummary) => { }; export const getPageSummary = async (pageUrl: string) => { - const [responseError, response] = await resolveCatch( - fetch(`${parseBaseUrl}${pageUrl}`, { method: 'GET' }), - ); + const [responseError, response] = await resolveCatch(fetch(pageUrl, { method: 'GET' })); if (responseError) { throw responseError; } diff --git a/src/modules/fart/fart.module.ts b/src/modules/fart/fart.module.ts index ff024a3..2472611 100644 --- a/src/modules/fart/fart.module.ts +++ b/src/modules/fart/fart.module.ts @@ -1,9 +1,10 @@ import { SlashCommandBuilder } from 'discord.js'; -import type { BotModule } from '../../types/bot'; +import { createModule } from '../../core/createModule'; -export const fart: BotModule = { - slashCommands: [ +export const fart = createModule({ + name: 'fart', + slashCommands: () => [ { schema: new SlashCommandBuilder() .setName('fart') @@ -14,4 +15,4 @@ export const fart: BotModule = { }, }, ], -}; +}); diff --git a/src/modules/patternReplace/patternReplace.module.ts b/src/modules/patternReplace/patternReplace.module.ts index 238550b..1b120a2 100644 --- a/src/modules/patternReplace/patternReplace.module.ts +++ b/src/modules/patternReplace/patternReplace.module.ts @@ -1,7 +1,7 @@ import { MessageType } from 'discord.js'; +import { z } from 'zod'; -import { config } from '../../config'; -import type { BotModule } from '../../types/bot'; +import { createModule } from '../../core/createModule'; const urlMappings = [ { @@ -10,13 +10,17 @@ const urlMappings = [ }, ]; -export const patternReplace: BotModule = { - eventHandlers: { +export const patternReplace = createModule({ + name: 'patternReplace', + env: { + EXCLUDED_CHANNEL_ID: z.string().nonempty(), + }, + eventHandlers: ({ env }) => ({ messageCreate: async (message) => { if ( message.author.bot || message.type !== MessageType.Default || - message.channelId === config.discord.coolLinksChannelId + message.channelId === env.EXCLUDED_CHANNEL_ID ) { return; } @@ -37,6 +41,6 @@ export const patternReplace: BotModule = { await message.channel.send(newMessage); await message.delete(); }, - }, + }), intents: ['GuildMessages', 'MessageContent'], -}; +}); diff --git a/src/modules/quoiFeur/quoiFeur.module.ts b/src/modules/quoiFeur/quoiFeur.module.ts index ebc88d2..9ffc7cf 100644 --- a/src/modules/quoiFeur/quoiFeur.module.ts +++ b/src/modules/quoiFeur/quoiFeur.module.ts @@ -1,14 +1,15 @@ import { SlashCommandBuilder } from 'discord.js'; -import type { BotModule } from '../../types/bot'; +import { createModule } from '../../core/createModule'; import { addQuoiFeurToChannel, reactOnEndWithQuoi, removeQuoiFeurFromChannel, } from './quoiFeur.helpers'; -export const quoiFeur: BotModule = { - slashCommands: [ +export const quoiFeur = createModule({ + name: 'quoiFeur', + slashCommands: () => [ { schema: new SlashCommandBuilder() .setName('quoi-feur') @@ -26,8 +27,8 @@ export const quoiFeur: BotModule = { }, }, ], - eventHandlers: { + eventHandlers: () => ({ messageCreate: reactOnEndWithQuoi, - }, + }), intents: ['Guilds', 'GuildMessages', 'MessageContent', 'GuildMessageReactions'], -}; +}); diff --git a/src/modules/recurringMessage/recurringMessage.module.ts b/src/modules/recurringMessage/recurringMessage.module.ts index 66f92ca..95cd3ea 100644 --- a/src/modules/recurringMessage/recurringMessage.module.ts +++ b/src/modules/recurringMessage/recurringMessage.module.ts @@ -1,6 +1,6 @@ import { SlashCommandBuilder } from 'discord.js'; -import type { BotModule } from '../../types/bot'; +import { createModule } from '../../core/createModule'; import { addRecurringMessage, hasPermission, @@ -9,8 +9,9 @@ import { removeRecurringMessage, } from './recurringMessage.helpers'; -export const recurringMessage: BotModule = { - slashCommands: [ +export const recurringMessage = createModule({ + name: 'recurringMessage', + slashCommands: () => [ { schema: new SlashCommandBuilder() .setName('recurrent') @@ -68,9 +69,9 @@ export const recurringMessage: BotModule = { }, }, ], - eventHandlers: { + eventHandlers: () => ({ // relaunch recurring messages on bot restart ready: relaunchRecurringMessages, - }, + }), intents: ['Guilds'], -}; +}); diff --git a/src/modules/voiceOnDemand/voiceOnDemand.module.ts b/src/modules/voiceOnDemand/voiceOnDemand.module.ts index df5d4e4..30401a6 100644 --- a/src/modules/voiceOnDemand/voiceOnDemand.module.ts +++ b/src/modules/voiceOnDemand/voiceOnDemand.module.ts @@ -1,11 +1,12 @@ import { ChannelType, Guild, SlashCommandBuilder } from 'discord.js'; import { cache } from '../../core/cache'; -import type { BotModule } from '../../types/bot'; +import { createModule } from '../../core/createModule'; import { handleJoin, handleLeave, isJoinState, isLeaveState } from './voiceOnDemand.helpers'; -export const voiceOnDemand: BotModule = { - slashCommands: [ +export const voiceOnDemand = createModule({ + name: 'voiceOnDemand', + slashCommands: () => [ { schema: new SlashCommandBuilder() .setName('voice-on-demand') @@ -51,7 +52,7 @@ export const voiceOnDemand: BotModule = { }, }, ], - eventHandlers: { + eventHandlers: () => ({ voiceStateUpdate: async (oldState, newState) => { const lobbyId = await cache.get('lobbyId'); if (lobbyId === undefined) { @@ -92,6 +93,6 @@ export const voiceOnDemand: BotModule = { ); } }, - }, + }), intents: ['GuildVoiceStates', 'GuildMembers'], -}; +}); From 0bfd13d8926676567468b3e594e82bad146eb680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Wed, 13 Sep 2023 21:33:07 +0200 Subject: [PATCH 02/11] use createModule when possible --- src/__tests__/summarize-cool-pages.spec.ts | 4 --- src/core/createModule.ts | 14 ++++---- src/core/createModules.ts | 33 +++++++++++++++++++ src/core/getIntentsFromModules.ts | 6 ++-- src/core/loadModules.ts | 10 +++--- src/core/routeHandlers.ts | 9 ++--- src/main.ts | 15 ++++++--- .../coolLinksManagement.module.ts | 1 - src/modules/fart/fart.module.ts | 1 - .../patternReplace/patternReplace.module.ts | 1 - src/modules/quoiFeur/quoiFeur.module.ts | 1 - .../recurringMessage.module.ts | 1 - .../voiceOnDemand/voiceOnDemand.module.ts | 1 - 13 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 src/core/createModules.ts diff --git a/src/__tests__/summarize-cool-pages.spec.ts b/src/__tests__/summarize-cool-pages.spec.ts index 8b37bb8..bcfeee1 100644 --- a/src/__tests__/summarize-cool-pages.spec.ts +++ b/src/__tests__/summarize-cool-pages.spec.ts @@ -30,10 +30,6 @@ type SummarizeCoolPagesFixture = ReturnType { let fixture: SummarizeCoolPagesFixture; beforeEach(() => { - // config is mocked to avoid to call the third party API and to avoid to handle env-var - vi.mock('../config', async () => ({ - config: (await import('./mocks/config.mock')).default, - })); // useless atm but will be useful when we will have to reset the fixture fixture = createSummarizeCoolPagesFixture(); }); diff --git a/src/core/createModule.ts b/src/core/createModule.ts index 9b1801c..ac2d9a4 100644 --- a/src/core/createModule.ts +++ b/src/core/createModule.ts @@ -21,27 +21,27 @@ type EventHandlers = { }; type BotModule> = { - name: string; env?: Env; intents?: ClientOptions['intents']; slashCommands?: ModuleFunction>; eventHandlers?: ModuleFunction; }; -interface CreatedModule { +interface CreatedModuleInput { + env: unknown; +} + +export interface CreatedModule { intents: ClientOptions['intents']; slashCommands: Array; eventHandlers: EventHandlers; } -interface CreatedModuleInput { - env: unknown; - cache: unknown; -} +export type ModuleFactory = (input: CreatedModuleInput) => Promise; export const createModule = >( module: BotModule, -): ((input: CreatedModuleInput) => Promise) => { +): ModuleFactory => { return async (input) => { const env = await z.object(module.env ?? ({} as Env)).parseAsync(input.env); diff --git a/src/core/createModules.ts b/src/core/createModules.ts new file mode 100644 index 0000000..830ee50 --- /dev/null +++ b/src/core/createModules.ts @@ -0,0 +1,33 @@ +import { constantCase } from 'constant-case'; + +import type { CreatedModule, ModuleFactory } from './createModule'; + +export const createModules = async ( + modules: Record, +): Promise => { + const createdModules: CreatedModule[] = []; + + for (const [name, factory] of Object.entries(modules)) { + const constantName = constantCase(name); + + const moduleEnv = Object.entries(process.env) + .filter(([key]) => key.startsWith(constantName)) + .reduce>((acc, [key, value]) => { + const envKey = key.replace(constantName, ''); + + if (value === undefined) { + return acc; + } + + acc[envKey] = value; + + return acc; + }, {}); + + const module = await factory({ env: moduleEnv }); + + createdModules.push(module); + } + + return createdModules; +}; diff --git a/src/core/getIntentsFromModules.ts b/src/core/getIntentsFromModules.ts index 1499726..962180e 100644 --- a/src/core/getIntentsFromModules.ts +++ b/src/core/getIntentsFromModules.ts @@ -1,6 +1,6 @@ -import type { BotModule } from '../types/bot'; +import type { CreatedModule } from './createModule'; -export const getIntentsFromModules = (modules: Record) => { - const intents = Object.values(modules).flatMap((module) => module.intents ?? []); +export const getIntentsFromModules = (modules: CreatedModule[]) => { + const intents = modules.flatMap((module) => module.intents ?? []); return [...new Set(intents)] as const; }; diff --git a/src/core/loadModules.ts b/src/core/loadModules.ts index 233f41c..7e2c61d 100644 --- a/src/core/loadModules.ts +++ b/src/core/loadModules.ts @@ -1,15 +1,16 @@ import { type Client } from 'discord.js'; -import type { BotModule } from '../types/bot'; import { checkUniqueSlashCommandNames } from './checkUniqueSlashCommandNames'; +import type { CreatedModule } from './createModule'; import { pushCommands, routeCommands } from './loaderCommands'; import { routeHandlers } from './routeHandlers'; export const loadModules = async ( client: Client, - modulesToLoad: Record, + modules: CreatedModule[], ): Promise => { - const botCommands = Object.values(modulesToLoad).flatMap((module) => module.slashCommands ?? []); + const botCommands = modules.flatMap((module) => module.slashCommands ?? []); + checkUniqueSlashCommandNames(botCommands); routeCommands(client, botCommands); @@ -25,5 +26,6 @@ export const loadModules = async ( guild.id, ); } - routeHandlers(client, modulesToLoad); + + routeHandlers(client, modules); }; diff --git a/src/core/routeHandlers.ts b/src/core/routeHandlers.ts index dc762b3..2745cee 100644 --- a/src/core/routeHandlers.ts +++ b/src/core/routeHandlers.ts @@ -1,15 +1,16 @@ import type { Client, ClientEvents } from 'discord.js'; -import type { BotModule, EventHandler } from '../types/bot'; +import type { EventHandler } from '../types/bot'; +import type { CreatedModule } from './createModule'; -export const routeHandlers = (client: Client, modulesToLoad: Record) => { - const eventNames = Object.values(modulesToLoad).flatMap( +export const routeHandlers = (client: Client, modules: CreatedModule[]) => { + const eventNames = modules.flatMap( (module) => Object.keys(module.eventHandlers ?? {}) as (keyof ClientEvents)[], ); const uniqueEventNames = [...new Set(eventNames)]; uniqueEventNames.forEach((eventName) => { - const eventHandlersToCall = Object.values(modulesToLoad) + const eventHandlersToCall = modules .map((module) => module.eventHandlers?.[eventName]) .filter((e): e is EventHandler => Boolean(e)); diff --git a/src/main.ts b/src/main.ts index c86eafa..a802b7a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,20 +1,27 @@ import { Client } from 'discord.js'; import { config } from './config'; +import { createModules } from './core/createModules'; import { getIntentsFromModules } from './core/getIntentsFromModules'; import { loadModules } from './core/loadModules'; import { modules } from './modules/modules'; const { discord } = config; +const createdModules = await createModules(modules); + const client = new Client({ - intents: ['Guilds', ...getIntentsFromModules(modules)], + intents: ['Guilds', ...getIntentsFromModules(createdModules)], }); await client.login(discord.token); + await new Promise((resolve) => { - client.on('ready', () => { - Object.values(modules).map((module) => module.eventHandlers?.ready?.(client)); + client.on('ready', async () => { + for (const module of createdModules) { + await module.eventHandlers?.ready?.(client); + } + resolve(); }); }); @@ -23,6 +30,6 @@ if (!client.isReady()) { throw new Error('Client should be ready at this stage'); } -await loadModules(client, modules); +await loadModules(client, createdModules); console.log('Bot started.'); diff --git a/src/modules/coolLinksManagement/coolLinksManagement.module.ts b/src/modules/coolLinksManagement/coolLinksManagement.module.ts index a77542b..f292986 100644 --- a/src/modules/coolLinksManagement/coolLinksManagement.module.ts +++ b/src/modules/coolLinksManagement/coolLinksManagement.module.ts @@ -33,7 +33,6 @@ const getThreadNameFromOpenGraph = async (url: string): Promise = const youtubeUrlRegex = new RegExp('^(https?)?(://)?(www.)?(m.)?((youtube.com)|(youtu.be))'); export const coolLinksManagement = createModule({ - name: 'coolLinksManagement', env: { CHANNEL_ID: z.string().nonempty(), PAGE_SUMMARIZER_BASE_URL: z.string().url(), diff --git a/src/modules/fart/fart.module.ts b/src/modules/fart/fart.module.ts index 2472611..c0fa453 100644 --- a/src/modules/fart/fart.module.ts +++ b/src/modules/fart/fart.module.ts @@ -3,7 +3,6 @@ import { SlashCommandBuilder } from 'discord.js'; import { createModule } from '../../core/createModule'; export const fart = createModule({ - name: 'fart', slashCommands: () => [ { schema: new SlashCommandBuilder() diff --git a/src/modules/patternReplace/patternReplace.module.ts b/src/modules/patternReplace/patternReplace.module.ts index 1b120a2..49cfa8c 100644 --- a/src/modules/patternReplace/patternReplace.module.ts +++ b/src/modules/patternReplace/patternReplace.module.ts @@ -11,7 +11,6 @@ const urlMappings = [ ]; export const patternReplace = createModule({ - name: 'patternReplace', env: { EXCLUDED_CHANNEL_ID: z.string().nonempty(), }, diff --git a/src/modules/quoiFeur/quoiFeur.module.ts b/src/modules/quoiFeur/quoiFeur.module.ts index 00837aa..50622d4 100644 --- a/src/modules/quoiFeur/quoiFeur.module.ts +++ b/src/modules/quoiFeur/quoiFeur.module.ts @@ -9,7 +9,6 @@ import { } from './quoiFeur.helpers'; export const quoiFeur = createModule({ - name: 'quoiFeur', slashCommands: () => [ { schema: new SlashCommandBuilder() diff --git a/src/modules/recurringMessage/recurringMessage.module.ts b/src/modules/recurringMessage/recurringMessage.module.ts index c7c5c6c..092953b 100644 --- a/src/modules/recurringMessage/recurringMessage.module.ts +++ b/src/modules/recurringMessage/recurringMessage.module.ts @@ -11,7 +11,6 @@ import { } from './recurringMessage.helpers'; export const recurringMessage = createModule({ - name: 'recurringMessage', slashCommands: () => [ { schema: new SlashCommandBuilder() diff --git a/src/modules/voiceOnDemand/voiceOnDemand.module.ts b/src/modules/voiceOnDemand/voiceOnDemand.module.ts index 522598d..0bbd32a 100644 --- a/src/modules/voiceOnDemand/voiceOnDemand.module.ts +++ b/src/modules/voiceOnDemand/voiceOnDemand.module.ts @@ -5,7 +5,6 @@ import { createModule } from '../../core/createModule'; import { handleJoin, handleLeave, isJoinState, isLeaveState } from './voiceOnDemand.helpers'; export const voiceOnDemand = createModule({ - name: 'voiceOnDemand', slashCommands: () => [ { schema: new SlashCommandBuilder() From 2c47a974910cbbbc841bfcd5f739f5a7115ff2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Wed, 13 Sep 2023 21:53:51 +0200 Subject: [PATCH 03/11] update env and naming --- .env.example | 6 +++--- src/core/{createModules.ts => createAllModules.ts} | 6 ++++-- src/main.ts | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) rename src/core/{createModules.ts => createAllModules.ts} (84%) diff --git a/.env.example b/.env.example index 902d6dc..783cf68 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,7 @@ DISCORD_TOKEN= REDIS_URL= # CHANNELS -COOL_LINKS_CHANNEL_ID= +COOL_LINKS_MANAGEMENT_CHANNEL_ID= +COOL_LINKS_MANAGEMENT_PAGE_SUMMARIZER_BASE_URL= -# API -PAGE_SUMMARIZER_BASE_URL= +PATTERN_REPLACE_EXCLUDED_CHANNEL_ID= \ No newline at end of file diff --git a/src/core/createModules.ts b/src/core/createAllModules.ts similarity index 84% rename from src/core/createModules.ts rename to src/core/createAllModules.ts index 830ee50..3857bdf 100644 --- a/src/core/createModules.ts +++ b/src/core/createAllModules.ts @@ -2,7 +2,7 @@ import { constantCase } from 'constant-case'; import type { CreatedModule, ModuleFactory } from './createModule'; -export const createModules = async ( +export const createAllModules = async ( modules: Record, ): Promise => { const createdModules: CreatedModule[] = []; @@ -13,7 +13,7 @@ export const createModules = async ( const moduleEnv = Object.entries(process.env) .filter(([key]) => key.startsWith(constantName)) .reduce>((acc, [key, value]) => { - const envKey = key.replace(constantName, ''); + const envKey = key.replace(`${constantName}_`, ''); if (value === undefined) { return acc; @@ -24,6 +24,8 @@ export const createModules = async ( return acc; }, {}); + console.log({ moduleEnv, name }); + const module = await factory({ env: moduleEnv }); createdModules.push(module); diff --git a/src/main.ts b/src/main.ts index a802b7a..5c8c715 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,14 @@ import { Client } from 'discord.js'; import { config } from './config'; -import { createModules } from './core/createModules'; +import { createAllModules } from './core/createAllModules'; import { getIntentsFromModules } from './core/getIntentsFromModules'; import { loadModules } from './core/loadModules'; import { modules } from './modules/modules'; const { discord } = config; -const createdModules = await createModules(modules); +const createdModules = await createAllModules(modules); const client = new Client({ intents: ['Guilds', ...getIntentsFromModules(createdModules)], From b665169d42d0db9775b7da5e918bd05887dff27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Wed, 13 Sep 2023 22:04:25 +0200 Subject: [PATCH 04/11] remove log --- src/core/createAllModules.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/createAllModules.ts b/src/core/createAllModules.ts index 3857bdf..5c57f21 100644 --- a/src/core/createAllModules.ts +++ b/src/core/createAllModules.ts @@ -24,9 +24,7 @@ export const createAllModules = async ( return acc; }, {}); - console.log({ moduleEnv, name }); - - const module = await factory({ env: moduleEnv }); + const module = await factory({ env: moduleEnv }); createdModules.push(module); } From f2cb0ba966754a78eaf66cccd68598d3518f7fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Wed, 13 Sep 2023 21:55:07 +0000 Subject: [PATCH 05/11] prettier --- src/core/createAllModules.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/createAllModules.ts b/src/core/createAllModules.ts index 5c57f21..10c577f 100644 --- a/src/core/createAllModules.ts +++ b/src/core/createAllModules.ts @@ -24,7 +24,7 @@ export const createAllModules = async ( return acc; }, {}); - const module = await factory({ env: moduleEnv }); + const module = await factory({ env: moduleEnv }); createdModules.push(module); } From 227abf99170778b7932620fed63f4bde8ef3275d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Fri, 15 Sep 2023 20:54:58 +0200 Subject: [PATCH 06/11] remove unused type --- src/types/bot.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/types/bot.ts b/src/types/bot.ts index efde7ad..5ca590e 100644 --- a/src/types/bot.ts +++ b/src/types/bot.ts @@ -1,7 +1,6 @@ import type { ChatInputCommandInteraction, ClientEvents, - ClientOptions, RESTPostAPIChatInputApplicationCommandsJSONBody, } from 'discord.js'; @@ -17,11 +16,3 @@ export type BotCommand = { schema: RESTPostAPIChatInputApplicationCommandsJSONBody; handler: slashCommandHandler | Record; }; - -export type BotModule = { - slashCommands?: Array; - eventHandlers?: { - [key in keyof ClientEvents]?: EventHandler; - }; - intents?: ClientOptions['intents']; -}; From 2d747a5bff3c9ae8d262bd962a213d5a0cc9de50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Fri, 15 Sep 2023 21:03:10 +0200 Subject: [PATCH 07/11] move ready call at the beginning of loadModules --- src/core/loadModules.ts | 2 ++ src/main.ts | 10 ---------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/core/loadModules.ts b/src/core/loadModules.ts index 7e2c61d..34073e6 100644 --- a/src/core/loadModules.ts +++ b/src/core/loadModules.ts @@ -9,6 +9,8 @@ export const loadModules = async ( client: Client, modules: CreatedModule[], ): Promise => { + await Promise.allSettled(modules.map((module) => module.eventHandlers?.ready?.(client))); + const botCommands = modules.flatMap((module) => module.slashCommands ?? []); checkUniqueSlashCommandNames(botCommands); diff --git a/src/main.ts b/src/main.ts index 5c8c715..988987f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,16 +16,6 @@ const client = new Client({ await client.login(discord.token); -await new Promise((resolve) => { - client.on('ready', async () => { - for (const module of createdModules) { - await module.eventHandlers?.ready?.(client); - } - - resolve(); - }); -}); - if (!client.isReady()) { throw new Error('Client should be ready at this stage'); } From 02ce375a1c24da87cd06093d3b94a641a161705d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Fri, 15 Sep 2023 21:12:56 +0200 Subject: [PATCH 08/11] remove env var and move config to core --- package.json | 1 - pnpm-lock.yaml | 8 -------- src/config.ts | 10 ---------- src/core/cache.ts | 4 ++-- src/core/env.ts | 13 +++++++++++++ src/core/loadModules.ts | 4 +--- src/core/loaderCommands.ts | 21 +++++++++++++-------- src/main.ts | 6 ++---- 8 files changed, 31 insertions(+), 36 deletions(-) delete mode 100644 src/config.ts create mode 100644 src/core/env.ts diff --git a/package.json b/package.json index 9df7fe0..256059f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "constant-case": "3.0.4", "cron": "2.4.3", "discord.js": "14.13.0", - "env-var": "7.4.1", "keyv": "4.5.3", "open-graph-scraper": "6.2.2", "param-case": "3.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4eca266..6467bba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ dependencies: discord.js: specifier: 14.13.0 version: 14.13.0 - env-var: - specifier: 7.4.1 - version: 7.4.1 keyv: specifier: 4.5.3 version: 4.5.3 @@ -1383,11 +1380,6 @@ packages: engines: {node: '>=0.12'} dev: false - /env-var@7.4.1: - resolution: {integrity: sha512-H8Ga2SbXTQwt6MKEawWSvmxoH1+J6bnAXkuyE7eDvbGmrhIL2i+XGjzGM3DFHcJu8GY1zY9/AnBJY8uGQYPHiw==} - engines: {node: '>=10'} - dev: false - /es-abstract@1.21.2: resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} engines: {node: '>= 0.4'} diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index a75e0cf..0000000 --- a/src/config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import env from 'env-var'; - -export const config = { - discord: { - token: env.get('DISCORD_TOKEN').required().asString(), - }, - redis: { - url: env.get('REDIS_URL').required().asString(), - }, -}; diff --git a/src/core/cache.ts b/src/core/cache.ts index 37915a8..f84ecad 100644 --- a/src/core/cache.ts +++ b/src/core/cache.ts @@ -2,8 +2,8 @@ import '@keyv/redis'; import Keyv from 'keyv'; -import { config } from '../config'; import type { Frequency } from '../modules/recurringMessage/recurringMessage.helpers'; +import { env } from './env'; // eslint-disable-next-line @typescript-eslint/no-explicit-any interface CacheGet> { @@ -28,7 +28,7 @@ interface CacheEntries { } class CacheImpl implements Cache { - private readonly backend = new Keyv(config.redis.url); + private readonly backend = new Keyv(env.redisUrl); public get(key: Key): Promise; public get( diff --git a/src/core/env.ts b/src/core/env.ts new file mode 100644 index 0000000..1470c29 --- /dev/null +++ b/src/core/env.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +const envShape = z + .object({ + DISCORD_TOKEN: z.string().nonempty(), + REDIS_URL: z.string().url(), + }) + .transform((object) => ({ + discordToken: object.DISCORD_TOKEN, + redisUrl: object.REDIS_URL, + })); + +export const env = envShape.parse(process.env); diff --git a/src/core/loadModules.ts b/src/core/loadModules.ts index 34073e6..4da7a64 100644 --- a/src/core/loadModules.ts +++ b/src/core/loadModules.ts @@ -23,9 +23,7 @@ export const loadModules = async ( for (const guild of guilds.cache.values()) { await pushCommands( - botCommands.map((command) => command.schema), - clientId, - guild.id, + {commands : botCommands.map((command) => command.schema), clientId : clientId, guildId : guild.id}, ); } diff --git a/src/core/loaderCommands.ts b/src/core/loaderCommands.ts index 3cbb7d7..2e87363 100644 --- a/src/core/loaderCommands.ts +++ b/src/core/loaderCommands.ts @@ -5,18 +5,23 @@ import { Routes, } from 'discord.js'; -import { config } from '../config'; import type { BotCommand } from '../types/bot'; import { deleteExistingCommands } from './deleteExistingCommands'; -const { discord } = config; +interface PushCommandsOptions { + commands: RESTPostAPIChatInputApplicationCommandsJSONBody[]; + clientId: string; + guildId: string; + discordToken: string; +} -export const pushCommands = async ( - commands: RESTPostAPIChatInputApplicationCommandsJSONBody[], - clientId: string, - guildId: string, -) => { - const rest = new REST({ version: '10' }).setToken(discord.token); +export const pushCommands = async ({ + commands, + clientId, + guildId, + discordToken, +}: PushCommandsOptions) => { + const rest = new REST({ version: '10' }).setToken(discordToken); await deleteExistingCommands(rest, clientId, guildId); await rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: commands, diff --git a/src/main.ts b/src/main.ts index 988987f..f4940ec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,20 +1,18 @@ import { Client } from 'discord.js'; -import { config } from './config'; import { createAllModules } from './core/createAllModules'; +import { env } from './core/env'; import { getIntentsFromModules } from './core/getIntentsFromModules'; import { loadModules } from './core/loadModules'; import { modules } from './modules/modules'; -const { discord } = config; - const createdModules = await createAllModules(modules); const client = new Client({ intents: ['Guilds', ...getIntentsFromModules(createdModules)], }); -await client.login(discord.token); +await client.login(env.discordToken); if (!client.isReady()) { throw new Error('Client should be ready at this stage'); From 9e89cb2d76a7c275c5dabcd698184a2d0d8457d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Fri, 15 Sep 2023 21:16:40 +0200 Subject: [PATCH 09/11] refactor out function --- ...ateAllModules.ts => createEnvForModule.ts} | 33 ++++++++++--------- src/main.ts | 2 +- 2 files changed, 18 insertions(+), 17 deletions(-) rename src/core/{createAllModules.ts => createEnvForModule.ts} (50%) diff --git a/src/core/createAllModules.ts b/src/core/createEnvForModule.ts similarity index 50% rename from src/core/createAllModules.ts rename to src/core/createEnvForModule.ts index 10c577f..7467d4a 100644 --- a/src/core/createAllModules.ts +++ b/src/core/createEnvForModule.ts @@ -2,28 +2,29 @@ import { constantCase } from 'constant-case'; import type { CreatedModule, ModuleFactory } from './createModule'; +const createEnvForModule = (constantName: string) => + Object.entries(process.env) + .filter(([key]) => key.startsWith(constantName)) + .reduce>((acc, [key, value]) => { + const envKey = key.replace(`${constantName}_`, ''); + + if (value === undefined) { + return acc; + } + + acc[envKey] = value; + + return acc; + }, {}); + export const createAllModules = async ( modules: Record, ): Promise => { const createdModules: CreatedModule[] = []; for (const [name, factory] of Object.entries(modules)) { - const constantName = constantCase(name); - - const moduleEnv = Object.entries(process.env) - .filter(([key]) => key.startsWith(constantName)) - .reduce>((acc, [key, value]) => { - const envKey = key.replace(`${constantName}_`, ''); - - if (value === undefined) { - return acc; - } - - acc[envKey] = value; - - return acc; - }, {}); - + const moduleConstantName = constantCase(name); + const moduleEnv = createEnvForModule(moduleConstantName); const module = await factory({ env: moduleEnv }); createdModules.push(module); diff --git a/src/main.ts b/src/main.ts index f4940ec..842f820 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { Client } from 'discord.js'; -import { createAllModules } from './core/createAllModules'; +import { createAllModules } from './core/createEnvForModule'; import { env } from './core/env'; import { getIntentsFromModules } from './core/getIntentsFromModules'; import { loadModules } from './core/loadModules'; From 2a08d4e80917980f09d01d5954068c748c2dd093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Fri, 15 Sep 2023 21:25:56 +0200 Subject: [PATCH 10/11] prettier --- src/core/loadModules.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/loadModules.ts b/src/core/loadModules.ts index 4da7a64..8f50b91 100644 --- a/src/core/loadModules.ts +++ b/src/core/loadModules.ts @@ -22,9 +22,11 @@ export const loadModules = async ( const { guilds } = client; for (const guild of guilds.cache.values()) { - await pushCommands( - {commands : botCommands.map((command) => command.schema), clientId : clientId, guildId : guild.id}, - ); + await pushCommands({ + commands: botCommands.map((command) => command.schema), + clientId: clientId, + guildId: guild.id, + }); } routeHandlers(client, modules); From 0830f37b2196a3c83e15f9d80f11722d6637456c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Fri, 15 Sep 2023 21:27:31 +0200 Subject: [PATCH 11/11] add token --- src/core/loadModules.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/loadModules.ts b/src/core/loadModules.ts index 8f50b91..309680b 100644 --- a/src/core/loadModules.ts +++ b/src/core/loadModules.ts @@ -2,6 +2,7 @@ import { type Client } from 'discord.js'; import { checkUniqueSlashCommandNames } from './checkUniqueSlashCommandNames'; import type { CreatedModule } from './createModule'; +import { env } from './env'; import { pushCommands, routeCommands } from './loaderCommands'; import { routeHandlers } from './routeHandlers'; @@ -26,6 +27,7 @@ export const loadModules = async ( commands: botCommands.map((command) => command.schema), clientId: clientId, guildId: guild.id, + discordToken: env.discordToken, }); }