diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index dcf024385..d832dc36f 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -285,6 +285,9 @@ router.post( }, route({ requestBody: "MessageCreateSchema", + stripNulls: { + components: true, + }, permission: "VIEW_CHANNEL", right: "SEND_MESSAGES", responses: { diff --git a/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts b/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts index 984475c6d..afa8a71a0 100644 --- a/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts +++ b/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { ButtonStyle, InteractionCallbackSchema, InteractionCallbackType, MessageComponentType, MessageType } from "@spacebar/schemas"; -import { route, sendMessage } from "@spacebar/api"; +import { BaseMessageComponents, InteractionCallbackSchema, InteractionCallbackType, MessageType } from "@spacebar/schemas"; +import { handleComps, route, sendMessage } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { emitEvent, FieldErrors, InteractionSuccessEvent, pendingInteractions, User } from "@spacebar/util"; +import { Config, emitEvent, InteractionSuccessEvent, Message, MessageUpdateEvent, pendingInteractions, User } from "@spacebar/util"; +import { HTTPError } from "#util/util/lambert-server"; const router = Router({ mergeParams: true }); @@ -110,9 +111,47 @@ router.post("/", route({}), async (req: Request, res: Response) => { // TODO break; case InteractionCallbackType.DEFERRED_UPDATE_MESSAGE: - // TODO - break; case InteractionCallbackType.UPDATE_MESSAGE: + { + if (!interaction.mesageId) 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.mesageId, + }, + }); + 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); + console.log(body.data.components); + message.embeds = body.data.embeds || []; + console.log(message); + 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: @@ -137,3 +176,7 @@ router.post("/", route({}), async (req: Request, res: Response) => { }); export default router; +function stripNull(components: BaseMessageComponents[]) { + throw new Error("Function not implemented."); +} + diff --git a/src/api/routes/interactions/index.ts b/src/api/routes/interactions/index.ts index 5542b9c19..65b8a02ef 100644 --- a/src/api/routes/interactions/index.ts +++ b/src/api/routes/interactions/index.ts @@ -39,7 +39,7 @@ router.post("/", route({}), async (req: Request, res: Response) => { id: interactionId, nonce: body.nonce, }, - } as InteractionCreateEvent); + } satisfies InteractionCreateEvent); const user = req.user; @@ -69,7 +69,7 @@ router.post("/", route({}), async (req: Request, res: Response) => { interactionData.app_permissions = (await getPermission(body.application_id, body.guild_id, body.channel_id)).bitfield.toString(); const guild = await Guild.findOneOrFail({ where: { id: body.guild_id } }); - const member = await Member.findOneOrFail({ where: { guild_id: body.guild_id, id: req.user_id }, relations: { user: true } }); + const member = await Member.findOneOrFail({ where: { guild_id: body.guild_id, id: req.user_id }, relations: { user: true, roles: true } }); interactionData.guild = { id: guild.id, @@ -91,7 +91,24 @@ router.post("/", route({}), async (req: Request, res: Response) => { } if (body.type === InteractionType.MessageComponent || body.data.type === InteractionType.ModalSubmit) { - interactionData.message = await Message.findOneOrFail({ where: { id: body.message_id, flags: undefined }, relations: { author: true } }); + interactionData.message = await Message.findOneOrFail({ + where: { id: body.message_id, flags: undefined }, + relations: { + author: true, + webhook: true, + application: true, + mentions: true, + mention_roles: true, + mention_channels: true, + sticker_items: true, + attachments: true, + thread: { + recipients: { + user: true, + }, + }, + }, + }); } await emitEvent({ @@ -122,6 +139,7 @@ router.post("/", route({}), async (req: Request, res: Response) => { type: body.type, commandType: body.data.type, commandName: body.data.name, + mesageId: body.message_id, }); res.sendStatus(204); diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 2c9274031..be40f7543 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -67,6 +67,7 @@ import { Reaction, ReadStateType, UnfurledMediaItem, + BaseMessageComponents, } from "@spacebar/schemas"; const allow_empty = false; // TODO: check webhook, application, system author, stickers @@ -201,20 +202,19 @@ async function processMedia(media: UnfurledMediaItem, messageId: string, batchId }); } } - -export async function handleMessage(opts: MessageOptions): Promise { +export function handleComps(components: BaseMessageComponents[], flags: number) { const errors: Record = {}; const knownComponentIds: string[] = []; - const compv2 = (opts.flags || 0) & Number(MessageFlags.FLAGS.IS_COMPONENTS_V2); + const compv2 = (flags || 0) & Number(MessageFlags.FLAGS.IS_COMPONENTS_V2); const medias: UnfurledMediaItem[] = []; - for (const comp of opts.components || []) { + for (const comp of components || []) { if (comp.type === MessageComponentType.ActionRow) { - checkActionRow(comp, knownComponentIds, errors, opts.components!.indexOf(comp)); + checkActionRow(comp, knownComponentIds, errors, components!.indexOf(comp)); } else if (comp.type === MessageComponentType.Section) { if (!compv2) throw new HTTPError("Must be comp v2"); const accessory = comp.accessory; if (comp.components.length < 1 || comp.components.length > 3) { - errors[`data.components[${opts.components!.indexOf(comp)}].components`] = { + errors[`data.components[${components!.indexOf(comp)}].components`] = { code: "TOO_LONG", message: "Component list is too long", }; @@ -227,7 +227,7 @@ export async function handleMessage(opts: MessageOptions): Promise { } else if (comp.type === MessageComponentType.MediaGallery) { if (!compv2) throw new HTTPError("Must be comp v2"); if (comp.items.length < 1 || comp.items.length > 10) { - errors[`data.components[${opts.components!.indexOf(comp)}].items`] = { + errors[`data.components[${components!.indexOf(comp)}].items`] = { code: "TOO_LONG", message: "Media list is too long", }; @@ -248,7 +248,7 @@ export async function handleMessage(opts: MessageOptions): Promise { case MessageComponentType.Section: { const accessory = elm.accessory; if (elm.components.length < 1 || elm.components.length > 3) { - errors[`data.components[${opts.components!.indexOf(comp)}].components[${comp.components!.indexOf(elm)}].components`] = { + errors[`data.components[${components!.indexOf(comp)}].components[${comp.components!.indexOf(elm)}].components`] = { code: "TOO_LONG", message: "Component list is too long", }; @@ -260,7 +260,7 @@ export async function handleMessage(opts: MessageOptions): Promise { } case MessageComponentType.MediaGallery: if (elm.items.length < 1 || elm.items.length > 10) { - errors[`data.components[${opts.components!.indexOf(comp)}].components[${comp.components!.indexOf(elm)}].items`] = { + errors[`data.components[${components!.indexOf(comp)}].components[${comp.components!.indexOf(elm)}].items`] = { code: "TOO_LONG", message: "Media list is too long", }; @@ -272,7 +272,7 @@ export async function handleMessage(opts: MessageOptions): Promise { break; } case MessageComponentType.ActionRow: - checkActionRow(elm, knownComponentIds, errors, opts.components!.indexOf(elm)); + checkActionRow(elm, knownComponentIds, errors, components!.indexOf(elm)); break; default: elm satisfies never; @@ -286,6 +286,13 @@ export async function handleMessage(opts: MessageOptions): Promise { if (Object.keys(errors).length > 0) { throw FieldErrors(errors); } + return async (messageId: string, user: User, channel: Channel) => { + const batchId = `CLOUD_compUploads_${randomString(128)}`; + (await Promise.all(medias.map((m, index) => processMedia(m, messageId, batchId, user, channel, index + "")))).forEach((_) => _?.()); + }; +} +export async function handleMessage(opts: MessageOptions): Promise { + const handle = opts.components ? handleComps(opts.components, opts.flags || 0) : undefined; const channel = await Channel.findOneOrFail({ where: { id: opts.channel_id }, @@ -335,7 +342,6 @@ export async function handleMessage(opts: MessageOptions): Promise { components: opts.components ?? undefined, // Fix Discord-Go? }); message.channel = channel; - const batchId = `CLOUD_${message.author_id}_${randomString(128)}`; if (opts.author_id) { message.author = await User.findOneOrFail({ @@ -345,8 +351,6 @@ export async function handleMessage(opts: MessageOptions): Promise { rights.hasThrow("SEND_MESSAGES"); } - (await Promise.all(medias.map((m, index) => processMedia(m, message.id, batchId, message.author as User, channel, index + "")))).forEach((_) => _?.()); - const ephermal = (message.flags & (1 << 6)) !== 0; if (!ephermal && channel.type === ChannelType.GUILD_PUBLIC_THREAD) { const rep = Channel.getRepository(); @@ -360,6 +364,7 @@ export async function handleMessage(opts: MessageOptions): Promise { if (cloudAttachments && cloudAttachments.length > 0) { console.log("[Message] Processing attachments for message", message.id, ":", message.attachments); + handle?.(message.id, message.author as User, message.channel); const uploadedAttachments = await Promise.all( cloudAttachments.map(async (att) => { const cAtt = att.attachment; diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts index 455ce21b3..175015246 100644 --- a/src/api/util/handlers/route.ts +++ b/src/api/util/handlers/route.ts @@ -41,7 +41,7 @@ export type RouteResponse = { body?: `${string}Response`; headers?: Record; }; - +export type stripNulls = { [key: string]: true | stripNulls }; export interface RouteOptions { permission?: PermissionResolvable; right?: RightResolvable; @@ -52,6 +52,7 @@ export interface RouteOptions { body?: string; }; }; + stripNulls?: stripNulls; event?: EVENT | EVENT[]; summary?: string; description?: string; @@ -73,7 +74,31 @@ export interface RouteOptions { // headers?: Record; // }; } - +export function stripNull(obj: object) { + console.log(Object.entries(obj)); + for (const [key, value] of Object.entries(obj)) { + if (value instanceof Object || (value && !value.__proto__)) { + stripNull(value); + } else if (value === null) { + //@ts-expect-error this is fine + delete obj[key]; + } + } +} +// eslint-disable-next-line +export function followNullPath(obj1: any, nullObj: stripNulls) { + for (const [key, value] of Object.entries(nullObj)) { + if (key in obj1) + if (value instanceof Object) { + if (obj1[key] instanceof Object) + //@ts-expect-error this works lol + followNullPath(obj1[key], nullObj[key]); + else delete obj1[key]; + } else if (obj1[key] instanceof Object) { + stripNull(obj1[key]); + } + } +} export function route(opts: RouteOptions) { let validate: AnyValidateFunction | undefined; if (opts.requestBody) { @@ -111,9 +136,12 @@ export function route(opts: RouteOptions) { } if (validate && !ignoredRequestSchemas.includes(opts.requestBody!)) { + if (opts.stripNulls) { + followNullPath(req.body, opts.stripNulls); + } const valid = validate(req.body); if (!valid) { - console.log(req.body); + console.log(JSON.stringify(req.body)); const fields: Record = {}; validate.errors?.forEach( (x) => diff --git a/src/util/imports/Interactions.ts b/src/util/imports/Interactions.ts index fbb74e5d5..9ebe1cae5 100644 --- a/src/util/imports/Interactions.ts +++ b/src/util/imports/Interactions.ts @@ -26,6 +26,7 @@ interface PendingInteraction { channelId?: string; guildId?: string; nonce?: string; + mesageId?: string; type: InteractionType; commandType: ApplicationCommandType; commandName: string;