From 2483a6708885eb32b903f82d33a8520a15cc3ec7 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Fri, 1 Oct 2021 21:00:03 +0300 Subject: [PATCH] implement new sendTo method --- cron/plugin.json | 9 +++ cron/plugin.ts | 166 ++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 85 ++++++++++++++++++++- package.json | 4 +- squeebot.repo.json | 8 ++ xprotocol/plugin.json | 9 +++ xprotocol/plugin.ts | 55 ++++++++++++++ 7 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 cron/plugin.json create mode 100644 cron/plugin.ts create mode 100644 xprotocol/plugin.json create mode 100644 xprotocol/plugin.ts diff --git a/cron/plugin.json b/cron/plugin.json new file mode 100644 index 0000000..8fef0ef --- /dev/null +++ b/cron/plugin.json @@ -0,0 +1,9 @@ +{ + "main": "plugin.js", + "name": "cron", + "description": "API for plugin-scoped cron tasks", + "tags": ["timers", "cron", "scheduler", "api"], + "version": "1.0.0", + "dependencies": ["control?"], + "npmDependencies": ["node-cron@3.0.0"] +} diff --git a/cron/plugin.ts b/cron/plugin.ts new file mode 100644 index 0000000..c7e1ddb --- /dev/null +++ b/cron/plugin.ts @@ -0,0 +1,166 @@ +import { logger } from '@squeebot/core/lib/core'; +import { + Plugin, + Configurable, + EventListener, + IPlugin, +} from '@squeebot/core/lib/plugin'; + +import nodeCron, { ScheduledTask } from 'node-cron'; + +type ExecutorFn = (...args: any[]) => void; + +class CronWrapper { + private cronTask: ScheduledTask | null = null; + public id = Math.random().toString(36).slice(2); + public stopped = true; + public destroyed = false; + + constructor( + public expression: string, + public taskFn: ExecutorFn, + public origin: IPlugin, + ) {} + + assert(): void { + if (!nodeCron.validate(this.expression)) { + throw new Error('Invalid cron pattern!'); + } + } + + start(): void { + if (this.destroyed) { + logger.warn('[cron] Someone tried to start a destroyed task! This could indicate a memory leak!'); + return; + } + + if (!this.cronTask) { + this.assert(); + this.cronTask = nodeCron.schedule(this.expression, this.execute, { + scheduled: false, + }); + } + + this.cronTask.start(); + this.stopped = false; + } + + stop(): void { + if (!this.cronTask) { + return; + } + + this.cronTask.stop(); + this.stopped = true; + } + + destroy(): void { + this.destroyed = true; + + if (!this.cronTask) { + return; + } + + this.cronTask.destroy(); + this.cronTask = null; + } + + execute(): void { + this.taskFn.call(this.origin); + } + + belongsTo(plugin: IPlugin | string): boolean { + if (typeof plugin === 'string') { + return this.origin.manifest.name === plugin; + } + return plugin === this.origin || + plugin.manifest.name === this.origin.manifest.name; + } +} + +// @Configurable({}) +class CronPlugin extends Plugin { + private timers: CronWrapper[] = []; + + @EventListener('pluginUnload') + public unloadEventHandler(plugin: string | Plugin): void { + if (plugin === this.name || plugin === this) { + this.timers.forEach((timer) => timer.destroy()); + this.timers = []; + this.emit('pluginUnloaded', this); + } + } + + @EventListener('pluginUnloaded') + public unloadedEventHandler(plugin: string | Plugin): void { + this.timers.forEach((timer) => { + if (timer.belongsTo(plugin)) { + timer.destroy(); + } + }); + this.cleanUp(); + } + + /** + * @returns A list of tasks including id, cron expression and origin plugin. + */ + public getList(): string[][] { + return this.timers.map((timer) => ([ + timer.id, + timer.expression, + timer.origin.manifest.name, + timer.destroyed || timer.stopped ? 'stopped' : 'running', + ])); + } + + /** + * Register a new cron task + * @param plugin Plugin registering this task + * @param expression Cron expression + * @param taskFn Function to execute + * @param autostart Start the scheduler on add + * @returns The task + */ + public registerTimer( + plugin: IPlugin, + expression: string, + taskFn: ExecutorFn, + autostart = true, + ): CronWrapper { + const newTimer = new CronWrapper(expression, taskFn, plugin); + + newTimer.assert(); + if (autostart) { + newTimer.start(); + } + + return newTimer; + } + + /** + * Remove a scheduled timer by ID or by the object itself. + * Always use this to ensure that memory is properly cleared. + * @param timer Timer or timer ID + */ + public removeTimer(timer: string | CronWrapper): void { + if (typeof timer === 'string') { + const find = this.timers.find((item) => item.id === timer); + if (find) { + find.destroy(); + } + } else { + timer.destroy(); + } + + this.cleanUp(); + } + + /** + * Remove destroyed timers from the cache + */ + private cleanUp(): void { + this.timers = this.timers.filter((timer) => !timer.destroyed); + } +} + +module.exports = CronPlugin; diff --git a/package-lock.json b/package-lock.json index 3932032..1129026 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "license": "ISC", "dependencies": { "@squeebot/core": "^3.3.1", + "node-cron": "^3.0.0", "typescript": "^4.4.2" }, "devDependencies": { - "@types/node": "^16.7.10" + "@types/node": "^16.7.10", + "@types/node-cron": "^2.0.4" } }, "../core": { @@ -54,6 +56,21 @@ "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==", "dev": true }, + "node_modules/@types/node-cron": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-2.0.4.tgz", + "integrity": "sha512-vXzgDRWCZpuut5wJVZtluEnkNhzGojYlyMch2c4kMj7H74L8xTLytVlgQzj+/17wfcjs49aJDFBDglFSGt7GeA==", + "dev": true, + "dependencies": { + "@types/tz-offset": "*" + } + }, + "node_modules/@types/tz-offset": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@types/tz-offset/-/tz-offset-0.0.0.tgz", + "integrity": "sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ==", + "dev": true + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -164,6 +181,36 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.33", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz", + "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==", + "dependencies": { + "moment": ">= 2.9.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-cron": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz", + "integrity": "sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA==", + "dependencies": { + "moment-timezone": "^0.5.31" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -238,6 +285,21 @@ "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==", "dev": true }, + "@types/node-cron": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-2.0.4.tgz", + "integrity": "sha512-vXzgDRWCZpuut5wJVZtluEnkNhzGojYlyMch2c4kMj7H74L8xTLytVlgQzj+/17wfcjs49aJDFBDglFSGt7GeA==", + "dev": true, + "requires": { + "@types/tz-offset": "*" + } + }, + "@types/tz-offset": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@types/tz-offset/-/tz-offset-0.0.0.tgz", + "integrity": "sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ==", + "dev": true + }, "at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -316,6 +378,27 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, + "moment-timezone": { + "version": "0.5.33", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz", + "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==", + "requires": { + "moment": ">= 2.9.0" + } + }, + "node-cron": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz", + "integrity": "sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA==", + "requires": { + "moment-timezone": "^0.5.31" + } + }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", diff --git a/package.json b/package.json index 651272d..469a6a6 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "license": "ISC", "dependencies": { "@squeebot/core": "^3.3.1", + "node-cron": "^3.0.0", "typescript": "^4.4.2" }, "devDependencies": { - "@types/node": "^16.7.10" + "@types/node": "^16.7.10", + "@types/node-cron": "^2.0.4" } } diff --git a/squeebot.repo.json b/squeebot.repo.json index 6433f11..00c81c1 100644 --- a/squeebot.repo.json +++ b/squeebot.repo.json @@ -5,6 +5,10 @@ "name": "control", "version": "0.1.1" }, + { + "name": "cron", + "version": "1.0.0" + }, { "name": "permissions", "version": "0.1.0" @@ -12,6 +16,10 @@ { "name": "simplecommands", "version": "1.1.1" + }, + { + "name": "xprotocol", + "version": "1.0.0" } ], "typescript": true diff --git a/xprotocol/plugin.json b/xprotocol/plugin.json new file mode 100644 index 0000000..b49641c --- /dev/null +++ b/xprotocol/plugin.json @@ -0,0 +1,9 @@ +{ + "main": "plugin.js", + "name": "xprotocol", + "description": "API for sending messages to other protocols", + "tags": ["messages", "relaying", "api"], + "version": "1.0.0", + "dependencies": ["control?"], + "npmDependencies": [] +} diff --git a/xprotocol/plugin.ts b/xprotocol/plugin.ts new file mode 100644 index 0000000..a194970 --- /dev/null +++ b/xprotocol/plugin.ts @@ -0,0 +1,55 @@ +import { ISqueebotCore, logger } from '@squeebot/core/lib/core'; +import { + Plugin, + EventListener, +} from '@squeebot/core/lib/plugin'; + +class XProtocolPlugin extends Plugin { + private core: ISqueebotCore | null = null; + + @EventListener('pluginUnload') + public unloadEventHandler(plugin: string | Plugin): void { + if (plugin === this.name || plugin === this) { + this.core = null; + this.emit('pluginUnloaded', this); + } + } + + public async sendTo(target: string, ...data: any[]): Promise { + if (!this.core) { + return false; + } + + // Find target plugin + const rxSplit = target.split('/'); + const plugin = this.core.pluginManager.getLoadedByName(rxSplit[0]); + if (!plugin || !plugin.service) { + return false; + } + + // Find target protocol + const protocol = plugin.service.getProtocolByName(rxSplit[1]); + if (!protocol || !protocol.running) { + return false; + } + + return protocol.sendTo(target, ...data); + } + + initialize(): void { + this.on('core', (core: ISqueebotCore) => { + this.core = core; + }); + + this.emitTo('core', 'request-core', this.name); + + this.on('send', (data: any[]) => { + const target = data[0]; + this.sendTo(target, ...data.slice(1)).catch((error: Error) => { + logger.error(`[sendto] Sending to protocol from event failed:`, error.message ?? error); + }); + }); + } +} + +module.exports = XProtocolPlugin;