From c951fee2f5e8cebd2a7f56b98e0d02d462cc9265 Mon Sep 17 00:00:00 2001 From: Zack Campbell Date: Wed, 10 May 2017 19:15:33 -0500 Subject: [PATCH] Add ListenerUtil It's helpful I swear --- package.json | 3 +- src/client/Client.ts | 23 +++++++++- src/index.ts | 2 + src/util/ListenerUtil.ts | 98 ++++++++++++++++++++++++++++++++++++++++ test/test_client.ts | 65 ++++++++++++++++++++------ 5 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 src/util/ListenerUtil.ts diff --git a/package.json b/package.json index a3348bd4..8b2236e0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "glob": "7.1.1", "node-json-db": "0.7.3", "performance-now": "^2.0.0", - "postinstall-build": "^3.0.0" + "postinstall-build": "^3.0.0", + "reflect-metadata": "^0.1.10" }, "devDependencies": { "@types/chalk": "^0.4.31", diff --git a/src/client/Client.ts b/src/client/Client.ts index e57012aa..e3176e81 100644 --- a/src/client/Client.ts +++ b/src/client/Client.ts @@ -1,6 +1,22 @@ import * as Discord from 'discord.js'; import * as path from 'path'; -import { Channel, ClientOptions, Collection, Emoji, Guild, GuildMember, Message, MessageReaction, Role, User, UserResolvable, ClientUserSettings, Snowflake } from 'discord.js'; + +import { + Channel, + ClientOptions, + Collection, + Emoji, + Guild, + GuildMember, + Message, + MessageReaction, + Role, + User, + UserResolvable, + ClientUserSettings, + Snowflake +} from 'discord.js'; + import { Command } from '../command/Command'; import { CommandDispatcher } from '../command/CommandDispatcher'; import { CommandLoader } from '../command/CommandLoader'; @@ -16,6 +32,9 @@ import { MiddlewareFunction } from '../types/MiddlewareFunction'; import { StorageProviderConstructor } from '../types/StorageProviderConstructor'; import { BaseCommandName } from '../types/BaseCommandName'; import { Logger, logger } from '../util/logger/Logger'; +import { ListenerUtil } from '../util/ListenerUtil'; + +const { registerListeners } = ListenerUtil; /** * The YAMDBF Client through which you can access [storage]{@link Client#storage} @@ -173,6 +192,8 @@ export class Client extends Discord.Client // Load commands if (!this.passive) this.loadCommand('all'); + + registerListeners(this); } /** diff --git a/src/index.ts b/src/index.ts index 1b8c79ba..85d51513 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,8 @@ export { LogLevel } from './types/LogLevel'; export { deprecated } from './util/DeprecatedDecorator'; +export { ListenerUtil } from './util/ListenerUtil'; + export { ArgOpts } from './types/ArgOpts'; export { BaseCommandName } from './types/BaseCommandName'; export { ClientStorage } from './types/ClientStorage'; diff --git a/src/util/ListenerUtil.ts b/src/util/ListenerUtil.ts new file mode 100644 index 00000000..09562c72 --- /dev/null +++ b/src/util/ListenerUtil.ts @@ -0,0 +1,98 @@ +import 'reflect-metadata'; +import { EventEmitter } from 'events'; + +/** + * Contains static methods for declaring class methods (within a class extending `EventEmitter`) + * as listeners for events that will be emitted by the class or parent classes + * @module ListenerUtil + */ +export class ListenerUtil +{ + /** + * Attaches any listeners registered via the `on` or `once` decorators. + * Must be called ***after*** `super()`, and only in classes extending `EventEmitter` + * (which includes the Discord.js Client class and thus the YAMDBF Client class); + * @static + * @method registerListeners + * @param {EventEmitter} emitter EventEmitter to register listeners for + * @returns {void} + */ + public static registerListeners(emitter: EventEmitter): void + { + if (!(emitter instanceof EventEmitter)) + throw new TypeError('Listeners can only be registered on classes extending EventEmitter'); + if (typeof Reflect.getMetadata('listeners', emitter.constructor.prototype) === 'undefined') return; + + for (const listener of Reflect.getMetadata('listeners', emitter.constructor.prototype)) + { + if (!( emitter)[listener.method]) continue; + if (listener.attached) continue; + listener.attached = true; + emitter[listener.once ? 'once' : 'on'](listener.event, + (...eventArgs: any[]) => ( emitter)[listener.method](...eventArgs)); + } + } + + /** + * Declares the decorated method as an event handler for the specified event + * Must be registered by calling {@link ListenerUtil.registerListeners()} + * + * > **Note:** `registerListeners()` is already called in the YAMDBF + * {@link Client} constructor and does not need to be called in classes + * extending it + * @static + * @method on + * @param {string} event The name of the event to handle + * @returns {MethodDecorator} + */ + public static on(event: string): MethodDecorator + { + return ListenerUtil._setListenerMetadata(event); + } + + /** + * Declares the decorated method as a single-use event handler for the + * specified event. Must be registered by calling + * {@link ListenerUtil.registerListeners()} + * + * > **Note:** `registerListeners()` is already called in the YAMDBF + * {@link Client} constructor and does not need to be called in classes + * extending it + * @static + * @method once + * @param {string} event The name of the event to handle + * @returns {MethodDecorator} + */ + public static once(event: string): MethodDecorator + { + return ListenerUtil._setListenerMetadata(event, true); + } + + /** + * Returns a MethodDecorator that handles setting the appropriate listener + * metadata for a class method + * @private + */ + private static _setListenerMetadata(event: string, once: boolean = false): MethodDecorator + { + return function(target: T, key: string, descriptor: PropertyDescriptor): PropertyDescriptor + { + const listeners: ListenerMetadata[] = Reflect.getMetadata('listeners', target) || []; + listeners.push({ event: event, method: key, once: once }); + Reflect.defineMetadata('listeners', listeners, target); + return descriptor; + }; + } +} + +/** + * Represents metadata used to build an event listener + * and assign it to a class method at runtime + */ +type ListenerMetadata = +{ + event: string; + method: string; + once: boolean; + attached?: boolean; +}; diff --git a/test/test_client.ts b/test/test_client.ts index 97f98149..698b787c 100644 --- a/test/test_client.ts +++ b/test/test_client.ts @@ -1,20 +1,55 @@ -import { Client, LogLevel, Logger } from '../bin/'; +import { Client, LogLevel, Logger, ListenerUtil } from '../bin/'; const config: any = require('./config.json'); const logger: Logger = Logger.instance(); +const { once } = ListenerUtil; -const client: Client = new Client({ - name: 'test', - token: config.token, - config: config, - commandsDir: './commands', - logLevel: LogLevel.DEBUG -}).start(); +// const client: Client = new Client({ +// name: 'test', +// token: config.token, +// config: config, +// commandsDir: './commands', +// logLevel: LogLevel.DEBUG +// }).start(); -client.on('waiting', async () => +// client.on('waiting', async () => +// { +// await client.setDefaultSetting('prefix', '.'); +// client.emit('finished'); +// }); +// logger.warn('Test', 'Testing Logger#warn()'); +// logger.error('Test', 'Testing Logger#error()'); +// logger.debug('Test', 'Testing Logger#debug()'); + +class Test extends Client { - await client.setDefaultSetting('prefix', '.'); - client.emit('finished'); -}); -logger.warn('Test', 'Testing Logger#warn()'); -logger.error('Test', 'Testing Logger#error()'); -logger.debug('Test', 'Testing Logger#debug()'); + public constructor() + { + super({ + name: 'test', + token: config.token, + config: config, + commandsDir: './commands', + logLevel: LogLevel.DEBUG + }); + } + + @once('waiting') + private async _onWaiting(): Promise + { + logger.debug('Test', 'Waiting...'); + await this.setDefaultSetting('prefix', '?'); + this.emit('finished'); + } + + @once('finished') + private _onFinished(): void + { + logger.debug('Test', 'Finished'); + + logger.warn('Test', 'Testing Logger#warn()'); + logger.debug('Test', 'Testing Logger#debug()'); + logger.error('Test', 'Testing Logger#error()'); + } +} +const test: Test = new Test(); +test.start();