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/package.json b/package.json index 7649bf7..0cd5961 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,13 @@ "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.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24d69c9..615b200 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,15 +11,15 @@ 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 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 @@ -29,6 +29,9 @@ dependencies: param-case: specifier: 3.0.4 version: 3.0.4 + zod: + specifier: 3.22.2 + version: 3.22.2 devDependencies: '@types/node': @@ -1187,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: @@ -1348,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: @@ -1369,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'} @@ -2307,7 +2313,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: @@ -2399,7 +2405,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: @@ -3093,6 +3099,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: @@ -3313,3 +3325,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/__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/config.ts b/src/config.ts deleted file mode 100644 index 69ef576..0000000 --- a/src/config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import env from 'env-var'; - -export const config = { - discord: { - token: env.get('DISCORD_TOKEN').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/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/createEnvForModule.ts b/src/core/createEnvForModule.ts new file mode 100644 index 0000000..7467d4a --- /dev/null +++ b/src/core/createEnvForModule.ts @@ -0,0 +1,34 @@ +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 moduleConstantName = constantCase(name); + const moduleEnv = createEnvForModule(moduleConstantName); + const module = await factory({ env: moduleEnv }); + + createdModules.push(module); + } + + return createdModules; +}; diff --git a/src/core/createModule.ts b/src/core/createModule.ts new file mode 100644 index 0000000..ac2d9a4 --- /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> = { + env?: Env; + intents?: ClientOptions['intents']; + slashCommands?: ModuleFunction>; + eventHandlers?: ModuleFunction; +}; + +interface CreatedModuleInput { + env: unknown; +} + +export interface CreatedModule { + intents: ClientOptions['intents']; + slashCommands: Array; + eventHandlers: EventHandlers; +} + +export type ModuleFactory = (input: CreatedModuleInput) => Promise; + +export const createModule = >( + module: BotModule, +): ModuleFactory => { + 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/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/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..309680b 100644 --- a/src/core/loadModules.ts +++ b/src/core/loadModules.ts @@ -1,15 +1,19 @@ import { type Client } from 'discord.js'; -import type { BotModule } from '../types/bot'; import { checkUniqueSlashCommandNames } from './checkUniqueSlashCommandNames'; +import type { CreatedModule } from './createModule'; +import { env } from './env'; 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 ?? []); + await Promise.allSettled(modules.map((module) => module.eventHandlers?.ready?.(client))); + + const botCommands = modules.flatMap((module) => module.slashCommands ?? []); + checkUniqueSlashCommandNames(botCommands); routeCommands(client, botCommands); @@ -19,11 +23,13 @@ export const loadModules = async ( const { guilds } = client; for (const guild of guilds.cache.values()) { - await pushCommands( - botCommands.map((command) => command.schema), - clientId, - guild.id, - ); + await pushCommands({ + commands: botCommands.map((command) => command.schema), + clientId: clientId, + guildId: guild.id, + discordToken: env.discordToken, + }); } - routeHandlers(client, modulesToLoad); + + routeHandlers(client, modules); }; 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/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..842f820 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,28 +1,23 @@ import { Client } from 'discord.js'; -import { config } from './config'; +import { createAllModules } from './core/createEnvForModule'; +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(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)); - resolve(); - }); -}); +await client.login(env.discordToken); 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 e13f37f..f292986 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,17 @@ 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({ + 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 +73,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..c0fa453 100644 --- a/src/modules/fart/fart.module.ts +++ b/src/modules/fart/fart.module.ts @@ -1,9 +1,9 @@ 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({ + slashCommands: () => [ { schema: new SlashCommandBuilder() .setName('fart') @@ -14,4 +14,4 @@ export const fart: BotModule = { }, }, ], -}; +}); diff --git a/src/modules/patternReplace/patternReplace.module.ts b/src/modules/patternReplace/patternReplace.module.ts index 238550b..49cfa8c 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,16 @@ const urlMappings = [ }, ]; -export const patternReplace: BotModule = { - eventHandlers: { +export const patternReplace = createModule({ + 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 +40,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 bdae8f4..50622d4 100644 --- a/src/modules/quoiFeur/quoiFeur.module.ts +++ b/src/modules/quoiFeur/quoiFeur.module.ts @@ -1,6 +1,6 @@ import { SlashCommandBuilder } from 'discord.js'; -import type { BotModule } from '../../types/bot'; +import { createModule } from '../../core/createModule'; import { addQuoiFeurToChannel, cleanCacheOnChannelDelete, @@ -8,8 +8,8 @@ import { removeQuoiFeurFromChannel, } from './quoiFeur.helpers'; -export const quoiFeur: BotModule = { - slashCommands: [ +export const quoiFeur = createModule({ + slashCommands: () => [ { schema: new SlashCommandBuilder() .setName('quoi-feur') @@ -27,9 +27,9 @@ export const quoiFeur: BotModule = { }, }, ], - eventHandlers: { + eventHandlers: () => ({ messageCreate: reactOnEndWithQuoi, channelDelete: cleanCacheOnChannelDelete, - }, + }), intents: ['Guilds', 'GuildMessages', 'MessageContent', 'GuildMessageReactions'], -}; +}); diff --git a/src/modules/recurringMessage/recurringMessage.module.ts b/src/modules/recurringMessage/recurringMessage.module.ts index bc24c7c..092953b 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, @@ -10,8 +10,8 @@ import { removeRecurringMessage, } from './recurringMessage.helpers'; -export const recurringMessage: BotModule = { - slashCommands: [ +export const recurringMessage = createModule({ + slashCommands: () => [ { schema: new SlashCommandBuilder() .setName('recurrent') @@ -69,9 +69,9 @@ export const recurringMessage: BotModule = { }, }, ], - eventHandlers: { + eventHandlers: () => ({ ready: relaunchRecurringMessages, channelDelete: removeAllFromChannel, - }, + }), intents: ['Guilds'], -}; +}); diff --git a/src/modules/voiceOnDemand/voiceOnDemand.module.ts b/src/modules/voiceOnDemand/voiceOnDemand.module.ts index 6257958..1166107 100644 --- a/src/modules/voiceOnDemand/voiceOnDemand.module.ts +++ b/src/modules/voiceOnDemand/voiceOnDemand.module.ts @@ -1,7 +1,7 @@ import { ChannelType, Guild, SlashCommandBuilder } from 'discord.js'; import { cache } from '../../core/cache'; -import type { BotModule } from '../../types/bot'; +import { createModule } from '../../core/createModule'; import { handleJoinLobby, handleLeaveOnDemand, @@ -9,8 +9,8 @@ import { isLeaveState, } from './voiceOnDemand.helpers'; -export const voiceOnDemand: BotModule = { - slashCommands: [ +export const voiceOnDemand = createModule({ + slashCommands: () => [ { schema: new SlashCommandBuilder() .setName('voice-on-demand') @@ -58,7 +58,7 @@ export const voiceOnDemand: BotModule = { }, }, ], - eventHandlers: { + eventHandlers: () => ({ voiceStateUpdate: async (oldState, newState) => { const lobbyIds = await cache.get('lobbyIds', []); const onDemandChannels = await cache.get('onDemandChannels', []); @@ -108,6 +108,6 @@ export const voiceOnDemand: BotModule = { }), ); }, - }, + }), intents: ['GuildVoiceStates', 'GuildMembers'], -}; +}); 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']; -};