From 63ca839336d38002843c69d43170cdb8f1437549 Mon Sep 17 00:00:00 2001 From: barthofu Date: Fri, 9 Feb 2024 14:27:08 +0000 Subject: [PATCH] style(#128): apply style fix lint + change typescript aliases from `@` to `@/` (ex: `@services` -> `@/services` to distinguish them from external npm packages --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .vscode/settings.json | 32 +- README.md | 5 +- eslint.config.js | 8 +- mikro-orm.config.ts | 27 +- src/api/controllers/bot.ts | 430 +++++---- src/api/controllers/database.ts | 134 +-- src/api/controllers/health.ts | 169 ++-- src/api/controllers/index.ts | 2 +- src/api/controllers/other.ts | 14 +- src/api/controllers/stats.ts | 200 ++-- src/api/middlewares/botOnline.ts | 33 +- src/api/middlewares/devAuthenticated.ts | 128 +-- src/api/middlewares/index.ts | 2 +- src/api/middlewares/log.ts | 50 +- src/api/server.ts | 110 +-- src/client.ts | 92 +- src/commands/Admin/prefix.ts | 56 +- src/commands/General/help.ts | 187 ++-- src/commands/General/info.ts | 49 +- src/commands/General/invite.ts | 33 +- src/commands/General/ping.ts | 24 +- src/commands/General/stats.ts | 133 ++- src/commands/Owner/maintenance.ts | 28 +- src/configs/api.ts | 8 +- src/configs/database.ts | 148 +-- src/configs/general.ts | 20 +- src/configs/index.ts | 2 +- src/configs/logs.ts | 98 +- src/configs/stats.ts | 16 +- src/entities/BaseEntity.ts | 9 +- src/entities/Data.ts | 80 +- src/entities/Guild.ts | 43 +- src/entities/Image.ts | 42 +- src/entities/Pastebin.ts | 19 +- src/entities/Stat.ts | 21 +- src/entities/User.ts | 31 +- src/entities/index.ts | 2 +- src/events/custom/simpleCommandCreate.ts | 112 ++- src/events/custom/templateReady.ts | 20 +- src/events/guildCreate.ts | 22 +- src/events/guildDelete.ts | 22 +- src/events/interactionCreate.ts | 88 +- src/events/messageCreate.ts | 53 +- src/events/messagePinned.ts | 21 +- src/events/ready.ts | 157 ++-- src/guards/disabled.ts | 39 +- src/guards/extractLocale.ts | 33 +- src/guards/guildOnly.ts | 23 +- src/guards/index.ts | 2 +- src/guards/maintenance.ts | 54 +- src/guards/match.ts | 23 +- src/guards/notBot.ts | 20 +- src/guards/nsfw.ts | 32 +- src/guards/requestContextIsolator.ts | 19 +- src/i18n/detectors.ts | 27 +- src/i18n/formatters.ts | 6 +- src/i18n/index.ts | 8 +- src/main.ts | 310 +++---- src/services/Database.ts | 377 ++++---- src/services/ErrorHandler.ts | 65 +- src/services/EventManager.ts | 62 +- src/services/ImagesUpload.ts | 278 +++--- src/services/Logger.ts | 1076 +++++++++++----------- src/services/Pastebin.ts | 97 +- src/services/PluginsManager.ts | 189 ++-- src/services/Scheduler.ts | 44 +- src/services/Stats.ts | 773 ++++++++-------- src/services/Store.ts | 42 +- src/services/index.ts | 2 +- src/utils/classes/BaseController.ts | 2 +- src/utils/classes/BaseError.ts | 31 +- src/utils/classes/Plugin.ts | 280 +++--- src/utils/classes/index.ts | 2 +- src/utils/decorators/ContextMenu.ts | 61 +- src/utils/decorators/On.ts | 38 +- src/utils/decorators/OnCustom.ts | 34 +- src/utils/decorators/Once.ts | 36 +- src/utils/decorators/Schedule.ts | 57 +- src/utils/decorators/Slash.ts | 69 +- src/utils/decorators/SlashChoice.ts | 59 +- src/utils/decorators/SlashGroup.ts | 69 +- src/utils/decorators/SlashOption.ts | 82 +- src/utils/decorators/index.ts | 20 +- src/utils/errors/InvalidOptionName.ts | 20 +- src/utils/errors/NoBotToken.ts | 18 +- src/utils/errors/UnknownReply.ts | 29 +- src/utils/errors/index.ts | 2 +- src/utils/functions/array.ts | 16 +- src/utils/functions/colors.ts | 15 +- src/utils/functions/converter.ts | 22 +- src/utils/functions/database.ts | 30 +- src/utils/functions/date.ts | 30 +- src/utils/functions/dependency.ts | 27 +- src/utils/functions/devs.ts | 14 +- src/utils/functions/embeds.ts | 29 +- src/utils/functions/error.ts | 16 +- src/utils/functions/eval.ts | 36 +- src/utils/functions/files.ts | 45 +- src/utils/functions/image.ts | 37 +- src/utils/functions/index.ts | 2 +- src/utils/functions/interactions.ts | 19 +- src/utils/functions/localization.ts | 114 ++- src/utils/functions/maintenance.ts | 29 +- src/utils/functions/prefix.ts | 24 +- src/utils/functions/resolvers.ts | 106 +-- src/utils/functions/string.ts | 83 +- src/utils/functions/synchronizer.ts | 143 ++- src/utils/types/configs.d.ts | 158 ++-- src/utils/types/database.d.ts | 60 +- src/utils/types/environment.d.ts | 50 +- src/utils/types/interactions.d.ts | 8 +- src/utils/types/localization.d.ts | 20 +- src/utils/types/state.d.ts | 4 +- src/utils/types/stats.d.ts | 12 +- src/utils/types/utils.d.ts | 56 +- tsconfig.json | 46 +- 117 files changed, 4365 insertions(+), 4378 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 00583915..a0d7a9f4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1 +1 @@ -**Please describe the changes this PR makes and why it should be merged:** \ No newline at end of file +**Please describe the changes this PR makes and why it should be merged:** diff --git a/.vscode/settings.json b/.vscode/settings.json index 6ba44a17..0b2d6710 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,24 +9,24 @@ "editor.formatOnSave": false, // Auto fix - // "editor.codeActionsOnSave": { - // "source.fixAll.eslint": "explicit", - // "source.organizeImports": "never" - // }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" + }, // Silent the stylistic rules in you IDE, but still auto fix them - // "eslint.rules.customizations": [ - // { "rule": "style/*", "severity": "off" }, - // { "rule": "format/*", "severity": "off" }, - // { "rule": "*-indent", "severity": "off" }, - // { "rule": "*-spacing", "severity": "off" }, - // { "rule": "*-spaces", "severity": "off" }, - // { "rule": "*-order", "severity": "off" }, - // { "rule": "*-dangle", "severity": "off" }, - // { "rule": "*-newline", "severity": "off" }, - // { "rule": "*quotes", "severity": "off" }, - // { "rule": "*semi", "severity": "off" } - // ], + "eslint.rules.customizations": [ + { "rule": "style/*", "severity": "off" }, + { "rule": "format/*", "severity": "off" }, + { "rule": "*-indent", "severity": "off" }, + { "rule": "*-spacing", "severity": "off" }, + { "rule": "*-spaces", "severity": "off" }, + { "rule": "*-order", "severity": "off" }, + { "rule": "*-dangle", "severity": "off" }, + { "rule": "*-newline", "severity": "off" }, + { "rule": "*quotes", "severity": "off" }, + { "rule": "*semi", "severity": "off" } + ], // Enable eslint for all supported languages "eslint.validate": [ diff --git a/README.md b/README.md index 02e64082..4bc18154 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@
- + # What is TSCord #### **TSCord** is a fully-featured **[discord bot](https://discord.com/developers/docs/intro#bots-and-apps)** *template* written in [Typescript](https://www.typescriptlang.org/), intended to provide a framework that's easy to use, extend and modify. It uses [`discordx`](https://github.com/discordx-ts/discordx) and [`discord.js v14`](https://github.com/discordjs/discord.js) under the hood to simplify the development of discord bots. -This template was created to give developers a starting point for new Discord bots, so that much of the initial setup can be avoided and developers can instead focus on meaningful bot features. Developers can simply follow the [installation](https://tscord.discbot.app/docs/bot/get-started/installation) and the [configuration](https://tscord.discbot.app/docs/bot/get-started/configuration) instructions, and have a working bot with many boilerplate features already included! +This template was created to give developers a starting point for new Discord bots, so that much of the initial setup can be avoided and developers can instead focus on meaningful bot features. Developers can simply follow the [installation](https://tscord.discbot.app/docs/bot/get-started/installation) and the [configuration](https://tscord.discbot.app/docs/bot/get-started/configuration) instructions, and have a working bot with many boilerplate features already included!
@@ -91,7 +91,6 @@ https://user-images.githubusercontent.com/66025667/196367258-94c77e23-779c-4d9b- - ## 📜 Features Talking about features, here are some of the core features of the template: diff --git a/eslint.config.js b/eslint.config.js index 228a4ce1..42c77be1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -45,7 +45,11 @@ module.exports = antfu( }, }], 'style/object-curly-spacing': ['error', 'always'], - 'style/padded-blocks': ['error', 'always'], + 'style/padded-blocks': ['error', { + blocks: 'never', + classes: 'always', + switches: 'never', + }], 'style/padding-line-between-statements': [ 'error', { blankLine: 'always', prev: '*', next: 'class' }, @@ -112,6 +116,8 @@ module.exports = antfu( // Packages. // Things that start with a letter (or digit or underscore), or `@` followed by a letter. ['^@\\w'], + // Internal packages. + // Things that start with `@/`. ['^\\w'], // Absolute imports and other imports such as Vue-style `@/foo`. // Anything not matched in another group. diff --git a/mikro-orm.config.ts b/mikro-orm.config.ts index 08de317a..ce897e92 100644 --- a/mikro-orm.config.ts +++ b/mikro-orm.config.ts @@ -1,18 +1,21 @@ // @ts-nocheck -import { mikroORMConfig } from "./src/configs/database" -import * as entities from "@entities" -import { PluginsManager } from "@services" -import { Options } from "@mikro-orm/core" -import { resolveDependency } from "@utils/functions" +import process from 'node:process' + +import { Options } from '@mikro-orm/core' + +import * as entities from '@/entities' +import { PluginsManager } from '@/services' +import { resolveDependency } from '@/utils/functions' + +import { mikroORMConfig } from './src/configs/database' export default async () => { - const pluginsManager = await resolveDependency(PluginsManager) - await pluginsManager.loadPlugins() + const pluginsManager = await resolveDependency(PluginsManager) + await pluginsManager.loadPlugins() - return { - ...mikroORMConfig[process.env.NODE_ENV || 'development'] as Options, - entities: [...Object.values(entities), ...pluginsManager.getEntities()] - } + return { + ...mikroORMConfig[process.env.NODE_ENV || 'development'] as Options, + entities: [...Object.values(entities), ...pluginsManager.getEntities()], + } } - diff --git a/src/api/controllers/bot.ts b/src/api/controllers/bot.ts index bc87d1b8..67ae4d54 100644 --- a/src/api/controllers/bot.ts +++ b/src/api/controllers/bot.ts @@ -1,226 +1,216 @@ -import { BodyParams, Controller, Delete, Get, PathParams, Post, UseBefore } from "@tsed/common" -import { NotFound, Unauthorized } from "@tsed/exceptions" -import { Required } from "@tsed/schema" -import { BaseGuildTextChannel, BaseGuildVoiceChannel, ChannelType, NewsChannel, PermissionsBitField } from "discord.js" -import { Client, MetadataStorage } from "discordx" - -import { DevAuthenticated, BotOnline } from "@api/middlewares" -import { generalConfig } from "@configs" -import { Guild, User } from "@entities" -import { Database } from "@services" -import { BaseController } from "@utils/classes" -import { getDevs, isDev, isInMaintenance, resolveDependencies, setMaintenance } from "@utils/functions" +import { BodyParams, Controller, Delete, Get, PathParams, Post, UseBefore } from '@tsed/common' +import { NotFound, Unauthorized } from '@tsed/exceptions' +import { Required } from '@tsed/schema' + +import { BaseGuildTextChannel, BaseGuildVoiceChannel, ChannelType, NewsChannel, PermissionsBitField } from 'discord.js' +import { Client, MetadataStorage } from 'discordx' + +import { BotOnline, DevAuthenticated } from '@/api/middlewares' +import { generalConfig } from '@/configs' +import { Guild, User } from '@/entities' +import { Database } from '@/services' +import { BaseController } from '@/utils/classes' +import { getDevs, isDev, isInMaintenance, resolveDependencies, setMaintenance } from '@/utils/functions' @Controller('/bot') @UseBefore( - BotOnline, - DevAuthenticated + BotOnline, + DevAuthenticated ) export class BotController extends BaseController { - - private client: Client - private db: Database - - constructor() { - super() - - resolveDependencies([Client, Database]).then(([client, db]) => { - this.client = client - this.db = db - }) - } - - @Get('/info') - async info() { - - const user: any = this.client.user?.toJSON() - if (user) { - user.iconURL = this.client.user?.displayAvatarURL() - user.bannerURL = this.client.user?.bannerURL() - } - - return { - user, - owner: (await this.client.users.fetch(generalConfig.ownerId).catch(() => null))?.toJSON(), - } - } - - - @Get('/commands') - async commands() { - - const commands = MetadataStorage.instance.applicationCommands - - return commands.map(command => command.toJSON()) - } - - @Get('/guilds') - async guilds() { - - const body: any[] = [] - - for (const discordRawGuild of this.client.guilds.cache.values()) { - - const discordGuild: any = discordRawGuild.toJSON() - discordGuild.iconURL = discordRawGuild.iconURL() - discordGuild.bannerURL = discordRawGuild.bannerURL() - - const databaseGuild = await this.db.get(Guild).findOne({ id: discordGuild.id }) - - body.push({ - discord: discordGuild, - database: databaseGuild - }) - } - - return body - } - - @Get('/guilds/:id') - async guild(@PathParams('id') id: string) { - - // get discord guild - const discordRawGuild = await this.client.guilds.fetch(id).catch(() => null) - if (!discordRawGuild) throw new NotFound('Guild not found') - - const discordGuild: any = discordRawGuild.toJSON() - discordGuild.iconURL = discordRawGuild.iconURL() - discordGuild.bannerURL = discordRawGuild.bannerURL() - - // get database guild - const databaseGuild = await this.db.get(Guild).findOne({ id }) - - return { - discord: discordGuild, - database: databaseGuild - } - } - - @Delete('/guilds/:id') - async deleteGuild(@PathParams('id') id: string) { - - const guild = await this.client.guilds.fetch(id).catch(() => null) - if (!guild) throw new NotFound('Guild not found') - - await guild.leave() - - return { - success: true, - message: 'Guild deleted' - } - } - - @Get('/guilds/:id/invite') - async invite(@PathParams('id') id: string) { - - const guild = await this.client.guilds.fetch(id).catch(() => null) - if (!guild) throw new NotFound('Guild not found') - - const guildChannels = await guild.channels.fetch() - - let invite: any - for (const channel of guildChannels.values()) { - - if ( - channel && - (guild.members.me?.permissionsIn(channel).has(PermissionsBitField.Flags.CreateInstantInvite) || false) && - [ChannelType.GuildText, ChannelType.GuildVoice, ChannelType.GuildAnnouncement].includes(channel.type) - ) { - invite = await (channel as BaseGuildTextChannel | BaseGuildVoiceChannel | NewsChannel | undefined)?.createInvite() - if (invite) break - } - } - - if (invite) return invite.toJSON() - else { - throw new Unauthorized('Missing permission to create an invite in this guild') - } - } - - @Get('/users') - async users() { - - const users: any[] = [], - guilds = this.client.guilds.cache.values() - - for (const guild of guilds) { - - const members = await guild.members.fetch() - - for (const member of members.values()) { - if (!users.find(user => user.id === member.id)) { - - const discordUser: any = member.user.toJSON() - discordUser.iconURL = member.user.displayAvatarURL() - discordUser.bannerURL = member.user.bannerURL() - - const databaseUser = await this.db.get(User).findOne({ id: discordUser.id }) - - users.push({ - discord: discordUser, - database: databaseUser - }) - } - } - } - - return users - } - - @Get('/users/:id') - async user(@PathParams('id') id: string) { - - // get discord user - const discordRawUser = await this.client.users.fetch(id).catch(() => null) - if (!discordRawUser) throw new NotFound('User not found') - - const discordUser: any = discordRawUser.toJSON() - discordUser.iconURL = discordRawUser.displayAvatarURL() - discordUser.bannerURL = discordRawUser.bannerURL() - - // get database user - const databaseUser = await this.db.get(User).findOne({ id }) - - return { - discord: discordUser, - database: databaseUser - } - } - - @Get('/users/cached') - async cachedUsers() { - - return this.client.users.cache.map(user => user.toJSON()) - } - - @Get('/maintenance') - async maintenance() { - - return { - maintenance: await isInMaintenance(), - } - } - - @Post('/maintenance') - async setMaintenance(@Required() @BodyParams('maintenance') maintenance: boolean) { - - await setMaintenance(maintenance) - - return { - maintenance - } - } - - @Get('/devs') - async devs() { - - return getDevs() - } - - @Get('/devs/:id') - async dev(@PathParams('id') id: string) { - - return isDev(id) - } -} \ No newline at end of file + private client: Client + + // test + private db: Database + + constructor() { + super() + + resolveDependencies([Client, Database]).then(([client, db]) => { + this.client = client + this.db = db + }) + } + + @Get('/info') + async info() { + const user: any = this.client.user?.toJSON() + if (user) { + user.iconURL = this.client.user?.displayAvatarURL() + user.bannerURL = this.client.user?.bannerURL() + } + + return { + user, + owner: (await this.client.users.fetch(generalConfig.ownerId).catch(() => null))?.toJSON(), + } + } + + @Get('/commands') + async commands() { + const commands = MetadataStorage.instance.applicationCommands + + return commands.map(command => command.toJSON()) + } + + @Get('/guilds') + async guilds() { + const body: any[] = [] + + for (const discordRawGuild of this.client.guilds.cache.values()) { + const discordGuild: any = discordRawGuild.toJSON() + discordGuild.iconURL = discordRawGuild.iconURL() + discordGuild.bannerURL = discordRawGuild.bannerURL() + + const databaseGuild = await this.db.get(Guild).findOne({ id: discordGuild.id }) + + body.push({ + discord: discordGuild, + database: databaseGuild, + }) + } + + return body + } + + @Get('/guilds/:id') + async guild(@PathParams('id') id: string) { + // get discord guild + const discordRawGuild = await this.client.guilds.fetch(id).catch(() => null) + if (!discordRawGuild) + throw new NotFound('Guild not found') + + const discordGuild: any = discordRawGuild.toJSON() + discordGuild.iconURL = discordRawGuild.iconURL() + discordGuild.bannerURL = discordRawGuild.bannerURL() + + // get database guild + const databaseGuild = await this.db.get(Guild).findOne({ id }) + + return { + discord: discordGuild, + database: databaseGuild, + } + } + + @Delete('/guilds/:id') + async deleteGuild(@PathParams('id') id: string) { + const guild = await this.client.guilds.fetch(id).catch(() => null) + if (!guild) + throw new NotFound('Guild not found') + + await guild.leave() + + return { + success: true, + message: 'Guild deleted', + } + } + + @Get('/guilds/:id/invite') + async invite(@PathParams('id') id: string) { + const guild = await this.client.guilds.fetch(id).catch(() => null) + if (!guild) + throw new NotFound('Guild not found') + + const guildChannels = await guild.channels.fetch() + + let invite: any + for (const channel of guildChannels.values()) { + if ( + channel + && (guild.members.me?.permissionsIn(channel).has(PermissionsBitField.Flags.CreateInstantInvite) || false) + && [ChannelType.GuildText, ChannelType.GuildVoice, ChannelType.GuildAnnouncement].includes(channel.type) + ) { + invite = await (channel as BaseGuildTextChannel | BaseGuildVoiceChannel | NewsChannel | undefined)?.createInvite() + if (invite) + break + } + } + + if (invite) + return invite.toJSON() + else + throw new Unauthorized('Missing permission to create an invite in this guild') + } + + @Get('/users') + async users() { + const users: any[] = [] + const guilds = this.client.guilds.cache.values() + + for (const guild of guilds) { + const members = await guild.members.fetch() + + for (const member of members.values()) { + if (!users.find(user => user.id === member.id)) { + const discordUser: any = member.user.toJSON() + discordUser.iconURL = member.user.displayAvatarURL() + discordUser.bannerURL = member.user.bannerURL() + + const databaseUser = await this.db.get(User).findOne({ id: discordUser.id }) + + users.push({ + discord: discordUser, + database: databaseUser, + }) + } + } + } + + return users + } + + @Get('/users/:id') + async user(@PathParams('id') id: string) { + // get discord user + const discordRawUser = await this.client.users.fetch(id).catch(() => null) + if (!discordRawUser) + throw new NotFound('User not found') + + const discordUser: any = discordRawUser.toJSON() + discordUser.iconURL = discordRawUser.displayAvatarURL() + discordUser.bannerURL = discordRawUser.bannerURL() + + // get database user + const databaseUser = await this.db.get(User).findOne({ id }) + + return { + discord: discordUser, + database: databaseUser, + } + } + + @Get('/users/cached') + async cachedUsers() { + return this.client.users.cache.map(user => user.toJSON()) + } + + @Get('/maintenance') + async maintenance() { + return { + maintenance: await isInMaintenance(), + } + } + + @Post('/maintenance') + async setMaintenance(@Required() @BodyParams('maintenance') maintenance: boolean) { + await setMaintenance(maintenance) + + return { + maintenance, + } + } + + @Get('/devs') + async devs() { + return getDevs() + } + + @Get('/devs/:id') + async dev(@PathParams('id') id: string) { + return isDev(id) + } + +} diff --git a/src/api/controllers/database.ts b/src/api/controllers/database.ts index f1ca56a7..e964d56c 100644 --- a/src/api/controllers/database.ts +++ b/src/api/controllers/database.ts @@ -1,74 +1,76 @@ -import { BodyParams, Controller, Get, Post, UseBefore } from "@tsed/common" -import { InternalServerError } from "@tsed/exceptions" -import { Required } from "@tsed/schema" -import { injectable } from "tsyringe" +import { BodyParams, Controller, Get, Post, UseBefore } from '@tsed/common' +import { InternalServerError } from '@tsed/exceptions' +import { Required } from '@tsed/schema' -import { DevAuthenticated } from "@api/middlewares" -import { databaseConfig } from "@configs" -import { Database } from "@services" -import { BaseController } from "@utils/classes" -import { formatDate, resolveDependencies } from "@utils/functions" +import { injectable } from 'tsyringe' + +import { DevAuthenticated } from '@/api/middlewares' +import { databaseConfig } from '@/configs' +import { Database } from '@/services' +import { BaseController } from '@/utils/classes' +import { formatDate, resolveDependencies } from '@/utils/functions' @Controller('/database') @UseBefore( - DevAuthenticated + DevAuthenticated ) @injectable() export class DatabaseController extends BaseController { - private db: Database - - constructor() { - super() - - resolveDependencies([Database]).then(([db]) => { - this.db = db - }) - } - - @Post('/backup') - async generateBackup() { - - const snapshotName = `snapshot-${formatDate(new Date(), 'onlyDateFileName')}-manual-${Date.now()}` - const success = await this.db.backup(snapshotName) - - if (success) { - return { - message: 'Backup generated', - data: { - snapshotName: snapshotName + '.txt' - } - } - } - else throw new InternalServerError("Couldn't generate backup, see the logs for more information") - } - - @Post('/restore') - async restoreBackup( - @Required() @BodyParams('snapshotName') snapshotName: string, - ) { - - const success = await this.db.restore(snapshotName) - - if (success) return { message: "Backup restored" } - else throw new InternalServerError("Couldn't restore backup, see the logs for more information") - } - - @Get('/backups') - async getBackups() { - - const backupPath = databaseConfig.backup.path - if (!backupPath) throw new InternalServerError("Backup path not set, couldn't find backups") - - const backupList = this.db.getBackupList() - - if (backupList) return backupList - else throw new InternalServerError("Couldn't get backup list, see the logs for more information") - } - - @Get('/size') - async size() { - - return await this.db.getSize() - } -} \ No newline at end of file + private db: Database + + constructor() { + super() + + resolveDependencies([Database]).then(([db]) => { + this.db = db + }) + } + + @Post('/backup') + async generateBackup() { + const snapshotName = `snapshot-${formatDate(new Date(), 'onlyDateFileName')}-manual-${Date.now()}` + const success = await this.db.backup(snapshotName) + + if (success) { + return { + message: 'Backup generated', + data: { + snapshotName: `${snapshotName}.txt`, + }, + } + } else { + throw new InternalServerError('Couldn\'t generate backup, see the logs for more information') + } + } + + @Post('/restore') + async restoreBackup( + @Required() @BodyParams('snapshotName') snapshotName: string + ) { + const success = await this.db.restore(snapshotName) + + if (success) + return { message: 'Backup restored' } + else throw new InternalServerError('Couldn\'t restore backup, see the logs for more information') + } + + @Get('/backups') + async getBackups() { + const backupPath = databaseConfig.backup.path + if (!backupPath) + throw new InternalServerError('Backup path not set, couldn\'t find backups') + + const backupList = this.db.getBackupList() + + if (backupList) + return backupList + else throw new InternalServerError('Couldn\'t get backup list, see the logs for more information') + } + + @Get('/size') + async size() { + return await this.db.getSize() + } + +} diff --git a/src/api/controllers/health.ts b/src/api/controllers/health.ts index 5605deab..5d523513 100644 --- a/src/api/controllers/health.ts +++ b/src/api/controllers/health.ts @@ -1,91 +1,88 @@ -import { Controller, Get, UseBefore } from "@tsed/common" -import { Client } from "discordx" +import { Controller, Get, UseBefore } from '@tsed/common' -import { Data } from "@entities" -import { Database, Logger, Stats } from "@services" -import { BaseController } from "@utils/classes" -import { isInMaintenance, resolveDependencies } from "@utils/functions" -import { DevAuthenticated } from "../middlewares/devAuthenticated" +import { Client } from 'discordx' + +import { Data } from '@/entities' +import { Database, Logger, Stats } from '@/services' +import { BaseController } from '@/utils/classes' +import { isInMaintenance, resolveDependencies } from '@/utils/functions' + +import { DevAuthenticated } from '../middlewares/devAuthenticated' @Controller('/health') export class HealthController extends BaseController { - private client: Client - private db: Database - private stats: Stats - private logger: Logger - - constructor() { - super() - - resolveDependencies([Client, Database, Stats, Logger]).then(([client, db, stats, logger]) => { - this.client = client - this.db = db - this.stats = stats - this.logger = logger - }) - } - - @Get('/check') - async healthcheck() { - - return { - online: this.client.user?.presence.status !== 'offline', - uptime: this.client.uptime, - lastStartup: await this.db.get(Data).get('lastStartup'), - } - } - - @Get('/latency') - async latency() { - - return this.stats.getLatency() - } - - @Get('/usage') - async usage() { - - const body = await this.stats.getPidUsage() - - return body - } - - @Get('/host') - async host() { - - const body = await this.stats.getHostUsage() - - return body - } - - @Get('/monitoring') - @UseBefore( - DevAuthenticated - ) - async monitoring() { - - const body = { - botStatus: { - online: true, - uptime: this.client.uptime, - maintenance: await isInMaintenance() - }, - host: await this.stats.getHostUsage(), - pid: await this.stats.getPidUsage(), - latency: this.stats.getLatency() - } - - return body - } - - @Get('/logs') - @UseBefore( - DevAuthenticated - ) - async logs() { - - const body = await this.logger.getLastLogs() - - return body - } -} \ No newline at end of file + private client: Client + private db: Database + private stats: Stats + private logger: Logger + + constructor() { + super() + + resolveDependencies([Client, Database, Stats, Logger]).then(([client, db, stats, logger]) => { + this.client = client + this.db = db + this.stats = stats + this.logger = logger + }) + } + + @Get('/check') + async healthcheck() { + return { + online: this.client.user?.presence.status !== 'offline', + uptime: this.client.uptime, + lastStartup: await this.db.get(Data).get('lastStartup'), + } + } + + @Get('/latency') + async latency() { + return this.stats.getLatency() + } + + @Get('/usage') + async usage() { + const body = await this.stats.getPidUsage() + + return body + } + + @Get('/host') + async host() { + const body = await this.stats.getHostUsage() + + return body + } + + @Get('/monitoring') + @UseBefore( + DevAuthenticated + ) + async monitoring() { + const body = { + botStatus: { + online: true, + uptime: this.client.uptime, + maintenance: await isInMaintenance(), + }, + host: await this.stats.getHostUsage(), + pid: await this.stats.getPidUsage(), + latency: this.stats.getLatency(), + } + + return body + } + + @Get('/logs') + @UseBefore( + DevAuthenticated + ) + async logs() { + const body = await this.logger.getLastLogs() + + return body + } + +} diff --git a/src/api/controllers/index.ts b/src/api/controllers/index.ts index fd080767..a1f1d4df 100644 --- a/src/api/controllers/index.ts +++ b/src/api/controllers/index.ts @@ -2,4 +2,4 @@ export * from './bot' export * from './database' export * from './health' export * from './other' -export * from './stats' \ No newline at end of file +export * from './stats' diff --git a/src/api/controllers/other.ts b/src/api/controllers/other.ts index 7c670f9b..6f898b98 100644 --- a/src/api/controllers/other.ts +++ b/src/api/controllers/other.ts @@ -1,13 +1,13 @@ -import { Controller, Get } from "@tsed/common" +import { Controller, Get } from '@tsed/common' -import { BaseController } from "@utils/classes" +import { BaseController } from '@/utils/classes' @Controller('/') export class OtherController extends BaseController { - @Get() - async status() { + @Get() + async status() { + return 'API server is running' + } - return 'API server is running' - } -} \ No newline at end of file +} diff --git a/src/api/controllers/stats.ts b/src/api/controllers/stats.ts index 0b657a42..f51f214f 100644 --- a/src/api/controllers/stats.ts +++ b/src/api/controllers/stats.ts @@ -1,110 +1,104 @@ -import { Controller, Get, QueryParams, UseBefore } from "@tsed/common" +import { Controller, Get, QueryParams, UseBefore } from '@tsed/common' -import { DevAuthenticated } from "@api/middlewares" -import { Stats } from "@services" -import { BaseController } from "@utils/classes" -import { resolveDependencies } from "@utils/functions" +import { DevAuthenticated } from '@/api/middlewares' +import { Stats } from '@/services' +import { BaseController } from '@/utils/classes' +import { resolveDependencies } from '@/utils/functions' @Controller('/stats') @UseBefore( - DevAuthenticated + DevAuthenticated ) export class StatsController extends BaseController { - private stats: Stats - - constructor() { - super() - - resolveDependencies([Stats]).then(([stats]) => { - this.stats = stats - }) - } - - @Get('/totals') - async info() { - - const totalStats = await this.stats.getTotalStats() - - return { - stats: { - totalUsers: totalStats.TOTAL_USERS, - totalGuilds: totalStats.TOTAL_GUILDS, - totalActiveUsers: totalStats.TOTAL_ACTIVE_USERS, - totalCommands: totalStats.TOTAL_COMMANDS, - } - } - } - - @Get('/interaction/last') - async lastInteraction() { - - const lastInteraction = await this.stats.getLastInteraction() - return lastInteraction - } - - @Get('/guilds/last') - async lastGuildAdded() { - - const lastGuild = await this.stats.getLastGuildAdded() - return lastGuild - } - - @Get('/commands/usage') - async commandsUsage(@QueryParams('numberOfDays') numberOfDays: number = 7) { - - const commandsUsage = { - slashCommands: await this.stats.countStatsPerDays('CHAT_INPUT_COMMAND_INTERACTION', numberOfDays), - simpleCommands: await this.stats.countStatsPerDays('SIMPLE_COMMAND_MESSAGE', numberOfDays), - userContextMenus: await this.stats.countStatsPerDays('USER_CONTEXT_MENU_COMMAND_INTERACTION', numberOfDays), - messageContextMenus: await this.stats.countStatsPerDays('MESSAGE_CONTEXT_MENU_COMMAND_INTERACTION', numberOfDays), - } - - const body = [] - for (let i = 0; i < numberOfDays; i++) { - body.push({ - date: commandsUsage.slashCommands[i].date, - slashCommands: commandsUsage.slashCommands[i].count, - simpleCommands: commandsUsage.simpleCommands[i].count, - contextMenus: commandsUsage.userContextMenus[i].count + commandsUsage.messageContextMenus[i].count - }) - } - - return body - } - - @Get('/commands/top') - async topCommands() { - - const topCommands = await this.stats.getTopCommands() - - return topCommands - } - - @Get('/users/activity') - async usersActivity() { - - const usersActivity = await this.stats.getUsersActivity() - - return usersActivity - } - - @Get('/guilds/top') - async topGuilds() { - - const topGuilds = await this.stats.getTopGuilds() - - return topGuilds - } - - @Get('/usersAndGuilds') - async usersAndGuilds(@QueryParams('numberOfDays') numberOfDays: number = 7) { - - return { - activeUsers: await this.stats.countStatsPerDays('TOTAL_ACTIVE_USERS', numberOfDays), - users: await this.stats.countStatsPerDays('TOTAL_USERS', numberOfDays), - guilds: await this.stats.countStatsPerDays('TOTAL_GUILDS', numberOfDays), - } - } - -} \ No newline at end of file + private stats: Stats + + constructor() { + super() + + resolveDependencies([Stats]).then(([stats]) => { + this.stats = stats + }) + } + + @Get('/totals') + async info() { + const totalStats = await this.stats.getTotalStats() + + return { + stats: { + totalUsers: totalStats.TOTAL_USERS, + totalGuilds: totalStats.TOTAL_GUILDS, + totalActiveUsers: totalStats.TOTAL_ACTIVE_USERS, + totalCommands: totalStats.TOTAL_COMMANDS, + }, + } + } + + @Get('/interaction/last') + async lastInteraction() { + const lastInteraction = await this.stats.getLastInteraction() + + return lastInteraction + } + + @Get('/guilds/last') + async lastGuildAdded() { + const lastGuild = await this.stats.getLastGuildAdded() + + return lastGuild + } + + @Get('/commands/usage') + async commandsUsage(@QueryParams('numberOfDays') numberOfDays: number = 7) { + const commandsUsage = { + slashCommands: await this.stats.countStatsPerDays('CHAT_INPUT_COMMAND_INTERACTION', numberOfDays), + simpleCommands: await this.stats.countStatsPerDays('SIMPLE_COMMAND_MESSAGE', numberOfDays), + userContextMenus: await this.stats.countStatsPerDays('USER_CONTEXT_MENU_COMMAND_INTERACTION', numberOfDays), + messageContextMenus: await this.stats.countStatsPerDays('MESSAGE_CONTEXT_MENU_COMMAND_INTERACTION', numberOfDays), + } + + const body = [] + for (let i = 0; i < numberOfDays; i++) { + body.push({ + date: commandsUsage.slashCommands[i].date, + slashCommands: commandsUsage.slashCommands[i].count, + simpleCommands: commandsUsage.simpleCommands[i].count, + contextMenus: commandsUsage.userContextMenus[i].count + commandsUsage.messageContextMenus[i].count, + }) + } + + return body + } + + @Get('/commands/top') + async topCommands() { + const topCommands = await this.stats.getTopCommands() + + return topCommands + } + + @Get('/users/activity') + async usersActivity() { + const usersActivity = await this.stats.getUsersActivity() + + return usersActivity + } + + @Get('/guilds/top') + async topGuilds() { + const topGuilds = await this.stats.getTopGuilds() + + return topGuilds + } + + @Get('/usersAndGuilds') + async usersAndGuilds(@QueryParams('numberOfDays') numberOfDays: number = 7) { + return { + activeUsers: await this.stats.countStatsPerDays('TOTAL_ACTIVE_USERS', numberOfDays), + users: await this.stats.countStatsPerDays('TOTAL_USERS', numberOfDays), + guilds: await this.stats.countStatsPerDays('TOTAL_GUILDS', numberOfDays), + } + } + +} diff --git a/src/api/middlewares/botOnline.ts b/src/api/middlewares/botOnline.ts index 4bcdf2f5..54e4b73b 100644 --- a/src/api/middlewares/botOnline.ts +++ b/src/api/middlewares/botOnline.ts @@ -1,23 +1,24 @@ -import { Middleware } from "@tsed/common" -import { InternalServerError } from "@tsed/exceptions" -import { resolveDependencies } from "@utils/functions" -import { Client } from "discordx" +import { Middleware } from '@tsed/common' +import { InternalServerError } from '@tsed/exceptions' -@Middleware() -export class BotOnline { +import { Client } from 'discordx' - private client: Client +import { resolveDependencies } from '@/utils/functions' - constructor() { +@Middleware() +export class BotOnline { - resolveDependencies([Client]).then(([client]) => { - this.client = client - }) - } + private client: Client - async use() { + constructor() { + resolveDependencies([Client]).then(([client]) => { + this.client = client + }) + } - if (this.client.user?.presence.status === 'offline') throw new InternalServerError('Bot is offline') - } + async use() { + if (this.client.user?.presence.status === 'offline') + throw new InternalServerError('Bot is offline') + } -} \ No newline at end of file +} diff --git a/src/api/middlewares/devAuthenticated.ts b/src/api/middlewares/devAuthenticated.ts index 15f191aa..e2e54ec4 100644 --- a/src/api/middlewares/devAuthenticated.ts +++ b/src/api/middlewares/devAuthenticated.ts @@ -1,71 +1,75 @@ -import { Context, Middleware, PlatformContext } from "@tsed/common" -import { BadRequest, Unauthorized } from "@tsed/exceptions" -import DiscordOauth2 from "discord-oauth2" +import process from 'node:process' -import { Store } from "@services" -import { isDev, resolveDependency } from "@utils/functions" +import { Context, Middleware, PlatformContext } from '@tsed/common' +import { BadRequest, Unauthorized } from '@tsed/exceptions' + +import DiscordOauth2 from 'discord-oauth2' + +import { Store } from '@/services' +import { isDev, resolveDependency } from '@/utils/functions' const discordOauth2 = new DiscordOauth2() const timeout = 10 * 60 * 1000 -const fmaTokenRegex = /mfa\.[\w-]{84}/ -const nonFmaTokenRegex = /[\w-]{24}\.[\w-]{6}\.[\w-]{27}/ + +// const fmaTokenRegex = /mfa\.[\w-]{84}/ +// const nonFmaTokenRegex = /[\w-]{24}\.[\w-]{6}\.[\w-]{27}/ @Middleware() export class DevAuthenticated { - private store: Store - - constructor() { - resolveDependency(Store).then((store) => { - this.store = store - }) - } - - async use(@Context() { request }: PlatformContext) { - - // if we are in development mode, we don't need to check the token - // if (process.env['NODE_ENV'] === 'development') return next() - - // check if the request includes valid authorization header - const authHeader = request.headers['authorization'] - if (!authHeader || !authHeader.startsWith('Bearer ')) throw new BadRequest('Missing token') - - // get the token from the authorization header - const token = authHeader.split(' ')[1] - if (!token) throw new BadRequest('Invalid token') - - // pass if the token is the admin token of the app - if (token === process.env['API_ADMIN_TOKEN']) return - - // verify that the token is a valid FMA protected (or not) OAuth2 token -> https://stackoverflow.com/questions/71166596/is-there-a-way-to-check-if-a-discord-account-token-is-valid-or-not - // FIXME: doesn't match actual tokens - //if (!token.match(fmaTokenRegex) && !token.match(nonFmaTokenRegex)) return ctx.throw(400, 'Invalid token') - - // directly skip the middleware if the token is already in the store, which is used here as a "cache" - const authorizedAPITokens = this.store.get('authorizedAPITokens') - if (authorizedAPITokens.includes(token)) return - - // we get the user's profile from the token using the `discord-oauth2` package - try { - - const user = await discordOauth2.getUser(token) - - // check if logged user is a dev (= admin) of the bot - if (isDev(user.id)) { - - // we add the token to the store and set a timeout to remove it after 10 minutes - this.store.update('authorizedAPITokens', (authorizedAPITokens) => [...authorizedAPITokens, token]) - setTimeout(() => { - this.store.update('authorizedAPITokens', (authorizedAPITokens) => authorizedAPITokens.filter(t => t !== token)) - }, timeout) - - } else { - throw new Unauthorized('Unauthorized') - } - - } catch (err) { - throw new BadRequest('Invalid discord token') - } - } -} \ No newline at end of file + private store: Store + + constructor() { + resolveDependency(Store).then((store) => { + this.store = store + }) + } + + async use(@Context() { request }: PlatformContext) { + // if we are in development mode, we don't need to check the token + // if (process.env['NODE_ENV'] === 'development') return next() + + // check if the request includes valid authorization header + const authHeader = request.headers.authorization + if (!authHeader || !authHeader.startsWith('Bearer ')) + throw new BadRequest('Missing token') + + // get the token from the authorization header + const token = authHeader.split(' ')[1] + if (!token) + throw new BadRequest('Invalid token') + + // pass if the token is the admin token of the app + if (token === process.env.API_ADMIN_TOKEN) + return + + // verify that the token is a valid FMA protected (or not) OAuth2 token -> https://stackoverflow.com/questions/71166596/is-there-a-way-to-check-if-a-discord-account-token-is-valid-or-not + // FIXME: doesn't match actual tokens + // if (!token.match(fmaTokenRegex) && !token.match(nonFmaTokenRegex)) return ctx.throw(400, 'Invalid token') + + // directly skip the middleware if the token is already in the store, which is used here as a "cache" + const authorizedAPITokens = this.store.get('authorizedAPITokens') + if (authorizedAPITokens.includes(token)) + return + + // we get the user's profile from the token using the `discord-oauth2` package + try { + const user = await discordOauth2.getUser(token) + + // check if logged user is a dev (= admin) of the bot + if (isDev(user.id)) { + // we add the token to the store and set a timeout to remove it after 10 minutes + this.store.update('authorizedAPITokens', authorizedAPITokens => [...authorizedAPITokens, token]) + setTimeout(() => { + this.store.update('authorizedAPITokens', authorizedAPITokens => authorizedAPITokens.filter(t => t !== token)) + }, timeout) + } else { + throw new Unauthorized('Unauthorized') + } + } catch (err) { + throw new BadRequest('Invalid discord token') + } + } + +} diff --git a/src/api/middlewares/index.ts b/src/api/middlewares/index.ts index 084d5d0a..323729c3 100644 --- a/src/api/middlewares/index.ts +++ b/src/api/middlewares/index.ts @@ -1,3 +1,3 @@ export * from './log' export * from './botOnline' -export * from './devAuthenticated' \ No newline at end of file +export * from './devAuthenticated' diff --git a/src/api/middlewares/log.ts b/src/api/middlewares/log.ts index bc7fda51..d01d66a4 100644 --- a/src/api/middlewares/log.ts +++ b/src/api/middlewares/log.ts @@ -1,36 +1,34 @@ -import { Context, Middleware, PlatformContext } from "@tsed/common" -import chalk from "chalk" +import { Context, Middleware, PlatformContext } from '@tsed/common' -import { Logger } from "@services" -import { resolveDependency } from "@utils/functions" +import chalk from 'chalk' + +import { Logger } from '@/services' +import { resolveDependency } from '@/utils/functions' @Middleware() export class Log { - private logger: Logger - - constructor() { - resolveDependency(Logger).then((logger) => { - this.logger = logger - }) - } + private logger: Logger - use(@Context() { request }: PlatformContext) { - - // don't log anything if the request has a `logIgnore` query param - if (!request.query.logIgnore) { + constructor() { + resolveDependency(Logger).then((logger) => { + this.logger = logger + }) + } - const { method, url } = request + use(@Context() { request }: PlatformContext) { + // don't log anything if the request has a `logIgnore` query param + if (!request.query.logIgnore) { + const { method, url } = request - const message = `(API) ${method} - ${url}` - const chalkedMessage = `(${chalk.bold.white('API')}) ${chalk.bold.green(method)} - ${chalk.bold.blue(url)}` + const message = `(API) ${method} - ${url}` + const chalkedMessage = `(${chalk.bold.white('API')}) ${chalk.bold.green(method)} - ${chalk.bold.blue(url)}` - this.logger.console(chalkedMessage) - this.logger.file(message) - - } else { - delete request.query.logIgnore - } - } + this.logger.console(chalkedMessage) + this.logger.file(message) + } else { + delete request.query.logIgnore + } + } -} \ No newline at end of file +} diff --git a/src/api/server.ts b/src/api/server.ts index 0ca601d8..f2474736 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -1,66 +1,68 @@ -import { Inject, PlatformAcceptMimesMiddleware, PlatformApplication } from "@tsed/common" -import { PlatformExpress } from "@tsed/platform-express" import '@tsed/swagger' -import { singleton } from "tsyringe" -import bodyParser from "body-parser" -import * as controllers from "@api/controllers" -import { Log } from "@api/middlewares" -import { MikroORM, UseRequestContext } from "@mikro-orm/core" -import { Database, PluginsManager, Store } from "@services" +import process from 'node:process' + +import { MikroORM, UseRequestContext } from '@mikro-orm/core' +import { Inject, PlatformAcceptMimesMiddleware, PlatformApplication } from '@tsed/common' +import { PlatformExpress } from '@tsed/platform-express' + +import bodyParser from 'body-parser' +import { singleton } from 'tsyringe' + +import * as controllers from '@/api/controllers' +import { Log } from '@/api/middlewares' +import { Database, PluginsManager, Store } from '@/services' @singleton() export class Server { - @Inject() app: PlatformApplication - - orm: MikroORM + @Inject() app: PlatformApplication + + orm: MikroORM - constructor( - private pluginsManager: PluginsManager, - private store: Store, - db: Database - ) { - this.orm = db.orm - } + constructor( + private pluginsManager: PluginsManager, + private store: Store, + db: Database + ) { + this.orm = db.orm + } - $beforeRoutesInit() { - - this.app - .use(bodyParser.json()) - .use(bodyParser.urlencoded({extended: true})) - .use(Log) - .use(PlatformAcceptMimesMiddleware) + $beforeRoutesInit() { + this.app + .use(bodyParser.json()) + .use(bodyParser.urlencoded({ extended: true })) + .use(Log) + .use(PlatformAcceptMimesMiddleware) - return null - } + return null + } - @UseRequestContext() - async start(): Promise { + @UseRequestContext() + async start(): Promise { + const platform = await PlatformExpress.bootstrap(Server, { + rootDir: __dirname, + httpPort: Number.parseInt(process.env.API_PORT) || 4000, + httpsPort: false, + acceptMimes: ['application/json'], + mount: { + '/': [...Object.values(controllers), ...this.pluginsManager.getControllers()], + }, + swagger: [ + { + path: '/docs', + specVersion: '3.0.1', + }, + ], + logger: { + level: 'warn', + disableRoutesSummary: true, + }, + }) - const platform = await PlatformExpress.bootstrap(Server, { - rootDir: __dirname, - httpPort: parseInt(process.env['API_PORT']) || 4000, - httpsPort: false, - acceptMimes: ['application/json'], - mount: { - '/': [...Object.values(controllers), ...this.pluginsManager.getControllers()] - }, - swagger: [ - { - path: '/docs', - specVersion: '3.0.1' - } - ], - logger: { - level: 'warn', - disableRoutesSummary: true - } - }) + platform.listen().then(() => { + this.store.update('ready', e => ({ ...e, api: true })) + }) + } - platform.listen().then(() => { - - this.store.update('ready', (e) => ({ ...e, api: true })) - }) - } -} \ No newline at end of file +} diff --git a/src/client.ts b/src/client.ts index 47891575..03c1e503 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,45 +1,49 @@ -import { GatewayIntentBits, Partials } from "discord.js" - -import { generalConfig, logsConfig } from "@configs" -import { ExtractLocale, Maintenance, NotBot, RequestContextIsolator } from "@guards" -import { ClientOptions } from "discordx" - -export const clientConfig = (): ClientOptions => ({ - - // to only use global commands (use @Guild for specific guild command), comment this line - botGuilds: process.env.NODE_ENV === 'development' ? [process.env.TEST_GUILD_ID] : undefined, - - // discord intents - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.GuildMessageReactions, - GatewayIntentBits.GuildVoiceStates, - GatewayIntentBits.GuildPresences, - GatewayIntentBits.DirectMessages, - GatewayIntentBits.MessageContent - ], - - partials: [ - Partials.Channel, - Partials.Message, - Partials.Reaction - ], - - // debug logs are disabled in silent mode - silent: !logsConfig.debug, - - guards: [ - RequestContextIsolator, - NotBot, - Maintenance, - ExtractLocale - ], - - // configuration for @SimpleCommand - simpleCommand: { - prefix: generalConfig.simpleCommandsPrefix, +import process from 'node:process' + +import { GatewayIntentBits, Partials } from 'discord.js' +import { ClientOptions } from 'discordx' + +import { generalConfig, logsConfig } from '@/configs' +import { ExtractLocale, Maintenance, NotBot, RequestContextIsolator } from '@/guards' + +export function clientConfig(): ClientOptions { + return { + + // to only use global commands (use @Guild for specific guild command), comment this line + botGuilds: process.env.NODE_ENV === 'development' ? [process.env.TEST_GUILD_ID] : undefined, + + // discord intents + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildMessageReactions, + GatewayIntentBits.GuildVoiceStates, + GatewayIntentBits.GuildPresences, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.MessageContent, + ], + + partials: [ + Partials.Channel, + Partials.Message, + Partials.Reaction, + ], + + // debug logs are disabled in silent mode + silent: !logsConfig.debug, + + guards: [ + RequestContextIsolator, + NotBot, + Maintenance, + ExtractLocale, + ], + + // configuration for @SimpleCommand + simpleCommand: { + prefix: generalConfig.simpleCommandsPrefix, + }, + } - -}) \ No newline at end of file +} diff --git a/src/commands/Admin/prefix.ts b/src/commands/Admin/prefix.ts index f20d767e..b088711c 100644 --- a/src/commands/Admin/prefix.ts +++ b/src/commands/Admin/prefix.ts @@ -1,15 +1,16 @@ -import { Category } from "@discordx/utilities" -import { ApplicationCommandOptionType, CommandInteraction } from "discord.js" -import { Client } from "discordx" -import { injectable } from "tsyringe" - -import { generalConfig } from "@configs" -import { Discord, Slash, SlashOption } from "@decorators" -import { Guild } from "@entities" -import { UnknownReplyError } from "@errors" -import { Guard, UserPermissions } from "@guards" -import { Database } from "@services" -import { resolveGuild, simpleSuccessEmbed } from "@utils/functions" +import { Category } from '@discordx/utilities' + +import { ApplicationCommandOptionType, CommandInteraction } from 'discord.js' +import { Client } from 'discordx' +import { injectable } from 'tsyringe' + +import { generalConfig } from '@/configs' +import { Discord, Slash, SlashOption } from '@/decorators' +import { Guild } from '@/entities' +import { UnknownReplyError } from '@/errors' +import { Guard, UserPermissions } from '@/guards' +import { Database } from '@/services' +import { resolveGuild, simpleSuccessEmbed } from '@/utils/functions' @Discord() @injectable() @@ -17,7 +18,7 @@ import { resolveGuild, simpleSuccessEmbed } from "@utils/functions" export default class PrefixCommand { constructor( - private db: Database, + private db: Database ) {} @Slash({ name: 'prefix' }) @@ -25,34 +26,31 @@ export default class PrefixCommand { UserPermissions(['Administrator']) ) async prefix( - @SlashOption({ - name: 'prefix', + @SlashOption({ + name: 'prefix', localizationSource: 'COMMANDS.PREFIX.OPTIONS.PREFIX', type: ApplicationCommandOptionType.String, }) prefix: string | undefined, - interaction: CommandInteraction, - client: Client, - { localize }: InteractionData + interaction: CommandInteraction, + client: Client, + { localize }: InteractionData ) { - - const guild = resolveGuild(interaction), - guildData = await this.db.get(Guild).findOne({ id: guild?.id || '' }) + const guild = resolveGuild(interaction) + const guildData = await this.db.get(Guild).findOne({ id: guild?.id || '' }) if (guildData) { - guildData.prefix = prefix || null this.db.get(Guild).persistAndFlush(guildData) simpleSuccessEmbed( - interaction, - localize['COMMANDS']['PREFIX']['EMBED']['DESCRIPTION']({ - prefix: prefix || generalConfig.simpleCommandsPrefix + interaction, + localize.COMMANDS.PREFIX.EMBED.DESCRIPTION({ + prefix: prefix || generalConfig.simpleCommandsPrefix, }) ) - } - else { + } else { throw new UnknownReplyError(interaction) } - } -} \ No newline at end of file + +} diff --git a/src/commands/General/help.ts b/src/commands/General/help.ts index cd637a58..d53aafec 100644 --- a/src/commands/General/help.ts +++ b/src/commands/General/help.ts @@ -1,10 +1,11 @@ -import { Category } from "@discordx/utilities" -import { ActionRowBuilder, APISelectMenuOption, CommandInteraction, EmbedBuilder, inlineCode, StringSelectMenuBuilder, StringSelectMenuInteraction } from "discord.js" -import { Client, MetadataStorage, SelectMenuComponent } from "discordx" +import { Category } from '@discordx/utilities' -import { Discord, Slash } from "@decorators" -import { chunkArray, getColor, resolveGuild, validString } from "@utils/functions" -import { TranslationFunctions } from "src/i18n/i18n-types" +import { ActionRowBuilder, APISelectMenuOption, CommandInteraction, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuInteraction } from 'discord.js' +import { Client, MetadataStorage, SelectMenuComponent } from 'discordx' +import { TranslationFunctions } from 'src/i18n/i18n-types' + +import { Discord, Slash } from '@/decorators' +import { chunkArray, getColor, resolveGuild, validString } from '@/utils/functions' @Discord() @Category('General') @@ -16,88 +17,82 @@ export default class HelpCommand { this.loadCategories() } - @Slash({ - name: 'help' - }) + @Slash({ + name: 'help', + }) async help( - interaction: CommandInteraction, - client: Client, + interaction: CommandInteraction, + client: Client, { localize }: InteractionData ) { - const embed = await this.getEmbed({ client, interaction, locale: localize }) - let components: any[] = [] - components.push(this.getSelectDropdown("categories", localize).toJSON()) + const components: any[] = [] + components.push(this.getSelectDropdown('categories', localize).toJSON()) - interaction.followUp({ + interaction.followUp({ embeds: [embed], - components + components, }) } @SelectMenuComponent({ - id: 'help-category-selector' + id: 'help-category-selector', }) async selectCategory(interaction: StringSelectMenuInteraction, client: Client, { localize }: InteractionData) { + const category = interaction.values[0] - const category = interaction.values[0] - - const embed = await this.getEmbed({ client, interaction, category, locale: localize }) - let components: any[] = [] + const embed = await this.getEmbed({ client, interaction, category, locale: localize }) + const components: any[] = [] components.push(this.getSelectDropdown(category, localize).toJSON()) - interaction.update({ - embeds: [embed], - components - }) - } - + interaction.update({ + embeds: [embed], + components, + }) + } private async getEmbed({ client, interaction, category = '', pageNumber = 0, locale }: { - client: Client, - interaction: CommandInteraction | StringSelectMenuInteraction , - category?: string, + client: Client + interaction: CommandInteraction | StringSelectMenuInteraction + category?: string pageNumber?: number locale: TranslationFunctions }): Promise { - const commands = this._categories.get(category) - + // default embed if (!commands) { - const embed = new EmbedBuilder() .setAuthor({ - name: interaction.user.username, - iconURL: interaction.user.displayAvatarURL({ forceStatic: false }) + name: interaction.user.username, + iconURL: interaction.user.displayAvatarURL({ forceStatic: false }), }) .setTitle(locale.COMMANDS.HELP.EMBED.TITLE()) .setThumbnail('https://upload.wikimedia.org/wikipedia/commons/a/a4/Cute-Ball-Help-icon.png') .setColor(getColor('primary')) - let currentGuild = resolveGuild(interaction) - let applicationCommands = [ + const currentGuild = resolveGuild(interaction) + const applicationCommands = [ ...(currentGuild ? (await currentGuild.commands.fetch()).values() : []), - ...(await client.application!.commands.fetch()).values() + ...(await client.application!.commands.fetch()).values(), ] - + for (const category of this._categories) { - - let commands = category[1] - .map(cmd => { - return " acmd.name == (cmd.group ? cmd.group : cmd.name))!.id + - ">" + const commands = category[1] + .map((cmd) => { + return ` acmd.name === (cmd.group ? cmd.group : cmd.name))!.id + }>` }) - + embed.addFields([{ name: category[0], - value: commands.join(', ') + value: commands.join(', '), }]) } @@ -105,85 +100,76 @@ export default class HelpCommand { } // specific embed - const chunks = chunkArray(commands, 24), - maxPage = chunks.length, - resultsOfPage = chunks[pageNumber] + const chunks = chunkArray(commands, 24) + const maxPage = chunks.length + const resultsOfPage = chunks[pageNumber] const embed = new EmbedBuilder() .setAuthor({ name: interaction.user.username, - iconURL: interaction.user.displayAvatarURL({ forceStatic: false }) + iconURL: interaction.user.displayAvatarURL({ forceStatic: false }), }) - .setTitle(locale.COMMANDS.HELP.EMBED.CATEGORY_TITLE({category})) + .setTitle(locale.COMMANDS.HELP.EMBED.CATEGORY_TITLE({ category })) .setFooter({ - text: `${client.user!.username} • Page ${pageNumber + 1} of ${maxPage}` + text: `${client.user!.username} • Page ${pageNumber + 1} of ${maxPage}`, }) - - if (!resultsOfPage) return embed - for (const item of resultsOfPage) { + if (!resultsOfPage) + return embed - let currentGuild = resolveGuild(interaction) - let applicationCommands = [ + for (const item of resultsOfPage) { + const currentGuild = resolveGuild(interaction) + const applicationCommands = [ ...(currentGuild ? (await currentGuild.commands.fetch()).values() : []), - ...(await client.application!.commands.fetch()).values() + ...(await client.application!.commands.fetch()).values(), ] const { description } = item - const fieldValue = validString(description) ? description : "No description" - const name = " acmd.name == (item.group ? item.group : item.name))!.id + - ">" + const fieldValue = validString(description) ? description : 'No description' + const name = ` acmd.name === (item.group ? item.group : item.name))!.id}>` embed.addFields([{ - name: name, - value: fieldValue, - inline: resultsOfPage.length > 5 + name, + value: fieldValue, + inline: resultsOfPage.length > 5, }]) } return embed } - private getSelectDropdown(defaultValue = "categories", locale: TranslationFunctions): ActionRowBuilder { - - const optionsForEmbed: APISelectMenuOption[] = [] + private getSelectDropdown(defaultValue = 'categories', locale: TranslationFunctions): ActionRowBuilder { + const optionsForEmbed: APISelectMenuOption[] = [] - optionsForEmbed.push({ - description: locale.COMMANDS.HELP.SELECT_MENU.TITLE(), - label: "Categories", - value: "categories", - default: defaultValue === "categories" - }) + optionsForEmbed.push({ + description: locale.COMMANDS.HELP.SELECT_MENU.TITLE(), + label: 'Categories', + value: 'categories', + default: defaultValue === 'categories', + }) - for (const [category] of this._categories) { + for (const [category] of this._categories) { + const description = locale.COMMANDS.HELP.SELECT_MENU.CATEGORY_DESCRIPTION({ category }) + optionsForEmbed.push({ + description, + label: category, + value: category, + default: defaultValue === category, + }) + } - const description = locale.COMMANDS.HELP.SELECT_MENU.CATEGORY_DESCRIPTION({category}) - optionsForEmbed.push({ - description, - label: category, - value: category, - default: defaultValue === category - }) - } + const selectMenu = new StringSelectMenuBuilder().addOptions(optionsForEmbed).setCustomId('help-category-selector') - const selectMenu = new StringSelectMenuBuilder().addOptions(optionsForEmbed).setCustomId("help-category-selector") - return new ActionRowBuilder().addComponents(selectMenu) - } + } loadCategories(): void { - const commands: CommandCategory[] = MetadataStorage.instance.applicationCommandSlashesFlat as CommandCategory[] - - for (const command of commands) { + for (const command of commands) { const { category } = command - if (!category || !validString(category)) continue + if (!category || !validString(category)) + continue if (this._categories.has(category)) { this._categories.get(category)?.push(command) @@ -192,4 +178,5 @@ export default class HelpCommand { } } } -} \ No newline at end of file + +} diff --git a/src/commands/General/info.ts b/src/commands/General/info.ts index e71820ae..34362d0c 100644 --- a/src/commands/General/info.ts +++ b/src/commands/General/info.ts @@ -1,24 +1,25 @@ -import { Category } from "@discordx/utilities" -import dayjs from "dayjs" -import relativeTime from "dayjs/plugin/relativeTime" -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, EmbedField } from "discord.js" -import { Client } from "discordx" -import { injectable } from "tsyringe" -dayjs.extend(relativeTime) +import { Category } from '@discordx/utilities' + +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, EmbedField } from 'discord.js' +import { Client } from 'discordx' +import { injectable } from 'tsyringe' -import { generalConfig } from "@configs" -import { Discord, Slash } from "@decorators" -import { Guard } from "@guards" -import { Stats } from "@services" -import { getColor, getTscordVersion, isValidUrl, timeAgo } from "@utils/functions" +import { generalConfig } from '@/configs' +import { Discord, Slash } from '@/decorators' +import { Guard } from '@/guards' +import { Stats } from '@/services' +import { getColor, getTscordVersion, isValidUrl, timeAgo } from '@/utils/functions' -// @ts-ignore - because it is outside the `rootDir` of tsconfig -import packageJson from "../../../package.json" +import packageJson from '../../../package.json' + +dayjs.extend(relativeTime) const links = [ { label: 'Invite me!', url: generalConfig.links.invite }, { label: 'Support server', url: generalConfig.links.supportServer }, - { label: 'Github', url: generalConfig.links.gitRemoteRepo } + { label: 'Github', url: generalConfig.links.gitRemoteRepo }, ] @Discord() @@ -36,10 +37,8 @@ export default class InfoCommand { @Guard() async info( interaction: CommandInteraction, - client: Client, - { localize }: InteractionData + client: Client ) { - const embed = new EmbedBuilder() .setAuthor({ name: interaction.user.username, @@ -107,7 +106,7 @@ export default class InfoCommand { */ fields.push({ name: 'Libraries', - value: `[discord.js](https://discord.js.org/) (*v${packageJson.dependencies['discord.js'].replace('^', '')}*)\n[discordx](https://discordx.js.org/) (*v${packageJson.dependencies['discordx'].replace('^', '')}*)`, + value: `[discord.js](https://discord.js.org/) (*v${packageJson.dependencies['discord.js'].replace('^', '')}*)\n[discordx](https://discordx.js.org/) (*v${packageJson.dependencies.discordx.replace('^', '')}*)`, inline: true, }) @@ -118,24 +117,26 @@ export default class InfoCommand { * Define links buttons */ const buttons = links - .map(link => { + .map((link) => { const url = link.url.split('_').join('') if (isValidUrl(url)) { return new ButtonBuilder() .setLabel(link.label) .setURL(url) .setStyle(ButtonStyle.Link) - } else return null + } else { + return null + } }) .filter(link => link) as ButtonBuilder[] const row = new ActionRowBuilder() .addComponents(...buttons) - + // finally send the embed interaction.followUp({ embeds: [embed], components: [row], }) - } -} \ No newline at end of file + +} diff --git a/src/commands/General/invite.ts b/src/commands/General/invite.ts index 384b56e7..b2ae919a 100644 --- a/src/commands/General/invite.ts +++ b/src/commands/General/invite.ts @@ -1,34 +1,35 @@ -import { Category } from "@discordx/utilities" -import { CommandInteraction, EmbedBuilder } from "discord.js" -import { Client } from "discordx" +import { Category } from '@discordx/utilities' -import { generalConfig } from "@configs" -import { Discord, Slash } from "@decorators" -import { Guard } from "@guards" -import { getColor } from "@utils/functions" +import { CommandInteraction, EmbedBuilder } from 'discord.js' +import { Client } from 'discordx' + +import { generalConfig } from '@/configs' +import { Discord, Slash } from '@/decorators' +import { Guard } from '@/guards' +import { getColor } from '@/utils/functions' @Discord() @Category('General') export default class InviteCommand { - @Slash({ - name: 'invite' - }) + @Slash({ + name: 'invite', + }) @Guard() async invite( - interaction: CommandInteraction, + interaction: CommandInteraction, client: Client, { localize }: InteractionData ) { - const embed = new EmbedBuilder() .setTitle(localize.COMMANDS.INVITE.EMBED.TITLE()) - .setDescription(localize.COMMANDS.INVITE.EMBED.DESCRIPTION({link: generalConfig.links.invite})) + .setDescription(localize.COMMANDS.INVITE.EMBED.DESCRIPTION({ link: generalConfig.links.invite })) .setColor(getColor('primary')) - .setFooter({ text : 'Powered by DiscBot Team ❤'}) + .setFooter({ text: 'Powered by DiscBot Team ❤' }) interaction.followUp({ - embeds: [embed] + embeds: [embed], }) } -} \ No newline at end of file + +} diff --git a/src/commands/General/ping.ts b/src/commands/General/ping.ts index 00fd00b7..ed475380 100644 --- a/src/commands/General/ping.ts +++ b/src/commands/General/ping.ts @@ -1,31 +1,31 @@ -import { Category } from "@discordx/utilities" -import type { CommandInteraction, Message } from "discord.js" -import { Client } from "discordx" +import { Category } from '@discordx/utilities' -import { Discord, Slash } from "@decorators" +import { CommandInteraction, Message } from 'discord.js' +import { Client } from 'discordx' + +import { Discord, Slash } from '@/decorators' @Discord() @Category('General') export default class PingCommand { - @Slash({ - name: 'ping' + @Slash({ + name: 'ping', }) async ping( interaction: CommandInteraction, client: Client, { localize }: InteractionData ) { - - const msg = (await interaction.followUp({ content: "Pinging...", fetchReply: true })) as Message + const msg = (await interaction.followUp({ content: 'Pinging...', fetchReply: true })) as Message - const content = localize["COMMANDS"]["PING"]["MESSAGE"]({ - member: msg.inGuild() ? `${interaction.member},` : "", + const content = localize.COMMANDS.PING.MESSAGE({ + member: msg.inGuild() ? `${interaction.member},` : '', time: msg.createdTimestamp - interaction.createdTimestamp, - heartbeat: client.ws.ping ? ` The heartbeat ping is ${Math.round(client.ws.ping)}ms.` : "" + heartbeat: client.ws.ping ? ` The heartbeat ping is ${Math.round(client.ws.ping)}ms.` : '', }) - await msg.edit(content) + await msg.edit(content) } } diff --git a/src/commands/General/stats.ts b/src/commands/General/stats.ts index 3825ba7f..51c37922 100644 --- a/src/commands/General/stats.ts +++ b/src/commands/General/stats.ts @@ -1,32 +1,31 @@ import { Pagination, - PaginationType -} from "@discordx/pagination" -import { Category } from "@discordx/utilities" -import { ApplicationCommandOptionType, CommandInteraction, EmbedBuilder, User } from "discord.js" -import { Client } from "discordx" -import { injectable } from "tsyringe" + PaginationType, +} from '@discordx/pagination' +import { Category } from '@discordx/utilities' -import { Discord, Slash, SlashOption } from "@decorators" -import { Stats } from "@services" -import { getColor } from "@utils/functions" +import { ApplicationCommandOptionType, CommandInteraction, EmbedBuilder, User } from 'discord.js' +import { Client } from 'discordx' +import { injectable } from 'tsyringe' + +import { Discord, Slash, SlashOption } from '@/decorators' +import { Stats } from '@/services' +import { getColor } from '@/utils/functions' const statsResolver: StatsResolverType = [ { name: 'COMMANDS', data: async (stats: Stats, days: number) => { - const simpleCommandMessages = await stats.countStatsPerDays('SIMPLE_COMMAND_MESSAGE', days) const commandInteractions = await stats.countStatsPerDays('CHAT_INPUT_COMMAND_INTERACTION', days) const userContextMenus = await stats.countStatsPerDays('USER_CONTEXT_MENU_COMMAND_INTERACTION', days) const messageContextMenus = await stats.countStatsPerDays('MESSAGE_CONTEXT_MENU_COMMAND_INTERACTION', days) - - return stats.sumStats( + return stats.sumStats( stats.sumStats(simpleCommandMessages, commandInteractions), stats.sumStats(userContextMenus, messageContextMenus) ) - } + }, }, { name: 'GUILDS', @@ -52,85 +51,83 @@ export default class StatsCommand { ) {} @Slash({ - name: 'stats' + name: 'stats', }) async statsHandler( @SlashOption({ name: 'days', type: ApplicationCommandOptionType.Number, required: true }) days: number, - interaction: CommandInteraction, - client: Client, - { localize }: InteractionData + interaction: CommandInteraction, + client: Client, + { localize }: InteractionData ) { - const embeds: EmbedBuilder[] = [] for (const stat of statsResolver) { - - const stats = await stat.data(this.stats, days), - link = await this.generateLink( - stats, - localize.COMMANDS.STATS.HEADERS[stat.name as keyof typeof localize['COMMANDS']['STATS']['HEADERS']]()), - embed = this.getEmbed(interaction.user, link) - + const stats = await stat.data(this.stats, days) + const link = await this.generateLink( + stats, + localize.COMMANDS.STATS.HEADERS[stat.name as keyof typeof localize['COMMANDS']['STATS']['HEADERS']]() + ) + const embed = this.getEmbed(interaction.user, link) + embeds.push(embed) } - + await new Pagination( interaction, - embeds.map((embed) => ({ - embeds: [embed] + embeds.map(embed => ({ + embeds: [embed], })), { - type: PaginationType.Button + type: PaginationType.Button, } ).send() } async generateLink(stats: StatPerInterval, name: string): Promise { - const obj = { - - type: 'line', - 'data': { - labels: stats.map(stat => stat.date.split('/').slice(0, 2).join('/')), // we remove the year from the date - datasets: [ - { - label: '', - data: stats.map(stat => stat.count), - fill: true, - backgroundColor: 'rgba(252,231,3,0.1)', - borderColor: 'rgb(252,186,3)', - borderCapStyle: 'round', - lineTension: 0.3 - } - ] - }, - options: { - title: { - display: true, - text: name, - fontColor: 'rgba(255,255,254,0.6)', - fontSize: 20, - padding: 15 - }, - legend: { display: false }, - scales: { - xAxes: [ { ticks: { fontColor: 'rgba(255,255,254,0.6)' } } ], - yAxes: [ { ticks: { fontColor: 'rgba(255,255,254,0.6)', beginAtZero: false, stepSize: 1 } } ] - } - } - } - - return `https://quickchart.io/chart?c=${JSON.stringify(obj)}&format=png`.split(' ').join('%20') + + type: 'line', + data: { + labels: stats.map(stat => stat.date.split('/').slice(0, 2).join('/')), // we remove the year from the date + datasets: [ + { + label: '', + data: stats.map(stat => stat.count), + fill: true, + backgroundColor: 'rgba(252,231,3,0.1)', + borderColor: 'rgb(252,186,3)', + borderCapStyle: 'round', + lineTension: 0.3, + }, + ], + }, + options: { + title: { + display: true, + text: name, + fontColor: 'rgba(255,255,254,0.6)', + fontSize: 20, + padding: 15, + }, + legend: { display: false }, + scales: { + xAxes: [{ ticks: { fontColor: 'rgba(255,255,254,0.6)' } }], + yAxes: [{ ticks: { fontColor: 'rgba(255,255,254,0.6)', beginAtZero: false, stepSize: 1 } }], + }, + }, + } + + return `https://quickchart.io/chart?c=${JSON.stringify(obj)}&format=png`.split(' ').join('%20') } getEmbed(author: User, link: string): EmbedBuilder { - return new EmbedBuilder() - .setAuthor({ - name: author.username, - iconURL: author.displayAvatarURL({ forceStatic: false }) + .setAuthor({ + name: author.username, + iconURL: author.displayAvatarURL({ forceStatic: false }), }) .setColor(getColor('primary')) .setImage(link) } -} \ No newline at end of file + +} diff --git a/src/commands/Owner/maintenance.ts b/src/commands/Owner/maintenance.ts index 27660fcf..1e259697 100644 --- a/src/commands/Owner/maintenance.ts +++ b/src/commands/Owner/maintenance.ts @@ -1,33 +1,33 @@ -import { ApplicationCommandOptionType, CommandInteraction } from "discord.js" -import { Client } from "discordx" +import { ApplicationCommandOptionType, CommandInteraction } from 'discord.js' +import { Client } from 'discordx' -import { Discord, Guard, Slash, SlashOption } from "@decorators" -import { Disabled } from "@guards" -import { setMaintenance, simpleSuccessEmbed } from "@utils/functions" +import { Discord, Guard, Slash, SlashOption } from '@/decorators' +import { Disabled } from '@/guards' +import { setMaintenance, simpleSuccessEmbed } from '@/utils/functions' @Discord() export default class MaintenanceCommand { - @Slash({ - name: 'maintenance' + @Slash({ + name: 'maintenance', }) @Guard( Disabled ) async maintenance( @SlashOption({ name: 'state', type: ApplicationCommandOptionType.Boolean, required: true }) state: boolean, - interaction: CommandInteraction, - client: Client, - { localize }: InteractionData + interaction: CommandInteraction, + client: Client, + { localize }: InteractionData ) { - await setMaintenance(state) simpleSuccessEmbed( - interaction, + interaction, localize.COMMANDS.MAINTENANCE.EMBED.DESCRIPTION({ - state: state ? 'on' : 'off' + state: state ? 'on' : 'off', }) ) } -} \ No newline at end of file + +} diff --git a/src/configs/api.ts b/src/configs/api.ts index 0f707487..c92179cd 100644 --- a/src/configs/api.ts +++ b/src/configs/api.ts @@ -1,5 +1,7 @@ +import process from 'node:process' + export const apiConfig: APIConfigType = { - enabled: false, // is the API server enabled or not - port: process.env['API_PORT'] ? parseInt(process.env['API_PORT']) : 4000, // the port on which the API server should be exposed -} \ No newline at end of file + enabled: false, // is the API server enabled or not + port: process.env.API_PORT ? Number.parseInt(process.env.API_PORT) : 4000, // the port on which the API server should be exposed +} diff --git a/src/configs/database.ts b/src/configs/database.ts index f21a6d6c..b2354b6a 100644 --- a/src/configs/database.ts +++ b/src/configs/database.ts @@ -1,84 +1,88 @@ -import { Options } from "@mikro-orm/core" -import { SqlHighlighter } from "@mikro-orm/sql-highlighter" +import { Options } from '@mikro-orm/core' +import { SqlHighlighter } from '@mikro-orm/sql-highlighter' -type Config = { production: Options, development?: Options } +interface Config { + production: Options + development?: Options +} export const databaseConfig: DatabaseConfigType = { - - path: './database/', // path to the folder containing the migrations and SQLite database (if used) - - // config for setting up an automated backup of the database (ONLY FOR SQLITE) - backup: { - enabled: false, - path: './database/backups/' // path to the backups folder (should be in the database/ folder) - } + + path: './database/', // path to the folder containing the migrations and SQLite database (if used) + + // config for setting up an automated backup of the database (ONLY FOR SQLITE) + backup: { + enabled: false, + path: './database/backups/', // path to the backups folder (should be in the database/ folder) + }, } const envMikroORMConfig = { - production: { - - /** - * SQLite - */ - type: 'better-sqlite', // or 'sqlite' - dbName: `${databaseConfig.path}db.sqlite`, - - /** - * MongoDB - */ - // type: 'mongo', - // clientUrl: process.env['DATABASE_HOST'], - - /** - * PostgreSQL - */ - // type: 'postgresql', - // dbName: process.env['DATABASE_NAME'], - // host: process.env['DATABASE_HOST'], - // port: Number(process.env['DATABASE_PORT']),, - // user: process.env['DATABASE_USER'], - // password: process.env['DATABASE_PASSWORD'], - - /** - * MySQL - */ - // type: 'mysql', - // dbName: process.env['DATABASE_NAME'], - // host: process.env['DATABASE_HOST'], - // port: Number(process.env['DATABASE_PORT']), - // user: process.env['DATABASE_USER'], - // password: process.env['DATABASE_PASSWORD'], - - /** - * MariaDB - */ - // type: 'mariadb', - // dbName: process.env['DATABASE_NAME'], - // host: process.env['DATABASE_HOST'], - // port: Number(process.env['DATABASE_PORT']), - // user: process.env['DATABASE_USER'], - // password: process.env['DATABASE_PASSWORD'], - - highlighter: new SqlHighlighter(), - debug: false, - - migrations: { - path: './database/migrations', - emit: 'js', - snapshot: true - } - }, - - development: { - - } + production: { + + /** + * SQLite + */ + type: 'better-sqlite', // or 'sqlite' + dbName: `${databaseConfig.path}db.sqlite`, + + /** + * MongoDB + */ + // type: 'mongo', + // clientUrl: process.env['DATABASE_HOST'], + + /** + * PostgreSQL + */ + // type: 'postgresql', + // dbName: process.env['DATABASE_NAME'], + // host: process.env['DATABASE_HOST'], + // port: Number(process.env['DATABASE_PORT']),, + // user: process.env['DATABASE_USER'], + // password: process.env['DATABASE_PASSWORD'], + + /** + * MySQL + */ + // type: 'mysql', + // dbName: process.env['DATABASE_NAME'], + // host: process.env['DATABASE_HOST'], + // port: Number(process.env['DATABASE_PORT']), + // user: process.env['DATABASE_USER'], + // password: process.env['DATABASE_PASSWORD'], + + /** + * MariaDB + */ + // type: 'mariadb', + // dbName: process.env['DATABASE_NAME'], + // host: process.env['DATABASE_HOST'], + // port: Number(process.env['DATABASE_PORT']), + // user: process.env['DATABASE_USER'], + // password: process.env['DATABASE_PASSWORD'], + + highlighter: new SqlHighlighter(), + debug: false, + + migrations: { + path: './database/migrations', + emit: 'js', + snapshot: true, + }, + }, + + development: { + + }, } satisfies Config -if (!envMikroORMConfig['development'] || Object.keys(envMikroORMConfig['development']).length === 0) envMikroORMConfig['development'] = envMikroORMConfig['production'] +if (!envMikroORMConfig.development || Object.keys(envMikroORMConfig.development).length === 0) + envMikroORMConfig.development = envMikroORMConfig.production export const mikroORMConfig = envMikroORMConfig as { - production: typeof envMikroORMConfig['production'], - development: typeof envMikroORMConfig['production'] -} \ No newline at end of file + production: typeof envMikroORMConfig['production'] + development: typeof envMikroORMConfig['production'] +} diff --git a/src/configs/general.ts b/src/configs/general.ts index 9151bf82..8ebb91e5 100644 --- a/src/configs/general.ts +++ b/src/configs/general.ts @@ -1,9 +1,11 @@ +import process from 'node:process' + export const generalConfig: GeneralConfigType = { name: 'tscord', // the name of your bot description: '', // the description of your bot defaultLocale: 'en', // default language of the bot, must be a valid locale - ownerId: process.env['BOT_OWNER_ID'] || '', + ownerId: process.env.BOT_OWNER_ID || '', timezone: 'Europe/Paris', // default TimeZone to well format and localize dates (logs, stats, etc) simpleCommandsPrefix: '!', // default prefix for simple command messages (old way to do commands on discord) @@ -15,32 +17,32 @@ export const generalConfig: GeneralConfigType = { supportServer: 'https://discord.com/your_invitation_link', gitRemoteRepo: 'https://github.com/barthofu/tscord', }, - + automaticUploadImagesToImgur: false, // enable or not the automatic assets upload devs: [], // discord IDs of the devs that are working on the bot (you don't have to put the owner's id here) eval: { name: 'bot', // name to trigger the eval command - onlyOwner: false // restrict the eval command to the owner only (if not, all the devs can trigger it) + onlyOwner: false, // restrict the eval command to the owner only (if not, all the devs can trigger it) }, // define the bot activities (phrases under its name). Types can be: PLAYING, LISTENING, WATCHING, STREAMING - activities: [ + activities: [ { text: 'discord.js v14', - type: 'PLAYING' + type: 'PLAYING', }, { text: 'some knowledge', - type: 'STREAMING' - } - ] + type: 'STREAMING', + }, + ], } // global colors export const colorsConfig = { - primary: '#2F3136' + primary: '#2F3136', } diff --git a/src/configs/index.ts b/src/configs/index.ts index 6b9099e9..7a5fc7b6 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -2,4 +2,4 @@ export * from './general' export * from './database' export * from './logs' export * from './stats' -export * from './api' \ No newline at end of file +export * from './api' diff --git a/src/configs/logs.ts b/src/configs/logs.ts index 208d96c1..2a3a5964 100644 --- a/src/configs/logs.ts +++ b/src/configs/logs.ts @@ -1,51 +1,51 @@ export const logsConfig: LogsConfigType = { - debug: false, // set the discordx client debug logs - logTailMaxSize: 50, // max size of the last logs kept in memory - - archive: { - enabled: true, // is the auto-archiving enabled or not - retention: 30, // the number of days to keep the logs - }, - - // for each type of log, you can precise : - // - if the log should be consoled - // - if the log should be saved to the log files - // - if the log should be sent to a discord channel (providing its IP) - - interaction: { - file: true, - console: true, - channel: null, - - // exclude some interactions types - exclude: [ - 'BUTTON_INTERACTION', - 'SELECT_MENU_INTERACTION' - ] - }, - - simpleCommand: { - file: true, - console: true, - channel: null - }, - - newUser: { - file: true, - console: true, - channel: null - }, - - guild: { - file: true, - console: true, - channel: null - }, - - error: { - file: true, - console: true, - channel: null - } -} \ No newline at end of file + debug: false, // set the discordx client debug logs + logTailMaxSize: 50, // max size of the last logs kept in memory + + archive: { + enabled: true, // is the auto-archiving enabled or not + retention: 30, // the number of days to keep the logs + }, + + // for each type of log, you can precise : + // - if the log should be consoled + // - if the log should be saved to the log files + // - if the log should be sent to a discord channel (providing its IP) + + interaction: { + file: true, + console: true, + channel: null, + + // exclude some interactions types + exclude: [ + 'BUTTON_INTERACTION', + 'SELECT_MENU_INTERACTION', + ], + }, + + simpleCommand: { + file: true, + console: true, + channel: null, + }, + + newUser: { + file: true, + console: true, + channel: null, + }, + + guild: { + file: true, + console: true, + channel: null, + }, + + error: { + file: true, + console: true, + channel: null, + }, +} diff --git a/src/configs/stats.ts b/src/configs/stats.ts index b0601ce0..fe52b725 100644 --- a/src/configs/stats.ts +++ b/src/configs/stats.ts @@ -1,11 +1,11 @@ export const statsConfig: StatsConfigType = { - interaction: { + interaction: { - // exclude interaction types from being recorded as stat - exclude: [ - 'BUTTON_INTERACTION', - 'SELECT_MENU_INTERACTION' - ] - } -} \ No newline at end of file + // exclude interaction types from being recorded as stat + exclude: [ + 'BUTTON_INTERACTION', + 'SELECT_MENU_INTERACTION', + ], + }, +} diff --git a/src/entities/BaseEntity.ts b/src/entities/BaseEntity.ts index 2f5d93fd..73641471 100644 --- a/src/entities/BaseEntity.ts +++ b/src/entities/BaseEntity.ts @@ -1,10 +1,11 @@ -import { Property } from "@mikro-orm/core" +import { Property } from '@mikro-orm/core' export abstract class CustomBaseEntity { - @Property() + @Property() createdAt: Date = new Date() - @Property({ onUpdate: () => new Date()}) + @Property({ onUpdate: () => new Date() }) updatedAt: Date = new Date() -} \ No newline at end of file + +} diff --git a/src/entities/Data.ts b/src/entities/Data.ts index e738a931..7a8c466a 100644 --- a/src/entities/Data.ts +++ b/src/entities/Data.ts @@ -1,16 +1,16 @@ -import { Entity, EntityRepositoryType, PrimaryKey, Property } from "@mikro-orm/core" -import { EntityRepository } from "@mikro-orm/sqlite" +import { Entity, EntityRepositoryType, PrimaryKey, Property } from '@mikro-orm/core' +import { EntityRepository } from '@mikro-orm/sqlite' -import { CustomBaseEntity } from "./BaseEntity" +import { CustomBaseEntity } from './BaseEntity' /** - * Default data for the Data table (dynamic EAV key/value pattern) + * Default data for the Data table (dynamic EAV key/value pattern) */ export const defaultData = { - maintenance: false, - lastMaintenance: Date.now(), - lastStartup: Date.now() + maintenance: false, + lastMaintenance: Date.now(), + lastStartup: Date.now(), } type DataType = keyof typeof defaultData @@ -22,13 +22,14 @@ type DataType = keyof typeof defaultData @Entity({ customRepository: () => DataRepository }) export class Data extends CustomBaseEntity { - [EntityRepositoryType]?: DataRepository + [EntityRepositoryType]?: DataRepository - @PrimaryKey() - key!: string + @PrimaryKey() + key!: string - @Property() + @Property() value: string = '' + } // =========================================== @@ -37,42 +38,37 @@ export class Data extends CustomBaseEntity { export class DataRepository extends EntityRepository { - async get(key: T): Promise { - - const data = await this.findOne({ key }) - - return JSON.parse(data!.value) - } + async get(key: T): Promise { + const data = await this.findOne({ key }) - async set(key: T, value: typeof defaultData[T]): Promise { + return JSON.parse(data!.value) + } - const data = await this.findOne({ key }) + async set(key: T, value: typeof defaultData[T]): Promise { + const data = await this.findOne({ key }) - if (!data) { + if (!data) { + const newData = new Data() + newData.key = key + newData.value = JSON.stringify(value) - const newData = new Data() - newData.key = key - newData.value = JSON.stringify(value) + await this.persistAndFlush(newData) + } else { + data.value = JSON.stringify(value) + await this.flush() + } + } - await this.persistAndFlush(newData) - } - else { - data.value = JSON.stringify(value) - await this.flush() - } - } + async add(key: T, value: typeof defaultData[T]): Promise { + const data = await this.findOne({ key }) - async add(key: T, value: typeof defaultData[T]): Promise { + if (!data) { + const newData = new Data() + newData.key = key + newData.value = JSON.stringify(value) - const data = await this.findOne({ key }) + await this.persistAndFlush(newData) + } + } - if (!data) { - - const newData = new Data() - newData.key = key - newData.value = JSON.stringify(value) - - await this.persistAndFlush(newData) - } - } -} \ No newline at end of file +} diff --git a/src/entities/Guild.ts b/src/entities/Guild.ts index 5e6de372..3188b27a 100644 --- a/src/entities/Guild.ts +++ b/src/entities/Guild.ts @@ -1,7 +1,7 @@ -import { Entity, PrimaryKey, Property, EntityRepositoryType } from "@mikro-orm/core" -import { EntityRepository } from "@mikro-orm/sqlite" +import { Entity, EntityRepositoryType, PrimaryKey, Property } from '@mikro-orm/core' +import { EntityRepository } from '@mikro-orm/sqlite' -import { CustomBaseEntity } from "./BaseEntity" +import { CustomBaseEntity } from './BaseEntity' // =========================================== // ================= Entity ================== @@ -10,38 +10,39 @@ import { CustomBaseEntity } from "./BaseEntity" @Entity({ customRepository: () => GuildRepository }) export class Guild extends CustomBaseEntity { - [EntityRepositoryType]?: GuildRepository + [EntityRepositoryType]?: GuildRepository - @PrimaryKey({ autoincrement: false }) + @PrimaryKey({ autoincrement: false }) id!: string - @Property({ nullable: true, type: 'string' }) + @Property({ nullable: true, type: 'string' }) prefix: string | null - @Property() + @Property() deleted: boolean = false - @Property() + @Property() lastInteract: Date = new Date() + } // =========================================== // =========== Custom Repository ============= // =========================================== -export class GuildRepository extends EntityRepository { +export class GuildRepository extends EntityRepository { + + async updateLastInteract(guildId?: string): Promise { + const guild = await this.findOne({ id: guildId }) - async updateLastInteract(guildId?: string): Promise { + if (guild) { + guild.lastInteract = new Date() + await this.flush() + } + } - const guild = await this.findOne({ id: guildId }) - - if (guild) { - guild.lastInteract = new Date() - await this.flush() - } - } + async getActiveGuilds() { + return this.find({ deleted: false }) + } - async getActiveGuilds() { - return this.find({ deleted: false }) - } -} \ No newline at end of file +} diff --git a/src/entities/Image.ts b/src/entities/Image.ts index 876f7ff8..77bbdb8d 100644 --- a/src/entities/Image.ts +++ b/src/entities/Image.ts @@ -1,7 +1,7 @@ -import { Entity, EntityRepositoryType, PrimaryKey, Property } from "@mikro-orm/core" -import { EntityRepository } from "@mikro-orm/sqlite" +import { Entity, EntityRepositoryType, PrimaryKey, Property } from '@mikro-orm/core' +import { EntityRepository } from '@mikro-orm/sqlite' -import { CustomBaseEntity } from "./BaseEntity" +import { CustomBaseEntity } from './BaseEntity' // =========================================== // ================= Entity ================== @@ -10,30 +10,30 @@ import { CustomBaseEntity } from "./BaseEntity" @Entity({ customRepository: () => ImageRepository }) export class Image extends CustomBaseEntity { - [EntityRepositoryType]?: ImageRepository + [EntityRepositoryType]?: ImageRepository - @PrimaryKey() + @PrimaryKey() id: number - @Property() + @Property() fileName: string - @Property({ default: '' }) + @Property({ default: '' }) basePath?: string - @Property() + @Property() url: string - @Property() + @Property() size: number - @Property() + @Property() tags: string[] - @Property() + @Property() hash: string - @Property() + @Property() deleteHash: string } @@ -42,14 +42,14 @@ export class Image extends CustomBaseEntity { // =========== Custom Repository ============= // =========================================== -export class ImageRepository extends EntityRepository { +export class ImageRepository extends EntityRepository { - async findByTags(tags: string[], explicit: boolean = true): Promise { - - const rows = await this.find({ - $and: tags.map(tag => ({ tags: new RegExp(tag) })) - }) + async findByTags(tags: string[], explicit: boolean = true): Promise { + const rows = await this.find({ + $and: tags.map(tag => ({ tags: new RegExp(tag) })), + }) - return explicit ? rows.filter(row => row.tags.length === tags.length) : rows - } -} \ No newline at end of file + return explicit ? rows.filter(row => row.tags.length === tags.length) : rows + } + +} diff --git a/src/entities/Pastebin.ts b/src/entities/Pastebin.ts index 2556a09e..9aa0b878 100644 --- a/src/entities/Pastebin.ts +++ b/src/entities/Pastebin.ts @@ -1,5 +1,5 @@ -import { Entity, PrimaryKey, Property, EntityRepositoryType } from "@mikro-orm/core" -import { EntityRepository } from "@mikro-orm/sqlite" +import { Entity, EntityRepositoryType, PrimaryKey, Property } from '@mikro-orm/core' +import { EntityRepository } from '@mikro-orm/sqlite' // =========================================== // ================= Entity ================== @@ -8,25 +8,26 @@ import { EntityRepository } from "@mikro-orm/sqlite" @Entity({ customRepository: () => PastebinRepository }) export class Pastebin { - [EntityRepositoryType]?: PastebinRepository + [EntityRepositoryType]?: PastebinRepository - @PrimaryKey({ autoincrement: false }) + @PrimaryKey({ autoincrement: false }) id: string - @Property() + @Property() editCode: string - @Property() + @Property() lifetime: number = -1 - @Property() + @Property() createdAt: Date = new Date() + } // =========================================== // =========== Custom Repository ============= // =========================================== -export class PastebinRepository extends EntityRepository { +export class PastebinRepository extends EntityRepository { -} \ No newline at end of file +} diff --git a/src/entities/Stat.ts b/src/entities/Stat.ts index 2b721029..5006502b 100644 --- a/src/entities/Stat.ts +++ b/src/entities/Stat.ts @@ -1,5 +1,5 @@ -import { Entity, EntityRepositoryType, PrimaryKey, Property } from "@mikro-orm/core" -import { EntityRepository } from "@mikro-orm/sqlite" +import { Entity, EntityRepositoryType, PrimaryKey, Property } from '@mikro-orm/core' +import { EntityRepository } from '@mikro-orm/sqlite' // =========================================== // ================= Entity ================== @@ -8,28 +8,29 @@ import { EntityRepository } from "@mikro-orm/sqlite" @Entity({ customRepository: () => StatRepository }) export class Stat { - [EntityRepositoryType]?: StatRepository + [EntityRepositoryType]?: StatRepository - @PrimaryKey() + @PrimaryKey() id: number - @Property() + @Property() type!: string - @Property() + @Property() value: string = '' - @Property({ type: 'json', nullable: true }) + @Property({ type: 'json', nullable: true }) additionalData?: any - @Property() + @Property() createdAt: Date = new Date() + } // =========================================== // =========== Custom Repository ============= // =========================================== -export class StatRepository extends EntityRepository { +export class StatRepository extends EntityRepository { -} \ No newline at end of file +} diff --git a/src/entities/User.ts b/src/entities/User.ts index e6e5ffcc..5e3284e7 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -1,7 +1,7 @@ -import { Entity, EntityRepositoryType, PrimaryKey, Property } from "@mikro-orm/core" -import { EntityRepository } from "@mikro-orm/sqlite" +import { Entity, EntityRepositoryType, PrimaryKey, Property } from '@mikro-orm/core' +import { EntityRepository } from '@mikro-orm/sqlite' -import { CustomBaseEntity } from "./BaseEntity" +import { CustomBaseEntity } from './BaseEntity' // =========================================== // ================= Entity ================== @@ -10,28 +10,29 @@ import { CustomBaseEntity } from "./BaseEntity" @Entity({ customRepository: () => UserRepository }) export class User extends CustomBaseEntity { - [EntityRepositoryType]?: UserRepository + [EntityRepositoryType]?: UserRepository - @PrimaryKey({ autoincrement: false }) + @PrimaryKey({ autoincrement: false }) id!: string - @Property() + @Property() lastInteract: Date = new Date() + } // =========================================== // =========== Custom Repository ============= // =========================================== -export class UserRepository extends EntityRepository { +export class UserRepository extends EntityRepository { - async updateLastInteract(userId?: string): Promise { + async updateLastInteract(userId?: string): Promise { + const user = await this.findOne({ id: userId }) - const user = await this.findOne({ id: userId }) + if (user) { + user.lastInteract = new Date() + await this.flush() + } + } - if (user) { - user.lastInteract = new Date() - await this.flush() - } - } -} \ No newline at end of file +} diff --git a/src/entities/index.ts b/src/entities/index.ts index ae0eb3ae..910bc8e7 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -5,4 +5,4 @@ export * from './Guild' export { Data } from './Data' export * from './Stat' export * from './Image' -export * from './Pastebin' \ No newline at end of file +export * from './Pastebin' diff --git a/src/events/custom/simpleCommandCreate.ts b/src/events/custom/simpleCommandCreate.ts index f1d08186..dff53efd 100644 --- a/src/events/custom/simpleCommandCreate.ts +++ b/src/events/custom/simpleCommandCreate.ts @@ -1,63 +1,61 @@ -import { ArgsOf, Client, Guard, SimpleCommandMessage } from "discordx" -import { inject, injectable, delay, container } from "tsyringe" +import { ArgsOf, Client, Guard, SimpleCommandMessage } from 'discordx' +import { injectable } from 'tsyringe' -import { Discord, On, OnCustom } from "@decorators" -import { Guild, User } from "@entities" -import { Maintenance } from "@guards" -import { Database, EventManager, Logger, Stats } from "@services" -import { getPrefixFromMessage, resolveDependency, syncUser } from "@utils/functions" +import { Discord, On, OnCustom } from '@/decorators' +import { Guild, User } from '@/entities' +import { Maintenance } from '@/guards' +import { Database, EventManager, Logger, Stats } from '@/services' +import { getPrefixFromMessage, syncUser } from '@/utils/functions' @Discord() @injectable() export default class SimpleCommandCreateEvent { - constructor( - private stats: Stats, - private logger: Logger, - private db: Database, - private eventManager: EventManager - ) {} - - // ============================= - // ========= Handler =========== - // ============================= - - @OnCustom('simpleCommandCreate') - async simpleCommandCreateHandler(command: SimpleCommandMessage) { - - // insert user in db if not exists - await syncUser(command.message.author) - - // update last interaction time of both user and guild - await this.db.get(User).updateLastInteract(command.message.author.id) - await this.db.get(Guild).updateLastInteract(command.message.guild?.id) - - await this.stats.registerSimpleCommand(command) - this.logger.logInteraction(command) - } - - // ============================= - // ========== Emitter ========== - // ============================= - - @On('messageCreate') - @Guard( - Maintenance - ) - async simpleCommandCreateEmitter( - [message]: ArgsOf<'messageCreate'>, - client: Client - ) { - - const prefix = await getPrefixFromMessage(message) - const command = await client.parseCommand(prefix, message, false) - - if (command && command instanceof SimpleCommandMessage) { - - /** - * @param {SimpleCommandMessage} command - */ - this.eventManager.emit('simpleCommandCreate', command) - } - } -} \ No newline at end of file + constructor( + private stats: Stats, + private logger: Logger, + private db: Database, + private eventManager: EventManager + ) {} + + // ============================= + // ========= Handler =========== + // ============================= + + @OnCustom('simpleCommandCreate') + async simpleCommandCreateHandler(command: SimpleCommandMessage) { + // insert user in db if not exists + await syncUser(command.message.author) + + // update last interaction time of both user and guild + await this.db.get(User).updateLastInteract(command.message.author.id) + await this.db.get(Guild).updateLastInteract(command.message.guild?.id) + + await this.stats.registerSimpleCommand(command) + this.logger.logInteraction(command) + } + + // ============================= + // ========== Emitter ========== + // ============================= + + @On('messageCreate') + @Guard( + Maintenance + ) + async simpleCommandCreateEmitter( + [message]: ArgsOf<'messageCreate'>, + client: Client + ) { + const prefix = await getPrefixFromMessage(message) + const command = await client.parseCommand(prefix, message, false) + + if (command && command instanceof SimpleCommandMessage) { + /** + * @param {SimpleCommandMessage} command + */ + this.eventManager.emit('simpleCommandCreate', command) + } + } + +} diff --git a/src/events/custom/templateReady.ts b/src/events/custom/templateReady.ts index c8534c83..75483224 100644 --- a/src/events/custom/templateReady.ts +++ b/src/events/custom/templateReady.ts @@ -1,18 +1,16 @@ -import { Client } from 'discordx' - -import { Discord, OnCustom } from '@decorators' +import { Discord, OnCustom } from '@/decorators' @Discord() export default class TemplateReadyEvent { - // ============================= - // ========= Handlers ========== - // ============================= + // ============================= + // ========= Handlers ========== + // ============================= - @OnCustom('templateReady') - async templateReadyHandler() { + @OnCustom('templateReady') + async templateReadyHandler() { - // console.log('the template is fully ready!') - } + // console.log('the template is fully ready!') + } -} \ No newline at end of file +} diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index d5f22295..38de543c 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -1,17 +1,17 @@ -import { ArgsOf, Client } from "discordx" +import { ArgsOf, Client } from 'discordx' -import { Discord, On } from "@decorators" -import { syncGuild } from "@utils/functions" +import { Discord, On } from '@/decorators' +import { syncGuild } from '@/utils/functions' @Discord() export default class GuildCreateEvent { - @On('guildCreate') - async guildCreateHandler( - [newGuild]: ArgsOf<'guildCreate'>, - client: Client - ) { + @On('guildCreate') + async guildCreateHandler( + [newGuild]: ArgsOf<'guildCreate'>, + client: Client + ) { + await syncGuild(newGuild.id, client) + } - await syncGuild(newGuild.id, client) - } -} \ No newline at end of file +} diff --git a/src/events/guildDelete.ts b/src/events/guildDelete.ts index 339578c4..a0083045 100644 --- a/src/events/guildDelete.ts +++ b/src/events/guildDelete.ts @@ -1,17 +1,17 @@ -import { ArgsOf, Client } from "discordx" +import { ArgsOf, Client } from 'discordx' -import { Discord, On } from "@decorators" -import { syncGuild } from "@utils/functions" +import { Discord, On } from '@/decorators' +import { syncGuild } from '@/utils/functions' @Discord() export default class GuildDeleteEvent { - @On('guildDelete') - async guildDeleteHandler( - [oldGuild]: ArgsOf<'guildDelete'>, - client: Client - ) { + @On('guildDelete') + async guildDeleteHandler( + [oldGuild]: ArgsOf<'guildDelete'>, + client: Client + ) { + await syncGuild(oldGuild.id, client) + } - await syncGuild(oldGuild.id, client) - } -} \ No newline at end of file +} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 896b3132..5e71b26e 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,50 +1,50 @@ -import { CommandInteraction } from "discord.js" -import { ArgsOf, Client } from "discordx" -import { injectable } from "tsyringe" +import { CommandInteraction } from 'discord.js' +import { ArgsOf, Client } from 'discordx' +import { injectable } from 'tsyringe' -import { Discord, Guard, On } from "@decorators" -import { Guild, User } from "@entities" -import { Maintenance } from "@guards" -import { Database, Logger, Stats } from "@services" -import { syncUser } from "@utils/functions" -import { generalConfig } from "@configs" +import { generalConfig } from '@/configs' +import { Discord, Guard, On } from '@/decorators' +import { Guild, User } from '@/entities' +import { Maintenance } from '@/guards' +import { Database, Logger, Stats } from '@/services' +import { syncUser } from '@/utils/functions' @Discord() @injectable() export default class InteractionCreateEvent { - constructor( - private stats: Stats, - private logger: Logger, - private db: Database - ) {} - - @On('interactionCreate') - @Guard( - Maintenance - ) - async interactionCreateHandler( - [interaction]: ArgsOf<'interactionCreate'>, - client: Client - ) { - - // defer the reply - if ( - generalConfig.automaticDeferring && - interaction instanceof CommandInteraction - ) await interaction.deferReply() - - // insert user in db if not exists - await syncUser(interaction.user) - - // update last interaction time of both user and guild - await this.db.get(User).updateLastInteract(interaction.user.id) - await this.db.get(Guild).updateLastInteract(interaction.guild?.id) - - // register logs and stats - await this.stats.registerInteraction(interaction as AllInteractions) - this.logger.logInteraction(interaction as AllInteractions) - - client.executeInteraction(interaction) - } -} \ No newline at end of file + constructor( + private stats: Stats, + private logger: Logger, + private db: Database + ) {} + + @On('interactionCreate') + @Guard( + Maintenance + ) + async interactionCreateHandler( + [interaction]: ArgsOf<'interactionCreate'>, + client: Client + ) { + // defer the reply + if ( + generalConfig.automaticDeferring + && interaction instanceof CommandInteraction + ) await interaction.deferReply() + + // insert user in db if not exists + await syncUser(interaction.user) + + // update last interaction time of both user and guild + await this.db.get(User).updateLastInteract(interaction.user.id) + await this.db.get(Guild).updateLastInteract(interaction.guild?.id) + + // register logs and stats + await this.stats.registerInteraction(interaction as AllInteractions) + this.logger.logInteraction(interaction as AllInteractions) + + client.executeInteraction(interaction) + } + +} diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index a784c4a0..3eb9659c 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -1,35 +1,32 @@ -import { ArgsOf, Client } from "discordx" +import { ArgsOf, Client } from 'discordx' -import { Discord, Guard, On } from "@decorators" -import { Maintenance } from "@guards" -import { executeEvalFromMessage, isDev } from "@utils/functions" - -import { generalConfig } from "@configs" +import { generalConfig } from '@/configs' +import { Discord, Guard, On } from '@/decorators' +import { Maintenance } from '@/guards' +import { executeEvalFromMessage, isDev } from '@/utils/functions' @Discord() export default class MessageCreateEvent { - @On("messageCreate") - @Guard( - Maintenance - ) - async messageCreateHandler( - [message]: ArgsOf<"messageCreate">, - client: Client - ) { - - // eval command - if ( - message.content.startsWith(`\`\`\`${generalConfig.eval.name}`) - && ( - (!generalConfig.eval.onlyOwner && isDev(message.author.id)) - || (generalConfig.eval.onlyOwner && message.author.id === generalConfig.ownerId) - ) - ) { - executeEvalFromMessage(message) - } + @On('messageCreate') + @Guard( + Maintenance + ) + async messageCreateHandler( + [message]: ArgsOf<'messageCreate'>, + client: Client + ) { + // eval command + if ( + message.content.startsWith(`\`\`\`${generalConfig.eval.name}`) + && ( + (!generalConfig.eval.onlyOwner && isDev(message.author.id)) + || (generalConfig.eval.onlyOwner && message.author.id === generalConfig.ownerId) + ) + ) + executeEvalFromMessage(message) - await client.executeCommand(message, false) - } + await client.executeCommand(message, false) + } -} \ No newline at end of file +} diff --git a/src/events/messagePinned.ts b/src/events/messagePinned.ts index 6fd12598..f616eb86 100644 --- a/src/events/messagePinned.ts +++ b/src/events/messagePinned.ts @@ -1,16 +1,15 @@ -import { Message } from "discord.js" -import { Client } from "discordx" +import { Message } from 'discord.js' -import { Discord, On } from "@decorators" +import { Discord, On } from '@/decorators' @Discord() export default class messagePinnedEvent { - @On('messagePinned') - async messagePinnedHandler( - [message]: [Message], - client: Client - ) { - console.log(`This message from ${message.author.tag} has been pinned : ${message.content}`) - } -} \ No newline at end of file + @On('messagePinned') + async messagePinnedHandler( + [message]: [Message] + ) { + console.log(`This message from ${message.author.tag} has been pinned : ${message.content}`) + } + +} diff --git a/src/events/ready.ts b/src/events/ready.ts index 4272038b..e20ce1ca 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,86 +1,83 @@ -import { ActivityType } from "discord.js" -import { Client } from "discordx" -import { injectable } from "tsyringe" +import { ActivityType } from 'discord.js' +import { Client } from 'discordx' +import { injectable } from 'tsyringe' -import { generalConfig, logsConfig } from "@configs" -import { Discord, Once, Schedule } from "@decorators" -import { Data } from "@entities" -import { Database, Logger, Scheduler, Store } from "@services" -import { resolveDependency, syncAllGuilds } from "@utils/functions" +import { generalConfig } from '@/configs' +import { Discord, Once, Schedule } from '@/decorators' +import { Data } from '@/entities' +import { Database, Logger, Scheduler, Store } from '@/services' +import { resolveDependency, syncAllGuilds } from '@/utils/functions' @Discord() @injectable() export default class ReadyEvent { - constructor( - private db: Database, - private logger: Logger, - private scheduler: Scheduler, - private store: Store - ) {} - - private activityIndex = 0 - - @Once('ready') - async readyHandler([client]: [Client]) { - - // make sure all guilds are cached - await client.guilds.fetch() - - // synchronize applications commands with Discord - await client.initApplicationCommands({ - global: { - disable: { - delete: false - } - } - }) - - // change activity - await this.changeActivity() - - // update last startup time in the database - await this.db.get(Data).set('lastStartup', Date.now()) - - // start scheduled jobs - this.scheduler.startAllJobs() - - // log startup - await this.logger.logStartingConsole() - - // synchronize guilds between discord and the database - await syncAllGuilds(client) - - // the bot is fully ready - this.store.update('ready', (e) => ({ ...e, bot: true })) - } - - @Schedule('*/15 * * * * *') // each 15 seconds - async changeActivity() { - - const ActivityTypeEnumString = ["PLAYING", "STREAMING", "LISTENING", "WATCHING", "CUSTOM", "COMPETING"] // DO NOT CHANGE THE ORDER - - const client = await resolveDependency(Client) - const activity = generalConfig.activities[this.activityIndex] - - activity.text = eval(`new String(\`${activity.text}\`).toString()`) - - if (activity.type === 'STREAMING') { //streaming activity - - client.user?.setStatus('online') - client.user?.setActivity(activity.text, { - 'url': 'https://www.twitch.tv/discord', - 'type': ActivityType.Streaming - }) - - } else { //other activities - - client.user?.setActivity(activity.text, { - type: ActivityTypeEnumString.indexOf(activity.type) - }) - } - - this.activityIndex++ - if (this.activityIndex === generalConfig.activities.length) this.activityIndex = 0 - } -} \ No newline at end of file + constructor( + private db: Database, + private logger: Logger, + private scheduler: Scheduler, + private store: Store + ) {} + + private activityIndex = 0 + + @Once('ready') + async readyHandler([client]: [Client]) { + // make sure all guilds are cached + await client.guilds.fetch() + + // synchronize applications commands with Discord + await client.initApplicationCommands({ + global: { + disable: { + delete: false, + }, + }, + }) + + // change activity + await this.changeActivity() + + // update last startup time in the database + await this.db.get(Data).set('lastStartup', Date.now()) + + // start scheduled jobs + this.scheduler.startAllJobs() + + // log startup + await this.logger.logStartingConsole() + + // synchronize guilds between discord and the database + await syncAllGuilds(client) + + // the bot is fully ready + this.store.update('ready', e => ({ ...e, bot: true })) + } + + @Schedule('*/15 * * * * *') // each 15 seconds + async changeActivity() { + const ActivityTypeEnumString = ['PLAYING', 'STREAMING', 'LISTENING', 'WATCHING', 'CUSTOM', 'COMPETING'] // DO NOT CHANGE THE ORDER + + const client = await resolveDependency(Client) + const activity = generalConfig.activities[this.activityIndex] + + activity.text = eval(`new String(\`${activity.text}\`).toString()`) + + if (activity.type === 'STREAMING') { // streaming activity + client.user?.setStatus('online') + client.user?.setActivity(activity.text, { + url: 'https://www.twitch.tv/discord', + type: ActivityType.Streaming, + }) + } else { // other activities + client.user?.setActivity(activity.text, { + type: ActivityTypeEnumString.indexOf(activity.type), + }) + } + + this.activityIndex++ + if (this.activityIndex === generalConfig.activities.length) + this.activityIndex = 0 + } + +} diff --git a/src/guards/disabled.ts b/src/guards/disabled.ts index 8792f0d7..c16e0b33 100644 --- a/src/guards/disabled.ts +++ b/src/guards/disabled.ts @@ -1,30 +1,27 @@ -import { CommandInteraction, ContextMenuCommandInteraction } from "discord.js" -import { GuardFunction, SimpleCommandMessage } from "discordx" +import { CommandInteraction, ContextMenuCommandInteraction } from 'discord.js' +import { GuardFunction, SimpleCommandMessage } from 'discordx' -import { getLocaleFromInteraction, L } from "@i18n" -import { isDev, replyToInteraction, resolveUser } from "@utils/functions" +import { getLocaleFromInteraction, L } from '@/i18n' +import { isDev, replyToInteraction, resolveUser } from '@/utils/functions' /** * Prevent interaction from running when it is disabled */ export const Disabled: GuardFunction< - | CommandInteraction - | SimpleCommandMessage - | ContextMenuCommandInteraction + | CommandInteraction + | SimpleCommandMessage + | ContextMenuCommandInteraction > = async (arg, client, next) => { + const user = resolveUser(arg) - const user = resolveUser(arg) + if (user?.id && isDev(user.id)) { + return next() + } else { + if (arg instanceof CommandInteraction || arg instanceof SimpleCommandMessage) { + const locale = getLocaleFromInteraction(arg) + const localizedReplyMessage = L[locale].GUARDS.DISABLED_COMMAND() - if (user?.id && isDev(user.id)) { - return next() - } - else { - if (arg instanceof CommandInteraction || arg instanceof SimpleCommandMessage) { - - const locale = getLocaleFromInteraction(arg), - localizedReplyMessage = L[locale].GUARDS.DISABLED_COMMAND() - - await replyToInteraction(arg, localizedReplyMessage) - } - } -} \ No newline at end of file + await replyToInteraction(arg, localizedReplyMessage) + } + } +} diff --git a/src/guards/extractLocale.ts b/src/guards/extractLocale.ts index ac217fde..aaaf909d 100644 --- a/src/guards/extractLocale.ts +++ b/src/guards/extractLocale.ts @@ -1,25 +1,24 @@ -import { ButtonInteraction, CommandInteraction, ContextMenuCommandInteraction, Interaction, StringSelectMenuInteraction } from "discord.js" -import { GuardFunction, SimpleCommandMessage } from "discordx" +import { ButtonInteraction, CommandInteraction, ContextMenuCommandInteraction, Interaction, StringSelectMenuInteraction } from 'discord.js' +import { GuardFunction, SimpleCommandMessage } from 'discordx' -import { getLocaleFromInteraction, L } from "@i18n" +import { getLocaleFromInteraction, L } from '@/i18n' /** * Extract locale from any interaction and pass it as guard data */ export const ExtractLocale: GuardFunction = async (interaction, client, next, guardData) => { - if ( - interaction instanceof SimpleCommandMessage - || interaction instanceof CommandInteraction - || interaction instanceof ContextMenuCommandInteraction - || interaction instanceof StringSelectMenuInteraction - || interaction instanceof ButtonInteraction - ) { + if ( + interaction instanceof SimpleCommandMessage + || interaction instanceof CommandInteraction + || interaction instanceof ContextMenuCommandInteraction + || interaction instanceof StringSelectMenuInteraction + || interaction instanceof ButtonInteraction + ) { + const sanitizedLocale = getLocaleFromInteraction(interaction as AllInteractions) - const sanitizedLocale = getLocaleFromInteraction(interaction as AllInteractions) + guardData.sanitizedLocale = sanitizedLocale + guardData.localize = L[sanitizedLocale] + } - guardData.sanitizedLocale = sanitizedLocale - guardData.localize = L[sanitizedLocale] - } - - await next(guardData) -} \ No newline at end of file + await next(guardData) +} diff --git a/src/guards/guildOnly.ts b/src/guards/guildOnly.ts index e7b21880..8970df8c 100644 --- a/src/guards/guildOnly.ts +++ b/src/guards/guildOnly.ts @@ -1,21 +1,20 @@ -import { CommandInteraction } from "discord.js" -import { GuardFunction, SimpleCommandMessage } from "discordx" +import { CommandInteraction } from 'discord.js' +import { GuardFunction, SimpleCommandMessage } from 'discordx' -import { getLocaleFromInteraction, L } from "@i18n" -import { replyToInteraction } from "@utils/functions" +import { getLocaleFromInteraction, L } from '@/i18n' +import { replyToInteraction } from '@/utils/functions' /** * Prevent the command from running on DM */ export const GuildOnly: GuardFunction< - | CommandInteraction - | SimpleCommandMessage + | CommandInteraction + | SimpleCommandMessage > = async (arg, client, next) => { + const isInGuild = arg instanceof CommandInteraction ? arg.inGuild() : arg.message.guild - const isInGuild = arg instanceof CommandInteraction ? arg.inGuild() : arg.message.guild - - if (isInGuild) return next() - else { - await replyToInteraction(arg, L[getLocaleFromInteraction(arg)].GUARDS.GUILD_ONLY()) - } + if (isInGuild) + return next() + else + await replyToInteraction(arg, L[getLocaleFromInteraction(arg)].GUARDS.GUILD_ONLY()) } diff --git a/src/guards/index.ts b/src/guards/index.ts index 7ed9c49e..9c407a6c 100644 --- a/src/guards/index.ts +++ b/src/guards/index.ts @@ -8,4 +8,4 @@ export * from './notBot' export * from './nsfw' export * from './match' export * from './extractLocale' -export * from './requestContextIsolator' \ No newline at end of file +export * from './requestContextIsolator' diff --git a/src/guards/maintenance.ts b/src/guards/maintenance.ts index 91d04d80..9733de71 100644 --- a/src/guards/maintenance.ts +++ b/src/guards/maintenance.ts @@ -1,8 +1,8 @@ -import { CommandInteraction, ContextMenuCommandInteraction } from "discord.js" -import { ArgsOf, GuardFunction, SimpleCommandMessage } from "discordx" +import { CommandInteraction, ContextMenuCommandInteraction } from 'discord.js' +import { ArgsOf, GuardFunction, SimpleCommandMessage } from 'discordx' -import { getLocaleFromInteraction, L } from "@i18n" -import { isDev, isInMaintenance, replyToInteraction, resolveUser } from "@utils/functions" +import { getLocaleFromInteraction, L } from '@/i18n' +import { isDev, isInMaintenance, replyToInteraction, resolveUser } from '@/utils/functions' /** * Prevent interactions from running when bot is in maintenance @@ -10,28 +10,28 @@ import { isDev, isInMaintenance, replyToInteraction, resolveUser } from "@utils/ export const Maintenance: GuardFunction< | ArgsOf<'messageCreate' | 'interactionCreate'> > = async (arg, client, next) => { + if ( + arg instanceof CommandInteraction + || arg instanceof SimpleCommandMessage + || arg instanceof ContextMenuCommandInteraction + ) { + const user = resolveUser(arg) + const maintenance = await isInMaintenance() - if ( - arg instanceof CommandInteraction || - arg instanceof SimpleCommandMessage || - arg instanceof ContextMenuCommandInteraction - ) { + if ( + maintenance + && user?.id + && !isDev(user.id) + ) { + const locale = getLocaleFromInteraction(arg) + const localizedReplyMessage = L[locale].GUARDS.MAINTENANCE() - const user = resolveUser(arg), - maintenance = await isInMaintenance() - - if ( - maintenance && - user?.id && - !isDev(user.id) - ) { - - const locale = getLocaleFromInteraction(arg), - localizedReplyMessage = L[locale].GUARDS.MAINTENANCE() - - if (arg instanceof CommandInteraction || arg instanceof SimpleCommandMessage) await replyToInteraction(arg, localizedReplyMessage) - } - else return next() - } - else return next() -} \ No newline at end of file + if (arg instanceof CommandInteraction || arg instanceof SimpleCommandMessage) + await replyToInteraction(arg, localizedReplyMessage) + } else { + return next() + } + } else { + return next() + } +} diff --git a/src/guards/match.ts b/src/guards/match.ts index 2ba73e08..e66bffae 100644 --- a/src/guards/match.ts +++ b/src/guards/match.ts @@ -1,17 +1,16 @@ -import type { ArgsOf, GuardFunction } from "discordx" - +import { ArgsOf, GuardFunction } from 'discordx' + /** * Pass only when the message match with a passed regular expression * @param regex The regex to test */ -export const Match = (regex: RegExp) => { - - const guard: GuardFunction< - | ArgsOf<"messageCreate"> - > = async ([message], client, next) => { - - if (message.content.match(regex)) next() - } +export function Match(regex: RegExp) { + const guard: GuardFunction< + | ArgsOf<'messageCreate'> + > = async ([message], client, next) => { + if (message.content.match(regex)) + next() + } - return guard -} \ No newline at end of file + return guard +} diff --git a/src/guards/notBot.ts b/src/guards/notBot.ts index ded316cb..07518f12 100644 --- a/src/guards/notBot.ts +++ b/src/guards/notBot.ts @@ -1,17 +1,17 @@ -import type { ArgsOf, GuardFunction } from "discordx" +import { ArgsOf, GuardFunction } from 'discordx' + +import { resolveUser } from '@/utils/functions' -import { resolveUser } from "@utils/functions" - /** * Prevent other bots to interact with this bot */ export const NotBot: GuardFunction< - | EmittedInteractions - | ArgsOf<"messageCreate" | "messageReactionAdd" | "voiceStateUpdate"> + | EmittedInteractions + | ArgsOf<'messageCreate' | 'messageReactionAdd' | 'voiceStateUpdate'> > = async (arg, client, next) => { + const parsedArg = Array.isArray(arg) ? arg[0] : arg + const user = resolveUser(parsedArg) - const parsedArg = arg instanceof Array ? arg[0] : arg, - user = resolveUser(parsedArg) - - if (!user?.bot) await next() -} \ No newline at end of file + if (!user?.bot) + await next() +} diff --git a/src/guards/nsfw.ts b/src/guards/nsfw.ts index 2c966f43..2d9e79fb 100644 --- a/src/guards/nsfw.ts +++ b/src/guards/nsfw.ts @@ -1,24 +1,24 @@ -import { CommandInteraction, TextChannel } from "discord.js" -import { GuardFunction, SimpleCommandMessage } from "discordx" +import { CommandInteraction, TextChannel } from 'discord.js' +import { GuardFunction, SimpleCommandMessage } from 'discordx' -import { getLocaleFromInteraction, L } from "@i18n" -import { replyToInteraction, resolveChannel } from "@utils/functions" +import { getLocaleFromInteraction, L } from '@/i18n' +import { replyToInteraction, resolveChannel } from '@/utils/functions' /** * Prevent NSFW command from running in non-NSFW channels */ export const NSFW: GuardFunction< - | CommandInteraction - | SimpleCommandMessage -> = async(arg, client, next) => { - - const channel = resolveChannel(arg) + | CommandInteraction + | SimpleCommandMessage +> = async (arg, client, next) => { + const channel = resolveChannel(arg) - if (!(channel instanceof TextChannel && !channel?.nsfw)) await next() - else { - const locale = getLocaleFromInteraction(arg), - localizedReplyMessage = L[locale].GUARDS.NSFW() + if (!(channel instanceof TextChannel && !channel?.nsfw)) { + await next() + } else { + const locale = getLocaleFromInteraction(arg) + const localizedReplyMessage = L[locale].GUARDS.NSFW() - await replyToInteraction(arg, localizedReplyMessage) - } -} \ No newline at end of file + await replyToInteraction(arg, localizedReplyMessage) + } +} diff --git a/src/guards/requestContextIsolator.ts b/src/guards/requestContextIsolator.ts index 684dea37..77c25c04 100644 --- a/src/guards/requestContextIsolator.ts +++ b/src/guards/requestContextIsolator.ts @@ -1,13 +1,14 @@ import { RequestContext } from '@mikro-orm/core' -import { Database } from '@services' -import { resolveDependency } from '@utils/functions' -import type { ArgsOf, GuardFunction } from 'discordx' - + +import { GuardFunction } from 'discordx' + +import { Database } from '@/services' +import { resolveDependency } from '@/utils/functions' + /** * Isolate all the handling pipeline to prevent any MikrORM global identity map issues */ -export const RequestContextIsolator: GuardFunction = async (_, client, next) => { - - const db = await resolveDependency(Database) - RequestContext.create(db.orm.em, next) -} \ No newline at end of file +export const RequestContextIsolator: GuardFunction = async (_, client, next) => { + const db = await resolveDependency(Database) + RequestContext.create(db.orm.em, next) +} diff --git a/src/i18n/detectors.ts b/src/i18n/detectors.ts index 202c2323..d2a98bcb 100644 --- a/src/i18n/detectors.ts +++ b/src/i18n/detectors.ts @@ -1,20 +1,19 @@ -import { detectLocale } from "./i18n-util" +import { generalConfig } from '@/configs' +import { resolveLocale } from '@/utils/functions' -import { resolveLocale } from "@utils/functions" +import { detectLocale } from './i18n-util' -import { generalConfig } from "@configs" +function allInteractionsLocaleDetector(interaction: AllInteractions) { + return () => { + let locale = resolveLocale(interaction) -const allInteractionsLocaleDetector = (interaction: AllInteractions) => { + if (['en-US', 'en-GB'].includes(locale)) + locale = 'en' + else if (locale === 'default') + locale = generalConfig.defaultLocale - return () => { - - let locale = resolveLocale(interaction) - - if (['en-US', 'en-GB'].includes(locale)) locale = 'en' - else if (locale === 'default') locale = generalConfig.defaultLocale - - return [locale] - } + return [locale] + } } -export const getLocaleFromInteraction = (interaction: AllInteractions) => detectLocale(allInteractionsLocaleDetector(interaction)) \ No newline at end of file +export const getLocaleFromInteraction = (interaction: AllInteractions) => detectLocale(allInteractionsLocaleDetector(interaction)) diff --git a/src/i18n/formatters.ts b/src/i18n/formatters.ts index 56801f8c..9d88caa9 100644 --- a/src/i18n/formatters.ts +++ b/src/i18n/formatters.ts @@ -1,8 +1,8 @@ -import type { FormattersInitializer } from "typesafe-i18n" -import type { Locales, Formatters } from "./i18n-types" +import { FormattersInitializer } from 'typesafe-i18n' -export const initFormatters: FormattersInitializer = (locale: Locales) => { +import { Formatters, Locales } from './i18n-types' +export const initFormatters: FormattersInitializer = (_locale: Locales) => { const formatters: Formatters = { // add your formatter functions here } diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 445640f1..b728e275 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,4 +1,4 @@ -export { L } from "./i18n-node" -export { getLocaleFromInteraction } from "./detectors" -export type { Locales, Translations, BaseTranslation } from "./i18n-types" -export { loadedLocales, locales } from "./i18n-util" \ No newline at end of file +export { L } from './i18n-node' +export { getLocaleFromInteraction } from './detectors' +export type { Locales, Translations, BaseTranslation } from './i18n-types' +export { loadedLocales, locales } from './i18n-util' diff --git a/src/main.ts b/src/main.ts index feae6010..8803575d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,179 +1,179 @@ -import 'dotenv/config' import 'reflect-metadata' +import 'dotenv/config' -import { resolve } from "@discordx/importer" -import chokidar from 'chokidar' -import discordLogs from "discord-logs" -import { Client, DIService, MetadataStorage, tsyringeDependencyRegistryEngine } from "discordx" -import { container } from "tsyringe" +import process from 'node:process' -import { Server } from "@api/server" -import { apiConfig, generalConfig } from "@configs" -import { NoBotTokenError } from "@errors" +import { resolve } from '@discordx/importer' import { RequestContext } from '@mikro-orm/core' -import { Database, ErrorHandler, EventManager, ImagesUpload, Logger, PluginsManager, Store } from "@services" -import { initDataTable, resolveDependency } from "@utils/functions" + import chalk from 'chalk' -import { clientConfig } from "./client" +import chokidar from 'chokidar' +import discordLogs from 'discord-logs' +import { Client, DIService, MetadataStorage, tsyringeDependencyRegistryEngine } from 'discordx' +import { container } from 'tsyringe' -const importPattern = __dirname + "/{events,commands}/**/*.{ts,js}" +import { Server } from '@/api/server' +import { apiConfig, generalConfig } from '@/configs' +import { NoBotTokenError } from '@/errors' +import { Database, ErrorHandler, EventManager, ImagesUpload, Logger, PluginsManager, Store } from '@/services' +import { initDataTable, resolveDependency } from '@/utils/functions' + +import { clientConfig } from './client' + +// eslint-disable-next-line node/no-path-concat +const importPattern = `${__dirname}/{events,commands}/**/*.{ts,js}` /** * Import files * @param path glob pattern */ async function loadFiles(path: string): Promise { - const files = await resolve(path) - await Promise.all( - files.map((file) => { - const newFileName = file.replace('file://', '') - delete require.cache[newFileName] - import(newFileName) - }) - ) + const files = await resolve(path) + await Promise.all( + // eslint-disable-next-line array-callback-return + files.map((file) => { + const newFileName = file.replace('file://', '') + delete require.cache[newFileName] + import(newFileName) + }) + ) } /** * Hot reload */ async function reload(client: Client) { + const store = await resolveDependency(Store) + store.set('botHasBeenReloaded', true) + + const logger = await resolveDependency(Logger) + console.log('\n') + logger.startSpinner('Hot reloading...') + + // Remove events + client.removeEvents() - const store = await resolveDependency(Store) - store.set('botHasBeenReloaded', true) - - const logger = await resolveDependency(Logger) - console.log('\n') - logger.startSpinner('Hot reloading...') - - // Remove events - client.removeEvents() - - // cleanup - MetadataStorage.clear() - DIService.engine.clearAllServices() - - // transfer store instance to the new container in order to keep the same states - container.registerInstance(Store, store) - - // reload files - await loadFiles(importPattern) - - // rebuild - await MetadataStorage.instance.build() - await client.initApplicationCommands() - client.initEvents() - - // re-init services - - // plugins - const pluginManager = await resolveDependency(PluginsManager) - await pluginManager.loadPlugins() - // await pluginManager.execMains() # TODO: need this? - - // database - const db = await resolveDependency(Database) - await db.initialize(false) - - logger.log(chalk.whiteBright('Hot reloaded')) + // cleanup + MetadataStorage.clear() + DIService.engine.clearAllServices() + + // transfer store instance to the new container in order to keep the same states + container.registerInstance(Store, store) + + // reload files + await loadFiles(importPattern) + + // rebuild + await MetadataStorage.instance.build() + await client.initApplicationCommands() + client.initEvents() + + // re-init services + + // plugins + const pluginManager = await resolveDependency(PluginsManager) + await pluginManager.loadPlugins() + + // await pluginManager.execMains() # TODO: need this? + + // database + const db = await resolveDependency(Database) + await db.initialize(false) + + logger.log(chalk.whiteBright('Hot reloaded')) } async function init() { - - const logger = await resolveDependency(Logger) - - // init error handler - await resolveDependency(ErrorHandler) - - // init plugins - const pluginManager = await resolveDependency(PluginsManager) - await pluginManager.loadPlugins() - await pluginManager.syncTranslations() - - // strart spinner - console.log('\n') - logger.startSpinner('Starting...') - - // init the database - const db = await resolveDependency(Database) - await db.initialize() - - // init the client - DIService.engine = tsyringeDependencyRegistryEngine.setInjector(container) - const client = new Client(clientConfig()) - - // Load all new events - discordLogs(client, { debug: false }) - container.registerInstance(Client, client) - - // import all the commands and events - await loadFiles(importPattern) - await pluginManager.importCommands() - await pluginManager.importEvents() - - RequestContext.create(db.orm.em, async () => { - - const watcher = chokidar.watch(importPattern) - - // init the data table if it doesn't exist - await initDataTable() - - // init plugins services - await pluginManager.initServices() - - // init the plugin main file - await pluginManager.execMains() - - // log in with the bot token - if (!process.env.BOT_TOKEN) throw new NoBotTokenError() - client.login(process.env.BOT_TOKEN) - .then(async () => { - - if (process.env.NODE_ENV === 'development') { - - // reload commands and events when a file changes - watcher.on('change', () => reload(client)) - - // reload commands and events when a file is added - watcher.on('add', () => reload(client)) - - // reload commands and events when a file is deleted - watcher.on('unlink', () => reload(client)) - } - - // start the api server - if (apiConfig.enabled) { - const server = await resolveDependency(Server) - await server.start() - } - - // upload images to imgur if configured - if (process.env.IMGUR_CLIENT_ID && generalConfig.automaticUploadImagesToImgur) { - const imagesUpload = await resolveDependency(ImagesUpload) - await imagesUpload.syncWithDatabase() - } - - const store = await container.resolve(Store) - store.select('ready').subscribe(async (ready) => { - - // check that all properties that are not null are set to true - if ( - Object - .values(ready) - .filter(value => value !== null) - .every(value => value === true) - ) { - const eventManager = await resolveDependency(EventManager) - eventManager.emit('templateReady') // the template is fully ready! - } - }) - - }) - .catch((err) => { - console.error(err) - process.exit(1) - }) - }) - + const logger = await resolveDependency(Logger) + + // init error handler + await resolveDependency(ErrorHandler) + + // init plugins + const pluginManager = await resolveDependency(PluginsManager) + await pluginManager.loadPlugins() + await pluginManager.syncTranslations() + + // strart spinner + console.log('\n') + logger.startSpinner('Starting...') + + // init the database + const db = await resolveDependency(Database) + await db.initialize() + + // init the client + DIService.engine = tsyringeDependencyRegistryEngine.setInjector(container) + const client = new Client(clientConfig()) + + // Load all new events + discordLogs(client, { debug: false }) + container.registerInstance(Client, client) + + // import all the commands and events + await loadFiles(importPattern) + await pluginManager.importCommands() + await pluginManager.importEvents() + + RequestContext.create(db.orm.em, async () => { + const watcher = chokidar.watch(importPattern) + + // init the data table if it doesn't exist + await initDataTable() + + // init plugins services + await pluginManager.initServices() + + // init the plugin main file + await pluginManager.execMains() + + // log in with the bot token + if (!process.env.BOT_TOKEN) + throw new NoBotTokenError() + client.login(process.env.BOT_TOKEN) + .then(async () => { + if (process.env.NODE_ENV === 'development') { + // reload commands and events when a file changes + watcher.on('change', () => reload(client)) + + // reload commands and events when a file is added + watcher.on('add', () => reload(client)) + + // reload commands and events when a file is deleted + watcher.on('unlink', () => reload(client)) + } + + // start the api server + if (apiConfig.enabled) { + const server = await resolveDependency(Server) + await server.start() + } + + // upload images to imgur if configured + if (process.env.IMGUR_CLIENT_ID && generalConfig.automaticUploadImagesToImgur) { + const imagesUpload = await resolveDependency(ImagesUpload) + await imagesUpload.syncWithDatabase() + } + + const store = await container.resolve(Store) + store.select('ready').subscribe(async (ready) => { + // check that all properties that are not null are set to true + if ( + Object + .values(ready) + .filter(value => value !== null) + .every(value => value === true) + ) { + const eventManager = await resolveDependency(EventManager) + eventManager.emit('templateReady') // the template is fully ready! + } + }) + }) + .catch((err) => { + console.error(err) + process.exit(1) + }) + }) } -init() \ No newline at end of file +init() diff --git a/src/services/Database.ts b/src/services/Database.ts index b30b67ba..897b1fdf 100644 --- a/src/services/Database.ts +++ b/src/services/Database.ts @@ -1,200 +1,195 @@ -import { databaseConfig, mikroORMConfig } from "@configs" -import { EntityName, MikroORM, Options } from "@mikro-orm/core" -import fastFolderSizeSync from "fast-folder-size/sync" -import fs from "fs" -import { delay, inject, singleton } from "tsyringe" - -import { Schedule } from "@decorators" -import * as entities from "@entities" -import { Logger, PluginsManager } from "@services" -import { resolveDependency } from "@utils/functions" -import { backup, restore } from "saveqlite" +import fs from 'node:fs' +import process from 'node:process' -@singleton() -export class Database { - - private _orm: MikroORM - - constructor( - @inject(delay(() => Logger)) private logger: Logger - ) {} - - async initialize(migrate = true) { - - const pluginsManager = await resolveDependency(PluginsManager) - - // get config - let config = mikroORMConfig[process.env.NODE_ENV || 'development'] as Options - - // defines entities into the config - config.entities = [...Object.values(entities), ...pluginsManager.getEntities()] - - // initialize the ORM using the configuration exported in `mikro-orm.config.ts` - this._orm = await MikroORM.init(config) - - if (migrate) { - const migrator = this._orm.getMigrator() - - // create migration if no one is present in the migrations folder - const pendingMigrations = await migrator.getPendingMigrations() - const executedMigrations = await migrator.getExecutedMigrations() - if (pendingMigrations.length === 0 && executedMigrations.length === 0) { - await migrator.createInitialMigration() - } - - // migrate to the latest migration - await this._orm.getMigrator().up() - } - } - - async refreshConnection() { - await this._orm.close() - this._orm = await MikroORM.init() - } - - get orm(): MikroORM { - return this._orm - } - - get em(): DatabaseEntityManager { - return this._orm.em - } - - /** - * Shorthand to get custom and natives repositories - * @param entity Entity of the custom repository to get - */ - get(entity: EntityName) { - return this._orm.em.getRepository(entity) - } - - /** - * Create a snapshot of the database each day at 00:00 - */ - @Schedule('0 0 * * *') - async backup(snapshotName?: string): Promise { - - const { formatDate } = await import('@utils/functions') - - if (!databaseConfig.backup.enabled && !snapshotName) return false - if (!this.isSQLiteDatabase()) { - this.logger.log('Database is not SQLite, couldn\'t backup') - return false - } - - const backupPath = databaseConfig.backup.path - if (!backupPath) { - this.logger.log('Backup path not set, couldn\'t backup', 'error', true) - return false - } - - if (!snapshotName) snapshotName = `snapshot-${formatDate(new Date(), 'onlyDateFileName')}` - const objectsPath = `${backupPath}objects/` as `${string}/` - - try { - - await backup( - mikroORMConfig[process.env.NODE_ENV]!.dbName!, - snapshotName + '.txt', - objectsPath - ) - - return true - - } catch(e) { - - const errorMessage = typeof e === 'string' ? e : e instanceof Error ? e.message : 'Unknown error' - - this.logger.log('Couldn\'t backup : ' + errorMessage, 'error', true) - return false - } - - } - - /** - * Restore the SQLite database from a snapshot file. - * @param snapshotDate Date of the snapshot to restore - * @returns - */ - async restore(snapshotName: string): Promise { - - if (!this.isSQLiteDatabase()) { - this.logger.log('Database is not SQLite, couldn\'t restore', 'error') - return false - } - - const backupPath = databaseConfig.backup.path - if (!backupPath) { - this.logger.log('Backup path not set, couldn\'t restore', 'error', true) - } - - try { - - console.debug(mikroORMConfig[process.env.NODE_ENV]!.dbName!) - console.debug(`${backupPath}${snapshotName}`) - await restore( - mikroORMConfig[process.env.NODE_ENV]!.dbName!, - `${backupPath}${snapshotName}`, - ) - - await this.refreshConnection() - - return true - - } catch (error) { - - console.debug(error) - this.logger.log('Snapshot file not found, couldn\'t restore', 'error', true) - return false - } - } - - getBackupList(): string[] | null { - - const backupPath = databaseConfig.backup.path - if (!backupPath) { - this.logger.log('Backup path not set, couldn\'t get list of backups', 'error') - return null - } - - const files = fs.readdirSync(backupPath) - const backupList = files.filter(file => file.startsWith('snapshot')) - - return backupList - } +import { EntityName, MikroORM, Options } from '@mikro-orm/core' - getSize(): DatabaseSize { +import fastFolderSizeSync from 'fast-folder-size/sync' +import { backup, restore } from 'saveqlite' +import { delay, inject, singleton } from 'tsyringe' - const size: DatabaseSize = { - db: null, - backups: null - } +import { databaseConfig, mikroORMConfig } from '@/configs' +import { Schedule } from '@/decorators' +import * as entities from '@/entities' +import { Logger, PluginsManager } from '@/services' +import { resolveDependency } from '@/utils/functions' - if (this.isSQLiteDatabase()) { - - const dbPath = mikroORMConfig[process.env.NODE_ENV]!.dbName! - const dbSize = fs.statSync(dbPath).size - - size.db = dbSize - } - - const backupPath = databaseConfig.backup.path - if (backupPath) { - - const backupSize = fastFolderSizeSync(backupPath) - - size.backups = backupSize || null - } +@singleton() +export class Database { - return size - } + private _orm: MikroORM - isSQLiteDatabase(): boolean { + constructor( + @inject(delay(() => Logger)) private logger: Logger + ) {} + + async initialize(migrate = true) { + const pluginsManager = await resolveDependency(PluginsManager) + + // get config + const config = mikroORMConfig[process.env.NODE_ENV || 'development'] as Options + + // defines entities into the config + config.entities = [...Object.values(entities), ...pluginsManager.getEntities()] + + // initialize the ORM using the configuration exported in `mikro-orm.config.ts` + this._orm = await MikroORM.init(config) + + if (migrate) { + const migrator = this._orm.getMigrator() + + // create migration if no one is present in the migrations folder + const pendingMigrations = await migrator.getPendingMigrations() + const executedMigrations = await migrator.getExecutedMigrations() + if (pendingMigrations.length === 0 && executedMigrations.length === 0) + await migrator.createInitialMigration() + + // migrate to the latest migration + await this._orm.getMigrator().up() + } + } + + async refreshConnection() { + await this._orm.close() + this._orm = await MikroORM.init() + } + + get orm(): MikroORM { + return this._orm + } + + get em(): DatabaseEntityManager { + return this._orm.em + } + + /** + * Shorthand to get custom and natives repositories + * @param entity Entity of the custom repository to get + */ + get(entity: EntityName) { + return this._orm.em.getRepository(entity) + } + + /** + * Create a snapshot of the database each day at 00:00 + */ + @Schedule('0 0 * * *') + async backup(snapshotName?: string): Promise { + const { formatDate } = await import('@/utils/functions') + + if (!databaseConfig.backup.enabled && !snapshotName) + return false + if (!this.isSQLiteDatabase()) { + this.logger.log('Database is not SQLite, couldn\'t backup') + + return false + } + + const backupPath = databaseConfig.backup.path + if (!backupPath) { + this.logger.log('Backup path not set, couldn\'t backup', 'error', true) + + return false + } + + if (!snapshotName) + snapshotName = `snapshot-${formatDate(new Date(), 'onlyDateFileName')}` + const objectsPath = `${backupPath}objects/` as `${string}/` + + try { + await backup( + mikroORMConfig[process.env.NODE_ENV]!.dbName!, + `${snapshotName}.txt`, + objectsPath + ) + + return true + } catch (e) { + const errorMessage = typeof e === 'string' ? e : e instanceof Error ? e.message : 'Unknown error' + + this.logger.log(`Couldn't backup : ${errorMessage}`, 'error', true) + + return false + } + } + + /** + * Restore the SQLite database from a snapshot file. + * @param snapshotName name of the snapshot to restore + * @returns true if the snapshot has been restored, false otherwise + */ + async restore(snapshotName: string): Promise { + if (!this.isSQLiteDatabase()) { + this.logger.log('Database is not SQLite, couldn\'t restore', 'error') + + return false + } + + const backupPath = databaseConfig.backup.path + if (!backupPath) + this.logger.log('Backup path not set, couldn\'t restore', 'error', true) + + try { + console.debug(mikroORMConfig[process.env.NODE_ENV]!.dbName!) + console.debug(`${backupPath}${snapshotName}`) + await restore( + mikroORMConfig[process.env.NODE_ENV]!.dbName!, + `${backupPath}${snapshotName}` + ) + + await this.refreshConnection() + + return true + } catch (error) { + console.debug(error) + this.logger.log('Snapshot file not found, couldn\'t restore', 'error', true) + + return false + } + } + + getBackupList(): string[] | null { + const backupPath = databaseConfig.backup.path + if (!backupPath) { + this.logger.log('Backup path not set, couldn\'t get list of backups', 'error') + + return null + } + + const files = fs.readdirSync(backupPath) + const backupList = files.filter(file => file.startsWith('snapshot')) + + return backupList + } + + getSize(): DatabaseSize { + const size: DatabaseSize = { + db: null, + backups: null, + } + + if (this.isSQLiteDatabase()) { + const dbPath = mikroORMConfig[process.env.NODE_ENV]!.dbName! + const dbSize = fs.statSync(dbPath).size + + size.db = dbSize + } + + const backupPath = databaseConfig.backup.path + if (backupPath) { + const backupSize = fastFolderSizeSync(backupPath) + + size.backups = backupSize || null + } - const type = mikroORMConfig[process.env.NODE_ENV]!.type + return size + } - if (type) return ['sqlite', 'better-sqlite'].includes(type) - else return false - } + isSQLiteDatabase(): boolean { + const type = mikroORMConfig[process.env.NODE_ENV]!.type -} \ No newline at end of file + if (type) + return ['sqlite', 'better-sqlite'].includes(type) + else return false + } + +} diff --git a/src/services/ErrorHandler.ts b/src/services/ErrorHandler.ts index f0f8f15e..3fa0410e 100644 --- a/src/services/ErrorHandler.ts +++ b/src/services/ErrorHandler.ts @@ -1,36 +1,39 @@ -import { singleton } from "tsyringe" +import process from 'node:process' -import { Logger } from "@services" -import { BaseError } from "@utils/classes" +import { singleton } from 'tsyringe' + +import { Logger } from '@/services' +import { BaseError } from '@/utils/classes' @singleton() export class ErrorHandler { - constructor( - private logger: Logger - ) { - - // Catch all exceptions - process.on('uncaughtException', (error: Error, origin: string) => { - - // stop in case of unhandledRejection - if (origin === 'unhandledRejection') return - - // if instance of BaseError, call `handle` method - if (error instanceof BaseError) return error.handle() - - // log the error - this.logger.logError(error, "Exception") - }) - - // catch all Unhandled Rejection (promise) - process.on('unhandledRejection', (error: Error | any, promise: Promise) => { - - // if instance of BaseError, call `handle` method - if (error instanceof BaseError) return error.handle() - - // log the error - this.logger.logError(error, "unhandledRejection") - }) - } -} \ No newline at end of file + constructor( + private logger: Logger + ) { + // Catch all exceptions + process.on('uncaughtException', (error: Error, origin: string) => { + // stop in case of unhandledRejection + if (origin === 'unhandledRejection') + return + + // if instance of BaseError, call `handle` method + if (error instanceof BaseError) + return error.handle() + + // log the error + this.logger.logError(error, 'Exception') + }) + + // catch all Unhandled Rejection (promise) + process.on('unhandledRejection', (error: Error | any, _: Promise) => { + // if instance of BaseError, call `handle` method + if (error instanceof BaseError) + return error.handle() + + // log the error + this.logger.logError(error, 'unhandledRejection') + }) + } + +} diff --git a/src/services/EventManager.ts b/src/services/EventManager.ts index f80e8111..a3119feb 100644 --- a/src/services/EventManager.ts +++ b/src/services/EventManager.ts @@ -1,38 +1,36 @@ import { singleton } from 'tsyringe' -import { Logger } from '@services' +import { Logger } from '@/services' @singleton() export class EventManager { - private _events: Map = new Map() - - constructor( - private logger: Logger - ) { - } - - register(eventName: string, callback: Function): void { - - this._events.set(eventName, [...(this._events.get(eventName) || []), callback]) - } - - async emit(eventName: string, ...args: any[]): Promise { - - const callbacks = this._events.get(eventName) - - if (!callbacks) return - - for (const callback of callbacks) { - - try { - await callback(...args) - } catch (error) { - console.error(error) - if (error instanceof Error) { - this.logger.log(`[EventError - ${eventName}] ${error.toString()}`, 'error', true) - } - } - } - } -} \ No newline at end of file + private _events: Map = new Map() + + constructor( + private logger: Logger + ) { + } + + register(eventName: string, callback: Function): void { + this._events.set(eventName, [...(this._events.get(eventName) || []), callback]) + } + + async emit(eventName: string, ...args: any[]): Promise { + const callbacks = this._events.get(eventName) + + if (!callbacks) + return + + for (const callback of callbacks) { + try { + await callback(...args) + } catch (error) { + console.error(error) + if (error instanceof Error) + this.logger.log(`[EventError - ${eventName}] ${error.toString()}`, 'error', true) + } + } + } + +} diff --git a/src/services/ImagesUpload.ts b/src/services/ImagesUpload.ts index ba502e4b..bdaa8ca3 100644 --- a/src/services/ImagesUpload.ts +++ b/src/services/ImagesUpload.ts @@ -1,163 +1,165 @@ -import axios from "axios" -import chalk from "chalk" -import { imageHash as callbackImageHash } from "image-hash" -import { ImgurClient } from "imgur" -import { singleton } from "tsyringe" -import { promisify } from "util" +import path from 'node:path' +import process from 'node:process' +import { promisify } from 'node:util' -import { Image, ImageRepository } from "@entities" -import { Database, Logger } from "@services" -import { base64Encode, fileOrDirectoryExists, getFiles } from "@utils/functions" +import axios from 'axios' +import chalk from 'chalk' +import { imageHash as callbackImageHash } from 'image-hash' +import { ImgurClient } from 'imgur' +import { singleton } from 'tsyringe' + +import { Image, ImageRepository } from '@/entities' +import { Database, Logger } from '@/services' +import { base64Encode, fileOrDirectoryExists, getFiles } from '@/utils/functions' const imageHasher = promisify(callbackImageHash) @singleton() export class ImagesUpload { - private validImageExtensions = ['.png', '.jpg', '.jpeg'] - private imageFolderPath = `${__dirname}/../../assets/images` - - private imgurClient: ImgurClient | null = process.env.IMGUR_CLIENT_ID ? - new ImgurClient({ - clientId: process.env.IMGUR_CLIENT_ID - }) : null - - private imageRepo: ImageRepository - - constructor( - private db: Database, - private logger: Logger - ) { - this.imageRepo = this.db.get(Image) - } - - isValidImageFormat(file: string): boolean { - for (const extension of this.validImageExtensions) { - if (file.endsWith(extension)) { - return true - } - } - return false - } - - async syncWithDatabase() { - - if (!fileOrDirectoryExists(this.imageFolderPath)) this.logger.log('Image folder does not exist, couldn\'t sync with database', 'warn') - - // get all images inside the assets/images folder - const images = getFiles(this.imageFolderPath) - .filter(file => this.isValidImageFormat(file)) - .map(file => file.replace(this.imageFolderPath + '/', '')) - - - // remove all images from the database that are not anymore in the filesystem - const imagesInDb = await this.imageRepo.findAll() - - for (const image of imagesInDb) { - const imagePath = `${image.basePath !== '' ? image.basePath + '/' : ''}${image.fileName}` - - // delete the image if it is not in the filesystem anymore - if (!images.includes(imagePath)) { - - await this.imageRepo.remove(image).flush() - await this.deleteImageFromImgur(image) - } else if (!await this.isImgurImageValid(image.url)) { - // reupload if the image is not on imgur anymore - await this.addNewImageToImgur(imagePath, image.hash, true) - } - } - - // check if the image is already in the database and that its md5 hash is the same. - for (const imagePath of images) { - const imageHash = await imageHasher( - `${this.imageFolderPath}/${imagePath}`, - 16, + private validImageExtensions = ['.png', '.jpg', '.jpeg'] + private imageFolderPath = path.join(__dirname, '..', '..', 'assets', 'images') + + private imgurClient: ImgurClient | null = process.env.IMGUR_CLIENT_ID + ? new ImgurClient({ + clientId: process.env.IMGUR_CLIENT_ID, + }) + : null + + private imageRepo: ImageRepository + + constructor( + private db: Database, + private logger: Logger + ) { + this.imageRepo = this.db.get(Image) + } + + isValidImageFormat(file: string): boolean { + for (const extension of this.validImageExtensions) { + if (file.endsWith(extension)) + return true + } + + return false + } + + async syncWithDatabase() { + if (!fileOrDirectoryExists(this.imageFolderPath)) + this.logger.log('Image folder does not exist, couldn\'t sync with database', 'warn') + + // get all images inside the assets/images folder + const images = getFiles(this.imageFolderPath) + .filter(file => this.isValidImageFormat(file)) + .map(file => file.replace(`${this.imageFolderPath}/`, '')) + + // remove all images from the database that are not anymore in the filesystem + const imagesInDb = await this.imageRepo.findAll() + + for (const image of imagesInDb) { + const imagePath = `${image.basePath !== '' ? `${image.basePath}/` : ''}${image.fileName}` + + // delete the image if it is not in the filesystem anymore + if (!images.includes(imagePath)) { + await this.imageRepo.remove(image).flush() + await this.deleteImageFromImgur(image) + } else if (!await this.isImgurImageValid(image.url)) { + // reupload if the image is not on imgur anymore + await this.addNewImageToImgur(imagePath, image.hash, true) + } + } + + // check if the image is already in the database and that its md5 hash is the same. + for (const imagePath of images) { + const imageHash = await imageHasher( + `${this.imageFolderPath}/${imagePath}`, + 16, true - ) as string - - const imageInDb = await this.imageRepo.findOne({ - hash: imageHash, - }) - - if (!imageInDb) await this.addNewImageToImgur(imagePath, imageHash) - else if ( - imageInDb && ( - imageInDb.basePath != imagePath.split('/').slice(0, -1).join('/') || - imageInDb.fileName != imagePath.split('/').slice(-1)[0] ) - ) console.warn(`Image ${chalk.bold.green(imagePath)} has the same hash as ${chalk.bold.green(imageInDb.basePath + (imageInDb.basePath?.length ? "/" : "") + imageInDb.fileName)} so it will skip`) - } - } - - async deleteImageFromImgur(image: Image) { - - if (!this.imgurClient) return - - await this.imgurClient.deleteImage(image.deleteHash) - - this.logger.log( - `Image ${image.fileName} deleted from database because it is not in the filesystem anymore`, + ) as string + + const imageInDb = await this.imageRepo.findOne({ + hash: imageHash, + }) + + if (!imageInDb) + await this.addNewImageToImgur(imagePath, imageHash) + else if ( + imageInDb && ( + imageInDb.basePath !== imagePath.split('/').slice(0, -1).join('/') + || imageInDb.fileName !== imagePath.split('/').slice(-1)[0]) + ) console.warn(`Image ${chalk.bold.green(imagePath)} has the same hash as ${chalk.bold.green(imageInDb.basePath + (imageInDb.basePath?.length ? '/' : '') + imageInDb.fileName)} so it will skip`) + } + } + + async deleteImageFromImgur(image: Image) { + if (!this.imgurClient) + return + + await this.imgurClient.deleteImage(image.deleteHash) + + this.logger.log( + `Image ${image.fileName} deleted from database because it is not in the filesystem anymore`, 'info', true - ) - } + ) + } - async addNewImageToImgur(imagePath: string, imageHash: string, reupload: boolean = false) { + async addNewImageToImgur(imagePath: string, imageHash: string, _reupload: boolean = false) { + if (!this.imgurClient) + return - if (!this.imgurClient) return + // upload the image to imgur + const base64 = base64Encode(`${this.imageFolderPath}/${imagePath}`) - // upload the image to imgur - const base64 = base64Encode(`${this.imageFolderPath}/${imagePath}`) - - try { + try { + const imageFileName = imagePath.split('/').slice(-1)[0] + const imageBasePath = imagePath.split('/').slice(0, -1).join('/') - const imageFileName = imagePath.split('/').slice(-1)[0], - imageBasePath = imagePath.split('/').slice(0, -1).join('/') + const uploadResponse = await this.imgurClient.upload({ + image: base64, + type: 'base64', + name: imageFileName, + }) - const uploadResponse = await this.imgurClient.upload({ - image: base64, - type: 'base64', - name: imageFileName - }) - - if (!uploadResponse.success ) { - this.logger.log( + if (!uploadResponse.success) { + this.logger.log( `Error uploading image ${imageFileName} to imgur: ${uploadResponse.status} ${uploadResponse.data}`, 'error', true - ) - return - } - - // add the image to the database - const image = new Image() - image.fileName = imageFileName - image.basePath = imageBasePath - image.url = uploadResponse.data.link - image.size = uploadResponse.data.size - image.tags = imageBasePath.split('/') - image.hash = imageHash - image.deleteHash = uploadResponse.data.deletehash || '' - await this.imageRepo.persistAndFlush(image) - - // log the success - this.logger.log( + ) + + return + } + + // add the image to the database + const image = new Image() + image.fileName = imageFileName + image.basePath = imageBasePath + image.url = uploadResponse.data.link + image.size = uploadResponse.data.size + image.tags = imageBasePath.split('/') + image.hash = imageHash + image.deleteHash = uploadResponse.data.deletehash || '' + await this.imageRepo.persistAndFlush(image) + + // log the success + this.logger.log( `Image ${chalk.bold.green(imagePath)} uploaded to imgur`, 'info', true - ) - - } - catch (error: any) { - this.logger.log(error?.toString(), 'error', true) - } - } + ) + } catch (error: any) { + this.logger.log(error?.toString(), 'error', true) + } + } - async isImgurImageValid(imageUrl: string): Promise { + async isImgurImageValid(imageUrl: string): Promise { + if (!this.imgurClient) + return false - if (!this.imgurClient) return false + const res = await axios.get(imageUrl) - const res = await axios.get(imageUrl) + return !res.request?.path.includes('/removed') + } - return !res.request?.path.includes('/removed') - } -} \ No newline at end of file +} diff --git a/src/services/Logger.ts b/src/services/Logger.ts index f227f1b4..cb9ca217 100644 --- a/src/services/Logger.ts +++ b/src/services/Logger.ts @@ -1,561 +1,581 @@ -import * as controllers from "@api/controllers" -import { apiConfig, logsConfig } from "@configs" -import { Schedule } from "@decorators" -import { Pastebin, PluginsManager, Scheduler, Store } from "@services" -import { fileOrDirectoryExists, formatDate, getTypeOfInteraction, numberAlign, oneLine, resolveAction, resolveChannel, resolveDependency, resolveGuild, resolveUser, validString } from "@utils/functions" -import archiver from "archiver" -import boxen from "boxen" -import { constant } from "case" -import chalk from "chalk" -import dayjs from "dayjs" -import { BaseMessageOptions, TextChannel, ThreadChannel, User } from "discord.js" -import { Client, MetadataStorage } from "discordx" -import fs from "fs" -import { unlink } from "fs/promises" -import ora from "ora" -import { StackFrame, parse } from "stacktrace-parser" -import { delay, inject, singleton } from "tsyringe" +import fs from 'node:fs' +import { unlink } from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' + +import archiver from 'archiver' +import boxen from 'boxen' +import { constant } from 'case' +import chalk from 'chalk' +import dayjs from 'dayjs' +import { BaseMessageOptions, TextChannel, ThreadChannel, User } from 'discord.js' +import { Client, MetadataStorage } from 'discordx' +import ora from 'ora' +import { parse, StackFrame } from 'stacktrace-parser' +import { delay, inject, singleton } from 'tsyringe' + +import * as controllers from '@/api/controllers' +import { apiConfig, logsConfig } from '@/configs' +import { Schedule } from '@/decorators' +import { Pastebin, PluginsManager, Scheduler, Store } from '@/services' +import { fileOrDirectoryExists, formatDate, getTypeOfInteraction, numberAlign, oneLine, resolveAction, resolveChannel, resolveDependency, resolveGuild, resolveUser, validString } from '@/utils/functions' const defaultConsole = { ...console } @singleton() export class Logger { - private readonly logPath: string = `${__dirname}/../../logs` - private readonly logArchivePath: string = `${this.logPath}/archives` - - private readonly levels = ['info', 'warn', 'error'] as const - private embedLevelBuilder = { - info: (message: string): BaseMessageOptions => ({ embeds: [{ title: "INFO", description: message, color: 0x007fe7, timestamp: new Date().toISOString() }] }), - warn: (message: string): BaseMessageOptions => ({ embeds: [{ title: "WARN", description: message, color: 0xf37100, timestamp: new Date().toISOString() }] }), - error: (message: string): BaseMessageOptions => ({ embeds: [{ title: "ERROR", description: message, color: 0x7C1715, timestamp: new Date().toISOString() }] }), - } - - private interactionTypeReadable: { [key in InteractionsConstants]: string } = { - "CHAT_INPUT_COMMAND_INTERACTION": "Slash command", - "SIMPLE_COMMAND_MESSAGE": "Simple command", - "CONTEXT_MENU_INTERACTION": "Context menu", - "BUTTON_INTERACTION": "Button", - "SELECT_MENU_INTERACTION": "Select menu", - "MODAL_SUBMIT_INTERACTION": "Modal submit", - } - - private spinner = ora() - - private lastLogsTail: string[] = [] - - constructor( - @inject(delay(() => Client)) private client: Client, - @inject(delay(() => Scheduler)) private scheduler: Scheduler, - @inject(delay(() => Store)) private store: Store, - @inject(delay(() => Pastebin)) private pastebin: Pastebin, - @inject(delay(() => PluginsManager)) private pluginsManager: PluginsManager - ) { - if (!this.store.get('botHasBeenReloaded')) { - console.info = (...args) => this.log(args.join(", "), 'info') - console.warn = (...args) => this.log(args.join(", "), 'warn') - console.error = (...args) => this.log(args.join(", "), 'error') - } - } - - // ================================= - // ======== Output Providers ======= - // ================================= - - /** - * Log a message in the console. - * @param message the message to log - * @param level info (default) | warn | error - * @param ignoreTemplate if it should ignore the timestamp template (default to false) - */ - console(message: string, level: typeof this.levels[number] = 'info', ignoreTemplate = false) { - - if (this.spinner.isSpinning) this.spinner.stop() - - if (!validString(message)) return - - let templatedMessage = ignoreTemplate ? message : `${level} [${chalk.dim.gray(formatDate(new Date()))}] ${message}` - if (level === 'error') templatedMessage = chalk.red(templatedMessage) - - defaultConsole[level](templatedMessage) - - // save the last logs tail queue - if (this.lastLogsTail.length >= logsConfig.logTailMaxSize) this.lastLogsTail.shift() - this.lastLogsTail.push(message) - } - - /** - * Log a message in a log file. - * @param message the message to log - * @param level info (default) | warn | error - */ - file(message: string, level: typeof this.levels[number] = 'info') { - - if (!validString(message)) return - - const templatedMessage = `[${formatDate(new Date())}] ${message}` - - const fileName = `${this.logPath}/${level}.log` - - // create the folder if it doesn't exist - if (!fileOrDirectoryExists(this.logPath)) fs.mkdirSync(this.logPath) - // create file if it doesn't exist - if (!fileOrDirectoryExists(fileName)) fs.writeFileSync(fileName, '') - - fs.appendFileSync(fileName, `${templatedMessage}\n`) - } - - /** - * Log a message in a Discord channel using embeds. - * @param channelId the ID of the discord channel to log to - * @param message the message to log or a [MessageOptions](https://discord.js.org/#/docs/discord.js/main/typedef/BaseMessageOptions) compliant object (like embeds, components, etc) - * @param level info (default) | warn | error - */ - async discordChannel(channelId: string, message: string | BaseMessageOptions, level?: typeof this.levels[number]) { - - if (!this.client.token) return - - const channel = await this.client.channels.fetch(channelId).catch(() => null) - - if ( - channel && - ( channel instanceof TextChannel - || channel instanceof ThreadChannel ) - ) { - - if (typeof message !== 'string') return channel.send(message).catch(console.error) - - channel.send(this.embedLevelBuilder[level ?? 'info'](message)).catch(console.error) - } - } - - // ================================= - // =========== Archive ============= - // ================================= - - /** - * Archive the logs in a zip file each day. - */ - @Schedule('0 0 * * *') - async archiveLogs() { - - if (!logsConfig.archive.enabled) return - - const date = dayjs().subtract(1, 'day').format('YYYY-MM-DD') - const currentLogsPaths = fs.readdirSync(this.logPath).filter(file => file.endsWith('.log')) - const output = fs.createWriteStream(`${this.logArchivePath}/logs-${date}.tar.gz`) - - if (!fileOrDirectoryExists(this.logArchivePath)) fs.mkdirSync(this.logArchivePath) - - const archive = archiver('tar', { - gzip: true, - gzipOptions: { - level: 9 // maximum compression - } - }) + private readonly logPath: string = path.join(__dirname, '..', '..', 'logs') + private readonly logArchivePath: string = path.join(this.logPath, 'archives') - archive.pipe(output) + private readonly levels = ['info', 'warn', 'error'] as const + private embedLevelBuilder = { + info: (message: string): BaseMessageOptions => ({ embeds: [{ title: 'INFO', description: message, color: 0x007FE7, timestamp: new Date().toISOString() }] }), + warn: (message: string): BaseMessageOptions => ({ embeds: [{ title: 'WARN', description: message, color: 0xF37100, timestamp: new Date().toISOString() }] }), + error: (message: string): BaseMessageOptions => ({ embeds: [{ title: 'ERROR', description: message, color: 0x7C1715, timestamp: new Date().toISOString() }] }), + } - // add files to the archive - for (const logPath of currentLogsPaths) { - archive.file(`${this.logPath}/${logPath}`, { name: logPath }) - } + private interactionTypeReadable: { [key in InteractionsConstants]: string } = { + CHAT_INPUT_COMMAND_INTERACTION: 'Slash command', + SIMPLE_COMMAND_MESSAGE: 'Simple command', + CONTEXT_MENU_INTERACTION: 'Context menu', + BUTTON_INTERACTION: 'Button', + SELECT_MENU_INTERACTION: 'Select menu', + MODAL_SUBMIT_INTERACTION: 'Modal submit', + } - // create archive - await archive.finalize() + private spinner = ora() - // delete old logs - await this.deleteCurrentLogs() + private lastLogsTail: string[] = [] - // retention policy - await this.deleteOldLogArchives() - - } - - private async deleteCurrentLogs() { - - const currentLogsPaths = fs.readdirSync(this.logPath).filter(file => file.endsWith('.log')) - - for (const logPath of currentLogsPaths) { - // empty the file - fs.writeFileSync(`${this.logPath}/${logPath}`, '') - } - } - - private async deleteOldLogArchives() { - - const archives = fs.readdirSync(this.logArchivePath).filter(file => file.endsWith('.tar.gz')) - - for (const archive of archives) { - const date = dayjs(archive.split('logs-')[1].split('.tar.gz')[0]) - console.log(date.format('YYYY-MM-DD')) - if (date.isBefore(dayjs().subtract(logsConfig.archive.retention, 'day'))) { - await unlink(`${this.logArchivePath}/${archive}`) - } - } - } - - // ================================= - // =========== Shortcut ============ - // ================================= - - /** - * Shortcut function that will log in the console, and optionally in a file or discord channel depending on params. - * @param message message to log - * @param level info (default) | warn | error - * @param saveToFile if true, the message will be saved to a file (default to true) - * @param channelId Discord channel to log to (if `null`, nothing will be logged to Discord) - */ - log( - message: string, - level: typeof this.levels[number] = 'info', + constructor( + @inject(delay(() => Client)) private client: Client, + @inject(delay(() => Scheduler)) private scheduler: Scheduler, + @inject(delay(() => Store)) private store: Store, + @inject(delay(() => Pastebin)) private pastebin: Pastebin, + @inject(delay(() => PluginsManager)) private pluginsManager: PluginsManager + ) { + if (!this.store.get('botHasBeenReloaded')) { + console.info = (...args) => this.log(args.join(', '), 'info') + console.warn = (...args) => this.log(args.join(', '), 'warn') + console.error = (...args) => this.log(args.join(', '), 'error') + } + } + + // ================================= + // ======== Output Providers ======= + // ================================= + + /** + * Log a message in the console. + * @param message the message to log + * @param level info (default) | warn | error + * @param ignoreTemplate if it should ignore the timestamp template (default to false) + */ + console(message: string, level: typeof this.levels[number] = 'info', ignoreTemplate = false) { + if (this.spinner.isSpinning) + this.spinner.stop() + + if (!validString(message)) + return + + let templatedMessage = ignoreTemplate ? message : `${level} [${chalk.dim.gray(formatDate(new Date()))}] ${message}` + if (level === 'error') + templatedMessage = chalk.red(templatedMessage) + + defaultConsole[level](templatedMessage) + + // save the last logs tail queue + if (this.lastLogsTail.length >= logsConfig.logTailMaxSize) + this.lastLogsTail.shift() + + this.lastLogsTail.push(message) + } + + /** + * Log a message in a log file. + * @param message the message to log + * @param level info (default) | warn | error + */ + file(message: string, level: typeof this.levels[number] = 'info') { + if (!validString(message)) + return + + const templatedMessage = `[${formatDate(new Date())}] ${message}` + + const fileName = `${this.logPath}/${level}.log` + + // create the folder if it doesn't exist + if (!fileOrDirectoryExists(this.logPath)) + fs.mkdirSync(this.logPath) + + // create file if it doesn't exist + if (!fileOrDirectoryExists(fileName)) + fs.writeFileSync(fileName, '') + + fs.appendFileSync(fileName, `${templatedMessage}\n`) + } + + /** + * Log a message in a Discord channel using embeds. + * @param channelId the ID of the discord channel to log to + * @param message the message to log or a [MessageOptions](https://discord.js.org/#/docs/discord.js/main/typedef/BaseMessageOptions) compliant object (like embeds, components, etc) + * @param level info (default) | warn | error + */ + async discordChannel(channelId: string, message: string | BaseMessageOptions, level?: typeof this.levels[number]) { + if (!this.client.token) + return + + const channel = await this.client.channels.fetch(channelId).catch(() => null) + + if ( + channel + && (channel instanceof TextChannel + || channel instanceof ThreadChannel) + ) { + if (typeof message !== 'string') + return channel.send(message).catch(console.error) + + channel.send(this.embedLevelBuilder[level ?? 'info'](message)).catch(console.error) + } + } + + // ================================= + // =========== Archive ============= + // ================================= + + /** + * Archive the logs in a zip file each day. + */ + @Schedule('0 0 * * *') + async archiveLogs() { + if (!logsConfig.archive.enabled) + return + + const date = dayjs().subtract(1, 'day').format('YYYY-MM-DD') + const currentLogsPaths = fs.readdirSync(this.logPath).filter(file => file.endsWith('.log')) + const output = fs.createWriteStream(`${this.logArchivePath}/logs-${date}.tar.gz`) + + if (!fileOrDirectoryExists(this.logArchivePath)) + fs.mkdirSync(this.logArchivePath) + + const archive = archiver('tar', { + gzip: true, + gzipOptions: { + level: 9, // maximum compression + }, + }) + + archive.pipe(output) + + // add files to the archive + for (const logPath of currentLogsPaths) + archive.file(`${this.logPath}/${logPath}`, { name: logPath }) + + // create archive + await archive.finalize() + + // delete old logs + await this.deleteCurrentLogs() + + // retention policy + await this.deleteOldLogArchives() + } + + private async deleteCurrentLogs() { + const currentLogsPaths = fs.readdirSync(this.logPath).filter(file => file.endsWith('.log')) + + for (const logPath of currentLogsPaths) { + // empty the file + fs.writeFileSync(`${this.logPath}/${logPath}`, '') + } + } + + private async deleteOldLogArchives() { + const archives = fs.readdirSync(this.logArchivePath).filter(file => file.endsWith('.tar.gz')) + + for (const archive of archives) { + const date = dayjs(archive.split('logs-')[1].split('.tar.gz')[0]) + console.log(date.format('YYYY-MM-DD')) + if (date.isBefore(dayjs().subtract(logsConfig.archive.retention, 'day'))) + await unlink(`${this.logArchivePath}/${archive}`) + } + } + + // ================================= + // =========== Shortcut ============ + // ================================= + + /** + * Shortcut function that will log in the console, and optionally in a file or discord channel depending on params. + * @param message message to log + * @param level info (default) | warn | error + * @param saveToFile if true, the message will be saved to a file (default to true) + * @param channelId Discord channel to log to (if `null`, nothing will be logged to Discord) + */ + log( + message: string, + level: typeof this.levels[number] = 'info', saveToFile: boolean = true, channelId: string | null = null - ) { - - if (message === '') return - - // log in the console - this.console(message, level) - - // save log to file - if (saveToFile) this.file(message, level) - - // send to discord channel - if (channelId) this.discordChannel(channelId, message, level) - } - - // ================================= - // ========= Log Templates ========= - // ================================= - - /** - * Logs any interaction that is not excluded in the config. - * @param interaction - */ - logInteraction(interaction: AllInteractions) { - - const type = constant(getTypeOfInteraction(interaction)) as InteractionsConstants - if (logsConfig.interaction.exclude.includes(type)) return - - const action = resolveAction(interaction) - const channel = resolveChannel(interaction) - const guild = resolveGuild(interaction) - const user = resolveUser(interaction) - - const message = oneLine` + ) { + if (message === '') + return + + // log in the console + this.console(message, level) + + // save log to file + if (saveToFile) + this.file(message, level) + + // send to discord channel + if (channelId) + this.discordChannel(channelId, message, level) + } + + // ================================= + // ========= Log Templates ========= + // ================================= + + /** + * Logs any interaction that is not excluded in the config. + * @param interaction + */ + logInteraction(interaction: AllInteractions) { + const type = constant(getTypeOfInteraction(interaction)) as InteractionsConstants + if (logsConfig.interaction.exclude.includes(type)) + return + + const action = resolveAction(interaction) + const channel = resolveChannel(interaction) + const guild = resolveGuild(interaction) + const user = resolveUser(interaction) + + const message = oneLine` (${type}) "${action}" - ${channel instanceof TextChannel || channel instanceof ThreadChannel ? `in channel #${channel.name}`: ''} - ${guild ? `in guild ${guild.name}`: ''} - ${user ? `by ${user.username}#${user.discriminator}`: ''} + ${channel instanceof TextChannel || channel instanceof ThreadChannel ? `in channel #${channel.name}` : ''} + ${guild ? `in guild ${guild.name}` : ''} + ${user ? `by ${user.username}#${user.discriminator}` : ''} ` - const chalkedMessage = oneLine` + const chalkedMessage = oneLine` (${chalk.bold.white(type)}) "${chalk.bold.green(action)}" - ${channel instanceof TextChannel || channel instanceof ThreadChannel ? - `${chalk.dim.italic.gray('in channel')} ${chalk.bold.blue(`#${channel.name}`)}` + ${channel instanceof TextChannel || channel instanceof ThreadChannel + ? `${chalk.dim.italic.gray('in channel')} ${chalk.bold.blue(`#${channel.name}`)}` : '' } - ${guild ? - `${chalk.dim.italic.gray('in guild')} ${chalk.bold.blue(`${guild.name}`)}` + ${guild + ? `${chalk.dim.italic.gray('in guild')} ${chalk.bold.blue(`${guild.name}`)}` : '' } - ${user ? - `${chalk.dim.italic.gray('by')} ${chalk.bold.blue(`${user.username}#${user.discriminator}`)}` + ${user + ? `${chalk.dim.italic.gray('by')} ${chalk.bold.blue(`${user.username}#${user.discriminator}`)}` : '' } ` - if (logsConfig.interaction.console) this.console(chalkedMessage) - if (logsConfig.interaction.file) this.file(message) - if (logsConfig.interaction.channel) this.discordChannel(logsConfig.interaction.channel, { - embeds: [{ - author: { - name: (user ? `${user.username}#${user.discriminator}` : 'Unknown user'), - icon_url: (user?.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}` : '') - }, - title: `Interaction`, - thumbnail: { - url: guild?.iconURL({ forceStatic: true }) ?? "" - }, - fields: [ - { - name: 'Type', - value: this.interactionTypeReadable[type], - inline: true - }, - { - name: "\u200b", - value: "\u200b", - inline: true - }, - { - name: 'Action', - value: action, - inline: true - }, - { - name: "Guild", - value: guild ? guild.name : 'Unknown', - inline: true - }, - { - name: "\u200b", - value: "\u200b", - inline: true - }, - { - name: "Channel", - value: channel instanceof TextChannel || channel instanceof ThreadChannel ? `#${channel.name}` : 'Unknown', - inline: true - } - ], - color: 0xdb5c21, - timestamp: new Date().toISOString() - }] - }) - } - - /** - * Logs all new users. - * @param user - */ - logNewUser(user: User) { - - const message = `(NEW_USER) ${user.tag} (${user.id}) has been added to the db` - const chalkedMessage = `(${chalk.bold.white('NEW_USER')}) ${chalk.bold.green(user.tag)} (${chalk.bold.blue(user.id)}) ${chalk.dim.italic.gray('has been added to the db')}` - - if (logsConfig.newUser.console) this.console(chalkedMessage) - if (logsConfig.newUser.file) this.file(message) - if (logsConfig.newUser.channel) this.discordChannel(logsConfig.newUser.channel, { - embeds: [{ - title: 'New user', - description: `**${user.tag}**`, - thumbnail: { - url: user.displayAvatarURL({ forceStatic: false }) - }, - color: 0x83dd80, - timestamp: new Date().toISOString(), - footer: { - text: user.id - } - }] - }) - } - - /** - * Logs all 'actions' (create, delete, etc) of a guild. - * @param type NEW_GUILD, DELETE_GUILD, RECOVER_GUILD - * @param guildId - */ - logGuild(type: 'NEW_GUILD' | 'DELETE_GUILD' | 'RECOVER_GUILD', guildId: string) { - - const additionalMessage = - type === 'NEW_GUILD' ? 'has been added to the db' : - type === 'DELETE_GUILD' ? 'has been deleted' : - type === 'RECOVER_GUILD' ? 'has been recovered' : '' - - resolveDependency(Client).then(async client => { - - const guild = await client.guilds.fetch(guildId).catch(() => null) - - const message = `(${type}) Guild ${guild ? `${guild.name} (${guildId})` : guildId} ${additionalMessage}` - const chalkedMessage = oneLine` + if (logsConfig.interaction.console) + this.console(chalkedMessage) + if (logsConfig.interaction.file) + this.file(message) + if (logsConfig.interaction.channel) { + this.discordChannel(logsConfig.interaction.channel, { + embeds: [{ + author: { + name: (user ? `${user.username}#${user.discriminator}` : 'Unknown user'), + icon_url: (user?.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}` : ''), + }, + title: `Interaction`, + thumbnail: { + url: guild?.iconURL({ forceStatic: true }) ?? '', + }, + fields: [ + { + name: 'Type', + value: this.interactionTypeReadable[type], + inline: true, + }, + { + name: '\u200B', + value: '\u200B', + inline: true, + }, + { + name: 'Action', + value: action, + inline: true, + }, + { + name: 'Guild', + value: guild ? guild.name : 'Unknown', + inline: true, + }, + { + name: '\u200B', + value: '\u200B', + inline: true, + }, + { + name: 'Channel', + value: channel instanceof TextChannel || channel instanceof ThreadChannel ? `#${channel.name}` : 'Unknown', + inline: true, + }, + ], + color: 0xDB5C21, + timestamp: new Date().toISOString(), + }], + }) + } + } + + /** + * Logs all new users. + * @param user + */ + logNewUser(user: User) { + const message = `(NEW_USER) ${user.tag} (${user.id}) has been added to the db` + const chalkedMessage = `(${chalk.bold.white('NEW_USER')}) ${chalk.bold.green(user.tag)} (${chalk.bold.blue(user.id)}) ${chalk.dim.italic.gray('has been added to the db')}` + + if (logsConfig.newUser.console) + this.console(chalkedMessage) + if (logsConfig.newUser.file) + this.file(message) + if (logsConfig.newUser.channel) { + this.discordChannel(logsConfig.newUser.channel, { + embeds: [{ + title: 'New user', + description: `**${user.tag}**`, + thumbnail: { + url: user.displayAvatarURL({ forceStatic: false }), + }, + color: 0x83DD80, + timestamp: new Date().toISOString(), + footer: { + text: user.id, + }, + }], + }) + } + } + + /** + * Logs all 'actions' (create, delete, etc) of a guild. + * @param type NEW_GUILD, DELETE_GUILD, RECOVER_GUILD + * @param guildId + */ + logGuild(type: 'NEW_GUILD' | 'DELETE_GUILD' | 'RECOVER_GUILD', guildId: string) { + const additionalMessage + = type === 'NEW_GUILD' + ? 'has been added to the db' + : type === 'DELETE_GUILD' + ? 'has been deleted' + : type === 'RECOVER_GUILD' ? 'has been recovered' : '' + + resolveDependency(Client).then(async (client) => { + const guild = await client.guilds.fetch(guildId).catch(() => null) + + const message = `(${type}) Guild ${guild ? `${guild.name} (${guildId})` : guildId} ${additionalMessage}` + const chalkedMessage = oneLine` (${chalk.bold.white(type)}) ${chalk.dim.italic.gray('Guild')} - ${guild ? - `${chalk.bold.green(guild.name)} (${chalk.bold.blue(guildId)})` + ${guild + ? `${chalk.bold.green(guild.name)} (${chalk.bold.blue(guildId)})` : guildId } ${chalk.dim.italic.gray(additionalMessage)} ` - if (logsConfig.guild.console) this.console(chalkedMessage) - if (logsConfig.guild.file) this.file(message) - if (logsConfig.guild.channel) this.discordChannel(logsConfig.guild.channel, { - embeds: [{ - title: (type === 'NEW_GUILD' ? 'New guild' : type === 'DELETE_GUILD' ? 'Deleted guild' : 'Recovered guild'), - //description: `**${guild.name} (\`${guild.id}\`)**\n${guild.memberCount} members`, - fields: [{ - name: guild?.name ?? 'Unknown', - value: `${guild?.memberCount ?? 'N/A'} members` - }], - footer: { - text: guild?.id ?? 'Unknown' - }, - thumbnail: { - url: guild?.iconURL() ?? '' - }, - color: (type === 'NEW_GUILD' ? 0x02fd77 : type === 'DELETE_GUILD' ? 0xff0000 : 0xfffb00), - timestamp: new Date().toISOString(), - }] - }) - }) - } - - /** - * Logs errors. - * @param error - * @param type uncaughtException, unhandledRejection - * @param trace - */ - async logError(error: Error | any, type: 'Exception' | 'unhandledRejection', trace: StackFrame[] = parse(error.stack ?? '')) { - - let message = '(ERROR)' - let embedMessage = '' - let embedTitle = '' - let chalkedMessage = `(${chalk.bold.white('ERROR')})` - - if (trace && trace[0]) { - message += ` ${type === 'Exception' ? 'Exception' : 'Unhandled rejection'} : ${error.message}\n${trace.map((frame: StackFrame) => `\t> ${frame.file}:${frame.lineNumber}`).join('\n')}` - embedMessage += `\`\`\`\n${trace.map((frame: StackFrame) => `\> ${frame.file}:${frame.lineNumber}`).join('\n')}\n\`\`\`` - embedTitle += `***${type === 'Exception' ? 'Exception' : 'Unhandled rejection'}* : ${error.message}**` - chalkedMessage += ` ${chalk.dim.italic.gray(type === 'Exception' ? 'Exception' : 'Unhandled rejection')} : ${error.message}\n${chalk.dim.italic(trace.map((frame: StackFrame) => `\t> ${frame.file}:${frame.lineNumber}`).join('\n'))}` - } else { - if (type === 'Exception') { - message += `An exception as occurred in a unknown file\n\t> ${error.message}` - embedMessage += `An exception as occurred in a unknown file\n${error.message}` - } else { - message += `An unhandled rejection as occurred in a unknown file\n\t> ${error}` - embedMessage += `An unhandled rejection as occurred in a unknown file\n${error}` - } - } - - if (embedMessage.length >= 4096) { - const paste = await this.pastebin.createPaste(embedTitle + "\n" + embedMessage) - console.log(paste?.getLink()) - embedMessage = `[Pastebin of the error](https://rentry.co/${paste?.getLink()})` - } - - if (logsConfig.error.console) this.console(chalkedMessage, 'error') - if (logsConfig.error.file) this.file(message, 'error') - if (logsConfig.error.channel && process.env['NODE_ENV'] === 'production') this.discordChannel(logsConfig.error.channel, { - embeds: [{ - title: (embedTitle.length >= 256 ? (embedTitle.substring(0, 252) + "...") : embedTitle), - description: embedMessage, - color: 0x7C1715, - timestamp: new Date().toISOString() - - }] - }, 'error') - } - - // ================================= - // ============= Other ============= - // ================================= - - getLastLogs() { - return this.lastLogsTail - } - - startSpinner(text: string) { - - this.spinner.start(text) - } - - async logStartingConsole() { - - const symbol = '✓', - tab = '\u200B \u200B' - - this.spinner.stop() - - this.console(chalk.dim.gray('\n━━━━━━━━━━ Started! ━━━━━━━━━━\n'), 'info', true) - - // commands - const slashCommands = MetadataStorage.instance.applicationCommandSlashes - const simpleCommands = MetadataStorage.instance.simpleCommands - const contextMenus = [ - ...MetadataStorage.instance.applicationCommandMessages, - ...MetadataStorage.instance.applicationCommandUsers - ] - const commandsSum = slashCommands.length + simpleCommands.length + contextMenus.length - - this.console(chalk.blue(`${symbol} ${numberAlign(commandsSum)} ${chalk.bold('commands')} loaded`), 'info', true) - this.console(chalk.dim.gray(`${tab}┝──╾ ${numberAlign(slashCommands.length)} slash commands\n${tab}┝──╾ ${numberAlign(simpleCommands.length)} simple commands\n${tab}╰──╾ ${numberAlign(contextMenus.length)} context menus`), 'info', true) - - // events - const events = MetadataStorage.instance.events - - this.console(chalk.magenta(`${symbol} ${numberAlign(events.length)} ${chalk.bold('events')} loaded`), 'info', true) - - // entities - const entities = fs.readdirSync(`${__dirname}/../entities`) - .filter(entity => - !entity.startsWith('index') - && !entity.startsWith('BaseEntity') - ) - - const pluginsEntitesCount = this.pluginsManager.plugins.reduce((acc, plugin) => acc + Object.values(plugin.entities).length, 0) - - this.console(chalk.red(`${symbol} ${numberAlign(entities.length + pluginsEntitesCount)} ${chalk.bold('entities')} loaded`), 'info', true) - - // services - const services = fs.readdirSync(`${__dirname}/../services`) - .filter(service => !service.startsWith('index')) - - const pluginsServicesCount = this.pluginsManager.plugins.reduce((acc, plugin) => acc + Object.values(plugin.services).length, 0) - - this.console(chalk.yellow(`${symbol} ${numberAlign(services.length + pluginsServicesCount)} ${chalk.bold('services')} loaded`), 'info', true) - - // api - if (apiConfig.enabled) { - - const endpointsCount = Object.values(controllers).reduce((acc, controller) => { - - const methodsName = Object - .getOwnPropertyNames(controller.prototype) - .filter(methodName => methodName !== 'constructor') - - return acc + methodsName.length - }, 0) - - this.console(chalk.cyan(`${symbol} ${numberAlign(endpointsCount)} ${chalk.bold('api endpoints')} loaded`), 'info', true) - } - - // scheduled jobs - const scheduledJobs = this.scheduler.jobs.size - - this.console(chalk.green(`${symbol} ${numberAlign(scheduledJobs)} ${chalk.bold('scheduled jobs')} loaded`), 'info', true) - - // plugins - const pluginsCount = this.pluginsManager.plugins.length - - this.console(chalk.hex('#47d188')(`${symbol} ${numberAlign(pluginsCount)} ${chalk.bold('plugin' + (pluginsCount > 1 ? 's':''))} loaded`), 'info', true) - - // connected - if (apiConfig.enabled) { - - this.console(chalk.gray(boxen( - ` API Server listening on port ${chalk.bold(apiConfig.port)} `, - { - padding: 0, - margin: { - top: 1, - bottom: 0, - left: 1, - right: 1 - }, - borderStyle: 'round', - dimBorder: true - } - )), 'info', true) - } - - this.console(chalk.hex('7289DA')(boxen( - ` ${this.client.user ? `${chalk.bold(this.client.user.tag)}` : 'Bot'} is ${chalk.green('connected')}! `, - { - padding: 0, - margin: { - top: 1, - bottom: 1, - left: 1 * 3, - right: 1 * 3 - }, - borderStyle: 'round', - dimBorder: true - } - )), 'info', true) - } -} \ No newline at end of file + if (logsConfig.guild.console) + this.console(chalkedMessage) + if (logsConfig.guild.file) + this.file(message) + if (logsConfig.guild.channel) { + this.discordChannel(logsConfig.guild.channel, { + embeds: [{ + title: (type === 'NEW_GUILD' ? 'New guild' : type === 'DELETE_GUILD' ? 'Deleted guild' : 'Recovered guild'), + + // description: `**${guild.name} (\`${guild.id}\`)**\n${guild.memberCount} members`, + fields: [{ + name: guild?.name ?? 'Unknown', + value: `${guild?.memberCount ?? 'N/A'} members`, + }], + footer: { + text: guild?.id ?? 'Unknown', + }, + thumbnail: { + url: guild?.iconURL() ?? '', + }, + color: (type === 'NEW_GUILD' ? 0x02FD77 : type === 'DELETE_GUILD' ? 0xFF0000 : 0xFFFB00), + timestamp: new Date().toISOString(), + }], + }) + } + }) + } + + /** + * Logs errors. + * @param error + * @param type uncaughtException, unhandledRejection + * @param trace + */ + async logError(error: Error | any, type: 'Exception' | 'unhandledRejection', trace: StackFrame[] = parse(error.stack ?? '')) { + let message = '(ERROR)' + let embedMessage = '' + let embedTitle = '' + let chalkedMessage = `(${chalk.bold.white('ERROR')})` + + if (trace && trace[0]) { + message += ` ${type === 'Exception' ? 'Exception' : 'Unhandled rejection'} : ${error.message}\n${trace.map((frame: StackFrame) => `\t> ${frame.file}:${frame.lineNumber}`).join('\n')}` + embedMessage += `\`\`\`\n${trace.map((frame: StackFrame) => `\> ${frame.file}:${frame.lineNumber}`).join('\n')}\n\`\`\`` + embedTitle += `***${type === 'Exception' ? 'Exception' : 'Unhandled rejection'}* : ${error.message}**` + chalkedMessage += ` ${chalk.dim.italic.gray(type === 'Exception' ? 'Exception' : 'Unhandled rejection')} : ${error.message}\n${chalk.dim.italic(trace.map((frame: StackFrame) => `\t> ${frame.file}:${frame.lineNumber}`).join('\n'))}` + } else { + if (type === 'Exception') { + message += `An exception as occurred in a unknown file\n\t> ${error.message}` + embedMessage += `An exception as occurred in a unknown file\n${error.message}` + } else { + message += `An unhandled rejection as occurred in a unknown file\n\t> ${error}` + embedMessage += `An unhandled rejection as occurred in a unknown file\n${error}` + } + } + + if (embedMessage.length >= 4096) { + const paste = await this.pastebin.createPaste(`${embedTitle}\n${embedMessage}`) + console.log(paste?.getLink()) + embedMessage = `[Pastebin of the error](https://rentry.co/${paste?.getLink()})` + } + + if (logsConfig.error.console) + this.console(chalkedMessage, 'error') + if (logsConfig.error.file) + this.file(message, 'error') + if (logsConfig.error.channel && process.env.NODE_ENV === 'production') { + this.discordChannel(logsConfig.error.channel, { + embeds: [{ + title: (embedTitle.length >= 256 ? (`${embedTitle.substring(0, 252)}...`) : embedTitle), + description: embedMessage, + color: 0x7C1715, + timestamp: new Date().toISOString(), + + }], + }, 'error') + } + } + + // ================================= + // ============= Other ============= + // ================================= + + getLastLogs() { + return this.lastLogsTail + } + + startSpinner(text: string) { + this.spinner.start(text) + } + + async logStartingConsole() { + const symbol = '✓' + const tab = '\u200B \u200B' + + this.spinner.stop() + + this.console(chalk.dim.gray('\n━━━━━━━━━━ Started! ━━━━━━━━━━\n'), 'info', true) + + // commands + const slashCommands = MetadataStorage.instance.applicationCommandSlashes + const simpleCommands = MetadataStorage.instance.simpleCommands + const contextMenus = [ + ...MetadataStorage.instance.applicationCommandMessages, + ...MetadataStorage.instance.applicationCommandUsers, + ] + const commandsSum = slashCommands.length + simpleCommands.length + contextMenus.length + + this.console(chalk.blue(`${symbol} ${numberAlign(commandsSum)} ${chalk.bold('commands')} loaded`), 'info', true) + this.console(chalk.dim.gray(`${tab}┝──╾ ${numberAlign(slashCommands.length)} slash commands\n${tab}┝──╾ ${numberAlign(simpleCommands.length)} simple commands\n${tab}╰──╾ ${numberAlign(contextMenus.length)} context menus`), 'info', true) + + // events + const events = MetadataStorage.instance.events + + this.console(chalk.magenta(`${symbol} ${numberAlign(events.length)} ${chalk.bold('events')} loaded`), 'info', true) + + // entities + const entities = fs.readdirSync(path.join(__dirname, '..', 'entities')) + .filter(entity => + !entity.startsWith('index') + && !entity.startsWith('BaseEntity') + ) + + const pluginsEntitesCount = this.pluginsManager.plugins.reduce((acc, plugin) => acc + Object.values(plugin.entities).length, 0) + + this.console(chalk.red(`${symbol} ${numberAlign(entities.length + pluginsEntitesCount)} ${chalk.bold('entities')} loaded`), 'info', true) + + // services + const services = fs.readdirSync(path.join(__dirname, '..', 'services')) + .filter(service => !service.startsWith('index')) + + const pluginsServicesCount = this.pluginsManager.plugins.reduce((acc, plugin) => acc + Object.values(plugin.services).length, 0) + + this.console(chalk.yellow(`${symbol} ${numberAlign(services.length + pluginsServicesCount)} ${chalk.bold('services')} loaded`), 'info', true) + + // api + if (apiConfig.enabled) { + const endpointsCount = Object.values(controllers).reduce((acc, controller) => { + const methodsName = Object + .getOwnPropertyNames(controller.prototype) + .filter(methodName => methodName !== 'constructor') + + return acc + methodsName.length + }, 0) + + this.console(chalk.cyan(`${symbol} ${numberAlign(endpointsCount)} ${chalk.bold('api endpoints')} loaded`), 'info', true) + } + + // scheduled jobs + const scheduledJobs = this.scheduler.jobs.size + + this.console(chalk.green(`${symbol} ${numberAlign(scheduledJobs)} ${chalk.bold('scheduled jobs')} loaded`), 'info', true) + + // plugins + const pluginsCount = this.pluginsManager.plugins.length + + this.console(chalk.hex('#47d188')(`${symbol} ${numberAlign(pluginsCount)} ${chalk.bold(`plugin${pluginsCount > 1 ? 's' : ''}`)} loaded`), 'info', true) + + // connected + if (apiConfig.enabled) { + this.console(chalk.gray(boxen( + ` API Server listening on port ${chalk.bold(apiConfig.port)} `, + { + padding: 0, + margin: { + top: 1, + bottom: 0, + left: 1, + right: 1, + }, + borderStyle: 'round', + dimBorder: true, + } + )), 'info', true) + } + + this.console(chalk.hex('7289DA')(boxen( + ` ${this.client.user ? `${chalk.bold(this.client.user.tag)}` : 'Bot'} is ${chalk.green('connected')}! `, + { + padding: 0, + margin: { + top: 1, + bottom: 1, + left: 1 * 3, + right: 1 * 3, + }, + borderStyle: 'round', + dimBorder: true, + } + )), 'info', true) + } + +} diff --git a/src/services/Pastebin.ts b/src/services/Pastebin.ts index cab237ce..b676e3df 100644 --- a/src/services/Pastebin.ts +++ b/src/services/Pastebin.ts @@ -1,68 +1,65 @@ -import dayjs from "dayjs" -import { Paste, RentryClient } from "rentry-pastebin" -import { singleton } from "tsyringe" +import dayjs from 'dayjs' +import { Paste, RentryClient } from 'rentry-pastebin' +import { singleton } from 'tsyringe' -import { Schedule } from "@decorators" -import { Pastebin as PastebinEntity } from "@entities" -import { Database } from "@services" +import { Schedule } from '@/decorators' +import { Pastebin as PastebinEntity } from '@/entities' +import { Database } from '@/services' @singleton() export class Pastebin { - private client: RentryClient = new RentryClient() - constructor( - private db: Database, - ) { - this.client.createToken() - } + private client: RentryClient = new RentryClient() - private async waitForToken(): Promise { + constructor( + private db: Database + ) { + this.client.createToken() + } - while (!this.client.getToken()) { - await new Promise(resolve => setTimeout(resolve, 100)) - } - } + private async waitForToken(): Promise { + while (!this.client.getToken()) + await new Promise(resolve => setTimeout(resolve, 100)) + } - async createPaste(content: string, lifetime?: number): Promise { + async createPaste(content: string, lifetime?: number): Promise { + await this.waitForToken() - await this.waitForToken() + const paste = await this.client.createPaste({ content }) - const paste = await this.client.createPaste({ content }) + const pasteEntity = new PastebinEntity() + pasteEntity.id = paste.url + pasteEntity.editCode = paste.editCode + if (lifetime) + pasteEntity.lifetime = Math.floor(lifetime) - let pasteEntity = new PastebinEntity() - pasteEntity.id = paste.url - pasteEntity.editCode = paste.editCode - if (lifetime) pasteEntity.lifetime = Math.floor(lifetime) - - await this.db.get(PastebinEntity).persistAndFlush(pasteEntity) - - return paste.paste - } + await this.db.get(PastebinEntity).persistAndFlush(pasteEntity) - async deletePaste(id: string): Promise { + return paste.paste + } - await this.waitForToken() - - const paste = await this.db.get(PastebinEntity).findOne({ id }) + async deletePaste(id: string): Promise { + await this.waitForToken() - if (!paste) return + const paste = await this.db.get(PastebinEntity).findOne({ id }) - await this.client.deletePaste(id, paste.editCode) - await this.db.get(PastebinEntity).remove(paste) - } + if (!paste) + return - @Schedule('*/30 * * * *') - private async autoDelete(): Promise { - - const pastes = await this.db.get(PastebinEntity).find({ lifetime: { $gt: 0 } }) + await this.client.deletePaste(id, paste.editCode) + await this.db.get(PastebinEntity).remove(paste) + } - for (const paste of pastes) { - - const diff = dayjs().diff(dayjs(paste.createdAt), 'day') + @Schedule('*/30 * * * *') + private async autoDelete(): Promise { + const pastes = await this.db.get(PastebinEntity).find({ lifetime: { $gt: 0 } }) - if (diff >= paste.lifetime) { - await this.client.deletePaste(paste.id, paste.editCode) - } - } - } -} \ No newline at end of file + for (const paste of pastes) { + const diff = dayjs().diff(dayjs(paste.createdAt), 'day') + + if (diff >= paste.lifetime) + await this.client.deletePaste(paste.id, paste.editCode) + } + } + +} diff --git a/src/services/PluginsManager.ts b/src/services/PluginsManager.ts index 41c22f9a..53356af1 100644 --- a/src/services/PluginsManager.ts +++ b/src/services/PluginsManager.ts @@ -1,127 +1,124 @@ -import { resolve } from "@discordx/importer" -import { AnyEntity, EntityClass } from "@mikro-orm/core" -import fs from "fs" -import { sep } from "node:path" -import { singleton } from "tsyringe" -import { BaseTranslation } from "typesafe-i18n" -import { ImportLocaleMapping, storeTranslationsToDisk } from "typesafe-i18n/importer" - -import { locales } from "@i18n" -import { BaseController, Plugin } from "@utils/classes" -import { getSourceCodeLocation } from "@utils/functions" -import { Store } from "@services" +import fs from 'node:fs' +import { sep } from 'node:path' -@singleton() -export class PluginsManager { - - private _plugins: Plugin[] = [] - - constructor( - private store: Store - ) {} +import { resolve } from '@discordx/importer' +import { AnyEntity, EntityClass } from '@mikro-orm/core' - public async loadPlugins(): Promise { +import { singleton } from 'tsyringe' +import { BaseTranslation } from 'typesafe-i18n' +import { ImportLocaleMapping, storeTranslationsToDisk } from 'typesafe-i18n/importer' - const pluginPaths = await resolve(`${getSourceCodeLocation()}/plugins/*`) +import { locales } from '@/i18n' +import { Store } from '@/services' +import { BaseController, Plugin } from '@/utils/classes' +import { getSourceCodeLocation } from '@/utils/functions' - for (const path of pluginPaths) { - - const plugin = new Plugin(path) - await plugin.load() +@singleton() +export class PluginsManager { - if (plugin.isValid()) this.plugins.push(plugin) - } - } + private _plugins: Plugin[] = [] - public getEntities(): EntityClass[] { - return this._plugins.map(plugin => Object.values(plugin.entities)).flat() - } + constructor( + private store: Store + ) {} - public getControllers(): typeof BaseController[] { - return this._plugins.map(plugin => Object.values(plugin.controllers)).flat() - } + public async loadPlugins(): Promise { + const pluginPaths = await resolve(`${getSourceCodeLocation()}/plugins/*`) - public async importCommands(): Promise { - for (const plugin of this._plugins) await plugin.importCommands() - } + for (const path of pluginPaths) { + const plugin = new Plugin(path) + await plugin.load() - public async importEvents(): Promise { - for (const plugin of this._plugins) await plugin.importEvents() - } + if (plugin.isValid()) + this.plugins.push(plugin) + } + } - public async initServices(): Promise<{ [key: string]: any }> { + public getEntities(): EntityClass[] { + return this._plugins.map(plugin => Object.values(plugin.entities)).flat() + } - let services: { [key: string]: any } = {} + public getControllers(): typeof BaseController[] { + return this._plugins.map(plugin => Object.values(plugin.controllers)).flat() + } - for (const plugin of this._plugins) { + public async importCommands(): Promise { + for (const plugin of this._plugins) await plugin.importCommands() + } - for (const service in plugin.services) { - - services[service] = new plugin.services[service]() - } - } + public async importEvents(): Promise { + for (const plugin of this._plugins) await plugin.importEvents() + } - return services - } + public async initServices(): Promise<{ [key: string]: any }> { + const services: { [key: string]: any } = {} - public async execMains(): Promise { + for (const plugin of this._plugins) { + for (const service in plugin.services) - for (const plugin of this._plugins) { - await plugin.execMain() - } - } + services[service] = new plugin.services[service]() + } - public async syncTranslations(): Promise { + return services + } - let localeMapping: ImportLocaleMapping[] = [] - let namespaces: { [key: string]: string[] } = {} - let translations: { [key: string]: BaseTranslation } = {} + public async execMains(): Promise { + for (const plugin of this._plugins) + await plugin.execMain() + } - for (const locale of locales) { - const path = getSourceCodeLocation() + '/i18n/' + locale - if (fs.existsSync(path)) translations[locale] = (await import(path))?.default - } + public async syncTranslations(): Promise { + const localeMapping: ImportLocaleMapping[] = [] + const namespaces: { [key: string]: string[] } = {} + const translations: { [key: string]: BaseTranslation } = {} - for (const plugin of this._plugins) { + for (const locale of locales) { + const path = `${getSourceCodeLocation()}/i18n/${locale}` + if (fs.existsSync(path)) + translations[locale] = (await import(path))?.default + } - for (const locale in plugin.translations) { - - if (!translations[locale]) translations[locale] = {} - if (!namespaces[locale]) namespaces[locale] = [] + for (const plugin of this._plugins) { + for (const locale in plugin.translations) { + if (!translations[locale]) + translations[locale] = {} + if (!namespaces[locale]) + namespaces[locale] = [] - translations[locale] = { ...translations[locale], [plugin.name]: plugin.translations[locale] } - namespaces[locale].push(plugin.name) - } - } + translations[locale] = { ...translations[locale], [plugin.name]: plugin.translations[locale] } + namespaces[locale].push(plugin.name) + } + } - for (const locale in translations) { + for (const locale in translations) { + if (!locales.includes(locale as any)) + continue - if (!locales.includes(locale as any)) continue + localeMapping.push({ + locale, + translations: translations[locale], + namespaces: namespaces[locale], + }) + } - localeMapping.push({ - locale, - translations: translations[locale], - namespaces: namespaces[locale] - }) - } + const pluginsName = this._plugins.map(plugin => plugin.name) - const pluginsName = this._plugins.map(plugin => plugin.name) + for (const path of await resolve(`${getSourceCodeLocation()}/i18n/*/*/index.ts`)) { + const name = path.split(sep).at(-2) || '' - for (const path of await resolve(getSourceCodeLocation() + '/i18n/*/*/index.ts')) { + if (!pluginsName.includes(name)) + await fs.rmSync(path.slice(0, -8), { recursive: true, force: true }) + } - const name = path.split(sep).at(-2) || "" - - if (!pluginsName.includes(name)) { - await fs.rmSync(path.slice(0, -8), { recursive: true, force: true }) - } - } + await storeTranslationsToDisk(localeMapping, true) + } - await storeTranslationsToDisk(localeMapping, true) - } + public isPluginLoad(pluginName: string): boolean { + return this._plugins.findIndex(plugin => plugin.name === pluginName) !== -1 + } - public isPluginLoad(pluginName: string): boolean { - return this._plugins.findIndex(plugin => plugin.name === pluginName) !== -1 - } + get plugins() { + return this._plugins + } - get plugins() { return this._plugins } -} \ No newline at end of file +} diff --git a/src/services/Scheduler.ts b/src/services/Scheduler.ts index 30eff88b..388c0ec8 100644 --- a/src/services/Scheduler.ts +++ b/src/services/Scheduler.ts @@ -1,33 +1,33 @@ -import { CronJob } from "cron" -import { singleton } from "tsyringe" +import { CronJob } from 'cron' +import { singleton } from 'tsyringe' @singleton() export class Scheduler { - private _jobs: Map = new Map() + private _jobs: Map = new Map() - get jobs() { - return this._jobs - } + get jobs() { + return this._jobs + } - addJob(jobName: string, job: CronJob) { - this._jobs.set(jobName, job) - } + addJob(jobName: string, job: CronJob) { + this._jobs.set(jobName, job) + } - startJob(jobName: string) { - this._jobs.get(jobName)?.start() - } + startJob(jobName: string) { + this._jobs.get(jobName)?.start() + } - stopJob(jobName: string) { - this._jobs.get(jobName)?.stop() - } + stopJob(jobName: string) { + this._jobs.get(jobName)?.stop() + } - stopAllJobs() { - this._jobs.forEach(job => job.stop()) - } + stopAllJobs() { + this._jobs.forEach(job => job.stop()) + } - startAllJobs() { - this._jobs.forEach(job => job.start()) - } + startAllJobs() { + this._jobs.forEach(job => job.start()) + } -} \ No newline at end of file +} diff --git a/src/services/Stats.ts b/src/services/Stats.ts index 0b744b97..ca9be77d 100644 --- a/src/services/Stats.ts +++ b/src/services/Stats.ts @@ -1,396 +1,387 @@ -import { EntityRepository } from "@mikro-orm/core" -import { constant } from "case" -import { Client, SimpleCommandMessage } from "discordx" -import osu from "node-os-utils" -import pidusage from "pidusage" -import { delay, inject, singleton } from "tsyringe" - -import { statsConfig } from "@configs" -import { Schedule } from "@decorators" -import { Guild, Stat, User } from "@entities" -import { Database } from "@services" -import { datejs, formatDate, getTypeOfInteraction, resolveAction, resolveChannel, resolveGuild, resolveUser } from "@utils/functions" - -const allInteractions = { - $or: [ - { type: 'SIMPLE_COMMAND_MESSAGE' }, - { type: 'CHAT_INPUT_COMMAND_INTERACTION' }, - { type: 'USER_CONTEXT_MENU_COMMAND_INTERACTION' }, - { type: 'MESSAGE_CONTEXT_MENU_COMMAND_INTERACTION' }, - ] +import process from 'node:process' + +import { EntityRepository } from '@mikro-orm/core' + +import { constant } from 'case' +import { Client, SimpleCommandMessage } from 'discordx' +import osu from 'node-os-utils' +import pidusage from 'pidusage' +import { delay, inject, singleton } from 'tsyringe' + +import { statsConfig } from '@/configs' +import { Schedule } from '@/decorators' +import { Guild, Stat, User } from '@/entities' +import { Database } from '@/services' +import { datejs, formatDate, getTypeOfInteraction, resolveAction, resolveChannel, resolveGuild, resolveUser } from '@/utils/functions' + +const allInteractions = { + $or: [ + { type: 'SIMPLE_COMMAND_MESSAGE' }, + { type: 'CHAT_INPUT_COMMAND_INTERACTION' }, + { type: 'USER_CONTEXT_MENU_COMMAND_INTERACTION' }, + { type: 'MESSAGE_CONTEXT_MENU_COMMAND_INTERACTION' }, + ], } @singleton() export class Stats { - private statsRepo: EntityRepository - - constructor( - private db: Database, - @inject(delay(() => Client)) private client: Client, - ) { - this.statsRepo = this.db.get(Stat) - } - - /** - * Add an entry to the stats table. - * @param type - * @param value - * @param additionalData in JSON format - */ - async register(type: string, value: string, additionalData?: any) { - - const stat = new Stat() - stat.type = type - stat.value = value - if (additionalData) stat.additionalData = additionalData - - await this.statsRepo.persistAndFlush(stat) - } - - /** - * Record an interaction and add it to the database. - * @param interaction - */ - async registerInteraction(interaction: AllInteractions) { - - // we extract data from the interaction - const type = constant(getTypeOfInteraction(interaction)) as InteractionsConstants - if (statsConfig.interaction.exclude.includes(type)) return - - const value = resolveAction(interaction) - const additionalData = { - user: resolveUser(interaction)?.id, - guild: resolveGuild(interaction)?.id || 'dm', - channel: resolveChannel(interaction)?.id - } - - // add it to the db - await this.register(type, value, additionalData) - } - - /** - * Record a simple command message and add it to the database. - * @param command - */ - async registerSimpleCommand(command: SimpleCommandMessage) { - - // we extract data from the interaction - const type = 'SIMPLE_COMMAND_MESSAGE' - const value = command.name - const additionalData = { - user: command.message.author.id, - guild: command.message.guild?.id || 'dm', - channel: command.message.channel?.id - } - - // add it to the db - await this.register(type, value, additionalData) - } - - /** - * Returns an object with the total stats for each type. - */ - async getTotalStats() { - - const totalStatsObj = { - TOTAL_USERS: this.client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0), - TOTAL_GUILDS: this.client.guilds.cache.size, - TOTAL_ACTIVE_USERS: await this.db.get(User).count(), - TOTAL_COMMANDS: await this.statsRepo.count(allInteractions) - } - - return totalStatsObj - } - - /** - * Get the last saved interaction. - */ - async getLastInteraction() { - - const lastInteraction = await this.statsRepo.findOne(allInteractions, { - orderBy: { createdAt: 'DESC' } - }) - - return lastInteraction - } - - /** - * Get the last guild added to the database. - */ - async getLastGuildAdded() { - - const guilds = await this.db.get(Guild).find({}, { - orderBy: { createdAt: 'DESC' } - }) - - return guilds[0] - } - - /** - * Get commands sorted by total amount of uses in DESC order. - */ - async getTopCommands() { - - if ('createQueryBuilder' in this.db.em) { - - // @ts-ignore - const qb = this.db.em.createQueryBuilder(Stat) - const query = qb - .select(['type', 'value as name', 'count(*) as count']) - .where(allInteractions) - .groupBy(['type', 'value']) - - const slashCommands = await query.execute() - - return slashCommands.sort((a: any, b: any) => b.count - a.count) - - } else if ('aggregate' in this.db.em) { - - // @ts-ignore - const slashCommands = await this.db.em.aggregate(Stat, [ - { - $match: allInteractions - }, - { - '$group': { - _id : { type: '$type', value: '$value' }, - count: { '$sum': 1 } - } - }, - { - '$replaceRoot': { - newRoot: { - '$mergeObjects': [ - '$_id', - { count: '$count' } - ] - } - } - } - ]) - - return slashCommands.sort((a: any, b: any) => b.count - a.count) - } else return [] - } - - /** - * Get the users activity per slice of interactions amount in percentage. - */ - async getUsersActivity() { - - const usersActivity = { - '1-10': 0, - '11-50': 0, - '51-100': 0, - '101-1000': 0, - '>1000': 0 - } - - const users = await this.db.get(User).findAll() - - for (const user of users) { - - const commandsCount = await this.db.get(Stat).count({ - ...allInteractions, - additionalData: { - user: user.id - } - }) - - if (commandsCount <= 10) usersActivity['1-10']++ - else if (commandsCount <= 50) usersActivity['11-50']++ - else if (commandsCount <= 100) usersActivity['51-100']++ - else if (commandsCount <= 1000) usersActivity['101-1000']++ - else usersActivity['>1000']++ - } - - return usersActivity - } - - /** - * Get guilds sorted by total amount of commands in DESC order. - */ - async getTopGuilds() { - - const topGuilds: { - id: string, - name: string, - totalCommands: number - }[] = [] - - const guilds = await this.db.get(Guild).getActiveGuilds() - - for (const guild of guilds) { - - const discordGuild = await this.client.guilds.fetch(guild.id).catch(() => null) - if (!discordGuild) continue - - const commandsCount = await this.db.get(Stat).count({ - ...allInteractions, - additionalData: { - guild: guild.id - } - }) - - topGuilds.push({ - id: guild.id, - name: discordGuild?.name || '', - totalCommands: commandsCount - }) - } - - return topGuilds.sort((a, b) => b.totalCommands - a.totalCommands) - } - - /** - * Returns the amount of row for a given type per day in a given interval of days from now. - * @param type the type of the stat to retrieve - * @param days interval of days from now - */ - async countStatsPerDays(type: string, days: number): Promise { - - const now = Date.now() - const stats: StatPerInterval = [] - - for (let i = 0; i < days; i++) { - - const date = new Date(now - (i * 24 * 60 * 60 * 1000)) - const statCount = await this.getCountForGivenDay(type, date) - - stats.push({ - date: formatDate(date, 'onlyDate'), - count: statCount - }) - } - - return this.cumulateStatPerInterval(stats) - } - - /** - * Transform individual day stats into cumulated stats. - * @param stats - */ - cumulateStatPerInterval(stats: StatPerInterval): StatPerInterval { - - const cumulatedStats = - stats - .reverse() - .reduce((acc, stat, i) => { - - if (acc.length === 0) acc.push(stat) - else acc.push({ - date: stat.date, - count: acc[i - 1].count + stat.count - }) - - return acc - }, [] as StatPerInterval) - .reverse() - - return cumulatedStats - } - - /** - * Sum two array of stats. - * @param stats1 - * @param stats2 - */ - sumStats(stats1: StatPerInterval, stats2: StatPerInterval): StatPerInterval { - - const allDays = [...new Set(stats1.concat(stats2).map(stat => stat.date))] - .sort((a, b) => { - var aa = a.split('/').reverse().join(), - bb = b.split('/').reverse().join() - return aa < bb ? -1 : (aa > bb ? 1 : 0) - }) - - const sumStats = allDays.map(day => ({ - date: day, - count: - (stats1.find(stat => stat.date === day)?.count || 0) - + (stats2.find(stat => stat.date === day)?.count || 0) - })) - - return sumStats - } - - /** - * Returns the total count of row for a given type at a given day. - * @param type - * @param date - day to get the stats for (any time of the day will work as it extract the very beginning and the very ending of the day as the two limits) - */ - async getCountForGivenDay(type: string, date: Date): Promise { - - const start = datejs(date).startOf('day').toDate() - const end = datejs(date).endOf('day').toDate() - - const stats = await this.statsRepo.find({ - type, - createdAt: { - $gte: start, - $lte: end - } - }) - - return stats.length - } - - /** - * Get the current process usage (CPU, RAM, etc). - */ - async getPidUsage() { - - const pidUsage = await pidusage(process.pid) - - return { - ...pidUsage, - cpu: pidUsage.cpu.toFixed(1), - memory: { - usedInMb: (pidUsage.memory / (1024 * 1024)).toFixed(1), - percentage: (pidUsage.memory / osu.mem.totalMem() * 100).toFixed(1) - } - } - } - - /** - * Get the current host health (CPU, RAM, etc). - */ - async getHostUsage() { - - return { - cpu: await osu.cpu.usage(), - memory: await osu.mem.info(), - os: await osu.os.oos(), - uptime: await osu.os.uptime(), - hostname: await osu.os.hostname(), - platform: await osu.os.platform() - // drive: osu.drive.info(), - } - } - - /** - * Get latency from the discord websocket gate. - */ - getLatency() { - - return { - ping: this.client.ws.ping - } - } - - /** - * Run each day at 23:59 to update daily stats. - */ - @Schedule('59 59 23 * * *') - async registerDailyStats() { - - const totalStats = await this.getTotalStats() - - for (const type of Object.keys(totalStats)) { - const value = JSON.stringify(totalStats[type as keyof typeof totalStats]) - await this.register(type, value) - } - } - -} \ No newline at end of file + private statsRepo: EntityRepository + + constructor( + private db: Database, + @inject(delay(() => Client)) private client: Client + ) { + this.statsRepo = this.db.get(Stat) + } + + /** + * Add an entry to the stats table. + * @param type + * @param value + * @param additionalData in JSON format + */ + async register(type: string, value: string, additionalData?: any) { + const stat = new Stat() + stat.type = type + stat.value = value + if (additionalData) + stat.additionalData = additionalData + + await this.statsRepo.persistAndFlush(stat) + } + + /** + * Record an interaction and add it to the database. + * @param interaction + */ + async registerInteraction(interaction: AllInteractions) { + // we extract data from the interaction + const type = constant(getTypeOfInteraction(interaction)) as InteractionsConstants + if (statsConfig.interaction.exclude.includes(type)) + return + + const value = resolveAction(interaction) + const additionalData = { + user: resolveUser(interaction)?.id, + guild: resolveGuild(interaction)?.id || 'dm', + channel: resolveChannel(interaction)?.id, + } + + // add it to the db + await this.register(type, value, additionalData) + } + + /** + * Record a simple command message and add it to the database. + * @param command + */ + async registerSimpleCommand(command: SimpleCommandMessage) { + // we extract data from the interaction + const type = 'SIMPLE_COMMAND_MESSAGE' + const value = command.name + const additionalData = { + user: command.message.author.id, + guild: command.message.guild?.id || 'dm', + channel: command.message.channel?.id, + } + + // add it to the db + await this.register(type, value, additionalData) + } + + /** + * Returns an object with the total stats for each type. + */ + async getTotalStats() { + const totalStatsObj = { + TOTAL_USERS: this.client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0), + TOTAL_GUILDS: this.client.guilds.cache.size, + TOTAL_ACTIVE_USERS: await this.db.get(User).count(), + TOTAL_COMMANDS: await this.statsRepo.count(allInteractions), + } + + return totalStatsObj + } + + /** + * Get the last saved interaction. + */ + async getLastInteraction() { + const lastInteraction = await this.statsRepo.findOne(allInteractions, { + orderBy: { createdAt: 'DESC' }, + }) + + return lastInteraction + } + + /** + * Get the last guild added to the database. + */ + async getLastGuildAdded() { + const guilds = await this.db.get(Guild).find({}, { + orderBy: { createdAt: 'DESC' }, + }) + + return guilds[0] + } + + /** + * Get commands sorted by total amount of uses in DESC order. + */ + async getTopCommands() { + if ('createQueryBuilder' in this.db.em) { + const qb = this.db.em.createQueryBuilder(Stat) + const query = qb + .select(['type', 'value as name', 'count(*) as count']) + .where(allInteractions) + .groupBy(['type', 'value']) + + const slashCommands = await query.execute() + + return slashCommands.sort((a: any, b: any) => b.count - a.count) + } else if ('aggregate' in this.db.em) { + // @ts-expect-error - aggregate is not in the types + const slashCommands = await this.db.em.aggregate(Stat, [ + { + $match: allInteractions, + }, + { + $group: { + _id: { type: '$type', value: '$value' }, + count: { $sum: 1 }, + }, + }, + { + $replaceRoot: { + newRoot: { + $mergeObjects: [ + '$_id', + { count: '$count' }, + ], + }, + }, + }, + ]) + + return slashCommands.sort((a: any, b: any) => b.count - a.count) + } else { + return [] + } + } + + /** + * Get the users activity per slice of interactions amount in percentage. + */ + async getUsersActivity() { + const usersActivity = { + '1-10': 0, + '11-50': 0, + '51-100': 0, + '101-1000': 0, + '>1000': 0, + } + + const users = await this.db.get(User).findAll() + + for (const user of users) { + const commandsCount = await this.db.get(Stat).count({ + ...allInteractions, + additionalData: { + user: user.id, + }, + }) + + if (commandsCount <= 10) + usersActivity['1-10']++ + else if (commandsCount <= 50) + usersActivity['11-50']++ + else if (commandsCount <= 100) + usersActivity['51-100']++ + else if (commandsCount <= 1000) + usersActivity['101-1000']++ + else usersActivity['>1000']++ + } + + return usersActivity + } + + /** + * Get guilds sorted by total amount of commands in DESC order. + */ + async getTopGuilds() { + const topGuilds: { + id: string + name: string + totalCommands: number + }[] = [] + + const guilds = await this.db.get(Guild).getActiveGuilds() + + for (const guild of guilds) { + const discordGuild = await this.client.guilds.fetch(guild.id).catch(() => null) + if (!discordGuild) + continue + + const commandsCount = await this.db.get(Stat).count({ + ...allInteractions, + additionalData: { + guild: guild.id, + }, + }) + + topGuilds.push({ + id: guild.id, + name: discordGuild?.name || '', + totalCommands: commandsCount, + }) + } + + return topGuilds.sort((a, b) => b.totalCommands - a.totalCommands) + } + + /** + * Returns the amount of row for a given type per day in a given interval of days from now. + * @param type the type of the stat to retrieve + * @param days interval of days from now + */ + async countStatsPerDays(type: string, days: number): Promise { + const now = Date.now() + const stats: StatPerInterval = [] + + for (let i = 0; i < days; i++) { + const date = new Date(now - (i * 24 * 60 * 60 * 1000)) + const statCount = await this.getCountForGivenDay(type, date) + + stats.push({ + date: formatDate(date, 'onlyDate'), + count: statCount, + }) + } + + return this.cumulateStatPerInterval(stats) + } + + /** + * Transform individual day stats into cumulated stats. + * @param stats + */ + cumulateStatPerInterval(stats: StatPerInterval): StatPerInterval { + const cumulatedStats = stats + .reverse() + .reduce((acc, stat, i) => { + if (acc.length === 0) { + acc.push(stat) + } else { + acc.push({ + date: stat.date, + count: acc[i - 1].count + stat.count, + }) + } + + return acc + }, [] as StatPerInterval) + .reverse() + + return cumulatedStats + } + + /** + * Sum two array of stats. + * @param stats1 + * @param stats2 + */ + sumStats(stats1: StatPerInterval, stats2: StatPerInterval): StatPerInterval { + const allDays = [...new Set(stats1.concat(stats2).map(stat => stat.date))] + .sort((a, b) => { + const aa = a.split('/').reverse().join() + const bb = b.split('/').reverse().join() + + return aa < bb ? -1 : (aa > bb ? 1 : 0) + }) + + const sumStats = allDays.map(day => ({ + date: day, + count: + (stats1.find(stat => stat.date === day)?.count || 0) + + (stats2.find(stat => stat.date === day)?.count || 0), + })) + + return sumStats + } + + /** + * Returns the total count of row for a given type at a given day. + * @param type + * @param date - day to get the stats for (any time of the day will work as it extract the very beginning and the very ending of the day as the two limits) + */ + async getCountForGivenDay(type: string, date: Date): Promise { + const start = datejs(date).startOf('day').toDate() + const end = datejs(date).endOf('day').toDate() + + const stats = await this.statsRepo.find({ + type, + createdAt: { + $gte: start, + $lte: end, + }, + }) + + return stats.length + } + + /** + * Get the current process usage (CPU, RAM, etc). + */ + async getPidUsage() { + const pidUsage = await pidusage(process.pid) + + return { + ...pidUsage, + cpu: pidUsage.cpu.toFixed(1), + memory: { + usedInMb: (pidUsage.memory / (1024 * 1024)).toFixed(1), + percentage: (pidUsage.memory / osu.mem.totalMem() * 100).toFixed(1), + }, + } + } + + /** + * Get the current host health (CPU, RAM, etc). + */ + async getHostUsage() { + return { + cpu: await osu.cpu.usage(), + memory: await osu.mem.info(), + os: await osu.os.oos(), + uptime: await osu.os.uptime(), + hostname: await osu.os.hostname(), + platform: await osu.os.platform(), + + // drive: osu.drive.info(), + } + } + + /** + * Get latency from the discord websocket gate. + */ + getLatency() { + return { + ping: this.client.ws.ping, + } + } + + /** + * Run each day at 23:59 to update daily stats. + */ + @Schedule('59 59 23 * * *') + async registerDailyStats() { + const totalStats = await this.getTotalStats() + + for (const type of Object.keys(totalStats)) { + const value = JSON.stringify(totalStats[type as keyof typeof totalStats]) + await this.register(type, value) + } + } + +} diff --git a/src/services/Store.ts b/src/services/Store.ts index 0e70274c..23f56ab0 100644 --- a/src/services/Store.ts +++ b/src/services/Store.ts @@ -1,31 +1,33 @@ -import { apiConfig } from "@configs" -import { Store as RxStore } from "rxeta" -import { singleton } from "tsyringe" +import { Store as RxStore } from 'rxeta' +import { singleton } from 'tsyringe' + +import { apiConfig } from '@/configs' interface State { - authorizedAPITokens: string[] - botHasBeenReloaded: boolean - ready: { - bot: boolean | null - api: boolean | null - } + authorizedAPITokens: string[] + botHasBeenReloaded: boolean + ready: { + bot: boolean | null + api: boolean | null + } } const initialState: State = { - - authorizedAPITokens: [], - botHasBeenReloaded: false, - ready: { - bot: false, - api: apiConfig.enabled ? false : null, - } + + authorizedAPITokens: [], + botHasBeenReloaded: false, + ready: { + bot: false, + api: apiConfig.enabled ? false : null, + }, } @singleton() export class Store extends RxStore { - constructor() { - super(initialState) - } -} \ No newline at end of file + constructor() { + super(initialState) + } + +} diff --git a/src/services/index.ts b/src/services/index.ts index b998cc41..79e2e238 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -7,4 +7,4 @@ export * from './ErrorHandler' export * from './Scheduler' export * from './Pastebin' export * from './PluginsManager' -export * from './EventManager' \ No newline at end of file +export * from './EventManager' diff --git a/src/utils/classes/BaseController.ts b/src/utils/classes/BaseController.ts index c53cdb42..2f4f17b8 100644 --- a/src/utils/classes/BaseController.ts +++ b/src/utils/classes/BaseController.ts @@ -1,3 +1,3 @@ export abstract class BaseController { -} \ No newline at end of file +} diff --git a/src/utils/classes/BaseError.ts b/src/utils/classes/BaseError.ts index 99c41bba..4998beb1 100644 --- a/src/utils/classes/BaseError.ts +++ b/src/utils/classes/BaseError.ts @@ -1,20 +1,23 @@ -import { Logger } from "@services" -import { resolveDependency } from "@utils/functions" +import process from 'node:process' + +import { Logger } from '@/services' +import { resolveDependency } from '@/utils/functions' export abstract class BaseError extends Error { - protected logger: Logger + protected logger: Logger + + constructor(message?: string) { + super(message) + resolveDependency(Logger).then((logger) => { + this.logger = logger + }) + } - constructor(message?: string) { - super(message) - resolveDependency(Logger).then(logger => { - this.logger = logger - }) - } + handle() {} - handle() {} + kill() { + process.exit(1) + } - kill() { - process.exit(1) - } -} \ No newline at end of file +} diff --git a/src/utils/classes/Plugin.ts b/src/utils/classes/Plugin.ts index f13edc99..f4b4a9db 100644 --- a/src/utils/classes/Plugin.ts +++ b/src/utils/classes/Plugin.ts @@ -1,124 +1,162 @@ -import { importx, resolve } from "@discordx/importer" -import { AnyEntity, EntityClass } from "@mikro-orm/core" -import fs from "fs" -import semver from "semver" -import { sep } from "node:path" -import { BaseTranslation } from "typesafe-i18n" - -import { generalConfig } from "@configs" -import { locales } from "@i18n" -import { BaseController } from "@utils/classes" -import { getSourceCodeLocation, getTscordVersion } from "@utils/functions" +import fs from 'node:fs' +import { sep } from 'node:path' + +import { importx, resolve } from '@discordx/importer' +import { AnyEntity, EntityClass } from '@mikro-orm/core' + +import semver from 'semver' +import { BaseTranslation } from 'typesafe-i18n' + +import { locales } from '@/i18n' +import { BaseController } from '@/utils/classes' +import { getSourceCodeLocation, getTscordVersion } from '@/utils/functions' export class Plugin { - // Common values - private _path: string - private _name: string - private _version: string - private _valid: boolean = true - - // Specific values - private _entities: { [key: string]: EntityClass } - private _controllers: { [key: string]: typeof BaseController } - private _services: { [key: string]: any } - private _translations: { [key: string]: BaseTranslation } - - constructor(path: string) { - this._path = path - } - - public async load(): Promise { - - // check if the plugin.json is present - if (!await fs.existsSync(this._path + "/plugin.json")) return this.stopLoad("plugin.json not found") - - // read plugin.json - const pluginConfig = await import(this._path + "/plugin.json") - - // check if the plugin.json is valid - if (!pluginConfig.name) return this.stopLoad("Missing name in plugin.json") - if (!pluginConfig.version) return this.stopLoad("Missing version in plugin.json") - if (!pluginConfig.tscordRequiredVersion) return this.stopLoad("Missing tscordRequiredVersion in plugin.json") - - // check plugin.json values - if (!pluginConfig.name.match(/^[a-zA-Z0-9-_]+$/)) return this.stopLoad("Invalid name in plugin.json") - if (!semver.valid(pluginConfig.version)) return this.stopLoad("Invalid version in plugin.json") - - // check if the plugin is compatible with the current version of Tscord - if (!semver.satisfies(semver.coerce(getTscordVersion())!, pluginConfig.tscordRequiredVersion)) return this.stopLoad(`Incompatible with the current version of Tscord (v${getTscordVersion()})`) - - // assign common values - this._name = pluginConfig.name - this._version = pluginConfig.version - - // Load specific values - this._entities = await this.getEntities() - this._controllers = await this.getControllers() - this._services = await this.getServices() - this._translations = await this.getTranslations() - } - - private stopLoad(error: string): void { - this._valid = false - console.error(`Plugin ${this._name ? this._name : this._path } ${this._version ? "v" + this._version : ""} is not valid: ${error}`) - } - - private async getControllers(): Promise<{ [key: string]: typeof BaseController }> { - if (!fs.existsSync(this._path + "/api/controllers")) return {} - return import(this._path + "/api/controllers") - } - - private async getEntities(): Promise<{ [key: string]: EntityClass }> { - if (!fs.existsSync(this._path + "/entities")) return {} - return import(this._path + "/entities") - } - - private async getServices(): Promise<{ [key: string]: any }> { - if (!fs.existsSync(this._path + "/services")) return {} - return import(this._path + "/services") - } - - private async getTranslations(): Promise<{ [key: string]: BaseTranslation }> { - const translations: { [key: string]: BaseTranslation } = {} - - const localesPath = await resolve(this._path + "/i18n/*.{ts,js}") - for (const localeFile of localesPath) { - const locale = localeFile.split(sep).at(-1)?.split(".")[0] || "unknown" - - translations[locale] = (await import(localeFile)).default - } - - for (const defaultLocale of locales) { - const path = `${getSourceCodeLocation()}/i18n/${defaultLocale}/${this._name}/_custom.` - if (fs.existsSync(path + "js")) translations[defaultLocale] = (await import(path + "js")).default - else if (fs.existsSync(path + "ts")) translations[defaultLocale] = (await import(path + "ts")).default - } - - return translations - } - - public execMain(): void { - if (!fs.existsSync(this._path + "/main.ts")) return - import(this._path + "/main.ts") - } - - public async importCommands(): Promise { - await importx(this._path + "/commands/**/*.{ts,js}") - } - - public async importEvents(): Promise { - await importx(this._path + "/events/**/*.{ts,js}") - } - - public isValid(): boolean { return this._valid } - - get path() { return this._path } - get name() { return this._name } - get version() { return this._version } - - get entities() { return this._entities } - get controllers() { return this._controllers } - get services() { return this._services } - get translations() { return this._translations } -} \ No newline at end of file + // Common values + private _path: string + private _name: string + private _version: string + private _valid: boolean = true + + // Specific values + private _entities: { [key: string]: EntityClass } + private _controllers: { [key: string]: typeof BaseController } + private _services: { [key: string]: any } + private _translations: { [key: string]: BaseTranslation } + + constructor(path: string) { + this._path = path + } + + public async load(): Promise { + // check if the plugin.json is present + if (!await fs.existsSync(`${this._path}/plugin.json`)) + return this.stopLoad('plugin.json not found') + + // read plugin.json + const pluginConfig = await import(`${this._path}/plugin.json`) + + // check if the plugin.json is valid + if (!pluginConfig.name) + return this.stopLoad('Missing name in plugin.json') + if (!pluginConfig.version) + return this.stopLoad('Missing version in plugin.json') + if (!pluginConfig.tscordRequiredVersion) + return this.stopLoad('Missing tscordRequiredVersion in plugin.json') + + // check plugin.json values + if (!pluginConfig.name.match(/^[a-zA-Z0-9-_]+$/)) + return this.stopLoad('Invalid name in plugin.json') + if (!semver.valid(pluginConfig.version)) + return this.stopLoad('Invalid version in plugin.json') + + // check if the plugin is compatible with the current version of Tscord + if (!semver.satisfies(semver.coerce(getTscordVersion())!, pluginConfig.tscordRequiredVersion)) + return this.stopLoad(`Incompatible with the current version of Tscord (v${getTscordVersion()})`) + + // assign common values + this._name = pluginConfig.name + this._version = pluginConfig.version + + // Load specific values + this._entities = await this.getEntities() + this._controllers = await this.getControllers() + this._services = await this.getServices() + this._translations = await this.getTranslations() + } + + private stopLoad(error: string): void { + this._valid = false + console.error(`Plugin ${this._name ? this._name : this._path} ${this._version ? `v${this._version}` : ''} is not valid: ${error}`) + } + + private async getControllers(): Promise<{ [key: string]: typeof BaseController }> { + if (!fs.existsSync(`${this._path}/api/controllers`)) + return {} + + return import(`${this._path}/api/controllers`) + } + + private async getEntities(): Promise<{ [key: string]: EntityClass }> { + if (!fs.existsSync(`${this._path}/entities`)) + return {} + + return import(`${this._path}/entities`) + } + + private async getServices(): Promise<{ [key: string]: any }> { + if (!fs.existsSync(`${this._path}/services`)) + return {} + + return import(`${this._path}/services`) + } + + private async getTranslations(): Promise<{ [key: string]: BaseTranslation }> { + const translations: { [key: string]: BaseTranslation } = {} + + const localesPath = await resolve(`${this._path}/i18n/*.{ts,js}`) + for (const localeFile of localesPath) { + const locale = localeFile.split(sep).at(-1)?.split('.')[0] || 'unknown' + + translations[locale] = (await import(localeFile)).default + } + + for (const defaultLocale of locales) { + const path = `${getSourceCodeLocation()}/i18n/${defaultLocale}/${this._name}/_custom.` + if (fs.existsSync(`${path}js`)) + translations[defaultLocale] = (await import(`${path}js`)).default + else if (fs.existsSync(`${path}ts`)) + translations[defaultLocale] = (await import(`${path}ts`)).default + } + + return translations + } + + public execMain(): void { + if (!fs.existsSync(`${this._path}/main.ts`)) + return + import(`${this._path}/main.ts`) + } + + public async importCommands(): Promise { + await importx(`${this._path}/commands/**/*.{ts,js}`) + } + + public async importEvents(): Promise { + await importx(`${this._path}/events/**/*.{ts,js}`) + } + + public isValid(): boolean { + return this._valid + } + + get path() { + return this._path + } + + get name() { + return this._name + } + + get version() { + return this._version + } + + get entities() { + return this._entities + } + + get controllers() { + return this._controllers + } + + get services() { + return this._services + } + + get translations() { + return this._translations + } + +} diff --git a/src/utils/classes/index.ts b/src/utils/classes/index.ts index 60f842e5..a6c106ca 100644 --- a/src/utils/classes/index.ts +++ b/src/utils/classes/index.ts @@ -1,3 +1,3 @@ export * from './BaseError' export * from './BaseController' -export * from './Plugin' \ No newline at end of file +export * from './Plugin' diff --git a/src/utils/decorators/ContextMenu.ts b/src/utils/decorators/ContextMenu.ts index b2965b1f..c294403d 100644 --- a/src/utils/decorators/ContextMenu.ts +++ b/src/utils/decorators/ContextMenu.ts @@ -1,7 +1,7 @@ -import { ApplicationCommandType } from "discord.js" -import { ContextMenu as ContextMenuX } from "discordx" +import { ApplicationCommandType } from 'discord.js' +import { ContextMenu as ContextMenuX } from 'discordx' -import { constantPreserveDots, getCallerFile, sanitizeLocales, setOptionsLocalization } from "@utils/functions" +import { constantPreserveDots, getCallerFile, sanitizeLocales, setOptionsLocalization } from '@/utils/functions' /** * Interact with context menu with a defined identifier @@ -13,30 +13,33 @@ import { constantPreserveDots, getCallerFile, sanitizeLocales, setOptionsLocaliz * * @category Decorator */ -export const ContextMenu = (options: ContextMenuOptions) => { - - let localizationSource: TranslationsNestedPaths | null = null - const commandNameFromFile = getCallerFile(1)?.split('/').pop()?.split('.')[0] - - if (options.localizationSource) localizationSource = constantPreserveDots(options.localizationSource) as TranslationsNestedPaths - else if (options.name) localizationSource = 'COMMANDS.' + constantPreserveDots(options.name) as TranslationsNestedPaths - else if (commandNameFromFile) localizationSource = 'COMMANDS.' + constantPreserveDots(commandNameFromFile) as TranslationsNestedPaths - - if (localizationSource) { - - options = setOptionsLocalization({ - target: 'name', - options, - localizationSource, - nameFallback: commandNameFromFile - }) - } - - options = sanitizeLocales(options) - - // interop type string if any into enum types - if (options.type === 'USER') options.type = ApplicationCommandType.User - else if (options.type === 'MESSAGE') options.type = ApplicationCommandType.Message - - return ContextMenuX(options as ContextMenuOptionsX) +export function ContextMenu(options: ContextMenuOptions) { + let localizationSource: TranslationsNestedPaths | null = null + const commandNameFromFile = getCallerFile(1)?.split('/').pop()?.split('.')[0] + + if (options.localizationSource) + localizationSource = constantPreserveDots(options.localizationSource) as TranslationsNestedPaths + else if (options.name) + localizationSource = `COMMANDS.${constantPreserveDots(options.name)}` as TranslationsNestedPaths + else if (commandNameFromFile) + localizationSource = `COMMANDS.${constantPreserveDots(commandNameFromFile)}` as TranslationsNestedPaths + + if (localizationSource) { + options = setOptionsLocalization({ + target: 'name', + options, + localizationSource, + nameFallback: commandNameFromFile, + }) + } + + options = sanitizeLocales(options) + + // interop type string if any into enum types + if (options.type === 'USER') + options.type = ApplicationCommandType.User + else if (options.type === 'MESSAGE') + options.type = ApplicationCommandType.Message + + return ContextMenuX(options as ContextMenuOptionsX) } diff --git a/src/utils/decorators/On.ts b/src/utils/decorators/On.ts index 2ce175e2..bccdda0c 100644 --- a/src/utils/decorators/On.ts +++ b/src/utils/decorators/On.ts @@ -1,4 +1,4 @@ -import { DOn, EventOptions, MetadataStorage, MethodDecoratorEx } from "discordx" +import { DOn, EventOptions, MetadataStorage, MethodDecoratorEx } from 'discordx' /** * Handle both discord and custom events with a defined handler @@ -10,23 +10,21 @@ import { DOn, EventOptions, MetadataStorage, MethodDecoratorEx } from "discordx" * * @category Decorator */ -export const On = (event: string, options?: Omit): MethodDecoratorEx => { +export function On(event: string, options?: Omit): MethodDecoratorEx { + return function ( + target: Record, + key: string, + descriptor?: PropertyDescriptor + ) { + const clazz = target as unknown as new () => unknown + const on = DOn.create({ + botIds: options?.botIds, + event, + once: false, + rest: false, + priority: options?.priority, + }).decorate(clazz.constructor, key, descriptor?.value) - return function ( - target: Record, - key: string, - descriptor?: PropertyDescriptor - ) { - - const clazz = target as unknown as new () => unknown - const on = DOn.create({ - botIds: options?.botIds, - event: event, - once: false, - rest: false, - priority: options?.priority, - }).decorate(clazz.constructor, key, descriptor?.value) - - MetadataStorage.instance.addOn(on) - } -} \ No newline at end of file + MetadataStorage.instance.addOn(on) + } +} diff --git a/src/utils/decorators/OnCustom.ts b/src/utils/decorators/OnCustom.ts index 1cfcfe83..70ea65d5 100644 --- a/src/utils/decorators/OnCustom.ts +++ b/src/utils/decorators/OnCustom.ts @@ -1,26 +1,24 @@ -import { resolveDependency } from '@utils/functions' import { container, InjectionToken } from 'tsyringe' -export const OnCustom = (event: string) => { +import { resolveDependency } from '@/utils/functions' - return function ( +export function OnCustom(event: string) { + return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor - ) { + ) { + // associate the context to the function, with the injected dependencies defined + const oldDescriptor = descriptor.value + descriptor.value = function (...args: any[]) { + return this ? oldDescriptor.apply(container.resolve(this.constructor as InjectionToken), args) : oldDescriptor.apply(this, args) + } - // associate the context to the function, with the injected dependencies defined - const oldDescriptor = descriptor.value - descriptor.value = function(...args: any[]) { - return this ? oldDescriptor.apply(container.resolve(this.constructor as InjectionToken), args) : oldDescriptor.apply(this, args) - } + import('@/services').then(async ({ EventManager }) => { + const eventManager = await resolveDependency(EventManager) + const callback = descriptor.value.bind(target) - import('@services').then(async ({ EventManager }) => { - - const eventManager = await resolveDependency(EventManager) - const callback = descriptor.value.bind(target) - - eventManager.register(event, callback) - }) - } -} \ No newline at end of file + eventManager.register(event, callback) + }) + } +} diff --git a/src/utils/decorators/Once.ts b/src/utils/decorators/Once.ts index 2c4f4056..38e59472 100644 --- a/src/utils/decorators/Once.ts +++ b/src/utils/decorators/Once.ts @@ -1,4 +1,4 @@ -import { DOn, EventOptions, MetadataStorage, MethodDecoratorEx } from "discordx" +import { DOn, EventOptions, MetadataStorage, MethodDecoratorEx } from 'discordx' /** * Handle both discord and custom events only **once** with a defined handler @@ -10,22 +10,20 @@ import { DOn, EventOptions, MetadataStorage, MethodDecoratorEx } from "discordx" * * @category Decorator */ -export const Once = (event: string, options?: EventOptions): MethodDecoratorEx => { +export function Once(event: string, options?: EventOptions): MethodDecoratorEx { + return function ( + target: Record, + key: string, + descriptor?: PropertyDescriptor + ) { + const clazz = target as unknown as new () => unknown + const on = DOn.create({ + botIds: options?.botIds, + event, + once: true, + rest: false, + }).decorate(clazz.constructor, key, descriptor?.value) - return function ( - target: Record, - key: string, - descriptor?: PropertyDescriptor - ) { - - const clazz = target as unknown as new () => unknown - const on = DOn.create({ - botIds: options?.botIds, - event: event, - once: true, - rest: false - }).decorate(clazz.constructor, key, descriptor?.value) - - MetadataStorage.instance.addOn(on) - } -} \ No newline at end of file + MetadataStorage.instance.addOn(on) + } +} diff --git a/src/utils/decorators/Schedule.ts b/src/utils/decorators/Schedule.ts index 4dd29b16..25407a3f 100644 --- a/src/utils/decorators/Schedule.ts +++ b/src/utils/decorators/Schedule.ts @@ -1,43 +1,42 @@ -import { CronJob } from "cron" -import { isValidCron } from "cron-validator" -import { container, InjectionToken } from "tsyringe" +import { CronJob } from 'cron' +import { isValidCron } from 'cron-validator' +import { container, InjectionToken } from 'tsyringe' -import { generalConfig } from "@configs" -import { resolveDependency } from "@utils/functions" +import { generalConfig } from '@/configs' +import { resolveDependency } from '@/utils/functions' /** * Schedule a job to be executed at a specific time (cron) * @param cronExpression - cron expression to use (e.g: "0 0 * * *" will run each day at 00:00) * @param jobName - name of the job (the name of the function will be used if it is not provided) */ -export const Schedule = (cronExpression: string, jobName?: string) => { +export function Schedule(cronExpression: string, jobName?: string) { + if (!isValidCron(cronExpression, { alias: true, seconds: true })) + throw new Error(`Invalid cron expression: ${cronExpression}`) - if (!isValidCron(cronExpression, { alias: true, seconds: true })) throw new Error(`Invalid cron expression: ${cronExpression}`) - - return ( + return ( target: any, propertyKey: string, descriptor: PropertyDescriptor ) => { - - // associate the context to the function, with the injected dependencies defined - const oldDescriptor = descriptor.value - descriptor.value = function(...args: any[]) { - return oldDescriptor.apply(container.resolve(this.constructor as InjectionToken), args) - } - - const job = new CronJob( - cronExpression, - descriptor.value, - null, - false, - generalConfig.timezone, - target - ) + // associate the context to the function, with the injected dependencies defined + const oldDescriptor = descriptor.value + descriptor.value = function (...args: any[]) { + return oldDescriptor.apply(container.resolve(this.constructor as InjectionToken), args) + } + + const job = new CronJob( + cronExpression, + descriptor.value, + null, + false, + generalConfig.timezone, + target + ) - import('@services').then(async services => { - const scheduler = await resolveDependency(services.Scheduler) - scheduler.addJob(jobName ?? propertyKey, job) - }) + import('@/services').then(async (services) => { + const scheduler = await resolveDependency(services.Scheduler) + scheduler.addJob(jobName ?? propertyKey, job) + }) } -} \ No newline at end of file +} diff --git a/src/utils/decorators/Slash.ts b/src/utils/decorators/Slash.ts index 93b3c9ad..a6520399 100644 --- a/src/utils/decorators/Slash.ts +++ b/src/utils/decorators/Slash.ts @@ -1,6 +1,6 @@ -import { ApplicationCommandOptions as ApplicationCommandOptionsX, Slash as SlashX, VerifyName } from "discordx" +import { ApplicationCommandOptions as ApplicationCommandOptionsX, Slash as SlashX, VerifyName } from 'discordx' -import { constantPreserveDots, sanitizeLocales, setFallbackDescription, setOptionsLocalization } from "@utils/functions" +import { constantPreserveDots, sanitizeLocales, setFallbackDescription, setOptionsLocalization } from '@/utils/functions' /** * Handle a slash command @@ -11,34 +11,37 @@ import { constantPreserveDots, sanitizeLocales, setFallbackDescription, setOptio * * @category Decorator */ -export const Slash = (options?: ApplicationCommandOptions | string) => { - - if (!options) options = { } - else if (typeof options === 'string') options = { name: options } - - let localizationSource: TranslationsNestedPaths | null = null - - if (options.localizationSource) localizationSource = constantPreserveDots(options.localizationSource) as TranslationsNestedPaths - else if (options.name) localizationSource = 'COMMANDS.' + constantPreserveDots(options.name) as TranslationsNestedPaths - - if (localizationSource) { - - options = setOptionsLocalization({ - target: 'description', - options, - localizationSource, - }) - - options = setOptionsLocalization({ - target: 'name', - options, - localizationSource, - }) - } - - options = sanitizeLocales(options) - - if (!options.description) options = setFallbackDescription(options) - - return SlashX(options as ApplicationCommandOptionsX, string>) -} \ No newline at end of file +export function Slash(options?: ApplicationCommandOptions | string) { + if (!options) + options = { } + else if (typeof options === 'string') + options = { name: options } + + let localizationSource: TranslationsNestedPaths | null = null + + if (options.localizationSource) + localizationSource = constantPreserveDots(options.localizationSource) as TranslationsNestedPaths + else if (options.name) + localizationSource = `COMMANDS.${constantPreserveDots(options.name)}` as TranslationsNestedPaths + + if (localizationSource) { + options = setOptionsLocalization({ + target: 'description', + options, + localizationSource, + }) + + options = setOptionsLocalization({ + target: 'name', + options, + localizationSource, + }) + } + + options = sanitizeLocales(options) + + if (!options.description) + options = setFallbackDescription(options) + + return SlashX(options as ApplicationCommandOptionsX, string>) +} diff --git a/src/utils/decorators/SlashChoice.ts b/src/utils/decorators/SlashChoice.ts index 8822412b..699fd02a 100644 --- a/src/utils/decorators/SlashChoice.ts +++ b/src/utils/decorators/SlashChoice.ts @@ -1,6 +1,6 @@ -import { SlashChoice as SlashChoiceX } from "discordx" +import { SlashChoice as SlashChoiceX } from 'discordx' -import { constantPreserveDots, sanitizeLocales, setOptionsLocalization } from "@utils/functions" +import { constantPreserveDots, sanitizeLocales, setOptionsLocalization } from '@/utils/functions' /** * The slash command option can implement autocompletion for string and number types @@ -12,37 +12,36 @@ import { constantPreserveDots, sanitizeLocales, setOptionsLocalization } from "@ * * @category Decorator */ - export const SlashChoice = (...options: string[] | number[] | SlashChoiceOption[]) => { +export function SlashChoice(...options: string[] | number[] | SlashChoiceOption[]) { + for (let i = 0; i < options.length; i++) { + let option = options[i] - for (let i = 0; i < options.length; i++) { + if (typeof option !== 'number' && typeof option !== 'string') { + let localizationSource: TranslationsNestedPaths | null = null + if (option.localizationSource) + localizationSource = constantPreserveDots(option.localizationSource) as TranslationsNestedPaths - let option = options[i] + if (localizationSource) { + option = setOptionsLocalization({ + target: 'description', + options: option, + localizationSource, + }) - if (typeof option !== 'number' && typeof option !== 'string') { - - let localizationSource: TranslationsNestedPaths | null = null - if (option.localizationSource) localizationSource = constantPreserveDots(option.localizationSource) as TranslationsNestedPaths - - if (localizationSource) { + option = setOptionsLocalization({ + target: 'name', + options: option, + localizationSource, + }) + } - option = setOptionsLocalization({ - target: 'description', - options: option, - localizationSource, - }) - - option = setOptionsLocalization({ - target: 'name', - options: option, - localizationSource, - }) - } - - options[i] = sanitizeLocales(option) - } - } + options[i] = sanitizeLocales(option) + } + } - if (typeof options[0] === 'string') return SlashChoiceX(...options as string[]) - else if (typeof options[0] === 'number') return SlashChoiceX(...options as number[]) - else return SlashChoiceX(...options as SlashChoiceOption[]) + if (typeof options[0] === 'string') + return SlashChoiceX(...options as string[]) + else if (typeof options[0] === 'number') + return SlashChoiceX(...options as number[]) + else return SlashChoiceX(...options as SlashChoiceOption[]) } diff --git a/src/utils/decorators/SlashGroup.ts b/src/utils/decorators/SlashGroup.ts index 283cef01..925702b4 100644 --- a/src/utils/decorators/SlashGroup.ts +++ b/src/utils/decorators/SlashGroup.ts @@ -1,7 +1,6 @@ -import type { SlashGroupOptions as SlashGroupOptionsX } from "discordx" -import { ClassDecoratorEx, ClassMethodDecorator, SlashGroup as SlashGroupX, VerifyName } from "discordx" +import { ClassDecoratorEx, ClassMethodDecorator, SlashGroup as SlashGroupX, SlashGroupOptions as SlashGroupOptionsX, VerifyName } from 'discordx' -import { constantPreserveDots, sanitizeLocales, setFallbackDescription, setOptionsLocalization } from "@utils/functions" +import { constantPreserveDots, sanitizeLocales, setFallbackDescription, setOptionsLocalization } from '@/utils/functions' /** * Create slash group @@ -16,7 +15,7 @@ import { constantPreserveDots, sanitizeLocales, setFallbackDescription, setOptio * @category Decorator */ export function SlashGroup( - options: SlashGroupOptions + options: SlashGroupOptions ): ClassDecoratorEx /** @@ -32,9 +31,9 @@ export function SlashGroup( * @category Decorator */ export function SlashGroup( - name: VerifyName + name: VerifyName ): ClassMethodDecorator - + /** * Assign a group to a method or class * @@ -49,10 +48,10 @@ export function SlashGroup( * @category Decorator */ export function SlashGroup( - name: VerifyName, - root: VerifyName + name: VerifyName, + root: VerifyName ): ClassMethodDecorator - + /** * Assign a group to a method or class * @@ -67,36 +66,34 @@ export function SlashGroup( * @category Decorator */ export function SlashGroup(options: VerifyName | SlashGroupOptions, root?: VerifyName) { + if (typeof options !== 'string') { + let localizationSource: TranslationsNestedPaths | null = null + if (options.localizationSource) + localizationSource = constantPreserveDots(options.localizationSource) as TranslationsNestedPaths - if (typeof options !== 'string') { - - let localizationSource: TranslationsNestedPaths | null = null - if (options.localizationSource) localizationSource = constantPreserveDots(options.localizationSource) as TranslationsNestedPaths + if (localizationSource) { + options = setOptionsLocalization({ + target: 'description', + options, + localizationSource, + }) - if (localizationSource) { + options = setOptionsLocalization({ + target: 'name', + options, + localizationSource, + }) + } - options = setOptionsLocalization({ - target: 'description', - options, - localizationSource, - }) + options = sanitizeLocales(options) - options = setOptionsLocalization({ - target: 'name', - options, - localizationSource, - }) - } + if (!options.description) + options = setFallbackDescription(options) - options = sanitizeLocales(options) - - if (!options.description) options = setFallbackDescription(options) - - return SlashGroupX(options as SlashGroupOptionsX, string, VerifyName>) - - } else { - if (root) return SlashGroupX(options, root) - else return SlashGroupX(options) - } + return SlashGroupX(options as SlashGroupOptionsX, string, VerifyName>) + } else { + if (root) + return SlashGroupX(options, root) + else return SlashGroupX(options) + } } - diff --git a/src/utils/decorators/SlashOption.ts b/src/utils/decorators/SlashOption.ts index a91b8847..a2a4d5e8 100644 --- a/src/utils/decorators/SlashOption.ts +++ b/src/utils/decorators/SlashOption.ts @@ -1,8 +1,8 @@ -import { of } from "case" -import { SlashOption as SlashOptionX, SlashOptionOptions as SlashOptionOptionsX, VerifyName } from "discordx" +import { of } from 'case' +import { SlashOption as SlashOptionX, SlashOptionOptions as SlashOptionOptionsX, VerifyName } from 'discordx' -import { InvalidOptionName } from "@errors" -import { constantPreserveDots, sanitizeLocales, setFallbackDescription, setOptionsLocalization } from "@utils/functions" +import { InvalidOptionName } from '@/errors' +import { constantPreserveDots, sanitizeLocales, setFallbackDescription, setOptionsLocalization } from '@/utils/functions' /** * Add a slash command option @@ -14,41 +14,43 @@ import { constantPreserveDots, sanitizeLocales, setFallbackDescription, setOptio * * @category Decorator */ - export const SlashOption = (options: SlashOptionOptions) => { - - let localizationSource: TranslationsNestedPaths | null = null - - if (options.localizationSource) localizationSource = constantPreserveDots(options.localizationSource) as TranslationsNestedPaths - - if (localizationSource) { - - options = setOptionsLocalization({ - target: 'description', - options, - localizationSource, - }) - - options = setOptionsLocalization({ - target: 'name', - options, - localizationSource, - }) - } - - options = sanitizeLocales(options) - - if (!isValidOptionName(options.name)) throw new InvalidOptionName(options.name) - if (options.nameLocalizations) { - for (const name of Object.values(options.nameLocalizations)) { - if (!isValidOptionName(name)) throw new InvalidOptionName(name) - } - } - - if (!options.description) options = setFallbackDescription(options) - - return SlashOptionX(options as SlashOptionOptionsX, string>) +export function SlashOption(options: SlashOptionOptions) { + let localizationSource: TranslationsNestedPaths | null = null + + if (options.localizationSource) + localizationSource = constantPreserveDots(options.localizationSource) as TranslationsNestedPaths + + if (localizationSource) { + options = setOptionsLocalization({ + target: 'description', + options, + localizationSource, + }) + + options = setOptionsLocalization({ + target: 'name', + options, + localizationSource, + }) + } + + options = sanitizeLocales(options) + + if (!isValidOptionName(options.name)) + throw new InvalidOptionName(options.name) + if (options.nameLocalizations) { + for (const name of Object.values(options.nameLocalizations)) { + if (!isValidOptionName(name)) + throw new InvalidOptionName(name) + } + } + + if (!options.description) + options = setFallbackDescription(options) + + return SlashOptionX(options as SlashOptionOptionsX, string>) } -const isValidOptionName = (name: string) => { - return ['lower', 'snake'].includes(of(name)) && !name.includes(' ') -} \ No newline at end of file +function isValidOptionName(name: string) { + return ['lower', 'snake'].includes(of(name)) && !name.includes(' ') +} diff --git a/src/utils/decorators/index.ts b/src/utils/decorators/index.ts index 15b81627..8ce6fc1b 100644 --- a/src/utils/decorators/index.ts +++ b/src/utils/decorators/index.ts @@ -1,13 +1,13 @@ export { - Bot, - ButtonComponent, - Discord, - Guard, - Guild, - ModalComponent, - SelectMenuComponent, - SimpleCommand, - SimpleCommandOption, + Bot, + ButtonComponent, + Discord, + Guard, + Guild, + ModalComponent, + SelectMenuComponent, + SimpleCommand, + SimpleCommandOption, } from 'discordx' export * from './On' @@ -18,4 +18,4 @@ export * from './SlashChoice' export * from './SlashGroup' export * from './ContextMenu' export * from './Schedule' -export * from './OnCustom' \ No newline at end of file +export * from './OnCustom' diff --git a/src/utils/errors/InvalidOptionName.ts b/src/utils/errors/InvalidOptionName.ts index 94a8a6df..e787da7f 100644 --- a/src/utils/errors/InvalidOptionName.ts +++ b/src/utils/errors/InvalidOptionName.ts @@ -1,16 +1,16 @@ -import { snake } from "case" +import { snake } from 'case' -import { BaseError } from "@utils/classes" +import { BaseError } from '@/utils/classes' export class InvalidOptionName extends BaseError { - constructor(nameOption: string) { - super(`Name option must be all lowercase with no spaces. '${nameOption}' should be '${snake(nameOption)}'`) - } + constructor(nameOption: string) { + super(`Name option must be all lowercase with no spaces. '${nameOption}' should be '${snake(nameOption)}'`) + } - handle() { + handle() { + this.logger.console(this.message, 'error') + this.kill() + } - this.logger.console(this.message, 'error') - this.kill() - } -} \ No newline at end of file +} diff --git a/src/utils/errors/NoBotToken.ts b/src/utils/errors/NoBotToken.ts index 09f99e02..7aefc050 100644 --- a/src/utils/errors/NoBotToken.ts +++ b/src/utils/errors/NoBotToken.ts @@ -1,14 +1,14 @@ -import { BaseError } from "@utils/classes" +import { BaseError } from '@/utils/classes' export class NoBotTokenError extends BaseError { - constructor() { - super('Could not find BOT_TOKEN in your environment') - } + constructor() { + super('Could not find BOT_TOKEN in your environment') + } - handle() { + handle() { + this.logger.console(this.message, 'error') + this.kill() + } - this.logger.console(this.message, 'error') - this.kill() - } -} \ No newline at end of file +} diff --git a/src/utils/errors/UnknownReply.ts b/src/utils/errors/UnknownReply.ts index de6df011..f6f73bfe 100644 --- a/src/utils/errors/UnknownReply.ts +++ b/src/utils/errors/UnknownReply.ts @@ -1,23 +1,22 @@ -import { CommandInteraction } from "discord.js" +import { CommandInteraction } from 'discord.js' -import { getLocaleFromInteraction, L } from "@i18n" -import { BaseError } from "@utils/classes" -import { simpleErrorEmbed } from "@utils/functions" +import { getLocaleFromInteraction, L } from '@/i18n' +import { BaseError } from '@/utils/classes' +import { simpleErrorEmbed } from '@/utils/functions' export class UnknownReplyError extends BaseError { - private interaction: CommandInteraction + private interaction: CommandInteraction - constructor(interaction: CommandInteraction, message?: string) { - - super(message) + constructor(interaction: CommandInteraction, message?: string) { + super(message) - this.interaction = interaction - } + this.interaction = interaction + } - handle() { + handle() { + const locale = getLocaleFromInteraction(this.interaction) + simpleErrorEmbed(this.interaction, L[locale].ERRORS.UNKNOWN()) + } - const locale = getLocaleFromInteraction(this.interaction) - simpleErrorEmbed(this.interaction, L[locale]['ERRORS']['UNKNOWN']()) - } -} \ No newline at end of file +} diff --git a/src/utils/errors/index.ts b/src/utils/errors/index.ts index 90cfe5aa..d3bfafbb 100644 --- a/src/utils/errors/index.ts +++ b/src/utils/errors/index.ts @@ -1,3 +1,3 @@ export * from './UnknownReply' export * from './NoBotToken' -export * from './InvalidOptionName' \ No newline at end of file +export * from './InvalidOptionName' diff --git a/src/utils/functions/array.ts b/src/utils/functions/array.ts index 2b37dab2..ad49eadc 100644 --- a/src/utils/functions/array.ts +++ b/src/utils/functions/array.ts @@ -1,14 +1,12 @@ -/** +/** * Split an array into chunks of a given size * @param array The array to split * @param chunkSize The size of each chunk (default to 2) */ -export const chunkArray = (array: T[], chunkSize: number = 2): T[][] => { +export function chunkArray(array: T[], chunkSize: number = 2): T[][] { + const newArray: T[][] = [] + for (let i = 0; i < array.length; i += chunkSize) + newArray.push(array.slice(i, i + chunkSize)) - const newArray: T[][] = [] - for (let i = 0; i < array.length; i += chunkSize) { - newArray.push(array.slice(i, i + chunkSize)) - } - - return newArray -} \ No newline at end of file + return newArray +} diff --git a/src/utils/functions/colors.ts b/src/utils/functions/colors.ts index 9df8cd3f..d2027e1c 100644 --- a/src/utils/functions/colors.ts +++ b/src/utils/functions/colors.ts @@ -1,13 +1,12 @@ -import { ColorResolvable } from "discord.js" +import { ColorResolvable } from 'discord.js' -import { colorsConfig } from "@configs" +import { colorsConfig } from '@/configs' /** * Get a color from the config - * @param colorResolver The color to resolve - * @returns + * @param colorResolver the color to resolve + * @returns the resolved color */ -export const getColor = (colorResolver: keyof typeof colorsConfig) => { - - return colorsConfig[colorResolver] as ColorResolvable -} \ No newline at end of file +export function getColor(colorResolver: keyof typeof colorsConfig) { + return colorsConfig[colorResolver] as ColorResolvable +} diff --git a/src/utils/functions/converter.ts b/src/utils/functions/converter.ts index 99dd44e8..f951e035 100644 --- a/src/utils/functions/converter.ts +++ b/src/utils/functions/converter.ts @@ -1,22 +1,24 @@ -import fs from "fs" +import { Buffer } from 'node:buffer' +import fs from 'node:fs' /** * Change a date timezone to the one defined in the config. - * @param date - * @param tzString + * @param date + * @param tzString */ -export const convertTZ = (date: Date, tzString: string): Date => { - return new Date((typeof date === "string" ? new Date(date) : date).toLocaleString("en-US", {timeZone: tzString})) +export function convertTZ(date: Date, tzString: string): Date { + return new Date((typeof date === 'string' ? new Date(date) : date).toLocaleString('en-US', { timeZone: tzString })) } /** * Function to encode file data to base64 encoded string * @param file - file to encode */ -export const base64Encode = (file: string) => { - // read binary data - var bitmap = fs.readFileSync(file) - // convert binary data to base64 encoded string - return Buffer.from(bitmap).toString('base64') +export function base64Encode(file: string) { + // read binary data + const bitmap = fs.readFileSync(file) + + // convert binary data to base64 encoded string + return Buffer.from(bitmap).toString('base64') } diff --git a/src/utils/functions/database.ts b/src/utils/functions/database.ts index a0876ba5..adff0b87 100644 --- a/src/utils/functions/database.ts +++ b/src/utils/functions/database.ts @@ -1,25 +1,23 @@ -import { Data } from "@entities" -import { Database } from "@services" -import { resolveDependency } from "@utils/functions" +import { Data } from '@/entities' +import { Database } from '@/services' +import { resolveDependency } from '@/utils/functions' -import { defaultData } from "../../entities/Data" +import { defaultData } from '../../entities/Data' type DataType = keyof typeof defaultData /** * Initiate the EAV Data table if properties defined in the `defaultData` doesn't exist in it yet. */ -export const initDataTable = async () => { +export async function initDataTable() { + const db = await resolveDependency(Database) - const db = await resolveDependency(Database) + for (const key of Object.keys(defaultData)) { + const dataRepository = db.get(Data) - for (const key of Object.keys(defaultData)) { - - const dataRepository = db.get(Data) - - await dataRepository.add( - key as DataType, - defaultData[key as DataType] - ) - } -} \ No newline at end of file + await dataRepository.add( + key as DataType, + defaultData[key as DataType] + ) + } +} diff --git a/src/utils/functions/date.ts b/src/utils/functions/date.ts index 0515e867..5d839c47 100644 --- a/src/utils/functions/date.ts +++ b/src/utils/functions/date.ts @@ -1,8 +1,8 @@ -import dayjs from "dayjs" -import dayjsTimeZone from "dayjs/plugin/timezone" -import dayjsUTC from "dayjs/plugin/utc" +import dayjs from 'dayjs' +import dayjsTimeZone from 'dayjs/plugin/timezone' +import dayjsUTC from 'dayjs/plugin/utc' -import { generalConfig } from "@configs" +import { generalConfig } from '@/configs' dayjs.extend(dayjsUTC) dayjs.extend(dayjsTimeZone) @@ -12,23 +12,21 @@ dayjs.tz.setDefault(generalConfig.timezone) export const datejs = dayjs.tz const dateMasks = { - default: 'DD/MM/YYYY - HH:mm:ss', - onlyDate: 'DD/MM/YYYY', - onlyDateFileName: 'YYYY-MM-DD' + default: 'DD/MM/YYYY - HH:mm:ss', + onlyDate: 'DD/MM/YYYY', + onlyDateFileName: 'YYYY-MM-DD', } /** * Format a date object to a templated string using the [date-and-time](https://www.npmjs.com/package/date-and-time) library. - * @param date + * @param date * @param mask - template for the date format - * @returns + * @returns formatted date */ -export const formatDate = (date: Date, mask: keyof typeof dateMasks = 'default') => { - - return datejs(date).format(dateMasks[mask]) +export function formatDate(date: Date, mask: keyof typeof dateMasks = 'default') { + return datejs(date).format(dateMasks[mask]) } -export const timeAgo = (date: Date) => { - - return dayjs(date).fromNow() -} \ No newline at end of file +export function timeAgo(date: Date) { + return dayjs(date).fromNow() +} diff --git a/src/utils/functions/dependency.ts b/src/utils/functions/dependency.ts index 434447ec..14c6efd7 100644 --- a/src/utils/functions/dependency.ts +++ b/src/utils/functions/dependency.ts @@ -1,20 +1,17 @@ -import { F } from "ts-toolbelt" -import { container, InjectionToken } from "tsyringe" +import { F } from 'ts-toolbelt' +import { container, InjectionToken } from 'tsyringe' -export const resolveDependency = async (token: InjectionToken, interval: number = 500): Promise => { +export async function resolveDependency(token: InjectionToken, interval: number = 500): Promise { + while (!container.isRegistered(token, true)) + await new Promise(resolve => setTimeout(resolve, interval)) - while (!container.isRegistered(token, true)) { - await new Promise(resolve => setTimeout(resolve, interval)) - } - - return container.resolve(token) + return container.resolve(token) } -type Forward = {[Key in keyof T]: T[Key] extends abstract new (...args: any) => any ? InstanceType : T[Key]} - -export const resolveDependencies = async (tokens: F.Narrow) => { +type Forward = { [Key in keyof T]: T[Key] extends abstract new (...args: any) => any ? InstanceType : T[Key] } - return Promise.all(tokens.map((token: any) => - resolveDependency(token) - )) as Promise>> -} \ No newline at end of file +export async function resolveDependencies(tokens: F.Narrow) { + return Promise.all(tokens.map((token: any) => + resolveDependency(token) + )) as Promise>> +} diff --git a/src/utils/functions/devs.ts b/src/utils/functions/devs.ts index 76e73c97..cd6e0c68 100644 --- a/src/utils/functions/devs.ts +++ b/src/utils/functions/devs.ts @@ -1,18 +1,16 @@ -import { generalConfig } from "@configs" +import { generalConfig } from '@/configs' /** * Get a curated list of devs including the owner id */ -export const getDevs = (): string[] => { - - return [...new Set([...generalConfig.devs, generalConfig.ownerId])] +export function getDevs(): string[] { + return [...new Set([...generalConfig.devs, generalConfig.ownerId])] } /** * Check if a given user is a dev with its ID * @param id Discord user id */ -export const isDev = (id: string): boolean => { - - return getDevs().includes(id) -} \ No newline at end of file +export function isDev(id: string): boolean { + return getDevs().includes(id) +} diff --git a/src/utils/functions/embeds.ts b/src/utils/functions/embeds.ts index 7704ae26..96d7dc78 100644 --- a/src/utils/functions/embeds.ts +++ b/src/utils/functions/embeds.ts @@ -1,18 +1,18 @@ -import { CommandInteraction, EmbedBuilder } from "discord.js" +import { CommandInteraction, EmbedBuilder } from 'discord.js' + +import { replyToInteraction } from '@/utils/functions' -import { replyToInteraction } from "@utils/functions" /** * Send a simple success embed * @param interaction - discord interaction * @param message - message to log */ -export const simpleSuccessEmbed = (interaction: CommandInteraction, message: string) => { - - const embed = new EmbedBuilder() - .setColor(0x57f287) // GREEN // see: https://github.com/discordjs/discord.js/blob/main/packages/discord.js/src/util/Colors.js - .setTitle(`✅ ${message}`) +export function simpleSuccessEmbed(interaction: CommandInteraction, message: string) { + const embed = new EmbedBuilder() + .setColor(0x57F287) // GREEN // see: https://github.com/discordjs/discord.js/blob/main/packages/discord.js/src/util/Colors.js + .setTitle(`✅ ${message}`) - replyToInteraction(interaction, { embeds: [embed] }) + replyToInteraction(interaction, { embeds: [embed] }) } /** @@ -20,11 +20,10 @@ export const simpleSuccessEmbed = (interaction: CommandInteraction, message: str * @param interaction - discord interaction * @param message - message to log */ -export const simpleErrorEmbed = (interaction: CommandInteraction, message: string) => { +export function simpleErrorEmbed(interaction: CommandInteraction, message: string) { + const embed = new EmbedBuilder() + .setColor(0xED4245) // RED // see: https://github.com/discordjs/discord.js/blob/main/packages/discord.js/src/util/Colors.js + .setTitle(`❌ ${message}`) - const embed = new EmbedBuilder() - .setColor(0xed4245) // RED // see: https://github.com/discordjs/discord.js/blob/main/packages/discord.js/src/util/Colors.js - .setTitle(`❌ ${message}`) - - replyToInteraction(interaction, { embeds: [embed] }) -} \ No newline at end of file + replyToInteraction(interaction, { embeds: [embed] }) +} diff --git a/src/utils/functions/error.ts b/src/utils/functions/error.ts index 3a5d2650..b74703df 100644 --- a/src/utils/functions/error.ts +++ b/src/utils/functions/error.ts @@ -1,11 +1,11 @@ -import { parse } from "stacktrace-parser" +import { parse } from 'stacktrace-parser' -export const getCallerFile = (depth: number = 0) => { +export function getCallerFile(depth: number = 0) { + const err = new Error('Error') + const trace = parse(err.stack || '') - const err = new Error() - const trace = parse(err.stack || '') + if (!trace[0]) + return - if (!trace[0]) return - - return trace[depth + 1].file -} \ No newline at end of file + return trace[depth + 1].file +} diff --git a/src/utils/functions/eval.ts b/src/utils/functions/eval.ts index d0d9c792..81ec03f2 100644 --- a/src/utils/functions/eval.ts +++ b/src/utils/functions/eval.ts @@ -1,29 +1,27 @@ -import { Message, StageChannel } from "discord.js" +import { Message, StageChannel } from 'discord.js' -import { generalConfig } from "@configs" +import { generalConfig } from '@/configs' -const clean = (text: any) => { - if (typeof (text) === 'string') return text.replace(/`/g, '`' + String.fromCharCode(8203)).replace(/@/g, '@' + String.fromCharCode(8203)) - else return text +function clean(text: any) { + if (typeof (text) === 'string') + return text.replace(/`/g, `\`${String.fromCharCode(8203)}`).replace(/@/g, `@${String.fromCharCode(8203)}`) + else return text } /** * Eval a code snippet extracted from a Discord message. * @param message - Discord message containing the code to eval */ -export const executeEvalFromMessage = (message: Message) => { +export function executeEvalFromMessage(message: Message) { + try { + const code = message.content.replace(`\`\`\`${generalConfig.eval.name}`, '').replace('```', '') - try { + let evaled = eval(code) - const code = message.content.replace('```' + generalConfig.eval.name, '').replace('```', '') - - let evaled = eval(code) - - if (typeof evaled !== 'string') evaled = require('util').inspect(evaled) - - } catch (err) { - if (!(message.channel instanceof StageChannel)) { - message.channel.send(`\`ERROR\` \`\`\`xl\n${clean(err)}\n\`\`\``) - } - } -} \ No newline at end of file + if (typeof evaled !== 'string') + evaled = require('node:util').inspect(evaled) + } catch (err) { + if (!(message.channel instanceof StageChannel)) + message.channel.send(`\`ERROR\` \`\`\`xl\n${clean(err)}\n\`\`\``) + } +} diff --git a/src/utils/functions/files.ts b/src/utils/functions/files.ts index 643f57ed..16b05198 100644 --- a/src/utils/functions/files.ts +++ b/src/utils/functions/files.ts @@ -1,35 +1,34 @@ -import fs from "fs" +import fs from 'node:fs' +import process from 'node:process' /** * recursively get files paths from a directory - * @param path + * @param path */ -export const getFiles = (path: string): string[] => { +export function getFiles(path: string): string[] { + if (!fs.existsSync(path)) + return [] - if (!fs.existsSync(path)) return [] + const files = fs.readdirSync(path) + const fileList = [] - const files = fs.readdirSync(path) - const fileList = [] + for (const file of files) { + const filePath = `${path}/${file}` + const stats = fs.statSync(filePath) - for (const file of files) { + if (stats.isDirectory()) + fileList.push(...getFiles(filePath)) + else + fileList.push(filePath) + } - const filePath = `${path}/${file}` - const stats = fs.statSync(filePath) - - if (stats.isDirectory()) { - fileList.push(...getFiles(filePath)) - } else { - fileList.push(filePath) - } - } - - return fileList + return fileList } -export const fileOrDirectoryExists = (path: string): boolean => { - return fs.existsSync(path) +export function fileOrDirectoryExists(path: string): boolean { + return fs.existsSync(path) } -export const getSourceCodeLocation = (): string => { - return process.cwd() + '/' + (process.env['NODE_ENV'] === 'production' ? 'build' : 'src') -} \ No newline at end of file +export function getSourceCodeLocation(): string { + return `${process.cwd()}/${process.env.NODE_ENV === 'production' ? 'build' : 'src'}` +} diff --git a/src/utils/functions/image.ts b/src/utils/functions/image.ts index ebdc438e..413db79b 100644 --- a/src/utils/functions/image.ts +++ b/src/utils/functions/image.ts @@ -1,26 +1,25 @@ -import { Image } from "@entities" -import { Database } from "@services" -import { resolveDependency } from "@utils/functions" +import { Image } from '@/entities' +import { Database } from '@/services' +import { resolveDependency } from '@/utils/functions' /** * Abstraction level for the image repository that will find an image by its name (with or without extension). - * @param imageName + * @param imageName * @returns image url */ -export const getImage = async (imageName: string): Promise => { +export async function getImage(imageName: string): Promise { + const db = await resolveDependency(Database) + const imageRepo = db.get(Image) - const db = await resolveDependency(Database) - const imageRepo = db.get(Image) + const image = await imageRepo.findOne({ + $or: [ + { fileName: imageName }, + { fileName: `${imageName}.png` }, + { fileName: `${imageName}.jpg` }, + { fileName: `${imageName}.jpeg` }, + { fileName: `${imageName}.gif` }, + ], + }) - let image = await imageRepo.findOne({ - $or: [ - { fileName: imageName }, - { fileName: `${imageName}.png` }, - { fileName: `${imageName}.jpg` }, - { fileName: `${imageName}.jpeg` }, - { fileName: `${imageName}.gif` }, - ] - }) - - return image?.url || null -} \ No newline at end of file + return image?.url || null +} diff --git a/src/utils/functions/index.ts b/src/utils/functions/index.ts index f74f5b80..37417104 100644 --- a/src/utils/functions/index.ts +++ b/src/utils/functions/index.ts @@ -16,4 +16,4 @@ export * from './devs' export * from './dependency' export * from './error' export * from './localization' -export * from './files' \ No newline at end of file +export * from './files' diff --git a/src/utils/functions/interactions.ts b/src/utils/functions/interactions.ts index de6daf10..a3b230ae 100644 --- a/src/utils/functions/interactions.ts +++ b/src/utils/functions/interactions.ts @@ -1,13 +1,14 @@ -import { CommandInteraction } from "discord.js" -import { SimpleCommandMessage } from "discordx" +import { CommandInteraction } from 'discord.js' +import { SimpleCommandMessage } from 'discordx' /** * Abstraction level to reply to either a slash command or a simple command message. - * @param interaction - * @param message + * @param interaction + * @param message */ -export const replyToInteraction = async (interaction: CommandInteraction | SimpleCommandMessage, message: string | {[key: string]: any}) => { - - if (interaction instanceof CommandInteraction) await interaction.followUp(message) - else if (interaction instanceof SimpleCommandMessage) await interaction.message.reply(message) -} \ No newline at end of file +export async function replyToInteraction(interaction: CommandInteraction | SimpleCommandMessage, message: string | { [key: string]: any }) { + if (interaction instanceof CommandInteraction) + await interaction.followUp(message) + else if (interaction instanceof SimpleCommandMessage) + await interaction.message.reply(message) +} diff --git a/src/utils/functions/localization.ts b/src/utils/functions/localization.ts index f7714e7b..c93fca35 100644 --- a/src/utils/functions/localization.ts +++ b/src/utils/functions/localization.ts @@ -1,76 +1,70 @@ -import { generalConfig } from "@configs" -import { L, loadedLocales, Locales, locales } from "@i18n" +import { generalConfig } from '@/configs' +import { L, loadedLocales, Locales, locales } from '@/i18n' -export const getLocalizedInfo = (target: 'NAME' | 'DESCRIPTION', localizationSource: TranslationsNestedPaths) => { +export function getLocalizedInfo(target: 'NAME' | 'DESCRIPTION', localizationSource: TranslationsNestedPaths) { + const localizations = Object.fromEntries( + locales + .map(locale => [locale, getLocalizationFromPathString(`${localizationSource}.${target}` as TranslationsNestedPaths, locale)]) + .filter(([_, value]) => value) + ) - const localizations = Object.fromEntries( - locales - .map(locale => [locale, getLocalizationFromPathString(localizationSource + '.' + target as TranslationsNestedPaths, locale)]) - .filter(([_, value]) => value) - ) - - return Object.keys(localizations).length > 0 ? localizations : undefined + return Object.keys(localizations).length > 0 ? localizations : undefined } -export const setOptionsLocalization = ({ options, target, localizationSource, nameFallback }: { - options: K, - target: 'name' | 'description', - localizationSource: TranslationsNestedPaths, - nameFallback?: string -}) => { - - if (!options[`${target}Localizations`]) { - options[`${target}Localizations`] = getLocalizedInfo(target.toUpperCase() as 'NAME' | 'DESCRIPTION', localizationSource) - } - - if (!options[target as keyof typeof options]) { - options[target as keyof typeof options] = - getLocalizedInfo(target.toUpperCase() as 'NAME' | 'DESCRIPTION', localizationSource)?.[generalConfig.defaultLocale] +export function setOptionsLocalization({ options, target, localizationSource, nameFallback }: { + options: K + target: 'name' | 'description' + localizationSource: TranslationsNestedPaths + nameFallback?: string +}) { + if (!options[`${target}Localizations`]) + options[`${target}Localizations`] = getLocalizedInfo(target.toUpperCase() as 'NAME' | 'DESCRIPTION', localizationSource) + + if (!options[target as keyof typeof options]) { + options[target as keyof typeof options] + = getLocalizedInfo(target.toUpperCase() as 'NAME' | 'DESCRIPTION', localizationSource)?.[generalConfig.defaultLocale] || (target === 'name' ? nameFallback : undefined) - } + } - return options + return options } -export const sanitizeLocales = (option: K) => { - - // convert 'en' localizations to 'en-US' and 'en-GB' - if (option?.nameLocalizations?.['en']) { - option.nameLocalizations['en-US'] = option.nameLocalizations['en'] - option.nameLocalizations['en-GB'] = option.nameLocalizations['en'] - delete option.nameLocalizations['en'] - } - if (option?.descriptionLocalizations?.['en']) { - option.descriptionLocalizations['en-US'] = option.descriptionLocalizations['en'] - option.descriptionLocalizations['en-GB'] = option.descriptionLocalizations['en'] - delete option.descriptionLocalizations['en'] - } - - return option +export function sanitizeLocales(option: K) { + // convert 'en' localizations to 'en-US' and 'en-GB' + if (option?.nameLocalizations?.en) { + option.nameLocalizations['en-US'] = option.nameLocalizations.en + option.nameLocalizations['en-GB'] = option.nameLocalizations.en + delete option.nameLocalizations.en + } + if (option?.descriptionLocalizations?.en) { + option.descriptionLocalizations['en-US'] = option.descriptionLocalizations.en + option.descriptionLocalizations['en-GB'] = option.descriptionLocalizations.en + delete option.descriptionLocalizations.en + } + + return option } -export const getLocalizationFromPathString = (path: TranslationsNestedPaths, locale?: Locales) => { - - const pathArray = path?.split('.') || [] - let currentLocalization: any = loadedLocales[locale ?? generalConfig.defaultLocale] +export function getLocalizationFromPathString(path: TranslationsNestedPaths, locale?: Locales) { + const pathArray = path?.split('.') || [] + let currentLocalization: any = loadedLocales[locale ?? generalConfig.defaultLocale] - for (const pathNode of pathArray) { - currentLocalization = currentLocalization[pathNode as keyof typeof currentLocalization] - if (!currentLocalization) return undefined - } + for (const pathNode of pathArray) { + currentLocalization = currentLocalization[pathNode as keyof typeof currentLocalization] + if (!currentLocalization) + return undefined + } - return currentLocalization + return currentLocalization } -export const setFallbackDescription = (options: K & { description?: string }) => { +export function setFallbackDescription(options: K & { description?: string }) { + options.description = L[generalConfig.defaultLocale].SHARED.NO_COMMAND_DESCRIPTION() + if (!options.descriptionLocalizations) + options.descriptionLocalizations = {} - options.description = L[generalConfig.defaultLocale].SHARED.NO_COMMAND_DESCRIPTION() - if (!options.descriptionLocalizations) options.descriptionLocalizations = {} + for (const locale of locales) + options.descriptionLocalizations[locale] = L[locale].SHARED.NO_COMMAND_DESCRIPTION() - for (const locale of locales) { - options.descriptionLocalizations[locale] = L[locale].SHARED.NO_COMMAND_DESCRIPTION() - } - - return sanitizeLocales(options) - -} \ No newline at end of file + return sanitizeLocales(options) +} diff --git a/src/utils/functions/maintenance.ts b/src/utils/functions/maintenance.ts index 913905a9..4f925d43 100644 --- a/src/utils/functions/maintenance.ts +++ b/src/utils/functions/maintenance.ts @@ -1,24 +1,23 @@ -import { Data } from "@entities" -import { Database } from "@services" -import { resolveDependency } from "@utils/functions" +import { Data } from '@/entities' +import { Database } from '@/services' +import { resolveDependency } from '@/utils/functions' /** * Get the maintenance state of the bot. */ -export const isInMaintenance = async (): Promise => { - - const db = await resolveDependency(Database) - const dataRepository = db.get(Data) - const maintenance = await dataRepository.get('maintenance') - - return maintenance +export async function isInMaintenance(): Promise { + const db = await resolveDependency(Database) + const dataRepository = db.get(Data) + const maintenance = await dataRepository.get('maintenance') + + return maintenance } /** * Set the maintenance state of the bot. */ -export const setMaintenance = async (maintenance: boolean) => { - const db = await resolveDependency(Database) - const dataRepository = db.get(Data) - await dataRepository.set('maintenance', maintenance) -} \ No newline at end of file +export async function setMaintenance(maintenance: boolean) { + const db = await resolveDependency(Database) + const dataRepository = db.get(Data) + await dataRepository.set('maintenance', maintenance) +} diff --git a/src/utils/functions/prefix.ts b/src/utils/functions/prefix.ts index 490acb3f..a1daa68a 100644 --- a/src/utils/functions/prefix.ts +++ b/src/utils/functions/prefix.ts @@ -1,20 +1,20 @@ -import { Message } from "discord.js" +import { Message } from 'discord.js' -import { generalConfig } from "@configs" -import { Guild } from "@entities" -import { Database } from "@services" -import { resolveDependency } from "@utils/functions" +import { generalConfig } from '@/configs' +import { Guild } from '@/entities' +import { Database } from '@/services' +import { resolveDependency } from '@/utils/functions' /** * Get prefix from the database or from the config file. * @param message */ -export const getPrefixFromMessage = async (message: Message) => { - const db = await resolveDependency(Database) - const guildRepo = db.get(Guild) +export async function getPrefixFromMessage(message: Message) { + const db = await resolveDependency(Database) + const guildRepo = db.get(Guild) - const guildId = message.guild?.id - const guildData = await guildRepo.findOne({ id: guildId }) + const guildId = message.guild?.id + const guildData = await guildRepo.findOne({ id: guildId }) - return guildData?.prefix || generalConfig.simpleCommandsPrefix -} \ No newline at end of file + return guildData?.prefix || generalConfig.simpleCommandsPrefix +} diff --git a/src/utils/functions/resolvers.ts b/src/utils/functions/resolvers.ts index ba377010..3d6ea73f 100644 --- a/src/utils/functions/resolvers.ts +++ b/src/utils/functions/resolvers.ts @@ -1,20 +1,19 @@ -import { SimpleCommandMessage } from "discordx" import { - CommandInteraction, - ChatInputCommandInteraction, ButtonInteraction, + ChatInputCommandInteraction, + CommandInteraction, ContextMenuCommandInteraction, - ModalSubmitInteraction, - StringSelectMenuInteraction, + Interaction, Message, - VoiceState, MessageReaction, + ModalSubmitInteraction, PartialMessageReaction, - Interaction, -} from "discord.js" + StringSelectMenuInteraction, + VoiceState, +} from 'discord.js' +import { SimpleCommandMessage } from 'discordx' -// @ts-ignore - because it is outside the `rootDir` of tsconfig -import packageJson from "../../../package.json" +import packageJson from '../../../package.json' const resolvers = { @@ -23,17 +22,17 @@ const resolvers = { ChatInputCommandInteraction: (interaction: ChatInputCommandInteraction) => interaction.user, UserContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.member?.user, MessageContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.member?.user, - + ButtonInteraction: (interaction: ButtonInteraction) => interaction.member?.user, StringSelectMenuInteraction: (interaction: StringSelectMenuInteraction) => interaction.member?.user, - ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.member?.user, + ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.member?.user, Message: (interaction: Message) => interaction.author, VoiceState: (interaction: VoiceState) => interaction.member?.user, MessageReaction: (interaction: MessageReaction) => interaction.message.author, PartialMessageReaction: (interaction: PartialMessageReaction) => interaction.message.author, - fallback: (interaction: any) => null + fallback: (_: any) => null, }, member: { @@ -41,17 +40,17 @@ const resolvers = { ChatInputCommandInteraction: (interaction: ChatInputCommandInteraction) => interaction.member, UserContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.member, MessageContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.member, - + ButtonInteraction: (interaction: ButtonInteraction) => interaction.member, StringSelectMenuInteraction: (interaction: StringSelectMenuInteraction) => interaction.member, - ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.member, + ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.member, Message: (interaction: Message) => interaction.member, VoiceState: (interaction: VoiceState) => interaction.member, MessageReaction: (interaction: MessageReaction) => interaction.message.member, PartialMessageReaction: (interaction: PartialMessageReaction) => interaction.message.member, - fallback: (interaction: any) => null + fallback: (_: any) => null, }, guild: { @@ -59,12 +58,12 @@ const resolvers = { ChatInputCommandInteraction: (interaction: ChatInputCommandInteraction) => interaction.guild, UserContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.guild, MessageContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.guild, - + ButtonInteraction: (interaction: ButtonInteraction) => interaction.guild, StringSelectMenuInteraction: (interaction: StringSelectMenuInteraction) => interaction.guild, - ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.guild, + ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.guild, - fallback: (interaction: any) => null + fallback: (_: any) => null, }, channel: { @@ -72,12 +71,12 @@ const resolvers = { SimpleCommandMessage: (interaction: SimpleCommandMessage) => interaction.message.channel, UserContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.channel, MessageContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.channel, - + ButtonInteraction: (interaction: ButtonInteraction) => interaction.channel, StringSelectMenuInteraction: (interaction: StringSelectMenuInteraction) => interaction.channel, - ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.channel, + ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.channel, - fallback: (interaction: any) => null + fallback: (_: any) => null, }, commandName: { @@ -86,24 +85,24 @@ const resolvers = { UserContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.commandName, MessageContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.commandName, - fallback: (_: any) => '' + fallback: (_: any) => '', }, action: { ChatInputCommandInteraction: (interaction: ChatInputCommandInteraction) => { - return interaction.commandName - + (interaction?.options.getSubcommandGroup(false) ? ' ' + interaction.options.getSubcommandGroup(false) : '') - + (interaction?.options.getSubcommand(false) ? ' ' + interaction.options.getSubcommand(false) : '') + return interaction.commandName + + (interaction?.options.getSubcommandGroup(false) ? ` ${interaction.options.getSubcommandGroup(false)}` : '') + + (interaction?.options.getSubcommand(false) ? ` ${interaction.options.getSubcommand(false)}` : '') }, SimpleCommandMessage: (interaction: SimpleCommandMessage) => interaction.name, UserContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.commandName, MessageContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.commandName, - + ButtonInteraction: (interaction: ButtonInteraction) => interaction.customId, StringSelectMenuInteraction: (interaction: StringSelectMenuInteraction) => interaction.customId, - ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.customId, - - fallback: (_: any) => '' + ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.customId, + + fallback: (_: any) => '', }, locale: { @@ -111,50 +110,47 @@ const resolvers = { ChatInputCommandInteraction: (interaction: ChatInputCommandInteraction) => interaction.locale, UserContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.locale, MessageContextMenuCommandInteraction: (interaction: ContextMenuCommandInteraction) => interaction.locale, - + ButtonInteraction: (interaction: ButtonInteraction) => interaction.locale, StringSelectMenuInteraction: (interaction: StringSelectMenuInteraction) => interaction.locale, - ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.locale, - - fallback: (_: any) => 'en' - } -} + ModalSubmitInteraction: (interaction: ModalSubmitInteraction) => interaction.locale, -export const resolveUser = (interaction: AllInteractions | Interaction | Message | VoiceState | MessageReaction | PartialMessageReaction) => { - return resolvers.user[getTypeOfInteraction(interaction) as keyof typeof resolvers.user]?.(interaction) || resolvers.user['fallback'](interaction) + fallback: (_: any) => 'en', + }, } -export const resolveMember = (interaction: AllInteractions | Interaction | Message | VoiceState | MessageReaction | PartialMessageReaction) => { - return resolvers.member[getTypeOfInteraction(interaction) as keyof typeof resolvers.member]?.(interaction) || resolvers.member['fallback'](interaction) +export function resolveUser(interaction: AllInteractions | Interaction | Message | VoiceState | MessageReaction | PartialMessageReaction) { + return resolvers.user[getTypeOfInteraction(interaction) as keyof typeof resolvers.user]?.(interaction) || resolvers.user.fallback(interaction) } -export const resolveGuild = (interaction: AllInteractions | Interaction | Message | VoiceState | MessageReaction | PartialMessageReaction) => { - return resolvers.guild[getTypeOfInteraction(interaction) as keyof typeof resolvers.guild]?.(interaction) || resolvers.guild['fallback'](interaction) +export function resolveMember(interaction: AllInteractions | Interaction | Message | VoiceState | MessageReaction | PartialMessageReaction) { + return resolvers.member[getTypeOfInteraction(interaction) as keyof typeof resolvers.member]?.(interaction) || resolvers.member.fallback(interaction) } -export const resolveChannel = (interaction: AllInteractions) => { - return resolvers.channel[getTypeOfInteraction(interaction) as keyof typeof resolvers.channel]?.(interaction) || resolvers.channel['fallback'](interaction) +export function resolveGuild(interaction: AllInteractions | Interaction | Message | VoiceState | MessageReaction | PartialMessageReaction) { + return resolvers.guild[getTypeOfInteraction(interaction) as keyof typeof resolvers.guild]?.(interaction) || resolvers.guild.fallback(interaction) } -export const resolveCommandName = (interaction: CommandInteraction | SimpleCommandMessage) => { - return resolvers.commandName[interaction.constructor.name as keyof typeof resolvers.commandName]?.(interaction) || resolvers.commandName['fallback'](interaction) +export function resolveChannel(interaction: AllInteractions) { + return resolvers.channel[getTypeOfInteraction(interaction) as keyof typeof resolvers.channel]?.(interaction) || resolvers.channel.fallback(interaction) } -export const resolveAction = (interaction: AllInteractions) => { - return resolvers.action[getTypeOfInteraction(interaction) as keyof typeof resolvers.action]?.(interaction) || resolvers.action['fallback'](interaction) +export function resolveCommandName(interaction: CommandInteraction | SimpleCommandMessage) { + return resolvers.commandName[interaction.constructor.name as keyof typeof resolvers.commandName]?.(interaction) || resolvers.commandName.fallback(interaction) } - -export const resolveLocale = (interaction: AllInteractions) => { - return resolvers.locale[getTypeOfInteraction(interaction) as keyof typeof resolvers.locale]?.(interaction) || resolvers.locale['fallback'](interaction) +export function resolveAction(interaction: AllInteractions) { + return resolvers.action[getTypeOfInteraction(interaction) as keyof typeof resolvers.action]?.(interaction) || resolvers.action.fallback(interaction) } +export function resolveLocale(interaction: AllInteractions) { + return resolvers.locale[getTypeOfInteraction(interaction) as keyof typeof resolvers.locale]?.(interaction) || resolvers.locale.fallback(interaction) +} -export const getTypeOfInteraction = (interaction: any): string => { +export function getTypeOfInteraction(interaction: any): string { return interaction.constructor.name } -export const getTscordVersion = () => { - +export function getTscordVersion() { return packageJson.tscord.version -} \ No newline at end of file +} diff --git a/src/utils/functions/string.ts b/src/utils/functions/string.ts index 725e0e82..5a08466a 100644 --- a/src/utils/functions/string.ts +++ b/src/utils/functions/string.ts @@ -1,54 +1,55 @@ -import { constant } from "case" +import { constant } from 'case' /** * Ensures value(s) strings and has a size after trim * @param strings - * @returns {boolean} + * @returns {boolean} true if all strings are valid */ -export const validString = (...strings: Array): boolean => { - - if (strings.length === 0) return false - - for (const currString of strings) { - - if (!currString) return false - if (typeof currString !== "string") return false - if (currString.length === 0) return false - if (currString.trim().length === 0) return false - } - - return true +export function validString(...strings: Array): boolean { + if (strings.length === 0) + return false + + for (const currString of strings) { + if (!currString) + return false + if (typeof currString !== 'string') + return false + if (currString.length === 0) + return false + if (currString.trim().length === 0) + return false + } + + return true } -export const oneLine = (strings: TemplateStringsArray, ...keys: any[]) => { - - return strings - .reduce((result, part, i) => result + part + (keys[i] ?? '') , '') - .replace(/(?:\n(?:\s*))+/g, ' ') - .split('\NEWLINE') - .join('\n') - .trim() +export function oneLine(strings: TemplateStringsArray, ...keys: any[]) { + return strings + .reduce((result, part, i) => result + part + (keys[i] ?? ''), '') + .replace(/(?:\n(?:\s*))+/g, ' ') + .split('\NEWLINE') + .join('\n') + .trim() } -export const numberAlign = (number: number, align: number = 2) => { - - return number.toString().padStart(align, ' ') +export function numberAlign(number: number, align: number = 2) { + return number.toString().padStart(align, ' ') } -export const constantPreserveDots = (string: string) => { - - return string - .split('.') - .map(word => constant(word)) - .join('.') +export function constantPreserveDots(string: string) { + return string + .split('.') + .map(word => constant(word)) + .join('.') } -export const isValidUrl = (url: string) => { - - try { - new URL(url) - return true - } catch { - return false - } -} \ No newline at end of file +export function isValidUrl(url: string) { + try { + // eslint-disable-next-line no-new + new URL(url) + + return true + } catch { + return false + } +} diff --git a/src/utils/functions/synchronizer.ts b/src/utils/functions/synchronizer.ts index b921f4b3..387472f9 100644 --- a/src/utils/functions/synchronizer.ts +++ b/src/utils/functions/synchronizer.ts @@ -1,105 +1,96 @@ -import { User as DUser } from "discord.js" -import { Client } from "discordx" +import { User as DUser } from 'discord.js' +import { Client } from 'discordx' -import { Guild, User } from "@entities" -import { Database, Logger, Stats } from "@services" -import { resolveDependencies, resolveDependency } from "@utils/functions" +import { Guild, User } from '@/entities' +import { Database, Logger, Stats } from '@/services' +import { resolveDependencies, resolveDependency } from '@/utils/functions' /** * Add a active user to the database if doesn't exist. - * @param user + * @param user */ -export const syncUser = async (user: DUser) => { - - const [ db, stats, logger ] = await resolveDependencies([Database, Stats, Logger]) +export async function syncUser(user: DUser) { + const [db, stats, logger] = await resolveDependencies([Database, Stats, Logger]) - const userRepo = db.get(User) + const userRepo = db.get(User) - const userData = await userRepo.findOne({ - id: user.id - }) + const userData = await userRepo.findOne({ + id: user.id, + }) - if (!userData) { + if (!userData) { + // add user to the db + const newUser = new User() + newUser.id = user.id + await userRepo.persistAndFlush(newUser) - // add user to the db - const newUser = new User() - newUser.id = user.id - await userRepo.persistAndFlush(newUser) - - // record new user both in logs and stats - stats.register('NEW_USER', user.id) - logger.logNewUser(user) - } + // record new user both in logs and stats + stats.register('NEW_USER', user.id) + logger.logNewUser(user) + } } /** * Sync a guild with the database. - * @param guildId - * @param client + * @param guildId + * @param client */ -export const syncGuild = async (guildId: string, client: Client) => { - - const [ db, stats, logger ] = await resolveDependencies([Database, Stats, Logger]) +export async function syncGuild(guildId: string, client: Client) { + const [db, stats, logger] = await resolveDependencies([Database, Stats, Logger]) - const guildRepo = db.get(Guild), - guildData = await guildRepo.findOne({ id: guildId, deleted: false }) + const guildRepo = db.get(Guild) + const guildData = await guildRepo.findOne({ id: guildId, deleted: false }) - const fetchedGuild = await client.guilds.fetch(guildId).catch(() => null) + const fetchedGuild = await client.guilds.fetch(guildId).catch(() => null) - //check if this guild exists in the database, if not it creates it (or recovers it from the deleted ones) - if (!guildData) { + // check if this guild exists in the database, if not it creates it (or recovers it from the deleted ones) + if (!guildData) { + const deletedGuildData = await guildRepo.findOne({ id: guildId, deleted: true }) - const deletedGuildData = await guildRepo.findOne({ id: guildId, deleted: true }) + if (deletedGuildData) { + // recover deleted guild - if (deletedGuildData) { - // recover deleted guild - - deletedGuildData.deleted = false - await guildRepo.persistAndFlush(deletedGuildData) + deletedGuildData.deleted = false + await guildRepo.persistAndFlush(deletedGuildData) - stats.register('RECOVER_GUILD', guildId) - logger.logGuild('RECOVER_GUILD', guildId) - } - else { - // create new guild - - const newGuild = new Guild() - newGuild.id = guildId - await guildRepo.persistAndFlush(newGuild) + stats.register('RECOVER_GUILD', guildId) + logger.logGuild('RECOVER_GUILD', guildId) + } else { + // create new guild - stats.register('NEW_GUILD', guildId) - logger.logGuild('NEW_GUILD', guildId) - } + const newGuild = new Guild() + newGuild.id = guildId + await guildRepo.persistAndFlush(newGuild) - } - else if (!fetchedGuild) { - // guild is deleted but still exists in the database + stats.register('NEW_GUILD', guildId) + logger.logGuild('NEW_GUILD', guildId) + } + } else if (!fetchedGuild) { + // guild is deleted but still exists in the database - guildData.deleted = true - await guildRepo.persistAndFlush(guildData) + guildData.deleted = true + await guildRepo.persistAndFlush(guildData) - stats.register('DELETE_GUILD', guildId) - logger.logGuild('DELETE_GUILD', guildId) - } + stats.register('DELETE_GUILD', guildId) + logger.logGuild('DELETE_GUILD', guildId) + } } /** * Sync all guilds with the database. - * @param client + * @param client */ -export const syncAllGuilds = async (client: Client) => { - const db = await resolveDependency(Database) - - // add missing guilds - const guilds = client.guilds.cache - for (const guild of guilds) { - await syncGuild(guild[1].id, client) - } - - // remove deleted guilds - const guildRepo = db.get(Guild) - const guildsData = await guildRepo.getActiveGuilds() - for (const guildData of guildsData) { - await syncGuild(guildData.id, client) - } -} \ No newline at end of file +export async function syncAllGuilds(client: Client) { + const db = await resolveDependency(Database) + + // add missing guilds + const guilds = client.guilds.cache + for (const guild of guilds) + await syncGuild(guild[1].id, client) + + // remove deleted guilds + const guildRepo = db.get(Guild) + const guildsData = await guildRepo.getActiveGuilds() + for (const guildData of guildsData) + await syncGuild(guildData.id, client) +} diff --git a/src/utils/types/configs.d.ts b/src/utils/types/configs.d.ts index eaff9246..82d7185f 100644 --- a/src/utils/types/configs.d.ts +++ b/src/utils/types/configs.d.ts @@ -1,98 +1,98 @@ -type GeneralConfigType = { - - name: string - description: string - defaultLocale: import('@i18n').Locales - ownerId: string - timezone: string - automaticUploadImagesToImgur: boolean - - simpleCommandsPrefix: string - automaticDeferring: boolean - - links: { +interface GeneralConfigType { + + name: string + description: string + defaultLocale: import('@/i18n').Locales + ownerId: string + timezone: string + automaticUploadImagesToImgur: boolean + + simpleCommandsPrefix: string + automaticDeferring: boolean + + links: { invite: string supportServer: string gitRemoteRepo: string } - devs: string[] + devs: string[] - eval: { - name: string - onlyOwner: boolean - } + eval: { + name: string + onlyOwner: boolean + } - activities: { - text: string - type: "PLAYING" | "STREAMING" | "LISTENING" | "WATCHING" | "CUSTOM" | "COMPETING" - }[] + activities: { + text: string + type: 'PLAYING' | 'STREAMING' | 'LISTENING' | 'WATCHING' | 'CUSTOM' | 'COMPETING' + }[] } -type DatabaseConfigType = { - - path: `${string}/` +interface DatabaseConfigType { + + path: `${string}/` - backup: { - enabled: boolean - path: `${string}/` - } + backup: { + enabled: boolean + path: `${string}/` + } } -type LogsConfigType = { - - debug: boolean - logTailMaxSize: number - - archive: { - enabled: boolean - retention: number - } - - interaction: { - file: boolean - console: boolean - channel: string | null - - exclude: InteractionsConstants[] - } - - simpleCommand: { - file: boolean - console: boolean - channel: string | null - } - - newUser: { - file: boolean - console: boolean - channel: string | null - } - - guild: { - file: boolean - console: boolean - channel: string | null - } - - error: { - file: boolean - console: boolean - channel: string | null - } +interface LogsConfigType { + + debug: boolean + logTailMaxSize: number + + archive: { + enabled: boolean + retention: number + } + + interaction: { + file: boolean + console: boolean + channel: string | null + + exclude: InteractionsConstants[] + } + + simpleCommand: { + file: boolean + console: boolean + channel: string | null + } + + newUser: { + file: boolean + console: boolean + channel: string | null + } + + guild: { + file: boolean + console: boolean + channel: string | null + } + + error: { + file: boolean + console: boolean + channel: string | null + } } -type StatsConfigType = { +interface StatsConfigType { + + interaction: { - interaction: { - - exclude: InteractionsConstants[] - } + exclude: InteractionsConstants[] + } } -type APIConfigType = { +interface APIConfigType { - enabled: boolean - port: number -} \ No newline at end of file + enabled: boolean + port: number +} diff --git a/src/utils/types/database.d.ts b/src/utils/types/database.d.ts index 36b08873..29df4ec4 100644 --- a/src/utils/types/database.d.ts +++ b/src/utils/types/database.d.ts @@ -1,34 +1,34 @@ -type DatabaseSize = { - db: number | null, - backups: number | null +interface DatabaseSize { + db: number | null + backups: number | null } -type DatabaseConfigs = { - 'sqlite': { - driver: import('@mikro-orm/sqlite').SqliteDriver, - entityManager: import('@mikro-orm/sqlite').SqlEntityManager, - } - 'better-sqlite': { - driver: import('@mikro-orm/better-sqlite').BetterSqliteDriver, - entityManager: import('@mikro-orm/better-sqlite').SqlEntityManager, - } - 'postgresql': { - driver: import('@mikro-orm/postgresql').PostgreSqlDriver, - entityManager: import('@mikro-orm/postgresql').SqlEntityManager, - } - 'mysql': { - driver: import('@mikro-orm/mysql').MySqlDriver, - entityManager: import('@mikro-orm/mysql').SqlEntityManager, - } - 'mariadb': { - driver: import('@mikro-orm/mariadb').MariaDbDriver, - entityManager: import('@mikro-orm/mariadb').SqlEntityManager, - } - 'mongo': { - driver: import('@mikro-orm/mongodb').MongoDriver, - entityManager: import('@mikro-orm/mongodb').MongoEntityManager, - } +interface DatabaseConfigs { + 'sqlite': { + driver: import('@mikro-orm/sqlite').SqliteDriver + entityManager: import('@mikro-orm/sqlite').SqlEntityManager + } + 'better-sqlite': { + driver: import('@mikro-orm/better-sqlite').BetterSqliteDriver + entityManager: import('@mikro-orm/better-sqlite').SqlEntityManager + } + 'postgresql': { + driver: import('@mikro-orm/postgresql').PostgreSqlDriver + entityManager: import('@mikro-orm/postgresql').SqlEntityManager + } + 'mysql': { + driver: import('@mikro-orm/mysql').MySqlDriver + entityManager: import('@mikro-orm/mysql').SqlEntityManager + } + 'mariadb': { + driver: import('@mikro-orm/mariadb').MariaDbDriver + entityManager: import('@mikro-orm/mariadb').SqlEntityManager + } + 'mongo': { + driver: import('@mikro-orm/mongodb').MongoDriver + entityManager: import('@mikro-orm/mongodb').MongoEntityManager + } } -type DatabaseDriver = DatabaseConfigs[typeof import('@configs').mikroORMConfig['production']['type']]['driver'] -type DatabaseEntityManager = DatabaseConfigs[typeof import('@configs').mikroORMConfig['production']['type']]['entityManager'] \ No newline at end of file +type DatabaseDriver = DatabaseConfigs[typeof import('@/configs').mikroORMConfig['production']['type']]['driver'] +type DatabaseEntityManager = DatabaseConfigs[typeof import('@/configs').mikroORMConfig['production']['type']]['entityManager'] diff --git a/src/utils/types/environment.d.ts b/src/utils/types/environment.d.ts index 179e4085..5a9ae163 100644 --- a/src/utils/types/environment.d.ts +++ b/src/utils/types/environment.d.ts @@ -1,27 +1,27 @@ declare global { - namespace NodeJS { - interface ProcessEnv { + namespace NodeJS { + interface ProcessEnv { - NODE_ENV: 'development' | 'production' - - BOT_TOKEN: string - TEST_GUILD_ID: string - BOT_OWNER_ID: string - - DATABASE_HOST: string - DATABASE_PORT: string - DATABASE_NAME: string - DATABASE_USER: string - DATABASE_PASSWORD: string - - API_PORT: string - API_ADMIN_TOKEN: string - - IMGUR_CLIENT_ID: string - } - } - } - - // If this file has no import/export statements (i.e. is a script) - // convert it into a module by adding an empty export statement. - export {} \ No newline at end of file + NODE_ENV: 'development' | 'production' + + BOT_TOKEN: string + TEST_GUILD_ID: string + BOT_OWNER_ID: string + + DATABASE_HOST: string + DATABASE_PORT: string + DATABASE_NAME: string + DATABASE_USER: string + DATABASE_PASSWORD: string + + API_PORT: string + API_ADMIN_TOKEN: string + + IMGUR_CLIENT_ID: string + } + } +} + +// If this file has no import/export statements (i.e. is a script) +// convert it into a module by adding an empty export statement. +export {} diff --git a/src/utils/types/interactions.d.ts b/src/utils/types/interactions.d.ts index 58778e66..99d5773e 100644 --- a/src/utils/types/interactions.d.ts +++ b/src/utils/types/interactions.d.ts @@ -10,7 +10,7 @@ type InteractionsConstants = 'CHAT_INPUT_COMMAND_INTERACTION' | 'SIMPLE_COMMAND_ type CommandCategory = import('discordx').DApplicationCommand & import('@discordx/utilities').ICategory -type InteractionData = { - sanitizedLocale: import('src/i18n').Locales - localize: import('src/i18n/i18n-types').TranslationFunctions -} \ No newline at end of file +interface InteractionData { + sanitizedLocale: import('src/i18n').Locales + localize: import('src/i18n/i18n-types').TranslationFunctions +} diff --git a/src/utils/types/localization.d.ts b/src/utils/types/localization.d.ts index cebd394a..41034440 100644 --- a/src/utils/types/localization.d.ts +++ b/src/utils/types/localization.d.ts @@ -1,15 +1,15 @@ declare enum AdditionalLocaleString { - English = 'en' + English = 'en' } -type TranslationsNestedPaths = NestedPaths +type TranslationsNestedPaths = NestedPaths type LocalizationMap = Partial> -type SanitizedOptions = { - descriptionLocalizations?: LocalizationMap - nameLocalizations?: LocalizationMap - localizationSource?: TranslationsNestedPaths +interface SanitizedOptions { + descriptionLocalizations?: LocalizationMap + nameLocalizations?: LocalizationMap + localizationSource?: TranslationsNestedPaths } type Sanitization = Modify @@ -29,9 +29,9 @@ type SlashOptionOptions = Sanitization< type SlashChoiceOption = Modify, SanitizedOptions> type ContextMenuOptionsX = Omit, string> & { - type: Exclude -}, "description" | "descriptionLocalizations"> + type: Exclude +}, 'description' | 'descriptionLocalizations'> type ContextMenuOptions = Modify, { - type: ContextMenuOptionsX['type'] | 'USER' | 'MESSAGE' -}> \ No newline at end of file + type: ContextMenuOptionsX['type'] | 'USER' | 'MESSAGE' +}> diff --git a/src/utils/types/state.d.ts b/src/utils/types/state.d.ts index 994cac43..a542de6e 100644 --- a/src/utils/types/state.d.ts +++ b/src/utils/types/state.d.ts @@ -1 +1,3 @@ -type state = { [key: string]: any } \ No newline at end of file +interface state { + [key: string]: any +} diff --git a/src/utils/types/stats.d.ts b/src/utils/types/stats.d.ts index e84e796c..208be3c2 100644 --- a/src/utils/types/stats.d.ts +++ b/src/utils/types/stats.d.ts @@ -1,9 +1,9 @@ type StatPerInterval = { - date: string, - count: number + date: string + count: number }[] -type StatsResolverType = { - name: string, - data: (statsHelper: import('@services').Stats, days: number) => Promise -}[] \ No newline at end of file +type StatsResolverType = { + name: string + data: (statsHelper: import('@/services').Stats, days: number) => Promise +}[] diff --git a/src/utils/types/utils.d.ts b/src/utils/types/utils.d.ts index 02b70e36..4813a112 100644 --- a/src/utils/types/utils.d.ts +++ b/src/utils/types/utils.d.ts @@ -3,7 +3,7 @@ type Modify = Omit & R type OmitPick = Pick> type WithOptional = OmitPick & Partial> type WithRequiredProperty = Type & { - [Property in Key]-?: Type[Property] + [Property in Key]-?: Type[Property] } type Primitive = string | number | symbol @@ -12,25 +12,25 @@ type GenericObject = Record type Join< L extends Primitive | undefined, - R extends Primitive | undefined, + R extends Primitive | undefined > = L extends string | number - ? R extends string | number - ? `${L}.${R}` - : L - : R extends string | number - ? R - : undefined + ? R extends string | number + ? `${L}.${R}` + : L + : R extends string | number + ? R + : undefined type Union< L extends unknown | undefined, - R extends unknown | undefined, + R extends unknown | undefined > = L extends undefined - ? R extends undefined - ? undefined - : R - : R extends undefined - ? L - : L | R + ? R extends undefined + ? undefined + : R + : R extends undefined + ? L + : L | R /** * NestedPaths @@ -42,11 +42,11 @@ type Union< type NestedPaths< T extends GenericObject, Prev extends Primitive | undefined = undefined, - Path extends Primitive | undefined = undefined, + Path extends Primitive | undefined = undefined > = { - [K in keyof T]: T[K] extends GenericObject - ? NestedPaths, Join> - : Union, Join> + [K in keyof T]: T[K] extends GenericObject + ? NestedPaths, Join> + : Union, Join> }[keyof T] /** @@ -58,13 +58,13 @@ type NestedPaths< */ type TypeFromPath< T extends GenericObject, - Path extends string, // Or, if you prefer, NestedPaths + Path extends string // Or, if you prefer, NestedPaths > = { - [K in Path]: K extends keyof T - ? T[K] - : K extends `${infer P}.${infer S}` - ? T[P] extends GenericObject - ? TypeFromPath - : never - : never -}[Path] \ No newline at end of file + [K in Path]: K extends keyof T + ? T[K] + : K extends `${infer P}.${infer S}` + ? T[P] extends GenericObject + ? TypeFromPath + : never + : never +}[Path] diff --git a/tsconfig.json b/tsconfig.json index 4cdd0c85..0fb7c82a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,40 +21,40 @@ "baseUrl": ".", "paths": { - "@decorators": ["src/utils/decorators"], - "@decorators/*": ["src/plugins/*/utils/decorators"], + "@/decorators": ["src/utils/decorators"], + "@/decorators/*": ["src/plugins/*/utils/decorators"], - "@errors": ["src/utils/errors"], - "@errors/*": ["src/plugins/*/utils/errors"], + "@/errors": ["src/utils/errors"], + "@/errors/*": ["src/plugins/*/utils/errors"], - "@entities": ["src/entities"], - "@entities/*": ["src/plugins/*/entities"], + "@/entities": ["src/entities"], + "@/entities/*": ["src/plugins/*/entities"], - "@guards": ["src/guards"], - "@guards/*": ["src/plugins/*/guards"], + "@/guards": ["src/guards"], + "@/guards/*": ["src/plugins/*/guards"], - "@services": ["src/services"], - "@services/*": ["src/plugins/*/services"], + "@/services": ["src/services"], + "@/services/*": ["src/plugins/*/services"], - "@i18n": ["src/i18n"], - "@i18n/*": ["src/plugins/*/i18n"], + "@/i18n": ["src/i18n"], + "@/i18n/*": ["src/plugins/*/i18n"], - "@configs": ["src/configs"], - "@configs/*": ["src/plugins/*/configs"], + "@/configs": ["src/configs"], + "@/configs/*": ["src/plugins/*/configs"], - "@utils/classes": ["src/utils/classes"], - "@utils/classes/*": ["src/plugins/*/utils/classes"], + "@/utils/classes": ["src/utils/classes"], + "@/utils/classes/*": ["src/plugins/*/utils/classes"], - "@utils/functions": ["src/utils/functions"], - "@utils/functions/*": ["src/plugins/*/utils/functions"], + "@/utils/functions": ["src/utils/functions"], + "@/utils/functions/*": ["src/plugins/*/utils/functions"], - "@api/controllers": ["src/api/controllers"], - "@api/controllers/*": ["src/plugins/*/api/controllers"], + "@/api/controllers": ["src/api/controllers"], + "@/api/controllers/*": ["src/plugins/*/api/controllers"], - "@api/middlewares": ["src/api/middlewares"], - "@api/middlewares/*": ["src/plugins/*/api/middlewares"], + "@/api/middlewares": ["src/api/middlewares"], + "@/api/middlewares/*": ["src/plugins/*/api/middlewares"], - "@api/server": ["src/api/server.ts"] + "@/api/server": ["src/api/server.ts"] } },