import { ApplicationCommandOptionType, ApplicationCommandType, ComponentType, InteractionResponseType, InteractionType, type APIApplicationCommand, type APIInteraction } from "discord-api-types/v10" import type { Method } from "axios" import axios from "axios" import { FunctCompiler } from "../functions/compiler" import { FunctInternals } from "../functions/funct" import { MissingContextError } from "../errors/builtin/missing-context" import { InteractionInternals, type CordoInteraction } from "./interaction" import { CordoMagic } from "./magic" import type { LockfileInternals } from "./files/lockfile" import type { ParsedCordoConfig } from "./files/config" import { Hooks } from "./hooks" import { RoutingResolve } from "./routing/resolve" import { RoutingRespond } from "./routing/respond" export namespace CordoGateway { /** * list of components that cannot have arbitrary user data in the response's value/values fields and thus can be safely parsed and evaluated * IMPORTANT: never parse value/values of components not in this list as this would allow arbitrary code execution */ const executeAllowedComponentTypes = [ ComponentType.RadioGroup, ComponentType.Checkbox, ComponentType.CheckboxGroup, ComponentType.StringSelect ] export async function triggerInteraction(opts: { interaction: CordoInteraction | APIInteraction, httpCallback?: (payload: any) => any lockfile: LockfileInternals.ParsedLockfile config: ParsedCordoConfig }) { const rawInteraction = await Hooks.callHook('onRawInteraction', opts.interaction, undefined, opts.config) if (!rawInteraction) return if (rawInteraction.type === InteractionType.Ping) return handlePing(rawInteraction, opts.httpCallback) const interaction = InteractionInternals.upgrade(rawInteraction) const internals = InteractionInternals.get(interaction) if (opts.httpCallback) internals.httpCallback = opts.httpCallback CordoMagic.Internals.runWithCtx(() => handleInteraction(interaction), { invoker: interaction, lockfile: opts.lockfile, config: opts.config, cwd: '', idCounter: 0 }) } function handlePing(_i: CordoInteraction | APIInteraction, httpCallback?: (payload: any) => any) { httpCallback?.({ type: InteractionResponseType.Pong }) } function apiRequest(method: Method, url: string, body?: Record) { const config = CordoMagic.getConfig() if (!config) throw new MissingContextError('Could not make API request, no config found in context.') return axios({ method, url, baseURL: config.upstream.baseUrl, headers: config.client.token ? { Authorization: `Bot ${config.client.token}` } : undefined, data: body, validateStatus: null }) .then(res => Hooks.callHook('onAfterRespond', res)) .catch(res => Hooks.callHook('onNetworkError', res)) } /** if the payload is null the interaction will be defered if not done already */ export async function respondTo(i: CordoInteraction, payload: Record | null, opts: { ephemeral?: boolean } = {}) { payload = await Hooks.callHook('onBeforeRespond', payload, { interaction: i }) // if not specifically set, check if it's triggered by a command and if so -> is that command private? if (opts?.ephemeral === undefined) { const commandPrivate = InteractionInternals.get(i).commandEntrypoint?.private opts.ephemeral = typeof commandPrivate === 'function' ? commandPrivate(i) : Boolean(commandPrivate) } const internals = InteractionInternals.get(i) if (internals.httpCallback && !internals.answered) { internals.answered = true if (payload) return internals.httpCallback(payload) if (i.type === InteractionType.ApplicationCommand) return internals.httpCallback({ type: InteractionResponseType.DeferredChannelMessageWithSource, data: { flags: opts?.ephemeral ? 1 << 6 : 0 } }) if (i.type === InteractionType.MessageComponent) return internals.httpCallback({ type: InteractionResponseType.DeferredMessageUpdate }) return null } if (!internals.answered) { internals.answered = true if (payload) return apiRequest('post', `/interactions/${i.id}/${i.token}/callback`, payload) if (i.type === InteractionType.ApplicationCommand) return apiRequest('post', `/interactions/${i.id}/${i.token}/callback`, { type: InteractionResponseType.DeferredChannelMessageWithSource, data: { flags: opts?.ephemeral ? 1 << 6 : 0 } }) if (i.type === InteractionType.MessageComponent) return apiRequest('post', `/interactions/${i.id}/${i.token}/callback`, { type: InteractionResponseType.DeferredMessageUpdate }) } if (!payload) return null const { type, data } = payload if (!type || !data) return null const config = CordoMagic.getConfig() if (!config) throw new MissingContextError('Could not respond to interaction, no config found in context.') if (!config.client.id) { console.warn(`No client id provided in config. Cannot respond to interactions.`) return null } if (type === InteractionResponseType.ChannelMessageWithSource) return apiRequest('post', `/webhooks/${config.client.id}/${i.token}`, data) if (type === InteractionResponseType.UpdateMessage) return apiRequest('patch', `/webhooks/${config.client.id}/${i.token}/messages/@original`, data) return null } // async function handleInteraction(i: CordoInteraction) { i = await Hooks.callHook('onBeforeHandle', i) if (!i) return const config = CordoMagic.getConfig() if (!config) throw new MissingContextError('Could not handle interaction, no config found in context.') const deferAfter = config.upstream.autoDeferMs if (deferAfter) { setTimeout(() => { if (!InteractionInternals.get(i).answered) respondTo(i, null) }, deferAfter) } if (i.type === InteractionType.ApplicationCommand) return handleCommandInteraction(i) else if (i.type === InteractionType.MessageComponent) return handleComponentInteraction(i) else if (i.type === InteractionType.ModalSubmit) return handleModalInteraction(i) } function handleCommandInteraction(i: CordoInteraction & { type: InteractionType.ApplicationCommand }) { // slash commands: need to check for subcommands if (i.data.type === ApplicationCommandType.ChatInput) { let name = i.data.name let option = i.data.options?.[0] while (option?.type === ApplicationCommandOptionType.Subcommand || option?.type === ApplicationCommandOptionType.SubcommandGroup) { name += ` ${option.name}` option = option.options?.[0] } const { route, path } = RoutingResolve.getRouteForCommand(name, 'slash') CordoMagic.setCwd(path) if (route.routeFilePath && CordoMagic.getLockfile()?.$runtime.registeredCommands.has(route.routeFilePath)) InteractionInternals.get(i).commandEntrypoint = CordoMagic.getLockfile()!.$runtime.registeredCommands.get(route.routeFilePath)! return RoutingRespond.callRoute(route.routeId, route.args, i) } // message/user commands: go ahead if (i.data.type === ApplicationCommandType.Message || i.data.type === ApplicationCommandType.User) { const { route, path } = RoutingResolve.getRouteForCommand( i.data.name, i.data.type === ApplicationCommandType.Message ? 'message' : 'user' ) CordoMagic.setCwd(path) if (route.routeFilePath && CordoMagic.getLockfile()?.$runtime.registeredCommands.has(route.routeFilePath)) InteractionInternals.get(i).commandEntrypoint = CordoMagic.getLockfile()!.$runtime.registeredCommands.get(route.routeFilePath)! return RoutingRespond.callRoute(route.routeId, route.args, i) } } async function handleComponentInteraction(i: CordoInteraction & { type: InteractionType.MessageComponent }) { if (i.data.component_type === ComponentType.StringSelect) { const success = await parseAndEvokeComponentWithValues(i.data, i) if (!success) return } const id = i.data.custom_id const parsedCustomId = FunctCompiler.parseCustomId(id) if (parsedCustomId.cwd) CordoMagic.setCwd(parsedCustomId.cwd) if (!parsedCustomId.functs.length && !InteractionInternals.get(i).answered) await respondTo(i, null) for (const action of parsedCustomId.functs) { const success = await FunctInternals.evalFunct(action, i) if (!success) return } } async function handleModalInteraction(i: CordoInteraction & { type: InteractionType.ModalSubmit }) { const values = new Map() for (const component of i.data.components) { const success = await parseAndEvokeModalLeafComponents(component, i, values) if (!success) return } // @ts-expect-error this type doesn't have a values property (yet) i.data.values = values const id = i.data.custom_id const parsedCustomId = FunctCompiler.parseCustomId(id) if (parsedCustomId.cwd) CordoMagic.setCwd(parsedCustomId.cwd) if (!parsedCustomId.functs.length && !InteractionInternals.get(i).answered) await respondTo(i, null) for (const action of parsedCustomId.functs) { const success = await FunctInternals.evalFunct(action, i) if (!success) return } } // async function parseAndEvokeModalLeafComponents(c: any, i: CordoInteraction, collectedFormItems: Map): Promise { if (c.type === ComponentType.ActionRow) { for (const item of c.components) { const success = await parseAndEvokeModalLeafComponents(item, i, collectedFormItems) if (!success) return false } return true } if (c.type === ComponentType.Label) { const success = await parseAndEvokeModalLeafComponents(c.component, i, collectedFormItems) return success } const parsedCustomId = FunctCompiler.parseCustomId(c.custom_id) if (!parsedCustomId.functs.length && !parsedCustomId.values.length) return true const formRef = parsedCustomId.values[0] // if ('values' in c && c.values.length > 0 && typeof c.values[0] === 'string') { if (executeAllowedComponentTypes.includes(c.type)) { const success = await parseAndEvokeComponentWithValues(c, i) if (!success) return false } if (formRef) collectedFormItems.set(formRef, c.values) } else if ('value' in c && typeof c.value === 'string') { if (executeAllowedComponentTypes.includes(c.type)) { const success = await parseAndEvokeComponentWithValue(c, i) if (!success) return false } if (formRef) collectedFormItems.set(formRef, c.value) } return true } async function parseAndEvokeComponentWithValues( data: ({ type: number } | { component_type: number }) & { values: string[] }, i: CordoInteraction ): Promise { if (!executeAllowedComponentTypes.includes('type' in data ? data.type : data.component_type)) // just to be sure return true const options = data.values.map(v => FunctCompiler.parseCustomId(v)) data.values = options.flatMap(o => o.values) for (const option of options) { if (option.cwd) CordoMagic.setCwd(option.cwd) for (const action of option.functs) { const success = await FunctInternals.evalFunct(action, i) if (!success) return false } } return true } async function parseAndEvokeComponentWithValue( data: ({ type: number } | { component_type: number }) & { value: string }, i: CordoInteraction ): Promise { if (!executeAllowedComponentTypes.includes('type' in data ? data.type : data.component_type)) // just to be sure return true const option = FunctCompiler.parseCustomId(data.value) data.value = option.values[0] if (option.cwd) CordoMagic.setCwd(option.cwd) for (const action of option.functs) { const success = await FunctInternals.evalFunct(action, i) if (!success) return false } return true } export function upsertCommand(command: APIApplicationCommand, guild?: string) { const config = CordoMagic.getConfig() if (!config?.client.id) throw new MissingContextError('Could not upsert command, no client id found in config context.') if (!config?.client.token) throw new MissingContextError('Could not upsert command, no bot token found in config context (client.token).') const endpoint = guild ? `/applications/${config.client.id}/guilds/${guild}/commands` : `/applications/${config.client.id}/commands` return apiRequest('post', endpoint, command) } export function bulkUpsertCommands(commands: APIApplicationCommand[], guild?: string) { const config = CordoMagic.getConfig() if (!config?.client.id) throw new MissingContextError('Could not bulk upsert commands, no client id found in config context.') if (!config?.client.token) throw new MissingContextError('Could not bulk upsert commands, no bot token found in config context (client.token).') const endpoint = guild ? `/applications/${config.client.id}/guilds/${guild}/commands` : `/applications/${config.client.id}/commands` return apiRequest('put', endpoint, commands) } }