feat(decorators): add support for @event and @command decorators

This commit is contained in:
DjDeveloperr 2020-12-06 12:58:01 +05:30
parent f6c307844f
commit 895b13a9b1
6 changed files with 164 additions and 11 deletions

4
mod.ts
View file

@ -12,13 +12,13 @@ export {
CommandsManager, CommandsManager,
CategoriesManager CategoriesManager
} from './src/models/command.ts' } from './src/models/command.ts'
export type { CommandContext } from './src/models/command.ts' export type { CommandContext, CommandOptions } from './src/models/command.ts'
export { export {
Extension, Extension,
ExtensionCommands, ExtensionCommands,
ExtensionsManager ExtensionsManager
} from './src/models/extensions.ts' } from './src/models/extensions.ts'
export { CommandClient } from './src/models/commandClient.ts' export { CommandClient, command } from './src/models/commandClient.ts'
export type { CommandClientOptions } from './src/models/commandClient.ts' export type { CommandClientOptions } from './src/models/commandClient.ts'
export { BaseManager } from './src/managers/base.ts' export { BaseManager } from './src/managers/base.ts'
export { BaseChildManager } from './src/managers/baseChild.ts' export { BaseChildManager } from './src/managers/baseChild.ts'

View file

@ -70,6 +70,7 @@ export class Client extends EventEmitter {
canary: boolean = false canary: boolean = false
/** Client's presence. Startup one if set before connecting */ /** Client's presence. Startup one if set before connecting */
presence: ClientPresence = new ClientPresence() presence: ClientPresence = new ClientPresence()
_decoratedEvents?: { [name: string]: (...args: any[]) => any }
private readonly _untypedOn = this.on private readonly _untypedOn = this.on
@ -101,6 +102,16 @@ export class Client extends EventEmitter {
this.reactionCacheLifetime = options.reactionCacheLifetime this.reactionCacheLifetime = options.reactionCacheLifetime
if (options.fetchUncachedReactions === true) if (options.fetchUncachedReactions === true)
this.fetchUncachedReactions = true this.fetchUncachedReactions = true
if (
this._decoratedEvents !== undefined &&
Object.keys(this._decoratedEvents).length !== 0
) {
Object.entries(this._decoratedEvents).forEach((entry) => {
this.on(entry[0], entry[1])
})
this._decoratedEvents = undefined
}
} }
/** /**
@ -127,6 +138,21 @@ export class Client extends EventEmitter {
this.emit('debug', `[${tag}] ${msg}`) this.emit('debug', `[${tag}] ${msg}`)
} }
/**
* EXPERIMENTAL Decorators support for listening to events.
* @param event Event name to listen for
*/
event(event: string): CallableFunction {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const parent = this
return function (
target: { [name: string]: CallableFunction },
prop: string
) {
parent.addListener(event, target[prop] as (...args: any[]) => any)
}
}
// TODO(DjDeveloperr): Implement this // TODO(DjDeveloperr): Implement this
// fetchApplication(): Promise<Application> // fetchApplication(): Promise<Application>
@ -153,3 +179,13 @@ export class Client extends EventEmitter {
this.gateway = new Gateway(this, token, intents) this.gateway = new Gateway(this, token, intents)
} }
} }
export function event(name?: string) {
return function (client: Client, prop: string) {
const listener = ((client as unknown) as {
[name: string]: (...args: any[]) => any
})[prop]
if (client._decoratedEvents === undefined) client._decoratedEvents = {}
client._decoratedEvents[name === undefined ? prop : name] = listener
}
}

View file

@ -5,6 +5,7 @@ import { User } from '../structures/user.ts'
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import { CommandClient } from './commandClient.ts' import { CommandClient } from './commandClient.ts'
import { Extension } from './extensions.ts' import { Extension } from './extensions.ts'
import { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts'
export interface CommandContext { export interface CommandContext {
/** The Client object */ /** The Client object */
@ -29,9 +30,9 @@ export interface CommandContext {
guild?: Guild guild?: Guild
} }
export class Command { export interface CommandOptions {
/** Name of the Command */ /** Name of the Command */
name: string = '' name?: string
/** Description of the Command */ /** Description of the Command */
description?: string description?: string
/** Category of the Command */ /** Category of the Command */
@ -66,6 +67,27 @@ export class Command {
dmOnly?: boolean dmOnly?: boolean
/** Whether the Command can only be used by Bot Owners */ /** Whether the Command can only be used by Bot Owners */
ownerOnly?: boolean ownerOnly?: boolean
}
export class Command implements CommandOptions {
name: string = ''
description?: string
category?: string
aliases?: string | string[]
extension?: Extension
usage?: string | string[]
examples?: string | string[]
args?: number | boolean | string[]
permissions?: string | string[]
userPermissions?: string | string[]
botPermissions?: string | string[]
roles?: string | string[]
whitelistedGuilds?: string | string[]
whitelistedChannels?: string | string[]
whitelistedUsers?: string | string[]
guildOnly?: boolean
dmOnly?: boolean
ownerOnly?: boolean
/** Method executed before executing actual command. Returns bool value - whether to continue or not (optional) */ /** Method executed before executing actual command. Returns bool value - whether to continue or not (optional) */
beforeExecute(ctx: CommandContext): boolean | Promise<boolean> { beforeExecute(ctx: CommandContext): boolean | Promise<boolean> {
@ -418,7 +440,8 @@ export const parseCommand = (
): ParsedCommand => { ): ParsedCommand => {
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 = content.split(client.betterArgs === true ? /[\S\s]*/ : / +/) const args = parse(content)._.map((e) => e.toString())
const name = args.shift() as string const name = args.shift() as string
const argString = content.slice(name.length).trim() const argString = content.slice(name.length).trim()

View file

@ -3,11 +3,13 @@ import { awaitSync } from '../utils/mixedPromise.ts'
import { Client, ClientOptions } from './client.ts' import { Client, ClientOptions } from './client.ts'
import { import {
CategoriesManager, CategoriesManager,
Command,
CommandContext, CommandContext,
CommandOptions,
CommandsManager, CommandsManager,
parseCommand parseCommand
} from './command.ts' } from './command.ts'
import { ExtensionsManager } from './extensions.ts' import { Extension, ExtensionsManager } from './extensions.ts'
type PrefixReturnType = string | string[] | Promise<string | string[]> type PrefixReturnType = string | string[] | Promise<string | string[]>
@ -29,8 +31,6 @@ export interface CommandClientOptions extends ClientOptions {
isChannelBlacklisted?: (guildID: string) => boolean | Promise<boolean> isChannelBlacklisted?: (guildID: string) => boolean | Promise<boolean>
/** Allow spaces after prefix? Recommended with Mention Prefix ON. */ /** Allow spaces after prefix? Recommended with Mention Prefix ON. */
spacesAfterPrefix?: boolean spacesAfterPrefix?: boolean
/** Better Arguments regex to split at every whitespace. */
betterArgs?: boolean
/** List of Bot's Owner IDs whom can access `ownerOnly` commands. */ /** List of Bot's Owner IDs whom can access `ownerOnly` commands. */
owners?: string[] owners?: string[]
/** Whether to allow Bots to use Commands or not, not allowed by default. */ /** Whether to allow Bots to use Commands or not, not allowed by default. */
@ -50,7 +50,6 @@ export class CommandClient extends Client implements CommandClientOptions {
isUserBlacklisted: (guildID: string) => boolean | Promise<boolean> isUserBlacklisted: (guildID: string) => boolean | Promise<boolean>
isChannelBlacklisted: (guildID: string) => boolean | Promise<boolean> isChannelBlacklisted: (guildID: string) => boolean | Promise<boolean>
spacesAfterPrefix: boolean spacesAfterPrefix: boolean
betterArgs: boolean
owners: string[] owners: string[]
allowBots: boolean allowBots: boolean
allowDMs: boolean allowDMs: boolean
@ -58,6 +57,7 @@ export class CommandClient extends Client implements CommandClientOptions {
extensions: ExtensionsManager = new ExtensionsManager(this) extensions: ExtensionsManager = new ExtensionsManager(this)
commands: CommandsManager = new CommandsManager(this) commands: CommandsManager = new CommandsManager(this)
categories: CategoriesManager = new CategoriesManager(this) categories: CategoriesManager = new CategoriesManager(this)
_decoratedCommands?: { [name: string]: Command }
constructor(options: CommandClientOptions) { constructor(options: CommandClientOptions) {
super(options) super(options)
@ -88,14 +88,18 @@ export class CommandClient extends Client implements CommandClientOptions {
options.spacesAfterPrefix === undefined options.spacesAfterPrefix === undefined
? false ? false
: options.spacesAfterPrefix : options.spacesAfterPrefix
this.betterArgs =
options.betterArgs === undefined ? false : options.betterArgs
this.owners = options.owners === undefined ? [] : options.owners this.owners = options.owners === undefined ? [] : options.owners
this.allowBots = options.allowBots === undefined ? false : options.allowBots this.allowBots = options.allowBots === undefined ? false : options.allowBots
this.allowDMs = options.allowDMs === undefined ? true : options.allowDMs this.allowDMs = options.allowDMs === undefined ? true : options.allowDMs
this.caseSensitive = this.caseSensitive =
options.caseSensitive === undefined ? false : options.caseSensitive options.caseSensitive === undefined ? false : options.caseSensitive
if (this._decoratedCommands !== undefined) {
Object.values(this._decoratedCommands).forEach((entry) => {
this.commands.add(entry)
})
}
this.on( this.on(
'messageCreate', 'messageCreate',
async (msg: Message) => await this.processMessage(msg) async (msg: Message) => await this.processMessage(msg)
@ -345,3 +349,25 @@ export class CommandClient extends Client implements CommandClientOptions {
} }
} }
} }
export function command(options?: CommandOptions) {
return function (target: CommandClient | Extension, name: string) {
const command = new Command()
command.name = name
command.execute = ((target as unknown) as {
[name: string]: (ctx: CommandContext) => any
})[name]
if (options !== undefined) Object.assign(command, options)
if (target instanceof CommandClient) {
if (target._decoratedCommands === undefined)
target._decoratedCommands = {}
target._decoratedCommands[command.name] = command
} else {
if (target._decorated === undefined) target._decorated = {}
target._decorated[command.name] = command
}
}
}

View file

@ -69,9 +69,15 @@ export class Extension {
commands: ExtensionCommands = new ExtensionCommands(this) commands: ExtensionCommands = new ExtensionCommands(this)
/** Events registered by this Extension */ /** Events registered by this Extension */
events: { [name: string]: (...args: any[]) => {} } = {} events: { [name: string]: (...args: any[]) => {} } = {}
_decorated?: { [name: string]: Command }
constructor(client: CommandClient) { constructor(client: CommandClient) {
this.client = client this.client = client
if (this._decorated !== undefined) {
Object.entries(this._decorated).forEach((entry) => {
this.commands.add(entry[1])
})
}
} }
/** Listen for an Event through Extension. */ /** Listen for an Event through Extension. */

62
src/test/class.ts Normal file
View file

@ -0,0 +1,62 @@
import {
CommandClient,
event,
Intents,
command,
CommandContext,
Extension
} from '../../mod.ts'
import { TOKEN } from './config.ts'
class MyClient extends CommandClient {
constructor() {
super({
prefix: '!',
caseSensitive: false
})
}
@event()
ready(): void {
console.log(`Logged in as ${this.user?.tag}!`)
}
@command({
aliases: 'pong'
})
Ping(ctx: CommandContext): void {
ctx.message.reply('Pong!')
}
}
class VCExtension extends Extension {
@command()
async join(ctx: CommandContext): Promise<void> {
const userVS = await ctx.guild?.voiceStates.get(ctx.author.id)
if (userVS === undefined) {
ctx.message.reply("You're not in VC.")
return
}
await userVS.channel?.join()
ctx.message.reply(`Joined VC channel - ${userVS.channel?.name}!`)
}
@command()
async leave(ctx: CommandContext): Promise<void> {
const userVS = await ctx.guild?.voiceStates.get(
(ctx.client.user?.id as unknown) as string
)
if (userVS === undefined) {
ctx.message.reply("I'm not in VC.")
return
}
userVS.channel?.leave()
ctx.message.reply(`Left VC channel - ${userVS.channel?.name}!`)
}
}
const client = new MyClient()
client.extensions.load(VCExtension)
client.connect(TOKEN, Intents.All)