mirror of
https://github.com/spacebarchat/server.git
synced 2026-03-30 13:55:39 +00:00
v2 init work
This commit is contained in:
@@ -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 }),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user