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
/**
* A Slash Command was triggered
* An Interaction was created
*/
interactionCreate: (interaction: Interaction) => void
}

View File

@ -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<ClientEvents[K]>
): 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)

View File

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

View File

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

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.name = data.name
this.description = data.description
this.options = data.options
this.options = data.options ?? []
}
async delete(): Promise<void> {
@ -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

View File

@ -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<T = any>(name: string): T {
return this.data.options.find((e) => e.name === name)?.value
get options(): InteractionOption[] {
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> {
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<Interaction> {
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: {

View File

@ -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'
// )
}
}

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()
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<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 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<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 url = await fetch('https://nekos.life/api/v2/img/kiss')
.then((r) => r.json())

View File

@ -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 "<User> used /<Command> with <Bot>" */
CHANNEL_MESSAGE = 3,
/** Send a channel message with "<User> used /<Command> with <Bot>" */
CHANNEL_MESSAGE_WITH_SOURCE = 4,
/** Send nothing further, but send "<User> used /<Command> with <Bot>" */
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
}