diff --git a/mod.ts b/mod.ts index 98f5d5b..a94898b 100644 --- a/mod.ts +++ b/mod.ts @@ -12,13 +12,13 @@ export { CommandsManager, CategoriesManager } from './src/models/command.ts' -export type { CommandContext } from './src/models/command.ts' +export type { CommandContext, CommandOptions } from './src/models/command.ts' export { Extension, ExtensionCommands, ExtensionsManager } 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 { BaseManager } from './src/managers/base.ts' export { BaseChildManager } from './src/managers/baseChild.ts' diff --git a/src/models/client.ts b/src/models/client.ts index 1a64dd3..10ad449 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -70,6 +70,7 @@ export class Client extends EventEmitter { canary: boolean = false /** Client's presence. Startup one if set before connecting */ presence: ClientPresence = new ClientPresence() + _decoratedEvents?: { [name: string]: (...args: any[]) => any } private readonly _untypedOn = this.on @@ -101,6 +102,16 @@ export class Client extends EventEmitter { this.reactionCacheLifetime = options.reactionCacheLifetime if (options.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}`) } + /** + * 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 // fetchApplication(): Promise @@ -153,3 +179,13 @@ export class Client extends EventEmitter { 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 + } +} diff --git a/src/models/command.ts b/src/models/command.ts index b9ee863..3db38d2 100644 --- a/src/models/command.ts +++ b/src/models/command.ts @@ -5,6 +5,7 @@ import { User } from '../structures/user.ts' import { Collection } from '../utils/collection.ts' import { CommandClient } from './commandClient.ts' import { Extension } from './extensions.ts' +import { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts' export interface CommandContext { /** The Client object */ @@ -29,9 +30,9 @@ export interface CommandContext { guild?: Guild } -export class Command { +export interface CommandOptions { /** Name of the Command */ - name: string = '' + name?: string /** Description of the Command */ description?: string /** Category of the Command */ @@ -66,6 +67,27 @@ export class Command { dmOnly?: boolean /** Whether the Command can only be used by Bot Owners */ 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) */ beforeExecute(ctx: CommandContext): boolean | Promise { @@ -418,7 +440,8 @@ export const parseCommand = ( ): ParsedCommand => { let content = msg.content.slice(prefix.length) 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 argString = content.slice(name.length).trim() diff --git a/src/models/commandClient.ts b/src/models/commandClient.ts index 8fd09ce..fc9199d 100644 --- a/src/models/commandClient.ts +++ b/src/models/commandClient.ts @@ -3,11 +3,13 @@ import { awaitSync } from '../utils/mixedPromise.ts' import { Client, ClientOptions } from './client.ts' import { CategoriesManager, + Command, CommandContext, + CommandOptions, CommandsManager, parseCommand } from './command.ts' -import { ExtensionsManager } from './extensions.ts' +import { Extension, ExtensionsManager } from './extensions.ts' type PrefixReturnType = string | string[] | Promise @@ -29,8 +31,6 @@ export interface CommandClientOptions extends ClientOptions { isChannelBlacklisted?: (guildID: string) => boolean | Promise /** Allow spaces after prefix? Recommended with Mention Prefix ON. */ spacesAfterPrefix?: boolean - /** Better Arguments regex to split at every whitespace. */ - betterArgs?: boolean /** List of Bot's Owner IDs whom can access `ownerOnly` commands. */ owners?: string[] /** 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 isChannelBlacklisted: (guildID: string) => boolean | Promise spacesAfterPrefix: boolean - betterArgs: boolean owners: string[] allowBots: boolean allowDMs: boolean @@ -58,6 +57,7 @@ export class CommandClient extends Client implements CommandClientOptions { extensions: ExtensionsManager = new ExtensionsManager(this) commands: CommandsManager = new CommandsManager(this) categories: CategoriesManager = new CategoriesManager(this) + _decoratedCommands?: { [name: string]: Command } constructor(options: CommandClientOptions) { super(options) @@ -88,14 +88,18 @@ export class CommandClient extends Client implements CommandClientOptions { options.spacesAfterPrefix === undefined ? false : options.spacesAfterPrefix - this.betterArgs = - options.betterArgs === undefined ? false : options.betterArgs this.owners = options.owners === undefined ? [] : options.owners this.allowBots = options.allowBots === undefined ? false : options.allowBots this.allowDMs = options.allowDMs === undefined ? true : options.allowDMs this.caseSensitive = options.caseSensitive === undefined ? false : options.caseSensitive + if (this._decoratedCommands !== undefined) { + Object.values(this._decoratedCommands).forEach((entry) => { + this.commands.add(entry) + }) + } + this.on( 'messageCreate', 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 + } + } +} diff --git a/src/models/extensions.ts b/src/models/extensions.ts index c68bdc3..130a2db 100644 --- a/src/models/extensions.ts +++ b/src/models/extensions.ts @@ -69,9 +69,15 @@ export class Extension { commands: ExtensionCommands = new ExtensionCommands(this) /** Events registered by this Extension */ events: { [name: string]: (...args: any[]) => {} } = {} + _decorated?: { [name: string]: Command } constructor(client: CommandClient) { this.client = client + if (this._decorated !== undefined) { + Object.entries(this._decorated).forEach((entry) => { + this.commands.add(entry[1]) + }) + } } /** Listen for an Event through Extension. */ diff --git a/src/test/class.ts b/src/test/class.ts new file mode 100644 index 0000000..2a4304b --- /dev/null +++ b/src/test/class.ts @@ -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 { + 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 { + 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)