import { resolve } from 'node:path' import defu from 'defu' import type { PartialDeep, Promisable } from 'type-fest' import type { APIInteraction } from 'discord-api-types/v10' import type { AxiosResponse } from 'axios' import type { CordoInteraction } from '../interaction' import type { StringComponentType } from '../../components/component' import { parseFlags as runParseFlags, type FlagOpts as RunFlagOpts } from '../../functions/impl/run' import { parseFlags as gotoParseFlags, type FlagOpts as GotoFlagOpts } from '../../functions/impl/goto' import type { RouteRequest } from './route' const CordoConfigSymbol = Symbol.for('CordoConfig') type HookFor = null | ((value: T, context: Context) => Promisable) type TransformHookFor = null | ((value: T, context: Context) => T) export type CordoConfig = { paths: { root: string routes: string lockfile: string types: string | null } defaults: { commandRoutePrefix?: string } upstream: { baseUrl: string /** if an interaction was not replied to within X millis, cordo will send the appropriate defer/ack response. Set to 0 to disable */ autoDeferMs: number } client: { id: string publicKey: string } /** hooks allow you to process certain data at certain points during cordo's internals. you can always return null to stop the flow of data at that point. */ hooks: { /** gets called before cordo does anything with the interaction */ onRawInteraction: HookFor, /** gets called after being read but before getting handled */ onBeforeHandle: HookFor, /** gets called before a response is returned or sent to discord */ onBeforeRespond: HookFor | null, { interaction: CordoInteraction }> /** gets called with the response of outgoing api calls. DOES NOT get called on the first response as that's not an outgoing api call */ onAfterRespond: HookFor, /** gets called when outgoing api calls fail */ onNetworkError: HookFor, /** transforms the name of the invoked command to a file or route name */ transformCommandName: TransformHookFor, /** gets called by all cordo builtin components that render user facing text. e.g. buttons, text components, selects, etc */ transformUserFacingText: TransformHookFor, /** capture errors that were produced while calling a funct yet could not be assigned a route and thus not be captured by a route error boundary */ captureUnroutableErrors: HookFor /** capture errors that were not caught by any other error boundary */ captureUnhandledErrors: HookFor } functDefaultFlags: { run: Required goto: Required } } export type ParsedCordoConfig = CordoConfig & { functDefaultFlags: { runBits: number gotoBits: number } } export function defineCordoConfig(conf: PartialDeep = {}): PartialDeep & { [CordoConfigSymbol]: typeof CordoConfigSymbol } { return { ...conf, [CordoConfigSymbol]: CordoConfigSymbol } } export namespace ConfigInternals { const defaultConfig: CordoConfig = { paths: { root: '.', routes: './routes', lockfile: './cordo.lock', types: null, }, defaults: { commandRoutePrefix: 'command' }, upstream: { baseUrl: 'https://discord.com/api/v10', autoDeferMs: 50 }, client: { id: '', publicKey: '' }, hooks: { onRawInteraction: null, onBeforeHandle: null, onBeforeRespond: null, onAfterRespond: null, onNetworkError: null, transformCommandName: null, transformUserFacingText: null, captureUnroutableErrors: null, captureUnhandledErrors: null }, functDefaultFlags: { goto: { asReply: false, private: false, disableComponents: false }, run: { wait: false, continueOnError: false, privateErrorMessage: false } } } function locatePath() { if (process.env.CORDO_CONFIG_PATH) return String(process.env.CORDO_CONFIG_PATH) const searchRoot = process.cwd() return resolve(searchRoot, 'cordo.config.ts') } async function readConfig(): Promise { try { const filePath = locatePath() const content = await import(filePath) if (!content || !content.default) return defaultConfig if (!content.default[CordoConfigSymbol]) return defaultConfig return defu(content.default, defaultConfig) } catch (ex) { return defaultConfig } } function resolveRelativePath(path: string, basePath = process.cwd()) { return resolve(basePath, path) } export async function readAndParseConfig(): Promise { const config = await readConfig() config.paths.root = resolveRelativePath(config.paths.root) config.paths.lockfile = resolveRelativePath(config.paths.lockfile, config.paths.root) config.paths.types = config.paths.types ? resolveRelativePath(config.paths.types, config.paths.root) : null config.paths.routes = resolveRelativePath(config.paths.routes, config.paths.root) return { ...config, functDefaultFlags: { run: config.functDefaultFlags.run, runBits: runParseFlags(config.functDefaultFlags.run), goto: config.functDefaultFlags.goto, gotoBits: gotoParseFlags(config.functDefaultFlags.goto) } } } }