feat: Entry Points (#256)

* feat: entry points types

* fix: update attachment

* feat: more types and command

* chore: apply formatting

* feat: entry interaction

* chore: apply formatting

* feat: entry commands

* feat: end

* fix: build

* fix: typing

* fix: entry point in handler

* fix: build

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Marcos Susaña 2024-08-27 21:18:16 -04:00 committed by GitHub
parent fc4c7ef3da
commit a9d14c4c01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 594 additions and 110 deletions

View File

@ -28,6 +28,3 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: npx tsc

View File

@ -1,11 +1,21 @@
import type { RESTPostAPIInteractionCallbackJSONBody } from '../../types';
import type {
RESTPostAPIInteractionCallbackJSONBody,
RESTPostAPIInteractionCallbackQuery,
RESTPostAPIInteractionCallbackResult,
} from '../../types';
import type { ProxyRequestMethod } from '../Router';
import type { RestArguments } from '../api';
export interface InteractionRoutes {
interactions: (id: string) => (token: string) => {
callback: {
post(args: RestArguments<ProxyRequestMethod.Post, RESTPostAPIInteractionCallbackJSONBody>): Promise<never>;
post(
args: RestArguments<
ProxyRequestMethod.Post,
RESTPostAPIInteractionCallbackJSONBody,
RESTPostAPIInteractionCallbackQuery
>,
): Promise<RESTPostAPIInteractionCallbackResult | undefined>;
};
};
}

View File

@ -347,9 +347,9 @@ export class ApiHandler {
const fileKey = file.key ?? `files[${index}]`;
if (isBufferLike(file.data)) {
formData.append(fileKey, new Blob([file.data], { type: file.contentType }), file.name);
formData.append(fileKey, new Blob([file.data], { type: file.contentType }), file.filename);
} else {
formData.append(fileKey, new Blob([`${file.data}`], { type: file.contentType }), file.name);
formData.append(fileKey, new Blob([`${file.data}`], { type: file.contentType }), file.filename);
}
}

View File

@ -24,7 +24,7 @@ export interface RawFile {
contentType?: string;
data: Buffer | Uint8Array | boolean | number | string;
key?: string;
name: string;
filename: string;
}
export interface ApiRequestOptions {

View File

@ -17,7 +17,7 @@ export type AttachmentResolvable =
| Attachment;
export type AttachmentDataType = keyof AttachmentResolvableMap;
export interface AttachmentData {
name: string;
filename: string;
description: string;
resolvable: AttachmentResolvable;
type: AttachmentDataType;
@ -40,7 +40,7 @@ export class AttachmentBuilder {
* @param data - The partial attachment data.
*/
constructor(
public data: Partial<AttachmentData> = { name: `${randomBytes?.(8)?.toString('base64url') || 'default'}.jpg` },
public data: Partial<AttachmentData> = { filename: `${randomBytes?.(8)?.toString('base64url') || 'default'}.jpg` },
) {}
/**
@ -51,7 +51,7 @@ export class AttachmentBuilder {
* attachment.setName('example.jpg');
*/
setName(name: string): this {
this.data.name = name;
this.data.filename = name;
return this;
}
@ -93,10 +93,10 @@ export class AttachmentBuilder {
setSpoiler(spoiler: boolean): this {
if (spoiler === this.spoiler) return this;
if (!spoiler) {
this.data.name = this.data.name!.slice('SPOILER_'.length);
this.data.filename = this.data.filename!.slice('SPOILER_'.length);
return this;
}
this.data.name = `SPOILER_${this.data.name}`;
this.data.filename = `SPOILER_${this.data.filename}`;
return this;
}
@ -104,7 +104,7 @@ export class AttachmentBuilder {
* Gets whether the attachment is a spoiler.
*/
get spoiler(): boolean {
return this.data.name?.startsWith('SPOILER_') ?? false;
return this.data.filename?.startsWith('SPOILER_') ?? false;
}
/**
@ -127,11 +127,11 @@ export function resolveAttachment(
if ('id' in resolve) return resolve;
if (resolve instanceof AttachmentBuilder) {
const data = resolve.toJSON();
return { filename: data.name, description: data.description };
const { filename, description } = resolve.toJSON();
return { filename, description };
}
return { filename: resolve.name, description: resolve.description };
return { filename: resolve.filename, description: resolve.description };
}
/**
@ -143,9 +143,9 @@ export async function resolveFiles(resources: (AttachmentBuilder | RawFile | Att
const data = await Promise.all(
resources.map(async (resource, i) => {
if (resource instanceof AttachmentBuilder) {
const { type, resolvable, name } = resource.toJSON();
const { type, resolvable, filename } = resource.toJSON();
const resolve = await resolveAttachmentData(resolvable, type);
return { ...resolve, key: `files[${i}]`, name } as RawFile;
return { ...resolve, key: `files[${i}]`, filename } as RawFile;
}
if (resource instanceof Attachment) {
const resolve = await resolveAttachmentData(resource.url, 'url');
@ -153,14 +153,14 @@ export async function resolveFiles(resources: (AttachmentBuilder | RawFile | Att
data: resolve.data,
contentType: resolve.contentType,
key: `files[${i}]`,
name: resource.filename,
filename: resource.filename,
} as RawFile;
}
return {
data: resource.data,
contentType: resource.contentType,
key: `files[${i}]`,
name: resource.name,
filename: resource.filename,
} as RawFile;
}),
);

View File

@ -43,6 +43,7 @@ import { LangsHandler } from '../langs/handler';
import type {
ChatInputCommandInteraction,
ComponentInteraction,
EntryPointInteraction,
MessageCommandInteraction,
ModalSubmitInteraction,
UserCommandInteraction,
@ -320,6 +321,11 @@ export class BaseClient {
const commands = this.commands!.values;
const filter = filterSplit(commands, command => !command.guildId);
if (this.commands?.entryPoint) {
// @ts-expect-error
filter.expect.push(this.commands.entryPoint);
}
if (!cachePath || (await this.shouldUploadCommands(cachePath)))
await this.proxy.applications(applicationId).commands.put({
body: filter.expect
@ -419,6 +425,7 @@ export interface BaseClientOptions {
| MessageCommandInteraction<boolean>
| ComponentInteraction
| ModalSubmitInteraction
| EntryPointInteraction<boolean>
| When<InferWithPrefix, MessageStructure, never>,
) => {};
globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[];

View File

@ -28,9 +28,9 @@ export class HttpClient extends BaseClient {
for (const [index, file] of files.entries()) {
const fileKey = file.key ?? `files[${index}]`;
if (isBufferLike(file.data)) {
response.append(fileKey, new Blob([file.data], { type: file.contentType }), file.name);
response.append(fileKey, new Blob([file.data], { type: file.contentType }), file.filename);
} else {
response.append(fileKey, new Blob([`${file.data}`], { type: file.contentType }), file.name);
response.append(fileKey, new Blob([`${file.data}`], { type: file.contentType }), file.filename);
}
}
if (body) {

View File

@ -10,6 +10,7 @@ import {
} from '../../types';
import type {
ComponentContext,
EntryPointContext,
MenuCommandContext,
ModalContext,
PermissionStrings,
@ -201,7 +202,7 @@ export class BaseCommand {
/** @internal */
static __runMiddlewares(
context: CommandContext<{}, never> | ComponentContext | MenuCommandContext<any> | ModalContext,
context: CommandContext<{}, never> | ComponentContext | MenuCommandContext<any> | ModalContext | EntryPointContext,
middlewares: (keyof RegisteredMiddlewares)[],
global: boolean,
): Promise<{ error?: string; pass?: boolean }> {

View File

@ -0,0 +1,71 @@
import { magicImport, type PermissionStrings } from '../../common';
import {
ApplicationCommandType,
type EntryPointCommandHandlerType,
type ApplicationIntegrationType,
type InteractionContextType,
type LocaleString,
} from '../../types';
import type { RegisteredMiddlewares } from '../decorators';
import type { EntryPointContext } from './entrycontext';
import type { ExtraProps, UsingClient } from './shared';
export abstract class EntryPointCommand {
middlewares: (keyof RegisteredMiddlewares)[] = [];
__filePath?: string;
__t?: { name: string | undefined; description: string | undefined };
name!: string;
type = ApplicationCommandType.PrimaryEntryPoint;
nsfw?: boolean;
integrationTypes: ApplicationIntegrationType[] = [];
contexts: InteractionContextType[] = [];
description!: string;
botPermissions?: bigint;
dm?: boolean;
handler!: EntryPointCommandHandlerType;
name_localizations?: Partial<Record<LocaleString, string>>;
description_localizations?: Partial<Record<LocaleString, string>>;
props: ExtraProps = {};
toJSON() {
return {
handler: this.handler,
name: this.name,
type: this.type,
nsfw: this.nsfw,
default_member_permissions: null,
guild_id: null,
description: this.description,
name_localizations: this.name_localizations,
description_localizations: this.description_localizations,
dm_permission: this.dm,
contexts: this.contexts,
integration_types: this.integrationTypes,
};
}
async reload() {
delete require.cache[this.__filePath!];
const __tempCommand = await magicImport(this.__filePath!).then(x => x.default ?? x);
Object.setPrototypeOf(this, __tempCommand.prototype);
}
abstract run?(context: EntryPointContext): any;
onAfterRun?(context: EntryPointContext, error: unknown | undefined): any;
onRunError(context: EntryPointContext<never>, error: unknown): any {
context.client.logger.fatal(`${this.name}.<onRunError>`, context.author.id, error);
}
onMiddlewaresError(context: EntryPointContext<never>, error: string): any {
context.client.logger.fatal(`${this.name}.<onMiddlewaresError>`, context.author.id, error);
}
onBotPermissionsFail(context: EntryPointContext<never>, permissions: PermissionStrings): any {
context.client.logger.fatal(`${this.name}.<onBotPermissionsFail>`, context.author.id, permissions);
}
onInternalError(client: UsingClient, command: EntryPointCommand, error?: unknown): any {
client.logger.fatal(command.name, error);
}
}

View File

@ -0,0 +1,130 @@
import { MessageFlags } from '../../types';
import type { ReturnCache } from '../..';
import type {
InteractionCreateBodyRequest,
InteractionMessageUpdateBodyRequest,
ModalCreateBodyRequest,
UnionToTuple,
When,
} from '../../common';
import type { AllChannels, EntryPointInteraction } from '../../structures';
import { BaseContext } from '../basecontext';
import type { RegisteredMiddlewares } from '../decorators';
import type { CommandMetadata, ExtendContext, GlobalMetadata, UsingClient } from './shared';
import type {
GuildMemberStructure,
GuildStructure,
MessageStructure,
WebhookMessageStructure,
} from '../../client/transformers';
import type { EntryPointCommand } from './entryPoint';
export interface EntryPointContext<M extends keyof RegisteredMiddlewares = never> extends BaseContext, ExtendContext {}
export class EntryPointContext<M extends keyof RegisteredMiddlewares = never> extends BaseContext {
constructor(
readonly client: UsingClient,
readonly interaction: EntryPointInteraction,
readonly shardId: number,
readonly command: EntryPointCommand,
) {
super(client);
}
metadata: CommandMetadata<UnionToTuple<M>> = {} as never;
globalMetadata: GlobalMetadata = {};
get t() {
return this.client.t(this.interaction.locale ?? this.client.langs!.defaultLang ?? 'en-US');
}
get fullCommandName() {
return this.command.name;
}
write<FR extends boolean = false>(
body: InteractionCreateBodyRequest,
fetchReply?: FR,
): Promise<When<FR, WebhookMessageStructure, void | WebhookMessageStructure>> {
return this.interaction.write(body, fetchReply);
}
modal(body: ModalCreateBodyRequest) {
return this.interaction.modal(body);
}
deferReply(ephemeral = false) {
return this.interaction.deferReply(ephemeral ? MessageFlags.Ephemeral : undefined);
}
editResponse(body: InteractionMessageUpdateBodyRequest) {
return this.interaction.editResponse(body);
}
deleteResponse() {
return this.interaction.deleteResponse();
}
editOrReply<FR extends boolean = false>(
body: InteractionCreateBodyRequest | InteractionMessageUpdateBodyRequest,
fetchReply?: FR,
): Promise<When<FR, WebhookMessageStructure | MessageStructure, void | WebhookMessageStructure | MessageStructure>> {
return this.interaction.editOrReply(body as InteractionCreateBodyRequest, fetchReply);
}
fetchResponse() {
return this.interaction.fetchResponse();
}
channel(mode?: 'rest' | 'flow'): Promise<AllChannels>;
channel(mode?: 'cache'): ReturnCache<AllChannels>;
channel(mode: 'cache' | 'rest' | 'flow' = 'cache') {
if (this.interaction?.channel && mode === 'cache')
return this.client.cache.adapter.isAsync ? Promise.resolve(this.interaction.channel) : this.interaction.channel;
return this.client.channels.fetch(this.channelId, mode === 'rest');
}
me(mode?: 'rest' | 'flow'): Promise<GuildMemberStructure>;
me(mode?: 'cache'): ReturnCache<GuildMemberStructure | undefined>;
me(mode: 'cache' | 'rest' | 'flow' = 'cache') {
if (!this.guildId)
return mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve();
switch (mode) {
case 'cache':
return this.client.cache.members?.get(this.client.botId, this.guildId);
default:
return this.client.members.fetch(this.guildId, this.client.botId, mode === 'rest');
}
}
guild(mode?: 'rest' | 'flow'): Promise<GuildStructure<'cached' | 'api'> | undefined>;
guild(mode?: 'cache'): ReturnCache<GuildStructure<'cached'> | undefined>;
guild(mode: 'cache' | 'rest' | 'flow' = 'cache') {
if (!this.guildId)
return (
mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve()
) as any;
switch (mode) {
case 'cache':
return this.client.cache.guilds?.get(this.guildId);
default:
return this.client.guilds.fetch(this.guildId, mode === 'rest');
}
}
get guildId() {
return this.interaction.guildId;
}
get channelId() {
return this.interaction.channelId!;
}
get author() {
return this.interaction.user;
}
get member() {
return this.interaction.member;
}
}

View File

@ -1,6 +1,7 @@
import {
ApplicationCommandType,
ApplicationIntegrationType,
type EntryPointCommandHandlerType,
InteractionContextType,
PermissionFlagsBits,
type LocaleString,
@ -11,8 +12,16 @@ import type { DefaultLocale, ExtraProps, IgnoreCommand, MiddlewareContext } from
export interface RegisteredMiddlewares {}
type DeclareOptions =
| {
export type CommandDeclareOptions =
| DecoratorDeclareOptions
| (Omit<DecoratorDeclareOptions, 'description'> & {
type: ApplicationCommandType.User | ApplicationCommandType.Message;
})
| (Omit<DecoratorDeclareOptions, 'ignore' | 'aliases' | 'guildId'> & {
type: ApplicationCommandType.PrimaryEntryPoint;
handler: EntryPointCommandHandlerType;
});
export interface DecoratorDeclareOptions {
name: string;
description: string;
botPermissions?: PermissionStrings | bigint;
@ -25,22 +34,6 @@ type DeclareOptions =
aliases?: string[];
props?: ExtraProps;
}
| (Omit<
{
name: string;
description: string;
botPermissions?: PermissionStrings | bigint;
defaultMemberPermissions?: PermissionStrings | bigint;
guildId?: string[];
nsfw?: boolean;
integrationTypes?: (keyof typeof ApplicationIntegrationType)[];
contexts?: (keyof typeof InteractionContextType)[];
props?: ExtraProps;
},
'type' | 'description'
> & {
type: ApplicationCommandType.User | ApplicationCommandType.Message;
});
export function Locales({
name: names,
@ -154,7 +147,7 @@ export function Middlewares(cbs: readonly (keyof RegisteredMiddlewares)[]) {
};
}
export function Declare(declare: DeclareOptions) {
export function Declare(declare: CommandDeclareOptions) {
return <T extends { new (...args: any[]): {} }>(target: T) =>
class extends target {
name = declare.name;
@ -177,6 +170,7 @@ export function Declare(declare: DeclareOptions) {
guildId?: string[];
ignore?: IgnoreCommand;
aliases?: string[];
handler?: EntryPointCommandHandlerType;
constructor(...args: any[]) {
super(...args);
if ('description' in declare) this.description = declare.description;
@ -184,6 +178,7 @@ export function Declare(declare: DeclareOptions) {
if ('guildId' in declare) this.guildId = declare.guildId;
if ('ignore' in declare) this.ignore = declare.ignore;
if ('aliases' in declare) this.aliases = declare.aliases;
if ('handler' in declare) this.handler = declare.handler;
// check if all properties are valid
}
};

View File

@ -28,6 +28,8 @@ import {
type SeyfertIntegerOption,
type SeyfertNumberOption,
type SeyfertStringOption,
EntryPointContext,
type EntryPointCommand,
} from '.';
import {
AutocompleteInteraction,
@ -38,6 +40,7 @@ import {
type MessageCommandInteraction,
type UserCommandInteraction,
type __InternalReplyFunction,
type EntryPointInteraction,
} from '../structures';
import type { PermissionsBitField } from '../structures/extra/Permissions';
import { ComponentContext, ModalContext } from '../components';
@ -132,6 +135,34 @@ export class HandleCommand {
}
}
async entryPoint(command: EntryPointCommand, interaction: EntryPointInteraction, context: EntryPointContext) {
if (command.botPermissions && interaction.appPermissions) {
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 {
try {
await command.run!(context);
await command.onAfterRun?.(context, undefined);
} catch (error) {
await command.onRunError(context, error);
await command.onAfterRun?.(context, error);
}
} catch (error) {
try {
await command.onInternalError(this.client, command, error);
} catch {
// pass
}
}
}
async chatInput(
command: Command | SubCommand,
interaction: ChatInputCommandInteraction,
@ -214,6 +245,16 @@ export class HandleCommand {
this.contextMenuUser(data.command, data.interaction, data.context);
break;
}
case ApplicationCommandType.PrimaryEntryPoint: {
const command = this.client.commands?.entryPoint;
if (!command?.run) return;
const interaction = BaseInteraction.from(this.client, body, __reply) as EntryPointInteraction;
const context = new EntryPointContext(this.client, interaction, shardId, command);
const extendContext = this.client.options?.context?.(interaction) ?? {};
Object.assign(context, extendContext);
await this.entryPoint(command, interaction, context);
break;
}
case ApplicationCommandType.ChatInput: {
const parentCommand = this.getCommand<Command>(body.data);
const optionsResolver = this.makeResolver(
@ -442,7 +483,7 @@ export class HandleCommand {
);
}
getCommand<T extends Command | ContextMenuCommand>(data: {
getCommand<T extends Command | ContextMenuCommand | EntryPointCommand>(data: {
guild_id?: string;
name: string;
}): T | undefined {
@ -487,8 +528,8 @@ export class HandleCommand {
}
async runGlobalMiddlewares(
command: Command | ContextMenuCommand | SubCommand,
context: CommandContext<{}, never> | MenuCommandContext<any>,
command: Command | ContextMenuCommand | SubCommand | EntryPointCommand,
context: CommandContext<{}, never> | MenuCommandContext<any> | EntryPointContext,
) {
try {
const resultRunGlobalMiddlewares = await BaseCommand.__runMiddlewares(
@ -513,8 +554,8 @@ export class HandleCommand {
}
async runMiddlewares(
command: Command | ContextMenuCommand | SubCommand,
context: CommandContext<{}, never> | MenuCommandContext<any>,
command: Command | ContextMenuCommand | SubCommand | EntryPointCommand,
context: CommandContext<{}, never> | MenuCommandContext<any> | EntryPointContext,
) {
try {
const resultRunMiddlewares = await BaseCommand.__runMiddlewares(

View File

@ -17,9 +17,11 @@ import { Command, type CommandOption, SubCommand } from './applications/chat';
import { ContextMenuCommand } from './applications/menu';
import type { UsingClient } from './applications/shared';
import { promises } from 'node:fs';
import type { EntryPointCommand } from '.';
export class CommandHandler extends BaseHandler {
values: (Command | ContextMenuCommand)[] = [];
entryPoint: EntryPointCommand | null = null;
protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts'));
@ -290,15 +292,17 @@ export class CommandHandler extends BaseHandler {
}
}
this.stablishContextCommandDefaults(commandInstance);
this.values.push(commandInstance);
this.parseLocales(commandInstance);
if ('handler' in commandInstance) {
this.entryPoint = commandInstance as EntryPointCommand;
} else this.values.push(commandInstance);
}
}
return this.values;
}
parseLocales(command: Command | SubCommand | ContextMenuCommand) {
parseLocales(command: InstanceType<HandleableCommand>) {
this.parseGlobalLocales(command);
if (command instanceof ContextMenuCommand) {
this.parseContextMenuLocales(command);
@ -322,7 +326,7 @@ export class CommandHandler extends BaseHandler {
return command;
}
parseGlobalLocales(command: Command | SubCommand | ContextMenuCommand) {
parseGlobalLocales(command: InstanceType<HandleableCommand>) {
if (command.__t) {
command.name_localizations = {};
command.description_localizations = {};
@ -488,7 +492,7 @@ export class CommandHandler extends BaseHandler {
return file.default ? [file.default] : undefined;
}
onCommand(file: HandleableCommand): Command | SubCommand | ContextMenuCommand | false {
onCommand(file: HandleableCommand): InstanceType<HandleableCommand> | false {
return new file();
}
@ -501,6 +505,6 @@ export type FileLoaded<T = null> = {
default?: NulleableCoalising<T, HandleableCommand>;
} & Record<string, NulleableCoalising<T, HandleableCommand>>;
export type HandleableCommand = new () => Command | SubCommand | ContextMenuCommand;
export type HandleableCommand = new () => Command | SubCommand | ContextMenuCommand | EntryPointCommand;
export type SeteableCommand = new () => Extract<InstanceType<HandleableCommand>, SubCommand>;
export type HandleableSubCommand = new () => SubCommand;

View File

@ -5,5 +5,7 @@ export * from './applications/chatcontext';
export * from './applications/menu';
export * from './applications/menucontext';
export * from './applications/options';
export * from './applications/entryPoint';
export * from './applications/entrycontext';
export * from './decorators';
export * from './optionresolver';

View File

@ -36,23 +36,28 @@ import {
type MessageFlags,
type RESTPostAPIInteractionCallbackJSONBody,
type RESTAPIAttachment,
type APIEntryPointCommandInteraction,
type InteractionCallbackData,
type InteractionCallbackResourceActivity,
type RESTPostAPIInteractionCallbackResult,
} from '../types';
import type { RawFile } from '../api';
import { ActionRow, Embed, Modal, PollBuilder, resolveAttachment, resolveFiles } from '../builders';
import type { ContextOptionsResolved, UsingClient } from '../commands';
import type {
ObjectToLower,
OmitInsert,
ToClass,
When,
ComponentInteractionMessageUpdate,
InteractionCreateBodyRequest,
InteractionMessageUpdateBodyRequest,
MessageCreateBodyRequest,
MessageUpdateBodyRequest,
MessageWebhookCreateBodyRequest,
ModalCreateBodyRequest,
import {
type ObjectToLower,
type OmitInsert,
type ToClass,
type When,
type ComponentInteractionMessageUpdate,
type InteractionCreateBodyRequest,
type InteractionMessageUpdateBodyRequest,
type MessageCreateBodyRequest,
type MessageUpdateBodyRequest,
type MessageWebhookCreateBodyRequest,
type ModalCreateBodyRequest,
toCamelCase,
} from '../common';
import { channelFrom, type AllChannels } from './';
import { DiscordBase } from './extra/DiscordBase';
@ -75,6 +80,7 @@ export type ReplyInteractionBody =
type: InteractionResponseType.ChannelMessageWithSource | InteractionResponseType.UpdateMessage;
data: InteractionCreateBodyRequest | InteractionMessageUpdateBodyRequest | ComponentInteractionMessageUpdate;
}
| { type: InteractionResponseType.LaunchActivity }
| Exclude<RESTPostAPIInteractionCallbackJSONBody, APIInteractionResponsePong>;
export type __InternalReplyFunction = (_: { body: APIInteractionResponse; files?: RawFile[] }) => Promise<any>;
@ -161,6 +167,8 @@ export class BaseInteraction<
: [],
},
};
case InteractionResponseType.LaunchActivity:
return body;
default:
return body;
}
@ -168,6 +176,7 @@ export class BaseInteraction<
static transformBody<T>(
body:
| InteractionCreateBodyRequest
| InteractionMessageUpdateBodyRequest
| MessageUpdateBodyRequest
| MessageCreateBodyRequest
@ -192,9 +201,9 @@ export class BaseInteraction<
...resolveAttachment(x),
})) ?? undefined;
} else if (files?.length) {
payload.attachments = files?.map((x, id) => ({
payload.attachments = files?.map(({ filename }, id) => ({
id,
filename: x.name,
filename,
})) as RESTAPIAttachment[];
}
return payload as T;
@ -279,6 +288,10 @@ export class BaseInteraction<
return false;
}
isEntryPoint(): this is EntryPointInteraction {
return false;
}
static from(client: UsingClient, gateway: GatewayInteractionCreateDispatchData, __reply?: __InternalReplyFunction) {
switch (gateway.type) {
case InteractionType.ApplicationCommandAutocomplete:
@ -296,6 +309,8 @@ export class BaseInteraction<
return new UserCommandInteraction(client, gateway as APIUserApplicationCommandInteraction, __reply);
case ApplicationCommandType.Message:
return new MessageCommandInteraction(client, gateway as APIMessageApplicationCommandInteraction, __reply);
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:
@ -345,6 +360,7 @@ export type AllInteractions =
| ComponentInteraction
| SelectMenuInteraction
| ModalSubmitInteraction
| EntryPointInteraction
| BaseInteraction;
export interface AutocompleteInteraction
@ -478,6 +494,64 @@ export class ApplicationCommandInteraction<
}
}
/**
* Seyfert don't support activities, so this interaction is blank
*/
export class EntryPointInteraction<FromGuild extends boolean = boolean> extends ApplicationCommandInteraction<
FromGuild,
APIEntryPointCommandInteraction
> {
async withReponse(data?: InteractionCreateBodyRequest) {
let body = { type: InteractionResponseType.LaunchActivity } as const;
if (data) {
let { files, ...rest } = data;
files = files ? await resolveFiles(files) : undefined;
body = BaseInteraction.transformBody(rest, files, this.client);
}
const response = (await this.client.proxy
.interactions(this.id)(this.token)
.callback.post({
body,
query: { with_response: true },
})) as RESTPostAPIInteractionCallbackResult;
const result: Partial<EntryPointWithResponseResult> = {
interaction: toCamelCase(response.interaction),
};
if (response.resource) {
if (response.resource.type !== InteractionResponseType.LaunchActivity) {
result.resource = {
type: response.resource.type,
message: Transformers.WebhookMessage(this.client, response.resource.message as any, this.id, this.token),
};
} else {
result.resource = {
type: response.resource.type,
activityInstance: response.resource.activity_instance!,
};
}
}
return result as EntryPointWithResponseResult;
}
isEntryPoint(): this is EntryPointInteraction {
return true;
}
}
export interface EntryPointWithResponseResult {
interaction: ObjectToLower<InteractionCallbackData>;
resource?:
| { type: InteractionResponseType.LaunchActivity; activityInstance: InteractionCallbackResourceActivity }
| {
type: Exclude<InteractionResponseType, InteractionResponseType.LaunchActivity>;
message: WebhookMessageStructure;
};
}
export interface ComponentInteraction
extends ObjectToLower<
Omit<

View File

@ -294,9 +294,9 @@ export class MessagesMethods extends DiscordBase {
...resolveAttachment(x),
})) ?? undefined;
} else if (files?.length) {
payload.attachments = files?.map((x, id) => ({
payload.attachments = files?.map(({ filename }, id) => ({
id,
filename: x.name,
filename,
})) as RESTAPIAttachment[];
}
return payload as T;

View File

@ -1,5 +1,9 @@
import type { APIInteractionDataResolved } from '../../index';
import type { APIApplicationCommandInteractionWrapper, ApplicationCommandType } from '../applicationCommands';
import type {
APIApplicationCommandInteractionWrapper,
APIEntryPointInteractionData,
ApplicationCommandType,
} from '../applicationCommands';
import type { APIDMInteractionWrapper, APIGuildInteractionWrapper } from '../base';
import type {
APIApplicationCommandAttachmentOption,
@ -128,3 +132,9 @@ export type APIChatInputApplicationCommandDMInteraction =
*/
export type APIChatInputApplicationCommandGuildInteraction =
APIGuildInteractionWrapper<APIChatInputApplicationCommandInteraction>;
/**
* Documentation goes brrrrrr
* @unstable
*/
export type APIEntryPointCommandInteraction = APIApplicationCommandInteractionWrapper<APIEntryPointInteractionData>;

View File

@ -5,6 +5,7 @@ import type {
APIChatInputApplicationCommandGuildInteraction,
APIChatInputApplicationCommandInteraction,
APIChatInputApplicationCommandInteractionData,
APIEntryPointCommandInteraction,
} from './_applicationCommands/chatInput';
import type {
APIContextMenuDMInteraction,
@ -12,6 +13,7 @@ import type {
APIContextMenuInteraction,
APIContextMenuInteractionData,
} from './_applicationCommands/contextMenu';
import type { APIBaseApplicationCommandInteractionData } from './_applicationCommands/internals';
import type { APIBaseInteraction } from './base';
import type { InteractionType } from './responses';
@ -92,28 +94,58 @@ export interface APIApplicationCommand {
/**
* Installation context(s) where the command is available, only for globally-scoped commands. Defaults to `GUILD_INSTALL ([0])`
*
* @unstable
*/
integration_types?: ApplicationIntegrationType[];
/**
* Interaction context(s) where the command can be used, only for globally-scoped commands. By default, all interaction context types included for new commands `[0,1,2]`.
*
* @unstable
*/
contexts?: InteractionContextType[] | null;
/**
* Autoincrementing version identifier updated during substantial record changes
*/
version: Snowflake;
/**
* Determines whether the interaction is handled by the app's interactions handler or by Discord
*/
handler?: EntryPointCommandHandlerType;
}
/**
* https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types
*/
export enum ApplicationCommandType {
/**
* Slash commands; a text-based command that shows up when a user types /
*/
ChatInput = 1,
/**
* A UI-based command that shows up when you right click or tap on a user
*/
User,
/**
* A UI-based command that shows up when you right click or tap on a message
*/
Message,
/**
* A UI-based command that represents the primary way to invoke an app's Activity
*/
PrimaryEntryPoint,
}
/**
* https://discord.com/developers/docs/interactions/application-commands#application-command-object-entry-point-command-handler-types
*/
export enum EntryPointCommandHandlerType {
/**
* The app handles the interaction using an interaction token
*/
AppHandler = 1,
/**
* Discord handles the interaction by launching an Activity and sending a follow-up message without coordinating with the app
*/
DiscordLaunchActivity,
}
/**
@ -148,12 +180,20 @@ export enum InteractionContextType {
PrivateChannel = 2,
}
/**
* Documentation goes brrrrrr
* @unstable
*/
export interface APIEntryPointInteractionData
extends Omit<APIBaseApplicationCommandInteractionData<ApplicationCommandType.PrimaryEntryPoint>, 'guild_id'> {}
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-data
*/
export type APIApplicationCommandInteractionData =
| APIChatInputApplicationCommandInteractionData
| APIContextMenuInteractionData;
| APIContextMenuInteractionData
| APIEntryPointInteractionData;
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object
@ -170,7 +210,10 @@ export type APIApplicationCommandInteractionWrapper<Data extends APIApplicationC
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object
*/
export type APIApplicationCommandInteraction = APIChatInputApplicationCommandInteraction | APIContextMenuInteraction;
export type APIApplicationCommandInteraction =
| APIChatInputApplicationCommandInteraction
| APIContextMenuInteraction
| APIEntryPointCommandInteraction;
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object

View File

@ -1,3 +1,4 @@
import type { MakeRequired } from '../../../common';
import type { RESTPostAPIWebhookWithTokenJSONBody } from '../../index';
import type { APIActionRowComponent, APIModalActionRowComponent } from '../channel';
import type { MessageFlags } from '../index';
@ -25,6 +26,7 @@ export type APIInteractionResponse =
| APIInteractionResponsePong
| APIInteractionResponseUpdateMessage
| APIModalInteractionResponse
| APIInteractionResponseLaunchActivity
| APIPremiumRequiredInteractionResponse;
export interface APIInteractionResponsePong {
@ -64,6 +66,10 @@ export interface APIInteractionResponseUpdateMessage {
data?: APIInteractionResponseCallbackData;
}
export interface APIInteractionResponseLaunchActivity {
type: InteractionResponseType.LaunchActivity;
}
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-interaction-callback-type
*/
@ -102,6 +108,10 @@ export enum InteractionResponseType {
* @deprecated See https://discord.com/developers/docs/change-log#premium-apps-new-premium-button-style-deep-linking-url-schemes
*/
PremiumRequired,
/**
* Launch the Activity associated with the app. Only available for apps with Activities enabled
*/
LaunchActivity = 12,
}
/**
@ -133,3 +143,71 @@ export interface APIModalInteractionResponseCallbackData {
*/
components: APIActionRowComponent<APIModalActionRowComponent>[];
}
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback-interaction-callback-object
*/
export interface InteractionCallbackData<T extends InteractionType = InteractionType> {
id: string;
type: T;
/**
* Instance ID of the Activity if one was launched or joined
*/
activity_instance_id?: string;
/**
* ID of the message that was created by the interaction
*/
response_message_id?: string;
/**
* Whether or not the message is in a loading state
*/
response_message_loading?: boolean;
/**
* Whether or not the response message was ephemeral
*/
response_message_ephemeral?: boolean;
}
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback-interaction-callback-resource-object
*/
export interface InteractionCallbackResourceActivity {
/**
* Instance ID of the Activity if one was launched or joined.
*/
id: string;
}
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback-interaction-callback-activity-instance-resource
*/
export interface InteractionCallbackResource<T extends InteractionResponseType = InteractionResponseType> {
type: T;
/**
* Represents the Activity launched by this interaction.
*/
activity_instance?: InteractionCallbackResourceActivity;
/**
* Message created by the interaction.
*/
message?: Omit<RESTPostAPIWebhookWithTokenJSONBody, 'avatar_url' | 'username'> & { flags?: MessageFlags };
}
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback-interaction-callback-response-object
*/
export interface InteractionCallbackResponse {
interaction: InteractionCallbackData;
resource?: InteractionCallbackResource;
}
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback-interaction-callback-response-object
*/
export type APIInteractionCallbackLaunchActivity = InteractionCallbackResponse & {
resource?: Omit<MakeRequired<InteractionCallbackResource, 'activity_instance'>, 'message'>;
};
export type APIInteractionCallbackMessage = InteractionCallbackResponse & {
resource?: Omit<MakeRequired<InteractionCallbackResource, 'message'>, 'activity_instance'>;
};

View File

@ -22,6 +22,7 @@ import type {
ThreadChannelType,
APIThreadMember,
APIThreadList,
APIAttachment,
} from '../payloads';
import type { AddUndefinedToPossiblyUndefinedPropertiesOfInterface, StrictPartial } from '../utils';
import type { RESTAPIPollCreate } from './poll';
@ -248,22 +249,11 @@ export type APIMessageReferenceSend = AddUndefinedToPossiblyUndefinedPropertiesO
};
/**
* https://discord.com/developers/docs/resources/channel#attachment-object
* https://discord.com/developers/docs/resources/message#attachment-object
*/
export interface RESTAPIAttachment {
/**
* Attachment id or a number that matches `n` in `files[n]`
*/
id: Snowflake | number;
/**
* Name of the file
*/
filename?: string | undefined;
/**
* Description of the file
*/
description?: string | undefined;
}
export type RESTAPIAttachment = Partial<
Pick<APIAttachment, 'description' | 'duration_secs' | 'filename' | 'title' | 'waveform'>
>;
/**
* https://discord.com/developers/docs/resources/channel#create-message
@ -444,7 +434,7 @@ export interface RESTPatchAPIChannelMessageJSONBody {
*
* Starting with API v10, the `attachments` array must contain all attachments that should be present after edit, including **retained and new** attachments provided in the request body.
*
* See https://discord.com/developers/docs/resources/channel#attachment-object
* See https://discord.com/developers/docs/resources/message#attachment-object
*/
attachments?: RESTAPIAttachment[] | undefined;
/**

View File

@ -2,9 +2,12 @@ import type {
APIApplicationCommand,
APIApplicationCommandPermission,
APIGuildApplicationCommandPermissions,
APIInteractionCallbackLaunchActivity,
APIInteractionCallbackMessage,
APIInteractionResponse,
APIInteractionResponseCallbackData,
ApplicationCommandType,
EntryPointCommandHandlerType,
} from '../payloads';
import type { AddUndefinedToPossiblyUndefinedPropertiesOfInterface, NonNullableFields, StrictPartial } from '../utils';
import type {
@ -53,6 +56,7 @@ type RESTPostAPIBaseApplicationCommandsJSONBody = AddUndefinedToPossiblyUndefine
| 'name_localized'
| 'type'
| 'version'
| 'handler'
> &
Partial<
NonNullableFields<Pick<APIApplicationCommand, 'contexts'>> &
@ -75,12 +79,22 @@ export interface RESTPostAPIContextMenuApplicationCommandsJSONBody extends RESTP
type: ApplicationCommandType.Message | ApplicationCommandType.User;
}
/**
* https://discord.com/developers/docs/interactions/application-commands#create-global-application-command
*/
export interface RESTPostAPIEntryPointApplicationCommandsJSONBody extends RESTPostAPIBaseApplicationCommandsJSONBody {
type: ApplicationCommandType.PrimaryEntryPoint;
description: string;
handler: EntryPointCommandHandlerType;
}
/**
* https://discord.com/developers/docs/interactions/application-commands#create-global-application-command
*/
export type RESTPostAPIApplicationCommandsJSONBody =
| RESTPostAPIChatInputApplicationCommandsJSONBody
| RESTPostAPIContextMenuApplicationCommandsJSONBody;
| RESTPostAPIContextMenuApplicationCommandsJSONBody
| RESTPostAPIEntryPointApplicationCommandsJSONBody;
/**
* https://discord.com/developers/docs/interactions/application-commands#create-global-application-command
@ -112,15 +126,17 @@ export type RESTPutAPIApplicationCommandsResult = APIApplicationCommand[];
*/
export type RESTGetAPIApplicationGuildCommandsQuery = RESTGetAPIApplicationCommandsQuery;
/**
* https://discord.com/developers/docs/interactions/application-commands#get-guild-application-commands
*/
export type RESTGetAPIApplicationGuildCommandsResult = Omit<APIApplicationCommand, 'dm_permission'>[];
export type RESTAPIApplicationGuildCommand = Omit<APIApplicationCommand, 'dm_permission' | 'handler'>;
/**
* https://discord.com/developers/docs/interactions/application-commands#get-guild-application-commands
*/
export type RESTGetAPIApplicationGuildCommandResult = Omit<APIApplicationCommand, 'dm_permission'>;
export type RESTGetAPIApplicationGuildCommandsResult = RESTAPIApplicationGuildCommand[];
/**
* https://discord.com/developers/docs/interactions/application-commands#get-guild-application-commands
*/
export type RESTGetAPIApplicationGuildCommandResult = RESTAPIApplicationGuildCommand;
/**
* https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command
@ -132,7 +148,7 @@ export type RESTPostAPIApplicationGuildCommandsJSONBody =
/**
* https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command
*/
export type RESTPostAPIApplicationGuildCommandsResult = Omit<APIApplicationCommand, 'dm_permission'>;
export type RESTPostAPIApplicationGuildCommandsResult = RESTAPIApplicationGuildCommand;
/**
* https://discord.com/developers/docs/interactions/application-commands#edit-guild-application-command
@ -145,7 +161,7 @@ export type RESTPatchAPIApplicationGuildCommandJSONBody = StrictPartial<
/**
* https://discord.com/developers/docs/interactions/application-commands#edit-guild-application-command
*/
export type RESTPatchAPIApplicationGuildCommandResult = Omit<APIApplicationCommand, 'dm_permission'>;
export type RESTPatchAPIApplicationGuildCommandResult = RESTAPIApplicationGuildCommand;
/**
* https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands
@ -160,13 +176,28 @@ export type RESTPutAPIApplicationGuildCommandsJSONBody = (
/**
* https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands
*/
export type RESTPutAPIApplicationGuildCommandsResult = Omit<APIApplicationCommand, 'dm_permission'>[];
export type RESTPutAPIApplicationGuildCommandsResult = RESTAPIApplicationGuildCommand[];
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response
*/
export type RESTPostAPIInteractionCallbackJSONBody = APIInteractionResponse;
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response-query-string-params
*/
export type RESTPostAPIInteractionCallbackQuery = {
/**
* Whether to include a RESTPostAPIInteractionCallbackResult as the response instead of a 204.
*/
with_response?: boolean;
};
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback
*/
export type RESTPostAPIInteractionCallbackResult = APIInteractionCallbackLaunchActivity | APIInteractionCallbackMessage;
/**
* https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response
*/

View File

@ -264,7 +264,7 @@ export type RESTPatchAPIWebhookWithTokenMessageJSONBody = AddUndefinedToPossibly
*
* Starting with API v10, the `attachments` array must contain all attachments that should be present after edit, including **retained and new** attachments provided in the request body.
*
* See https://discord.com/developers/docs/resources/channel#attachment-object
* See https://discord.com/developers/docs/resources/message#attachment-object
*/
attachments?: RESTAPIAttachment[] | undefined;
};