diff --git a/base/Client.js b/base/Client.js index 38076a36..8e1f042d 100644 --- a/base/Client.js +++ b/base/Client.js @@ -103,10 +103,10 @@ class JaBaClient extends Client { mongoose .connect(this.config.mongoDB) .then(() => { - this.logger.log("Connected to the Mongodb database."); + this.logger.log("Connected to the MongoDB database."); }) .catch(err => { - this.logger.error(`Unable to connect to the Mongodb database.\nError: ${err}`); + this.logger.error(`Unable to connect to the MongoDB database.\nError: ${err}`); }); this.login(this.config.token); @@ -115,9 +115,8 @@ class JaBaClient extends Client { /** * Loads all the commands from the specified directory and registers them with the Discord API. * - * This method is responsible for dynamically loading all the command files from the specified directory, - * creating instances of the corresponding command classes, and registering the commands with the Discord API. - * It also handles any additional setup or initialization required by the loaded commands. + * This method dynamically loads all command files from the specified directory, + * creates instances of the corresponding command classes, and registers them with the Discord API. * * @param {string} dir - The directory path where the command files are located. * @returns {Promise} A Promise that resolves when all the commands have been loaded and registered. @@ -125,43 +124,119 @@ class JaBaClient extends Client { async loadCommands(dir) { const rest = new REST().setToken(this.config.token), filePath = path.join(__dirname, dir), - folders = (await fs.readdir(filePath)).map(file => path.join(filePath, file)).filter(async path => (await fs.lstat(path)).isDirectory()); + folders = (await fs.readdir(filePath)).map(file => path.join(filePath, file)); const commands = []; - for (let index = 0; index < folders.length; index++) { - const folder = folders[index]; - - if (folder.endsWith("!DISABLED")) continue; - + for (const folder of folders) { const files = await fs.readdir(folder); - for (let index = 0; index < files.length; index++) { - const file = files[index]; + for (const file of files) { + if (!file.endsWith(".js")) continue; - if (file.endsWith(".js")) { - const Command = require(path.join(folder, file)); + const Command = require(path.join(folder, file)); - if (Command.prototype instanceof BaseCommand) { - const command = new Command(this); - this.commands.set(command.command.name, command); + if (!(Command.prototype instanceof BaseCommand)) continue; - if (command.onLoad && typeof command.onLoad === "function") await command.onLoad(this); + const command = new Command(this); + this.commands.set(command.command.name, command); - commands.push(command.command instanceof SlashCommandBuilder || command.command instanceof ContextMenuCommandBuilder ? command.command.toJSON() : command.command); + if (typeof command.onLoad === "function") await command.onLoad(this); - this.logger.log(`Successfully loaded "${file}" command file. (Command: ${command.command.name})`); - } - } + commands.push(command.command instanceof SlashCommandBuilder || command.command instanceof ContextMenuCommandBuilder ? command.command.toJSON() : command.command); + + this.logger.log(`Successfully loaded "${file}" command. (Command: ${command.command.name})`); } } try { - if (this.config.production) await rest.put(Routes.applicationCommands(this.config.userId), { body: commands }); - else await rest.put(Routes.applicationGuildCommands(this.config.userId, this.config.support.id), { body: commands }); + const route = this.config.production ? Routes.applicationCommands(this.config.userId) : Routes.applicationGuildCommands(this.config.userId, this.config.support.id); + + await rest.put(route, { body: commands }); this.logger.log("Successfully registered application commands."); } catch (err) { - console.log(err); + this.logger.error("Error registering application commands:", err); + } + } + + /** + * Loads a command from the specified directory and file. + * @param {string} dir - The directory containing the command file. + * @param {string} file - The name of the command file (without the .js extension). + * @returns {Promise} This method does not return a value. + */ + async loadCommand(dir, file) { + try { + const Command = require(path.join(dir, `${file}.js`)); + + if (!(Command.prototype instanceof BaseCommand)) { + return this.logger.error(`Tried to load a non-command file: "${file}.js"`); + } + + const command = new Command(this); + this.commands.set(command.command.name, command); + + if (typeof command.onLoad === "function") await command.onLoad(this); + + this.logger.log(`Successfully loaded "${file}" command file. (Command: ${command.command.name})`); + } catch (error) { + this.logger.error(`Error loading command "${file}":`, error); + } + } + + /** + * Unloads a command from the specified directory and file. + * @param {string} dir - The directory containing the command file. + * @param {string} name - The name of the command file (without the .js extension). + * @returns {void} This method does not return a value. + */ + unloadCommand(dir, name) { + delete require.cache[require.resolve(`${dir}${path.sep}${name}.js`)]; + + return; + } + + /** + * Loads all event files from the specified directory and its subdirectories. + * @param {string} dir - The directory containing the event files. + * @returns {Promise} This method does not return a value. + */ + async loadEvents(dir) { + const filePath = path.join(__dirname, dir); + const files = await fs.readdir(filePath); + + for (const file of files) { + const fullPath = path.join(filePath, file); + const stat = await fs.lstat(fullPath); + + if (stat.isDirectory()) { + await this.loadEvents(path.join(dir, file)); + continue; + } + + if (file.endsWith(".js")) { + try { + const Event = require(fullPath); + + if (!(Event.prototype instanceof BaseEvent)) { + this.logger.error(`"${file}" is not a valid event file.`); + continue; + } + + const event = new Event(); + + if (!event.name || !event.name.length) { + this.logger.error(`Cannot load "${file}" event: Event name is missing!`); + continue; + } + + event.once ? this.once(event.name, event.execute.bind(event, this)) : this.on(event.name, event.execute.bind(event, this)); + + this.logger.log(`Successfully loaded "${file}" event. (Event: ${event.name})`); + } catch (error) { + this.logger.error(`Error loading event "${file}":`, error); + } + } } } @@ -195,37 +270,24 @@ class JaBaClient extends Client { * @param {Object[]} [data.fields] - An array of field objects for the embed. * @param {string} [data.image] - The URL of the image for the embed. * @param {string} [data.url] - The URL to be used as the embed's hyperlink. - * @param {string} [data.color] - The HEX color of the embed's border. If not provided, the default color from the client's configuration will be used. - * @param {string} [data.footer] - The text to be displayed as the embed's footer. If not provided, the default footer from the client's configuration will be used. - * @param {Date} [data.timestamp] - The timestamp to be displayed in the embed's footer. If not provided, the current timestamp will be used. - * @param {string|Object} [data.author] - The author information for the embed. Can be a string (name) or an object with `name` and/or `iconURL` properties. + * @param {string} [data.color] - The HEX color of the embed's border. + * @param {string|Object} [data.footer] - The text to be displayed as the embed's footer. + * @param {Date} [data.timestamp] - The timestamp to be displayed in the embed. + * @param {string|Object} [data.author] - The author information for the embed. * @returns {EmbedBuilder} The generated EmbedBuilder instance. */ embed(data) { const embed = new EmbedBuilder() - .setTitle(data.title || null) - .setDescription(data.description || null) - .setThumbnail(data.thumbnail || null) - .addFields(data.fields || []) - .setImage(data.image || null) - .setURL(data.url || null); - - if (data.color) embed.setColor(data.color); - else if (data.color === null) embed.setColor(null); - else embed.setColor(this.config.embed.color); - - if (data.footer) embed.setFooter(data.footer); - else if (data.footer === null) embed.setFooter(null); - else embed.setFooter(this.config.embed.footer); - - if (data.timestamp) embed.setTimestamp(data.timestamp); - else if (data.timestamp === null) embed.setTimestamp(null); - else embed.setTimestamp(); - - if (!data.author || data.author === null) embed.setAuthor(null); - else if (typeof data.author === "string") embed.setAuthor({ name: data.author, iconURL: this.user.avatarURL() }); - else if (typeof data.author === "object" && (data.author.iconURL !== null || data.author.iconURL !== undefined)) embed.setAuthor({ name: data.author.name, iconURL: data.author.iconURL }); - else embed.setAuthor(data.author); + .setTitle(data.title ?? null) + .setDescription(data.description ?? null) + .setThumbnail(data.thumbnail ?? null) + .addFields(data.fields ?? []) + .setImage(data.image ?? null) + .setURL(data.url ?? null) + .setColor(data.color ?? this.config.embed.color) + .setFooter(typeof data.footer === "object" ? data.footer : data.footer ? { text: data.footer } : this.config.embed.footer) + .setTimestamp(data.timestamp ?? null) + .setAuthor(typeof data.author === "string" ? { name: data.author, iconURL: this.user.avatarURL() } : data.author ?? null); return embed; } @@ -243,65 +305,6 @@ class JaBaClient extends Client { if (channel) return (await channel.createInvite()).url || "No channels found or missing permissions"; } - /** - * Loads a command from the specified directory and file. - * @param {string} dir - The directory containing the command file. - * @param {string} file - The name of the command file (without the .js extension). - * @returns {Promise} A log message indicating the successful loading of the command. - */ - async loadCommand(dir, file) { - const Command = require(path.join(dir, `${file}.js`)); - if (!(Command.prototype instanceof BaseCommand)) return this.logger.error("Tried to load a non-command file!"); - - const command = new Command(this); - this.commands.set(command.command.name, command); - - if (command.onLoad && typeof command.onLoad === "function") await command.onLoad(this); - - return this.logger.log(`Successfully loaded "${file}" command file. (Command: ${command.command.name})`); - } - - /** - * Unloads a command from the specified directory and file. - * @param {string} dir - The directory containing the command file. - * @param {string} name - The name of the command file (without the .js extension). - * @returns {void} This method does not return a value. - */ - unloadCommand(dir, name) { - delete require.cache[require.resolve(`${dir}${path.sep}${name}.js`)]; - - return; - } - - /** - * Loads all event files from the specified directory and its subdirectories. - * @param {string} dir - The directory containing the event files. - * @returns {void} This method does not return a value. - */ - async loadEvents(dir) { - const filePath = path.join(__dirname, dir); - const files = await fs.readdir(filePath); - - for (let index = 0; index < files.length; index++) { - const file = files[index]; - const stat = await fs.lstat(path.join(filePath, file)); - if (stat.isDirectory()) this.loadEvents(path.join(dir, file)); - - if (file.endsWith(".js")) { - const Event = require(path.join(filePath, file)); - - if (Event.prototype instanceof BaseEvent) { - const event = new Event(); - if (!event.name || !event.name.length) return console.error(`Cannot load "${file}" event file: Event name is not set!`); - if (event.once) this.once(event.name, event.execute.bind(event, this)); - else this.on(event.name, event.execute.bind(event, this)); - - this.logger.log(`Successfully loaded "${file}" event file. (Event: ${event.name})`); - } - } - } - } - /** * Returns a User data from the database. * @param {string} userID - The ID of the user to find or create. @@ -310,19 +313,14 @@ class JaBaClient extends Client { async getUserData(userID) { let userData = await this.usersData.findOne({ id: userID }); - if (userData) { - this.databaseCache.users.set(userID, userData); - - return userData; - } else { + if (!userData) { userData = new this.usersData({ id: userID }); - await userData.save(); - - this.databaseCache.users.set(userID, userData); - - return userData; } + + this.databaseCache.users.set(userID, userData); + + return userData; } /** @@ -334,27 +332,19 @@ class JaBaClient extends Client { async getMemberData(memberId, guildId) { let memberData = await this.membersData.findOne({ guildID: guildId, id: memberId }); - if (memberData) { - this.databaseCache.members.set(`${memberId}${guildId}`, memberData); - - return memberData; - } else { + if (!memberData) { memberData = new this.membersData({ id: memberId, guildID: guildId }); - await memberData.save(); const guildData = await this.getGuildData(guildId); - if (guildData) { guildData.members.push(memberData._id); - await guildData.save(); } - - this.databaseCache.members.set(`${memberId}/${guildId}`, memberData); - - return memberData; } + + this.databaseCache.members.set(`${memberId}/${guildId}`, memberData); + return memberData; } /** @@ -365,19 +355,14 @@ class JaBaClient extends Client { async getGuildData(guildId) { let guildData = await this.guildsData.findOne({ id: guildId }).populate("members"); - if (guildData) { - this.databaseCache.guilds.set(guildId, guildData); - - return guildData; - } else { + if (!guildData) { guildData = new this.guildsData({ id: guildId }); - await guildData.save(); - - this.databaseCache.guilds.set(guildId, guildData); - - return guildData; } + + this.databaseCache.guilds.set(guildId, guildData); + + return guildData; } } diff --git a/commands/Owner/reload.js b/commands/Owner/reload.js index c5315e32..c624084d 100644 --- a/commands/Owner/reload.js +++ b/commands/Owner/reload.js @@ -1,7 +1,6 @@ const { SlashCommandBuilder, InteractionContextType, ApplicationIntegrationType } = require("discord.js"); const BaseCommand = require("../../base/BaseCommand"), i18next = require("i18next"); -// autoUpdateDocs = require("../../helpers/autoUpdateDocs"); class Reload extends BaseCommand { /** @@ -52,7 +51,6 @@ class Reload extends BaseCommand { await client.loadCommand(`../commands/${cmd.category}`, cmd.command.name); i18next.reloadResources(["ru-RU", "uk-UA", "en-US"]); - // autoUpdateDocs.update(client); interaction.success("owner/reload:SUCCESS", { command: cmd.command.name, diff --git a/dashboard/dashboard.js b/dashboard/dashboard.js index 5307672b..8279623d 100644 --- a/dashboard/dashboard.js +++ b/dashboard/dashboard.js @@ -108,12 +108,10 @@ module.exports.load = async client => { const user = req.session?.user; const username = (user?.discriminator === "0" ? user?.username : user?.tag) || "Guest"; - const hiddenGuildMembersCount = client.guilds.cache.get("568120814776614924").memberCount; let users = 0; client.guilds.cache.forEach(g => { users += g.memberCount; }); - users = users - hiddenGuildMembersCount; const cards = [ { diff --git a/events/CommandHandler.js b/events/CommandHandler.js index 07280578..1fee651e 100644 --- a/events/CommandHandler.js +++ b/events/CommandHandler.js @@ -10,16 +10,15 @@ class CommandHandler extends BaseEvent { } /** - * + * Handles command interaction events. * @param {import("../base/Client")} client * @param {import("discord.js").CommandInteraction} interaction */ async execute(client, interaction) { const command = client.commands.get(interaction.commandName); + if (!command) return interaction.reply({ content: "Command not found!", ephemeral: true }); - const data = []; - - data.user = await client.getUserData(interaction.user.id); + const data = { user: await client.getUserData(interaction.user.id) }; if (interaction.inGuild()) { data.guild = await client.getGuildData(interaction.guildId); @@ -30,44 +29,39 @@ class CommandHandler extends BaseEvent { if (interaction.isButton() && interaction.customId === "quote_delete" && interaction.message.deletable) return interaction.message.delete(); if (interaction.isAutocomplete()) return await command.autocompleteRun(client, interaction); + if (interaction.type !== InteractionType.ApplicationCommand || !interaction.isCommand()) return; - if (interaction.type !== InteractionType.ApplicationCommand && !interaction.isCommand()) return; + // IAT Guild Command Check + if (command?.dirname.includes("IAT") && interaction.guildId !== "1039187019957555252") + return interaction.reply({ content: "IAT only", ephemeral: true }); - if (command?.dirname.includes("IAT") && interaction.guildId !== "1039187019957555252") return interaction.reply({ content: "IAT only", ephemeral: true }); - if (command.ownerOnly && interaction.user.id !== client.config.owner.id) return interaction.error("misc:OWNER_ONLY", null, { ephemeral: true }); + // Owner-only command check + if (command.ownerOnly && interaction.user.id !== client.config.owner.id) + return interaction.error("misc:OWNER_ONLY", null, { ephemeral: true }); - if (!interaction.data.user.achievements.firstCommand.achieved) { - const args = { - content: interaction.user.toString(), - files: [ - { - name: "achievement_unlocked2.png", - attachment: "./assets/img/achievements/achievement_unlocked2.png", - }, - ], - }; - - interaction.data.user.achievements.firstCommand.progress.now = 1; - interaction.data.user.achievements.firstCommand.achieved = true; + // First command achievement check + const { firstCommand } = interaction.data.user.achievements; + if (!firstCommand.achieved) { + firstCommand.progress.now = 1; + firstCommand.achieved = true; await interaction.data.user.save(); + const achievementMessage = { + content: interaction.user.toString(), + files: [{ name: "achievement_unlocked2.png", attachment: "./assets/img/achievements/achievement_unlocked2.png" }], + }; + try { - interaction.user.send(args); - } catch (e) { /**/ } + await interaction.user.send(achievementMessage); + } catch (e) { + client.logger.warn("Failed to send achievement message to user:", e); + } } - client.logger.cmd( - `[${interaction.guild ? interaction.guild.name : "DM/Private Channel"}]: [${interaction.user.getUsername()}] => /${command.command.name}${ - interaction.options.data.length > 0 - ? `, args: [${interaction.options.data - .map(arg => { - return `${arg.name}: ${arg.value}`; - }) - .join(", ")}]` - : "" - }`, - ); + // Command logging + const args = interaction.options.data.map(arg => `${arg.name}: ${arg.value}`).join(", "); + client.logger.cmd(`[${interaction.guild ? interaction.guild.name : "DM/Private Channel"}]: [${interaction.user.tag}] => /${command.command.name}${args ? `, args: [${args}]` : ""}`); return command.execute(client, interaction); } diff --git a/events/Guild/guildBanAdd.js b/events/Guild/guildBanAdd.js index c5feaa92..be02d479 100644 --- a/events/Guild/guildBanAdd.js +++ b/events/Guild/guildBanAdd.js @@ -23,7 +23,7 @@ class guildBanAdd extends BaseEvent { }); try { - ban.user.send({ + await ban.user.send({ embeds: [embed], }); } catch (e) { /**/ } diff --git a/events/Guild/guildCreate.js b/events/Guild/guildCreate.js index 3fa92589..ffa6a96f 100644 --- a/events/Guild/guildCreate.js +++ b/events/Guild/guildCreate.js @@ -23,23 +23,25 @@ class GuildCreate extends BaseEvent { await userData.save(); } - const thanks = client.embed({ + const embed = client.embed({ author: "Thanks for inviting me to your server!", - description: "Use in your server to get list of all commands!.", + description: "Use in your server to get a list of all commands!", }); try { const owner = await guild.fetchOwner(); - owner.send({ + await owner.send({ files: [ { name: "unlocked.png", attachment: "./assets/img/achievements/achievement_unlocked7.png", }, ], - embeds: [thanks], + embeds: [embed], }); - } catch (e) { /**/ } + } catch (e) { + client.logger.error(`Failed to send welcome message to guild owner: ${e.message}`); + } if (client.config.support.logs) { const users = guild.members.cache.filter(m => !m.user.bot).size; @@ -50,12 +52,20 @@ class GuildCreate extends BaseEvent { name: guild.name, iconURL: guild.iconURL() || client.user.avatarURL(), }, - description: `Joined a new guild **${guild.name}**. It has **${users}** ${client.functions.getNoun(users, client.translate("misc:NOUNS:USERS:1"), client.translate("misc:NOUNS:USERS:2"), client.translate("misc:NOUNS:USERS:5"))} and **${bots}** ${client.functions.getNoun(bots, client.translate("misc:NOUNS:BOTS:1"), client.translate("misc:NOUNS:BOTS:2"), client.translate("misc:NOUNS:BOTS:5"))}`, + description: `Joined a new guild **${guild.name}**. It has **${users}** ${client.functions.getNoun( + users, + client.translate("misc:NOUNS:USERS:1"), + client.translate("misc:NOUNS:USERS:2"), + client.translate("misc:NOUNS:USERS:5"), + )} and **${bots}** ${client.functions.getNoun(bots, client.translate("misc:NOUNS:BOTS:1"), client.translate("misc:NOUNS:BOTS:2"), client.translate("misc:NOUNS:BOTS:5"))}.`, }); - client.channels.cache.get(client.config.support.logs).send({ - embeds: [embed], - }); + const logChannel = client.channels.cache.get(client.config.support.logs); + + if (logChannel) + await logChannel.send({ + embeds: [embed], + }); } } } diff --git a/events/Guild/guildDelete.js b/events/Guild/guildDelete.js index 15041b14..20b795cc 100644 --- a/events/Guild/guildDelete.js +++ b/events/Guild/guildDelete.js @@ -23,9 +23,13 @@ class GuildDelete extends BaseEvent { description: `Left from guild **${guild.name}**.`, }); - client.channels.cache.get(client.config.support.logs).send({ - embeds: [embed], - }); + const logChannel = client.channels.cache.get(client.config.support.logs); + + if (logChannel) + await logChannel.send({ + embeds: [embed], + }); + else client.logger.warn(`Log channel not found for guild deletion: ${guild.name}`); } } } diff --git a/events/Guild/guildMemberAdd.js b/events/Guild/guildMemberAdd.js index 8e9f40c3..0112ae64 100644 --- a/events/Guild/guildMemberAdd.js +++ b/events/Guild/guildMemberAdd.js @@ -1,8 +1,3 @@ -// const Canvas = require("@napi-rs/canvas"), -// BaseEvent = require("../../base/BaseEvent"), -// { AttachmentBuilder } = require("discord.js"), -// { applyText } = require("../../helpers/functions"); - const BaseEvent = require("../../base/BaseEvent"); class GuildMemberAdd extends BaseEvent { @@ -19,13 +14,18 @@ class GuildMemberAdd extends BaseEvent { * @param {import("discord.js").GuildMember} member */ async execute(client, member) { - if (member.guild && member.guildId === "568120814776614924") return; - await member.guild.members.fetch(); const guildData = await client.getGuildData(member.guild.id); - if (guildData.plugins.autorole.enabled) member.roles.add(guildData.plugins.autorole.role); + if (guildData.plugins.autorole.enabled) { + const role = guildData.plugins.autorole.role; + if (role) { + await member.roles.add(role).catch(err => { + client.logger.error(`Failed to add role to ${member.user.tag}: ${err}`); + }); + } + } if (guildData.plugins.welcome.enabled) { const channel = member.guild.channels.cache.get(guildData.plugins.welcome.channel); @@ -36,103 +36,11 @@ class GuildMemberAdd extends BaseEvent { .replace(/{server}/g, member.guild.name) .replace(/{membercount}/g, member.guild.memberCount); - /* - if (guildData.plugins.welcome.withImage) { - const canvas = Canvas.createCanvas(1024, 450), - ctx = canvas.getContext("2d"); - - // Draw background - const background = await Canvas.loadImage("./assets/img/greetings_background.png"); - ctx.drawImage(background, 0, 0, canvas.width, canvas.height); - - // Draw layer - ctx.fillStyle = "#FFFFFF"; - ctx.globalAlpha = "0.4"; - ctx.fillRect(0, 0, 25, canvas.height); - ctx.fillRect(canvas.width - 25, 0, 25, canvas.height); - ctx.fillRect(25, 0, canvas.width - 50, 25); - ctx.fillRect(25, canvas.height - 25, canvas.width - 50, 25); - ctx.fillStyle = "#FFFFFF"; - ctx.globalAlpha = "0.4"; - ctx.fillRect(344, canvas.height - 296, 625, 65); - ctx.fillStyle = "#FFFFFF"; - ctx.globalAlpha = "0.4"; - ctx.fillRect(389, canvas.height - 225, 138, 65); - ctx.fillStyle = "#FFFFFF"; - ctx.globalAlpha = "0.4"; - ctx.fillRect(308, canvas.height - 110, 672, 65); - - // Draw username - ctx.globalAlpha = 1; - ctx.fillStyle = "#FFFFFF"; - ctx.font = applyText(canvas, member.user.username, 48, 600, "RubikMonoOne"); - ctx.fillText(member.user.username, canvas.width - 670, canvas.height - 250); - - // Draw server name - ctx.font = applyText( - canvas, - client.translate("administration/welcome:IMG_WELCOME", { - server: member.guild.name, - }, guildData.language), - 53, - 625, - "RubikMonoOne", - ); - - ctx.fillText( - client.translate("administration/welcome:IMG_WELCOME", { - server: member.guild.name, - }, guildData.language), - canvas.width - 700, - canvas.height - 70, - ); - - // Draw discriminator - ctx.font = "35px RubikMonoOne"; - ctx.fillText(member.user.discriminator === "0" ? "" : member.user.discriminator, canvas.width - 623, canvas.height - 178); - - // Draw membercount - ctx.font = "22px RubikMonoOne"; - ctx.fillText(`${member.guild.memberCount}й ${client.translate("misc:NOUNS:MEMBERS:1", null, guildData.language)}`, 40, canvas.height - 35); - - // Draw # for discriminator - ctx.fillStyle = "#FFFFFF"; - ctx.font = "70px RubikMonoOne"; - ctx.fillText(member.user.discriminator === "0" ? "" : "#", canvas.width - 690, canvas.height - 165); - - // Draw title - ctx.font = "45px RubikMonoOne"; - ctx.strokeStyle = "#000000"; - ctx.lineWidth = 10; - ctx.strokeText(client.translate("administration/welcome:TITLE", null, guildData.language), canvas.width - 670, canvas.height - 330); - ctx.fillStyle = "#FFFFFF"; - ctx.fillText(client.translate("administration/welcome:TITLE", null, guildData.language), canvas.width - 670, canvas.height - 330); - - // Draw avatar circle - ctx.beginPath(); - ctx.lineWidth = 10; - ctx.strokeStyle = "#FFFFFF"; - ctx.arc(180, 225, 135, 0, Math.PI * 2, true); - ctx.stroke(); - ctx.closePath(); - ctx.clip(); - - const avatar = await Canvas.loadImage( - member.displayAvatarURL({ - extension: "jpg", - }), - ); - ctx.drawImage(avatar, 45, 90, 270, 270); - - const attachment = new AttachmentBuilder((await canvas.encode("png")), { name: "welcome.png" }); - - channel.send({ - content: message, - files: [attachment], - }); - } else */ - - channel.send({ content: message }); + await channel.send({ content: message }).catch(err => { + client.logger.error(`Failed to send welcome message in channel ${channel.id}: ${err}`); + }); + } else { + client.logger.warn(`Welcome channel not found: ${guildData.plugins.welcome.channel}`); } } } diff --git a/events/Guild/guildMemberRemove.js b/events/Guild/guildMemberRemove.js index 318f8bd2..c3e1499a 100644 --- a/events/Guild/guildMemberRemove.js +++ b/events/Guild/guildMemberRemove.js @@ -1,8 +1,3 @@ -// const Canvas = require("@napi-rs/canvas"), -// BaseEvent = require("../../base/BaseEvent"), -// { AttachmentBuilder } = require("discord.js"), -// { applyText } = require("../../helpers/functions"); - const BaseEvent = require("../../base/BaseEvent"); class GuildMemberRemove extends BaseEvent { @@ -19,8 +14,6 @@ class GuildMemberRemove extends BaseEvent { * @param {import("discord.js").GuildMember} member */ async execute(client, member) { - if (member.guild && member.guildId === "568120814776614924") return; - await member.guild.members.fetch(); const guildData = await client.getGuildData(member.guild.id); @@ -34,108 +27,11 @@ class GuildMemberRemove extends BaseEvent { .replace(/{server}/g, member.guild.name) .replace(/{membercount}/g, member.guild.memberCount); - /* - if (guildData.plugins.goodbye.withImage) { - const canvas = Canvas.createCanvas(1024, 450), - ctx = canvas.getContext("2d"); - - // Draw background - const background = await Canvas.loadImage("./assets/img/greetings_background.png"); - ctx.drawImage(background, 0, 0, canvas.width, canvas.height); - - // Draw layer - ctx.fillStyle = "#FFFFFF"; - ctx.globalAlpha = "0.4"; - ctx.fillRect(0, 0, 25, canvas.height); - ctx.fillRect(canvas.width - 25, 0, 25, canvas.height); - ctx.fillRect(25, 0, canvas.width - 50, 25); - ctx.fillRect(25, canvas.height - 25, canvas.width - 50, 25); - ctx.fillStyle = "#FFFFFF"; - ctx.globalAlpha = "0.4"; - ctx.fillRect(344, canvas.height - 296, 625, 65); - ctx.fillStyle = "#FFFFFF"; - ctx.globalAlpha = "0.4"; - ctx.fillRect(389, canvas.height - 225, 138, 65); - ctx.fillStyle = "#FFFFFF"; - ctx.globalAlpha = "0.4"; - ctx.fillRect(308, canvas.height - 110, 672, 65); - - // Draw username - ctx.globalAlpha = 1; - ctx.fillStyle = "#FFFFFF"; - ctx.font = applyText(canvas, member.user.username, 48, 600, "RubikMonoOne"); - ctx.fillText(member.user.username, canvas.width - 670, canvas.height - 250); - - // Draw server name - ctx.font = applyText( - canvas, - client.translate("administration/goodbye:IMG_GOODBYE", { - server: member.guild.name, - }, guildData.language), - 53, - 625, - "RubikMonoOne", - ); - - ctx.fillText( - client.translate("administration/goodbye:IMG_GOODBYE", { - server: member.guild.name, - }, guildData.language), - canvas.width - 700, - canvas.height - 70, - ); - - // Draw discriminator - ctx.font = "35px RubikMonoOne"; - ctx.fillText(member.user.discriminator === "0" ? "" : member.user.discriminator, canvas.width - 623, canvas.height - 178); - - // Draw membercount - ctx.font = "22px RubikMonoOne"; - ctx.fillText( - `${member.guild.memberCount} ${client.functions.getNoun(member.guild.memberCount, client.translate("misc:NOUNS:MEMBERS:1", null, guildData.language), client.translate("misc:NOUNS:MEMBERS:2", null, guildData.language), client.translate("misc:NOUNS:MEMBERS:5", null, guildData.language))}`, - 40, - canvas.height - 35, - ); - - // Draw # for discriminator - ctx.fillStyle = "#FFFFFF"; - ctx.font = "70px RubikMonoOne"; - ctx.fillText(member.user.discriminator === "0" ? "" : "#", canvas.width - 690, canvas.height - 165); - - // Draw title - ctx.font = "45px RubikMonoOne"; - ctx.strokeStyle = "#000000"; - ctx.lineWidth = 10; - ctx.strokeText(client.translate("administration/goodbye:TITLE", null, guildData.language), canvas.width - 670, canvas.height - 330); - ctx.fillStyle = "#FFFFFF"; - ctx.fillText(client.translate("administration/goodbye:TITLE", null, guildData.language), canvas.width - 670, canvas.height - 330); - - // Draw avatar circle - ctx.beginPath(); - ctx.lineWidth = 10; - ctx.strokeStyle = "#FFFFFF"; - ctx.arc(180, 225, 135, 0, Math.PI * 2, true); - ctx.stroke(); - ctx.closePath(); - ctx.clip(); - - const avatar = await Canvas.loadImage( - member.displayAvatarURL({ - extension: "png", - size: 512, - }), - ); - ctx.drawImage(avatar, 45, 90, 270, 270); - - const attachment = new AttachmentBuilder((await canvas.encode("png")), { name: "goodbye-image.png" }); - - channel.send({ - content: message, - files: [attachment], - }); - } else */ - - channel.send({ content: message }); + await channel.send({ content: message }).catch(err => { + client.logger.error(`Failed to send goodbye message in channel ${channel.id}: ${err}`); + }); + } else { + client.logger.warn(`Goodbye channel not found: ${guildData.plugins.goodbye.channel}`); } } } diff --git a/events/Guild/guildMemberUpdate.js b/events/Guild/guildMemberUpdate.js index d522050a..2c48d1a9 100644 --- a/events/Guild/guildMemberUpdate.js +++ b/events/Guild/guildMemberUpdate.js @@ -3,7 +3,7 @@ const BaseEvent = require("../../base/BaseEvent"); class GuildMemberUpdate extends BaseEvent { constructor() { super({ - name: "guildMemberRemove", + name: "guildMemberUpdate", once: false, }); } @@ -15,7 +15,6 @@ class GuildMemberUpdate extends BaseEvent { * @param {import("discord.js").GuildMember} newMember */ async execute(client, oldMember, newMember) { - if (oldMember.guild && oldMember.guildId === "568120814776614924") return; if (oldMember.guild.id !== client.config.support.id) return; if (oldMember.roles.cache.some(r => r.id === "940149470975365191")) return; @@ -25,16 +24,22 @@ class GuildMemberUpdate extends BaseEvent { userData.achievements.tip.progress.now = 1; userData.achievements.tip.achieved = true; - await userData.save(); - - newMember.send({ - files: [ - { - name: "achievement_unlocked5.png", - attachment: "./assets/img/achievements/achievement_unlocked5.png", - }, - ], + await userData.save().catch(err => { + client.logger.error(`Failed to save user data for ${newMember.id}: ${err}`); }); + + try { + await newMember.send({ + files: [ + { + name: "achievement_unlocked5.png", + attachment: "./assets/img/achievements/achievement_unlocked5.png", + }, + ], + }); + } catch (err) { + client.logger.error(`Failed to send achievement message to ${newMember.id}: ${err}`); + } } } } diff --git a/events/MessageHandler.js b/events/MessageHandler.js index 79e050f1..63644d4a 100644 --- a/events/MessageHandler.js +++ b/events/MessageHandler.js @@ -17,14 +17,20 @@ class MessageCreate extends BaseEvent { * @param {import("discord.js").Message} message */ async execute(client, message) { - if (message.guild && message.guild.id === "568120814776614924") return; - - const data = []; - if (message.author.bot) return; if (message.content.match(new RegExp(`^<@!?${client.user.id}>( |)$`))) return message.replyT("misc:HELLO_SERVER", null, { mention: true }); - data.user = await client.getUserData(message.author.id); + const data = await this.initializeMessageData(client, message); + message.data = data; + + if (message.guild) + await this.handleGuildMessage(client, message); + + return; + } + + async initializeMessageData(client, message) { + const data = { user: await client.getUserData(message.author.id) }; if (message.guild) { if (!message.member) await message.guild.members.fetch(message.author.id); @@ -33,86 +39,93 @@ class MessageCreate extends BaseEvent { data.member = await client.getMemberData(message.author.id, message.guildId); } - message.data = data; + return data; + } - if (message.guild) { - await updateXp(message); + async handleGuildMessage(client, message) { + await updateXp(message); - if (message.content.match(/(https|http):\/\/(ptb\.|canary\.)?(discord.com)\/(channels)\/\d+\/\d+\/\d+/g)) { - const link = message.content.match(/(https|http):\/\/(ptb\.|canary\.)?(discord.com)\/(channels)\/\d+\/\d+\/\d+/g)[0], - ids = link.match(/\d+/g), - channelId = ids[1], - messageId = ids[2]; + if (this.isLinkQuote(message)) await this.handleLinkQuote(client, message); + if (message.data.guild.plugins.automod.enabled && !message.data.guild.plugins.automod.ignored.includes(message.channelId)) await this.checkAutomod(message); - const msg = await message.guild.channels.cache.get(channelId).messages.fetch(messageId); - const embed = client.embed({ - author: { - name: message.translate("misc:QUOTE_TITLE", { - user: msg.author.getUsername(), - }), - iconURL: "https://wynem.com/assets/images/icons/quote.webp", - }, - thumbnail: msg.author.displayAvatarURL(), - footer: { - text: message.translate("misc:QUOTE_FOOTER", { user: message.author.getUsername() }), - }, - timestamp: msg.createdTimestamp, - }); + await this.checkAfkStatus(client, message); + await this.checkMentionedUsersAfk(client, message); + } - if (msg.content !== "") embed.addFields([ - { - name: message.translate("misc:QUOTE_CONTENT"), - value: msg.content, - }, - ]); + isLinkQuote(message) { + return /(https?:\/\/(ptb\.|canary\.)?(discord\.com)\/channels\/\d+\/\d+)/g.test(message.content); + } - if (msg.attachments.size > 0) { - if (msg.attachments.find(a => a.contentType.includes("image/"))) embed.setImage(msg.attachments.find(a => a.contentType.includes("image/")).url); + async handleLinkQuote(client, message) { + const link = message.content.match(/(https?:\/\/(ptb\.|canary\.)?(discord\.com)\/channels\/\d+\/\d+)/g)[0]; + const ids = link.match(/\d+/g); + const channelId = ids[1]; + const messageId = ids[2]; - embed.addFields([ - { - name: message.translate("misc:QUOTE_ATTACHED"), - value: msg.attachments.map(a => { return `[${a.name}](${a.url})`; }).join(", "), - }, - ]); - } + try { + const msg = await message.guild.channels.cache.get(channelId).messages.fetch(messageId); + const embed = this.createQuoteEmbed(client, msg, message); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setLabel("Jump").setStyle(ButtonStyle.Link).setURL(msg.url), + new ButtonBuilder().setCustomId("quote_delete").setEmoji("1273665480451948544").setStyle(ButtonStyle.Danger), + ); - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setLabel(message.translate("misc:QUOTE_JUMP")).setStyle(ButtonStyle.Link).setURL(msg.url), - new ButtonBuilder().setCustomId("quote_delete").setEmoji("1273665480451948544").setStyle(ButtonStyle.Danger), - ); + await message.reply({ embeds: [embed], components: [row] }); + } catch (error) { + client.logger.error("Failed to fetch quoted message:", error); + } + } - message.reply({ - embeds: [embed], - components: [row], - }); - } + createQuoteEmbed(client, msg, message) { + const embed = client.embed({ + author: { + name: message.translate("misc:QUOTE_TITLE", { user: msg.author.getUsername() }), + iconURL: "https://wynem.com/assets/images/icons/quote.webp", + }, + thumbnail: msg.author.displayAvatarURL(), + footer: message.translate("misc:QUOTE_FOOTER"), + timestamp: msg.createdTimestamp, + }); - if (message.data.guild.plugins.automod.enabled && !message.data.guild.plugins.automod.ignored.includes(message.channelId)) - if (/(discord\.(gg|io|me|li)\/.+|discordapp\.com\/invite\/.+)/i.test(message.content)) - if (!message.channel.permissionsFor(message.member).has(PermissionsBitField.Flags.ManageMessages)) { - await message.error("administration/automod:DELETED", null, { mention: true }); - message.delete(); - } + if (msg.content) embed.addFields([{ name: message.translate("misc:QUOTE_CONTENT"), value: msg.content }]); + if (msg.attachments.size > 0) { + const images = msg.attachments.filter(a => a.contentType.includes("image/")); + if (images.size > 0) embed.setImage(images.first().url); - if (message.data.user.afk) { - message.data.user.afk = null; - - await message.data.user.save(); - - message.replyT("general/afk:DELETED", { - user: message.author.username, - }, { mention: true }); - } - - message.mentions.users.forEach(async u => { - const userData = await client.getUserData(u.id); - - if (userData.afk) message.replyT("general/afk:IS_AFK", { user: u.getUsername(), message: userData.afk }, { ephemeral: true }); - }); + embed.addFields([ + { + name: message.translate("misc:QUOTE_ATTACHED"), + value: msg.attachments.map(a => `[${a.name}](${a.url})`).join(", "), + }, + ]); } - return; + return embed; + } + + async checkAutomod(message) { + const inviteRegex = /(discord\.(gg|io|me|li)\/.+|discordapp\.com\/invite\/.+)/i; + if (inviteRegex.test(message.content) && !message.channel.permissionsFor(message.member).has(PermissionsBitField.Flags.ManageMessages)) { + await message.error("administration/automod:DELETED", null, { mention: true }); + await message.delete(); + } + } + + async checkAfkStatus(client, message) { + if (message.data.user.afk) { + message.data.user.afk = null; + await message.data.user.save(); + + message.replyT("general/afk:DELETED", { user: message.author.username }, { mention: true }); + } + } + + async checkMentionedUsersAfk(client, message) { + for (const user of message.mentions.users.values()) { + const userData = await client.getUserData(user.id); + + if (userData.afk) message.replyT("general/afk:IS_AFK", { user: user.getUsername(), message: userData.afk }, { ephemeral: true }); + } } } @@ -123,27 +136,28 @@ class MessageCreate extends BaseEvent { * @returns */ async function updateXp(message) { - const memberData = message.data.member, - points = parseInt(memberData.exp), - level = parseInt(memberData.level), - isInCooldown = xpCooldown[message.author.id]; + const memberData = message.data.member; + let level = parseInt(memberData.level), + exp = parseInt(memberData.exp); + const isInCooldown = xpCooldown[message.author.id]; - if (isInCooldown) if (isInCooldown > Date.now()) return; + if (isInCooldown && isInCooldown > Date.now()) return; const toWait = Date.now() + 60 * 1000; // 1 min xpCooldown[message.author.id] = toWait; const won = message.client.functions.randomNum(1, 2); - const newXp = parseInt(points + won, 10); - const neededXp = 5 * (level * level) + 80 * level + 100; + const newXp = exp + won; + const neededXp = 5 * level ** 2 + 80 * level + 100; if (newXp > neededXp) { - memberData.level = parseInt(level + 1, 10); - memberData.exp = 0; - message.replyT("misc:LEVEL_UP", { - level: memberData.level, - }, { mention: false }); - } else memberData.exp = parseInt(newXp, 10); + level += 1; + exp = 0; + + message.replyT("misc:LEVEL_UP", { level: level }, { mention: false }); + } else { + exp = newXp; + } await memberData.save(); } diff --git a/events/Monitoring/messageDelete.js b/events/Monitoring/messageDelete.js index 8539e447..5b760cd1 100644 --- a/events/Monitoring/messageDelete.js +++ b/events/Monitoring/messageDelete.js @@ -14,7 +14,6 @@ class messageDelete extends BaseEvent { * @param {import("discord.js").Message} message The deleted message */ async execute(client, message) { - if (message.guild && message.guildId === "568120814776614924") return; if (message.author.bot) return; const guildData = message.data.guild; @@ -26,12 +25,17 @@ class messageDelete extends BaseEvent { iconURL: message.author.displayAvatarURL(), }, title: message.translate("misc:MONITORING:DELETE:TITLE", { user: message.author.getUsername() }), - description: message.translate("misc:MONITORING:DELETE:DESCRIPTION", { content: message.content, channel: message.channel.toString(), time: `` }), + description: message.translate("misc:MONITORING:DELETE:DESCRIPTION", { + content: message.content, + channel: message.channel.toString(), + time: ``, + }), }); - message.guild.channels.cache.get(guildData.plugins.monitoring.messageDelete).send({ - embeds: [embed], - }); + const monitoringChannelId = guildData.plugins.monitoring.messageDelete; + const monitoringChannel = message.guild.channels.cache.get(monitoringChannelId); + + if (monitoringChannel) await monitoringChannel.send({ embeds: [embed] }); } } } diff --git a/events/Monitoring/messageUpdate.js b/events/Monitoring/messageUpdate.js index 49adfe4e..72bc91df 100644 --- a/events/Monitoring/messageUpdate.js +++ b/events/Monitoring/messageUpdate.js @@ -15,9 +15,7 @@ class messageUpdate extends BaseEvent { * @param {import("discord.js").Message} newMessage The message after the update */ async execute(client, oldMessage, newMessage) { - if (oldMessage.guild && oldMessage.guildId === "568120814776614924") return; if (oldMessage.author.bot) return; - if (oldMessage.content === newMessage.content) return; const guildData = newMessage.data.guild; @@ -29,12 +27,17 @@ class messageUpdate extends BaseEvent { iconURL: newMessage.author.displayAvatarURL(), }, title: newMessage.translate("misc:MONITORING:UPDATE:TITLE", { user: newMessage.author.getUsername() }), - description: newMessage.translate("misc:MONITORING:UPDATE:DESCRIPTION", { oldContent: oldMessage.content, newContent: newMessage.content, url: newMessage.url }), + description: newMessage.translate("misc:MONITORING:UPDATE:DESCRIPTION", { + oldContent: oldMessage.content, + newContent: newMessage.content, + url: newMessage.url, + }), }); - newMessage.guild.channels.cache.get(guildData.plugins.monitoring.messageUpdate).send({ - embeds: [embed], - }); + const monitoringChannelId = guildData.plugins.monitoring.messageUpdate; + const monitoringChannel = newMessage.guild.channels.cache.get(monitoringChannelId); + + if (monitoringChannel) await monitoringChannel.send({ embeds: [embed] }); } } } diff --git a/events/ready.js b/events/ready.js index 66f17b30..41fd0645 100644 --- a/events/ready.js +++ b/events/ready.js @@ -8,15 +8,16 @@ class Ready extends BaseEvent { once: false, }); } + /** * * @param {import("../base/Client")} client */ async execute(client) { const commands = [...new Map(client.commands.map(v => [v.constructor.name, v])).values()]; - let servers = client.guilds.cache.size; let users = 0; + client.guilds.cache.forEach(g => { users += g.memberCount; }); @@ -31,7 +32,6 @@ class Ready extends BaseEvent { client.logger.ready(`Loaded a total of ${commands.length} command(s).`); client.logger.ready(`${client.user.getUsername()}, ready to serve ${users} members in ${servers} servers.`); - console.timeEnd("botReady"); const version = require("../package.json").version; @@ -44,9 +44,10 @@ class Ready extends BaseEvent { ]; let i = 0; - setInterval(() => { - servers = client.guilds.fetch().then(g => g.size); + setInterval(async () => { + servers = (await client.guilds.fetch()).size; users = 0; + client.guilds.cache.forEach(g => { users += g.memberCount; }); @@ -57,8 +58,7 @@ class Ready extends BaseEvent { state: `${status[i]} | v${version}`, }); - if (status[i + 1]) i++; - else i = 0; + i = (i + 1) % status.length; // Wrap around to the start when reaching the end }, 30 * 1000); // Every 30 seconds } } diff --git a/helpers/birthdays.js b/helpers/birthdays.js index 9e555640..96bce021 100644 --- a/helpers/birthdays.js +++ b/helpers/birthdays.js @@ -5,30 +5,35 @@ const { CronJob } = require("cron"); * @param {import("../base/Client")} client */ module.exports.init = async client => { - const cronjob = new CronJob("0 5 * * *", async function () { - client.guilds.cache.forEach(async guild => { - try { - console.log(`Checking birthdays for "${guild.name}"`); + const cronjob = new CronJob("0 5 * * *", + async function () { + // Iterate over all guilds the bot is in + for (const guild of client.guilds.cache.values()) { + try { + console.log(`Checking birthdays for "${guild.name}"`); - const guildData = await client.getGuildData(guild.id); - const channel = guildData.plugins.birthdays ? await client.channels.fetch(guildData.plugins.birthdays) : null; + const guildData = await client.getGuildData(guild.id); + const channel = guildData.plugins.birthdays ? await client.channels.fetch(guildData.plugins.birthdays) : null; - if (channel) { - const date = new Date(), - currentDay = date.getDate(), - currentMonth = date.getMonth() + 1, - currentYear = date.getFullYear(); + if (channel) { + const date = new Date(); + const currentDay = date.getDate(); + const currentMonth = date.getMonth() + 1; + const currentYear = date.getFullYear(); + + const users = await client.usersData.find({ birthdate: { $gt: 1 } }); - client.usersData.find({ birthdate: { $gt: 1 } }).then(async users => { for (const user of users) { - if (!guild.members.cache.find(m => m.id === user.id)) return; + // Check if the user is in the guild + if (!guild.members.cache.has(user.id)) continue; - const userDate = new Date(user.birthdate * 1000), - day = userDate.getDate(), - month = userDate.getMonth() + 1, - year = userDate.getFullYear(), - age = currentYear - year; + const userDate = new Date(user.birthdate * 1000); + const day = userDate.getDate(); + const month = userDate.getMonth() + 1; + const year = userDate.getFullYear(); + const age = currentYear - year; + // Check if it's the user's birthday if (currentMonth === month && currentDay === day) { const embed = client.embed({ author: client.user.getUsername(), @@ -48,21 +53,18 @@ module.exports.init = async client => { ], }); - channel.send({ - embeds: [embed], - }).then(m => m.react("🎉")); + await channel.send({ embeds: [embed] }).then(m => m.react("🎉")); } } - }); + } + } catch (err) { + if (err.code === 10003) console.log(`No channel found for ${guild.name}`); + else console.error(`Error processing guild "${guild.name}":`, err); } - } catch (err) { - if (err.code === 10003) console.log(`No channel found for ${guild.name}`); - else throw err; } - }); - }, - null, - true, + }, + null, + true, ); cronjob.start(); @@ -73,51 +75,51 @@ module.exports.init = async client => { * @param {import("../base/Client")} client */ module.exports.run = async client => { - client.guilds.cache.forEach(async guild => { + for (const guild of client.guilds.cache.values()) { const guildData = await client.getGuildData(guild.id); const channel = guildData.plugins.birthdays ? await client.channels.fetch(guildData.plugins.birthdays) : null; if (channel) { - const date = new Date(), - currentDay = date.getDate(), - currentMonth = date.getMonth() + 1, - currentYear = date.getFullYear(); + const date = new Date(); + const currentDay = date.getDate(); + const currentMonth = date.getMonth() + 1; + const currentYear = date.getFullYear(); - client.usersData.find({ birthdate: { $gt: 1 } }).then(async users => { - for (const user of users) { - if (!guild.members.cache.find(m => m.id === user.id)) return; + const users = await client.usersData.find({ birthdate: { $gt: 1 } }); - const userDate = new Date(user.birthdate * 1000), - day = userDate.getDate(), - month = userDate.getMonth() + 1, - year = userDate.getFullYear(), - age = currentYear - year; + for (const user of users) { + // Check if the user is in the guild + if (!guild.members.cache.has(user.id)) continue; - if (currentMonth === month && currentDay === day) { - const embed = client.embed({ - author: client.user.getUsername(), - fields: [ - { - name: client.translate("economy/birthdate:HAPPY_BIRTHDAY", null, guildData.language), - value: client.translate("economy/birthdate:HAPPY_BIRTHDAY_MESSAGE", { - user: user.id, - age: `**${age}** ${client.functions.getNoun( - age, - client.translate("misc:NOUNS:AGE:1", null, guildData.language), - client.translate("misc:NOUNS:AGE:2", null, guildData.language), - client.translate("misc:NOUNS:AGE:5", null, guildData.language), - )}`, - }, guildData.language), - }, - ], - }); + const userDate = new Date(user.birthdate * 1000); + const day = userDate.getDate(); + const month = userDate.getMonth() + 1; + const year = userDate.getFullYear(); + const age = currentYear - year; - channel.send({ - embeds: [embed], - }).then(m => m.react("🎉")); - } + // Check if it's the user's birthday + if (currentMonth === month && currentDay === day) { + const embed = client.embed({ + author: client.user.getUsername(), + fields: [ + { + name: client.translate("economy/birthdate:HAPPY_BIRTHDAY", null, guildData.language), + value: client.translate("economy/birthdate:HAPPY_BIRTHDAY_MESSAGE", { + user: user.id, + age: `**${age}** ${client.functions.getNoun( + age, + client.translate("misc:NOUNS:AGE:1", null, guildData.language), + client.translate("misc:NOUNS:AGE:2", null, guildData.language), + client.translate("misc:NOUNS:AGE:5", null, guildData.language), + )}`, + }, guildData.language), + }, + ], + }); + + await channel.send({ embeds: [embed] }).then(m => m.react("🎉")); } - }); + } } - }); -}; \ No newline at end of file + } +}; diff --git a/helpers/autoUpdateDocs.js b/helpers/updateDocs.js similarity index 52% rename from helpers/autoUpdateDocs.js rename to helpers/updateDocs.js index ca5a6d5a..3f20a398 100644 --- a/helpers/autoUpdateDocs.js +++ b/helpers/updateDocs.js @@ -6,25 +6,31 @@ const table = require("markdown-table"), * @param {import("../base/Client")} client */ module.exports.update = function (client) { - const commands = [...new Map(client.commands.map(v => [v.constructor.name, v])).values()], - categories = []; + const commands = [...new Map(client.commands.map(v => [v.constructor.name, v])).values()]; + const categories = []; + // Collect unique command categories, ignoring the "Owner" category commands.forEach(cmd => { - if (cmd.category === "Owner") return; if (!categories.includes(cmd.category)) categories.push(cmd.category); }); - let text = `# JaBa has **${commands.length} ${client.functions.getNoun(commands.length, "command", "commands", "commands")}** in **${categories.length} ${client.functions.getNoun(categories.length, "category", "categories", "categories")}**! \n\n#### Table content \n**Name**: Command name \n**Description**: Command description \n**Usage**: How to use the command (*[]* - required, *()* - optional) \n**Accessible in**: Where you can use the command \n\n`; + // Build the initial text for the documentation + let text = `# JaBa has **${commands.length} ${client.functions.getNoun(commands.length, "command", "commands", "commands")}** in **${categories.length} ${client.functions.getNoun(categories.length, "category", "categories", "categories")}**! \n\n` + + "#### Table content \n" + + "**Name**: Command name \n" + + "**Description**: Command description \n" + + "**Usage**: How to use the command (*[]* - required, *()* - optional) \n" + + "**Accessible in**: Where you can use the command \n\n"; + // Sort categories and generate command documentation for each category categories.sort().forEach(cat => { const categoriesArray = [["Name", "Description", "Usage", "Accessible in"]]; - const cmds = [...new Map(commands.filter(cmd => cmd.category === cat).map(v => [v.constructor.name, v])).values()]; + const cmds = commands.filter(cmd => cmd.category === cat); text += `### ${cat} (${cmds.length} ${client.functions.getNoun(cmds.length, "command", "commands", "commands")})\n\n`; - cmds.sort(function (a, b) { - if (a.command.name < b.command.name) return -1; - else return 1; - }).forEach(cmd => { + + // Sort commands alphabetically by name + cmds.sort((a, b) => a.command.name.localeCompare(b.command.name)).forEach(cmd => { categoriesArray.push([ `**${cmd.command.name}**`, client.translate(`${cmd.category.toLowerCase()}/${cmd.command.name}:DESCRIPTION`), @@ -32,11 +38,18 @@ module.exports.update = function (client) { cmd.command.dm_permission ? "Anywhere" : "Servers only", ]); }); + + // Append the generated table to the documentation text text += `${table(categoriesArray)}\n\n`; }); - if (!fs.existsSync("./dashboard/public/docs")) fs.mkdirSync("./dashboard/public/docs"); - fs.writeFileSync("./dashboard/public/docs/commands.md", text); + // Ensure the output directory exists + const outputDir = "./dashboard/public/docs"; + if (!fs.existsSync(outputDir)) + fs.mkdirSync(outputDir, { recursive: true }); + + // Write the generated documentation to a Markdown file + fs.writeFileSync(`${outputDir}/commands.md`, text); client.logger.log("Dashboard docs updated!"); }; diff --git a/languages/en-US/misc.json b/languages/en-US/misc.json index 11607d92..921aba39 100644 --- a/languages/en-US/misc.json +++ b/languages/en-US/misc.json @@ -22,7 +22,7 @@ "QUOTE_CONTENT": "Content", "QUOTE_ATTACHED": "Attached files", "QUOTE_JUMP": "Jump to", - "QUOTE_FOOTER": "Quoted by {{user}}", + "QUOTE_FOOTER": "Sended", "MONTHS": { "JANUARY": "January", diff --git a/languages/ru-RU/misc.json b/languages/ru-RU/misc.json index 3c36affc..1fca914e 100644 --- a/languages/ru-RU/misc.json +++ b/languages/ru-RU/misc.json @@ -22,7 +22,7 @@ "QUOTE_CONTENT": "Содержимое", "QUOTE_ATTACHED": "Прикреплённые файлы", "QUOTE_JUMP": "Перейти к", - "QUOTE_FOOTER": "Цитировал {{user}}", + "QUOTE_FOOTER": "Отправлено", "MONTHS": { "JANUARY": "Январь", diff --git a/languages/uk-UA/misc.json b/languages/uk-UA/misc.json index aaf08e28..61d74810 100644 --- a/languages/uk-UA/misc.json +++ b/languages/uk-UA/misc.json @@ -22,7 +22,7 @@ "QUOTE_CONTENT": "Вміст", "QUOTE_ATTACHED": "Прикріплені файли", "QUOTE_JUMP": "Перейти до", - "QUOTE_FOOTER": "Цитував {{user}}", + "QUOTE_FOOTER": "Надіслано", "MONTHS": { "JANUARY": "Січень", diff --git a/package.json b/package.json index 6b29c19a..4ef4edd8 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "gamedig": "^4.1.0", "i18next": "^21.10.0", "i18next-fs-backend": "^1.2.0", + "markdown-table": "^2.0.0", "md5": "^2.3.0", "moment": "^2.29.4", "mongoose": "^7.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 508c5a3b..6ad51829 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: i18next-fs-backend: specifier: ^1.2.0 version: 1.2.0 + markdown-table: + specifier: ^2.0.0 + version: 2.0.0 md5: specifier: ^2.3.0 version: 2.3.0 @@ -845,6 +848,9 @@ packages: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} @@ -1077,6 +1083,10 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -2216,6 +2226,10 @@ snapshots: dependencies: semver: 6.3.1 + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + md5@2.3.0: dependencies: charenc: 0.0.2 @@ -2425,6 +2439,8 @@ snapshots: regenerator-runtime@0.14.1: {} + repeat-string@1.6.1: {} + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {}