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 "" +
- (cmd.group ? cmd.group + ' ' : '') +
- (cmd.subgroup ? cmd.subgroup + ' ' : '') +
- cmd.name +
- ":" +
- applicationCommands.find(acmd => acmd.name == (cmd.group ? cmd.group : cmd.name))!.id +
- ">"
+ const commands = category[1]
+ .map((cmd) => {
+ return `${
+ cmd.group ? `${cmd.group} ` : ''
+ }${cmd.subgroup ? `${cmd.subgroup} ` : ''
+ }${cmd.name
+ }:${
+ applicationCommands.find(acmd => 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 = "" +
- (item.group ? item.group + ' ' : '') +
- (item.subgroup ? item.subgroup + ' ' : '') +
- item.name +
- ":" +
- applicationCommands.find(acmd => acmd.name == (item.group ? item.group : item.name))!.id +
- ">"
+ const fieldValue = validString(description) ? description : 'No description'
+ const name = `${item.group ? `${item.group} ` : ''}${item.subgroup ? `${item.subgroup} ` : ''}${item.name}:${applicationCommands.find(acmd => 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"]
}
},