diff --git a/src/gateway/handlers/index.ts b/src/gateway/handlers/index.ts index af1b69d..a85d8fc 100644 --- a/src/gateway/handlers/index.ts +++ b/src/gateway/handlers/index.ts @@ -330,7 +330,7 @@ export interface ClientEvents extends EventTypes { */ webhooksUpdate: (guild: Guild, channel: GuildTextChannel) => void /** - * A Slash Command was triggered + * An Interaction was created */ interactionCreate: (interaction: Interaction) => void } diff --git a/src/models/client.ts b/src/models/client.ts index 0b3d4da..a807fb9 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -15,6 +15,7 @@ import { Extension } from './extensions.ts' import { SlashClient } from './slashClient.ts' import { Interaction } from '../structures/slash.ts' import { SlashModule } from './slashModule.ts' +import type { ShardManager } from './shard.ts' /** OS related properties sent with Gateway Identify */ export interface ClientProperties { @@ -93,6 +94,8 @@ export class Client extends EventEmitter { _decoratedSlash?: Array<{ name: string guild?: string + parent?: string + group?: string handler: (interaction: Interaction) => any }> @@ -110,6 +113,11 @@ export class Client extends EventEmitter { ...args: Parameters ): boolean => this._untypedEmit(event, ...args) + /** Shard on which this Client is */ + shard: number = 0 + /** Shard Manager of this Client if Sharded */ + shardManager?: ShardManager + constructor(options: ClientOptions = {}) { super() this.token = options.token @@ -231,6 +239,47 @@ export function slash(name?: string, guild?: string) { } } +export function subslash(parent: string, name?: string, guild?: string) { + return function (client: Client | SlashModule, prop: string) { + if (client._decoratedSlash === undefined) client._decoratedSlash = [] + const item = (client as { [name: string]: any })[prop] + if (typeof item !== 'function') { + item.parent = parent + client._decoratedSlash.push(item) + } else + client._decoratedSlash.push({ + parent, + name: name ?? prop, + guild, + handler: item + }) + } +} + +export function groupslash( + parent: string, + group: string, + name?: string, + guild?: string +) { + return function (client: Client | SlashModule, prop: string) { + if (client._decoratedSlash === undefined) client._decoratedSlash = [] + const item = (client as { [name: string]: any })[prop] + if (typeof item !== 'function') { + item.parent = parent + item.group = group + client._decoratedSlash.push(item) + } else + client._decoratedSlash.push({ + group, + parent, + name: name ?? prop, + guild, + handler: item + }) + } +} + export function slashModule() { return function (client: Client, prop: string) { if (client._decoratedSlashModules === undefined) diff --git a/src/models/command.ts b/src/models/command.ts index d2fc939..2cbc033 100644 --- a/src/models/command.ts +++ b/src/models/command.ts @@ -503,12 +503,13 @@ export const parseCommand = ( client: CommandClient, msg: Message, prefix: string -): ParsedCommand => { +): ParsedCommand | undefined => { let content = msg.content.slice(prefix.length) if (client.spacesAfterPrefix === true) content = content.trim() const args = parse(content)._.map((e) => e.toString()) - const name = args.shift() as string + const name = args.shift() + if (name === undefined) return const argString = content.slice(name.length).trim() return { diff --git a/src/models/commandClient.ts b/src/models/commandClient.ts index 7fba315..72da0fe 100644 --- a/src/models/commandClient.ts +++ b/src/models/commandClient.ts @@ -187,6 +187,7 @@ export class CommandClient extends Client implements CommandClientOptions { prefix = usedPrefix const parsed = parseCommand(this, msg, prefix) + if (parsed === undefined) return const command = this.commands.fetch(parsed) if (command === undefined) return diff --git a/src/models/shard.ts b/src/models/shard.ts index a9eed47..52927cb 100644 --- a/src/models/shard.ts +++ b/src/models/shard.ts @@ -1 +1,69 @@ -// TODO: write code +import { Collection } from '../utils/collection.ts' +import { Client, ClientOptions } from './client.ts' +import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts' +import { RESTManager } from './rest.ts' +// import { GATEWAY_BOT } from '../types/endpoint.ts' +// import { GatewayBotPayload } from '../types/gatewayBot.ts' + +// TODO(DjDeveloperr) +// I'm kinda confused; will continue on this later once +// Deno namespace in Web Woker is stable! +export interface ShardManagerOptions { + client: Client | typeof Client + token?: string + intents?: number[] + options?: ClientOptions + shards: number +} + +export interface ShardManagerInitOptions { + file: string + token?: string + intents?: number[] + options?: ClientOptions + shards?: number +} + +export class ShardManager extends EventEmitter { + workers: Collection = new Collection() + token: string + intents: number[] + shardCount: number + private readonly __client: Client + + get rest(): RESTManager { + return this.__client.rest + } + + constructor(options: ShardManagerOptions) { + super() + this.__client = + options.client instanceof Client + ? options.client + : // eslint-disable-next-line new-cap + new options.client(options.options) + + if (this.__client.token === undefined || options.token === undefined) + throw new Error('Token should be provided when constructing ShardManager') + if (this.__client.intents === undefined || options.intents === undefined) + throw new Error( + 'Intents should be provided when constructing ShardManager' + ) + + this.token = this.__client.token ?? options.token + this.intents = this.__client.intents ?? options.intents + this.shardCount = options.shards + } + + // static async init(): Promise {} + + // async start(): Promise { + // const info = ((await this.rest.get( + // GATEWAY_BOT() + // )) as unknown) as GatewayBotPayload + + // const totalShards = this.__shardCount ?? info.shards + + // return this + // } +} diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index c397dde..74a0ea3 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -34,7 +34,7 @@ export class SlashCommand { this.applicationID = data.application_id this.name = data.name this.description = data.description - this.options = data.options + this.options = data.options ?? [] } async delete(): Promise { @@ -158,6 +158,8 @@ export type SlashCommandHandlerCallback = (interaction: Interaction) => any export interface SlashCommandHandler { name: string guild?: string + parent?: string + group?: string handler: SlashCommandHandlerCallback } @@ -182,38 +184,45 @@ export class SlashClient { } this.client.on('interactionCreate', (interaction) => - this.process(interaction) + this._process(interaction) ) } /** Adds a new Slash Command Handler */ - handle( - name: string, - handler: SlashCommandHandlerCallback, - guild?: string - ): SlashClient { - this.handlers.push({ - name, - guild, - handler - }) + handle(handler: SlashCommandHandler): SlashClient { + this.handlers.push(handler) return this } + private _getCommand(i: Interaction): SlashCommandHandler | undefined { + return this.handlers.find((e) => { + const hasGroupOrParent = e.group !== undefined || e.parent !== undefined + const groupMatched = + e.group !== undefined && e.parent !== undefined + ? i.options + .find((o) => o.name === e.group) + ?.options?.find((o) => o.name === e.name) !== undefined + : true + const subMatched = + e.group === undefined && e.parent !== undefined + ? i.options.find((o) => o.name === e.name) !== undefined + : true + const nameMatched1 = e.name === i.name + const parentMatched = hasGroupOrParent ? e.parent === i.name : true + const nameMatched = hasGroupOrParent ? parentMatched : nameMatched1 + + const matched = groupMatched && subMatched && nameMatched + return matched + }) + } + /** Process an incoming Slash Command (interaction) */ - private process(interaction: Interaction): void { + private _process(interaction: Interaction): void { if (!this.enabled) return if (interaction.type !== InteractionType.APPLICATION_COMMAND) return - let cmd - - if (interaction.guild !== undefined) - cmd = - this.handlers.find( - (e) => e.guild !== undefined && e.name === interaction.name - ) ?? this.handlers.find((e) => e.name === interaction.name) - else cmd = this.handlers.find((e) => e.name === interaction.name) + const cmd = this._getCommand(interaction) if (cmd === undefined) return diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 81666ff..0a4f427 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -3,6 +3,7 @@ import { MessageOption } from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { InteractionData, + InteractionOption, InteractionPayload, InteractionResponsePayload, InteractionResponseType @@ -76,10 +77,15 @@ export class Interaction { return this.data.name } - option(name: string): T { - return this.data.options.find((e) => e.name === name)?.value + get options(): InteractionOption[] { + return this.data.options ?? [] } + option(name: string): T { + return this.options.find((e) => e.name === name)?.value + } + + /** Respond to an Interaction */ async respond(data: InteractionResponse): Promise { const payload: InteractionResponsePayload = { type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, @@ -105,6 +111,7 @@ export class Interaction { return this } + /** Edit the original Interaction response */ async editResponse(data: { content?: string embeds?: Embed[] @@ -121,6 +128,7 @@ export class Interaction { return this } + /** Delete the original Interaction Response */ async deleteResponse(): Promise { const url = WEBHOOK_MESSAGE( this.client.user?.id as string, @@ -135,6 +143,7 @@ export class Interaction { return `https://discord.com/api/v8/webhooks/${this.client.user?.id}/${this.token}` } + /** Send a followup message */ async send( text?: string | AllWebhookMessageOptions, option?: AllWebhookMessageOptions @@ -195,6 +204,7 @@ export class Interaction { return res } + /** Edit a Followup message */ async editMessage( msg: Message | string, data: { diff --git a/src/test/music.ts b/src/test/music.ts index e43b2a4..3801237 100644 --- a/src/test/music.ts +++ b/src/test/music.ts @@ -3,6 +3,8 @@ import { event, Intents, command, + subslash, + groupslash, CommandContext, Extension, Collection @@ -11,7 +13,10 @@ import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts' import { Manager, Player -} from 'https://raw.githubusercontent.com/DjDeveloperr/lavaclient-deno/master/mod.ts' +} from 'https://raw.githubusercontent.com/Lavaclient/lavadeno/master/mod.ts' +import { Interaction } from '../structures/slash.ts' +import { slash } from '../models/client.ts' +// import { SlashCommandOptionType } from '../types/slash.ts' export const nodes = [ { @@ -54,10 +59,68 @@ class MyClient extends CommandClient { }) } + @subslash('cmd', 'sub-cmd-no-grp') + subCmdNoGrp(d: Interaction): void { + d.respond({ content: 'sub-cmd-no-group worked' }) + } + + @groupslash('cmd', 'sub-cmd-group', 'sub-cmd') + subCmdGrp(d: Interaction): void { + d.respond({ content: 'sub-cmd-group worked' }) + } + + @slash() + run(d: Interaction): void { + console.log(d.name) + } + @event() ready(): void { console.log(`Logged in as ${this.user?.tag}!`) this.manager.init(this.user?.id as string) + // client.slash.commands.create( + // { + // name: 'cmd', + // description: 'Parent command', + // options: [ + // { + // name: 'sub-cmd-group', + // type: SlashCommandOptionType.SUB_COMMAND_GROUP, + // description: 'Sub Cmd Group', + // options: [ + // { + // name: 'sub-cmd', + // type: SlashCommandOptionType.SUB_COMMAND, + // description: 'Sub Cmd' + // } + // ] + // }, + // { + // name: 'sub-cmd-no-grp', + // type: SlashCommandOptionType.SUB_COMMAND, + // description: 'Sub Cmd' + // }, + // { + // name: 'sub-cmd-grp-2', + // type: SlashCommandOptionType.SUB_COMMAND_GROUP, + // description: 'Sub Cmd Group 2', + // options: [ + // { + // name: 'sub-cmd-1', + // type: SlashCommandOptionType.SUB_COMMAND, + // description: 'Sub Cmd 1' + // }, + // { + // name: 'sub-cmd-2', + // type: SlashCommandOptionType.SUB_COMMAND, + // description: 'Sub Cmd 2' + // } + // ] + // } + // ] + // }, + // '783319033205751809' + // ) } } diff --git a/src/test/slash-cmd.ts b/src/test/slash-cmd.ts deleted file mode 100644 index 043b96d..0000000 --- a/src/test/slash-cmd.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { TOKEN } from './config.ts' - -export const CMD = { - name: 'blep', - description: 'Send a random adorable animal photo', - options: [ - { - name: 'animal', - description: 'The type of animal', - type: 3, - required: true, - choices: [ - { - name: 'Dog', - value: 'animal_dog' - }, - { - name: 'Cat', - value: 'animal_dog' - }, - { - name: 'Penguin', - value: 'animal_penguin' - } - ] - }, - { - name: 'only_smol', - description: 'Whether to show only baby animals', - type: 5, - required: false - } - ] -} - -// fetch('https://discord.com/api/v8/applications/783937840752099332/commands', { -fetch( - 'https://discord.com/api/v8/applications/783937840752099332/guilds/783319033205751809/commands', - { - method: 'POST', - body: JSON.stringify(CMD), - headers: { - 'Content-Type': 'application/json', - Authorization: - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - 'Bot ' + TOKEN - } - } -) - .then((r) => r.json()) - .then(console.log) diff --git a/src/test/slash.ts b/src/test/slash.ts index 5a1c3b9..07e9ac0 100644 --- a/src/test/slash.ts +++ b/src/test/slash.ts @@ -12,7 +12,7 @@ export class MyClient extends Client { @slash() send(d: Interaction): void { d.respond({ - content: d.data.options.find((e) => e.name === 'content')?.value + content: d.data.options?.find((e) => e.name === 'content')?.value }) } @@ -26,7 +26,7 @@ export class MyClient extends Client { content: 'This command can only be used by owner!' }) } else { - const code = d.data.options.find((e) => e.name === 'code') + const code = d.data.options?.find((e) => e.name === 'code') ?.value as string try { // eslint-disable-next-line no-eval @@ -50,7 +50,7 @@ export class MyClient extends Client { @slash() async hug(d: Interaction): Promise { - const id = d.data.options.find((e) => e.name === 'user')?.value as string + const id = d.data.options?.find((e) => e.name === 'user')?.value as string const user = (await client.users.get(id)) ?? (await client.users.fetch(id)) const url = await fetch('https://nekos.life/api/v2/img/hug') .then((r) => r.json()) @@ -68,7 +68,7 @@ export class MyClient extends Client { @slash() async kiss(d: Interaction): Promise { - const id = d.data.options.find((e) => e.name === 'user')?.value as string + const id = d.data.options?.find((e) => e.name === 'user')?.value as string const user = (await client.users.get(id)) ?? (await client.users.fetch(id)) const url = await fetch('https://nekos.life/api/v2/img/kiss') .then((r) => r.json()) diff --git a/src/types/slash.ts b/src/types/slash.ts index 6e5290c..d48f363 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -2,34 +2,54 @@ import { EmbedPayload } from './channel.ts' import { MemberPayload } from './guild.ts' export interface InteractionOption { + /** Option name */ name: string + /** Value of the option */ value?: any + /** Sub options */ options?: any[] } export interface InteractionData { + /** Name of the Slash Command */ name: string + /** Unique ID of the Slash Command */ id: string + /** Options (arguments) sent with Interaction */ options: InteractionOption[] } export enum InteractionType { + /** Ping sent by the API (HTTP-only) */ PING = 1, + /** Slash Command Interaction */ APPLICATION_COMMAND = 2 } export interface InteractionPayload { + /** Type of the Interaction */ type: InteractionType + /** Token of the Interaction to respond */ token: string + /** Member object of user who invoked */ member: MemberPayload + /** ID of the Interaction */ id: string + /** + * Data sent with the interaction + * **This can be undefined only when Interaction is not a Slash Command** + */ data: InteractionData + /** ID of the Guild in which Interaction was invoked */ guild_id: string + /** ID of the Channel in which Interaction was invoked */ channel_id: string } export interface SlashCommandChoice { + /** (Display) name of the Choice */ name: string + /** Actual value to be sent in Interaction */ value: string } @@ -48,7 +68,8 @@ export interface SlashCommandOption { name: string description: string type: SlashCommandOptionType - required: boolean + required?: boolean + default?: boolean choices?: SlashCommandChoice[] options?: SlashCommandOption[] } @@ -56,7 +77,7 @@ export interface SlashCommandOption { export interface SlashCommandPartial { name: string description: string - options: SlashCommandOption[] + options?: SlashCommandOption[] } export interface SlashCommandPayload extends SlashCommandPartial { @@ -65,10 +86,15 @@ export interface SlashCommandPayload extends SlashCommandPartial { } export enum InteractionResponseType { + /** Just ack a ping, Http-only. */ PONG = 1, + /** Do nothing, just acknowledge the Interaction */ ACKNOWLEDGE = 2, + /** Send a channel message without " used / with " */ CHANNEL_MESSAGE = 3, + /** Send a channel message with " used / with " */ CHANNEL_MESSAGE_WITH_SOURCE = 4, + /** Send nothing further, but send " used / with " */ ACK_WITH_SOURCE = 5 } @@ -88,3 +114,8 @@ export interface InteractionResponseDataPayload { } flags?: number } + +export enum InteractionResponseFlags { + /** A Message which is only visible to Interaction User, and is not saved on backend */ + EPHEMERAL = 1 << 6 +}