diff --git a/mod.ts b/mod.ts index d05784b..0a9a01a 100644 --- a/mod.ts +++ b/mod.ts @@ -39,8 +39,11 @@ 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/structures/interactions.ts' +export * from './src/structures/messageComponents.ts' export * from './src/types/slashCommands.ts' export * from './src/types/interactions.ts' +export * from './src/types/messageComponents.ts' export { GuildEmojisManager } from './src/managers/guildEmojis.ts' export { MembersManager } from './src/managers/members.ts' export { MessageReactionsManager } from './src/managers/messageReactions.ts' @@ -192,3 +195,4 @@ export { isVoiceChannel, default as getChannelByType } from './src/utils/channel.ts' +export * from './src/utils/interactions.ts' diff --git a/src/client/client.ts b/src/client/client.ts index 457d77b..ea3650d 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -12,7 +12,6 @@ import { EmojisManager } from '../managers/emojis.ts' import { ActivityGame, ClientActivity } from '../types/presence.ts' import type { Extension } from '../commands/extension.ts' import { SlashClient } from '../interactions/slashClient.ts' -import type { Interaction } from '../structures/slash.ts' import { ShardManager } from './shard.ts' import { Application } from '../structures/application.ts' import { Invite } from '../structures/invite.ts' @@ -113,17 +112,6 @@ export class Client extends HarmonyEventEmitter { /** Client's presence. Startup one if set before connecting */ presence: ClientPresence = new ClientPresence() - _decoratedEvents?: { - [name: string]: (...args: any[]) => void - } - - _decoratedSlash?: Array<{ - name: string - guild?: string - parent?: string - group?: string - handler: (interaction: Interaction) => any - }> _id?: string @@ -175,13 +163,13 @@ export class Client extends HarmonyEventEmitter { this.fetchUncachedReactions = true if ( - this._decoratedEvents !== undefined && - Object.keys(this._decoratedEvents).length !== 0 + (this as any)._decoratedEvents !== undefined && + Object.keys((this as any)._decoratedEvents).length !== 0 ) { - Object.entries(this._decoratedEvents).forEach((entry) => { - this.on(entry[0] as keyof ClientEvents, entry[1].bind(this)) + Object.entries((this as any)._decoratedEvents).forEach((entry) => { + this.on(entry[0] as keyof ClientEvents, (entry as any)[1].bind(this)) }) - this._decoratedEvents = undefined + ;(this as any)._decoratedEvents = undefined } this.clientProperties = @@ -422,19 +410,23 @@ export class Client extends HarmonyEventEmitter { } /** Event decorator to create an Event handler from function */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function event(name?: keyof ClientEvents) { return function ( client: Client | Extension, prop: keyof ClientEvents | string ) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const c = client as any const listener = ((client as unknown) as { [name in keyof ClientEvents]: (...args: ClientEvents[name]) => any })[(prop as unknown) as keyof ClientEvents] if (typeof listener !== 'function') throw new Error('@event decorator requires a function') - if (client._decoratedEvents === undefined) client._decoratedEvents = {} + + if (c._decoratedEvents === undefined) c._decoratedEvents = {} const key = name === undefined ? prop : name - client._decoratedEvents[key] = listener + c._decoratedEvents[key] = listener } } diff --git a/src/commands/extension.ts b/src/commands/extension.ts index 80f4bc1..1a29be1 100644 --- a/src/commands/extension.ts +++ b/src/commands/extension.ts @@ -73,27 +73,25 @@ export class Extension { /** Events registered by this Extension */ events: { [name: string]: (...args: any[]) => {} } = {} - _decoratedCommands?: { [name: string]: Command } - _decoratedEvents?: { [name: string]: (...args: any[]) => any } - constructor(client: CommandClient) { this.client = client - if (this._decoratedCommands !== undefined) { - Object.entries(this._decoratedCommands).forEach((entry) => { + const self = this as any + if (self._decoratedCommands !== undefined) { + Object.entries(self._decoratedCommands).forEach((entry: any) => { entry[1].extension = this this.commands.add(entry[1]) }) - this._decoratedCommands = undefined + self._decoratedCommands = undefined } if ( - this._decoratedEvents !== undefined && - Object.keys(this._decoratedEvents).length !== 0 + self._decoratedEvents !== undefined && + Object.keys(self._decoratedEvents).length !== 0 ) { - Object.entries(this._decoratedEvents).forEach((entry) => { + Object.entries(self._decoratedEvents).forEach((entry: any) => { this.listen(entry[0] as keyof ClientEvents, entry[1].bind(this)) }) - this._decoratedEvents = undefined + self._decoratedEvents = undefined } } diff --git a/src/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts index 156441b..f99c7e1 100644 --- a/src/gateway/handlers/interactionCreate.ts +++ b/src/gateway/handlers/interactionCreate.ts @@ -18,6 +18,11 @@ import { Permissions } from '../../utils/permissions.ts' import type { Gateway, GatewayEventHandler } from '../mod.ts' import { User } from '../../structures/user.ts' import { Role } from '../../structures/role.ts' +import { RolePayload } from '../../types/role.ts' +import { InteractionChannelPayload } from '../../types/slashCommands.ts' +import { Message } from '../../structures/message.ts' +import { TextChannel } from '../../structures/textChannel.ts' +import { MessageComponentInteraction } from '../../structures/messageComponents.ts' export const interactionCreate: GatewayEventHandler = async ( gateway: Gateway, @@ -32,13 +37,22 @@ export const interactionCreate: GatewayEventHandler = async ( const guild = d.guild_id === undefined ? undefined - : await gateway.client.guilds.get(d.guild_id) + : (await gateway.client.guilds.get(d.guild_id)) ?? + new Guild(gateway.client, { unavailable: true, id: d.guild_id } as any) if (d.member !== undefined) await guild?.members.set(d.member.user.id, d.member) const member = d.member !== undefined - ? (((await guild?.members.get(d.member.user.id)) as unknown) as Member) + ? (await guild?.members.get(d.member.user.id))! ?? + new Member( + gateway.client, + d.member!, + new User(gateway.client, d.member.user), + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + guild!, + new Permissions(d.member.permissions) + ) : undefined if (d.user !== undefined) await gateway.client.users.set(d.user.id, d.user) const dmUser = @@ -47,9 +61,9 @@ export const interactionCreate: GatewayEventHandler = async ( const user = member !== undefined ? member.user : dmUser if (user === undefined) return - const channel = - (await gateway.client.channels.get(d.channel_id)) ?? - (await gateway.client.channels.fetch(d.channel_id)) + const channel = await gateway.client.channels.get( + d.channel_id + ) const resolved: InteractionApplicationCommandResolved = { users: {}, @@ -58,9 +72,11 @@ export const interactionCreate: GatewayEventHandler = async ( roles: {} } - if (d.data?.resolved !== undefined) { - for (const [id, data] of Object.entries(d.data.resolved.users ?? {})) { - await gateway.client.users.set(id, data) + if ((d.data as any)?.resolved !== undefined) { + for (const [id, data] of Object.entries( + (d.data as any)?.resolved.users ?? {} + )) { + await gateway.client.users.set(id, data as UserPayload) resolved.users[id] = ((await gateway.client.users.get( id )) as unknown) as User @@ -68,43 +84,69 @@ export const interactionCreate: GatewayEventHandler = async ( resolved.users[id].member = resolved.members[id] } - for (const [id, data] of Object.entries(d.data.resolved.members ?? {})) { + for (const [id, data] of Object.entries( + (d.data as any)?.resolved.members ?? {} + )) { const roles = await guild?.roles.array() let permissions = new Permissions(Permissions.DEFAULT) if (roles !== undefined) { const mRoles = roles.filter( - (r) => (data?.roles?.includes(r.id) as boolean) || r.id === guild?.id + (r) => + ((data as any)?.roles?.includes(r.id) as boolean) || + r.id === guild?.id ) permissions = new Permissions(mRoles.map((r) => r.permissions)) } - data.user = (d.data.resolved.users?.[id] as unknown) as UserPayload + ;(data as any).user = ((d.data as any).resolved.users?.[ + id + ] as unknown) as UserPayload resolved.members[id] = new Member( gateway.client, - data, + data as any, resolved.users[id], guild as Guild, permissions ) } - for (const [id, data] of Object.entries(d.data.resolved.roles ?? {})) { + for (const [id, data] of Object.entries( + (d.data as any).resolved.roles ?? {} + )) { if (guild !== undefined) { - await guild.roles.set(id, data) + await guild.roles.set(id, data as RolePayload) resolved.roles[id] = ((await guild.roles.get(id)) as unknown) as Role } else { resolved.roles[id] = new Role( gateway.client, - data, + data as any, (guild as unknown) as Guild ) } } - for (const [id, data] of Object.entries(d.data.resolved.channels ?? {})) { - resolved.channels[id] = new InteractionChannel(gateway.client, data) + for (const [id, data] of Object.entries( + (d.data as any).resolved.channels ?? {} + )) { + resolved.channels[id] = new InteractionChannel( + gateway.client, + data as InteractionChannelPayload + ) } } + let message: Message | undefined + if (d.message !== undefined) { + const channel = (await gateway.client.channels.get( + d.message.channel_id + ))! + message = new Message( + gateway.client, + d.message, + channel, + new User(gateway.client, d.message.author) + ) + } + let interaction if (d.type === InteractionType.APPLICATION_COMMAND) { interaction = new SlashCommandInteraction(gateway.client, d, { @@ -114,12 +156,21 @@ export const interactionCreate: GatewayEventHandler = async ( user, resolved }) + } else if (d.type === InteractionType.MESSAGE_COMPONENT) { + interaction = new MessageComponentInteraction(gateway.client, d, { + member, + guild, + channel, + user, + message + }) } else { interaction = new Interaction(gateway.client, d, { member, guild, channel, - user + user, + message }) } diff --git a/src/gateway/handlers/mod.ts b/src/gateway/handlers/mod.ts index dc649e8..561c898 100644 --- a/src/gateway/handlers/mod.ts +++ b/src/gateway/handlers/mod.ts @@ -69,6 +69,7 @@ import { applicationCommandCreate } from './applicationCommandCreate.ts' import { applicationCommandDelete } from './applicationCommandDelete.ts' import { applicationCommandUpdate } from './applicationCommandUpdate.ts' import type { SlashCommand } from '../../interactions/slashCommand.ts' +import { MessageComponentInteraction } from '../../structures/messageComponents.ts' export const gatewayHandlers: { [eventCode in GatewayEvents]: GatewayEventHandler | undefined @@ -364,7 +365,12 @@ export type ClientEvents = { * An Interaction was created * @param interaction Created interaction object */ - interactionCreate: [interaction: Interaction | SlashCommandInteraction] + interactionCreate: [ + interaction: + | Interaction + | SlashCommandInteraction + | MessageComponentInteraction + ] /** * When debug message was made diff --git a/src/interactions/slashClient.ts b/src/interactions/slashClient.ts index 9efd4c9..e254c16 100644 --- a/src/interactions/slashClient.ts +++ b/src/interactions/slashClient.ts @@ -17,6 +17,7 @@ import { User } from '../structures/user.ts' import { HarmonyEventEmitter } from '../utils/events.ts' import { encodeText, decodeText } from '../utils/encoding.ts' import { SlashCommandsManager } from './slashCommand.ts' +import { MessageComponentInteraction } from '../structures/messageComponents.ts' export type SlashCommandHandlerCallback = (interaction: Interaction) => unknown export interface SlashCommandHandler { @@ -77,8 +78,10 @@ export class SlashClient extends HarmonyEventEmitter { this.enabled = options.enabled ?? true - if (this.client?._decoratedSlash !== undefined) { - this.client._decoratedSlash.forEach((e) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const client = this.client as any + if (client?._decoratedSlash !== undefined) { + client._decoratedSlash.forEach((e: any) => { e.handler = e.handler.bind(this.client) this.handlers.push(e) }) @@ -205,7 +208,10 @@ export class SlashClient extends HarmonyEventEmitter { /** Process an incoming Interaction */ private async _process( - interaction: Interaction | SlashCommandInteraction + interaction: + | Interaction + | SlashCommandInteraction + | MessageComponentInteraction ): Promise { if (!this.enabled) return @@ -282,7 +288,7 @@ export class SlashClient extends HarmonyEventEmitter { member: payload.member as any, guild: payload.guild_id as any, channel: payload.channel_id as any, - resolved: ((payload.data + resolved: (((payload.data as any) ?.resolved as unknown) as InteractionApplicationCommandResolved) ?? { users: {}, members: {}, @@ -400,12 +406,14 @@ export class SlashClient extends HarmonyEventEmitter { /** Decorator to create a Slash Command handler */ export function slash(name?: string, guild?: string) { return function (client: Client | SlashClient | SlashModule, prop: string) { - if (client._decoratedSlash === undefined) client._decoratedSlash = [] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const c = client as any + if (c._decoratedSlash === undefined) c._decoratedSlash = [] const item = (client as { [name: string]: any })[prop] if (typeof item !== 'function') { throw new Error('@slash decorator requires a function') } else - client._decoratedSlash.push({ + c._decoratedSlash.push({ name: name ?? prop, guild, handler: item @@ -416,12 +424,14 @@ export function slash(name?: string, guild?: string) { /** Decorator to create a Sub-Slash Command handler */ export function subslash(parent: string, name?: string, guild?: string) { return function (client: Client | SlashModule | SlashClient, prop: string) { - if (client._decoratedSlash === undefined) client._decoratedSlash = [] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const c = client as any + if (c._decoratedSlash === undefined) c._decoratedSlash = [] const item = (client as { [name: string]: any })[prop] if (typeof item !== 'function') { throw new Error('@subslash decorator requires a function') } else - client._decoratedSlash.push({ + c._decoratedSlash.push({ parent, name: name ?? prop, guild, @@ -438,12 +448,14 @@ export function groupslash( guild?: string ) { return function (client: Client | SlashModule | SlashClient, prop: string) { - if (client._decoratedSlash === undefined) client._decoratedSlash = [] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const c = client as any + if (c._decoratedSlash === undefined) c._decoratedSlash = [] const item = (client as { [name: string]: any })[prop] if (typeof item !== 'function') { throw new Error('@groupslash decorator requires a function') } else - client._decoratedSlash.push({ + c._decoratedSlash.push({ group, parent, name: name ?? prop, diff --git a/src/managers/_util.ts b/src/managers/_util.ts new file mode 100644 index 0000000..8b5b670 --- /dev/null +++ b/src/managers/_util.ts @@ -0,0 +1,19 @@ +import { + MessageComponentData, + MessageComponentPayload +} from '../types/messageComponents.ts' + +export function transformComponent( + d: MessageComponentData[] +): MessageComponentPayload[] { + return d.map((e: any) => { + if (e.customID !== undefined) { + e.custom_id = e.customID + delete e.customID + } + if (e.components !== undefined) { + e.components = transformComponent(e.components) + } + return e + }) +} diff --git a/src/managers/channels.ts b/src/managers/channels.ts index 2caca4d..6810ecb 100644 --- a/src/managers/channels.ts +++ b/src/managers/channels.ts @@ -11,6 +11,7 @@ import type { import { CHANNEL } from '../types/endpoint.ts' import getChannelByType from '../utils/channel.ts' import { BaseManager } from './base.ts' +import { transformComponent } from './_util.ts' export type AllMessageOptions = MessageOptions | Embed @@ -100,7 +101,10 @@ export class ChannelsManager extends BaseManager { content: content, embed: option?.embed, file: option?.file, - components: option?.components, + components: + option?.components !== undefined + ? transformComponent(option.components) + : undefined, files: option?.files, tts: option?.tts, allowed_mentions: option?.allowedMentions, @@ -168,6 +172,10 @@ export class ChannelsManager extends BaseManager { embed: option?.embed !== undefined ? option.embed.toJSON() : undefined, // Cannot upload new files with Message // file: option?.file, + components: + option?.components !== undefined + ? transformComponent(option.components) + : undefined, tts: option?.tts, allowed_mentions: option?.allowedMentions }) diff --git a/src/rest/error.ts b/src/rest/error.ts index 3f4d4ed..1063f43 100644 --- a/src/rest/error.ts +++ b/src/rest/error.ts @@ -13,7 +13,7 @@ export class DiscordAPIError extends Error { this.message = typeof error === 'string' ? `${error} ` - : `\n${error.method.toUpperCase()} ${error.url.slice(7)} returned ${ + : `\n${error.method.toUpperCase()} ${error.url} returned ${ error.status }\n(${error.code ?? 'unknown'}) ${error.message}${ fmt.length === 0 diff --git a/src/structures/interactions.ts b/src/structures/interactions.ts index dad5e0c..f848012 100644 --- a/src/structures/interactions.ts +++ b/src/structures/interactions.ts @@ -1,4 +1,5 @@ import type { Client } from '../client/client.ts' +import { transformComponent } from '../managers/_util.ts' import { AllowedMentionsPayload, ChannelTypes, @@ -13,11 +14,14 @@ import { InteractionResponseType, InteractionType } from '../types/interactions.ts' +import { + InteractionMessageComponentData, + MessageComponentData +} from '../types/messageComponents.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' @@ -26,7 +30,6 @@ 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' @@ -47,6 +50,7 @@ export interface InteractionMessageOptions { allowedMentions?: AllowedMentionsPayload /** Whether the Message Response should be Ephemeral (only visible to User) or not */ ephemeral?: boolean + components?: MessageComponentData[] } export interface InteractionResponse extends InteractionMessageOptions { @@ -76,13 +80,6 @@ export class InteractionChannel extends SnowflakeBase { } } -export interface InteractionApplicationCommandResolved { - users: Dict - members: Dict - channels: Dict - roles: Dict -} - export class InteractionUser extends User { member?: Member } @@ -110,7 +107,8 @@ export class Interaction extends SnowflakeBase { _httpResponded?: boolean applicationID: string /** Data sent with Interaction. Only applies to Application Command */ - data?: InteractionApplicationCommandData + data?: InteractionApplicationCommandData | InteractionMessageComponentData + message?: Message constructor( client: Client, @@ -120,6 +118,7 @@ export class Interaction extends SnowflakeBase { guild?: Guild member?: Member user: User + message?: Message } ) { super(client) @@ -132,6 +131,7 @@ export class Interaction extends SnowflakeBase { this.data = data.data this.guild = others.guild this.channel = others.channel + this.message = others.message } /** Respond to an Interaction */ @@ -154,7 +154,11 @@ export class Interaction extends SnowflakeBase { embeds: data.embeds, tts: data.tts ?? false, flags, - allowed_mentions: data.allowedMentions ?? undefined + allowed_mentions: data.allowedMentions ?? undefined, + components: + data.components === undefined + ? undefined + : transformComponent(data.components) } : undefined } @@ -227,6 +231,7 @@ export class Interaction extends SnowflakeBase { embeds?: Array flags?: number | number[] allowedMentions?: AllowedMentionsPayload + components?: MessageComponentData[] }): Promise { const url = WEBHOOK_MESSAGE(this.applicationID, this.token, '@original') await this.client.rest.patch(url, { @@ -236,7 +241,11 @@ export class Interaction extends SnowflakeBase { typeof data.flags === 'object' ? data.flags.reduce((p, a) => p | a, 0) : data.flags, - allowed_mentions: data.allowedMentions + allowed_mentions: data.allowedMentions, + components: + data.components === undefined + ? undefined + : transformComponent(data.components) }) return this } diff --git a/src/structures/messageComponents.ts b/src/structures/messageComponents.ts new file mode 100644 index 0000000..1e47bd3 --- /dev/null +++ b/src/structures/messageComponents.ts @@ -0,0 +1,42 @@ +import { + InteractionMessageComponentData, + MessageComponentType +} from '../types/messageComponents.ts' +import { Interaction } from './interactions.ts' +import type { Client } from '../client/mod.ts' +import { InteractionPayload } from '../types/interactions.ts' +import type { Guild } from './guild.ts' +import type { GuildTextChannel } from './guildTextChannel.ts' +import type { Member } from './member.ts' +import type { TextChannel } from './textChannel.ts' +import { User } from './user.ts' +import { Message } from './message.ts' + +export class MessageComponentInteraction extends Interaction { + data: InteractionMessageComponentData + declare message: Message + + constructor( + client: Client, + data: InteractionPayload, + others: { + channel?: TextChannel | GuildTextChannel + guild?: Guild + member?: Member + user: User + message?: Message + } + ) { + super(client, data, others) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + this.data = data.data as InteractionMessageComponentData + } + + get customID(): string { + return this.data.custom_id + } + + get componentType(): MessageComponentType { + return this.data.component_type + } +} diff --git a/src/types/channel.ts b/src/types/channel.ts index a07e064..f6542b5 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -7,7 +7,10 @@ import type { EmojiPayload } from './emoji.ts' import type { MemberPayload } from './guild.ts' import type { InteractionType } from './interactions.ts' import type { UserPayload } from './user.ts' -import type { MessageComponentPayload } from './messageComponents.ts' +import type { + MessageComponentData, + MessageComponentPayload +} from './messageComponents.ts' export interface ChannelPayload { id: string @@ -189,6 +192,7 @@ export interface MessagePayload { flags?: number stickers?: MessageStickerPayload[] interaction?: MessageInteractionPayload + components?: MessageComponentPayload[] } export enum AllowedMentionType { @@ -211,7 +215,7 @@ export interface MessageOptions { files?: MessageAttachment[] allowedMentions?: AllowedMentionsPayload reply?: Message | MessageReference | string - components?: MessageComponentPayload[] + components?: MessageComponentData[] } export interface ChannelMention { @@ -395,6 +399,7 @@ export interface EditMessagePayload { embed?: EmbedPayload allowed_mentions?: AllowedMentionsPayload flags?: number + components?: MessageComponentPayload[] } export interface CreateMessagePayload extends EditMessagePayload { diff --git a/src/types/interactions.ts b/src/types/interactions.ts index 41962c0..01c12a4 100644 --- a/src/types/interactions.ts +++ b/src/types/interactions.ts @@ -1,5 +1,13 @@ -import { AllowedMentionsPayload, EmbedPayload } from './channel.ts' +import { + AllowedMentionsPayload, + EmbedPayload, + MessagePayload +} from './channel.ts' import type { MemberPayload } from './guild.ts' +import { + InteractionMessageComponentData, + MessageComponentData +} from './messageComponents.ts' import type { InteractionApplicationCommandData } from './slashCommands.ts' import type { UserPayload } from './user.ts' @@ -7,7 +15,9 @@ export enum InteractionType { /** Ping sent by the API (HTTP-only) */ PING = 1, /** Slash Command Interaction */ - APPLICATION_COMMAND = 2 + APPLICATION_COMMAND = 2, + /** Message Component Interaction */ + MESSAGE_COMPONENT = 3 } export interface InteractionMemberPayload extends MemberPayload { @@ -27,14 +37,17 @@ export interface InteractionPayload { /** ID of the Interaction */ id: string /** - * Data sent with the interaction. Undefined only when Interaction is not Slash Command.* + * Data sent with the interaction. Undefined only when Interaction is PING (http-only).* */ - data?: InteractionApplicationCommandData + data?: InteractionApplicationCommandData | InteractionMessageComponentData /** 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 of the Client who received interaction */ application_id: string + /** Message ID if the Interaction was of type MESSAGE_COMPONENT */ + message?: MessagePayload } export enum InteractionResponseType { @@ -62,6 +75,7 @@ export interface InteractionResponseDataPayload { /** Allowed Mentions object */ allowed_mentions?: AllowedMentionsPayload flags?: number + components?: MessageComponentData[] } export enum InteractionResponseFlags { diff --git a/src/types/messageComponents.ts b/src/types/messageComponents.ts index c666546..22d344f 100644 --- a/src/types/messageComponents.ts +++ b/src/types/messageComponents.ts @@ -17,4 +17,19 @@ export interface MessageComponentPayload { label?: string style?: ButtonStyle url?: string + custom_id?: string +} + +export interface MessageComponentData { + type: MessageComponentType + components?: MessageComponentData[] + label?: string + style?: ButtonStyle + url?: string + customID?: string +} + +export interface InteractionMessageComponentData { + custom_id: string + component_type: MessageComponentType } diff --git a/src/utils/err_fmt.ts b/src/utils/err_fmt.ts index 5bd8672..9bf1609 100644 --- a/src/utils/err_fmt.ts +++ b/src/utils/err_fmt.ts @@ -12,7 +12,7 @@ export function simplifyAPIError(errors: any): SimplifiedError { const arrayIndex = !isNaN(Number(obj[0])) if (arrayIndex) obj[0] = `[${obj[0]}]` if (acum !== '' && !arrayIndex) acum += '.' - fmt(obj[1], (acum += obj[0])) + fmt(obj[1], acum + obj[0]) }) } } diff --git a/src/utils/interactions.ts b/src/utils/interactions.ts new file mode 100644 index 0000000..0849e72 --- /dev/null +++ b/src/utils/interactions.ts @@ -0,0 +1,16 @@ +import { InteractionType } from '../../mod.ts' +import { Interaction } from '../structures/interactions.ts' +import { MessageComponentInteraction } from '../structures/messageComponents.ts' +import { SlashCommandInteraction } from '../structures/slash.ts' + +export function isSlashCommandInteraction( + d: Interaction +): d is SlashCommandInteraction { + return d.type === InteractionType.APPLICATION_COMMAND +} + +export function isMessageComponentInteraction( + d: Interaction +): d is MessageComponentInteraction { + return d.type === InteractionType.MESSAGE_COMPONENT +} diff --git a/test/components.ts b/test/components.ts new file mode 100644 index 0000000..61208f0 --- /dev/null +++ b/test/components.ts @@ -0,0 +1,184 @@ +import { + CommandClient, + Command, + CommandContext, + ButtonStyle, + MessageComponentType, + isMessageComponentInteraction, + MessageComponentInteraction, + Message +} from '../mod.ts' +import { TOKEN } from './config.ts' + +const client = new CommandClient({ + prefix: '.', + spacesAfterPrefix: true +}) + +enum Choice { + Rock, + Paper, + Scissor +} + +const games = new Map< + string, + { user: number; bot: number; msg: Message; txt: string } +>() +const components = [ + { + type: MessageComponentType.ActionRow, + components: [ + { + type: MessageComponentType.Button, + style: ButtonStyle.Primary, + label: 'Rock', + customID: 'rps::Rock' + }, + { + type: MessageComponentType.Button, + style: ButtonStyle.Primary, + label: 'Paper', + customID: 'rps::Paper' + }, + { + type: MessageComponentType.Button, + style: ButtonStyle.Primary, + label: 'Scissor', + customID: 'rps::Scissor' + } + ] + } +] + +client.once('ready', () => { + console.log('Ready!') +}) + +client.commands.add( + class extends Command { + name = 'button' + + execute(ctx: CommandContext): void { + ctx.channel.send('Test Buttons', { + components: [ + { + type: MessageComponentType.ActionRow, + components: [ + { + type: MessageComponentType.Button, + label: 'Primary', + style: ButtonStyle.Primary, + customID: '1' + }, + { + type: MessageComponentType.Button, + label: 'Secondary', + style: ButtonStyle.Secondary, + customID: '2' + }, + { + type: MessageComponentType.Button, + label: 'Destructive', + style: ButtonStyle.Destructive, + customID: '3' + }, + { + type: MessageComponentType.Button, + label: 'Success', + style: ButtonStyle.Success, + customID: '4' + }, + { + type: MessageComponentType.Button, + label: 'Link', + style: ButtonStyle.Link, + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' + } + ] + } + ] + }) + } + } +) + +client.commands.add( + class extends Command { + name = 'play' + + execute(ctx: CommandContext): any { + if (games.has(ctx.author.id)) + return ctx.message.reply('You are already playing!') + ctx.channel + .send('Game starts now!', { + components + }) + .then((msg) => { + games.set(ctx.author.id, { + user: 0, + bot: 0, + msg, + txt: 'Game starts now!' + }) + }) + } + } +) + +// client.on('raw', (e, d) => { +// if (e === 'INTERACTION_CREATE') console.log(e, d) +// }) + +client.on('interactionCreate', (i) => { + if (isMessageComponentInteraction(i) === true) { + const d = i as MessageComponentInteraction + + if (d.customID.startsWith('rps::') === true) { + const game = games.get(d.user.id) + if (game === undefined) return + const choice = d.customID.split('::')[1] + const c: number = Number(Choice[choice as any]) + const rand = Math.floor(Math.random() * 2) + + game.txt += '\n\n' + game.txt += `You: ${choice}, Bot: ${Choice[rand]}` + let msg + if (rand === c) { + msg = 'Both chose ' + Choice[rand] + '!' + } else if ( + (rand === 0 && c === 2) || + (rand === 1 && c === 0) || + (rand === 2 && c === 1) + ) { + msg = 'Bot got one point!' + game.bot++ + } else { + msg = 'You got one point!' + game.user++ + } + game.txt += '\nInfo: ' + msg + + if (game.bot === 5 || game.user === 5) { + const won = game.bot === 5 ? 'Bot' : 'You' + game.msg.edit( + `${won} won!\n\n**Points:** You: ${game.user} | Bot: ${game.bot}`, + { + components: [] + } + ) + games.delete(d.user.id) + } else { + game.msg.edit( + `${game.txt}\n\n**Points:** You: ${game.user} | Bot: ${game.bot}`, + { + components + } + ) + } + } + } +}) + +console.log('Connecting...') +client.connect(TOKEN, ['GUILDS', 'GUILD_MESSAGES', 'DIRECT_MESSAGES']) diff --git a/test/music.ts b/test/music.ts index ea7a463..c25aa31 100644 --- a/test/music.ts +++ b/test/music.ts @@ -8,12 +8,12 @@ import { CommandContext, Extension, Collection, - GuildTextChannel -} from '../../mod.ts' + GuildTextChannel, + slash, + SlashCommandInteraction +} from '../mod.ts' import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts' import { Manager, Player } from 'https://deno.land/x/lavadeno/mod.ts' -import { Interaction } from '../structures/slash.ts' -import { slash } from '../client/mod.ts' // import { SlashCommandOptionType } from '../types/slash.ts' export const nodes = [ @@ -58,12 +58,12 @@ class MyClient extends CommandClient { } @subslash('cmd', 'sub-cmd-no-grp') - subCmdNoGroup(d: Interaction): void { + subCmdNoGroup(d: SlashCommandInteraction): void { d.respond({ content: 'sub-cmd-no-group worked' }) } @groupslash('cmd', 'sub-cmd-group', 'sub-cmd') - subCmdGroup(d: Interaction): void { + subCmdGroup(d: SlashCommandInteraction): void { d.respond({ content: 'sub-cmd-group worked' }) } @@ -79,7 +79,7 @@ class MyClient extends CommandClient { } @slash() - run(d: Interaction): void { + run(d: SlashCommandInteraction): void { console.log(d.name) }