From c830bae505314d92aa7917d95e18f60bea59053c Mon Sep 17 00:00:00 2001 From: Slincnik Date: Fri, 2 May 2025 20:30:18 +0300 Subject: [PATCH 1/4] WIP: rewrite to mikro-orm --- package.json | 3 + pnpm-lock.yaml | 135 ++++++++++ src/adapters/database/MikroOrmAdapter.ts | 103 +++++++ src/adapters/database/mirko-orm.config.ts | 10 + src/commands/Administration/config.ts | 126 +++++++-- src/helpers/tasks/birthdays.ts | 36 ++- src/helpers/tasks/checkReminds.ts | 6 +- src/models/BaseModel.ts | 20 ++ src/models/GuildModel.ts | 164 +++++++----- src/models/MemberModel.ts | 126 ++++----- src/models/UserModel.ts | 312 ++++++++++++---------- src/structures/client.ts | 16 +- src/utils/config-fields.ts | 71 +++-- tsconfig.json | 8 +- 14 files changed, 810 insertions(+), 326 deletions(-) create mode 100644 src/adapters/database/MikroOrmAdapter.ts create mode 100644 src/adapters/database/mirko-orm.config.ts create mode 100644 src/models/BaseModel.ts diff --git a/package.json b/package.json index d7410870..5476b8db 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "dependencies": { "@discordjs/opus": "^0.10.0", "@discordjs/voice": "^0.18.0", + "@mikro-orm/core": "^6.4.13", + "@mikro-orm/mongodb": "^6.4.13", "@napi-rs/canvas": "^0.1.68", "chalk": "^5.4.1", "cron": "^4.1.0", @@ -21,6 +23,7 @@ "i18next": "^24.2.2", "i18next-fs-backend": "^2.6.0", "md5": "^2.3.0", + "mongodb": "^6.16.0", "mongoose": "^8.12.1", "ms": "^2.1.3", "node-fetch": "^3.3.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3ec06f0..548390c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@discordjs/voice': specifier: ^0.18.0 version: 0.18.0(@discordjs/opus@0.10.0) + '@mikro-orm/core': + specifier: ^6.4.13 + version: 6.4.13 + '@mikro-orm/mongodb': + specifier: ^6.4.13 + version: 6.4.13(@mikro-orm/core@6.4.13) '@napi-rs/canvas': specifier: ^0.1.68 version: 0.1.69 @@ -41,6 +47,9 @@ importers: md5: specifier: ^2.3.0 version: 2.3.0 + mongodb: + specifier: ^6.16.0 + version: 6.16.0 mongoose: specifier: ^8.12.1 version: 8.14.0 @@ -369,6 +378,16 @@ packages: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@mikro-orm/core@6.4.13': + resolution: {integrity: sha512-DG6QGr3J9bBy64DBBxoA6WwvnKr35QqOGsQ8OE2B8ccUaK4jrpMolCkKYbZpJmCHGG4qKaECRu46at6nYZzogA==} + engines: {node: '>= 18.12.0'} + + '@mikro-orm/mongodb@6.4.13': + resolution: {integrity: sha512-nBALSAcO7GaPf1v02D3BBBfW6tViC74GI+KSJqtvBZ4Xc1pSmyR5Bm7MSWOzs1ggTJBzii3ZS1SzYtPagExOmw==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + '@mongodb-js/saslprep@1.2.2': resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==} @@ -760,6 +779,9 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + dataloader@2.2.3: + resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -818,6 +840,10 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -874,6 +900,11 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -953,6 +984,10 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -1018,6 +1053,9 @@ packages: resolution: {integrity: sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==} engines: {node: '>=16'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -1130,6 +1168,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + kareem@2.6.3: resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} engines: {node: '>=12.0.0'} @@ -1193,6 +1234,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mikro-orm@6.4.13: + resolution: {integrity: sha512-sQZezhVLjH/gHUjVqsn+k60xC4TsL7h+Q/0UPe+OF4LoywTZNKwid1mN3WPxNd4c5eI6Aavl11KIsu2t9OKqzQ==} + engines: {node: '>= 18.12.0'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -1235,6 +1280,33 @@ packages: mongodb-connection-string-url@3.0.2: resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} + mongodb@6.15.0: + resolution: {integrity: sha512-ifBhQ0rRzHDzqp9jAQP6OwHSH7dbYIQjD3SbJs9YYk9AikKEettW/9s/tbSFDTpXcRbF+u1aLrhHxDFaYtZpFQ==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + mongodb@6.16.0: resolution: {integrity: sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==} engines: {node: '>=16.20.1'} @@ -1458,6 +1530,9 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -1647,6 +1722,10 @@ packages: resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} engines: {node: '>=18.17'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1980,6 +2059,30 @@ snapshots: dependencies: '@sinclair/typebox': 0.27.8 + '@mikro-orm/core@6.4.13': + dependencies: + dataloader: 2.2.3 + dotenv: 16.5.0 + esprima: 4.0.1 + fs-extra: 11.3.0 + globby: 11.1.0 + mikro-orm: 6.4.13 + reflect-metadata: 0.2.2 + + '@mikro-orm/mongodb@6.4.13(@mikro-orm/core@6.4.13)': + dependencies: + '@mikro-orm/core': 6.4.13 + bson: 6.10.3 + mongodb: 6.15.0 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + '@mongodb-js/saslprep@1.2.2': dependencies: sparse-bitfield: 3.0.3 @@ -2369,6 +2472,8 @@ snapshots: data-uri-to-buffer@4.0.1: {} + dataloader@2.2.3: {} + debug@4.4.0: dependencies: ms: 2.1.3 @@ -2426,6 +2531,8 @@ snapshots: dependencies: esutils: 2.0.3 + dotenv@16.5.0: {} + emoji-regex@8.0.0: {} esbuild@0.25.3: @@ -2569,6 +2676,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 3.4.3 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -2646,6 +2755,12 @@ snapshots: dependencies: fetch-blob: 3.2.0 + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs-minipass@2.1.0: dependencies: minipass: 3.3.6 @@ -2740,6 +2855,8 @@ snapshots: p-cancelable: 3.0.0 responselike: 3.0.0 + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} has-ansi@2.0.0: @@ -2828,6 +2945,12 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + kareem@2.6.3: {} keyv@4.5.4: @@ -2883,6 +3006,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mikro-orm@6.4.13: {} + mimic-response@3.1.0: {} mimic-response@4.0.0: {} @@ -2919,6 +3044,12 @@ snapshots: '@types/whatwg-url': 11.0.5 whatwg-url: 14.2.0 + mongodb@6.15.0: + dependencies: + '@mongodb-js/saslprep': 1.2.2 + bson: 6.10.3 + mongodb-connection-string-url: 3.0.2 + mongodb@6.16.0: dependencies: '@mongodb-js/saslprep': 1.2.2 @@ -3106,6 +3237,8 @@ snapshots: dependencies: picomatch: 2.3.1 + reflect-metadata@0.2.2: {} + regenerator-runtime@0.14.1: {} require-relative@0.8.7: {} @@ -3269,6 +3402,8 @@ snapshots: undici@6.21.1: {} + universalify@2.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 diff --git a/src/adapters/database/MikroOrmAdapter.ts b/src/adapters/database/MikroOrmAdapter.ts new file mode 100644 index 00000000..0b0f9e66 --- /dev/null +++ b/src/adapters/database/MikroOrmAdapter.ts @@ -0,0 +1,103 @@ +import { + EntityName, + EntityManager, + MikroORM, + RequiredEntityData, + FilterQuery, +} from "@mikro-orm/mongodb"; +import IDatabaseAdapter from "./IDatabaseAdapter.js"; +import Cache from "../cache/MapCache.js"; +import { MongoDriver } from "@mikro-orm/mongodb"; +import mirkoOrmConfig from "./mirko-orm.config.js"; +import logger from "@/helpers/logger.js"; +import { BaseEntity } from "@/models/BaseModel.js"; + +export default class MikroOrmAdapter extends IDatabaseAdapter< + EntityName, + Record, + FilterQuery, + unknown, + any +> { + cache = new Cache(); + em!: EntityManager; + orm!: MikroORM; + + uri!: string; + + constructor(uri: string) { + super(); + this.uri = uri; + } + + async connect() { + this.orm = await MikroORM.init({ + ...mirkoOrmConfig, + clientUrl: this.uri, + }); + this.em = this.orm.em.fork(); + this.em.clear(); + + BaseEntity.useEntityManager(this.em); + + logger.log("Database connected."); + } + + async disconnect() { + await this.orm.close(true); + logger.warn("Database disconnected."); + this.cache.clear(); + } + + #generateCacheKey(modelName: string, query: {}, options: {}) { + return `${modelName}:${JSON.stringify(query)}:${JSON.stringify(options)}`; + } + + async find(model: EntityName, query: FilterQuery = {}) { + const cacheKey = this.#generateCacheKey(model.toString(), query, {}); + if (this.cache.get(cacheKey)) { + return this.cache.get(cacheKey) as T[]; + } + + const result = await this.em.find(model, query); + this.cache.set(cacheKey, result); + return result; + } + + async findOne(model: EntityName, query: FilterQuery = {}) { + const cacheKey = this.#generateCacheKey(model.toString(), query, {}); + if (this.cache.get(cacheKey)) { + return this.cache.get(cacheKey) as T[]; + } + + const result = await this.em.findOne(model, query); + this.cache.set(cacheKey, result); + + return result; + } + + async updateOne(model: EntityName, filter: FilterQuery, update: any) { + const result = await this.em.nativeUpdate(model, filter, update); + this.cache.clear(); + return result; + } + + async deleteOne(model: EntityName, filter: FilterQuery) { + const result = await this.em.nativeDelete(model, filter); + this.cache.clear(); + return result; + } + + async findOneOrCreate(model: EntityName, filter: FilterQuery) { + this.cache.clear(); + const result = await this.em.findOne(model, filter).then(async entity => { + if (entity) { + return entity; + } + const res = this.em.create(model, filter as unknown as RequiredEntityData); + await this.em.persistAndFlush(res); + return res; + }); + return result; + } +} diff --git a/src/adapters/database/mirko-orm.config.ts b/src/adapters/database/mirko-orm.config.ts new file mode 100644 index 00000000..3bc23559 --- /dev/null +++ b/src/adapters/database/mirko-orm.config.ts @@ -0,0 +1,10 @@ +import { defineConfig, MongoDriver } from "@mikro-orm/mongodb"; + +export default defineConfig({ + entities: ["dist/models/**/*.js"], + entitiesTs: ["src/models/**/*.ts"], + debug: process.env.NODE_ENV !== "production", + forceEntityConstructor: true, + dbName: "discordbot", + driver: MongoDriver, +}); diff --git a/src/commands/Administration/config.ts b/src/commands/Administration/config.ts index 3386b691..24396416 100644 --- a/src/commands/Administration/config.ts +++ b/src/commands/Administration/config.ts @@ -1,10 +1,19 @@ import { getLocalizedDesc, translateContext } from "@/helpers/extenders.js"; -import GuildModel from "@/models/GuildModel.js"; +import { Guild, Plugins } from "@/models/GuildModel.js"; import { CommandData, SlashCommandProps } from "@/types.js"; import { generateFields } from "@/utils/config-fields.js"; import { createEmbed } from "@/utils/create-embed.js"; import useClient from "@/utils/use-client.js"; -import { ApplicationCommandOptionType, ApplicationIntegrationType, Channel, ChannelType, ChatInputCommandInteraction, InteractionContextType, MessageFlags, PermissionsBitField } from "discord.js"; +import { + ApplicationCommandOptionType, + ApplicationIntegrationType, + Channel, + ChannelType, + ChatInputCommandInteraction, + InteractionContextType, + MessageFlags, + PermissionsBitField, +} from "discord.js"; const client = useClient(); @@ -51,15 +60,42 @@ export const data: CommandData = { type: ApplicationCommandOptionType.String, required: true, choices: [ - { name: client.i18n.translate("administration/config:BIRTHDAYS"), value: "birthdays" }, - { name: client.i18n.translate("administration/config:MODLOGS"), value: "modlogs" }, - { name: client.i18n.translate("administration/config:REPORTS"), value: "reports" }, - { name: client.i18n.translate("administration/config:SUGGESTIONS"), value: "suggestions" }, - { name: client.i18n.translate("administration/config:TICKETSCATEGORY"), value: "tickets.ticketsCategory" }, - { name: client.i18n.translate("administration/config:TICKETLOGS"), value: "tickets.ticketLogs" }, - { name: client.i18n.translate("administration/config:TRANSCRIPTIONLOGS"), value: "tickets.transcriptionLogs" }, - { name: client.i18n.translate("administration/config:MESSAGEUPDATE"), value: "monitoring.messageUpdate" }, - { name: client.i18n.translate("administration/config:MESSAGEDELETE"), value: "monitoring.messageDelete" }, + { + name: client.i18n.translate("administration/config:BIRTHDAYS"), + value: "birthdays", + }, + { + name: client.i18n.translate("administration/config:MODLOGS"), + value: "modlogs", + }, + { + name: client.i18n.translate("administration/config:REPORTS"), + value: "reports", + }, + { + name: client.i18n.translate("administration/config:SUGGESTIONS"), + value: "suggestions", + }, + { + name: client.i18n.translate("administration/config:TICKETSCATEGORY"), + value: "tickets.ticketsCategory", + }, + { + name: client.i18n.translate("administration/config:TICKETLOGS"), + value: "tickets.ticketLogs", + }, + { + name: client.i18n.translate("administration/config:TRANSCRIPTIONLOGS"), + value: "tickets.transcriptionLogs", + }, + { + name: client.i18n.translate("administration/config:MESSAGEUPDATE"), + value: "monitoring.messageUpdate", + }, + { + name: client.i18n.translate("administration/config:MESSAGEDELETE"), + value: "monitoring.messageDelete", + }, ], }, { @@ -119,31 +155,53 @@ export const run = async ({ interaction }: SlashCommandProps) => { return interaction.editReply({ embeds: [embed] }); } - const parameter = interaction.options.getString("parameter", true); + const parameter = interaction.options.getString("parameter", true) as keyof Plugins; const state = interaction.options.getBoolean("state", true); const channel = interaction.options.getChannel("channel") as Channel; await changeSetting(interaction, guildData, parameter, state, channel); }; -async function saveSettings(guildData: InstanceType, parameter: string, value: unknown) { - guildData.plugins[parameter] = value; - guildData.markModified(`plugins.${parameter}`); +async function saveSettings( + guildData: InstanceType, + parameter: keyof Plugins, + value: unknown, +) { + guildData.plugins[parameter] = value as any; await guildData.save(); } -async function generateReply(interaction: ChatInputCommandInteraction, guildData: InstanceType, parameter: string, state: boolean, channel?: Channel | null) { - const translatedParam = await translateContext(interaction, `administration/config:${parameter.toUpperCase()}`); +async function generateReply( + interaction: ChatInputCommandInteraction, + guildData: InstanceType, + parameter: string, + state: boolean, + channel?: Channel | null, +) { + const translatedParam = await translateContext( + interaction, + `administration/config:${parameter.toUpperCase()}`, + ); const enabledText = await translateContext(interaction, "common:ENABLED"); const disabledText = await translateContext(interaction, "common:DISABLED"); if (channel) return `${translatedParam}: **${enabledText}** (${channel.toString()})`; - return `${translatedParam}: ${state ? `**${enabledText}** (<#${guildData.plugins[parameter]}>)` : `**${disabledText}**`}`; + return `${translatedParam}: ${ + state + ? `**${enabledText}** (<#${guildData.plugins[parameter as keyof Plugins]}>)` + : `**${disabledText}**` + }`; } -async function changeSetting(interaction: ChatInputCommandInteraction, guildData: any, parameter: string, state: boolean, channel?: Channel | null) { +async function changeSetting( + interaction: ChatInputCommandInteraction, + guildData: any, + parameter: string, + state: boolean, + channel?: Channel | null, +) { const parameterSplitted = parameter.split("."); const isNested = parameterSplitted.length === 2; @@ -152,24 +210,42 @@ async function changeSetting(interaction: ChatInputCommandInteraction, guildData } if (!state) { - await saveSettings(guildData, parameter, null); + await saveSettings(guildData, parameter as keyof Plugins, null); return interaction.editReply({ - content: await generateReply(interaction, guildData, parameterSplitted[isNested ? 1 : 0], state), + content: await generateReply( + interaction, + guildData, + parameterSplitted[isNested ? 1 : 0], + state, + ), }); } - if (isNested && parameterSplitted[1] === "ticketsCategory" && channel?.type !== ChannelType.GuildCategory) { + if ( + isNested && + parameterSplitted[1] === "ticketsCategory" && + channel?.type !== ChannelType.GuildCategory + ) { return interaction.editReply({ - content: await translateContext(interaction, "administration/config:TICKETS_NOT_CATEGORY"), + content: await translateContext( + interaction, + "administration/config:TICKETS_NOT_CATEGORY", + ), }); } if (channel) { - await saveSettings(guildData, parameter, channel.id); + await saveSettings(guildData, parameter as keyof Plugins, channel.id); } return interaction.editReply({ - content: await generateReply(interaction, guildData, parameterSplitted[isNested ? 1 : 0], state, channel), + content: await generateReply( + interaction, + guildData, + parameterSplitted[isNested ? 1 : 0], + state, + channel, + ), }); } diff --git a/src/helpers/tasks/birthdays.ts b/src/helpers/tasks/birthdays.ts index 9580ba19..278f0226 100644 --- a/src/helpers/tasks/birthdays.ts +++ b/src/helpers/tasks/birthdays.ts @@ -1,5 +1,5 @@ import useClient from "@/utils/use-client.js"; -import UserModel from "@/models/UserModel.js"; +import { User } from "@/models/UserModel.js"; import { createEmbed } from "@/utils/create-embed.js"; import logger from "../logger.js"; import { getNoun } from "../functions.js"; @@ -10,7 +10,7 @@ export const data = { const client = useClient(); const guilds = client.guilds.cache.values(); - const users = await client.adapter.find(UserModel, { + const users = await client.adapter.find(User, { birthdate: { $ne: null }, }); @@ -22,7 +22,9 @@ export const data = { for (const guild of guilds) { try { const data = await client.getGuildData(guild.id); - const channel = data.plugins.birthdays ? await client.channels.fetch(data.plugins.birthdays) : null; + const channel = data.plugins.birthdays + ? await client.channels.fetch(data.plugins.birthdays) + : null; if (!channel) return; @@ -35,7 +37,10 @@ export const data = { const user = users.find(u => u.id === userID); if (!user) return; - const userData = new Date(user.birthdate!).getFullYear() <= 1970 ? new Date(user.birthdate! * 1000) : new Date(user.birthdate!); + const userData = + new Date(user.birthdate!).getFullYear() <= 1970 + ? new Date(user.birthdate! * 1000) + : new Date(user.birthdate!); const userYear = userData.getFullYear(); const userMonth = userData.getMonth(); const userDate = userData.getDate(); @@ -49,14 +54,21 @@ export const data = { }, fields: [ { - name: client.i18n.translate("economy/birthdate:HAPPY_BIRTHDAY", { - lng: data.language, - }), - value: client.i18n.translate("economy/birthdate:HAPPY_BIRTHDAY_MESSAGE", { - lng: data.language, - user: user.id, - age: `**${age}** ${getNoun(age, [client.i18n.translate("misc:NOUNS:AGE:1", data.language), client.i18n.translate("misc:NOUNS:AGE:2", data.language), client.i18n.translate("misc:NOUNS:AGE:5", data.language)])}`, - }), + name: client.i18n.translate( + "economy/birthdate:HAPPY_BIRTHDAY", + { + lng: data.language, + }, + ), + value: client.i18n.translate( + "economy/birthdate:HAPPY_BIRTHDAY_MESSAGE", + { + lng: data.language, + user: user.id, + // eslint-disable-next-line max-len + age: `**${age}** ${getNoun(age, [client.i18n.translate("misc:NOUNS:AGE:1", data.language), client.i18n.translate("misc:NOUNS:AGE:2", data.language), client.i18n.translate("misc:NOUNS:AGE:5", data.language)])}`, + }, + ), }, ], }); diff --git a/src/helpers/tasks/checkReminds.ts b/src/helpers/tasks/checkReminds.ts index ac3f1923..54244e53 100644 --- a/src/helpers/tasks/checkReminds.ts +++ b/src/helpers/tasks/checkReminds.ts @@ -1,5 +1,5 @@ import { createEmbed } from "@/utils/create-embed.js"; -import UserModel from "../../models/UserModel.js"; +import { User } from "../../models/UserModel.js"; import useClient from "../../utils/use-client.js"; import { CronTaskData } from "@/types.js"; @@ -8,7 +8,7 @@ export const data: CronTaskData = { task: async () => { const client = useClient(); - const users = await client.adapter.find(UserModel, { reminds: { $gt: [] } }); + const users = await client.adapter.find(User, { reminds: { $exists: true, $ne: null } }); for (const user of users) { if (!client.users.cache.has(user.id)) { @@ -58,7 +58,7 @@ export const data: CronTaskData = { }); await client.adapter.updateOne( - UserModel, + User, { id }, { $pull: { diff --git a/src/models/BaseModel.ts b/src/models/BaseModel.ts new file mode 100644 index 00000000..fb93c2b8 --- /dev/null +++ b/src/models/BaseModel.ts @@ -0,0 +1,20 @@ +import { EntityManager } from "@mikro-orm/mongodb"; + +export abstract class BaseEntity { + static em: EntityManager; + + static useEntityManager(em: EntityManager) { + this.em = em; + } + + async save(): Promise { + const ctor = this.constructor as typeof BaseEntity; + ctor.em.persist(this); + await ctor.em.flush(); + return this; + } + async remove(): Promise { + const ctor = this.constructor as typeof BaseEntity; + await ctor.em.removeAndFlush(this); + } +} diff --git a/src/models/GuildModel.ts b/src/models/GuildModel.ts index 629b2629..656fbe32 100644 --- a/src/models/GuildModel.ts +++ b/src/models/GuildModel.ts @@ -1,67 +1,111 @@ -import { model, Schema, Types } from "mongoose"; +import { Entity, OneToMany, Property } from "@mikro-orm/core"; +import { Member } from "./MemberModel.js"; import useClient from "@/utils/use-client.js"; +import { BaseEntity } from "./BaseModel.js"; const client = useClient(); -export interface IGuildSchema { - id: string; - membersData: Record; - members: Types.ObjectId[]; - language: string; - plugins: any; -} +export type Plugins = { + welcome: { + enabled: boolean; + message: string | null; + channel: string | null; + withImage: string | null; + }; + goodbye: { + enabled: boolean; + message: string | null; + channel: string | null; + withImage: string | null; + }; + autorole: { + enabled: boolean; + role: string | null; + }; + automod: { + enabled: boolean; + ignored: string[]; + }; + warnsSanctions: { + kick: string | null; + ban: string | null; + }; + monitoring: { + messageUpdate: string | null; + messageDelete: string | null; + }; + tickets: { + count: number; + ticketLogs: string | null; + transcriptionLogs: string | null; + ticketsCategory: string | null; + }; + suggestions: string | null; + reports: string | null; + birthdays: string | null; + modlogs: string | null; +}; + +@Entity({ collection: "guilds" }) +export class Guild extends BaseEntity { + @Property({ type: "ObjectId", primary: true }) + _id!: string; + + @Property({ type: "string", unique: true }) + id!: string; + + @Property({ type: "json" }) + membersData: Record = {}; -const GuildSchema = new Schema({ - id: { type: String, required: true }, - membersData: { type: Object, default: {} }, - members: [{ type: Schema.Types.ObjectId, ref: "Member" }], - language: { - type: String, - default: client.configService.get("defaultLang"), - }, - plugins: { - type: Object, - default: { - welcome: { - enabled: false, - message: null, - channel: null, - withImage: null, - }, - goodbye: { - enabled: false, - message: null, - channel: null, - withImage: null, - }, - autorole: { - enabled: false, - role: null, - }, - automod: { - enabled: false, - ignored: [], - }, - warnsSanctions: { - kick: null, - ban: null, - }, - monitoring: { - messageUpdate: null, - messageDelete: null, - }, - tickets: { - count: 0, - ticketLogs: null, - transcriptionLogs: null, - ticketsCategory: null, - }, - suggestions: null, - reports: null, - birthdays: null, - modlogs: null, + @OneToMany(() => Member, (member: Member) => member.guildID) + members = new Array(); + + @Property({ type: "string" }) + language: string = client.configService.get("defaultLang"); + + @Property({ type: "json" }) + plugins: Plugins = { + welcome: { + enabled: false, + message: null, + channel: null, + withImage: null, + }, + goodbye: { + enabled: false, + message: null, + channel: null, + withImage: null, }, - }, -}); + autorole: { + enabled: false, + role: null, + }, + automod: { + enabled: false, + ignored: [], + }, + warnsSanctions: { + kick: null, + ban: null, + }, + monitoring: { + messageUpdate: null, + messageDelete: null, + }, + tickets: { + count: 0, + ticketLogs: null, + transcriptionLogs: null, + ticketsCategory: null, + }, + suggestions: null, + reports: null, + birthdays: null, + modlogs: null, + }; -export default model("Guild", GuildSchema); + hasMember(memberId: string): boolean { + return this.members.some(member => member.id === memberId); + } +} diff --git a/src/models/MemberModel.ts b/src/models/MemberModel.ts index 01afc0f8..4f2125b6 100644 --- a/src/models/MemberModel.ts +++ b/src/models/MemberModel.ts @@ -1,65 +1,65 @@ -import { Schema, model } from "mongoose"; - -export interface IMemberSchema { - id: string; - guildID: string; - money: number; - workStreak: number; - bankSold: number; - exp: number; - level: number; - transactions: Array<{ - user: string; - amount: number; - date: number; - type: string; - }>; - registeredAt: number; - cooldowns: { - work: number; - rob: number; - }; - sanctions: any[][]; - mute: { - muted: boolean; - case: string | null; - endDate: number | null; - }; +import type { ObjectId } from "mongodb"; +import { Entity, ManyToOne, Property } from "@mikro-orm/core"; +import { Guild } from "./GuildModel.js"; +import { BaseEntity } from "./BaseModel.js"; + +// Вложенные интерфейсы для типизации +interface Transaction { + user: string; + amount: number; + date: number; + type: string; +} + +interface Cooldowns { + work: number; + rob: number; +} + +interface Mute { + muted: boolean; + case: string | null; + endDate: number | null; } -const memberSchema = new Schema({ - id: { type: String, required: true }, - guildID: { type: String, required: true, ref: "Guild" }, - money: { type: Number, default: 0 }, - workStreak: { type: Number, default: 0 }, - bankSold: { type: Number, default: 0 }, - exp: { type: Number, default: 0 }, - level: { type: Number, default: 0 }, - transactions: [ - { - user: String, - amount: Number, - date: Number, - type: String, - }, - ], - registeredAt: { type: Number, default: () => Date.now() }, - cooldowns: { - type: Object, - default: { - work: 0, - rob: 0, - }, - }, - sanctions: [[String]], - mute: { - type: Object, - default: { - muted: false, - case: null, - endDate: null, - }, - }, -}); - -export default model("Member", memberSchema); +@Entity({ collection: "members" }) +export class Member extends BaseEntity { + @Property({ type: "ObjectId", primary: true }) + _id!: ObjectId; + + @Property({ type: "string", unique: true }) + id!: string; + + @ManyToOne(() => Guild) + guildID!: string; + + @Property({ type: "int", default: 0 }) + money = 0; + + @Property({ type: "int", default: 0 }) + workStreak = 0; + + @Property({ type: "int", default: 0 }) + bankSold = 0; + + @Property({ type: "int", default: 0 }) + exp = 0; + + @Property({ type: "int", default: 0 }) + level = 0; + + @Property({ type: "json" }) // Храним как JSON-массив + transactions: Transaction[] = []; + + @Property({ type: "int", onCreate: () => Date.now() }) // Дефолтное значение + registeredAt!: number; + + @Property({ type: "json" }) // Объект как JSON + cooldowns: Cooldowns = { work: 0, rob: 0 }; + + @Property({ type: "json", default: [] }) // Массив массивов + sanctions: any[][] = []; + + @Property({ type: "json" }) // Объект + mute: Mute = { muted: false, case: null, endDate: null }; +} diff --git a/src/models/UserModel.ts b/src/models/UserModel.ts index 9e4826c4..f805a0c2 100644 --- a/src/models/UserModel.ts +++ b/src/models/UserModel.ts @@ -1,6 +1,8 @@ /* eslint-disable max-len */ -import { Schema, model } from "mongoose"; +import { Entity, Property } from "@mikro-orm/core"; import { createCanvas, loadImage } from "@napi-rs/canvas"; +import { ObjectId } from "mongodb"; +import { BaseEntity } from "./BaseModel.js"; export type UserReminds = { message: string; @@ -8,30 +10,61 @@ export type UserReminds = { sendAt: number; }; -interface IUserSchema { - id: string; - rep: number; - bio: string; - birthdate: number | null; - lover: string; - registeredAt: number; - achievements: { - [key: string]: { - achieved: boolean; - progress: { - now: number; - total: number; - }; +type UserAchievements = { + married: { + achieved: boolean; + progress: { + now: number; + total: number; }; }; - cooldowns: { - rep: number; + work: { + achieved: boolean; + progress: { + now: number; + total: number; + }; }; - afk: string; - reminds: UserReminds[]; - logged: boolean; - apiToken: string; -} + firstCommand: { + achieved: boolean; + progress: { + now: number; + total: number; + }; + }; + slots: { + achieved: boolean; + progress: { + now: number; + total: number; + }; + }; + tip: { + achieved: boolean; + progress: { + now: number; + total: number; + }; + }; + rep: { + achieved: boolean; + progress: { + now: number; + total: number; + }; + }; + invite: { + achieved: boolean; + progress: { + now: number; + total: number; + }; + }; +}; + +type UserCooldowns = { + rep: number; +}; const genToken = () => { let token = ""; @@ -43,128 +76,135 @@ const genToken = () => { return token; }; -const userSchema = new Schema({ - id: { type: String, required: true }, - - rep: { type: Number, default: 0 }, - bio: { type: String }, - birthdate: { type: Number }, - lover: { type: String }, - - registeredAt: { type: Number, default: Date.now() }, - - achievements: { - type: Object, - default: { - married: { - achieved: false, - progress: { - now: 0, - total: 1, - }, - }, - work: { - achieved: false, - progress: { - now: 0, - total: 10, - }, - }, - firstCommand: { - achieved: false, - progress: { - now: 0, - total: 1, - }, +@Entity({ collection: "users" }) +export class User extends BaseEntity { + @Property({ type: "ObjectId", primary: true }) + _id!: ObjectId; + + @Property({ type: "string", unique: true }) + id!: string; + + @Property({ type: "number", default: 0 }) + rep: number = 0; + + @Property({ type: "string" }) + bio: string = ""; + + @Property({ type: "number", default: null }) + birthdate!: number | null; + + @Property({ type: "string" }) + lover: string = ""; + + @Property({ type: "number", onCreate: () => Date.now() }) + registeredAt!: number; + + @Property({ type: "json" }) + achievements: UserAchievements = { + married: { + achieved: false, + progress: { + now: 0, + total: 1, }, - slots: { - achieved: false, - progress: { - now: 0, - total: 3, - }, + }, + work: { + achieved: false, + progress: { + now: 0, + total: 10, }, - tip: { - achieved: false, - progress: { - now: 0, - total: 1, - }, + }, + firstCommand: { + achieved: false, + progress: { + now: 0, + total: 1, }, - rep: { - achieved: false, - progress: { - now: 0, - total: 20, - }, + }, + slots: { + achieved: false, + progress: { + now: 0, + total: 3, }, - invite: { - achieved: false, - progress: { - now: 0, - total: 1, - }, + }, + tip: { + achieved: false, + progress: { + now: 0, + total: 1, }, }, - }, - - cooldowns: { - type: Object, - default: { - rep: 0, + rep: { + achieved: false, + progress: { + now: 0, + total: 20, + }, }, - }, - - afk: { type: String, default: null }, - reminds: [ - { - type: Object, - default: { - message: null, - createdAt: null, - sendAt: null, + invite: { + achieved: false, + progress: { + now: 0, + total: 1, }, }, - ], - logged: { type: Boolean, default: false }, - apiToken: { type: String, default: genToken() }, -}); - -userSchema.method("getAchievements", async function () { - const canvas = createCanvas(1800, 250), - ctx = canvas.getContext("2d"); - - const images = [ - await loadImage( - `./assets/img/achievements/achievement${this.achievements.work.achieved ? "_colored" : ""}1.png`, - ), - await loadImage( - `./assets/img/achievements/achievement${this.achievements.firstCommand.achieved ? "_colored" : ""}2.png`, - ), - await loadImage( - `./assets/img/achievements/achievement${this.achievements.married.achieved ? "_colored" : ""}3.png`, - ), - await loadImage( - `./assets/img/achievements/achievement${this.achievements.slots.achieved ? "_colored" : ""}4.png`, - ), - await loadImage( - `./assets/img/achievements/achievement${this.achievements.tip.achieved ? "_colored" : ""}5.png`, - ), - await loadImage( - `./assets/img/achievements/achievement${this.achievements.rep.achieved ? "_colored" : ""}6.png`, - ), - await loadImage( - `./assets/img/achievements/achievement${this.achievements.invite.achieved ? "_colored" : ""}7.png`, - ), - ]; - let dim = 0; - - for (let i = 0; i < images.length; i++) { - ctx.drawImage(images[i], dim, 10, 350, 200); - dim += 200; - } + }; + + @Property({ type: "json" }) + cooldowns: UserCooldowns = { + rep: 0, + }; + + @Property({ type: "string", default: null }) + afk!: string | null; + + @Property({ type: "array", default: [] }) + reminds: UserReminds[] = []; + + @Property({ type: "boolean" }) + logged: boolean = false; - return await canvas.encode("png"); -}); + @Property({ type: "string", onCreate: () => genToken() }) + token!: string; -export default model("User", userSchema); + async getAchievements() { + const canvas = createCanvas(1800, 250), + ctx = canvas.getContext("2d"); + + const images = [ + await loadImage( + `./assets/img/achievements/achievement${this.achievements.work.achieved ? "_colored" : ""}1.png`, + ), + await loadImage( + `./assets/img/achievements/achievement${this.achievements.firstCommand.achieved ? "_colored" : ""}2.png`, + ), + await loadImage( + `./assets/img/achievements/achievement${this.achievements.married.achieved ? "_colored" : ""}3.png`, + ), + await loadImage( + `./assets/img/achievements/achievement${this.achievements.slots.achieved ? "_colored" : ""}4.png`, + ), + await loadImage( + `./assets/img/achievements/achievement${this.achievements.tip.achieved ? "_colored" : ""}5.png`, + ), + await loadImage( + `./assets/img/achievements/achievement${this.achievements.rep.achieved ? "_colored" : ""}6.png`, + ), + await loadImage( + `./assets/img/achievements/achievement${this.achievements.invite.achieved ? "_colored" : ""}7.png`, + ), + ]; + let dim = 0; + + for (let i = 0; i < images.length; i++) { + ctx.drawImage(images[i], dim, 10, 350, 200); + dim += 200; + } + + const encodedPhoto = await canvas.encode("png"); + + return encodedPhoto; + } +} diff --git a/src/structures/client.ts b/src/structures/client.ts index 80b1ee35..503def20 100644 --- a/src/structures/client.ts +++ b/src/structures/client.ts @@ -1,17 +1,17 @@ import { Client, ClientOptions } from "discord.js"; import { GiveawaysManager } from "discord-giveaways"; import { Handlers } from "@/handlers/index.js"; -import MongooseAdapter from "@/adapters/database/MongooseAdapter.js"; import logger from "@/helpers/logger.js"; import ConfigService from "@/services/config/index.js"; import InternationalizationService from "@/services/languages/index.js"; import { SUPER_CONTEXT } from "@/constants/index.js"; import { cacheRemindsData } from "@/types.js"; +import MikroOrmAdapter from "@/adapters/database/MikroOrmAdapter.js"; // import { Player } from "discord-player"; export class ExtendedClient extends Client { configService = new ConfigService(); - adapter = new MongooseAdapter(this.configService.get("mongoDB")); + adapter = new MikroOrmAdapter(this.configService.get("mongoDB")); cacheReminds = new Map(); i18n = new InternationalizationService(this); @@ -49,21 +49,21 @@ export class ExtendedClient extends Client { } async getGuildData(guildId: string) { - const { default: GuildModel } = await import("@/models/GuildModel.js"); + const { Guild: GuildModel } = await import("@/models/GuildModel.js"); const guildData = await this.adapter.findOneOrCreate(GuildModel, { id: guildId }); return guildData; } async getUserData(userID: string) { - const { default: UserModel } = await import("@/models/UserModel.js"); + const { User: UserModel } = await import("@/models/UserModel.js"); const userData = await this.adapter.findOneOrCreate(UserModel, { id: userID }); return userData; } async getMemberData(memberId: string, guildID: string) { - const { default: MemberModel } = await import("@/models/MemberModel.js"); + const { Member: MemberModel } = await import("@/models/MemberModel.js"); const memberData = await this.adapter.findOneOrCreate(MemberModel, { id: memberId, guildID, @@ -71,9 +71,9 @@ export class ExtendedClient extends Client { const guildData = await this.getGuildData(guildID); - if (!guildData.members.includes(memberData._id)) { - guildData.members.push(memberData._id); - await guildData.save(); + if (!guildData.hasMember(memberData.id)) { + guildData.members.push(memberData); + await this.adapter.em.flush(); } return memberData; diff --git a/src/utils/config-fields.ts b/src/utils/config-fields.ts index 5da5849c..f1973b7b 100644 --- a/src/utils/config-fields.ts +++ b/src/utils/config-fields.ts @@ -1,5 +1,5 @@ import { translateContext } from "@/helpers/extenders.js"; -import GuildModel from "@/models/GuildModel.js"; +import { Guild } from "@/models/GuildModel.js"; import { APIEmbedField, ChatInputCommandInteraction } from "discord.js"; enum ConfigFieldsType { @@ -29,7 +29,10 @@ type ConfigGroupField = { type ConfigField = ConfigPluginField | ConfigGroupField; -const processPluginField = async (interaction: ChatInputCommandInteraction, field: ConfigPluginField) => { +const processPluginField = async ( + interaction: ChatInputCommandInteraction, + field: ConfigPluginField, +) => { const name = await translateContext(interaction, field.nameKey); const value = field.enabled @@ -41,7 +44,10 @@ const processPluginField = async (interaction: ChatInputCommandInteraction, fiel return { name, value, inline: true }; }; -const processGroupField = async (interaction: ChatInputCommandInteraction, field: ConfigGroupField) => { +const processGroupField = async ( + interaction: ChatInputCommandInteraction, + field: ConfigGroupField, +) => { const name = await translateContext(interaction, field.nameKey); const lines = await Promise.all( field.items.map(async item => { @@ -51,7 +57,9 @@ const processGroupField = async (interaction: ChatInputCommandInteraction, field let formattedValue; switch (item.format) { case "channel": - formattedValue = rawValue ? `<#${rawValue}>` : `*${await translateContext(interaction, "common:NOT_DEFINED")}*`; + formattedValue = rawValue + ? `<#${rawValue}>` + : `*${await translateContext(interaction, "common:NOT_DEFINED")}*`; break; default: formattedValue = rawValue?.toString() || "N/A"; @@ -64,21 +72,24 @@ const processGroupField = async (interaction: ChatInputCommandInteraction, field return { name, value: lines.join("\n"), inline: false }; }; -export const generateFields = async (interaction: ChatInputCommandInteraction, guildData: InstanceType) => { +export const generateFields = async ( + interaction: ChatInputCommandInteraction, + guildData: InstanceType, +) => { const fieldsConfig: ConfigField[] = [ { type: ConfigFieldsType.plugin, nameKey: "administration/config:WELCOME_TITLE", valueKey: "administration/config:WELCOME_CONTENT", enabled: guildData.plugins.welcome.enabled, - data: { channel: guildData.plugins.welcome.channel }, + data: { channel: guildData.plugins.welcome.channel! }, }, { type: ConfigFieldsType.plugin, nameKey: "administration/config:GOODBYE_TITLE", valueKey: "administration/config:GOODBYE_CONTENT", enabled: guildData.plugins.goodbye.enabled, - data: { channel: guildData.plugins.goodbye.channel }, + data: { channel: guildData.plugins.goodbye.channel! }, }, { type: ConfigFieldsType.group, @@ -86,12 +97,12 @@ export const generateFields = async (interaction: ChatInputCommandInteraction, g items: [ { key: "administration/config:MESSAGEUPDATE", - path: guildData.plugins.monitoring.messageUpdate, + path: guildData.plugins.monitoring.messageUpdate!, format: "channel", }, { key: "administration/config:MESSAGEDELETE", - path: guildData.plugins.monitoring.messageDelete, + path: guildData.plugins.monitoring.messageDelete!, format: "channel", }, ], @@ -100,13 +111,41 @@ export const generateFields = async (interaction: ChatInputCommandInteraction, g type: ConfigFieldsType.group, nameKey: "administration/config:SPECIAL_CHANNELS", items: [ - { key: "administration/config:BIRTHDAYS", path: guildData.plugins.birthdays, format: "channel" }, - { key: "administration/config:MODLOGS", path: guildData.plugins.modlogs, format: "channel" }, - { key: "administration/config:REPORTS", path: guildData.plugins.reports, format: "channel" }, - { key: "administration/config:SUGGESTIONS", path: guildData.plugins.suggestions, format: "channel" }, - { key: "administration/config:TICKETSCATEGORY", path: guildData.plugins.tickets.ticketsCategory, format: "channel" }, - { key: "administration/config:TICKETLOGS", path: guildData.plugins.tickets.ticketLogs, format: "channel" }, - { key: "administration/config:TRANSCRIPTIONLOGS", path: guildData.plugins.tickets.transcriptionLogs, format: "channel" }, + { + key: "administration/config:BIRTHDAYS", + path: guildData.plugins.birthdays!, + format: "channel", + }, + { + key: "administration/config:MODLOGS", + path: guildData.plugins.modlogs!, + format: "channel", + }, + { + key: "administration/config:REPORTS", + path: guildData.plugins.reports!, + format: "channel", + }, + { + key: "administration/config:SUGGESTIONS", + path: guildData.plugins.suggestions!, + format: "channel", + }, + { + key: "administration/config:TICKETSCATEGORY", + path: guildData.plugins.tickets.ticketsCategory!, + format: "channel", + }, + { + key: "administration/config:TICKETLOGS", + path: guildData.plugins.tickets.ticketLogs!, + format: "channel", + }, + { + key: "administration/config:TRANSCRIPTIONLOGS", + path: guildData.plugins.tickets.transcriptionLogs!, + format: "channel", + }, ], }, ]; diff --git a/tsconfig.json b/tsconfig.json index 606dd8cf..fef9a320 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,11 +14,13 @@ "incremental": true, // Инкрементальная сборка (ускоряет повторную компиляцию, сохраняя результаты предыдущих компиляции) "noEmitOnError": true, // Чтобы не билдил без ошибок "noUnusedLocals": true, // Не использовать неиспользуемые локальные переменные - "noUnusedParameters": true, + "noUnusedParameters": true, + "declaration": true, // Генерация файлов деклараций (.d.ts) + "experimentalDecorators": true, // Включение экспериментальных декораторов "paths": { "@/*": ["./src/*"] } }, - "include": ["src/**/*"], // Включаемые файлы - "exclude": ["node_modules"] // Исключаемые файлы + "include": ["src/**/*.ts"], // Включаемые файлы + "exclude": ["node_modules"], // Исключаемые файлы } From adf3c893957194c6f88bd67d9a4b8aa57eec8b51 Mon Sep 17 00:00:00 2001 From: Slincnik Date: Sat, 3 May 2025 23:11:34 +0300 Subject: [PATCH 2/4] style: changed naming --- src/structures/client.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/structures/client.ts b/src/structures/client.ts index 9684ba2f..d33a7f94 100644 --- a/src/structures/client.ts +++ b/src/structures/client.ts @@ -49,29 +49,29 @@ export class ExtendedClient extends Client { } async getGuildData(guildId: string) { - const { Guild: GuildModel } = await import("@/models/GuildModel.js"); - const guildData = await this.adapter.findOneOrCreate(GuildModel, { id: guildId }); + const { Guild } = await import("@/models/GuildModel.js"); + const guildData = await this.adapter.findOneOrCreate(Guild, { id: guildId }); return guildData; } async getUserData(userID: string) { - const { User: UserModel } = await import("@/models/UserModel.js"); - const userData = await this.adapter.findOneOrCreate(UserModel, { id: userID }); + const { User } = await import("@/models/UserModel.js"); + const userData = await this.adapter.findOneOrCreate(User, { id: userID }); return userData; } async getUsersData() { - const { default: UserModel } = await import("@/models/UserModel.js"); - const usersData = await this.adapter.find(UserModel); + const { User } = await import("@/models/UserModel.js"); + const usersData = await this.adapter.find(User); return usersData; } async getMemberData(memberId: string, guildID: string) { - const { Member: MemberModel } = await import("@/models/MemberModel.js"); - const memberData = await this.adapter.findOneOrCreate(MemberModel, { + const { Member } = await import("@/models/MemberModel.js"); + const memberData = await this.adapter.findOneOrCreate(Member, { id: memberId, guildID, }); @@ -87,8 +87,8 @@ export class ExtendedClient extends Client { } async getMembersData(guildID: string) { - const { default: MemberModel } = await import("@/models/MemberModel.js"); - const membersData = await this.adapter.find(MemberModel, { + const { Member } = await import("@/models/MemberModel.js"); + const membersData = await this.adapter.find(Member, { guildID, }); From 9b60414168aed19edf1463da3c47962b9eb65862 Mon Sep 17 00:00:00 2001 From: Slincnik Date: Sun, 4 May 2025 23:52:03 +0300 Subject: [PATCH 3/4] style: some changed models --- src/models/MemberModel.ts | 18 +++++++++--------- src/models/UserModel.ts | 10 +++++----- src/structures/client.ts | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/models/MemberModel.ts b/src/models/MemberModel.ts index 419df455..4502cc80 100644 --- a/src/models/MemberModel.ts +++ b/src/models/MemberModel.ts @@ -33,31 +33,31 @@ export class Member extends BaseEntity { @ManyToOne(() => Guild) guildID!: string; - @Property({ type: "int", default: 0 }) + @Property({ type: "int" }) money = 0; - @Property({ type: "int", default: 0 }) + @Property({ type: "int" }) workStreak = 0; - @Property({ type: "int", default: 0 }) + @Property({ type: "int" }) bankSold = 0; - @Property({ type: "int", default: 0 }) + @Property({ type: "int" }) exp = 0; - @Property({ type: "int", default: 0 }) + @Property({ type: "int" }) level = 0; - @Property({ type: "json" }) // Храним как JSON-массив + @Property({ type: "array" }) transactions: Transaction[] = []; - @Property({ type: "int", onCreate: () => Date.now() }) // Дефолтное значение + @Property({ type: "int", onCreate: () => Date.now() }) registeredAt!: number; - @Property({ type: "json" }) // Объект как JSON + @Property({ type: "json" }) cooldowns: Cooldowns = { work: 0, rob: 0 }; - @Property({ type: "json", default: [] }) // Массив массивов + @Property({ type: "array" }) sanctions: any[][] = []; @Property({ type: "json" }) // Объект diff --git a/src/models/UserModel.ts b/src/models/UserModel.ts index 5b611bc3..cb9c890e 100644 --- a/src/models/UserModel.ts +++ b/src/models/UserModel.ts @@ -84,14 +84,14 @@ export class User extends BaseEntity { @Property({ type: "string", unique: true }) id!: string; - @Property({ type: "number", default: 0 }) + @Property({ type: "number" }) rep: number = 0; @Property({ type: "string" }) bio: string = ""; - @Property({ type: "number", default: null }) - birthdate!: number | null; + @Property({ type: "number" }) + birthdate: number | null = null; @Property({ type: "string" }) lover: string = ""; @@ -157,8 +157,8 @@ export class User extends BaseEntity { rep: 0, }; - @Property({ type: "string", default: null }) - afk!: string | null; + @Property({ type: "string" }) + afk: string | null = null; @Property({ type: "array" }) reminds: UserReminds[] = []; diff --git a/src/structures/client.ts b/src/structures/client.ts index 40262320..a85a693e 100644 --- a/src/structures/client.ts +++ b/src/structures/client.ts @@ -68,7 +68,7 @@ export class ExtendedClient extends Client { if (!guildData.hasMember(memberData.id)) { guildData.members.push(memberData); - await this.adapter.em.flush(); + await guildData.save(); } return memberData; From c19e00e193896842db67e0af203e232562cc21f3 Mon Sep 17 00:00:00 2001 From: Slincnik Date: Mon, 5 May 2025 00:15:09 +0300 Subject: [PATCH 4/4] build: little changes to model --- src/models/GuildModel.ts | 8 +-- src/models/MemberModel.ts | 2 +- src/models/UserModel.ts | 107 ++++++++++++++++++++------------------ src/structures/client.ts | 3 +- 4 files changed, 62 insertions(+), 58 deletions(-) diff --git a/src/models/GuildModel.ts b/src/models/GuildModel.ts index 48e36120..caeec93a 100644 --- a/src/models/GuildModel.ts +++ b/src/models/GuildModel.ts @@ -1,4 +1,4 @@ -import { Entity, OneToMany, Property } from "@mikro-orm/core"; +import { Entity, EntityManager, OneToMany, Property } from "@mikro-orm/core"; import { Member } from "./MemberModel.js"; import useClient from "@/utils/use-client.js"; import { BaseEntity } from "../structures/BaseEntity.js"; @@ -51,7 +51,7 @@ export class Guild extends BaseEntity { @Property({ type: "ObjectId", primary: true }) _id!: string; - @Property({ type: "string", unique: true }) + @Property({ type: "string", unique: true, index: true }) id!: string; @Property({ type: "json" }) @@ -105,7 +105,7 @@ export class Guild extends BaseEntity { modlogs: null, }; - hasMember(memberId: string): boolean { - return this.members.some(member => member.id === memberId); + async hasMember(memberId: string, em: EntityManager): Promise { + return (await em.count(Member, { id: memberId, guildID: this.id })) > 0; } } diff --git a/src/models/MemberModel.ts b/src/models/MemberModel.ts index 4502cc80..a5fa672e 100644 --- a/src/models/MemberModel.ts +++ b/src/models/MemberModel.ts @@ -30,7 +30,7 @@ export class Member extends BaseEntity { @Property({ type: "string", unique: true }) id!: string; - @ManyToOne(() => Guild) + @ManyToOne(() => Guild, { index: true }) guildID!: string; @Property({ type: "int" }) diff --git a/src/models/UserModel.ts b/src/models/UserModel.ts index cb9c890e..a4c0e114 100644 --- a/src/models/UserModel.ts +++ b/src/models/UserModel.ts @@ -81,17 +81,17 @@ export class User extends BaseEntity { @Property({ type: "ObjectId", primary: true }) _id!: ObjectId; - @Property({ type: "string", unique: true }) + @Property({ type: "string", unique: true, index: true }) id!: string; @Property({ type: "number" }) rep: number = 0; - @Property({ type: "string" }) - bio: string = ""; + @Property({ type: "string", nullable: true }) + bio?: string; - @Property({ type: "number" }) - birthdate: number | null = null; + @Property({ type: "number", nullable: true }) + birthdate!: number | null; @Property({ type: "string" }) lover: string = ""; @@ -99,58 +99,61 @@ export class User extends BaseEntity { @Property({ type: "number", onCreate: () => Date.now() }) registeredAt!: number; - @Property({ type: "json" }) - achievements: UserAchievements = { - married: { - achieved: false, - progress: { - now: 0, - total: 1, + @Property({ + type: "json", + onCreate: () => ({ + married: { + achieved: false, + progress: { + now: 0, + total: 1, + }, }, - }, - work: { - achieved: false, - progress: { - now: 0, - total: 10, + work: { + achieved: false, + progress: { + now: 0, + total: 10, + }, }, - }, - firstCommand: { - achieved: false, - progress: { - now: 0, - total: 1, + firstCommand: { + achieved: false, + progress: { + now: 0, + total: 1, + }, }, - }, - slots: { - achieved: false, - progress: { - now: 0, - total: 3, + slots: { + achieved: false, + progress: { + now: 0, + total: 3, + }, }, - }, - tip: { - achieved: false, - progress: { - now: 0, - total: 1, + tip: { + achieved: false, + progress: { + now: 0, + total: 1, + }, }, - }, - rep: { - achieved: false, - progress: { - now: 0, - total: 20, + rep: { + achieved: false, + progress: { + now: 0, + total: 20, + }, }, - }, - invite: { - achieved: false, - progress: { - now: 0, - total: 1, + invite: { + achieved: false, + progress: { + now: 0, + total: 1, + }, }, - }, - }; + }), + }) + achievements!: UserAchievements; @Property({ type: "json" }) cooldowns: UserCooldowns = { @@ -173,7 +176,7 @@ export class User extends BaseEntity { const canvas = createCanvas(1800, 250), ctx = canvas.getContext("2d"); - const images = [ + const images = await Promise.all([ await loadImage( `./assets/img/achievements/achievement${this.achievements.work.achieved ? "_colored" : ""}1.png`, ), @@ -195,7 +198,7 @@ export class User extends BaseEntity { await loadImage( `./assets/img/achievements/achievement${this.achievements.invite.achieved ? "_colored" : ""}7.png`, ), - ]; + ]); let dim = 0; for (let i = 0; i < images.length; i++) { diff --git a/src/structures/client.ts b/src/structures/client.ts index a85a693e..362eca2b 100644 --- a/src/structures/client.ts +++ b/src/structures/client.ts @@ -65,8 +65,9 @@ export class ExtendedClient extends Client { }); const guildData = await this.getGuildData(guildID); + const isMemberInGuild = await guildData.hasMember(memberData.id, this.adapter.em); - if (!guildData.hasMember(memberData.id)) { + if (!isMemberInGuild) { guildData.members.push(memberData); await guildData.save(); }