v2 init work

This commit is contained in:
MathMan05
2026-02-13 16:08:16 -06:00
committed by Rory&
parent a4ff9e9d41
commit 33c93f8b88
2 changed files with 185 additions and 32 deletions

View File

@@ -53,6 +53,7 @@ import {
import { HTTPError } from "lambert-server";
import { In, Or, Equal, IsNull } from "typeorm";
import {
ActionRowComponent,
ButtonStyle,
ChannelType,
Embed,
@@ -64,53 +65,140 @@ import {
MessageType,
Reaction,
ReadStateType,
UnfurledMediaItem,
} from "@spacebar/schemas";
const allow_empty = false;
// TODO: check webhook, application, system author, stickers
// TODO: embed gifs/videos/images
const LINK_REGEX = /<?https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)>?/g;
function checkActionRow(row: ActionRowComponent, knownComponentIds: string[], errors: Record<string, { code?: string; message: string }>, rowIndex: number) {
if (!row.components) {
return;
}
if (row.components.length < 1 || row.components.length > 5) {
errors[`data.components[${rowIndex}].components`] = {
code: "BASE_TYPE_BAD_LENGTH",
message: `Must be between 1 and 5 in length.`,
};
}
for (const component of row.components) {
if (component.type == MessageComponentType.Button && component.style != ButtonStyle.Link) {
if (component.custom_id?.trim() === "") {
errors[`data.components[${rowIndex}].components[${row.components.indexOf(component)}].custom_id`] = {
code: "BUTTON_COMPONENT_CUSTOM_ID_REQUIRED",
message: "A custom id required",
};
}
if (knownComponentIds.includes(component.custom_id!)) {
errors[`data.components[${rowIndex}].components[${row.components.indexOf(component)}].custom_id`] = {
code: "COMPONENT_CUSTOM_ID_DUPLICATED",
message: "Component custom id cannot be duplicated",
};
} else {
knownComponentIds.push(component.custom_id!);
}
}
}
}
async function processMedia(media: UnfurledMediaItem) {
if (Object.keys(media).length > 1) throw new HTTPError("no, you can't send those");
if (!URL.canParse(media.url)) throw new HTTPError("media URL must be a URI");
const url = new URL(media.url);
if (!["http", "https", "attachment"].includes(url.protocol)) throw new HTTPError("invalid media protocol");
}
export async function handleMessage(opts: MessageOptions): Promise<Message> {
const errors: Record<string, { code?: string; message: string }> = {};
const knownComponentIds: string[] = [];
for (const row of opts.components || []) {
if (!row.components) {
continue;
}
if (row.components.length < 1 || row.components.length > 5) {
errors[`data.components[${opts.components!.indexOf(row)}].components`] = {
code: "BASE_TYPE_BAD_LENGTH",
message: `Must be between 1 and 5 in length.`,
};
}
for (const component of row.components) {
if (component.type == MessageComponentType.Button && component.style != ButtonStyle.Link) {
if (component.custom_id?.trim() === "") {
errors[`data.components[${opts.components!.indexOf(row)}].components[${row.components.indexOf(component)}].custom_id`] = {
code: "BUTTON_COMPONENT_CUSTOM_ID_REQUIRED",
message: "A custom id required",
};
}
if (knownComponentIds.includes(component.custom_id!)) {
errors[`data.components[${opts.components!.indexOf(row)}].components[${row.components.indexOf(component)}].custom_id`] = {
code: "COMPONENT_CUSTOM_ID_DUPLICATED",
message: "Component custom id cannot be duplicated",
};
} else {
knownComponentIds.push(component.custom_id!);
const compv2 = (opts.flags || 0) & Number(MessageFlags.FLAGS.IS_COMPONENTS_V2);
const medias: UnfurledMediaItem[] = [];
for (const comp of opts.components || []) {
if (comp.type === MessageComponentType.ActionRow) {
checkActionRow(comp, knownComponentIds, errors, opts.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`] = {
code: "TOO_LONG",
message: "Component list is too long",
};
}
if (accessory.type === MessageComponentType.Thumbnail) {
medias.push(accessory.media);
}
} else if (comp.type === MessageComponentType.TextDisplay) {
if (!compv2) throw new HTTPError("Must be comp v2");
} 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`] = {
code: "TOO_LONG",
message: "Media list is too long",
};
}
medias.push(...comp.items.map(({ media }) => media));
} else if (comp.type === MessageComponentType.File) {
if (!compv2) throw new HTTPError("Must be comp v2");
medias.push(comp.file);
} else if (comp.type === MessageComponentType.Separator) {
if (!compv2) throw new HTTPError("Must be comp v2");
} else if (comp.type === MessageComponentType.Container) {
if (!compv2) throw new HTTPError("Must be comp v2");
for (const elm of comp.components) {
switch (elm.type) {
case MessageComponentType.Separator:
case MessageComponentType.TextDisplay:
break;
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`] = {
code: "TOO_LONG",
message: "Component list is too long",
};
}
if (accessory.type === MessageComponentType.Thumbnail) {
medias.push(accessory.media);
}
break;
}
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`] = {
code: "TOO_LONG",
message: "Media list is too long",
};
}
medias.push(...elm.items.map(({ media }) => media));
break;
case MessageComponentType.File: {
medias.push(elm.file);
break;
}
case MessageComponentType.ActionRow:
checkActionRow(elm, knownComponentIds, errors, opts.components!.indexOf(elm));
break;
default:
elm satisfies never;
}
}
} else {
comp satisfies never;
}
}
if (Object.keys(errors).length > 0) {
throw FieldErrors(errors);
}
await Promise.all(medias.map(processMedia));
const channel = await Channel.findOneOrFail({
where: { id: opts.channel_id },
relations: { recipients: true },
@@ -722,8 +810,8 @@ export async function sendMessage(opts: MessageOptions) {
const ephemeral = (message.flags & Number(MessageFlags.FLAGS.EPHEMERAL)) !== 0;
await Promise.all([
Message.insert(message),
Channel.update(message.channel.id, message.channel),
message.insert(),
message.channel.save(),
emitEvent({
event: "MESSAGE_CREATE",
...(ephemeral ? { user_id: message.interaction_metadata?.user_id } : { channel_id: message.channel_id }),

View File

@@ -20,13 +20,78 @@ import { PartialEmoji } from "@spacebar/schemas";
export interface MessageComponent {
type: MessageComponentType;
id?: number;
}
export interface SectionComponent extends MessageComponent {
type: MessageComponentType.Section;
components: TextDispalyComponent[];
accessory: ThumbnailComponent | ButtonComponent;
}
export interface ThumbnailComponent extends MessageComponent {
type: MessageComponentType.Thumbnail;
description?: string;
media: UnfurledMediaItem;
spoiler?: boolean;
}
export interface UnfurledMediaItem {
id?: string;
url: string;
proxy_url?: string;
height?: number;
width?: number;
flags?: number;
content_type?: string;
content_scan_metadata?: unknown; //TODO deal with this lol
placeholder_version?: number;
placeholder?: string;
loading_state?: number;
attachment_id?: string;
}
export interface TextDispalyComponent extends MessageComponent {
type: MessageComponentType.TextDisplay;
content: string;
}
export interface MediaGalleryComponent extends MessageComponent {
type: MessageComponentType.MediaGallery;
items: {
media: UnfurledMediaItem;
description?: string;
spoiler?: boolean;
}[];
}
export interface FileComponent extends MessageComponent {
type: MessageComponentType.File;
file: UnfurledMediaItem;
spoiler: boolean;
name: string;
size: number;
}
export const enum SeperatorSpacing {
Small = 1,
Large = 2,
}
export interface SeperatorComponent extends MessageComponent {
type: MessageComponentType.Separator;
divider?: boolean;
spacing?: SeperatorSpacing;
}
export interface ActionRowComponent extends MessageComponent {
type: MessageComponentType.ActionRow;
components: (ButtonComponent | StringSelectMenuComponent | SelectMenuComponent | TextInputComponent)[];
}
export type BaseMessageComponents = ActionRowComponent;
export interface ContainerComponent extends MessageComponent {
type: MessageComponentType.Container;
components: (ActionRowComponent | TextDispalyComponent | SectionComponent | MediaGalleryComponent | SeperatorComponent | FileComponent)[];
accent_color?: number;
spoiler?: boolean;
}
export type BaseMessageComponents = ActionRowComponent | SectionComponent | TextDispalyComponent | MediaGalleryComponent | FileComponent | SeperatorComponent | ContainerComponent;
export interface ButtonComponent extends MessageComponent {
type: MessageComponentType.Button;