diff --git a/scripts/schemaExclusions.json b/scripts/schemaExclusions.json index f25e8f0..82e545a 100644 --- a/scripts/schemaExclusions.json +++ b/scripts/schemaExclusions.json @@ -3,7 +3,8 @@ "MessageInteractionSchema" ], "includeRe": [ - "^MessageComponentType\\..*" + "^MessageComponentType\\..*", + "^InteractionCallbackType\\..*" ], "manual": [ "DefaultSchema", diff --git "a/src/api/routes/interactions/\043interaction_id/\043interaction_token/callback.ts" "b/src/api/routes/interactions/\043interaction_id/\043interaction_token/callback.ts" index d9af736..6c7fc12 100644 --- "a/src/api/routes/interactions/\043interaction_id/\043interaction_token/callback.ts" +++ "b/src/api/routes/interactions/\043interaction_id/\043interaction_token/callback.ts" @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { BaseMessageComponents, InteractionCallbackSchema, InteractionCallbackType, MessageType } from "@spacebar/schemas"; +import { BaseMessageComponents, InteractionCallbackSchema, InteractionCallbacksSchema, InteractionCallbackType, InteractionFailureReason, MessageType } from "@spacebar/schemas"; import { handleComps, route, sendMessage } from "@spacebar/api"; import { Request, Response, Router } from "express"; import { Config, emitEvent, InteractionSuccessEvent, Message, MessageUpdateEvent, pendingInteractions, User, InteractionFailureEvent } from "@spacebar/util"; @@ -24,40 +24,46 @@ const router = Router({ mergeParams: true }); -router.post("/", route({}), async (req: Request, res: Response) => { - const body = req.body as InteractionCallbackSchema; +router.post( + "/", + route({ + stripNulls: true, + requestBody: "InteractionCallbacksSchema", + }), + async (req: Request, res: Response) => { + const body = req.body as InteractionCallbacksSchema; - const interactionId = req.params.interaction_id as string; - const interaction = pendingInteractions.get(req.params.interaction_id); + const interactionId = req.params.interaction_id as string; + const interaction = pendingInteractions.get(req.params.interaction_id); - if (!interaction) { - return; - } + if (!interaction) { + return; + } - clearTimeout(interaction.timeout); + clearTimeout(interaction.timeout); await emitEvent({ - event: "INTERACTION_SUCCESS", - user_id: interaction?.userId, - data: { - id: interactionId, - nonce: interaction?.nonce, - }, - } as InteractionSuccessEvent); + event: "INTERACTION_SUCCESS", + user_id: interaction?.userId, + data: { + id: interactionId, + nonce: interaction?.nonce, + }, + } as InteractionSuccessEvent); - switch (body.type) { - case InteractionCallbackType.PONG: - // TODO - break; - case InteractionCallbackType.ACKNOWLEDGE: - // Deprected - break; - case InteractionCallbackType.CHANNEL_MESSAGE: - // TODO - break; - case InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE: { - const user = await User.findOneOrFail({ where: { id: interaction.userId } }); - /* + switch (body.type) { + case InteractionCallbackType.PONG: + // TODO + break; + case InteractionCallbackType.ACKNOWLEDGE: + // Deprected + break; + case InteractionCallbackType.CHANNEL_MESSAGE: + // TODO + break; + case InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE: { + const user = await User.findOneOrFail({ where: { id: interaction.userId } }); + /* const files = (req.files as Express.Multer.File[]) ?? []; //I don't think traditional attachments are allowed anyways const attachments: (Attachment | MessageCreateAttachment | MessageCreateCloudAttachment)[] = []; @@ -70,123 +76,126 @@ } } */ - await sendMessage({ - type: MessageType.APPLICATION_COMMAND, - timestamp: new Date(), - application_id: interaction.applicationId, - channel_id: interaction.channelId, - author_id: interaction.applicationId, - nonce: interaction.nonce, - content: body.data.content, - components: body.data.components || [], - tts: body.data.tts, - embeds: body.data.embeds || [], - attachments: body.data.attachments, - poll: body.data.poll, - flags: body.data.flags, - reactions: [], - // webhook_id: interaction.applicationId, // This one requires a webhook to be created first - interaction: { - id: interactionId, - name: interaction.commandName, - type: 2, - user, - }, - interaction_metadata: { - id: interactionId, - type: 2, - user_id: interaction.userId, - user, - authorizing_integration_owners: { - "1": interaction.userId, - }, - name: interaction.commandName, - command_type: interaction.commandType, - }, - }); - - break; - } - case InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: - // TODO - break; - case InteractionCallbackType.DEFERRED_UPDATE_MESSAGE: - //TODO keep track of state of this - interaction.timeout = setTimeout(() => { - emitEvent({ - event: "INTERACTION_FAILURE", - user_id: req.user_id, - data: { + await sendMessage({ + type: MessageType.APPLICATION_COMMAND, + timestamp: new Date(), + application_id: interaction.applicationId, + channel_id: interaction.channelId, + author_id: interaction.applicationId, + nonce: interaction.nonce, + content: body.data.content, + components: body.data.components || [], + tts: body.data.tts, + embeds: body.data.embeds || [], + attachments: body.data.attachments, + poll: body.data.poll, + flags: body.data.flags, + reactions: [], + // webhook_id: interaction.applicationId, // This one requires a webhook to be created first + interaction: { id: interactionId, - nonce: interaction.nonce, - reason_code: InteractionFailureReason.TIMEOUT, + name: interaction.commandName, + type: 2, + user, }, - } as InteractionFailureEvent); - }, 30000); - pendingInteractions.delete(interactionId); - res.sendStatus(204); - return; - case InteractionCallbackType.UPDATE_MESSAGE: - { - if (!interaction.messageId) throw new HTTPError("no. That was not a message"); - const message = await Message.findOneOrFail({ - relations: { - author: true, - webhook: true, - application: true, - mentions: true, - mention_roles: true, - mention_channels: true, - sticker_items: true, - attachments: true, - thread: { - recipients: { - user: true, - }, + interaction_metadata: { + id: interactionId, + type: 2, + user_id: interaction.userId, + user, + authorizing_integration_owners: { + "1": interaction.userId, }, - channel: true, - }, - where: { - id: interaction.messageId, + name: interaction.commandName, + command_type: interaction.commandType, }, }); - if (body.data.content && body.data.content.length > Config.get().limits.message.maxCharacters) { - throw new HTTPError("Content length over max character limit"); - } - if (body.data.components) stripNull(body.data.components); - message.embeds = body.data.embeds || []; - const handle = body.data.components ? handleComps(body.data.components, message.flags) : undefined; - await handle?.(message.id, message.author as User, message.channel); - message.components = body.data.components; - await message.save(); - emitEvent({ - event: "MESSAGE_UPDATE", - channel_id: message.channel_id, - data: message.toJSON(), - } satisfies MessageUpdateEvent); - } - // TODO - break; - case InteractionCallbackType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT: - // TODO - break; - case InteractionCallbackType.MODAL: - // TODO - break; - case InteractionCallbackType.PREMIUM_REQUIRED: - // Deprecated - break; - case InteractionCallbackType.IFRAME_MODAL: - // TODO - break; - case InteractionCallbackType.LAUNCH_ACTIVITY: - // TODO - break; - } - pendingInteractions.delete(interactionId); - res.sendStatus(204); -}); + break; + } + case InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: + // TODO + break; + case InteractionCallbackType.DEFERRED_UPDATE_MESSAGE: + //TODO keep track of state of this + interaction.timeout = setTimeout(() => { + emitEvent({ + event: "INTERACTION_FAILURE", + user_id: req.user_id, + data: { + id: interactionId, + nonce: interaction.nonce, + reason_code: InteractionFailureReason.TIMEOUT, + }, + } as InteractionFailureEvent); + }, 30000); + pendingInteractions.delete(interactionId); + res.sendStatus(204); + return; + case InteractionCallbackType.UPDATE_MESSAGE: + { + if (!interaction.messageId) throw new HTTPError("no. That was not a message"); + const message = await Message.findOneOrFail({ + relations: { + author: true, + webhook: true, + application: true, + mentions: true, + mention_roles: true, + mention_channels: true, + sticker_items: true, + attachments: true, + thread: { + recipients: { + user: true, + }, + }, + channel: true, + }, + where: { + id: interaction.messageId, + }, + }); + if (body.data.content && body.data.content.length > Config.get().limits.message.maxCharacters) { + throw new HTTPError("Content length over max character limit"); + } + if (body.data.components) stripNull(body.data.components); + message.embeds = body.data.embeds || []; + const handle = body.data.components ? handleComps(body.data.components, message.flags) : undefined; + await handle?.(message.id, message.author as User, message.channel); + message.components = body.data.components; + await message.save(); + emitEvent({ + event: "MESSAGE_UPDATE", + channel_id: message.channel_id, + data: message.toJSON(), + } satisfies MessageUpdateEvent); + } + // TODO + break; + /* + case InteractionCallbackType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT: + // TODO + break; + case InteractionCallbackType.MODAL: + // TODO + break; + case InteractionCallbackType.PREMIUM_REQUIRED: + // Deprecated + break; + case InteractionCallbackType.IFRAME_MODAL: + // TODO + break; + case InteractionCallbackType.LAUNCH_ACTIVITY: + // TODO + break; + */ + } + + pendingInteractions.delete(interactionId); + res.sendStatus(204); + }, +); export default router; function stripNull(components: BaseMessageComponents[]) { diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts index 0dd4a4b..c00a959 100644 --- a/src/api/util/handlers/route.ts +++ b/src/api/util/handlers/route.ts @@ -20,6 +20,7 @@ import { AnyValidateFunction } from "ajv/dist/core"; import { NextFunction, Request, Response } from "express"; import { ajv } from "@spacebar/schemas"; +import { BigNumber } from "bignumber.js"; const ignoredRequestSchemas = [ // skip validation for settings proto JSON updates - TODO: figure out if this even possible to fix? @@ -98,6 +99,20 @@ } } } +//It's pretty safe to assume numbers over the number limit aren't really meant to be numbers, so we turn them to strings. +export function bigNumberToString(obj1: unknown) { + if (obj1 && typeof obj1 === "object") { + for (const [key, value] of Object.entries(obj1)) { + if (typeof value === "object") { + if (value instanceof BigNumber) { + //@ts-expect-error this is fine lol + obj1[key] = value.toString(); + } + bigNumberToString(value); + } + } + } +} export function route(opts: RouteOptions) { let validate: AnyValidateFunction | undefined; if (opts.requestBody) { @@ -133,15 +148,16 @@ throw SpacebarApiErrors.MISSING_RIGHTS.withParams(opts.right as string); } } + bigNumberToString(req.body); if (validate && !ignoredRequestSchemas.includes(opts.requestBody!)) { if (opts.stripNulls) { if (opts.stripNulls === true) stripNull(req.body); else followNullPath(req.body, opts.stripNulls); } + const valid = validate(req.body); if (!valid) { - //console.log(JSON.stringify(req.body)); const fields: Record = {}; validate.errors?.forEach( (x) => diff --git a/src/schemas/api/bots/InteractionCallbackSchema.ts b/src/schemas/api/bots/InteractionCallbackSchema.ts index 66a4e66..dd09fb9 100644 --- a/src/schemas/api/bots/InteractionCallbackSchema.ts +++ b/src/schemas/api/bots/InteractionCallbackSchema.ts @@ -18,8 +18,54 @@ import { Message } from "@spacebar/util"; import { InteractionCallbackType } from "./InteractionCallbackType"; +import { AllowedMentions, BaseMessageComponents, Embed, MessageComponentType } from "../messages"; +import { MessageCreateAttachment, MessageCreateCloudAttachment, PollCreationSchema } from "#schemas/uncategorised"; export interface InteractionCallbackSchema { type: InteractionCallbackType; - data: Message; + data: unknown; +} +export interface PongCallback extends InteractionCallbackSchema { + type: InteractionCallbackType.PONG; +} +export interface AckCallback extends InteractionCallbackSchema { + type: InteractionCallbackType.ACKNOWLEDGE; +} +export interface MessageCallback extends InteractionCallbackSchema { + type: InteractionCallbackType.CHANNEL_MESSAGE; +} +export interface MessageWSourceCallback extends InteractionCallbackSchema { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE; + data: InteractionMessage; +} +export interface MessageDWSourceCallback extends InteractionCallbackSchema { + type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE; + data: InteractionMessage; +} +export interface MessageUpdateCallback extends InteractionCallbackSchema { + type: InteractionCallbackType.UPDATE_MESSAGE; + data: InteractionMessage; +} +export interface MessageDUpdateCallback extends InteractionCallbackSchema { + type: InteractionCallbackType.DEFERRED_UPDATE_MESSAGE; + data: InteractionMessage; +} +export type InteractionCallbacksSchema = + | PongCallback + | AckCallback + | MessageCallback + | MessageWSourceCallback + | MessageDWSourceCallback + | MessageUpdateCallback + | MessageDUpdateCallback; + +export interface InteractionMessage { + content?: string; + tts?: boolean; + embeds?: Embed[]; + allowed_mentions?: AllowedMentions; + components?: BaseMessageComponents[]; + flags?: number; + attachments?: (MessageCreateAttachment | MessageCreateCloudAttachment)[]; + poll?: PollCreationSchema; }