diff --git a/src/commands/build.ts b/src/commands/build.ts index 951f7a5..c7a2244 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -1,17 +1,17 @@ import esbuild from 'esbuild'; import { getConfig } from '../utilities/getConfig'; -import { resolve } from 'node:path'; +import p from 'node:path'; import { glob } from 'glob'; import { configDotenv } from 'dotenv'; import assert from 'node:assert'; import defaultEsbuild from '../utilities/defaultEsbuildConfig'; import { require } from '../utilities/require'; import { pathExists, pathExistsSync } from 'find-up'; -import { mkdir, writeFile } from 'fs/promises'; +import { mkdir, writeFile, readFile } from 'fs/promises'; import * as Preprocessor from '../utilities/preprocessor'; import { bold, magentaBright } from 'colorette'; -const validExtensions = ['.ts', '.js', '.json', '.png', '.jpg', '.jpeg', '.webp']; +const VALID_EXTENSIONS = ['.ts', '.js' ]; type BuildOptions = { /** @@ -24,10 +24,6 @@ type BuildOptions = { * default = esm */ format?: 'cjs' | 'esm'; - /** - * extra esbuild plugins to build with sern. - */ - esbuildPlugins?: esbuild.Plugin[]; /** * https://esbuild.github.io/api/#drop-labels **/ @@ -49,28 +45,39 @@ type BuildOptions = { env?: string; }; +const CommandHandlerPlugin = (buildConfig: Partial, ambientFilePath: string, sernTsConfigPath: string) => { + return { + name: "commandhandler", + setup(build) { + + const options = build.initialOptions + const defVersion = () => JSON.stringify(require(p.resolve('package.json')).version); + options.define = { + ...buildConfig.define ?? {}, + __DEV__: `${buildConfig.mode === 'development'}`, + __PROD__: `${buildConfig.mode === 'production'}`, + __VERSION__: `${buildConfig.defineVersion ? `${defVersion()}` : 'undefined'}` + } ?? {} + Preprocessor.writeTsConfig(buildConfig.format!, sernTsConfigPath, writeFile); + Preprocessor.writeAmbientFile(ambientFilePath, options.define!, writeFile); + + } + } as esbuild.Plugin +} +const resolveBuildConfig = (path: string|undefined, language: string) => { + if(language === 'javascript') { + return path ?? 'jsconfig.json' + } + return path ?? 'tsconfig.json' +} + export async function build(options: Record) { if (!options.supressWarnings) { console.info(`${magentaBright('EXPERIMENTAL')}: This API has not been stabilized. add -W or --suppress-warnings flag to suppress`); } const sernConfig = await getConfig(); - let buildConfig: Partial = {}; - - const entryPoints = await glob(`./src/**/*{${validExtensions.join(',')}}`, { - //for some reason, my ignore glob wasn't registering correctly' - ignore: { - ignored: (p) => p.name.endsWith('.d.ts'), - }, - }); - - const buildConfigPath = resolve(options.project ?? 'sern.build.js'); - - const resolveBuildConfig = (path: string|undefined, language: string) => { - if(language === 'javascript') { - return path ?? resolve('jsconfig.json') - } - return path ?? resolve('tsconfig.json') - } + let buildConfig: BuildOptions; + const buildConfigPath = p.resolve(options.project ?? 'sern.build.js'); const defaultBuildConfig = { defineVersion: true, @@ -78,48 +85,32 @@ export async function build(options: Record) { mode: options.mode ?? 'development', dropLabels: [], tsconfig: resolveBuildConfig(options.tsconfig, sernConfig.language), - env: options.env ?? resolve('.env'), + env: options.env ?? '.env', + include: [] }; if (pathExistsSync(buildConfigPath)) { //throwable, buildConfigPath may not exist - buildConfig = { - ...defaultBuildConfig, - ...(await import('file:///' + buildConfigPath)).default, - }; + buildConfig = { ...defaultBuildConfig, ...(await import('file:///' + buildConfigPath)).default }; } else { buildConfig = defaultBuildConfig; console.log('No build config found, defaulting'); } - - let env = {} as Record; - configDotenv({ path: buildConfig.env, processEnv: env }); - const modeButNotNodeEnvExists = env.MODE && !env.NODE_ENV; - if (modeButNotNodeEnvExists) { - console.warn('Use NODE_ENV instead of MODE'); - console.warn('MODE has no effect.'); - console.warn(`https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production`); - } - - if (env.NODE_ENV) { - buildConfig.mode = env.NODE_ENV as 'production' | 'development'; + configDotenv({ path: buildConfig.env }); + + if (process.env.NODE_ENV) { + buildConfig.mode = process.env.NODE_ENV as 'production' | 'development'; console.log(magentaBright('NODE_ENV:'), 'Found NODE_ENV variable, setting `mode` to this.'); } - assert(buildConfig.mode === 'development' || buildConfig.mode === 'production', 'Mode is not `production` or `development`'); - - try { - let config = require(buildConfig.tsconfig!); + let config = JSON.parse(await readFile(buildConfig.tsconfig!, 'utf8')); config.extends && console.warn("Extend the generated tsconfig") } catch(e) { - console.warn("no tsconfig / jsconfig found"); - console.warn(`Please create a ${sernConfig.language === 'javascript' ? 'jsconfig.json' : 'tsconfig.json' }`); - console.warn("It should have at least extend the generated one sern makes.") - console.warn(` - { - "extends": "./.sern/tsconfig.json", - }`.trim()) - throw e; + console.error("no tsconfig / jsconfig found"); + console.error(`Please create a ${sernConfig.language === 'javascript' ? 'jsconfig.json' : 'tsconfig.json' }`); + console.error('It should have at least extend the generated one sern makes.\n \ + { "extends": "./.sern/tsconfig.json" }'); + throw e; } console.log(bold('Building with:')); @@ -129,41 +120,32 @@ export async function build(options: Record) { console.log(' ', magentaBright('tsconfig'), buildConfig.tsconfig); console.log(' ', magentaBright('env'), buildConfig.env); - const sernDir = resolve('.sern'), - genDir = resolve(sernDir, 'generated'), - ambientFilePath = resolve(sernDir, 'ambient.d.ts'), - packageJsonPath = resolve('package.json'), - sernTsConfigPath = resolve(sernDir, 'tsconfig.json'), - packageJson = () => require(packageJsonPath); + const sernDir = p.resolve('.sern'), + [ambientFilePath, sernTsConfigPath, genDir] = + ['ambient.d.ts', 'tsconfig.json', 'generated'].map(f => p.resolve(sernDir, f)); if (!(await pathExists(genDir))) { console.log('Making .sern/generated dir, does not exist'); await mkdir(genDir, { recursive: true }); } + + const entryPoints = await glob(`src/**/*{${VALID_EXTENSIONS.join(',')}}`,{ + ignore: { + ignored: (p) => p.name.endsWith('.d.ts'), + } + }); + //https://esbuild.github.io/content-types/#tsconfig-json + const ctx = await esbuild.context({ + entryPoints, + plugins: [CommandHandlerPlugin(buildConfig, ambientFilePath, sernTsConfigPath)], + ...defaultEsbuild(buildConfig.format!, buildConfig.tsconfig), + dropLabels: [buildConfig.mode === 'production' ? '__DEV__' : '__PROD__', ...buildConfig.dropLabels!], + }); - try { - const defVersion = () => JSON.stringify(packageJson().version); - const define = { - ...(buildConfig.define ?? {}), - __DEV__: `${buildConfig.mode === 'development'}`, - __PROD__: `${buildConfig.mode === 'production'}`, - } satisfies Record; - - buildConfig.defineVersion && Object.assign(define, { __VERSION__: defVersion() }); - - await Preprocessor.writeTsConfig(buildConfig.format!, sernTsConfigPath, writeFile); - await Preprocessor.writeAmbientFile(ambientFilePath, define, writeFile); - - //https://esbuild.github.io/content-types/#tsconfig-json - await esbuild.build({ - entryPoints, - plugins: [...(buildConfig.esbuildPlugins ?? [])], - ...defaultEsbuild(buildConfig.format!, buildConfig.tsconfig), - define, - dropLabels: [buildConfig.mode === 'production' ? '__DEV__' : '__PROD__', ...buildConfig.dropLabels!], - }); - } catch (e) { - console.error(e); - process.exit(1); + await ctx.rebuild() + if(options.watch) { + await ctx.watch() + } else { + await ctx.dispose() } } diff --git a/src/commands/plugins.ts b/src/commands/plugins.ts index 0dbfdbd..cff9712 100644 --- a/src/commands/plugins.ts +++ b/src/commands/plugins.ts @@ -2,12 +2,13 @@ import { greenBright } from 'colorette'; import fs from 'fs'; import prompt from 'prompts'; import { fetch } from 'undici'; -import { pluginsQ } from '../prompts/plugin.js'; import { fromCwd } from '../utilities/fromCwd.js'; import esbuild from 'esbuild'; -import { getLang } from '../utilities/getLang.js'; +import { getConfig } from '../utilities/getConfig.js'; +import type { PromptObject } from 'prompts'; import { resolve } from 'path'; import { require } from '../utilities/require.js'; + interface PluginData { description: string; hash: string; @@ -15,25 +16,57 @@ interface PluginData { author: string[]; link: string; example: string; - version: '1.0.0'; + version: string; } +const link = `https://raw.githubusercontent.com/sern-handler/awesome-plugins/main/pluginlist.json`; +export async function fetchPluginData(): Promise { + return fetch(link) + .then(res => res.json()) + .then(data => (data as PluginData[])) + .catch(() => []) +} + +export function pluginsQ(choices: PluginData[]): PromptObject[] { + return [{ + name: 'list', + type: 'autocompleteMultiselect', + message: 'What plugins do you want to install?', + choices: choices.map(e => ({ title: e.name, value: e })), + min: 1, + }]; +} /** * Installs plugins to project */ -export async function plugins() { - const e: PluginData[] = (await prompt([await pluginsQ()])).list; - if (!e) process.exit(1); - - const lang = await getLang(); - for await (const plgData of e) { +export async function plugins(args: string[], opts: Record) { + const plugins = await fetchPluginData(); + let selectedPlugins : PluginData[]; + if(args.length) { + const normalizedArgs = args.map(str => str.toLowerCase()) + console.log("Trying to find plugins to install..."); + const results = plugins.reduce((acc, cur) => { + if(normalizedArgs.includes(cur.name.toLowerCase())) { + return [...acc, cur] + } + return acc; + }, [] as PluginData[]); + selectedPlugins = results; + } else { + selectedPlugins = (await prompt(pluginsQ(plugins))).list; + } + if (!selectedPlugins.length) { + process.exit(1); + } + const { language } = await getConfig(); + for await (const plgData of selectedPlugins) { const pluginText = await download(plgData.link); const dir = fromCwd('/src/plugins'); const linkNoExtension = `${process.cwd()}/src/plugins/${plgData.name}`; if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } - if (lang === 'typescript') { + if (language === 'typescript') { fs.writeFileSync(linkNoExtension + '.ts', pluginText); } else { const { type = undefined } = require(resolve('package.json')); @@ -54,7 +87,7 @@ export async function plugins() { } } - const pluginNames = e.map((data) => { + const pluginNames = selectedPlugins.map((data) => { return 'Installed ' + data.name + ' ' + 'from ' + data.author.join(','); }); console.log(`Successfully downloaded plugin(s):\n${greenBright(pluginNames.join('\n'))}`); diff --git a/src/create-publish.mts b/src/create-publish.mts index e145914..9fa0974 100644 --- a/src/create-publish.mts +++ b/src/create-publish.mts @@ -2,65 +2,38 @@ * This file is meant to be run with the esm / cjs esbuild-kit loader to properly import typescript modules */ -import { readdir, stat, mkdir, writeFile } from 'fs/promises'; -import { join, basename, extname, resolve } from 'node:path'; +import { readdir, mkdir, writeFile } from 'fs/promises'; +import { basename, resolve, posix as pathpsx } from 'node:path'; import { pathExistsSync } from 'find-up'; import assert from 'assert'; import { once } from 'node:events'; import * as Rest from './rest'; -import type { sernConfig } from './utilities/getConfig'; +import type { SernConfig } from './utilities/getConfig'; import type { PublishableData, PublishableModule, Typeable } from './create-publish.d.ts'; import { cyanBright, greenBright, redBright } from 'colorette'; import { inspect } from 'node:util' import ora from 'ora'; -async function deriveFileInfo(dir: string, file: string) { - const fullPath = join(dir, file); - return { - fullPath, - fileStats: await stat(fullPath), - base: basename(file), - }; -} - -function isSkippable(filename: string) { - // empty string is for non extension files (directories) - const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', '']; - return filename[0] === '!' || !validExtensions.includes(extname(filename)); -} async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator { - try { - const files = await readdir(dir); - for (const file of files) { - const { fullPath, fileStats, base } = await deriveFileInfo(dir, file); - - if (fileStats.isDirectory()) { - // TODO: refactor so that i dont repeat myself for files (line 71) - if (isSkippable(base)) { - if (shouldDebug) console.info(`ignored directory: ${fullPath}`); - } else { - yield* readPaths(fullPath, shouldDebug); - } - } else { - if (isSkippable(base)) { - if (shouldDebug) console.info(`ignored: ${fullPath}`); - } else { - yield 'file:///' + fullPath; - } + const files = await readdir(dir, { withFileTypes: true }); + for (const file of files) { + const fullPath = pathpsx.join(dir, file.name); + if (file.isDirectory()) { + if (!file.name.startsWith('!')) { + yield* readPaths(fullPath, shouldDebug); } + } else if (!file.name.startsWith('!')) { + yield "file:///"+resolve(fullPath); } - } catch (err) { - throw err; } } // recieved sern config const [{ config, preloads, commandDir }] = await once(process, 'message'), - { paths } = config as sernConfig; + { paths } = config as SernConfig; for (const preload of preloads) { - console.log("preloading: ", preload); await import('file:///' + resolve(preload)); } @@ -85,6 +58,9 @@ for await (const absPath of filePaths) { const filenameNoExtension = filename.substring(0, filename.lastIndexOf('.')); commandModule.name ??= filenameNoExtension; commandModule.description ??= ''; + commandModule.meta = { + absPath + } commandModule.absPath = absPath; if (typeof config === 'function') { config = config(absPath, commandModule); @@ -146,7 +122,7 @@ const makePublishData = ({ commandModule, config }: Record>(); for (const [guildId, array] of guildCommandMap.entries()) { diff --git a/src/index.ts b/src/index.ts index 9bfcc7c..290c769 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ program program .command('plugins') .description('Install plugins from https://github.com/sern-handler/awesome-plugins') - .option('-n --name', 'Name of plugin') + .argument('[names...]', 'Names of plugins to install') .action((...args) => importDynamic('plugins.js').then((m) => m.plugins(...args))); program @@ -66,6 +66,7 @@ program .description('Build your bot') .option('-f --format [fmt]', 'The module system of your application. `cjs` or `esm`', 'esm') .option('-m --mode [mode]', 'the mode for sern to build in. `production` or `development`', 'development') + .option('-w --watch') .option('-W --suppress-warnings', 'suppress experimental warning') .option('-p --project [filePath]', 'build with the provided sern.build file') .option('-e --env', 'path to .env file') diff --git a/src/prompts/plugin.ts b/src/prompts/plugin.ts deleted file mode 100644 index d94efbc..0000000 --- a/src/prompts/plugin.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Choice, PromptObject } from 'prompts'; -import { fetch } from 'undici'; - -async function gimmechoices(): Promise { - const link = `https://raw.githubusercontent.com/sern-handler/awesome-plugins/main/pluginlist.json`; - - const resp = await fetch(link).catch(() => null); - if (!resp) return [{ title: 'No plugins found!', value: '', disabled: true }]; - const data = (await resp.json()) as Data[]; - const choices = data.map((e) => ({ - title: e.name, - value: e, - })); - - return choices; -} - -export async function pluginsQ(): Promise { - return { - name: 'list', - type: 'autocompleteMultiselect', - message: 'What plugins do you want to install?', - choices: await gimmechoices(), - min: 1, - }; -} - -interface Data { - name: string; - link: string; -} diff --git a/src/types/config.d.ts b/src/types/config.d.ts index 0179a39..da4cde8 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -4,9 +4,6 @@ export interface sernConfig { base: string; commands: string; }; - scripts?: { - prepublish?: string; - }; buildPath: string; rest?: Record>; } @@ -14,6 +11,6 @@ export interface sernConfig { export interface TheoreticalEnv { DISCORD_TOKEN: string; APPLICATION_ID?: string; - MODE: 'PROD' | 'DEV'; + MODE: 'production' | 'environment'; [name: string]: string; } diff --git a/src/utilities/defaultEsbuildConfig.ts b/src/utilities/defaultEsbuildConfig.ts index 633c722..431765b 100644 --- a/src/utilities/defaultEsbuildConfig.ts +++ b/src/utilities/defaultEsbuildConfig.ts @@ -1,12 +1,11 @@ import type esbuild from 'esbuild'; import { resolve } from 'path'; -export default (format: 'cjs' | 'esm', tsconfig: string|undefined) => - ({ +export default (format: 'cjs' | 'esm', tsconfig: string|undefined, outdir='dist') => ({ platform: 'node', format, - tsconfig: tsconfig, + tsconfig, logLevel: 'info', minify: false, - outdir: resolve('dist'), + outdir: resolve(outdir), } satisfies esbuild.BuildOptions); diff --git a/src/utilities/getConfig.ts b/src/utilities/getConfig.ts index fee0886..6e163ae 100644 --- a/src/utilities/getConfig.ts +++ b/src/utilities/getConfig.ts @@ -1,17 +1,19 @@ -import { readFile } from 'node:fs/promises'; import { findUp } from 'find-up'; +import { readFile } from 'node:fs/promises'; import assert from 'node:assert'; -export async function getConfig(): Promise { + + +export async function getConfig(): Promise { const sernLocation = await findUp('sern.config.json'); assert(sernLocation, "Can't find sern.config.json"); - const output = JSON.parse(await readFile(sernLocation, 'utf8')) as sernConfig; + const output = JSON.parse(await readFile(sernLocation, 'utf8')) as SernConfig; assert(output, "Can't read your sern.config.json."); return output; } -export interface sernConfig { +export interface SernConfig { language: 'typescript' | 'javascript'; defaultPrefix?: string; paths: { @@ -19,21 +21,7 @@ export interface sernConfig { commands: string; events?: string; }; - app?: { - customInstallUrl?: string; - description?: string; - roleConnectionsVerificationUrl?: string; - installParams?: { - type: 'install params object'; - }; - integrationTypesConfig?: { - type: 'dictionary with keys of application integration types'; - description: 'In preview. Default scopes and permissions for each supported installation context. Value for each key is an integration type configuration object'; - }; - flags?: number; - icon?: '?image data'; - coverImage?: '?image data'; - interactionsEndpointUrl?: string; - tags: string[]; + build?: { + } } diff --git a/src/utilities/getLang.ts b/src/utilities/getLang.ts deleted file mode 100644 index de9263d..0000000 --- a/src/utilities/getLang.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { findUp } from 'find-up'; -import { readFile } from 'node:fs/promises'; - -/** - * It finds the sern.config.json file, reads it, and returns the language property - * @returns The language of the project. - */ -export async function getLang(): Promise<'typescript' | 'javascript'> { - const sernLocation = await findUp('sern.config.json'); - - if (!sernLocation) throw new Error("Can't find sern.config.json"); - - const output = JSON.parse(await readFile(sernLocation, 'utf8')); - - if (!output) throw new Error("Can't read your sern.config.json."); - - return output.language; -} diff --git a/src/utilities/preprocessor.ts b/src/utilities/preprocessor.ts index a31785e..b91360b 100644 --- a/src/utilities/preprocessor.ts +++ b/src/utilities/preprocessor.ts @@ -37,15 +37,14 @@ const writeAmbientFile = async (path: string, define: Record, wr const writeTsConfig = async (format: 'cjs' | 'esm', configPath: string, fw: FileWriter) => { //maybe better way to do this - const target = format === 'esm' ? { target: 'esnext' } : {}; const sernTsConfig = { compilerOptions: { //module determines top level await. CJS doesn't have that abliity afaik module: format === 'cjs' ? 'node' : 'esnext', - moduleResolution: 'node', + moduleResolution: 'node16', strict: true, skipLibCheck: true, - ...target, + target: 'esnext', rootDirs: ['./generated', '../src'], }, include: ['./ambient.d.ts', '../src'], diff --git a/src/utilities/routebuilder.ts b/src/utilities/routebuilder.ts new file mode 100644 index 0000000..ca9b34a --- /dev/null +++ b/src/utilities/routebuilder.ts @@ -0,0 +1,92 @@ +import { readdir, stat } from 'fs/promises'; +import { basename, join, parse, dirname } from 'path'; +import assert from 'assert'; + + +/** + * Import any module based on the absolute path. + * This can accept four types of exported modules + * commonjs, javascript : + * ```js + * exports = commandModule({ }) + * + * //or + * exports.default = commandModule({ }) + * ``` + * esm javascript, typescript, and commonjs typescript + * export default commandModule({}) + */ +export async function importModule(absPath: string) { + let fileModule = await import(absPath); + + let commandModule = fileModule.default; + + assert(commandModule , `Found no export @ ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`); + if ('default' in commandModule ) { + commandModule = commandModule.default; + } + return { module: commandModule } as T; +} + + +export const fmtFileName = (fileName: string) => parse(fileName).name; + + +export const getfilename = (path: string) => fmtFileName(basename(path)); + + + +async function deriveFileInfo(dir: string, file: string) { + const fullPath = join(dir, file); + return { fullPath, + fileStats: await stat(fullPath), + base: basename(file) }; +} + +function parseWildcardName(filename: string): string | null { + const wildcardMatch = filename.match(/\[(.*?)\]/); + return wildcardMatch ? wildcardMatch[1] : null; +} + +export class RouteEntry { + public import_path: string + public filename: string + public parent: string + public wildcardName: string | null; + constructor(public route: string) { + this.import_path = "file:///"+route + this.filename = getfilename(this.route) + this.parent = dirname(this.route); + this.wildcardName = parseWildcardName(this.filename); + } +} + +export interface ReadPathsConfig { + dir: string + onDir?: (dir: string) => Promise|boolean + onEntry?: (etry: RouteEntry) => RouteEntry +} + +export async function* readPaths( + config: ReadPathsConfig +): AsyncGenerator { + const files = await readdir(config.dir); + for (const file of files) { + const { fullPath, fileStats } = await deriveFileInfo(config.dir, file); + if (fileStats.isDirectory()) { + if(config.onDir && await config.onDir(fullPath)) { + yield* readPaths({ ...config, dir: fullPath }); + } else { + yield* readPaths({ ...config, dir: fullPath }); + } + } else { + const nowindowsPath = fullPath.replace(/\\/g, '/'); + if(config.onEntry) { + yield config.onEntry(new RouteEntry(nowindowsPath)); + } else { + yield new RouteEntry(nowindowsPath); + } + } + } +} + diff --git a/templates/cf.js b/templates/cf.js new file mode 100644 index 0000000..db18cd5 --- /dev/null +++ b/templates/cf.js @@ -0,0 +1,117 @@ +// i got this idea from chooks22 +"use modules"; + +import { + InteractionResponseFlags, + InteractionType, + verifyKey, + InteractionResponseType +} from 'discord-interactions'; +//this will import all the modules statically + +class JsonResponse extends Response { + constructor(body, init) { + const jsonBody = JSON.stringify(body); + init = init || { + headers: { + 'content-type': 'application/json;charset=UTF-8', + }, + }; + super(jsonBody, init); + } +} + +function createContext(rawcontext) { + return rawcontext +} +async function executeModule( + emitter, + logger, + errHandler, + { module, task, args }, +) { + try { + await module.execute(args); + //emitter.emit('module.activate', /*resultPayload(PayloadType.Success, module)*/); + } catch(e) { + throw e + } + +} + +async function applyPlugins(module, payload) { + let success = true; + for (const plg of module.onEvent) { + const res = await plg.execute(payload); + if(!res.isOk()) { + success = false; + } + } + return success; +} + +const router = Router(); + +/** + * A simple :wave: hello page to verify the worker is working. + */ +router.get('/', (request, env) => { + return new Response(`👋 ${env.DISCORD_APPLICATION_ID}`); +}); + +/** + * Main route for all requests sent from Discord. All incoming messages will + * include a JSON payload described here: + * https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object + */ +router.post('/', async (request, env) => { + const { isValid, interaction } = await server.verifyDiscordRequest( + request, + env, + ); + if (!isValid || !interaction) { + return new Response('Bad request signature.', { status: 401 }); + } + + if (interaction.type === InteractionType.PING) { + // The `PING` message is used during the initial webhook handshake, and is + // required to configure the webhook in the developer portal. + return new JsonResponse({ + type: InteractionResponseType.PONG, + }); + } + if(interaction.type === InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE) { + "use autocomplete"; + } else if (interaction.type === InteractionType.APPLICATION_COMMAND) { + "use slash"; + } + + console.error('Unknown Type'); + return new JsonResponse({ error: 'Unknown Type' }, { status: 400 }); +}); +router.all('*', () => new Response('Not Found.', { status: 404 })); + + +async function verifyDiscordRequest(request, env) { + const signature = request.headers.get('x-signature-ed25519'); + const timestamp = request.headers.get('x-signature-timestamp'); + const body = await request.text(); + const isValidRequest = + signature && + timestamp && + verifyKey(body, signature, timestamp, env.DISCORD_PUBLIC_KEY); + if (!isValidRequest) { + return { isValid: false }; + } + + return { interaction: JSON.parse(body), isValid: true }; +} + + +const server = { + verifyDiscordRequest: verifyDiscordRequest, + fetch: async function (request, env) { + return router.handle(request, env); + }, +}; +export default server; diff --git a/tsup.config.ts b/tsup.config.ts index b9eb246..fbb520d 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'tsup'; import { createRequire } from 'node:module'; + const shared = { entry: [ 'src/index.ts',