Merge pull request #61 from DjDeveloperr/slash

Add Sub Commands and Sub Command Group handlers and decorators
This commit is contained in:
Helloyunho 2020-12-16 22:52:08 +09:00 committed by GitHub
commit 803700a0cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 266 additions and 85 deletions

View File

@ -330,7 +330,7 @@ export interface ClientEvents extends EventTypes {
*/ */
webhooksUpdate: (guild: Guild, channel: GuildTextChannel) => void webhooksUpdate: (guild: Guild, channel: GuildTextChannel) => void
/** /**
* A Slash Command was triggered * An Interaction was created
*/ */
interactionCreate: (interaction: Interaction) => void interactionCreate: (interaction: Interaction) => void
} }

View File

@ -15,6 +15,7 @@ import { Extension } from './extensions.ts'
import { SlashClient } from './slashClient.ts' import { SlashClient } from './slashClient.ts'
import { Interaction } from '../structures/slash.ts' import { Interaction } from '../structures/slash.ts'
import { SlashModule } from './slashModule.ts' import { SlashModule } from './slashModule.ts'
import type { ShardManager } from './shard.ts'
/** OS related properties sent with Gateway Identify */ /** OS related properties sent with Gateway Identify */
export interface ClientProperties { export interface ClientProperties {
@ -93,6 +94,8 @@ export class Client extends EventEmitter {
_decoratedSlash?: Array<{ _decoratedSlash?: Array<{
name: string name: string
guild?: string guild?: string
parent?: string
group?: string
handler: (interaction: Interaction) => any handler: (interaction: Interaction) => any
}> }>
@ -110,6 +113,11 @@ export class Client extends EventEmitter {
...args: Parameters<ClientEvents[K]> ...args: Parameters<ClientEvents[K]>
): boolean => this._untypedEmit(event, ...args) ): 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 = {}) { constructor(options: ClientOptions = {}) {
super() super()
this.token = options.token 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() { export function slashModule() {
return function (client: Client, prop: string) { return function (client: Client, prop: string) {
if (client._decoratedSlashModules === undefined) if (client._decoratedSlashModules === undefined)

View File

@ -503,12 +503,13 @@ export const parseCommand = (
client: CommandClient, client: CommandClient,
msg: Message, msg: Message,
prefix: string prefix: string
): ParsedCommand => { ): ParsedCommand | undefined => {
let content = msg.content.slice(prefix.length) let content = msg.content.slice(prefix.length)
if (client.spacesAfterPrefix === true) content = content.trim() if (client.spacesAfterPrefix === true) content = content.trim()
const args = parse(content)._.map((e) => e.toString()) 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() const argString = content.slice(name.length).trim()
return { return {

View File

@ -187,6 +187,7 @@ export class CommandClient extends Client implements CommandClientOptions {
prefix = usedPrefix prefix = usedPrefix
const parsed = parseCommand(this, msg, prefix) const parsed = parseCommand(this, msg, prefix)
if (parsed === undefined) return
const command = this.commands.fetch(parsed) const command = this.commands.fetch(parsed)
if (command === undefined) return if (command === undefined) return

View File

@ -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<string, Worker> = 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<ShardManager> {}
// async start(): Promise<ShardManager> {
// const info = ((await this.rest.get(
// GATEWAY_BOT()
// )) as unknown) as GatewayBotPayload
// const totalShards = this.__shardCount ?? info.shards
// return this
// }
}

View File

@ -34,7 +34,7 @@ export class SlashCommand {
this.applicationID = data.application_id this.applicationID = data.application_id
this.name = data.name this.name = data.name
this.description = data.description this.description = data.description
this.options = data.options this.options = data.options ?? []
} }
async delete(): Promise<void> { async delete(): Promise<void> {
@ -158,6 +158,8 @@ export type SlashCommandHandlerCallback = (interaction: Interaction) => any
export interface SlashCommandHandler { export interface SlashCommandHandler {
name: string name: string
guild?: string guild?: string
parent?: string
group?: string
handler: SlashCommandHandlerCallback handler: SlashCommandHandlerCallback
} }
@ -182,38 +184,45 @@ export class SlashClient {
} }
this.client.on('interactionCreate', (interaction) => this.client.on('interactionCreate', (interaction) =>
this.process(interaction) this._process(interaction)
) )
} }
/** Adds a new Slash Command Handler */ /** Adds a new Slash Command Handler */
handle( handle(handler: SlashCommandHandler): SlashClient {
name: string, this.handlers.push(handler)
handler: SlashCommandHandlerCallback,
guild?: string
): SlashClient {
this.handlers.push({
name,
guild,
handler
})
return this 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) */ /** Process an incoming Slash Command (interaction) */
private process(interaction: Interaction): void { private _process(interaction: Interaction): void {
if (!this.enabled) return if (!this.enabled) return
if (interaction.type !== InteractionType.APPLICATION_COMMAND) return if (interaction.type !== InteractionType.APPLICATION_COMMAND) return
let cmd const cmd = this._getCommand(interaction)
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)
if (cmd === undefined) return if (cmd === undefined) return

View File

@ -3,6 +3,7 @@ import { MessageOption } from '../types/channel.ts'
import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts'
import { import {
InteractionData, InteractionData,
InteractionOption,
InteractionPayload, InteractionPayload,
InteractionResponsePayload, InteractionResponsePayload,
InteractionResponseType InteractionResponseType
@ -76,10 +77,15 @@ export class Interaction {
return this.data.name return this.data.name
} }
option<T = any>(name: string): T { get options(): InteractionOption[] {
return this.data.options.find((e) => e.name === name)?.value return this.data.options ?? []
} }
option<T = any>(name: string): T {
return this.options.find((e) => e.name === name)?.value
}
/** Respond to an Interaction */
async respond(data: InteractionResponse): Promise<Interaction> { async respond(data: InteractionResponse): Promise<Interaction> {
const payload: InteractionResponsePayload = { const payload: InteractionResponsePayload = {
type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
@ -105,6 +111,7 @@ export class Interaction {
return this return this
} }
/** Edit the original Interaction response */
async editResponse(data: { async editResponse(data: {
content?: string content?: string
embeds?: Embed[] embeds?: Embed[]
@ -121,6 +128,7 @@ export class Interaction {
return this return this
} }
/** Delete the original Interaction Response */
async deleteResponse(): Promise<Interaction> { async deleteResponse(): Promise<Interaction> {
const url = WEBHOOK_MESSAGE( const url = WEBHOOK_MESSAGE(
this.client.user?.id as string, 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}` return `https://discord.com/api/v8/webhooks/${this.client.user?.id}/${this.token}`
} }
/** Send a followup message */
async send( async send(
text?: string | AllWebhookMessageOptions, text?: string | AllWebhookMessageOptions,
option?: AllWebhookMessageOptions option?: AllWebhookMessageOptions
@ -195,6 +204,7 @@ export class Interaction {
return res return res
} }
/** Edit a Followup message */
async editMessage( async editMessage(
msg: Message | string, msg: Message | string,
data: { data: {

View File

@ -3,6 +3,8 @@ import {
event, event,
Intents, Intents,
command, command,
subslash,
groupslash,
CommandContext, CommandContext,
Extension, Extension,
Collection Collection
@ -11,7 +13,10 @@ import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts'
import { import {
Manager, Manager,
Player 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 = [ 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() @event()
ready(): void { ready(): void {
console.log(`Logged in as ${this.user?.tag}!`) console.log(`Logged in as ${this.user?.tag}!`)
this.manager.init(this.user?.id as string) 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'
// )
} }
} }

View File

@ -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)

View File

@ -12,7 +12,7 @@ export class MyClient extends Client {
@slash() @slash()
send(d: Interaction): void { send(d: Interaction): void {
d.respond({ 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!' content: 'This command can only be used by owner!'
}) })
} else { } else {
const code = d.data.options.find((e) => e.name === 'code') const code = d.data.options?.find((e) => e.name === 'code')
?.value as string ?.value as string
try { try {
// eslint-disable-next-line no-eval // eslint-disable-next-line no-eval
@ -50,7 +50,7 @@ export class MyClient extends Client {
@slash() @slash()
async hug(d: Interaction): Promise<void> { async hug(d: Interaction): Promise<void> {
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 user = (await client.users.get(id)) ?? (await client.users.fetch(id))
const url = await fetch('https://nekos.life/api/v2/img/hug') const url = await fetch('https://nekos.life/api/v2/img/hug')
.then((r) => r.json()) .then((r) => r.json())
@ -68,7 +68,7 @@ export class MyClient extends Client {
@slash() @slash()
async kiss(d: Interaction): Promise<void> { async kiss(d: Interaction): Promise<void> {
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 user = (await client.users.get(id)) ?? (await client.users.fetch(id))
const url = await fetch('https://nekos.life/api/v2/img/kiss') const url = await fetch('https://nekos.life/api/v2/img/kiss')
.then((r) => r.json()) .then((r) => r.json())

View File

@ -2,34 +2,54 @@ import { EmbedPayload } from './channel.ts'
import { MemberPayload } from './guild.ts' import { MemberPayload } from './guild.ts'
export interface InteractionOption { export interface InteractionOption {
/** Option name */
name: string name: string
/** Value of the option */
value?: any value?: any
/** Sub options */
options?: any[] options?: any[]
} }
export interface InteractionData { export interface InteractionData {
/** Name of the Slash Command */
name: string name: string
/** Unique ID of the Slash Command */
id: string id: string
/** Options (arguments) sent with Interaction */
options: InteractionOption[] options: InteractionOption[]
} }
export enum InteractionType { export enum InteractionType {
/** Ping sent by the API (HTTP-only) */
PING = 1, PING = 1,
/** Slash Command Interaction */
APPLICATION_COMMAND = 2 APPLICATION_COMMAND = 2
} }
export interface InteractionPayload { export interface InteractionPayload {
/** Type of the Interaction */
type: InteractionType type: InteractionType
/** Token of the Interaction to respond */
token: string token: string
/** Member object of user who invoked */
member: MemberPayload member: MemberPayload
/** ID of the Interaction */
id: string id: string
/**
* Data sent with the interaction
* **This can be undefined only when Interaction is not a Slash Command**
*/
data: InteractionData data: InteractionData
/** ID of the Guild in which Interaction was invoked */
guild_id: string guild_id: string
/** ID of the Channel in which Interaction was invoked */
channel_id: string channel_id: string
} }
export interface SlashCommandChoice { export interface SlashCommandChoice {
/** (Display) name of the Choice */
name: string name: string
/** Actual value to be sent in Interaction */
value: string value: string
} }
@ -48,7 +68,8 @@ export interface SlashCommandOption {
name: string name: string
description: string description: string
type: SlashCommandOptionType type: SlashCommandOptionType
required: boolean required?: boolean
default?: boolean
choices?: SlashCommandChoice[] choices?: SlashCommandChoice[]
options?: SlashCommandOption[] options?: SlashCommandOption[]
} }
@ -56,7 +77,7 @@ export interface SlashCommandOption {
export interface SlashCommandPartial { export interface SlashCommandPartial {
name: string name: string
description: string description: string
options: SlashCommandOption[] options?: SlashCommandOption[]
} }
export interface SlashCommandPayload extends SlashCommandPartial { export interface SlashCommandPayload extends SlashCommandPartial {
@ -65,10 +86,15 @@ export interface SlashCommandPayload extends SlashCommandPartial {
} }
export enum InteractionResponseType { export enum InteractionResponseType {
/** Just ack a ping, Http-only. */
PONG = 1, PONG = 1,
/** Do nothing, just acknowledge the Interaction */
ACKNOWLEDGE = 2, ACKNOWLEDGE = 2,
/** Send a channel message without "<User> used /<Command> with <Bot>" */
CHANNEL_MESSAGE = 3, CHANNEL_MESSAGE = 3,
/** Send a channel message with "<User> used /<Command> with <Bot>" */
CHANNEL_MESSAGE_WITH_SOURCE = 4, CHANNEL_MESSAGE_WITH_SOURCE = 4,
/** Send nothing further, but send "<User> used /<Command> with <Bot>" */
ACK_WITH_SOURCE = 5 ACK_WITH_SOURCE = 5
} }
@ -88,3 +114,8 @@ export interface InteractionResponseDataPayload {
} }
flags?: number flags?: number
} }
export enum InteractionResponseFlags {
/** A Message which is only visible to Interaction User, and is not saved on backend */
EPHEMERAL = 1 << 6
}