diff --git a/package.json b/package.json index fbb1243..8424913 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "fuse.js": "^6.5.3", "lodash": "^4.17.21", "mqtt": "^4.3.7", + "node-cron": "^3.0.2", "reflect-metadata": "^0.1.13", "romans": "^2.0.3", "typedi": "^0.10.0", @@ -52,6 +53,7 @@ "@swc/core": "^1.3.20", "@types/lodash": "^4.14.191", "@types/node": "^18.11.18", + "@types/node-cron": "^3.0.7", "@types/romans": "^2.0.0", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.44.0", @@ -70,4 +72,4 @@ "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } -} \ No newline at end of file +} diff --git a/src/classes/Primaries.ts b/src/classes/Primaries.ts index 1eee5b1..3193b63 100644 --- a/src/classes/Primaries.ts +++ b/src/classes/Primaries.ts @@ -161,4 +161,53 @@ export default class Primaries { this.mqtt.publish('dynamica/primaries', (await this.count).toString()); } } + + public async cleanUp() { + this.logger.debug('Cleaning up primary channels'); + + const dbPrimaries = await this.db.primary.findMany(); + + const loadResult = await Promise.allSettled( + dbPrimaries.map(async (channel) => { + await this.client.channels.fetch(channel.id); + const primary = new Primary( + channel.id, + channel.guildId, + this.db, + this.client, + this.secondaries, + this.logger, + this + ); + + return primary; + }) + ); + + loadResult.forEach(async (result, index) => { + if (result.status === 'rejected') { + if (result.reason instanceof DiscordAPIError) { + this.logger.error( + `Failed to load primary ${dbPrimaries[index].id} (${result.reason.message})` + ); + if ( + result.reason.code === 10013 || + result.reason.code === 10003 || + result.reason.code === 50001 + ) { + await this.db.primary.delete({ + where: { id: dbPrimaries[index].id }, + }); + } else { + this.logger.error(result.reason); + } + } + } + }); + + if (this.mqtt) { + this.mqtt.publish('dynamica/primaries', (await this.count).toString()); + } + this.logger.debug('Primary channel cleanup complete'); + } } diff --git a/src/classes/Secondaries.ts b/src/classes/Secondaries.ts index be79e1e..0185ac4 100644 --- a/src/classes/Secondaries.ts +++ b/src/classes/Secondaries.ts @@ -192,4 +192,56 @@ export default class Secondaries { // ); return dynamicaSecondary; } + + public async cleanUp() { + this.logger.debug('Cleaning up secondary channels...'); + const dbSecondaries = await this.db.secondary.findMany(); + + const loadResult = await Promise.allSettled( + dbSecondaries.map(async (channel) => { + await this.client.channels.fetch(channel.id); + const secondary = new Secondary( + channel.id, + channel.guildId, + channel.primaryId, + this.db, + this.client, + this.logger, + this.mqtt + ); + + return secondary; + }) + ); + + loadResult.forEach(async (result, index) => { + if (result.status === 'rejected') { + if (result.reason instanceof DiscordAPIError) { + this.logger + .scope('Secondary', dbSecondaries[index].id) + .error( + `Failed to load secondary ${dbSecondaries[index].id} (${result.reason.message})` + ); + if (result.reason.code === 10013 || result.reason.code === 10003) { + await this.delete(dbSecondaries[index].id); + } else { + this.logger + .scope('Secondary', dbSecondaries[index].id) + .error(result.reason); + } + } + } else { + try { + await result.value.update(); + } catch (error) { + this.logger.scope('Secondary', dbSecondaries[index].id).error(error); + } + } + }); + + if (this.mqtt) { + this.mqtt.publish('dynamica/secondaries', (await this.count).toString()); + } + this.logger.debug('Secondary channel cleanup complete.'); + } } diff --git a/src/events/ReadyEvent.ts b/src/events/ReadyEvent.ts index 7786d7c..ecb40d7 100644 --- a/src/events/ReadyEvent.ts +++ b/src/events/ReadyEvent.ts @@ -9,6 +9,8 @@ import updatePresence from '@/utils/presence'; import Event, { EventToken } from '@/classes/Event'; import { Service } from 'typedi'; import Client from '@/services/Client'; +import Tasks from '@/services/Tasks'; +import cron from 'node-cron'; @Service({ id: EventToken, multiple: true }) export default class ReadyEvent extends Event<'ready'> { @@ -19,7 +21,8 @@ export default class ReadyEvent extends Event<'ready'> { private primaries: Primaries, private aliases: Aliases, private guilds: Guilds, - private client: Client + private client: Client, + private tasks: Tasks ) { super(); } @@ -44,6 +47,18 @@ export default class ReadyEvent extends Event<'ready'> { this.mqtt.publish('dynamica/presence', this.client.readyAt.toISOString()); updatePresence(); + + // cleanup tasks (every hour) + this.tasks.addTask( + cron.schedule('0 * * * *', async () => { + this.primaries.cleanUp(); + }) + ); + this.tasks.addTask( + cron.schedule('0 * * * *', async () => { + this.secondaries.cleanUp(); + }) + ); } catch (error) { logger.error(error); } diff --git a/src/index.ts b/src/index.ts index 13017af..c9d42ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,10 +11,11 @@ import registerHelp from './register-help'; import registerCommands from './register-commands'; import MQTT from './services/MQTT'; import deployCommands from './scripts/deploy'; +import Tasks from './services/Tasks'; dotenv.config(); -Container.import([Logger, DB, Events, Client, MQTT]); +Container.import([Logger, DB, Events, Client, MQTT, Tasks]); registerCommands(); registerHelp(); diff --git a/src/services/Tasks.ts b/src/services/Tasks.ts new file mode 100644 index 0000000..c94db15 --- /dev/null +++ b/src/services/Tasks.ts @@ -0,0 +1,20 @@ +import { Service } from 'typedi'; +import cron from 'node-cron'; +import Logger from './Logger'; + +@Service() +export default class Tasks { + private tasks: Array = []; + + constructor(private readonly logger: Logger) {} + + public stop() { + this.logger.info('Stopping tasks...'); + this.tasks.forEach((task) => task.stop()); + this.logger.info('Tasks stopped.'); + } + + public addTask(task: cron.ScheduledTask) { + this.tasks.push(task); + } +} diff --git a/yarn.lock b/yarn.lock index bf28d68..5588e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -262,6 +262,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== +"@types/node-cron@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.7.tgz#978bf75f7247385c61d23b6a060ba9eedb03e2f4" + integrity sha512-9PuLtBboc/+JJ7FshmJWv769gDonTpItN0Ol5TMwclpSQNjVyB2SRxSKBcTtbSysSL5R7Oea06kTTFNciCoYwA== + "@types/node@*", "@types/node@^18.11.18": version "18.11.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" @@ -1850,6 +1855,13 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +node-cron@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.2.tgz#bb0681342bd2dfb568f28e464031280e7f06bd01" + integrity sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ== + dependencies: + uuid "8.3.2" + node-gyp-build@^4.3.0: version "4.5.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" @@ -2547,6 +2559,11 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +uuid@8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"