chore: 3.1.0 (#339)

* perf: optimize members cache

* feat: components V2 (#337)

* feat: components v2

* fix: build

* chore: apply formatting

* refactor(components): some types

* refactor(types): replace TopLevelComponents with APITopLevelComponent in REST

* fix: unify components

* refactor(TextDisplay): rename content method to setContent for clarity

* refactor(builders): add missing builder from component

* fix: touche

* feat(webhook): webhook params for components v2

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: use protected instead of private

* fix(editOrReply): accept flags when editing message

* feat: add onBeforeMiddlewares and onBeforeOptions (#338)

* chore: package version

---------

Co-authored-by: MARCROCK22 <marcos22dev@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: MARCROCK22 <57925328+MARCROCK22@users.noreply.github.com>
This commit is contained in:
Marcos Susaña 2025-04-27 01:21:56 -04:00 committed by GitHub
parent ce3d75121d
commit e3b6f57741
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 1547 additions and 578 deletions

View File

@ -1,6 +1,6 @@
{
"name": "seyfert",
"version": "3.0.0",
"version": "3.1.0",
"description": "The most advanced framework for discord bots",
"main": "./lib/index.js",
"module": "./lib/index.js",

View File

@ -9,6 +9,7 @@ import type {
RESTPatchAPIWebhookResult,
RESTPatchAPIWebhookWithTokenJSONBody,
RESTPatchAPIWebhookWithTokenMessageJSONBody,
RESTPatchAPIWebhookWithTokenMessageQuery,
RESTPatchAPIWebhookWithTokenMessageResult,
RESTPatchAPIWebhookWithTokenResult,
RESTPostAPIWebhookWithTokenGitHubQuery,
@ -33,7 +34,9 @@ export interface WebhookRoutes {
token: string,
): {
get(args?: RestArgumentsNoBody): Promise<RESTGetAPIWebhookWithTokenResult>;
patch(args: RestArguments<RESTPatchAPIWebhookWithTokenJSONBody>): Promise<RESTPatchAPIWebhookWithTokenResult>;
patch(
args: RestArguments<RESTPatchAPIWebhookWithTokenJSONBody, RESTPatchAPIWebhookWithTokenMessageQuery>,
): Promise<RESTPatchAPIWebhookWithTokenResult>;
delete(args?: RestArgumentsNoBody): Promise<RESTDeleteAPIWebhookWithTokenResult>;
post(
args: RestArguments<RESTPostAPIWebhookWithTokenJSONBody, RESTPostAPIWebhookWithTokenQuery>,

View File

@ -98,13 +98,13 @@ export class ApiHandler {
}
}
#randomUUID(): UUID {
randomUUID(): UUID {
const uuid = randomUUID();
if (this.workerPromises!.has(uuid)) return this.#randomUUID();
if (this.workerPromises!.has(uuid)) return this.randomUUID();
return uuid;
}
private sendMessage(_body: WorkerSendApiRequest) {
protected sendMessage(_body: WorkerSendApiRequest) {
throw new Error('Function not implemented');
}
@ -121,7 +121,7 @@ export class ApiHandler {
{ auth = true, ...request }: ApiRequestOptions = {},
): Promise<T> {
if (this.options.workerProxy) {
const nonce = this.#randomUUID();
const nonce = this.randomUUID();
return this.postMessage<T>({
method,
url,

View File

@ -7,13 +7,13 @@ import {
} from '../types';
import { BaseComponentBuilder } from './Base';
import { fromComponent } from './index';
import type { BuilderComponents, FixedComponents } from './types';
import type { ActionBuilderComponents, FixedComponents } from './types';
/**
* Represents an Action Row component in a message.
* @template T - The type of components in the Action Row.
*/
export class ActionRow<T extends BuilderComponents> extends BaseComponentBuilder<
export class ActionRow<T extends ActionBuilderComponents = ActionBuilderComponents> extends BaseComponentBuilder<
APIActionRowComponent<APIActionRowComponentTypes>
> {
/**
@ -50,8 +50,8 @@ export class ActionRow<T extends BuilderComponents> extends BaseComponentBuilder
* @example
* actionRow.setComponents([buttonComponent1, buttonComponent2]);
*/
setComponents(component: FixedComponents<T>[]): this {
this.components = [...component];
setComponents(...component: RestOrArray<FixedComponents<T>>): this {
this.components = component.flat() as FixedComponents<T>[];
return this;
}

View File

@ -1,17 +1,14 @@
import { type EmojiResolvable, resolvePartialEmoji } from '../common';
import { type APIButtonComponent, type APIMessageComponentEmoji, type ButtonStyle, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a button component.
* @template Type - The type of the button component.
*/
export class Button {
/**
* Creates a new Button instance.
* @param data - The initial data for the button.
*/
constructor(public data: Partial<APIButtonComponent> = {}) {
this.data.type = ComponentType.Button;
export class Button extends BaseComponentBuilder<APIButtonComponent> {
constructor(data: Partial<APIButtonComponent> = {}) {
super({ type: ComponentType.Button, ...data });
}
/**
@ -76,12 +73,4 @@ export class Button {
(this.data as Extract<APIButtonComponent, { sku_id?: string }>).sku_id = skuId;
return this;
}
/**
* Converts the Button instance to its JSON representation.
* @returns The JSON representation of the Button instance.
*/
toJSON() {
return { ...this.data } as Partial<APIButtonComponent>;
}
}

100
src/builders/Container.ts Normal file
View File

@ -0,0 +1,100 @@
import { type ActionRow, fromComponent } from '.';
import { type ColorResolvable, type RestOrArray, resolveColor } from '../common';
import { type APIContainerComponent, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
import type { File } from './File';
import type { MediaGallery } from './MediaGallery';
import type { Section } from './Section';
import type { Separator } from './Separator';
import type { TextDisplay } from './TextDisplay';
/**
* Represents the possible component types that can be added to a Container.
*/
export type ContainerBuilderComponents = ActionRow | TextDisplay | Section | MediaGallery | Separator | File;
/**
* Represents a container component builder.
* Containers group other components together.
* @example
* ```ts
* const container = new Container()
* .addComponents(
* new TextDisplay('This is text inside a container!'),
* new ActionRow().addComponents(new Button().setLabel('Click me!'))
* )
* .setColor('Blue');
* ```
*/
export class Container extends BaseComponentBuilder<APIContainerComponent> {
/**
* The components held within this container.
*/
components: ContainerBuilderComponents[];
/**
* Constructs a new Container.
* @param data Optional initial data for the container.
*/
constructor({ components, ...data }: Partial<APIContainerComponent> = {}) {
super({ ...data, type: ComponentType.Container });
this.components = (components?.map(fromComponent) ?? []) as ContainerBuilderComponents[];
}
/**
* Adds components to the container.
* @param components The components to add. Can be a single component, an array of components, or multiple components as arguments.
* @returns The updated Container instance.
*/
addComponents(...components: RestOrArray<ContainerBuilderComponents>) {
this.components = this.components.concat(components.flat());
return this;
}
/**
* Sets the components for the container, replacing any existing components.
* @param components The components to set. Can be a single component, an array of components, or multiple components as arguments.
* @returns The updated Container instance.
*/
setComponents(...components: RestOrArray<ContainerBuilderComponents>) {
this.components = components.flat();
return this;
}
/**
* Sets whether the container's content should be visually marked as a spoiler.
* @param spoiler Whether the content is a spoiler (defaults to true).
* @returns The updated Container instance.
*/
setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Sets the accent color for the container.
* @param color The color resolvable (e.g., hex code, color name, integer).
* @returns The updated Container instance.
*/
setColor(color: ColorResolvable) {
this.data.accent_color = resolveColor(color);
return this;
}
/**
* Sets the ID for the container.
* @param id The ID to set.
* @returns The updated Container instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
toJSON() {
return {
...this.data,
components: this.components.map(c => c.toJSON()),
} as APIContainerComponent;
}
}

52
src/builders/File.ts Normal file
View File

@ -0,0 +1,52 @@
import { type APIFileComponent, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a file component builder.
* Used to display files within containers.
* @example
* ```ts
* const file = new File()
* .setMedia('https://example.com/image.png')
* .setSpoiler();
* ```
*/
export class File extends BaseComponentBuilder<APIFileComponent> {
/**
* Constructs a new File component.
* @param data Optional initial data for the file component.
*/
constructor(data: Partial<APIFileComponent> = {}) {
super({ type: ComponentType.File, ...data });
}
/**
* Sets the ID for the file component.
* @param id The ID to set.
* @returns The updated File instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Sets the media URL for the file.
* @param url The URL of the file to display.
* @returns The updated File instance.
*/
setMedia(url: string) {
this.data.file = { url };
return this;
}
/**
* Sets whether the file should be visually marked as a spoiler.
* @param spoiler Whether the file is a spoiler (defaults to true).
* @returns The updated File instance.
*/
setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
}

View File

@ -0,0 +1,112 @@
import type { RestOrArray } from '../common';
import { type APIMediaGalleryComponent, type APIMediaGalleryItems, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a media gallery component builder.
* Used to display a collection of media items.
* @example
* ```ts
* const gallery = new MediaGallery()
* .addItems(
* new MediaGalleryItem().setMedia('https://example.com/image1.png').setDescription('Image 1'),
* new MediaGalleryItem().setMedia('https://example.com/image2.jpg').setSpoiler()
* );
* ```
*/
export class MediaGallery extends BaseComponentBuilder<APIMediaGalleryComponent> {
items: MediaGalleryItem[];
/**
* Constructs a new MediaGallery.
* @param data Optional initial data for the media gallery.
*/
constructor({ items, ...data }: Partial<APIMediaGalleryComponent> = {}) {
super({ type: ComponentType.MediaGallery, ...data });
this.items = (items?.map(i => new MediaGalleryItem(i)) ?? []) as MediaGalleryItem[];
}
/**
* Sets the ID for the media gallery component.
* @param id The ID to set.
* @returns The updated MediaGallery instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Adds items to the media gallery.
* @param items The items to add. Can be a single item, an array of items, or multiple items as arguments.
* @returns The updated MediaGallery instance.
*/
addItems(...items: RestOrArray<MediaGalleryItem>) {
this.items = this.items.concat(items.flat());
return this;
}
/**
* Sets the items for the media gallery, replacing any existing items.
* @param items The items to set. Can be a single item, an array of items, or multiple items as arguments.
* @returns The updated MediaGallery instance.
*/
setItems(...items: RestOrArray<MediaGalleryItem>) {
this.items = items.flat();
return this;
}
toJSON() {
return {
...this.data,
items: this.items.map(i => i.toJSON()),
} as APIMediaGalleryComponent;
}
}
/**
* Represents an item within a MediaGallery.
*/
export class MediaGalleryItem {
/**
* Constructs a new MediaGalleryItem.
* @param data Optional initial data for the media gallery item.
*/
constructor(public data: Partial<APIMediaGalleryItems> = {}) {}
/**
* Sets the media URL for this gallery item.
* @param url The URL of the media.
* @returns The updated MediaGalleryItem instance.
*/
setMedia(url: string) {
this.data.media = { url };
return this;
}
/**
* Sets the description for this gallery item.
* @param desc The description text.
* @returns The updated MediaGalleryItem instance.
*/
setDescription(desc: string) {
this.data.description = desc;
return this;
}
/**
* Sets whether this gallery item should be visually marked as a spoiler.
* @param spoiler Whether the item is a spoiler (defaults to true).
* @returns The updated MediaGalleryItem instance.
*/
setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Converts this MediaGalleryItem instance to its JSON representation.
* @returns The JSON representation of the item data.
*/
toJSON() {
return { ...this.data };
}
}

55
src/builders/Section.ts Normal file
View File

@ -0,0 +1,55 @@
import { type Button, fromComponent } from '.';
import type { RestOrArray } from '../common';
import { type APISectionComponent, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
import type { TextDisplay } from './TextDisplay';
import type { Thumbnail } from './Thumbnail';
export class Section<
Ac extends Button | Thumbnail = Button | Thumbnail,
> extends BaseComponentBuilder<APISectionComponent> {
components: TextDisplay[];
accessory!: Ac;
constructor({ components, accessory, ...data }: Partial<APISectionComponent> = {}) {
super({ type: ComponentType.Section, ...data });
this.components = (components?.map(component => fromComponent(component)) ?? []) as TextDisplay[];
if (accessory) this.accessory = fromComponent(accessory) as Ac;
}
/**
* Adds components to this section.
* @param components The components to add
* @example section.addComponents(new TextDisplay().content('Hello'));
*/
addComponents(...components: RestOrArray<TextDisplay>) {
this.components = this.components.concat(components.flat());
return this;
}
/**
* Sets the components for this section.
* @param components The components to set
* @example section.setComponents(new TextDisplay().content('Hello'));
*/
setComponents(...components: RestOrArray<TextDisplay>) {
this.components = components.flat();
return this;
}
setAccessory(accessory: Ac) {
this.accessory = accessory;
return this;
}
/**
* Converts this section to JSON.
* @returns The JSON representation of this section
*/
toJSON() {
return {
...this.data,
components: this.components.map(component => component.toJSON()),
accessory: this.accessory.toJSON(),
} as APISectionComponent;
}
}

54
src/builders/Separator.ts Normal file
View File

@ -0,0 +1,54 @@
import { type APISeparatorComponent, ComponentType, type Spacing } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a separator component builder.
* Used to add visual spacing or dividers between components.
* @example
* ```ts
* // A simple separator for spacing
* const spacingSeparator = new Separator().setSpacing(Spacing.Small);
*
* // A separator acting as a visual divider
* const dividerSeparator = new Separator().setDivider(true);
* ```
*/
export class Separator extends BaseComponentBuilder<APISeparatorComponent> {
/**
* Constructs a new Separator component.
* @param data Optional initial data for the separator component.
*/
constructor(data: Partial<APISeparatorComponent> = {}) {
super({ type: ComponentType.Separator, ...data });
}
/**
* Sets the ID for the separator component.
* @param id The ID to set.
* @returns The updated Separator instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Sets whether this separator should act as a visual divider.
* @param divider Whether to render as a divider (defaults to false).
* @returns The updated Separator instance.
*/
setDivider(divider = false) {
this.data.divider = divider;
return this;
}
/**
* Sets the amount of spacing this separator provides.
* @param spacing The desired spacing level ('None', 'Small', 'Medium', 'Large').
* @returns The updated Separator instance.
*/
setSpacing(spacing: Spacing) {
this.data.spacing = spacing;
return this;
}
}

View File

@ -0,0 +1,40 @@
import { type APITextDispalyComponent, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a text display component builder.
* Used to display simple text content.
* @example
* ```ts
* const text = new TextDisplay().content('Hello, world!');
* ```
*/
export class TextDisplay extends BaseComponentBuilder<APITextDispalyComponent> {
/**
* Constructs a new TextDisplay component.
* @param data Optional initial data for the text display component.
*/
constructor(data: Partial<APITextDispalyComponent> = {}) {
super({ type: ComponentType.TextDisplay, ...data });
}
/**
* Sets the ID for the text display component.
* @param id The ID to set.
* @returns The updated TextDisplay instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Sets the text content to display.
* @param content The text content.
* @returns The updated TextDisplay instance.
*/
setContent(content: string) {
this.data.content = content;
return this;
}
}

62
src/builders/Thumbnail.ts Normal file
View File

@ -0,0 +1,62 @@
import { type APIThumbnailComponent, ComponentType } from '../types';
import { BaseComponentBuilder } from './Base';
/**
* Represents a thumbnail component builder.
* Used to display a small image preview, often alongside other content.
* @example
* ```ts
* const thumbnail = new Thumbnail()
* .setMedia('https://example.com/thumbnail.jpg')
* .setDescription('A cool thumbnail');
* ```
*/
export class Thumbnail extends BaseComponentBuilder<APIThumbnailComponent> {
/**
* Constructs a new Thumbnail component.
* @param data Optional initial data for the thumbnail component.
*/
constructor(data: Partial<APIThumbnailComponent> = {}) {
super({ type: ComponentType.Thumbnail, ...data });
}
/**
* Sets whether the thumbnail should be visually marked as a spoiler.
* @param spoiler Whether the thumbnail is a spoiler (defaults to true).
* @returns The updated Thumbnail instance.
*/
setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Sets the description for the thumbnail.
* @param description The description text. Can be undefined to remove the description.
* @returns The updated Thumbnail instance.
*/
setDescription(description: string | undefined) {
this.data.description = description;
return this;
}
/**
* Sets the ID for the thumbnail component.
* @param id The ID to set.
* @returns The updated Thumbnail instance.
*/
setId(id: number) {
this.data.id = id;
return this;
}
/**
* Sets the media URL for the thumbnail.
* @param url The URL of the image to display as a thumbnail.
* @returns The updated Thumbnail instance.
*/
setMedia(url: string) {
this.data.media = { url };
return this;
}
}

View File

@ -1,7 +1,11 @@
import { type APIActionRowComponent, type APIActionRowComponentTypes, ComponentType } from '../types';
import { type APIComponents, ComponentType } from '../types';
import { ActionRow } from './ActionRow';
import { Button } from './Button';
import { Container } from './Container';
import { File } from './File';
import { MediaGallery } from './MediaGallery';
import { TextInput } from './Modal';
import { Section } from './Section';
import {
ChannelSelectMenu,
MentionableSelectMenu,
@ -9,29 +13,32 @@ import {
StringSelectMenu,
UserSelectMenu,
} from './SelectMenu';
import { Separator } from './Separator';
import { TextDisplay } from './TextDisplay';
import { Thumbnail } from './Thumbnail';
import type { BuilderComponents } from './types';
export * from './ActionRow';
export * from './Attachment';
export * from './Base';
export * from './Button';
export * from './Container';
export * from './Embed';
export * from './File';
export * from './MediaGallery';
export * from './Modal';
export * from './SelectMenu';
export * from './Poll';
export * from './Section';
export * from './SelectMenu';
export * from './Separator';
export * from './TextDisplay';
export * from './Thumbnail';
export * from './types';
export function fromComponent(
data:
| BuilderComponents
| APIActionRowComponentTypes
| APIActionRowComponent<APIActionRowComponentTypes>
| ActionRow<BuilderComponents>,
): BuilderComponents | ActionRow<BuilderComponents> {
export function fromComponent(data: BuilderComponents | APIComponents): BuilderComponents {
if ('toJSON' in data) {
return data;
}
switch (data.type) {
case ComponentType.Button:
return new Button(data);
@ -49,5 +56,19 @@ export function fromComponent(
return new ChannelSelectMenu(data);
case ComponentType.ActionRow:
return new ActionRow(data);
case ComponentType.Section:
return new Section(data);
case ComponentType.TextDisplay:
return new TextDisplay(data);
case ComponentType.Thumbnail:
return new Thumbnail(data);
case ComponentType.Container:
return new Container(data);
case ComponentType.MediaGallery:
return new MediaGallery(data);
case ComponentType.Separator:
return new Separator(data);
case ComponentType.File:
return new File(data);
}
}

View File

@ -3,9 +3,17 @@ import type {
ModalSubmitInteraction,
StringSelectMenuInteraction,
} from '../structures/Interaction';
import type { ActionRow } from './ActionRow';
import type { Button } from './Button';
import type { Container } from './Container';
import type { File } from './File';
import type { MediaGallery } from './MediaGallery';
import type { TextInput } from './Modal';
import type { Section } from './Section';
import type { BuilderSelectMenus } from './SelectMenu';
import type { Separator } from './Separator';
import type { TextDisplay } from './TextDisplay';
import type { Thumbnail } from './Thumbnail';
export type ComponentCallback<
T extends ComponentInteraction | StringSelectMenuInteraction = ComponentInteraction | StringSelectMenuInteraction,
@ -25,7 +33,22 @@ export type ButtonID = Omit<Button, 'setURL'>;
export type MessageBuilderComponents = FixedComponents<Button> | BuilderSelectMenus;
export type ModalBuilderComponents = TextInput;
export type BuilderComponents = MessageBuilderComponents | TextInput;
export type ActionBuilderComponents = MessageBuilderComponents | TextInput;
export type BuilderComponents =
| ActionRow
| ActionBuilderComponents
| Section<Button | Thumbnail>
| Thumbnail
| TextDisplay
| Container
| Separator
| MediaGallery
| File
| TextInput;
export type TopLevelBuilders = Exclude<BuilderComponents, Thumbnail | TextInput>;
export type FixedComponents<T = Button> = T extends Button ? ButtonLink | ButtonID : T;
export interface ListenerOptions {
timeout?: number;

View File

@ -18,7 +18,7 @@ export class Guilds extends BaseResource<any, APIGuild | GatewayGuildCreateDispa
);
}
raw(id: string): ReturnCache<APIGuild | undefined> {
raw(id: string): ReturnCache<(APIGuild & { member_count?: number }) | undefined> {
return super.get(id);
}
@ -28,7 +28,7 @@ export class Guilds extends BaseResource<any, APIGuild | GatewayGuildCreateDispa
);
}
bulkRaw(ids: string[]): ReturnCache<APIGuild[]> {
bulkRaw(ids: string[]): ReturnCache<(APIGuild & { member_count?: number })[]> {
return super.bulk(ids);
}
@ -38,7 +38,7 @@ export class Guilds extends BaseResource<any, APIGuild | GatewayGuildCreateDispa
);
}
valuesRaw(): ReturnCache<APIGuild[]> {
valuesRaw(): ReturnCache<(APIGuild & { member_count?: number })[]> {
return super.values();
}

View File

@ -1,7 +1,7 @@
import type { CacheFrom, ReturnCache } from '../..';
import { type GuildMemberStructure, Transformers } from '../../client/transformers';
import { fakePromise } from '../../common';
import type { APIGuildMember } from '../../types';
import type { APIGuildMember, APIUser } from '../../types';
import { GuildBasedResource } from './default/guild-based';
export class Members extends GuildBasedResource<any, APIGuildMember> {
namespace = 'member';
@ -39,14 +39,21 @@ export class Members extends GuildBasedResource<any, APIGuildMember> {
override bulk(ids: string[], guild: string): ReturnCache<GuildMemberStructure[]> {
return fakePromise(super.bulk(ids, guild)).then(members =>
fakePromise(this.client.cache.users?.bulkRaw(ids)).then(users =>
members
fakePromise(this.client.cache.users?.bulkRaw(ids)).then(users => {
if (!users) return [];
let usersRecord: null | Partial<Record<string, APIUser>> = {};
for (const user of users) {
usersRecord[user.id] = user;
}
const result = members
.map(rawMember => {
const user = users?.find(x => x.id === rawMember.id);
const user = usersRecord![rawMember.id];
return user ? Transformers.GuildMember(this.client, rawMember, user, guild) : undefined;
})
.filter(x => x !== undefined),
),
.filter(x => x !== undefined);
usersRecord = null;
return result;
}),
);
}
@ -56,14 +63,21 @@ export class Members extends GuildBasedResource<any, APIGuildMember> {
override values(guild: string): ReturnCache<GuildMemberStructure[]> {
return fakePromise(super.values(guild)).then(members =>
fakePromise(this.client.cache.users?.valuesRaw()).then(users =>
members
fakePromise(this.client.cache.users?.bulkRaw(members.map(member => member.id))).then(users => {
if (!users) return [];
let usersRecord: null | Partial<Record<string, APIUser>> = {};
for (const user of users) {
usersRecord[user.id] = user;
}
const result = members
.map(rawMember => {
const user = users?.find(x => x.id === rawMember.id);
const user = usersRecord![rawMember.id];
return user ? Transformers.GuildMember(this.client, rawMember, user, rawMember.guild_id) : undefined;
})
.filter(x => x !== undefined),
),
.filter(x => x !== undefined);
usersRecord = null;
return result;
}),
);
}

View File

@ -470,6 +470,8 @@ export interface BaseClientOptions {
globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[];
commands?: {
defaults?: {
onBeforeMiddlewares?: (context: CommandContext | MenuCommandContext<any, never>) => unknown;
onBeforeOptions?: Command['onBeforeOptions'];
onRunError?: (context: MenuCommandContext<any, never> | CommandContext, error: unknown) => unknown;
onPermissionsFail?: Command['onPermissionsFail'];
onBotPermissionsFail?: (
@ -489,6 +491,7 @@ export interface BaseClientOptions {
};
components?: {
defaults?: {
onBeforeMiddlewares?: ComponentCommand['onBeforeMiddlewares'];
onRunError?: ComponentCommand['onRunError'];
onInternalError?: ComponentCommand['onInternalError'];
onMiddlewaresError?: ComponentCommand['onMiddlewaresError'];
@ -497,6 +500,7 @@ export interface BaseClientOptions {
};
modals?: {
defaults?: {
onBeforeMiddlewares?: ModalCommand['onBeforeMiddlewares'];
onRunError?: ModalCommand['onRunError'];
onInternalError?: ModalCommand['onInternalError'];
onMiddlewaresError?: ModalCommand['onMiddlewaresError'];

View File

@ -285,6 +285,8 @@ export class BaseCommand {
Object.setPrototypeOf(this, __tempCommand.prototype);
}
onBeforeMiddlewares?(context: CommandContext): any;
onBeforeOptions?(context: CommandContext): any;
run?(context: CommandContext): any;
onAfterRun?(context: CommandContext, error: unknown | undefined): any;
onRunError?(context: CommandContext, error: unknown): any;

View File

@ -54,6 +54,7 @@ export abstract class EntryPointCommand {
Object.setPrototypeOf(this, __tempCommand.prototype);
}
onBeforeMiddlewares?(context: EntryPointContext): any;
abstract run?(context: EntryPointContext): any;
onAfterRun?(context: EntryPointContext, error: unknown | undefined): any;
onRunError(context: EntryPointContext<never>, error: unknown): any {

View File

@ -53,6 +53,7 @@ export abstract class ContextMenuCommand {
Object.setPrototypeOf(this, __tempCommand.prototype);
}
onBeforeMiddlewares?(context: MenuCommandContext<any>): any;
abstract run?(context: MenuCommandContext<any>): any;
onAfterRun?(context: MenuCommandContext<any>, error: unknown | undefined): any;
onRunError?(context: MenuCommandContext<any, never>, error: unknown): any;

View File

@ -91,7 +91,7 @@ export class HandleCommand {
await optionsResolver.getCommand()?.onInternalError?.(this.client, optionsResolver.getCommand()!, error);
}
} catch (error) {
// pass
this.client.logger.error(`[${optionsResolver.fullCommandName}] Internal error:`, error);
}
}
@ -100,17 +100,18 @@ export class HandleCommand {
interaction: MessageCommandInteraction | UserCommandInteraction,
context: MenuCommandContext<MessageCommandInteraction | UserCommandInteraction>,
) {
if (context.guildId && command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return command.onBotPermissionsFail?.(context, permissions);
}
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
if (context.guildId && command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return await command.onBotPermissionsFail?.(context, permissions);
}
await command.onBeforeMiddlewares?.(context);
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
await command.run!(context);
await command.onAfterRun?.(context, undefined);
@ -121,8 +122,8 @@ export class HandleCommand {
} catch (error) {
try {
await command.onInternalError?.(this.client, command, error);
} catch {
// pass
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
}
@ -144,17 +145,18 @@ export class HandleCommand {
}
async entryPoint(command: EntryPointCommand, interaction: EntryPointInteraction, context: EntryPointContext) {
if (context.guildId && command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return command.onBotPermissionsFail(context, permissions);
}
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
if (context.guildId && command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return await command.onBotPermissionsFail(context, permissions);
}
await command.onBeforeMiddlewares?.(context);
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
await command.run!(context);
await command.onAfterRun?.(context, undefined);
@ -165,8 +167,8 @@ export class HandleCommand {
} catch (error) {
try {
await command.onInternalError(this.client, command, error);
} catch {
// pass
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
}
@ -177,26 +179,28 @@ export class HandleCommand {
resolver: OptionResolverStructure,
context: CommandContext,
) {
if (context.guildId) {
if (command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return command.onBotPermissionsFail?.(context, permissions);
}
if (command.defaultMemberPermissions) {
const permissions = this.checkPermissions(interaction.member!.permissions, command.defaultMemberPermissions);
if (permissions) return command.onPermissionsFail?.(context, permissions);
}
}
if (!(await this.runOptions(command, context, resolver))) return;
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
if (context.guildId) {
if (command.botPermissions) {
const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions);
if (permissions) return await command.onBotPermissionsFail?.(context, permissions);
}
if (command.defaultMemberPermissions) {
const permissions = this.checkPermissions(interaction.member!.permissions, command.defaultMemberPermissions);
if (permissions) return await command.onPermissionsFail?.(context, permissions);
}
}
await command.onBeforeOptions?.(context);
if (!(await this.runOptions(command, context, resolver))) return;
await command.onBeforeMiddlewares?.(context);
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
if (typeof resultMiddle === 'boolean') return;
try {
await command.run!(context);
await command.onAfterRun?.(context, undefined);
@ -207,8 +211,8 @@ export class HandleCommand {
} catch (error) {
try {
await command.onInternalError?.(this.client, command, error);
} catch {
// pass
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
}
@ -350,17 +354,17 @@ export class HandleCommand {
attachments: {},
};
const args = this.argsParser(argsContent, command, message);
const { options, errors } = await this.argsOptionsParser(command, rawMessage, args, resolved);
const optionsResolver = this.makeResolver(self, options, parent as Command, rawMessage.guild_id, resolved);
const context = new CommandContext(self, message, optionsResolver, shardId, command);
//@ts-expect-error
const extendContext = self.options?.context?.(message) ?? {};
Object.assign(context, extendContext);
try {
const args = this.argsParser(argsContent, command, message);
const { options, errors } = await this.argsOptionsParser(command, rawMessage, args, resolved);
const optionsResolver = this.makeResolver(self, options, parent as Command, rawMessage.guild_id, resolved);
const context = new CommandContext(self, message, optionsResolver, shardId, command);
//@ts-expect-error
const extendContext = self.options?.context?.(message) ?? {};
Object.assign(context, extendContext);
if (errors.length) {
return command.onOptionsError?.(
return await command.onOptionsError?.(
context,
Object.fromEntries(
errors.map(x => {
@ -383,7 +387,7 @@ export class HandleCommand {
const permissions = this.checkPermissions(memberPermissions, command.defaultMemberPermissions);
const guild = await this.client.guilds.raw(rawMessage.guild_id);
if (permissions && guild.owner_id !== rawMessage.author.id) {
return command.onPermissionsFail?.(context, memberPermissions.keys(permissions));
return await command.onPermissionsFail?.(context, memberPermissions.keys(permissions));
}
}
@ -391,13 +395,15 @@ export class HandleCommand {
const appPermissions = await self.members.permissions(rawMessage.guild_id, self.botId);
const permissions = this.checkPermissions(appPermissions, command.botPermissions);
if (permissions) {
return command.onBotPermissionsFail?.(context, permissions);
return await command.onBotPermissionsFail?.(context, permissions);
}
}
}
await command.onBeforeOptions?.(context);
if (!(await this.runOptions(command, context, optionsResolver))) return;
await command.onBeforeMiddlewares?.(context);
const resultGlobal = await this.runGlobalMiddlewares(command, context);
if (typeof resultGlobal === 'boolean') return;
const resultMiddle = await this.runMiddlewares(command, context);
@ -412,8 +418,8 @@ export class HandleCommand {
} catch (error) {
try {
await command.onInternalError?.(this.client, command, error);
} catch {
// http 418
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
}
@ -574,8 +580,8 @@ export class HandleCommand {
} catch (e) {
try {
await command.onInternalError?.(this.client, command as never, e);
} catch {
// http 418
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
return false;
@ -602,8 +608,8 @@ export class HandleCommand {
} catch (e) {
try {
await command.onInternalError?.(this.client, command as never, e);
} catch {
// http 418
} catch (err) {
this.client.logger.error(`[${command.name}] Internal error:`, err);
}
}
return false;
@ -627,15 +633,7 @@ export class HandleCommand {
async runOptions(command: Command | SubCommand, context: CommandContext, resolver: OptionResolverStructure) {
const [erroredOptions, result] = await command.__runOptions(context, resolver);
if (erroredOptions) {
try {
await command.onOptionsError?.(context, result);
} catch (e) {
try {
await command.onInternalError?.(this.client, command, e);
} catch {
// http 418
}
}
await command.onOptionsError?.(context, result);
return false;
}
return true;

View File

@ -466,6 +466,8 @@ export class CommandHandler extends BaseHandler {
stablishContextCommandDefaults(commandInstance: InstanceType<HandleableCommand>): ContextMenuCommand | false {
if (!(commandInstance instanceof ContextMenuCommand)) return false;
commandInstance.onBeforeMiddlewares ??= this.client.options.commands?.defaults?.onBeforeMiddlewares;
commandInstance.onAfterRun ??= this.client.options.commands?.defaults?.onAfterRun;
commandInstance.onBotPermissionsFail ??= this.client.options.commands?.defaults?.onBotPermissionsFail;
@ -482,6 +484,8 @@ export class CommandHandler extends BaseHandler {
commandInstance: InstanceType<HandleableCommand>,
): OmitInsert<Command, 'options', { options: NonNullable<Command['options']> }> | false {
if (!(commandInstance instanceof Command)) return false;
commandInstance.onBeforeMiddlewares ??= this.client.options.commands?.defaults?.onBeforeMiddlewares;
commandInstance.onBeforeOptions ??= this.client.options.commands?.defaults?.onBeforeOptions;
commandInstance.onAfterRun ??= this.client.options.commands?.defaults?.onAfterRun;
commandInstance.onBotPermissionsFail ??= this.client.options.commands?.defaults?.onBotPermissionsFail;
commandInstance.onInternalError ??= this.client.options.commands?.defaults?.onInternalError;
@ -495,6 +499,14 @@ export class CommandHandler extends BaseHandler {
stablishSubCommandDefaults(commandInstance: Command, option: SubCommand): SubCommand {
option.middlewares = (commandInstance.middlewares ?? []).concat(option.middlewares ?? []);
option.onBeforeMiddlewares =
option.onBeforeMiddlewares?.bind(option) ??
commandInstance.onBeforeMiddlewares?.bind(commandInstance) ??
this.client.options.commands?.defaults?.onBeforeMiddlewares;
option.onBeforeOptions =
option.onBeforeOptions?.bind(option) ??
commandInstance.onBeforeOptions?.bind(commandInstance) ??
this.client.options.commands?.defaults?.onBeforeOptions;
option.onMiddlewaresError =
option.onMiddlewaresError?.bind(option) ??
commandInstance.onMiddlewaresError?.bind(commandInstance) ??

View File

@ -1,20 +1,11 @@
import type { RawFile } from '../../api';
import type { Attachment, AttachmentBuilder, Embed, Modal, PollBuilder, TopLevelBuilders } from '../../builders';
import type {
ActionRow,
Attachment,
AttachmentBuilder,
BuilderComponents,
Embed,
Modal,
PollBuilder,
} from '../../builders';
import type {
APIActionRowComponent,
APIEmbed,
APIInteractionResponseCallbackData,
APIInteractionResponseChannelMessageWithSource,
APIMessageActionRowComponent,
APIModalInteractionResponse,
MessageFlags,
RESTAPIPollCreate,
RESTPatchAPIChannelMessageJSONBody,
RESTPatchAPIWebhookWithTokenMessageJSONBody,
@ -26,7 +17,7 @@ import type { OmitInsert } from './util';
export interface ResolverProps {
embeds?: Embed[] | APIEmbed[] | undefined;
components?: APIActionRowComponent<APIMessageActionRowComponent>[] | ActionRow<BuilderComponents>[] | undefined;
components?: TopLevelBuilders[];
files?: AttachmentBuilder[] | Attachment[] | RawFile[] | undefined;
}
@ -62,7 +53,9 @@ export type InteractionMessageUpdateBodyRequest = OmitInsert<
RESTPatchAPIWebhookWithTokenMessageJSONBody,
'components' | 'embeds' | 'poll',
SendResolverProps
>;
> & {
flags?: MessageFlags;
};
export type ComponentInteractionMessageUpdate = OmitInsert<
APIInteractionResponseCallbackData,

View File

@ -4,7 +4,7 @@ import type { ActionRowMessageComponents } from './index';
import { componentFactory } from './index';
export class MessageActionRowComponent<
T extends ActionRowMessageComponents,
T extends ActionRowMessageComponents = ActionRowMessageComponents,
> extends BaseComponent<ComponentType.ActionRow> {
private ComponentsFactory: T[];
constructor(data: {

View File

@ -1,13 +1,37 @@
import { fromComponent } from '../builders';
import {
type ActionRow,
type Button,
type ChannelSelectMenu,
type Container,
type File,
type MediaGallery,
type MentionableSelectMenu,
type RoleSelectMenu,
type Section,
type Separator,
type StringSelectMenu,
type TextDisplay,
type TextInput,
type Thumbnail,
type UserSelectMenu,
fromComponent,
} from '../builders';
import {
type APIActionRowComponent,
type APIActionRowComponentTypes,
type APIButtonComponent,
type APIChannelSelectComponent,
type APIContainerComponent,
type APIFileComponent,
type APIMediaGalleryComponent,
type APIMentionableSelectComponent,
type APIRoleSelectComponent,
type APISectionComponent,
type APISeparatorComponent,
type APIStringSelectComponent,
type APITextDispalyComponent,
type APITextInputComponent,
type APIThumbnailComponent,
type APIUserSelectComponent,
ComponentType,
} from '../types';
@ -24,7 +48,7 @@ export class BaseComponent<T extends ComponentType> {
}
toBuilder() {
return fromComponent(this.data);
return fromComponent(this.data) as BuilderComponentsMap[T];
}
}
export interface APIComponentsMap {
@ -36,4 +60,29 @@ export interface APIComponentsMap {
[ComponentType.StringSelect]: APIStringSelectComponent;
[ComponentType.UserSelect]: APIUserSelectComponent;
[ComponentType.TextInput]: APITextInputComponent;
[ComponentType.File]: APIFileComponent;
[ComponentType.Thumbnail]: APIThumbnailComponent;
[ComponentType.Section]: APISectionComponent;
[ComponentType.Container]: APIContainerComponent;
[ComponentType.MediaGallery]: APIMediaGalleryComponent;
[ComponentType.Separator]: APISeparatorComponent;
[ComponentType.TextDisplay]: APITextDispalyComponent;
}
export interface BuilderComponentsMap {
[ComponentType.ActionRow]: ActionRow;
[ComponentType.Button]: Button;
[ComponentType.ChannelSelect]: ChannelSelectMenu;
[ComponentType.MentionableSelect]: MentionableSelectMenu;
[ComponentType.RoleSelect]: RoleSelectMenu;
[ComponentType.StringSelect]: StringSelectMenu;
[ComponentType.UserSelect]: UserSelectMenu;
[ComponentType.TextInput]: TextInput;
[ComponentType.File]: File;
[ComponentType.Thumbnail]: Thumbnail;
[ComponentType.Section]: Section;
[ComponentType.Container]: Container;
[ComponentType.MediaGallery]: MediaGallery;
[ComponentType.Separator]: Separator;
[ComponentType.TextDisplay]: TextDisplay;
}

View File

@ -0,0 +1,23 @@
import { type ContainerComponents, componentFactory } from '.';
import type { APIContainerComponent, ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class ContainerComponent extends BaseComponent<ComponentType.Container> {
_components: ContainerComponents[];
constructor(data: APIContainerComponent) {
super(data);
this._components = this.data.components.map(componentFactory) as ContainerComponents[];
}
get components() {
return this.data.components;
}
get accentColor() {
return this.data.accent_color;
}
get spoiler() {
return this.data.spoiler;
}
}

16
src/components/File.ts Normal file
View File

@ -0,0 +1,16 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class FileComponent extends BaseComponent<ComponentType.File> {
get spoiler() {
return this.data.spoiler;
}
get file() {
return this.data.file;
}
get id() {
return this.data.id;
}
}

View File

@ -0,0 +1,12 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class MediaGalleryComponent extends BaseComponent<ComponentType.MediaGallery> {
get items() {
return this.data.items;
}
get id() {
return this.data.id;
}
}

23
src/components/Section.ts Normal file
View File

@ -0,0 +1,23 @@
import { componentFactory } from '.';
import type { APISectionComponent, ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
import type { ButtonComponent } from './ButtonComponent';
import type { TextDisplayComponent } from './TextDisplay';
import type { ThumbnailComponent } from './Thumbnail';
export class SectionComponent extends BaseComponent<ComponentType.Section> {
protected _components: TextDisplayComponent[];
protected _accessory: ThumbnailComponent | ButtonComponent;
constructor(data: APISectionComponent) {
super(data);
this._components = data.components?.map(componentFactory) as TextDisplayComponent[];
this._accessory = componentFactory(data.accessory) as ThumbnailComponent | ButtonComponent;
}
get components() {
return this._components;
}
get accessory() {
return this._accessory;
}
}

View File

@ -0,0 +1,16 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class SeparatorComponent extends BaseComponent<ComponentType.Separator> {
get id() {
return this.data.id;
}
get spacing() {
return this.data.spacing;
}
get divider() {
return this.data.divider;
}
}

View File

@ -0,0 +1,8 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class TextDisplayComponent extends BaseComponent<ComponentType.TextDisplay> {
get content() {
return this.data.content;
}
}

View File

@ -0,0 +1,20 @@
import type { ComponentType } from '../types';
import { BaseComponent } from './BaseComponent';
export class ThumbnailComponent extends BaseComponent<ComponentType.Thumbnail> {
get id() {
return this.data.id;
}
get media() {
return this.data.media;
}
get description() {
return this.data.description;
}
get spoiler() {
return this.data.spoiler;
}
}

View File

@ -33,6 +33,7 @@ export abstract class ComponentCommand {
return ComponentType[this.componentType];
}
onBeforeMiddlewares?(context: ComponentContext): any;
onAfterRun?(context: ComponentContext, error: unknown | undefined): any;
onRunError?(context: ComponentContext, error: unknown): any;
onMiddlewaresError?(context: ComponentContext, error: string): any;

View File

@ -204,6 +204,7 @@ export class ComponentHandler extends BaseHandler {
component.onMiddlewaresError ??= this.client.options?.[is]?.defaults?.onMiddlewaresError;
component.onRunError ??= this.client.options?.[is]?.defaults?.onRunError;
component.onAfterRun ??= this.client.options?.[is]?.defaults?.onAfterRun;
component.onBeforeMiddlewares ??= this.client.options?.[is]?.defaults?.onBeforeMiddlewares;
}
set(instances: (new () => ComponentCommands)[]) {
@ -289,6 +290,7 @@ export class ComponentHandler extends BaseHandler {
async execute(i: ComponentCommands, context: ComponentContext | ModalContext) {
try {
await i.onBeforeMiddlewares?.(context as never);
const resultRunGlobalMiddlewares = await BaseCommand.__runMiddlewares(
context,
(context.client.options?.globalMiddlewares ?? []) as keyof RegisteredMiddlewares,
@ -298,7 +300,7 @@ export class ComponentHandler extends BaseHandler {
return;
}
if ('error' in resultRunGlobalMiddlewares) {
return i.onMiddlewaresError?.(context as never, resultRunGlobalMiddlewares.error ?? 'Unknown error');
return await i.onMiddlewaresError?.(context as never, resultRunGlobalMiddlewares.error ?? 'Unknown error');
}
const resultRunMiddlewares = await BaseCommand.__runMiddlewares(context, i.middlewares, false);
@ -306,7 +308,7 @@ export class ComponentHandler extends BaseHandler {
return;
}
if ('error' in resultRunMiddlewares) {
return i.onMiddlewaresError?.(context as never, resultRunMiddlewares.error ?? 'Unknown error');
return await i.onMiddlewaresError?.(context as never, resultRunMiddlewares.error ?? 'Unknown error');
}
try {
@ -319,9 +321,8 @@ export class ComponentHandler extends BaseHandler {
} catch (error) {
try {
await i.onInternalError?.(this.client, error);
} catch (e) {
// supress error
this.logger.error(e);
} catch (err) {
this.client.logger.error(`[${i.customId ?? 'Component/Modal command'}] Internal error:`, err);
}
}
}

View File

@ -1,11 +1,19 @@
import { type APIMessageActionRowComponent, ButtonStyle, ComponentType } from '../types';
import { type APIComponents, type APITopLevelComponent, ButtonStyle, ComponentType } from '../types';
import { MessageActionRowComponent } from './ActionRow';
import { BaseComponent } from './BaseComponent';
import { ButtonComponent, LinkButtonComponent, SKUButtonComponent } from './ButtonComponent';
import { ChannelSelectMenuComponent } from './ChannelSelectMenuComponent';
import { ContainerComponent } from './Container';
import { FileComponent } from './File';
import { MediaGalleryComponent } from './MediaGallery';
import { MentionableSelectMenuComponent } from './MentionableSelectMenuComponent';
import { RoleSelectMenuComponent } from './RoleSelectMenuComponent';
import { SectionComponent } from './Section';
import { SeparatorComponent } from './Separator';
import { StringSelectMenuComponent } from './StringSelectMenuComponent';
import type { TextInputComponent } from './TextInputComponent';
import { TextDisplayComponent } from './TextDisplay';
import { TextInputComponent } from './TextInputComponent';
import { ThumbnailComponent } from './Thumbnail';
import { UserSelectMenuComponent } from './UserSelectMenuComponent';
export type MessageComponents =
@ -21,20 +29,37 @@ export type MessageComponents =
export type ActionRowMessageComponents = Exclude<MessageComponents, TextInputComponent>;
export type AllComponents = MessageComponents | TopLevelComponents | ContainerComponents | BaseComponent<ComponentType>;
export * from './componentcommand';
export * from './componentcontext';
export * from './modalcommand';
export * from './modalcontext';
export type TopLevelComponents =
| SectionComponent
| ActionRowMessageComponents
| TextDisplayComponent
| ContainerComponent
| FileComponent
| MediaGalleryComponent
| BaseComponent<APITopLevelComponent['type']>;
export type ContainerComponents =
| MessageActionRowComponent
| TextDisplayComponent
| MediaGalleryComponent
| SectionComponent
| SeparatorComponent
| FileComponent;
/**
* Return a new component instance based on the component type.
*
* @param component The component to create.
* @returns The component instance.
*/
export function componentFactory(
component: APIMessageActionRowComponent,
): ActionRowMessageComponents | BaseComponent<ActionRowMessageComponents['type']> {
export function componentFactory(component: APIComponents): AllComponents {
switch (component.type) {
case ComponentType.Button: {
if (component.style === ButtonStyle.Link) {
@ -55,7 +80,25 @@ export function componentFactory(
return new UserSelectMenuComponent(component);
case ComponentType.MentionableSelect:
return new MentionableSelectMenuComponent(component);
case ComponentType.ActionRow:
return new MessageActionRowComponent(component as any);
case ComponentType.Container:
return new ContainerComponent(component);
case ComponentType.File:
return new FileComponent(component);
case ComponentType.MediaGallery:
return new MediaGalleryComponent(component);
case ComponentType.Section:
return new SectionComponent(component);
case ComponentType.TextDisplay:
return new TextDisplayComponent(component);
case ComponentType.Separator:
return new SeparatorComponent(component);
case ComponentType.Thumbnail:
return new ThumbnailComponent(component);
case ComponentType.TextInput:
return new TextInputComponent(component);
default:
return new BaseComponent<ActionRowMessageComponents['type']>(component);
return new BaseComponent<ComponentType>(component);
}
}

View File

@ -23,6 +23,7 @@ export abstract class ModalCommand {
props!: ExtraProps;
onBeforeMiddlewares?(context: ModalContext): any;
onAfterRun?(context: ModalContext, error: unknown | undefined): any;
onRunError?(context: ModalContext, error: unknown): any;
onMiddlewaresError?(context: ModalContext, error: string): any;

View File

@ -148,6 +148,7 @@ export class BaseInteraction<
//@ts-ignore
return {
type: body.type,
// @ts-expect-error
data: BaseInteraction.transformBody(body.data ?? {}, files, self),
};
}
@ -330,7 +331,6 @@ export class BaseInteraction<
case ApplicationCommandType.PrimaryEntryPoint:
return new EntryPointInteraction(client, gateway as APIEntryPointCommandInteraction, __reply);
}
// biome-ignore lint/suspicious/noFallthroughSwitchClause: bad interaction between biome and ts-server
case InteractionType.MessageComponent:
switch (gateway.data.component_type) {
case ComponentType.Button:
@ -357,6 +357,8 @@ export class BaseInteraction<
gateway as APIMessageComponentSelectMenuInteraction,
__reply,
);
default:
return;
}
case InteractionType.ModalSubmit:
return new ModalSubmitInteraction(client, gateway);
@ -488,8 +490,8 @@ export class Interaction<
fetchReply?: FR,
): Promise<WebhookMessageStructure> {
if (await this.replied) {
const { content, embeds, allowed_mentions, components, files, attachments, poll } = body;
return this.editResponse({ content, embeds, allowed_mentions, components, files, attachments, poll });
const { content, embeds, allowed_mentions, components, files, attachments, poll, flags } = body;
return this.editResponse({ content, embeds, allowed_mentions, components, files, attachments, poll, flags });
}
return this.write(body as InteractionCreateBodyRequest, fetchReply);
}

View File

@ -1,4 +1,4 @@
import { type AllChannels, Embed, type ReturnCache } from '..';
import { type AllChannels, Embed, type ReturnCache, componentFactory } from '..';
import type { ListenerOptions } from '../builders';
import {
type GuildMemberStructure,
@ -15,8 +15,7 @@ import { type ObjectToLower, toCamelCase } from '../common';
import { Formatter } from '../common';
import type { EmojiResolvable } from '../common/types/resolvables';
import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../common/types/write';
import type { ActionRowMessageComponents } from '../components';
import { MessageActionRowComponent } from '../components/ActionRow';
import type { TopLevelComponents } from '../components';
import type {
APIChannelMention,
APIEmbed,
@ -37,7 +36,7 @@ export interface BaseMessage
guildId?: string;
author: UserStructure;
member?: GuildMemberStructure;
components: MessageActionRowComponent<ActionRowMessageComponents>[];
components: TopLevelComponents[];
poll?: PollStructure;
mentions: {
roles: string[];
@ -55,7 +54,7 @@ export class BaseMessage extends DiscordBase {
channels: data.mention_channels ?? [],
users: [],
};
this.components = data.components?.map(x => new MessageActionRowComponent(x)) ?? [];
this.components = (data.components?.map(componentFactory) as TopLevelComponents[]) ?? [];
this.embeds = data.embeds.map(embed => new InMessageEmbed(embed));
this.patch(data);
}

View File

@ -21,9 +21,9 @@ import type {
*/
import type {
APIWebhook,
RESTGetAPIWebhookWithTokenMessageQuery,
RESTPatchAPIWebhookJSONBody,
RESTPatchAPIWebhookWithTokenJSONBody,
RESTPatchAPIWebhookWithTokenMessageQuery,
RESTPostAPIWebhookWithTokenQuery,
} from '../types';
import type { AllChannels } from './channels';
@ -181,7 +181,7 @@ export class Webhook extends DiscordBase {
/** Type definition for parameters of editing a message through a webhook. */
export type MessageWebhookMethodEditParams = MessageWebhookPayload<
MessageWebhookUpdateBodyRequest,
{ messageId: string; query?: RESTGetAPIWebhookWithTokenMessageQuery }
{ messageId: string; query?: RESTPatchAPIWebhookWithTokenMessageQuery }
>;
/** Type definition for parameters of writing a message through a webhook. */
export type MessageWebhookMethodWriteParams = MessageWebhookPayload<

View File

@ -1,5 +1,5 @@
import { Collection, Formatter, type RawFile, type ReturnCache } from '..';
import { ActionRow, Embed, PollBuilder, resolveAttachment } from '../builders';
import { Embed, PollBuilder, resolveAttachment } from '../builders';
import type { Overwrites } from '../cache/resources/overwrites';
import {
type BaseChannelStructure,
@ -353,8 +353,8 @@ export class MessagesMethods extends DiscordBase {
const payload = {
allowed_mentions: self.options?.allowedMentions,
...body,
components: body.components?.map(x => (x instanceof ActionRow ? x.toJSON() : x)) ?? undefined,
embeds: body.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)) ?? undefined,
embeds: body.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)),
components: body.components?.map(x => ('toJSON' in x ? x.toJSON() : x)) ?? undefined,
poll: poll ? (poll instanceof PollBuilder ? poll.toJSON() : poll) : undefined,
};

View File

@ -1,5 +1,5 @@
import type { Snowflake } from '../../index';
import type { ComponentType } from '../channel';
import type { ComponentType, Snowflake } from '../../index';
import type { APIBaseInteraction, InteractionType } from '../interactions';
import type {
APIDMInteractionWrapper,

View File

@ -1,8 +1,9 @@
import type { APIActionRowComponent, APIModalActionRowComponent } from '../channel';
import type {
APIActionRowComponent,
APIBaseInteraction,
APIDMInteractionWrapper,
APIGuildInteractionWrapper,
APIModalActionRowComponent,
ComponentType,
InteractionType,
} from '../index';

View File

@ -1,7 +1,7 @@
import type { MakeRequired } from '../../../common';
import type { RESTPostAPIWebhookWithTokenJSONBody } from '../../index';
import type { APIActionRowComponent, APIModalActionRowComponent } from '../channel';
import type { MessageFlags } from '../index';
import type { APIActionRowComponent, APIModalActionRowComponent, MessageFlags } from '../index';
import type { APIApplicationCommandOptionChoice } from './applicationCommands';
/**

View File

@ -3,7 +3,15 @@
*/
import type { PickRequired } from '../../common';
import type { ChannelType, OverwriteType, Permissions, Snowflake, VideoQualityMode } from '../index';
import type {
APITopLevelComponent,
ChannelFlags,
ChannelType,
OverwriteType,
Permissions,
Snowflake,
VideoQualityMode,
} from '../index';
import type { APIApplication } from './application';
import type { APIPartialEmoji } from './emoji';
import type { APIGuildMember } from './guild';
@ -579,7 +587,7 @@ export interface APIMessage {
*
* See https://support-dev.discord.com/hc/articles/4404772028055
*/
components?: APIActionRowComponent<APIMessageActionRowComponent>[];
components?: APITopLevelComponent[];
/**
* Sent if the message contains stickers
*
@ -813,6 +821,14 @@ export enum MessageFlags {
* This message is a voice message
*/
IsVoiceMessage = 1 << 13,
/**
* This message has a snapshot (via Message Forwarding)
*/
HasSnapshot = 1 << 14,
/**
* This message allows you to create fully component driven messages
*/
IsComponentsV2 = 1 << 15,
}
/**
@ -1498,403 +1514,3 @@ export interface APIAllowedMentions {
*/
replied_user?: boolean;
}
/**
* https://discord.com/developers/docs/interactions/message-components#component-object
*/
export interface APIBaseComponent<T extends ComponentType> {
/**
* The type of the component
*/
type: T;
}
/**
* https://discord.com/developers/docs/interactions/message-components#component-object-component-types
*/
export enum ComponentType {
/**
* Action Row component
*/
ActionRow = 1,
/**
* Button component
*/
Button,
/**
* Select menu for picking from defined text options
*/
StringSelect,
/**
* Text Input component
*/
TextInput,
/**
* Select menu for users
*/
UserSelect,
/**
* Select menu for roles
*/
RoleSelect,
/**
* Select menu for users and roles
*/
MentionableSelect,
/**
* Select menu for channels
*/
ChannelSelect,
}
/**
* https://discord.com/developers/docs/interactions/message-components#action-rows
*/
export interface APIActionRowComponent<T extends APIActionRowComponentTypes>
extends APIBaseComponent<ComponentType.ActionRow> {
/**
* The components in the ActionRow
*/
components: T[];
}
/**
* https://discord.com/developers/docs/interactions/message-components#buttons
*/
export interface APIButtonComponentBase<Style extends ButtonStyle> extends APIBaseComponent<ComponentType.Button> {
/**
* The label to be displayed on the button
*/
label?: string;
/**
* The style of the button
*/
style: Style;
/**
* The emoji to display to the left of the text
*/
emoji?: APIMessageComponentEmoji;
/**
* The status of the button
*/
disabled?: boolean;
}
export interface APIMessageComponentEmoji {
/**
* Emoji id
*/
id?: Snowflake;
/**
* Emoji name
*/
name?: string;
/**
* Whether this emoji is animated
*/
animated?: boolean;
}
export interface APIButtonComponentWithCustomId
extends APIButtonComponentBase<
ButtonStyle.Danger | ButtonStyle.Primary | ButtonStyle.Secondary | ButtonStyle.Success
> {
/**
* The custom_id to be sent in the interaction when clicked
*/
custom_id: string;
}
export interface APIButtonComponentWithURL extends APIButtonComponentBase<ButtonStyle.Link> {
/**
* The URL to direct users to when clicked for Link buttons
*/
url: string;
}
export interface APIButtonComponentWithSKUId
extends Omit<APIButtonComponentBase<ButtonStyle.Premium>, 'custom_id' | 'emoji' | 'label'> {
/**
* The id for a purchasable SKU
*/
sku_id: Snowflake;
}
export type APIButtonComponent =
| APIButtonComponentWithCustomId
| APIButtonComponentWithSKUId
| APIButtonComponentWithURL;
/**
* https://discord.com/developers/docs/interactions/message-components#button-object-button-styles
*/
export enum ButtonStyle {
Primary = 1,
Secondary,
Success,
Danger,
Link,
Premium,
}
/**
* https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles
*/
export enum TextInputStyle {
Short = 1,
Paragraph,
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export interface APIBaseSelectMenuComponent<
T extends
| ComponentType.ChannelSelect
| ComponentType.MentionableSelect
| ComponentType.RoleSelect
| ComponentType.StringSelect
| ComponentType.UserSelect,
> extends APIBaseComponent<T> {
/**
* A developer-defined identifier for the select menu, max 100 characters
*/
custom_id: string;
/**
* Custom placeholder text if nothing is selected, max 150 characters
*/
placeholder?: string;
/**
* The minimum number of items that must be chosen; min 0, max 25
*
* @default 1
*/
min_values?: number;
/**
* The maximum number of items that can be chosen; max 25
*
* @default 1
*/
max_values?: number;
/**
* Disable the select
*
* @default false
*/
disabled?: boolean;
}
export interface APIBaseAutoPopulatedSelectMenuComponent<
T extends
| ComponentType.ChannelSelect
| ComponentType.MentionableSelect
| ComponentType.RoleSelect
| ComponentType.UserSelect,
D extends SelectMenuDefaultValueType,
> extends APIBaseSelectMenuComponent<T> {
/**
* List of default values for auto-populated select menu components
*/
default_values?: APISelectMenuDefaultValue<D>[];
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export interface APIStringSelectComponent extends APIBaseSelectMenuComponent<ComponentType.StringSelect> {
/**
* Specified choices in a select menu; max 25
*/
options: APISelectMenuOption[];
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export type APIUserSelectComponent = APIBaseAutoPopulatedSelectMenuComponent<
ComponentType.UserSelect,
SelectMenuDefaultValueType.User
>;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export type APIRoleSelectComponent = APIBaseAutoPopulatedSelectMenuComponent<
ComponentType.RoleSelect,
SelectMenuDefaultValueType.Role
>;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export type APIMentionableSelectComponent = APIBaseAutoPopulatedSelectMenuComponent<
ComponentType.MentionableSelect,
SelectMenuDefaultValueType.Role | SelectMenuDefaultValueType.User
>;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export interface APIChannelSelectComponent
extends APIBaseAutoPopulatedSelectMenuComponent<ComponentType.ChannelSelect, SelectMenuDefaultValueType.Channel> {
/**
* List of channel types to include in the ChannelSelect component
*/
channel_types?: ChannelType[];
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-default-value-structure
*/
export enum SelectMenuDefaultValueType {
Channel = 'channel',
Role = 'role',
User = 'user',
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-default-value-structure
*/
export interface APISelectMenuDefaultValue<T extends SelectMenuDefaultValueType> {
type: T;
id: Snowflake;
}
export type APIAutoPopulatedSelectMenuComponent =
| APIChannelSelectComponent
| APIMentionableSelectComponent
| APIRoleSelectComponent
| APIUserSelectComponent;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export type APISelectMenuComponent =
| APIChannelSelectComponent
| APIMentionableSelectComponent
| APIRoleSelectComponent
| APIStringSelectComponent
| APIUserSelectComponent;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure
*/
export interface APISelectMenuOption {
/**
* The user-facing name of the option (max 100 chars)
*/
label: string;
/**
* The dev-defined value of the option (max 100 chars)
*/
value: string;
/**
* An additional description of the option (max 100 chars)
*/
description?: string;
/**
* The emoji to display to the left of the option
*/
emoji?: APIMessageComponentEmoji;
/**
* Whether this option should be already-selected by default
*/
default?: boolean;
}
/**
* https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure
*/
export interface APITextInputComponent extends APIBaseComponent<ComponentType.TextInput> {
/**
* One of text input styles
*/
style: TextInputStyle;
/**
* The custom id for the text input
*/
custom_id: string;
/**
* Text that appears on top of the text input field, max 45 characters
*/
label: string;
/**
* Placeholder for the text input
*/
placeholder?: string;
/**
* The pre-filled text in the text input
*/
value?: string;
/**
* Minimal length of text input
*/
min_length?: number;
/**
* Maximal length of text input
*/
max_length?: number;
/**
* Whether or not this text input is required or not
*/
required?: boolean;
}
/**
* https://discord.com/developers/docs/resources/channel#channel-object-channel-flags
*/
export enum ChannelFlags {
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
GuildFeedRemoved = 1 << 0,
/**
* This thread is pinned to the top of its parent forum channel
*/
Pinned = 1 << 1,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
ActiveChannelsRemoved = 1 << 2,
/**
* Whether a tag is required to be specified when creating a thread in a forum channel.
* Tags are specified in the `applied_tags` field
*/
RequireTag = 1 << 4,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
IsSpam = 1 << 5,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
IsGuildResourceChannel = 1 << 7,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
ClydeAI = 1 << 8,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
IsScheduledForDeletion = 1 << 9,
/**
* Whether media download options are hidden.
*/
HideMediaDownloadOptions = 1 << 15,
}
/**
* https://discord.com/developers/docs/interactions/message-components#message-components
*/
export type APIMessageComponent = APIActionRowComponent<APIMessageActionRowComponent> | APIMessageActionRowComponent;
export type APIModalComponent = APIActionRowComponent<APIModalActionRowComponent> | APIModalActionRowComponent;
export type APIActionRowComponentTypes = APIMessageActionRowComponent | APIModalActionRowComponent;
/**
* https://discord.com/developers/docs/interactions/message-components#message-components
*/
export type APIMessageActionRowComponent = APIButtonComponent | APISelectMenuComponent;
// Modal components
export type APIModalActionRowComponent = APITextInputComponent;

View File

@ -0,0 +1,594 @@
import type { APIAttachment, Snowflake } from '..';
import type { Identify, MakeRequired } from '../../common';
import type { ChannelType } from '../utils';
/**
* https://discord.com/developers/docs/interactions/message-components#component-object
*/
export interface APIBaseComponent<T extends ComponentType> {
/**
* The type of the component
*/
type: T;
}
/**
* https://discord.com/developers/docs/interactions/message-components#component-object-component-types
*/
export enum ComponentType {
/**
* Action Row component
*/
ActionRow = 1,
/**
* Button component
*/
Button,
/**
* Select menu for picking from defined text options
*/
StringSelect,
/**
* Text Input component
*/
TextInput,
/**
* Select menu for users
*/
UserSelect,
/**
* Select menu for roles
*/
RoleSelect,
/**
* Select menu for users and roles
*/
MentionableSelect,
/**
* Select menu for channels
*/
ChannelSelect,
/**
* Section for accessory
*/
Section,
/**
* Text display component
*/
TextDisplay,
/**
* Thumbnail component
*/
Thumbnail,
/**
* Media Gallery component
*/
MediaGallery,
/**
* File component
*/
File,
/**
* Separator component
*/
Separator,
/**
* Container component
*/
Container = 17,
}
/**
* https://discord.com/developers/docs/interactions/message-components#action-rows
*/
export interface APIActionRowComponent<T extends APIActionRowComponentTypes>
extends APIBaseComponent<ComponentType.ActionRow> {
/**
* The components in the ActionRow
*/
components: T[];
}
/**
* https://discord.com/developers/docs/interactions/message-components#buttons
*/
export interface APIButtonComponentBase<Style extends ButtonStyle> extends APIBaseComponent<ComponentType.Button> {
/**
* The label to be displayed on the button
*/
label?: string;
/**
* The style of the button
*/
style: Style;
/**
* The emoji to display to the left of the text
*/
emoji?: APIMessageComponentEmoji;
/**
* The status of the button
*/
disabled?: boolean;
}
export interface APIMessageComponentEmoji {
/**
* Emoji id
*/
id?: Snowflake;
/**
* Emoji name
*/
name?: string;
/**
* Whether this emoji is animated
*/
animated?: boolean;
}
export interface APIButtonComponentWithCustomId
extends APIButtonComponentBase<
ButtonStyle.Danger | ButtonStyle.Primary | ButtonStyle.Secondary | ButtonStyle.Success
> {
/**
* The custom_id to be sent in the interaction when clicked
*/
custom_id: string;
}
export interface APIButtonComponentWithURL extends APIButtonComponentBase<ButtonStyle.Link> {
/**
* The URL to direct users to when clicked for Link buttons
*/
url: string;
}
export interface APIButtonComponentWithSKUId
extends Omit<APIButtonComponentBase<ButtonStyle.Premium>, 'custom_id' | 'emoji' | 'label'> {
/**
* The id for a purchasable SKU
*/
sku_id: Snowflake;
}
export type APIButtonComponent =
| APIButtonComponentWithCustomId
| APIButtonComponentWithSKUId
| APIButtonComponentWithURL;
/**
* https://discord.com/developers/docs/interactions/message-components#button-object-button-styles
*/
export enum ButtonStyle {
Primary = 1,
Secondary,
Success,
Danger,
Link,
Premium,
}
/**
* https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles
*/
export enum TextInputStyle {
Short = 1,
Paragraph,
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export interface APIBaseSelectMenuComponent<
T extends
| ComponentType.ChannelSelect
| ComponentType.MentionableSelect
| ComponentType.RoleSelect
| ComponentType.StringSelect
| ComponentType.UserSelect,
> extends APIBaseComponent<T> {
/**
* A developer-defined identifier for the select menu, max 100 characters
*/
custom_id: string;
/**
* Custom placeholder text if nothing is selected, max 150 characters
*/
placeholder?: string;
/**
* The minimum number of items that must be chosen; min 0, max 25
*
* @default 1
*/
min_values?: number;
/**
* The maximum number of items that can be chosen; max 25
*
* @default 1
*/
max_values?: number;
/**
* Disable the select
*
* @default false
*/
disabled?: boolean;
}
export interface APIBaseAutoPopulatedSelectMenuComponent<
T extends
| ComponentType.ChannelSelect
| ComponentType.MentionableSelect
| ComponentType.RoleSelect
| ComponentType.UserSelect,
D extends SelectMenuDefaultValueType,
> extends APIBaseSelectMenuComponent<T> {
/**
* List of default values for auto-populated select menu components
*/
default_values?: APISelectMenuDefaultValue<D>[];
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export interface APIStringSelectComponent extends APIBaseSelectMenuComponent<ComponentType.StringSelect> {
/**
* Specified choices in a select menu; max 25
*/
options: APISelectMenuOption[];
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export type APIUserSelectComponent = APIBaseAutoPopulatedSelectMenuComponent<
ComponentType.UserSelect,
SelectMenuDefaultValueType.User
>;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export type APIRoleSelectComponent = APIBaseAutoPopulatedSelectMenuComponent<
ComponentType.RoleSelect,
SelectMenuDefaultValueType.Role
>;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export type APIMentionableSelectComponent = APIBaseAutoPopulatedSelectMenuComponent<
ComponentType.MentionableSelect,
SelectMenuDefaultValueType.Role | SelectMenuDefaultValueType.User
>;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export interface APIChannelSelectComponent
extends APIBaseAutoPopulatedSelectMenuComponent<ComponentType.ChannelSelect, SelectMenuDefaultValueType.Channel> {
/**
* List of channel types to include in the ChannelSelect component
*/
channel_types?: ChannelType[];
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-default-value-structure
*/
export enum SelectMenuDefaultValueType {
Channel = 'channel',
Role = 'role',
User = 'user',
}
/**
* https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-default-value-structure
*/
export interface APISelectMenuDefaultValue<T extends SelectMenuDefaultValueType> {
type: T;
id: Snowflake;
}
export type APIAutoPopulatedSelectMenuComponent =
| APIChannelSelectComponent
| APIMentionableSelectComponent
| APIRoleSelectComponent
| APIUserSelectComponent;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menus
*/
export type APISelectMenuComponent =
| APIChannelSelectComponent
| APIMentionableSelectComponent
| APIRoleSelectComponent
| APIStringSelectComponent
| APIUserSelectComponent;
/**
* https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure
*/
export interface APISelectMenuOption {
/**
* The user-facing name of the option (max 100 chars)
*/
label: string;
/**
* The dev-defined value of the option (max 100 chars)
*/
value: string;
/**
* An additional description of the option (max 100 chars)
*/
description?: string;
/**
* The emoji to display to the left of the option
*/
emoji?: APIMessageComponentEmoji;
/**
* Whether this option should be already-selected by default
*/
default?: boolean;
}
/**
* https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure
*/
export interface APITextInputComponent extends APIBaseComponent<ComponentType.TextInput> {
/**
* One of text input styles
*/
style: TextInputStyle;
/**
* The custom id for the text input
*/
custom_id: string;
/**
* Text that appears on top of the text input field, max 45 characters
*/
label: string;
/**
* Placeholder for the text input
*/
placeholder?: string;
/**
* The pre-filled text in the text input
*/
value?: string;
/**
* Minimal length of text input
*/
min_length?: number;
/**
* Maximal length of text input
*/
max_length?: number;
/**
* Whether or not this text input is required or not
*/
required?: boolean;
}
/**
* https://discord.com/developers/docs/resources/channel#channel-object-channel-flags
*/
export enum ChannelFlags {
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
GuildFeedRemoved = 1 << 0,
/**
* This thread is pinned to the top of its parent forum channel
*/
Pinned = 1 << 1,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
ActiveChannelsRemoved = 1 << 2,
/**
* Whether a tag is required to be specified when creating a thread in a forum channel.
* Tags are specified in the `applied_tags` field
*/
RequireTag = 1 << 4,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
IsSpam = 1 << 5,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
IsGuildResourceChannel = 1 << 7,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
ClydeAI = 1 << 8,
/**
* @unstable This channel flag is currently not documented by Discord but has a known value which we will try to keep up to date.
*/
IsScheduledForDeletion = 1 << 9,
/**
* Whether media download options are hidden.
*/
HideMediaDownloadOptions = 1 << 15,
}
/**
* https://discord.com/developers/docs/interactions/message-components#message-components
*/
export type APIMessageComponent = APIActionRowComponent<APIMessageActionRowComponent> | APIMessageActionRowComponent;
export type APIModalComponent = APIActionRowComponent<APIModalActionRowComponent> | APIModalActionRowComponent;
export type APIActionRowComponentTypes = APIMessageActionRowComponent | APIModalActionRowComponent;
/**
* https://discord.com/developers/docs/interactions/message-components#message-components
*/
export type APIMessageActionRowComponent = APIButtonComponent | APISelectMenuComponent;
export type APIComponents =
| APIMessageActionRowComponent
| APIModalActionRowComponent
| APIContainerComponent
| APIContainerComponents
| APITopLevelComponent;
// Modal components
export type APIModalActionRowComponent = APITextInputComponent;
/**
* https://discord.com/developers/docs/components/reference#section
*
* A Section is a top-level layout component that allows you to join text contextually with an accessory.
*/
export interface APISectionComponent {
/** 9 for section component */
type: ComponentType.Section;
/** Optional identifier for component */
id?: number;
/** One to three text components */
components: APITextDispalyComponent[];
/** A thumbnail or a button component, with a future possibility of adding more compatible components */
accessory: APIButtonComponent | APIThumbnailComponent;
}
/**
* https://discord.com/developers/docs/components/reference#text-display
*
* A Text Display is a top-level content component that allows you to add text to your message formatted with markdown and mention users and roles. This is similar to the content field of a message, but allows you to add multiple text components, controlling the layout of your message.
* Text Displays are only available in messages.
*/
export interface APITextDispalyComponent {
/** 10 for text display */
type: ComponentType.TextDisplay;
/** Optional identifier for component */
id?: number;
/** Text that will be displayed similar to a message */
content: string;
}
/**
* https://discord.com/developers/docs/components/reference#thumbnail
*
* A Thumbnail is a content component that is a small image only usable as an accessory in a section. The preview comes from an url or attachment through the unfurled media item structure.
* Thumbnails are only available in messages as an accessory in a section.
*/
export interface APIThumbnailComponent {
/** 11 for thumbnail */
type: ComponentType.Thumbnail;
/** Optional identifier for component */
id?: number;
/** A url or attachment */
media: APIUnfurledMediaItem;
/** Alt text for the media */
description?: string;
/** Whether the thumbnail should be a spoiler (or blurred out). Defaults to false */
spoiler?: boolean;
}
/**
* https://discord.com/developers/docs/components/reference#media-gallery
*
* A Media Gallery is a top-level content component that allows you to display 1-10 media attachments in an organized gallery format. Each item can have optional descriptions and can be marked as spoilers.
* Media Galleries are only available in messages.
*/
export interface APIMediaGalleryComponent {
/** 12 for media gallery */
type: ComponentType.MediaGallery;
/** Optional identifier for component */
id?: number;
/** 1 to 10 media gallery items */
items: APIMediaGalleryItems[];
}
export interface APIMediaGalleryItems {
/** A url or attachment */
media: APIUnfurledMediaItem;
/** Alt text for the media */
description?: string;
/** Whether the thumbnail should be a spoiler (or blurred out). Defaults to false */
spoiler?: boolean;
}
/**
* https://discord.com/developers/docs/components/reference#file
*
* A File is a top-level component that allows you to display an uploaded file as an attachment to the message and reference it in the component. Each file component can only display 1 attached file, but you can upload multiple files and add them to different file components within your payload. This is similar to the embeds field of a message but allows you to control the layout of your message by using this anywhere as a component.
* Files are only available in messages.
*/
export interface APIFileComponent {
/** 13 for file */
type: ComponentType.File;
/** Optional identifier for component */
id?: number;
/** This unfurled media item is unique in that it only supports attachment references using the attachment://<filename> syntax */
file: APIUnfurledMediaItem;
/** Whether the media should be a spoiler (or blurred out). Defaults to false */
spoiler?: boolean;
}
/**
* https://discord.com/developers/docs/components/reference#separator
*
* A Separator is a top-level layout component that adds vertical padding and visual division between other components.
*/
export interface APISeparatorComponent {
/** 14 for separator */
type: ComponentType.Separator;
/** Optional identifier for component */
id?: number;
/** Whether a visual divider should be displayed in the component. Defaults to true */
divider?: boolean;
/** Size of separator padding—1 for small padding, 2 for large padding. Defaults to 1 */
spacing?: Spacing;
}
export enum Spacing {
/** For small padding */
Small = 1,
/** For large padding */
Large,
}
export type APIContainerComponents =
| APIActionRowComponent<APIActionRowComponentTypes>
| APITextDispalyComponent
| APISectionComponent
| APIMediaGalleryComponent
| APIFileComponent
| APISeparatorComponent
| APIThumbnailComponent;
/**
* https://discord.com/developers/docs/components/reference#container
*/
export interface APIContainerComponent {
/** 15 for container */
type: ComponentType.Container;
/** Optional identifier for component */
id?: number;
/** Up to 10 components of the type action row, text display, section, media gallery, separator, or file */
components: APIContainerComponents[];
/** Color for the accent on the container as RGB from 0x000000 to 0xFFFFFF */
accent_color?: number;
/** Whether the container should be a spoiler (or blurred out). Defaults to false. */
spoiler?: boolean;
}
/**
* https://discord.com/developers/docs/components/reference#unfurled-media-item-structure
*/
export interface APIUnfurledMediaItem
extends Identify<
MakeRequired<Partial<Pick<APIAttachment, 'url' | 'proxy_url' | 'height' | 'width' | 'content_type'>>, 'url'>
> {}
export type APITopLevelComponent =
| APIContainerComponent
| APIActionRowComponent<APIActionRowComponentTypes>
| APIFileComponent
| APIMediaGalleryComponent
| APISectionComponent
| APISeparatorComponent
| APITextDispalyComponent;

View File

@ -20,6 +20,7 @@ export * from './voice';
export * from './webhook';
export * from './monetization';
export * from './soundboard';
export * from './components';
import type { LocaleString } from '../rest';

View File

@ -1,7 +1,6 @@
import type { ChannelType, OverwriteType, Permissions, Snowflake, VideoQualityMode } from '..';
import type {
APIActionRowComponent,
APIAllowedMentions,
APIAttachment,
APIChannel,
@ -11,10 +10,10 @@ import type {
APIGuildForumDefaultReactionEmoji,
APIGuildForumTag,
APIMessage,
APIMessageActionRowComponent,
APIMessageReference,
APIThreadList,
APIThreadMember,
APITopLevelComponent,
APIUser,
ChannelFlags,
ForumLayoutType,
@ -294,7 +293,7 @@ export interface RESTPostAPIChannelMessageJSONBody {
*
* See https://discord.com/developers/docs/interactions/message-components#component-object
*/
components?: APIActionRowComponent<APIMessageActionRowComponent>[] | undefined;
components?: APITopLevelComponent[] | undefined;
/**
* IDs of up to 3 stickers in the server to send in the message
*
@ -442,7 +441,7 @@ export interface RESTPatchAPIChannelMessageJSONBody {
*
* See https://discord.com/developers/docs/interactions/message-components#component-object
*/
components?: APIActionRowComponent<APIMessageActionRowComponent>[] | null | undefined;
components?: APITopLevelComponent[] | null | undefined;
}
/**

View File

@ -1,10 +1,9 @@
import type { Snowflake } from '..';
import type {
APIActionRowComponent,
APIAllowedMentions,
APIEmbed,
APIMessage,
APIMessageActionRowComponent,
APITopLevelComponent,
APIWebhook,
MessageFlags,
} from '../payloads';
@ -136,7 +135,7 @@ export interface RESTPostAPIWebhookWithTokenJSONBody {
*
* See https://discord.com/developers/docs/interactions/message-components#component-object
*/
components?: APIActionRowComponent<APIMessageActionRowComponent>[] | undefined;
components?: APITopLevelComponent[] | undefined;
/**
* Attachment objects with filename and description
*/
@ -190,6 +189,14 @@ export interface RESTPostAPIWebhookWithTokenQuery {
* Available only if the {@link RESTPostAPIWebhookWithTokenJSONBody.thread_name} JSON body property is not specified
*/
thread_id?: Snowflake;
/**
* Whether to respect the components field of the request. When enabled, allows application-owned webhooks to use all components and
* non-owned webhooks to use non-interactive components.
*
* @default false
*/
with_components?: boolean;
}
/**
@ -208,7 +215,7 @@ export type RESTPostAPIWebhookWithTokenWaitResult = APIMessage;
/**
* https://discord.com/developers/docs/resources/webhook#execute-slackcompatible-webhook-query-string-params
*/
export type RESTPostAPIWebhookWithTokenSlackQuery = RESTPostAPIWebhookWithTokenQuery;
export type RESTPostAPIWebhookWithTokenSlackQuery = Omit<RESTPostAPIWebhookWithTokenQuery, 'with_components'>;
/**
* https://discord.com/developers/docs/resources/webhook#execute-slackcompatible-webhook
@ -226,7 +233,7 @@ export type RESTPostAPIWebhookWithTokenSlackWaitResult = APIMessage;
/**
* https://discord.com/developers/docs/resources/webhook#execute-githubcompatible-webhook-query-string-params
*/
export type RESTPostAPIWebhookWithTokenGitHubQuery = RESTPostAPIWebhookWithTokenQuery;
export type RESTPostAPIWebhookWithTokenGitHubQuery = Omit<RESTPostAPIWebhookWithTokenQuery, 'with_components'>;
/**
* https://discord.com/developers/docs/resources/webhook#execute-githubcompatible-webhook
@ -269,6 +276,7 @@ export type RESTPatchAPIWebhookWithTokenMessageJSONBody = AddUndefinedToPossibly
attachments?: RESTAPIAttachment[] | undefined;
};
export type RESTPatchAPIWebhookWithTokenMessageQuery = Omit<RESTPostAPIWebhookWithTokenQuery, 'wait'>;
/**
* https://discord.com/developers/docs/resources/webhook#edit-webhook-message
*/