diff --git a/README.md b/README.md index c987f5c..2b4d26b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

An easy to use Discord API Library for Deno

- + Support

@@ -156,7 +156,7 @@ Documentation is available for `main` (branch) and `stable` (release). ## Found a bug or want support? Join our discord server! -[![Widget for the Discord Server](https://discord.com/api/guilds/783319033205751809/widget.png?style=banner1)](https://discord.gg/harmonyland) +[![Widget for the Discord Server](https://discord.com/api/guilds/783319033205751809/widget.png?style=banner1)](https://discord.gg/harmony) ## Maintainer diff --git a/deploy.ts b/deploy.ts index 7684b41..9e5ad9a 100644 --- a/deploy.ts +++ b/deploy.ts @@ -1,15 +1,20 @@ +import { Interaction } from './src/structures/interactions.ts ' import { SlashCommandsManager, SlashClient, SlashCommandHandlerCallback, SlashCommandHandler } from './src/interactions/mod.ts' -import { InteractionResponseType, InteractionType } from './src/types/slash.ts' +import { + InteractionResponseType, + InteractionType +} from './src/types/interactions.ts' export interface DeploySlashInitOptions { env?: boolean publicKey?: string token?: string + path?: string } /** Current Slash Client being used to handle commands */ @@ -37,8 +42,12 @@ let commands: SlashCommandsManager * * @param options Initialization options */ -export function init(options: { env: boolean }): void -export function init(options: { publicKey: string; token?: string }): void +export function init(options: { env: boolean; path?: string }): void +export function init(options: { + publicKey: string + token?: string + path?: string +}): void export function init(options: DeploySlashInitOptions): void { if (client !== undefined) throw new Error('Already initialized') if (options.env === true) { @@ -60,6 +69,9 @@ export function init(options: DeploySlashInitOptions): void { respondWith: CallableFunction request: Request }): Promise => { + if (options.path !== undefined) { + if (new URL(evt.request.url).pathname !== options.path) return + } try { // we have to wrap because there are some weird scope errors const d = await client.verifyFetchEvent({ @@ -124,8 +136,16 @@ export function handle( client.handle(cmd, handler) } +export function interactions(cb: (i: Interaction) => any): void { + client.on('interaction', cb) +} + export { commands, client } -export * from './src/types/slash.ts' +export * from './src/types/slashCommands.ts' +export * from './src/types/interactions.ts' export * from './src/structures/slash.ts' export * from './src/interactions/mod.ts' export * from './src/types/channel.ts' +export * from './src/structures/interactions.ts' +export * from './src/structures/message.ts' +export * from './src/structures/embed.ts' diff --git a/mod.ts b/mod.ts index f2a0842..901d22a 100644 --- a/mod.ts +++ b/mod.ts @@ -39,7 +39,9 @@ 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/structures/interactions.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' @@ -191,3 +193,5 @@ export { isVoiceChannel, default as getChannelByType } from './src/utils/channel.ts' +export * from './src/utils/interactions.ts' +export * from "./src/utils/command.ts" diff --git a/src/commands/client.ts b/src/commands/client.ts index 22b0117..9442f4f 100644 --- a/src/commands/client.ts +++ b/src/commands/client.ts @@ -9,6 +9,7 @@ import { CommandsManager, parseCommand } from './command.ts' +import { parseArgs } from '../utils/command.ts' import { Extension, ExtensionsManager } from './extension.ts' type PrefixReturnType = string | string[] | Promise @@ -239,7 +240,7 @@ export class CommandClient extends Client implements CommandClientOptions { client: this, name: parsed.name, prefix, - args: parsed.args, + args: parseArgs(command.args, parsed.args), argString: parsed.argString, message: msg, author: msg.author, diff --git a/src/commands/command.ts b/src/commands/command.ts index 987eaee..8601d70 100644 --- a/src/commands/command.ts +++ b/src/commands/command.ts @@ -6,7 +6,7 @@ import { Collection } from '../utils/collection.ts' import type { CommandClient } from './client.ts' import type { Extension } from './extension.ts' import { join, walk } from '../../deps.ts' - +import type { Args } from '../utils/command.ts' export interface CommandContext { /** The Client object */ client: CommandClient @@ -23,7 +23,7 @@ export interface CommandContext { /** Name of Command which was used */ name: string /** Array of Arguments used with Command */ - args: string[] + args: Record | null /** Complete Raw String of Arguments */ argString: string /** Guild which the command has called */ @@ -46,7 +46,7 @@ export interface CommandOptions { /** Usage Example of Command, only Arguments (without Prefix and Name) */ examples?: string | string[] /** Does the Command take Arguments? Maybe number of required arguments? Or list of arguments? */ - args?: number | boolean | string[] + args?: Args[] /** Permissions(s) required by both User and Bot in order to use Command */ permissions?: string | string[] /** Permission(s) required for using Command */ @@ -81,7 +81,7 @@ export class Command implements CommandOptions { extension?: Extension usage?: string | string[] examples?: string | string[] - args?: number | boolean | string[] + args?: Args[] permissions?: string | string[] userPermissions?: string | string[] botPermissions?: string | string[] diff --git a/src/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts index 3d4c5d4..43a80bb 100644 --- a/src/gateway/handlers/interactionCreate.ts +++ b/src/gateway/handlers/interactionCreate.ts @@ -1,17 +1,31 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ 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' import { User } from '../../structures/user.ts' import { Role } from '../../structures/role.ts' +import { RolePayload } from '../../types/role.ts' +import { + InteractionApplicationCommandData, + InteractionChannelPayload +} from '../../types/slashCommands.ts' +import { Message } from '../../structures/message.ts' +import { TextChannel } from '../../structures/textChannel.ts' export const interactionCreate: GatewayEventHandler = async ( gateway: Gateway, @@ -26,13 +40,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 = @@ -41,9 +64,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: {}, @@ -52,9 +75,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 InteractionApplicationCommandData)?.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 @@ -62,49 +87,87 @@ 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 InteractionApplicationCommandData)?.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 InteractionApplicationCommandData).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 InteractionApplicationCommandData).resolved?.channels ?? {} + )) { + resolved.channels[id] = new InteractionChannel( + gateway.client, + data as InteractionChannelPayload + ) } } - const interaction = new Interaction(gateway.client, d, { - member, - guild, - channel, - user, - resolved - }) + 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, { + member, + guild, + channel, + user, + resolved + }) + } else { + interaction = new Interaction(gateway.client, d, { + member, + guild, + channel, + user, + message + }) + } + 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 02f60c1..5c6bdf1 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' @@ -17,7 +18,9 @@ import { HarmonyEventEmitter } from '../utils/events.ts' import { encodeText, decodeText } from '../utils/encoding.ts' import { SlashCommandsManager } from './slashCommand.ts' -export type SlashCommandHandlerCallback = (interaction: Interaction) => unknown +export type SlashCommandHandlerCallback = ( + interaction: SlashCommandInteraction +) => unknown export interface SlashCommandHandler { name: string guild?: string @@ -185,7 +188,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 = @@ -216,28 +221,28 @@ 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 await this.emit('interaction', interaction) try { - await cmd.handler(interaction) + await cmd.handler(interaction as SlashCommandInteraction) } catch (e) { await this.emit('interactionError', e) } @@ -286,20 +291,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 as any) + ?.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 c05181c..faa5675 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/_util.ts b/src/managers/_util.ts index e7f0809..e69de29 100644 --- a/src/managers/_util.ts +++ b/src/managers/_util.ts @@ -1 +0,0 @@ -export {} diff --git a/src/managers/channels.ts b/src/managers/channels.ts index 6cb30bb..8056de3 100644 --- a/src/managers/channels.ts +++ b/src/managers/channels.ts @@ -12,8 +12,6 @@ import type { import { CHANNEL } from '../types/endpoint.ts' import getChannelByType from '../utils/channel.ts' import { BaseManager } from './base.ts' -// Deno is bugged -import {} from './_util.ts' export type AllMessageOptions = MessageOptions | Embed 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/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/rest/request.ts b/src/rest/request.ts index 273eadb..ca26850 100644 --- a/src/rest/request.ts +++ b/src/rest/request.ts @@ -90,7 +90,7 @@ export class APIRequest { ) form.append('payload_json', JSON.stringify(body)) body = form - } else { + } else if (body !== undefined) { contentType = 'application/json' body = JSON.stringify(body) } diff --git a/src/structures/interactions.ts b/src/structures/interactions.ts new file mode 100644 index 0000000..86001e9 --- /dev/null +++ b/src/structures/interactions.ts @@ -0,0 +1,348 @@ +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 { 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 { 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 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 + message?: Message + + constructor( + client: Client, + data: InteractionPayload, + others: { + channel?: TextChannel | GuildTextChannel + guild?: Guild + member?: Member + user: User + message?: Message + } + ) { + 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 + this.message = others.message + } + + /** 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 dbb82a2..ee6e839 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -5,7 +5,7 @@ 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' export interface ChannelPayload { 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..e4d4d94 --- /dev/null +++ b/src/types/interactions.ts @@ -0,0 +1,77 @@ +import { + AllowedMentionsPayload, + EmbedPayload, + MessagePayload +} 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 PING (http-only).* + */ + 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 of the Client who received interaction */ + application_id: string + /** Message ID if the Interaction was of type MESSAGE_COMPONENT */ + message?: MessagePayload +} + +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/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 -} diff --git a/src/utils/command.ts b/src/utils/command.ts new file mode 100644 index 0000000..64bd821 --- /dev/null +++ b/src/utils/command.ts @@ -0,0 +1,107 @@ +interface MentionToRegex { + [key: string]: RegExp + mentionUser: RegExp + mentionRole: RegExp + mentionChannel: RegExp +} + +const mentionToRegex: MentionToRegex = { + mentionUser: /<@!?(\d{17,19})>/, + mentionRole: /<@&(\d{17,19})>/, + mentionChannel: /<#(\d{17,19})>/ +} + +export type CommandArgumentMatchTypes = + | 'flag' + | 'mentionUser' + | 'mentionRole' + | 'mentionChannel' + | 'content' + | 'rest' + +export interface Args { + name: string + match: CommandArgumentMatchTypes + defaultValue?: T + flag?: string +} + +export function parseArgs( + commandArgs: Args[] | undefined, + messageArgs: string[] +): Record | null { + if (commandArgs === undefined) return null + + const messageArgsNullableCopy: Array = [...messageArgs] + const args: Record = {} + + for (const entry of commandArgs) { + switch (entry.match) { + case 'flag': + parseFlags(args, entry, messageArgsNullableCopy) + break + case 'mentionUser': + case 'mentionRole': + case 'mentionChannel': + parseMention(args, entry, messageArgsNullableCopy) + break + case 'content': + parseContent(args, entry, messageArgs) + break + case 'rest': + parseRest(args, entry, messageArgsNullableCopy) + break + } + } + return args +} + +function parseFlags( + args: Record, + entry: Args, + argsNullable: Array +): void { + for (let i = 0; i < argsNullable.length; i++) { + if (entry.flag === argsNullable[i]) { + argsNullable[i] = null + args[entry.name] = true + break + } else args[entry.name] = entry.defaultValue ?? false + } +} + +function parseMention( + args: Record, + entry: Args, + argsNullable: Array +): void { + const regex = mentionToRegex[entry.match] + const index = argsNullable.findIndex( + (x) => typeof x === 'string' && regex.test(x) + ) + const regexMatches = regex.exec(argsNullable[index]!) + args[entry.name] = + regexMatches !== null + ? regexMatches[0].replace(regex, '$1') + : entry.defaultValue + argsNullable[index] = null +} + +function parseContent( + args: Record, + entry: Args, + argsNonNullable: Array +): void { + args[entry.name] = + argsNonNullable.length > 0 ? argsNonNullable : entry.defaultValue +} + +function parseRest( + args: Record, + entry: Args, + argsNullable: Array +): void { + const restValues = argsNullable.filter((x) => typeof x === 'string') + args[entry.name] = + restValues.length > 0 ? restValues?.join(' ') : entry.defaultValue +} 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..c5d4249 --- /dev/null +++ b/src/utils/interactions.ts @@ -0,0 +1,9 @@ +import { InteractionType } from '../../mod.ts' +import { Interaction } from '../structures/interactions.ts' +import { SlashCommandInteraction } from '../structures/slash.ts' + +export function isSlashCommandInteraction( + d: Interaction +): d is SlashCommandInteraction { + return d.type === InteractionType.APPLICATION_COMMAND +} diff --git a/test/argsparser_test.ts b/test/argsparser_test.ts new file mode 100644 index 0000000..25777ed --- /dev/null +++ b/test/argsparser_test.ts @@ -0,0 +1,174 @@ +import { Args, parseArgs } from '../src/utils/command.ts' +import { + assertEquals, + assertNotEquals +} from 'https://deno.land/std@0.95.0/testing/asserts.ts' + +const commandArgs: Args[] = [ + { + name: 'originalMessage', + match: 'content' + }, + { + name: 'permaban', + match: 'flag', + flag: '--permanent', + defaultValue: true + }, + { + name: 'user', + match: 'mentionUser' + }, + { + name: 'reason', + match: 'rest', + defaultValue: 'ree' + } +] + +const messageArgs1: string[] = [ + '<@!708544768342229012>', + '--permanent', + 'bye', + 'bye', + 'Skyler' +] +const expectedResult1 = { + originalMessage: [ + '<@!708544768342229012>', + '--permanent', + 'bye', + 'bye', + 'Skyler' + ], + permaban: true, + user: '708544768342229012', + reason: 'bye bye Skyler' +} + +Deno.test({ + only: false, + name: 'parse command arguments 1 (assertEquals)', + fn: () => { + const result = parseArgs(commandArgs, messageArgs1) + assertEquals(result, expectedResult1) + }, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true +}) + +const messageArgs2: string[] = [ + '<@!708544768342229012>', + 'bye', + 'bye', + 'Skyler' +] +const expectedResult2 = { + originalMessage: ['<@!708544768342229012>', 'bye', 'bye', 'Skyler'], + permaban: true, + user: '708544768342229012', + reason: 'bye bye Skyler' +} + +Deno.test({ + name: 'parse command arguments 2 (assertEquals)', + fn: () => { + const result = parseArgs(commandArgs, messageArgs2) + assertEquals(result, expectedResult2) + }, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true +}) + +const messageArgs3: string[] = [ + '<@!708544768342229012>', + 'bye', + 'bye', + 'Skyler' +] +const expectedResult3 = { + permaban: false, + user: '708544768342229012', + reason: 'bye bye Skyler' +} + +Deno.test({ + name: 'parse command arguments default value (assertNotEquals)', + fn: () => { + const result = parseArgs(commandArgs, messageArgs3) + assertNotEquals(result, expectedResult3) + }, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true +}) + +const commandArgs2: Args[] = [ + { + name: 'user', + match: 'mentionUser' + }, + { + name: 'channel', + match: 'mentionChannel' + }, + { + name: 'role', + match: 'mentionRole' + }, + { + name: 'reason', + match: 'rest', + defaultValue: 'ree' + } +] + +const messageArgs4: string[] = [ + '<@!708544768342229012>', + 'bye', + '<#783319033730564098>', + '<@&836715188690092032>' +] +const expectedResult4 = { + channel: '783319033730564098', + role: '836715188690092032', + user: '708544768342229012', + reason: 'bye' +} + +Deno.test({ + name: 'parse command arguments mentions (assertEquals)', + fn: () => { + const result = parseArgs(commandArgs2, messageArgs4) + assertEquals(result, expectedResult4) + }, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true +}) + +const messageArgs5: string[] = ['<@!708544768342229012>'] +const expectedResult5 = { + user: '708544768342229012', + reason: 'No reason provided' +} +const commandArgs5: Args[] = [ + { + name: 'user', + match: 'mentionUser' + }, + { + name: 'reason', + match: 'rest', + defaultValue: 'No reason provided' + } +] +Deno.test({ + name: 'parse command arguments, rest match default', + fn: () => { + const result = parseArgs(commandArgs5, messageArgs5) + assertEquals(result, expectedResult5) + } +}) diff --git a/test/music.ts b/test/music.ts index 9f621ee..c25aa31 100644 --- a/test/music.ts +++ b/test/music.ts @@ -9,8 +9,8 @@ import { Extension, Collection, GuildTextChannel, - Interaction, - slash + 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' @@ -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) }