diff --git a/mod.ts b/mod.ts index 8536510..d05784b 100644 --- a/mod.ts +++ b/mod.ts @@ -39,7 +39,8 @@ export { GuildChannelsManager } from './src/managers/guildChannels.ts' export { GuildManager } from './src/managers/guilds.ts' export * from './src/structures/base.ts' export * from './src/structures/slash.ts' -export * from './src/types/slash.ts' +export * from './src/types/slashCommands.ts' +export * from './src/types/interactions.ts' export { GuildEmojisManager } from './src/managers/guildEmojis.ts' export { MembersManager } from './src/managers/members.ts' export { MessageReactionsManager } from './src/managers/messageReactions.ts' diff --git a/src/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts index 3d4c5d4..156441b 100644 --- a/src/gateway/handlers/interactionCreate.ts +++ b/src/gateway/handlers/interactionCreate.ts @@ -1,12 +1,18 @@ import { Guild } from '../../structures/guild.ts' import { Member } from '../../structures/member.ts' import { - Interaction, InteractionApplicationCommandResolved, - InteractionChannel + SlashCommandInteraction } from '../../structures/slash.ts' +import { + Interaction, + InteractionChannel +} from '../../structures/interactions.ts' import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' -import { InteractionPayload } from '../../types/slash.ts' +import { + InteractionPayload, + InteractionType +} from '../../types/interactions.ts' import { UserPayload } from '../../types/user.ts' import { Permissions } from '../../utils/permissions.ts' import type { Gateway, GatewayEventHandler } from '../mod.ts' @@ -99,12 +105,23 @@ export const interactionCreate: GatewayEventHandler = async ( } } - const interaction = new Interaction(gateway.client, d, { - member, - guild, - channel, - user, - resolved - }) + let interaction + if (d.type === InteractionType.APPLICATION_COMMAND) { + interaction = new SlashCommandInteraction(gateway.client, d, { + member, + guild, + channel, + user, + resolved + }) + } else { + interaction = new Interaction(gateway.client, d, { + member, + guild, + channel, + user + }) + } + gateway.client.emit('interactionCreate', interaction) } diff --git a/src/gateway/handlers/mod.ts b/src/gateway/handlers/mod.ts index 1c299f3..dc649e8 100644 --- a/src/gateway/handlers/mod.ts +++ b/src/gateway/handlers/mod.ts @@ -59,7 +59,8 @@ import type { EveryTextChannelTypes } from '../../utils/channel.ts' import { interactionCreate } from './interactionCreate.ts' -import type { Interaction } from '../../structures/slash.ts' +import type { Interaction } from '../../structures/interactions.ts' +import type { SlashCommandInteraction } from '../../structures/slash.ts' import type { CommandContext } from '../../commands/command.ts' import type { RequestMethods } from '../../rest/types.ts' import type { PartialInvitePayload } from '../../types/invite.ts' @@ -358,11 +359,12 @@ export type ClientEvents = { * @param channel Channel of which Webhooks were updated */ webhooksUpdate: [guild: Guild, channel: GuildTextBasedChannel] + /** * An Interaction was created * @param interaction Created interaction object */ - interactionCreate: [interaction: Interaction] + interactionCreate: [interaction: Interaction | SlashCommandInteraction] /** * When debug message was made diff --git a/src/interactions/slashClient.ts b/src/interactions/slashClient.ts index 29e808c..9efd4c9 100644 --- a/src/interactions/slashClient.ts +++ b/src/interactions/slashClient.ts @@ -1,13 +1,14 @@ import { - Interaction, + SlashCommandInteraction, InteractionApplicationCommandResolved } from '../structures/slash.ts' +import { Interaction } from '../structures/interactions.ts' import { InteractionPayload, InteractionResponsePayload, - InteractionType, - SlashCommandOptionType -} from '../types/slash.ts' + InteractionType +} from '../types/interactions.ts' +import { SlashCommandOptionType } from '../types/slashCommands.ts' import type { Client } from '../client/mod.ts' import { RESTManager } from '../rest/mod.ts' import { SlashModule } from './slashModule.ts' @@ -170,7 +171,9 @@ export class SlashClient extends HarmonyEventEmitter { } /** Get Handler for an Interaction. Supports nested sub commands and sub command groups. */ - private _getCommand(i: Interaction): SlashCommandHandler | undefined { + private _getCommand( + i: SlashCommandInteraction + ): SlashCommandHandler | undefined { return this.getHandlers().find((e) => { const hasGroupOrParent = e.group !== undefined || e.parent !== undefined const groupMatched = @@ -201,22 +204,22 @@ export class SlashClient extends HarmonyEventEmitter { } /** Process an incoming Interaction */ - private async _process(interaction: Interaction): Promise { + private async _process( + interaction: Interaction | SlashCommandInteraction + ): Promise { if (!this.enabled) return - if ( - interaction.type !== InteractionType.APPLICATION_COMMAND || - interaction.data === undefined - ) - return + if (interaction.type !== InteractionType.APPLICATION_COMMAND) return const cmd = - this._getCommand(interaction) ?? + this._getCommand(interaction as SlashCommandInteraction) ?? this.getHandlers().find((e) => e.name === '*') if (cmd?.group !== undefined) - interaction.data.options = interaction.data.options[0].options ?? [] + (interaction as SlashCommandInteraction).data.options = + (interaction as SlashCommandInteraction).data.options[0].options ?? [] if (cmd?.parent !== undefined) - interaction.data.options = interaction.data.options[0].options ?? [] + (interaction as SlashCommandInteraction).data.options = + (interaction as SlashCommandInteraction).data.options[0].options ?? [] if (cmd === undefined) return @@ -271,20 +274,31 @@ export class SlashClient extends HarmonyEventEmitter { const payload: InteractionPayload = JSON.parse(decodeText(rawbody)) // TODO: Maybe fix all this hackery going on here? - const res = new Interaction(this as any, payload, { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - user: new User(this as any, (payload.member?.user ?? payload.user)!), - member: payload.member as any, - guild: payload.guild_id as any, - channel: payload.channel_id as any, - resolved: ((payload.data - ?.resolved as unknown) as InteractionApplicationCommandResolved) ?? { - users: {}, - members: {}, - roles: {}, - channels: {} - } - }) + let res + if (payload.type === InteractionType.APPLICATION_COMMAND) { + res = new SlashCommandInteraction(this as any, payload, { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + user: new User(this as any, (payload.member?.user ?? payload.user)!), + member: payload.member as any, + guild: payload.guild_id as any, + channel: payload.channel_id as any, + resolved: ((payload.data + ?.resolved as unknown) as InteractionApplicationCommandResolved) ?? { + users: {}, + members: {}, + roles: {}, + channels: {} + } + }) + } else { + res = new Interaction(this as any, payload, { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + user: new User(this as any, (payload.member?.user ?? payload.user)!), + member: payload.member as any, + guild: payload.guild_id as any, + channel: payload.channel_id as any + }) + } res._httpRespond = async (d: InteractionResponsePayload | FormData) => await req.respond({ status: 200, diff --git a/src/interactions/slashCommand.ts b/src/interactions/slashCommand.ts index 26237d3..e079bb2 100644 --- a/src/interactions/slashCommand.ts +++ b/src/interactions/slashCommand.ts @@ -6,7 +6,7 @@ import { SlashCommandOptionType, SlashCommandPartial, SlashCommandPayload -} from '../types/slash.ts' +} from '../types/slashCommands.ts' import { Collection } from '../utils/collection.ts' import type { SlashClient, SlashCommandHandlerCallback } from './slashClient.ts' diff --git a/src/managers/channels.ts b/src/managers/channels.ts index 69efe6c..2caca4d 100644 --- a/src/managers/channels.ts +++ b/src/managers/channels.ts @@ -100,6 +100,7 @@ export class ChannelsManager extends BaseManager { content: content, embed: option?.embed, file: option?.file, + components: option?.components, files: option?.files, tts: option?.tts, allowed_mentions: option?.allowedMentions, diff --git a/src/rest/endpoints.ts b/src/rest/endpoints.ts index ff14299..31c70ce 100644 --- a/src/rest/endpoints.ts +++ b/src/rest/endpoints.ts @@ -30,11 +30,11 @@ import type { InviteWithMetadataPayload } from '../types/invite.ts' import type { RoleModifyPayload, RolePayload } from '../types/role.ts' +import type { InteractionResponsePayload } from '../types/interactions.ts' import type { - InteractionResponsePayload, SlashCommandPartial, SlashCommandPayload -} from '../types/slash.ts' +} from '../types/slashCommands.ts' import type { TemplatePayload } from '../types/template.ts' import type { UserPayload } from '../types/user.ts' import type { VoiceRegion } from '../types/voice.ts' diff --git a/src/structures/interactions.ts b/src/structures/interactions.ts new file mode 100644 index 0000000..dad5e0c --- /dev/null +++ b/src/structures/interactions.ts @@ -0,0 +1,354 @@ +import type { Client } from '../client/client.ts' +import { + AllowedMentionsPayload, + ChannelTypes, + EmbedPayload, + MessageOptions +} from '../types/channel.ts' +import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' +import { + InteractionPayload, + InteractionResponseFlags, + InteractionResponsePayload, + InteractionResponseType, + InteractionType +} from '../types/interactions.ts' +import { + InteractionApplicationCommandData, + InteractionChannelPayload +} from '../types/slashCommands.ts' +import { Dict } from '../utils/dict.ts' +import { Permissions } from '../utils/permissions.ts' +import { SnowflakeBase } from './base.ts' +import { Channel } from './channel.ts' +import { Embed } from './embed.ts' +import { Guild } from './guild.ts' +import { GuildTextChannel } from './guildTextChannel.ts' +import { Member } from './member.ts' +import { Message } from './message.ts' +import { Role } from './role.ts' +import { TextChannel } from './textChannel.ts' +import { User } from './user.ts' + +interface WebhookMessageOptions extends MessageOptions { + embeds?: Array + name?: string + avatar?: string +} + +type AllWebhookMessageOptions = string | WebhookMessageOptions + +/** Interaction Message related Options */ +export interface InteractionMessageOptions { + content?: string + embeds?: Array + tts?: boolean + flags?: number | InteractionResponseFlags[] + allowedMentions?: AllowedMentionsPayload + /** Whether the Message Response should be Ephemeral (only visible to User) or not */ + ephemeral?: boolean +} + +export interface InteractionResponse extends InteractionMessageOptions { + /** Type of Interaction Response */ + type?: InteractionResponseType +} + +/** Represents a Channel Object for an Option in Slash Command */ +export class InteractionChannel extends SnowflakeBase { + /** Name of the Channel */ + name: string + /** Channel Type */ + type: ChannelTypes + permissions: Permissions + + constructor(client: Client, data: InteractionChannelPayload) { + super(client) + this.id = data.id + this.name = data.name + this.type = data.type + this.permissions = new Permissions(data.permissions) + } + + /** Resolve to actual Channel object if present in Cache */ + async resolve(): Promise { + return this.client.channels.get(this.id) + } +} + +export interface InteractionApplicationCommandResolved { + users: Dict + members: Dict + channels: Dict + roles: Dict +} + +export class InteractionUser extends User { + member?: Member +} + +export class Interaction extends SnowflakeBase { + /** Type of Interaction */ + type: InteractionType + /** Interaction Token */ + token: string + /** Interaction ID */ + id: string + /** Channel in which Interaction was initiated */ + channel?: TextChannel | GuildTextChannel + /** Guild in which Interaction was initiated */ + guild?: Guild + /** Member object of who initiated the Interaction */ + member?: Member + /** User object of who invoked Interaction */ + user: User + /** Whether we have responded to Interaction or not */ + responded: boolean = false + /** Whether response was deferred or not */ + deferred: boolean = false + _httpRespond?: (d: InteractionResponsePayload) => unknown + _httpResponded?: boolean + applicationID: string + /** Data sent with Interaction. Only applies to Application Command */ + data?: InteractionApplicationCommandData + + constructor( + client: Client, + data: InteractionPayload, + others: { + channel?: TextChannel | GuildTextChannel + guild?: Guild + member?: Member + user: User + } + ) { + super(client) + this.type = data.type + this.token = data.token + this.member = others.member + this.id = data.id + this.applicationID = data.application_id + this.user = others.user + this.data = data.data + this.guild = others.guild + this.channel = others.channel + } + + /** Respond to an Interaction */ + async respond(data: InteractionResponse): Promise { + if (this.responded) throw new Error('Already responded to Interaction') + let flags = 0 + if (data.ephemeral === true) flags |= InteractionResponseFlags.EPHEMERAL + if (data.flags !== undefined) { + if (Array.isArray(data.flags)) + flags = data.flags.reduce((p, a) => p | a, flags) + else if (typeof data.flags === 'number') flags |= data.flags + } + const payload: InteractionResponsePayload = { + type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: + data.type === undefined || + data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE + ? { + content: data.content ?? '', + embeds: data.embeds, + tts: data.tts ?? false, + flags, + allowed_mentions: data.allowedMentions ?? undefined + } + : undefined + } + + if (this._httpRespond !== undefined && this._httpResponded !== true) { + this._httpResponded = true + await this._httpRespond(payload) + } else + await this.client.rest.post( + INTERACTION_CALLBACK(this.id, this.token), + payload + ) + this.responded = true + + return this + } + + /** Defer the Interaction i.e. let the user know bot is processing and will respond later. You only have 15 minutes to edit the response! */ + async defer(ephemeral = false): Promise { + await this.respond({ + type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE, + flags: ephemeral ? 1 << 6 : 0 + }) + this.deferred = true + return this + } + + /** Reply with a Message to the Interaction */ + async reply(content: string): Promise + async reply(options: InteractionMessageOptions): Promise + async reply( + content: string, + options: InteractionMessageOptions + ): Promise + async reply( + content: string | InteractionMessageOptions, + messageOptions?: InteractionMessageOptions + ): Promise { + let options: InteractionMessageOptions | undefined = + typeof content === 'object' ? content : messageOptions + if ( + typeof content === 'object' && + messageOptions !== undefined && + options !== undefined + ) + Object.assign(options, messageOptions) + if (options === undefined) options = {} + if (typeof content === 'string') Object.assign(options, { content }) + + if (this.deferred && this.responded) { + await this.editResponse({ + content: options.content, + embeds: options.embeds, + flags: options.flags, + allowedMentions: options.allowedMentions + }) + } else + await this.respond( + Object.assign(options, { + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE + }) + ) + + return this + } + + /** Edit the original Interaction response */ + async editResponse(data: { + content?: string + embeds?: Array + flags?: number | number[] + allowedMentions?: AllowedMentionsPayload + }): Promise { + const url = WEBHOOK_MESSAGE(this.applicationID, this.token, '@original') + await this.client.rest.patch(url, { + content: data.content ?? '', + embeds: data.embeds ?? [], + flags: + typeof data.flags === 'object' + ? data.flags.reduce((p, a) => p | a, 0) + : data.flags, + allowed_mentions: data.allowedMentions + }) + return this + } + + /** Delete the original Interaction Response */ + async deleteResponse(): Promise { + const url = WEBHOOK_MESSAGE(this.applicationID, this.token, '@original') + await this.client.rest.delete(url) + return this + } + + get url(): string { + return `https://discord.com/api/v8/webhooks/${this.applicationID}/${this.token}` + } + + /** Send a followup message */ + async send( + text?: string | AllWebhookMessageOptions, + option?: AllWebhookMessageOptions + ): Promise { + if (typeof text === 'object') { + option = text + text = undefined + } + + if (text === undefined && option === undefined) { + throw new Error('Either text or option is necessary.') + } + + if (option instanceof Embed) + option = { + embeds: [option] + } + + const payload: any = { + content: text, + embeds: + (option as WebhookMessageOptions)?.embed !== undefined + ? [(option as WebhookMessageOptions).embed] + : (option as WebhookMessageOptions)?.embeds !== undefined + ? (option as WebhookMessageOptions).embeds + : undefined, + file: (option as WebhookMessageOptions)?.file, + files: (option as WebhookMessageOptions)?.files, + tts: (option as WebhookMessageOptions)?.tts, + allowed_mentions: (option as WebhookMessageOptions)?.allowedMentions + } + + if ((option as WebhookMessageOptions)?.name !== undefined) { + payload.username = (option as WebhookMessageOptions)?.name + } + + if ((option as WebhookMessageOptions)?.avatar !== undefined) { + payload.avatar = (option as WebhookMessageOptions)?.avatar + } + + if ( + payload.embeds !== undefined && + payload.embeds instanceof Array && + payload.embeds.length > 10 + ) + throw new Error( + `Cannot send more than 10 embeds through Interaction Webhook` + ) + + const resp = await this.client.rest.post(`${this.url}?wait=true`, payload) + + const res = new Message( + this.client, + resp, + (this as unknown) as TextChannel, + (this as unknown) as User + ) + await res.mentions.fromPayload(resp) + return res + } + + /** Edit a Followup message */ + async editMessage( + msg: Message | string, + data: { + content?: string + embeds?: Array + file?: any + allowed_mentions?: { + parse?: string + roles?: string[] + users?: string[] + everyone?: boolean + } + } + ): Promise { + await this.client.rest.patch( + WEBHOOK_MESSAGE( + this.applicationID, + this.token ?? this.client.token, + typeof msg === 'string' ? msg : msg.id + ), + data + ) + return this + } + + /** Delete a follow-up Message */ + async deleteMessage(msg: Message | string): Promise { + await this.client.rest.delete( + WEBHOOK_MESSAGE( + this.applicationID, + this.token ?? this.client.token, + typeof msg === 'string' ? msg : msg.id + ) + ) + return this + } +} diff --git a/src/structures/message.ts b/src/structures/message.ts index 1fd5e4e..5e81b82 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -20,7 +20,7 @@ import type { Guild } from './guild.ts' import { MessageReactionsManager } from '../managers/messageReactions.ts' import { MessageSticker } from './messageSticker.ts' import type { Emoji } from './emoji.ts' -import type { InteractionType } from '../types/slash.ts' +import type { InteractionType } from '../types/interactions.ts' import { encodeText } from '../utils/encoding.ts' type AllMessageOptions = MessageOptions | Embed diff --git a/src/structures/slash.ts b/src/structures/slash.ts index c222814..c23f476 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -1,80 +1,22 @@ import type { Client } from '../client/mod.ts' -import type { - AllowedMentionsPayload, - ChannelTypes, - EmbedPayload, - MessageOptions -} from '../types/channel.ts' -import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' +import { InteractionPayload } from '../types/interactions.ts' import { InteractionApplicationCommandData, InteractionApplicationCommandOption, - InteractionChannelPayload, - InteractionPayload, - InteractionResponseFlags, - InteractionResponsePayload, - InteractionResponseType, - InteractionType, SlashCommandOptionType -} from '../types/slash.ts' +} from '../types/slashCommands.ts' import type { Dict } from '../utils/dict.ts' -import { Permissions } from '../utils/permissions.ts' -import { SnowflakeBase } from './base.ts' -import type { Channel } from './channel.ts' -import { Embed } from './embed.ts' import type { Guild } from './guild.ts' import type { GuildTextChannel } from './guildTextChannel.ts' import type { Member } from './member.ts' -import { Message } from './message.ts' import type { Role } from './role.ts' import type { TextChannel } from './textChannel.ts' import { User } from './user.ts' - -interface WebhookMessageOptions extends MessageOptions { - embeds?: Array - name?: string - avatar?: string -} - -type AllWebhookMessageOptions = string | WebhookMessageOptions - -/** Interaction Message related Options */ -export interface InteractionMessageOptions { - content?: string - embeds?: Array - tts?: boolean - flags?: number | InteractionResponseFlags[] - allowedMentions?: AllowedMentionsPayload - /** Whether the Message Response should be Ephemeral (only visible to User) or not */ - ephemeral?: boolean -} - -export interface InteractionResponse extends InteractionMessageOptions { - /** Type of Interaction Response */ - type?: InteractionResponseType -} - -/** Represents a Channel Object for an Option in Slash Command */ -export class InteractionChannel extends SnowflakeBase { - /** Name of the Channel */ - name: string - /** Channel Type */ - type: ChannelTypes - permissions: Permissions - - constructor(client: Client, data: InteractionChannelPayload) { - super(client) - this.id = data.id - this.name = data.name - this.type = data.type - this.permissions = new Permissions(data.permissions) - } - - /** Resolve to actual Channel object if present in Cache */ - async resolve(): Promise { - return this.client.channels.get(this.id) - } -} +import { + InteractionUser, + InteractionChannel, + Interaction +} from './interactions.ts' export interface InteractionApplicationCommandResolved { users: Dict @@ -83,36 +25,11 @@ export interface InteractionApplicationCommandResolved { roles: Dict } -export class InteractionUser extends User { - member?: Member -} - -export class Interaction extends SnowflakeBase { - /** Type of Interaction */ - type: InteractionType - /** Interaction Token */ - token: string - /** Interaction ID */ - id: string +export class SlashCommandInteraction extends Interaction { /** Data sent with Interaction. Only applies to Application Command */ - data?: InteractionApplicationCommandData - /** Channel in which Interaction was initiated */ - channel?: TextChannel | GuildTextChannel - /** Guild in which Interaction was initiated */ - guild?: Guild - /** Member object of who initiated the Interaction */ - member?: Member - /** User object of who invoked Interaction */ - user: User - /** Whether we have responded to Interaction or not */ - responded: boolean = false + data: InteractionApplicationCommandData /** Resolved data for Snowflakes in Slash Command Arguments */ resolved: InteractionApplicationCommandResolved - /** Whether response was deferred or not */ - deferred: boolean = false - _httpRespond?: (d: InteractionResponsePayload) => unknown - _httpResponded?: boolean - applicationID: string constructor( client: Client, @@ -125,26 +42,19 @@ export class Interaction extends SnowflakeBase { resolved: InteractionApplicationCommandResolved } ) { - super(client) - this.type = data.type - this.token = data.token - this.member = others.member - this.id = data.id - this.applicationID = data.application_id - this.user = others.user - this.data = data.data - this.guild = others.guild - this.channel = others.channel + super(client, data, others) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + this.data = data.data as InteractionApplicationCommandData this.resolved = others.resolved } /** Name of the Command Used (may change with future additions to Interactions!) */ - get name(): string | undefined { - return this.data?.name + get name(): string { + return this.data.name } get options(): InteractionApplicationCommandOption[] { - return this.data?.options ?? [] + return this.data.options ?? [] } /** Get an option by name */ @@ -162,222 +72,4 @@ export class Interaction extends SnowflakeBase { return this.resolved.channels[op.value] as any else return op.value } - - /** Respond to an Interaction */ - async respond(data: InteractionResponse): Promise { - if (this.responded) throw new Error('Already responded to Interaction') - let flags = 0 - if (data.ephemeral === true) flags |= InteractionResponseFlags.EPHEMERAL - if (data.flags !== undefined) { - if (Array.isArray(data.flags)) - flags = data.flags.reduce((p, a) => p | a, flags) - else if (typeof data.flags === 'number') flags |= data.flags - } - const payload: InteractionResponsePayload = { - type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: - data.type === undefined || - data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE - ? { - content: data.content ?? '', - embeds: data.embeds, - tts: data.tts ?? false, - flags, - allowed_mentions: data.allowedMentions ?? undefined - } - : undefined - } - - if (this._httpRespond !== undefined && this._httpResponded !== true) { - this._httpResponded = true - await this._httpRespond(payload) - } else - await this.client.rest.post( - INTERACTION_CALLBACK(this.id, this.token), - payload - ) - this.responded = true - - return this - } - - /** Defer the Interaction i.e. let the user know bot is processing and will respond later. You only have 15 minutes to edit the response! */ - async defer(ephemeral = false): Promise { - await this.respond({ - type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE, - flags: ephemeral ? 1 << 6 : 0 - }) - this.deferred = true - return this - } - - /** Reply with a Message to the Interaction */ - async reply(content: string): Promise - async reply(options: InteractionMessageOptions): Promise - async reply( - content: string, - options: InteractionMessageOptions - ): Promise - async reply( - content: string | InteractionMessageOptions, - messageOptions?: InteractionMessageOptions - ): Promise { - let options: InteractionMessageOptions | undefined = - typeof content === 'object' ? content : messageOptions - if ( - typeof content === 'object' && - messageOptions !== undefined && - options !== undefined - ) - Object.assign(options, messageOptions) - if (options === undefined) options = {} - if (typeof content === 'string') Object.assign(options, { content }) - - if (this.deferred && this.responded) { - await this.editResponse({ - content: options.content, - embeds: options.embeds, - flags: options.flags, - allowedMentions: options.allowedMentions - }) - } else - await this.respond( - Object.assign(options, { - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE - }) - ) - - return this - } - - /** Edit the original Interaction response */ - async editResponse(data: { - content?: string - embeds?: Array - flags?: number | number[] - allowedMentions?: AllowedMentionsPayload - }): Promise { - const url = WEBHOOK_MESSAGE(this.applicationID, this.token, '@original') - await this.client.rest.patch(url, { - content: data.content ?? '', - embeds: data.embeds ?? [], - flags: - typeof data.flags === 'object' - ? data.flags.reduce((p, a) => p | a, 0) - : data.flags, - allowed_mentions: data.allowedMentions - }) - return this - } - - /** Delete the original Interaction Response */ - async deleteResponse(): Promise { - const url = WEBHOOK_MESSAGE(this.applicationID, this.token, '@original') - await this.client.rest.delete(url) - return this - } - - get url(): string { - return `https://discord.com/api/v8/webhooks/${this.applicationID}/${this.token}` - } - - /** Send a followup message */ - async send( - text?: string | AllWebhookMessageOptions, - option?: AllWebhookMessageOptions - ): Promise { - if (typeof text === 'object') { - option = text - text = undefined - } - - if (text === undefined && option === undefined) { - throw new Error('Either text or option is necessary.') - } - - if (option instanceof Embed) - option = { - embeds: [option] - } - - const payload: any = { - content: text, - embeds: - (option as WebhookMessageOptions)?.embed !== undefined - ? [(option as WebhookMessageOptions).embed] - : (option as WebhookMessageOptions)?.embeds !== undefined - ? (option as WebhookMessageOptions).embeds - : undefined, - file: (option as WebhookMessageOptions)?.file, - files: (option as WebhookMessageOptions)?.files, - tts: (option as WebhookMessageOptions)?.tts, - allowed_mentions: (option as WebhookMessageOptions)?.allowedMentions - } - - if ((option as WebhookMessageOptions)?.name !== undefined) { - payload.username = (option as WebhookMessageOptions)?.name - } - - if ((option as WebhookMessageOptions)?.avatar !== undefined) { - payload.avatar = (option as WebhookMessageOptions)?.avatar - } - - if ( - payload.embeds !== undefined && - payload.embeds instanceof Array && - payload.embeds.length > 10 - ) - throw new Error( - `Cannot send more than 10 embeds through Interaction Webhook` - ) - - const resp = await this.client.rest.post(`${this.url}?wait=true`, payload) - - const res = new Message( - this.client, - resp, - (this as unknown) as TextChannel, - (this as unknown) as User - ) - await res.mentions.fromPayload(resp) - return res - } - - /** Edit a Followup message */ - async editMessage( - msg: Message | string, - data: { - content?: string - embeds?: Array - file?: any - allowed_mentions?: { - parse?: string - roles?: string[] - users?: string[] - everyone?: boolean - } - } - ): Promise { - await this.client.rest.patch( - WEBHOOK_MESSAGE( - this.applicationID, - this.token ?? this.client.token, - typeof msg === 'string' ? msg : msg.id - ), - data - ) - return this - } - - /** Delete a follow-up Message */ - async deleteMessage(msg: Message | string): Promise { - await this.client.rest.delete( - WEBHOOK_MESSAGE( - this.applicationID, - this.token ?? this.client.token, - typeof msg === 'string' ? msg : msg.id - ) - ) - return this - } } diff --git a/src/types/channel.ts b/src/types/channel.ts index 8db4e5e..a07e064 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -5,8 +5,9 @@ import type { Role } from '../structures/role.ts' import type { Permissions } from '../utils/permissions.ts' import type { EmojiPayload } from './emoji.ts' import type { MemberPayload } from './guild.ts' -import type { InteractionType } from './slash.ts' +import type { InteractionType } from './interactions.ts' import type { UserPayload } from './user.ts' +import type { MessageComponentPayload } from './messageComponents.ts' export interface ChannelPayload { id: string @@ -210,6 +211,7 @@ export interface MessageOptions { files?: MessageAttachment[] allowedMentions?: AllowedMentionsPayload reply?: Message | MessageReference | string + components?: MessageComponentPayload[] } export interface ChannelMention { @@ -401,6 +403,7 @@ export interface CreateMessagePayload extends EditMessagePayload { message_reference?: MessageReference file?: MessageAttachment files?: MessageAttachment[] + components?: MessageComponentPayload[] } export interface CreateWebhookMessageBasePayload { diff --git a/src/types/gateway.ts b/src/types/gateway.ts index d5c2a64..195ff07 100644 --- a/src/types/gateway.ts +++ b/src/types/gateway.ts @@ -11,7 +11,7 @@ import type { ClientStatus } from './presence.ts' import type { RolePayload } from './role.ts' -import type { SlashCommandPayload } from './slash.ts' +import type { SlashCommandPayload } from './slashCommands.ts' import type { UserPayload } from './user.ts' /** diff --git a/src/types/interactions.ts b/src/types/interactions.ts new file mode 100644 index 0000000..41962c0 --- /dev/null +++ b/src/types/interactions.ts @@ -0,0 +1,70 @@ +import { AllowedMentionsPayload, EmbedPayload } from './channel.ts' +import type { MemberPayload } from './guild.ts' +import type { InteractionApplicationCommandData } from './slashCommands.ts' +import type { UserPayload } from './user.ts' + +export enum InteractionType { + /** Ping sent by the API (HTTP-only) */ + PING = 1, + /** Slash Command Interaction */ + APPLICATION_COMMAND = 2 +} + +export interface InteractionMemberPayload extends MemberPayload { + /** Permissions of the Member who initiated Interaction (Guild-only) */ + permissions: string +} + +export interface InteractionPayload { + /** Type of the Interaction */ + type: InteractionType + /** Token of the Interaction to respond */ + token: string + /** Member object of user who invoked */ + member?: InteractionMemberPayload + /** User who initiated Interaction (only in DMs) */ + user?: UserPayload + /** ID of the Interaction */ + id: string + /** + * Data sent with the interaction. Undefined only when Interaction is not Slash Command.* + */ + data?: InteractionApplicationCommandData + /** ID of the Guild in which Interaction was invoked */ + guild_id?: string + /** ID of the Channel in which Interaction was invoked */ + channel_id?: string + application_id: string +} + +export enum InteractionResponseType { + /** Just ack a ping, Http-only. */ + PONG = 1, + /** Send a channel message as response. */ + CHANNEL_MESSAGE_WITH_SOURCE = 4, + /** Let the user know bot is processing ("thinking") and you can edit the response later */ + DEFERRED_CHANNEL_MESSAGE = 5 +} + +export interface InteractionResponsePayload { + /** Type of the response */ + type: InteractionResponseType + /** Data to be sent with response. Optional for types: Pong, Acknowledge, Ack with Source */ + data?: InteractionResponseDataPayload +} + +export interface InteractionResponseDataPayload { + tts?: boolean + /** Text content of the Response (Message) */ + content: string + /** Upto 10 Embed Objects to send with Response */ + embeds?: EmbedPayload[] + /** Allowed Mentions object */ + allowed_mentions?: AllowedMentionsPayload + flags?: number +} + +export enum InteractionResponseFlags { + /** A Message which is only visible to Interaction User. */ + EPHEMERAL = 1 << 6 +} diff --git a/src/types/messageComponents.ts b/src/types/messageComponents.ts new file mode 100644 index 0000000..c666546 --- /dev/null +++ b/src/types/messageComponents.ts @@ -0,0 +1,20 @@ +export enum MessageComponentType { + ActionRow = 1, + Button = 2 +} + +export enum ButtonStyle { + Primary = 1, + Secondary = 2, + Success = 3, + Destructive = 4, + Link = 5 +} + +export interface MessageComponentPayload { + type: MessageComponentType + components?: MessageComponentPayload[] + label?: string + style?: ButtonStyle + url?: string +} diff --git a/src/types/slash.ts b/src/types/slashCommands.ts similarity index 55% rename from src/types/slash.ts rename to src/types/slashCommands.ts index 2791740..c0447c7 100644 --- a/src/types/slash.ts +++ b/src/types/slashCommands.ts @@ -1,9 +1,5 @@ import type { Dict } from '../utils/dict.ts' -import type { - AllowedMentionsPayload, - ChannelTypes, - EmbedPayload -} from './channel.ts' +import type { ChannelTypes } from './channel.ts' import type { MemberPayload } from './guild.ts' import type { RolePayload } from './role.ts' import type { UserPayload } from './user.ts' @@ -44,40 +40,6 @@ export interface InteractionApplicationCommandData { resolved?: InteractionApplicationCommandResolvedPayload } -export enum InteractionType { - /** Ping sent by the API (HTTP-only) */ - PING = 1, - /** Slash Command Interaction */ - APPLICATION_COMMAND = 2 -} - -export interface InteractionMemberPayload extends MemberPayload { - /** Permissions of the Member who initiated Interaction (Guild-only) */ - permissions: string -} - -export interface InteractionPayload { - /** Type of the Interaction */ - type: InteractionType - /** Token of the Interaction to respond */ - token: string - /** Member object of user who invoked */ - member?: InteractionMemberPayload - /** User who initiated Interaction (only in DMs) */ - user?: UserPayload - /** ID of the Interaction */ - id: string - /** - * Data sent with the interaction. Undefined only when Interaction is not Slash Command.* - */ - data?: InteractionApplicationCommandData - /** ID of the Guild in which Interaction was invoked */ - guild_id?: string - /** ID of the Channel in which Interaction was invoked */ - channel_id?: string - application_id: string -} - export interface SlashCommandChoice { /** (Display) name of the Choice */ name: string @@ -131,35 +93,3 @@ export interface SlashCommandPayload extends SlashCommandPartial { /** Application ID */ application_id: string } - -export enum InteractionResponseType { - /** Just ack a ping, Http-only. */ - PONG = 1, - /** Send a channel message as response. */ - CHANNEL_MESSAGE_WITH_SOURCE = 4, - /** Let the user know bot is processing ("thinking") and you can edit the response later */ - DEFERRED_CHANNEL_MESSAGE = 5 -} - -export interface InteractionResponsePayload { - /** Type of the response */ - type: InteractionResponseType - /** Data to be sent with response. Optional for types: Pong, Acknowledge, Ack with Source */ - data?: InteractionResponseDataPayload -} - -export interface InteractionResponseDataPayload { - tts?: boolean - /** Text content of the Response (Message) */ - content: string - /** Upto 10 Embed Objects to send with Response */ - embeds?: EmbedPayload[] - /** Allowed Mentions object */ - allowed_mentions?: AllowedMentionsPayload - flags?: number -} - -export enum InteractionResponseFlags { - /** A Message which is only visible to Interaction User. */ - EPHEMERAL = 1 << 6 -}