diff --git a/events/messageCreate.js b/events/messageCreate.js index ded25b9..13183e6 100644 --- a/events/messageCreate.js +++ b/events/messageCreate.js @@ -1,8 +1,9 @@ -const Embed = require("../functions/embed"); -const Collector = require("../functions/messageCollector"); -const EditCollector = require("../functions/messageEdit"); -const CommandDB = require('../models/commands'); -const { isJson } = require('../functions/randomStr'); +const path = require("path"); +const Embed = require(path.join(__dirname, "../functions/embed")); +const Collector = require(path.join(__dirname, "../functions/messageCollector")); +const EditCollector = require(path.join(__dirname, "../functions/messageEdit")); +const CommandDB = require(path.join(__dirname, "../models/commands")); +const { isJson } = require(path.join(__dirname, "../functions/randomStr")); module.exports = async (client, message) => { // Early return checks diff --git a/events/messageDelete.js b/events/messageDelete.js index b84633a..60f7521 100644 --- a/events/messageDelete.js +++ b/events/messageDelete.js @@ -1,6 +1,7 @@ -const PollDB = require("../models/polls"); -const Giveaways = require("../models/giveaways"); -const GuildDB = require("../models/guilds"); +const path = require("path"); +const PollDB = require(path.join(__dirname, "../models/polls")); +const Giveaways = require(path.join(__dirname, "../models/giveaways")); +const GuildDB = require(path.join(__dirname, "../models/guilds")); module.exports = async (client, msg) => { const paginateCheck = client.paginate.get(msg.authorId); const pollCheck = client.polls.get(msg.id); diff --git a/events/messageReactionAdd.js b/events/messageReactionAdd.js index b32a4f8..9e62fa1 100644 --- a/events/messageReactionAdd.js +++ b/events/messageReactionAdd.js @@ -1,220 +1,362 @@ -const Embed = require("../functions/embed"); -const PollDB = require("../models/polls"); -const Giveaways = require("../models/giveaways"); -const emojis = [{ name: "1️⃣", id: 0 }, { name: "2️⃣", id: 1 }, { name: "3️⃣", id: 2 }, { name: "4️⃣", id: 3 }, { name: "5️⃣", id: 4 }, { name: "6️⃣", id: 5 }, { name: "7️⃣", id: 6 }, { name: "8️⃣", id: 7 }, { name: "9️⃣", id: 8 }, { name: "🔟", id: 9 }, { name: "🛑", id: "stop" }] -const colors = /^([A-Z0-9]+)/; +const path = require("path"); +const Embed = require(path.join(__dirname, "../functions/embed")); +const PollDB = require(path.join(__dirname, "../models/polls")); +const Giveaways = require(path.join(__dirname, "../models/giveaways")); -module.exports = async (client, message, userId, emojiId) => { - const paginateCheck = client.paginate.get(userId); - const pollCheck = client.polls.get(message.id); - const collector = client.messageCollector.get(userId); - const editCollector = client.messageEdit.get(userId); +// Constants +const EMOJIS = [ + { name: "1️⃣", id: 0 }, { name: "2️⃣", id: 1 }, { name: "3️⃣", id: 2 }, + { name: "4️⃣", id: 3 }, { name: "5️⃣", id: 4 }, { name: "6️⃣", id: 5 }, + { name: "7️⃣", id: 6 }, { name: "8️⃣", id: 7 }, { name: "9️⃣", id: 8 }, + { name: "🔟", id: 9 }, { name: "🛑", id: "stop" } +]; +const COLORS_REGEX = /^([A-Z0-9]+)/; - if (collector && collector.messageId === message.id || collector?.oldMessageId && collector?.oldMessageId === message.id && collector.channelId === message.channelId) { - if (emojiId === client.config.emojis.check) { - if (collector.roles.length === 0) { - const db = await client.database.getGuild(message.server.id); - message.delete().catch(() => { }); - client.messages.get(collector?.oldMessageId)?.delete().catch(() => { }) - const reactions = [...collector.rolesDone.map(e => e.emoji)]; - message.channel.sendMessage(collector.type === "content" ? { content: `${message.content}\n\n##### ${client.translate.get(db.language, "Events.messageReactionAdd.cooldown")}`, interactions: [reactions] } : { embeds: [new Embed().setColor("#A52F05").setDescription(`${client.messages.get(message.id).embeds[0].description}\n\n##### ${client.translate.get(db.language, "Events.messageReactionAdd.cooldown")}`)], interactions: [reactions] }).then(async (msg) => { - db.roles.push({ msgId: msg.id, chanId: msg.channelId, roles: [...collector.rolesDone] }); - await client.database.updateGuild(msg.server.id, { roles: db.roles }); - }); - - clearTimeout(client.messageCollector.get(userId).timeout); - return client.messageCollector.delete(userId); - } else return; - } else if (emojiId === client.config.emojis.cross) { +/** + * Handles message collector reactions + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {string} userId - The user ID + * @param {string} emojiId - The emoji ID + * @param {Object} collector - The message collector + * @returns {Promise} + */ +async function handleMessageCollector(client, message, userId, emojiId, collector) { + if (emojiId === client.config.emojis.check) { + if (collector.roles.length === 0) { const db = await client.database.getGuild(message.server.id); - client.messageCollector.delete(userId); - return message.reply({ embeds: [new Embed().setColor("#A52F05").setDescription(client.translate.get(db.language, "Events.messageReactionAdd.deleteCollector"))] },); - } else { - if (collector.roles.length === 0) return; - let emote; - if (colors.test(emojiId)) emote = `:${emojiId}:`; - else if (!colors.test(emojiId)) emote = emojiId - collector.rolesDone.push({ emoji: emojiId, role: collector.roles[0][0], name: collector.roles[0][1].name, color: collector.roles[0][1].colour?.includes("linear-gradient") ? '#000000' : collector.roles[0][1].colour }); - message.edit(collector.type === "content" ? { content: message.content.replace(`{role:${collector.regex[0]}}`, `${emote} $\\text{\\textcolor{${collector.roles[0][1].colour?.includes("linear-gradient") ? '#000000' : collector.roles[0][1].colour}}{${collector.roles[0][1].name}}}$`) } : { embeds: [new Embed().setColor("#A52F05").setDescription(client.messages.get(message.id).embeds[0].description.replace(`{role:${collector.regex[0]}}`, `:${emojiId}: $\\text{\\textcolor{${collector.roles[0][1].colour?.includes("linear-gradient") ? '#000000' : collector.roles[0][1].colour}}{${collector.roles[0][1].name}}}$`))] }) - collector.roles.shift(); - return collector.regex.shift(); - } - } else if (editCollector && editCollector.messageId === message.id || editCollector?.botMessage && editCollector?.botMessage === message.id && editCollector.channelId === message.channelId) { - if (emojiId === client.config.emojis.check) { - if (editCollector.roles.length === 0) { - const db = await client.database.getGuild(message.server.id); - message.delete().catch(() => { }); - client.messages.get(editCollector?.oldMessageId)?.delete().catch(() => { }) - client.messages.get(editCollector?.botMessage)?.delete().catch(() => { }) - const reactions = [...editCollector.rolesDone.map(e => e.emoji)]; - message.channel.sendMessage(editCollector.type === "content" ? { content: `${message.content}\n\n##### ${client.translate.get(db.language, "Events.messageReactionAdd.cooldown")}`, interactions: [reactions] } : { embeds: [new Embed().setColor("#A52F05").setDescription(`${client.messages.get(message.id).embeds[0].description}\n\n##### ${client.translate.get(db.language, "Events.messageReactionAdd.cooldown")}`)], interactions: [reactions] }).then(async (msg) => { - db.roles.push({ msgId: msg.id, chanId: msg.channelId, roles: [...editCollector.rolesDone] }); - await client.database.updateGuild(msg.server.id, { roles: db.roles.filter(e => e.msgId !== editCollector.oldMessageId) }); - }); + await Promise.all([ + message.delete().catch(() => {}), + client.messages.get(collector?.oldMessageId)?.delete().catch(() => {}) + ]); - clearTimeout(client.messageEdit.get(userId).timeout); - return client.messageEdit.delete(userId); - } else return; - } else if (emojiId === client.config.emojis.cross) { - const db = await client.database.getGuild(message.server.id); - client.messageEdit.delete(userId); - return message.reply({ embeds: [new Embed().setColor("#A52F05").setDescription(client.translate.get(db.language, "Events.messageReactionAdd.deleteCollector"))] },); - } else { - if (editCollector.roles.length === 0) return; - let emote; - if (colors.test(emojiId)) emote = `:${emojiId}:`; - else if (!colors.test(emojiId)) emote = emojiId - editCollector.rolesDone.push({ emoji: emojiId, role: editCollector.roles[0][0], name: editCollector.roles[0][1].name, color: editCollector.roles[0][1].colour?.includes("linear-gradient") ? '#000000' : editCollector.roles[0][1].colour }); - message.edit(editCollector.type === "content" ? { content: message.content.replace(`{role:${editCollector.regex[0]}}`, `${emote} $\\text{\\textcolor{${editCollector.roles[0][1].colour?.includes("linear-gradient") ? '#000000' : editCollector.roles[0][1].colour}}{${editCollector.roles[0][1].name}}}$`) } : { embeds: [new Embed().setColor("#A52F05").setDescription(client.messages.get(message.id).embeds[0].description.replace(`{role:${editCollector.regex[0]}}`, `:${emojiId}: $\\text{\\textcolor{${editCollector.roles[0][1].colour?.includes("linear-gradient") ? '#000000' : editCollector.roles[0][1].colour}}{${editCollector.roles[0][1].name}}}$`))] }) - editCollector.roles.shift(); - return editCollector.regex.shift(); + const reactions = [...collector.rolesDone.map(e => e.emoji)]; + const content = collector.type === "content" + ? { content: `${message.content}\n\n##### ${client.translate.get(db.language, "Events.messageReactionAdd.cooldown")}`, interactions: [reactions] } + : { embeds: [new Embed().setColor("#A52F05").setDescription(`${client.messages.get(message.id).embeds[0].description}\n\n##### ${client.translate.get(db.language, "Events.messageReactionAdd.cooldown")}`)], interactions: [reactions] }; + + const msg = await message.channel.sendMessage(content); + db.roles.push({ msgId: msg.id, chanId: msg.channelId, roles: [...collector.rolesDone] }); + await client.database.updateGuild(msg.server.id, { roles: db.roles }); + + clearTimeout(client.messageCollector.get(userId).timeout); + return client.messageCollector.delete(userId); } - } else if (paginateCheck && paginateCheck.message == message.id) { - let pages = paginateCheck.pages; - let page = paginateCheck.page; - switch (emojiId) { - case "⏪": - if (page !== 0) { - message.edit({ - embeds: [pages[0]] - }).catch(() => { }); - return paginateCheck.page = 0 - } else { - return; - } - case "⬅️": - if (pages[page - 1]) { - message.edit({ - embeds: [pages[--page]] - }).catch(() => { }); - return paginateCheck.page = paginateCheck.page - 1 - } else { - return; - } - case "➡️": - if (pages[page + 1]) { - message.edit({ - embeds: [pages[++page]] - }).catch(() => { }); - return paginateCheck.page = paginateCheck.page + 1 - } else { - return; - } - case "⏩": - if (page !== pages.length) { - message.edit({ - embeds: [pages[pages.length - 1]] - }).catch(() => { }); - return paginateCheck.page = pages.length - 1 - } else { - return; - } + return; + } + + if (emojiId === client.config.emojis.cross) { + const db = await client.database.getGuild(message.server.id); + client.messageCollector.delete(userId); + return message.reply({ + embeds: [new Embed().setColor("#A52F05").setDescription(client.translate.get(db.language, "Events.messageReactionAdd.deleteCollector"))] + }); + } + + if (collector.roles.length === 0) return; + + const emote = COLORS_REGEX.test(emojiId) ? `:${emojiId}:` : emojiId; + const role = collector.roles[0]; + const roleColor = role[1].colour?.includes("linear-gradient") ? '#000000' : role[1].colour; + + collector.rolesDone.push({ + emoji: emojiId, + role: role[0], + name: role[1].name, + color: roleColor + }); + + const content = collector.type === "content" + ? { content: message.content.replace(`{role:${collector.regex[0]}}`, `${emote} $\\text{\\textcolor{${roleColor}}{${role[1].name}}}$`) } + : { embeds: [new Embed().setColor("#A52F05").setDescription(client.messages.get(message.id).embeds[0].description.replace(`{role:${collector.regex[0]}}`, `:${emojiId}: $\\text{\\textcolor{${roleColor}}{${role[1].name}}}$`))] }; + + await message.edit(content); + collector.roles.shift(); + return collector.regex.shift(); +} + +/** + * Handles pagination reactions + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {Object} paginateCheck - The pagination check object + * @param {string} emojiId - The emoji ID + * @returns {Promise} + */ +async function handlePagination(client, message, paginateCheck, emojiId) { + const { pages, page } = paginateCheck; + let newPage = page; + + switch (emojiId) { + case "⏪": + if (page !== 0) { + await message.edit({ embeds: [pages[0]] }).catch(() => {}); + newPage = 0; + } + break; + case "⬅️": + if (pages[page - 1]) { + await message.edit({ embeds: [pages[--newPage]] }).catch(() => {}); + } + break; + case "➡️": + if (pages[page + 1]) { + await message.edit({ embeds: [pages[++newPage]] }).catch(() => {}); + } + break; + case "⏩": + if (page !== pages.length) { + await message.edit({ embeds: [pages[pages.length - 1]] }).catch(() => {}); + newPage = pages.length - 1; + } + break; + } + + if (newPage !== page) { + paginateCheck.page = newPage; + } +} + +/** + * Handles poll reactions + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {Object} pollCheck - The poll check object + * @param {string} userId - The user ID + * @param {string} emojiId - The emoji ID + * @returns {Promise} + */ +async function handlePoll(client, message, pollCheck, userId, emojiId) { + const convert = EMOJIS.findIndex(e => e.name === emojiId); + + if (convert === 10 && pollCheck.owner === userId) { + await PollDB.findOneAndDelete({ messageId: message.id }); + await pollCheck.poll.update(); + + const tooMuch = []; + if (pollCheck.poll.options.description.length > 80) { + tooMuch.push(`**${client.translate.get(pollCheck.lang, "Events.messageReactionAdd.title")}**: ${pollCheck.poll.options.description}`); } - } else if (pollCheck) { - let tooMuch = []; - if (pollCheck.poll.options.description.length > 80) tooMuch.push(`**${client.translate.get(pollCheck.lang, "Events.messageReactionAdd.title")}**: ${pollCheck.poll.options.description}`) pollCheck.poll.voteOptions.name.filter(e => e).forEach((e, i) => { - i++ if (e.length > 70) { - tooMuch.push(`**${i}.** ${e}`) + tooMuch.push(`**${i + 1}.** ${e}`); } }); - let convert = emojis.findIndex(e => e.name === emojiId); - if (convert === 10 && pollCheck.owner === userId) { - await PollDB.findOneAndDelete({ messageId: message.id }); - await pollCheck.poll.update(); - message.edit({ content: `${client.translate.get(pollCheck.lang, "Events.messageReactionAdd.owner")} (<@${pollCheck.owner}>) ${client.translate.get(pollCheck.lang, "Events.messageReactionAdd.end")}:`, embeds: [new Embed().setDescription(tooMuch.length > 0 ? tooMuch.map(e => e).join("\n") : null).setMedia(await client.Uploader.upload(pollCheck.poll.canvas.toBuffer(), `Poll.png`)).setColor("#F24646")] }).catch(() => { }); - return client.polls.delete(message.id); - } else if (convert === 0 && convert !== 10 || convert !== -1 && convert !== 10) { - if (client.reactions.get(userId)) return client.users.get(userId)?.openDM().then(dm => dm.sendMessage(client.translate.get(pollCheck.lang, "Events.messageReactionAdd.tooFast"))).catch(() => { }); - if (pollCheck.users.includes(userId)) return; - pollCheck.users.push(userId); - const user = (client.users.get(userId)) || await client.users.fetch(userId); - // console.log(user.avatar.id ? user.avatar.createFileURL() : 'https://chat.f95.io/api/users/01HATCWS7XZ7KEHW64AV20SMKR/default_avatar') - await pollCheck.poll.addVote(convert, userId, 'https://chat.f95.io/api/users/01HATCWS7XZ7KEHW64AV20SMKR/default_avatar', message.id); - message.edit({ embeds: [new Embed().setDescription(tooMuch.length > 0 ? tooMuch.map(e => e).join("\n") : null).setMedia(await client.Uploader.upload(pollCheck.poll.canvas.toBuffer(), `Poll.png`)).setColor("#A52F05")] }).catch(() => { }); - client.reactions.set(userId, Date.now() + 3000) - return setTimeout(() => client.reactions.delete(userId), 3000) - } else return; - } else { - const db = await Giveaways.findOne({ messageId: message.id }); - if (db) { - if (emojiId === client.config.emojis.confetti && db && !db.ended) { - if (client.reactions.get(userId)) return; - if (db.users.find(u => u.userID === userId)) return; - db.users.push({ userID: userId }); - db.picking.push({ userID: userId }); - db.save(); + await message.edit({ + content: `${client.translate.get(pollCheck.lang, "Events.messageReactionAdd.owner")} (<@${pollCheck.owner}>) ${client.translate.get(pollCheck.lang, "Events.messageReactionAdd.end")}:`, + embeds: [new Embed() + .setDescription(tooMuch.length > 0 ? tooMuch.join("\n") : null) + .setMedia(await client.Uploader.upload(pollCheck.poll.canvas.toBuffer(), `Poll.png`)) + .setColor("#F24646")] + }).catch(() => {}); - client.reactions.set(userId, Date.now() + 3000) - setTimeout(() => client.reactions.delete(userId), 3000) + return client.polls.delete(message.id); + } - client.users.get(userId)?.openDM().then(dm => dm.sendMessage(`${client.translate.get(db.lang, "Events.messageReactionAdd.joined")} [${db.prize}](https://chat.f95.io/server/${db.serverId}/channel/${db.channelId}/${db.messageId})!\n${client.translate.get(db.lang, "Events.messageReactionAdd.joined2")} **${db.users.length}** ${client.translate.get(db.lang, "Events.messageReactionAdd.joined3")}`)).catch(() => { }); - } else if (emojiId === client.config.emojis.stop && db && db.owner === userId && !db.ended) { - let endDate = Date.now(); + if (convert === 0 || (convert !== -1 && convert !== 10)) { + if (client.reactions.get(userId)) { + return client.users.get(userId)?.openDM() + .then(dm => dm.sendMessage(client.translate.get(pollCheck.lang, "Events.messageReactionAdd.tooFast"))) + .catch(() => {}); + } - if (db.users.length === 0) { - const noUsers = new Embed() - .setColor("#A52F05") - .setTitle(client.translate.get(db.lang, "Events.messageReactionAdd.giveaway")) - .setDescription(`${client.translate.get(db.lang, "Events.messageReactionAdd.owner")} (<@${userId}>) ${client.translate.get(db.lang, "Events.messageReactionAdd.early")}\n${client.translate.get(db.lang, "Events.messageReactionAdd.endNone")}!\n\n${client.translate.get(db.lang, "Events.messageReactionAdd.ended")}: \n${client.translate.get(db.lang, "Events.messageReactionAdd.prize")}: ${db.prize}\n${client.translate.get(db.lang, "Events.messageReactionAdd.winnersNone")}${db.requirement ? `\n${client.translate.get(db.lang, "Events.messageReactionAdd.reqs")}: ${db.requirement}` : ``}`) + if (pollCheck.users.includes(userId)) return; - await db.updateOne({ ended: true, endDate: endDate }) - await db.save(); - return await client.api.patch(`/channels/${db.channelId}/messages/${db.messageId}`, { "embeds": [noUsers] }); - } + pollCheck.users.push(userId); + const user = client.users.get(userId) || await client.users.fetch(userId); + await pollCheck.poll.addVote(convert, userId, 'https://chat.f95.io/api/users/01HATCWS7XZ7KEHW64AV20SMKR/default_avatar', message.id); - for (let i = 0; i < db.winners; i++) { - let winner = db.picking[Math.floor(Math.random() * db.picking.length)]; - if (winner) { - const filtered = db.picking.filter(object => object.userID != winner.userID) - db.picking = filtered; - db.pickedWinners.push({ id: winner.userID }) - } - } - - await db.updateOne({ ended: true, endDate: endDate }) - await db.save(); - - const noUsers = new Embed() - .setColor("#A52F05") - .setTitle(client.translate.get(db.lang, "Events.messageReactionAdd.giveaway")) - .setDescription(`${client.translate.get(db.lang, "Events.messageReactionAdd.owner")} (<@${userId}>) ${client.translate.get(db.lang, "Events.messageReactionAdd.early")}\n${client.translate.get(db.lang, "Events.messageReactionAdd.partici")}: ${db.users.length}\n\n${client.translate.get(db.lang, "Events.messageReactionAdd.ended")}: \n${client.translate.get(db.lang, "Events.messageReactionAdd.prize")}: ${db.prize}\n${client.translate.get(db.lang, "Events.messageReactionAdd.winners")}: ${db.pickedWinners.length > 0 ? db.pickedWinners.map(w => `<@${w.id}>`).join(", ") : client.translate.get(db.lang, "Events.messageReactionAdd.none")}${db.requirement ? `\n${client.translate.get(db.lang, "Events.messageReactionAdd.reqs")}: ${db.requirement}` : ``}`) - - message.edit({ embeds: [noUsers] }).catch(() => { }); - await client.api.post(`/channels/${db.channelId}/messages`, { "content": `${client.translate.get(db.lang, "Events.messageReactionAdd.congrats")} ${db.pickedWinners.map(w => `<@${w.id}>`).join(", ")}! ${client.translate.get(db.lang, "Events.messageReactionAdd.youWon")} **[${db.prize}](https://chat.f95.io/server/${db.serverId}/channel/${db.channelId}/${db.messageId})**!` }).catch(() => { }); - client.reactions.set(userId, Date.now() + 3000) - setTimeout(() => client.reactions.delete(userId), 3000) + const tooMuch = []; + if (pollCheck.poll.options.description.length > 80) { + tooMuch.push(`**${client.translate.get(pollCheck.lang, "Events.messageReactionAdd.title")}**: ${pollCheck.poll.options.description}`); + } + pollCheck.poll.voteOptions.name.filter(e => e).forEach((e, i) => { + if (e.length > 70) { + tooMuch.push(`**${i + 1}.** ${e}`); } - } else { - const db2 = await client.database.getGuild(message.server.id, true) - if (db2 && db2.roles.find(e => e.msgId === message.id) && db2.roles.find(e => e.roles.find(e => e.emoji === emojiId))) { - if (client.reactions.get(userId)) return; + }); - const roles = []; - db2.roles.find(e => e.msgId === message.id).roles.map(e => roles.push(e)); - const role = roles.find(e => e.emoji === emojiId); - const member = await (client.servers.get(message.server.id) || await client.servers.fetch(message.server.id))?.fetchMember(userId); - if (!member) return; + await message.edit({ + embeds: [new Embed() + .setDescription(tooMuch.length > 0 ? tooMuch.join("\n") : null) + .setMedia(await client.Uploader.upload(pollCheck.poll.canvas.toBuffer(), `Poll.png`)) + .setColor("#A52F05")] + }).catch(() => {}); - let error = false; - let dataRoles = []; - if (member.roles) member.roles.map(e => dataRoles.push(e)); - if (dataRoles.includes(role.role)) return; + client.reactions.set(userId, Date.now() + 3000); + setTimeout(() => client.reactions.delete(userId), 3000); + } +} - client.reactions.set(userId, Date.now() + 3000); - setTimeout(() => client.reactions.delete(userId), 3000); +/** + * Handles giveaway reactions + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {Object} db - The giveaway database object + * @param {string} userId - The user ID + * @param {string} emojiId - The emoji ID + * @returns {Promise} + */ +async function handleGiveaway(client, message, db, userId, emojiId) { + if (emojiId === client.config.emojis.confetti && !db.ended) { + if (client.reactions.get(userId) || db.users.find(u => u.userID === userId)) return; - dataRoles.push(role.role); - await member.edit({ roles: dataRoles }).catch(() => { error = true }) + db.users.push({ userID: userId }); + db.picking.push({ userID: userId }); + await db.save(); - if (error && db2.dm) { - member?.user?.openDM().then((dm) => { dm.sendMessage(`${client.translate.get(db2.language, "Events.messageReactionAdd.noPerms").replace("{role}", `**${role.name}**`)}!`) }).catch(() => { }); - } else if (db2.dm) { - member?.user?.openDM().then((dm) => { dm.sendMessage(`${client.translate.get(db2.language, "Events.messageReactionAdd.success").replace("{role}", `**${role.name}**`)}!`) }).catch(() => { }); - } + client.reactions.set(userId, Date.now() + 3000); + setTimeout(() => client.reactions.delete(userId), 3000); + + await client.users.get(userId)?.openDM() + .then(dm => dm.sendMessage( + `${client.translate.get(db.lang, "Events.messageReactionAdd.joined")} [${db.prize}](https://chat.f95.io/server/${db.serverId}/channel/${db.channelId}/${db.messageId})!\n` + + `${client.translate.get(db.lang, "Events.messageReactionAdd.joined2")} **${db.users.length}** ${client.translate.get(db.lang, "Events.messageReactionAdd.joined3")}` + )) + .catch(() => {}); + } else if (emojiId === client.config.emojis.stop && db.owner === userId && !db.ended) { + const endDate = Date.now(); + + if (db.users.length === 0) { + const noUsers = new Embed() + .setColor("#A52F05") + .setTitle(client.translate.get(db.lang, "Events.messageReactionAdd.giveaway")) + .setDescription( + `${client.translate.get(db.lang, "Events.messageReactionAdd.owner")} (<@${userId}>) ${client.translate.get(db.lang, "Events.messageReactionAdd.early")}\n` + + `${client.translate.get(db.lang, "Events.messageReactionAdd.endNone")}!\n\n` + + `${client.translate.get(db.lang, "Events.messageReactionAdd.ended")}: \n` + + `${client.translate.get(db.lang, "Events.messageReactionAdd.prize")}: ${db.prize}\n` + + `${client.translate.get(db.lang, "Events.messageReactionAdd.winnersNone")}` + + (db.requirement ? `\n${client.translate.get(db.lang, "Events.messageReactionAdd.reqs")}: ${db.requirement}` : ``) + ); + + await db.updateOne({ ended: true, endDate }); + await db.save(); + return await client.api.patch(`/channels/${db.channelId}/messages/${db.messageId}`, { embeds: [noUsers] }); + } + + for (let i = 0; i < db.winners; i++) { + const winner = db.picking[Math.floor(Math.random() * db.picking.length)]; + if (winner) { + db.picking = db.picking.filter(object => object.userID !== winner.userID); + db.pickedWinners.push({ id: winner.userID }); } } + + await db.updateOne({ ended: true, endDate }); + await db.save(); + + const noUsers = new Embed() + .setColor("#A52F05") + .setTitle(client.translate.get(db.lang, "Events.messageReactionAdd.giveaway")) + .setDescription( + `${client.translate.get(db.lang, "Events.messageReactionAdd.owner")} (<@${userId}>) ${client.translate.get(db.lang, "Events.messageReactionAdd.early")}\n` + + `${client.translate.get(db.lang, "Events.messageReactionAdd.partici")}: ${db.users.length}\n\n` + + `${client.translate.get(db.lang, "Events.messageReactionAdd.ended")}: \n` + + `${client.translate.get(db.lang, "Events.messageReactionAdd.prize")}: ${db.prize}\n` + + `${client.translate.get(db.lang, "Events.messageReactionAdd.winners")}: ${db.pickedWinners.length > 0 ? db.pickedWinners.map(w => `<@${w.id}>`).join(", ") : client.translate.get(db.lang, "Events.messageReactionAdd.none")}` + + (db.requirement ? `\n${client.translate.get(db.lang, "Events.messageReactionAdd.reqs")}: ${db.requirement}` : ``) + ); + + await message.edit({ embeds: [noUsers] }).catch(() => {}); + await client.api.post(`/channels/${db.channelId}/messages`, { + content: `${client.translate.get(db.lang, "Events.messageReactionAdd.congrats")} ${db.pickedWinners.map(w => `<@${w.id}>`).join(", ")}! ${client.translate.get(db.lang, "Events.messageReactionAdd.youWon")} **[${db.prize}](https://chat.f95.io/server/${db.serverId}/channel/${db.channelId}/${db.messageId})**!` + }).catch(() => {}); + + client.reactions.set(userId, Date.now() + 3000); + setTimeout(() => client.reactions.delete(userId), 3000); } -} \ No newline at end of file +} + +/** + * Handles role reactions + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {Object} db - The guild database object + * @param {string} userId - The user ID + * @param {string} emojiId - The emoji ID + * @returns {Promise} + */ +async function handleRoleReaction(client, message, db, userId, emojiId) { + if (client.reactions.get(userId)) return; + + const roles = []; + db.roles.find(e => e.msgId === message.id).roles.map(e => roles.push(e)); + const role = roles.find(e => e.emoji === emojiId); + + const member = await (client.servers.get(message.server.id) || await client.servers.fetch(message.server.id))?.fetchMember(userId); + if (!member) return; + + let error = false; + let dataRoles = []; + if (member.roles) member.roles.map(e => dataRoles.push(e)); + if (dataRoles.includes(role.role)) return; + + client.reactions.set(userId, Date.now() + 3000); + setTimeout(() => client.reactions.delete(userId), 3000); + + dataRoles.push(role.role); + await member.edit({ roles: dataRoles }).catch(() => { error = true }); + + if (db.dm) { + const dmMessage = error + ? client.translate.get(db.language, "Events.messageReactionAdd.noPerms").replace("{role}", `**${role.name}**`) + : client.translate.get(db.language, "Events.messageReactionAdd.success").replace("{role}", `**${role.name}**`); + + await member?.user?.openDM() + .then(dm => dm.sendMessage(dmMessage)) + .catch(() => {}); + } +} + +/** + * Main event handler for message reactions + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {string} userId - The user ID + * @param {string} emojiId - The emoji ID + * @returns {Promise} + */ +module.exports = async (client, message, userId, emojiId) => { + try { + const paginateCheck = client.paginate.get(userId); + const pollCheck = client.polls.get(message.id); + const collector = client.messageCollector.get(userId); + const editCollector = client.messageEdit.get(userId); + + // Handle message collector + if (collector && (collector.messageId === message.id || (collector?.oldMessageId === message.id && collector.channelId === message.channelId))) { + return await handleMessageCollector(client, message, userId, emojiId, collector); + } + + // Handle edit collector + if (editCollector && (editCollector.messageId === message.id || (editCollector?.botMessage === message.id && editCollector.channelId === message.channelId))) { + return await handleMessageCollector(client, message, userId, emojiId, editCollector); + } + + // Handle pagination + if (paginateCheck && paginateCheck.message === message.id) { + return await handlePagination(client, message, paginateCheck, emojiId); + } + + // Handle poll + if (pollCheck) { + return await handlePoll(client, message, pollCheck, userId, emojiId); + } + + // Handle giveaway + const giveaway = await Giveaways.findOne({ messageId: message.id }); + if (giveaway) { + return await handleGiveaway(client, message, giveaway, userId, emojiId); + } + + // Handle role reaction + const db = await client.database.getGuild(message.server.id, true); + if (db?.roles.find(e => e.msgId === message.id)?.roles.find(e => e.emoji === emojiId)) { + return await handleRoleReaction(client, message, db, userId, emojiId); + } + } catch (error) { + console.error('Error in messageReactionAdd:', error); + } +}; \ No newline at end of file diff --git a/events/messageReactionRemove.js b/events/messageReactionRemove.js index 5ad5dbf..bde7def 100644 --- a/events/messageReactionRemove.js +++ b/events/messageReactionRemove.js @@ -1,100 +1,199 @@ -const Embed = require("../functions/embed"); -const Giveaways = require("../models/giveaways"); -const emojis = [{ name: "1️⃣", id: 0 }, { name: "2️⃣", id: 1 }, { name: "3️⃣", id: 2 }, { name: "4️⃣", id: 3 }, { name: "5️⃣", id: 4 }, { name: "6️⃣", id: 5 }, { name: "7️⃣", id: 6 }, { name: "8️⃣", id: 7 }, { name: "9️⃣", id: 8 }, { name: "🔟", id: 9 }, { name: "🛑", id: "stop" }] -const colors = /^([A-Z0-9]+)/; +const path = require("path"); +const Embed = require(path.join(__dirname, "../functions/embed")); +const Giveaways = require(path.join(__dirname, "../models/giveaways")); -module.exports = async (client, message, userId, emojiId) => { - const pollCheck = client.polls.get(message.id); - const collector = client.messageCollector.get(userId); - const editCollector = client.messageEdit.get(userId); +// Constants +const EMOJIS = [ + { name: "1️⃣", id: 0 }, { name: "2️⃣", id: 1 }, { name: "3️⃣", id: 2 }, + { name: "4️⃣", id: 3 }, { name: "5️⃣", id: 4 }, { name: "6️⃣", id: 5 }, + { name: "7️⃣", id: 6 }, { name: "8️⃣", id: 7 }, { name: "9️⃣", id: 8 }, + { name: "🔟", id: 9 }, { name: "🛑", id: "stop" } +]; +const COLORS_REGEX = /^([A-Z0-9]+)/; - if (collector && collector.messageId === message.id && collector.channelId === message.channelId) { - const emoji = collector.rolesDone.find(e => e.emoji === emojiId); - if (emoji) { - collector.rolesDone = collector.rolesDone.filter(object => object.emoji != emojiId); - collector.roles.push([emoji.role, { name: emoji.name, colour: emoji.color }]); - collector.regex.push(emoji.name); +/** + * Handles message collector reaction removal + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {string} emojiId - The emoji ID + * @param {Object} collector - The message collector + * @returns {Promise} + */ +async function handleMessageCollector(client, message, emojiId, collector) { + const emoji = collector.rolesDone.find(e => e.emoji === emojiId); + if (!emoji) return; - if (colors.test(emojiId)) emote = `:${emojiId}:`; - else if (!colors.test(emojiId)) emote = emojiId - return message.edit(collector.type === "content" ? { content: message.content.replace(`${emote} $\\text{\\textcolor{${emoji.color}}{${emoji.name}}}$`, `{role:${emoji.name}}`) } : { embeds: [new Embed().setColor("#A52F05").setDescription(client.messages.get(message.id).embeds[0].description.replace(`{role:${editCollector.regex[0]}}`, `:${emojiId}: $\\text{\\textcolor{${editCollector.roles[0][1].colour?.includes("linear-gradient") ? '#000000' : editCollector.roles[0][1].colour}}{${editCollector.roles[0][1].name}}}$`))] }).catch(() => { }); - } - } else if (editCollector && editCollector.messageId === message.id && editCollector.channelId === message.channelId) { - const emoji = editCollector.rolesDone.find(e => e.emoji === emojiId); - if (emoji) { - editCollector.rolesDone = editCollector.rolesDone.filter(object => object.emoji != emojiId); - editCollector.roles.push([emoji.role, { name: emoji.name, colour: emoji.color }]); - editCollector.regex.push(emoji.name); + collector.rolesDone = collector.rolesDone.filter(object => object.emoji !== emojiId); + collector.roles.push([emoji.role, { name: emoji.name, colour: emoji.color }]); + collector.regex.push(emoji.name); - if (colors.test(emojiId)) emote = `:${emojiId}:`; - else if (!colors.test(emojiId)) emote = emojiId - return message.edit(editCollector.type === "content" ? { content: message.content.replace(`${emote} $\\text{\\textcolor{${emoji.color}}{${emoji.name}}}$`, `{role:${emoji.name}}`) } : { embeds: [new Embed().setColor("#A52F05").setDescription(client.messages.get(message.id).embeds[0].description.replace(`{role:${editCollector.regex[0]}}`, `:${emojiId}: $\\text{\\textcolor{${editCollector.roles[0][1].colour?.includes("linear-gradient") ? '#000000' : editCollector.roles[0][1].colour}}{${editCollector.roles[0][1].name}}}$`))] }).catch(() => { }); - } - } else if (pollCheck) { - if (client.reactions.get(userId)) return client.users.get(userId)?.openDM().then(dm => dm.sendMessage(client.translate.get(pollCheck.language, "Events.messageReactionRemove.tooFast"))).catch(() => { }); - let convert = emojis.findIndex(e => e.name === emojiId); - if (convert === 0 && convert !== 10 || convert !== -1 && convert !== 10) { - if (!pollCheck.users.includes(userId)) return; + const emote = COLORS_REGEX.test(emojiId) ? `:${emojiId}:` : emojiId; + const content = collector.type === "content" + ? { content: message.content.replace(`${emote} $\\text{\\textcolor{${emoji.color}}{${emoji.name}}}$`, `{role:${emoji.name}}`) } + : { embeds: [new Embed().setColor("#A52F05").setDescription(client.messages.get(message.id).embeds[0].description.replace(`{role:${collector.regex[0]}}`, `:${emojiId}: $\\text{\\textcolor{${collector.roles[0][1].colour?.includes("linear-gradient") ? '#000000' : collector.roles[0][1].colour}}{${collector.roles[0][1].name}}}$`))] }; - let tooMuch = []; - if (pollCheck.poll.options.description.length > 80) tooMuch.push(`**${client.translate.get(pollCheck.language, "Events.messageReactionRemove.title")}**: ${pollCheck.poll.options.description}`) - pollCheck.poll.voteOptions.name.filter(e => e).forEach((e, i) => { - i++ - if (e.length > 70) { - tooMuch.push(`**${i}.** ${e}`) - } - }); + return message.edit(content).catch(() => {}); +} - pollCheck.users = pollCheck.users.filter(object => object != userId); - const user = (client.users.get(userId)) || await client.users.fetch(userId); - await pollCheck.poll.removeVote(convert, userId, 'https://chat.f95.io/api/users/01HATCWS7XZ7KEHW64AV20SMKR/default_avatar', message.id); - message.edit({ embeds: [new Embed().setDescription(tooMuch.length > 0 ? tooMuch.map(e => e).join("\n") : null).setMedia(await client.Uploader.upload(pollCheck.poll.canvas.toBuffer(), `Poll.png`)).setColor("#A52F05")] }).catch(() => { }); - client.reactions.set(userId, Date.now() + 3000) - return setTimeout(() => client.reactions.delete(userId), 3000) - } else return; - } else { - if (client.reactions.get(userId)) return; - const db = await Giveaways.findOne({ messageId: message.id }); - if (db && !db.ended) { - if (emojiId === client.config.emojis.confetti) { - if (!db.users.find(u => u.userID === userId)) return; - const filtered = db.users.filter(object => object.userID != userId) - db.users = filtered; - const filtered2 = db.picking.filter(object => object.userID != userId) - db.picking = filtered2; - db.save(); - - client.reactions.set(userId, Date.now() + 3000) - setTimeout(() => client.reactions.delete(userId), 3000) - - client.users.get(userId)?.openDM().then(dm => dm.sendMessage(`${client.translate.get(pollCheck.language, "Events.messageReactionRemove.left")} [${db.prize}](https://chat.f95.io/server/${db.serverId}/channel/${db.channelId}/${db.messageId})!\n${client.translate.get(pollCheck.language, "Events.messageReactionRemove.left2")} **${db.users.length}** ${client.translate.get(pollCheck.language, "Events.messageReactionRemove.left")}!`)).catch(() => { }); - } - } else { - const db2 = await client.database.getGuild(message.server.id, true) - if (db2 && db2.roles.find(e => e.msgId === message.id) && db2.roles.find(e => e.roles.find(e => e.emoji === emojiId))) { - const roles = []; - db2.roles.find(e => e.msgId === message.id).roles.map(e => roles.push(e)); - const role = roles.find(e => e.emoji === emojiId); - const member = await (client.servers.get(message.server.id) || await client.servers.fetch(message.server.id))?.fetchMember(userId); - if (!member) return; - - let error = false; - let dataRoles = []; - if (member.roles) member.roles.map(e => dataRoles.push(e)); - if (!dataRoles.includes(role.role)) return; - - client.reactions.set(userId, Date.now() + 3000); - setTimeout(() => client.reactions.delete(userId), 3000); - - dataRoles = dataRoles.filter(object => object != role.role); - await member.edit({ roles: dataRoles }).catch(() => { error = true }) - - if (error) { - if (db2.dm) member?.user?.openDM().then((dm) => { dm.sendMessage(`${client.translate.get(db2.language, "Events.messageReactionRemove.noPerms").replace("{role}", `**${role.name}**`)}!`) }).catch(() => { }); - } else { - if (db2.dm) member?.user?.openDM().then((dm) => { dm.sendMessage(`${client.translate.get(db2.language, "Events.messageReactionRemove.success").replace("{role}", `**${role.name}**`)}!`) }).catch(() => { }); - } - } - } +/** + * Handles poll reaction removal + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {Object} pollCheck - The poll check object + * @param {string} userId - The user ID + * @param {string} emojiId - The emoji ID + * @returns {Promise} + */ +async function handlePoll(client, message, pollCheck, userId, emojiId) { + if (client.reactions.get(userId)) { + return client.users.get(userId)?.openDM() + .then(dm => dm.sendMessage(client.translate.get(pollCheck.language, "Events.messageReactionRemove.tooFast"))) + .catch(() => {}); } -} \ No newline at end of file + + const convert = EMOJIS.findIndex(e => e.name === emojiId); + if (convert !== 0 && convert === 10 || convert === -1) return; + + if (!pollCheck.users.includes(userId)) return; + + const tooMuch = []; + if (pollCheck.poll.options.description.length > 80) { + tooMuch.push(`**${client.translate.get(pollCheck.language, "Events.messageReactionRemove.title")}**: ${pollCheck.poll.options.description}`); + } + pollCheck.poll.voteOptions.name.filter(e => e).forEach((e, i) => { + if (e.length > 70) { + tooMuch.push(`**${i + 1}.** ${e}`); + } + }); + + pollCheck.users = pollCheck.users.filter(object => object !== userId); + const user = client.users.get(userId) || await client.users.fetch(userId); + await pollCheck.poll.removeVote(convert, userId, 'https://chat.f95.io/api/users/01HATCWS7XZ7KEHW64AV20SMKR/default_avatar', message.id); + + await message.edit({ + embeds: [new Embed() + .setDescription(tooMuch.length > 0 ? tooMuch.join("\n") : null) + .setMedia(await client.Uploader.upload(pollCheck.poll.canvas.toBuffer(), `Poll.png`)) + .setColor("#A52F05")] + }).catch(() => {}); + + client.reactions.set(userId, Date.now() + 3000); + setTimeout(() => client.reactions.delete(userId), 3000); +} + +/** + * Handles giveaway reaction removal + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {Object} db - The giveaway database object + * @param {string} userId - The user ID + * @param {string} emojiId - The emoji ID + * @returns {Promise} + */ +async function handleGiveaway(client, message, db, userId, emojiId) { + if (client.reactions.get(userId)) return; + + if (emojiId === client.config.emojis.confetti && !db.ended) { + if (!db.users.find(u => u.userID === userId)) return; + + db.users = db.users.filter(object => object.userID !== userId); + db.picking = db.picking.filter(object => object.userID !== userId); + await db.save(); + + client.reactions.set(userId, Date.now() + 3000); + setTimeout(() => client.reactions.delete(userId), 3000); + + await client.users.get(userId)?.openDM() + .then(dm => dm.sendMessage( + `${client.translate.get(db.language, "Events.messageReactionRemove.left")} [${db.prize}](https://chat.f95.io/server/${db.serverId}/channel/${db.channelId}/${db.messageId})!\n` + + `${client.translate.get(db.language, "Events.messageReactionRemove.left2")} **${db.users.length}** ${client.translate.get(db.language, "Events.messageReactionRemove.left")}!` + )) + .catch(() => {}); + } +} + +/** + * Handles role reaction removal + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {Object} db - The guild database object + * @param {string} userId - The user ID + * @param {string} emojiId - The emoji ID + * @returns {Promise} + */ +async function handleRoleReaction(client, message, db, userId, emojiId) { + if (client.reactions.get(userId)) return; + + const roles = []; + db.roles.find(e => e.msgId === message.id).roles.map(e => roles.push(e)); + const role = roles.find(e => e.emoji === emojiId); + + const member = await (client.servers.get(message.server.id) || await client.servers.fetch(message.server.id))?.fetchMember(userId); + if (!member) return; + + let error = false; + let dataRoles = []; + if (member.roles) member.roles.map(e => dataRoles.push(e)); + if (!dataRoles.includes(role.role)) return; + + client.reactions.set(userId, Date.now() + 3000); + setTimeout(() => client.reactions.delete(userId), 3000); + + dataRoles = dataRoles.filter(object => object !== role.role); + await member.edit({ roles: dataRoles }).catch(() => { error = true }); + + if (db.dm) { + const dmMessage = error + ? client.translate.get(db.language, "Events.messageReactionRemove.noPerms").replace("{role}", `**${role.name}**`) + : client.translate.get(db.language, "Events.messageReactionRemove.success").replace("{role}", `**${role.name}**`); + + await member?.user?.openDM() + .then(dm => dm.sendMessage(dmMessage)) + .catch(() => {}); + } +} + +/** + * Main event handler for message reaction removal + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {string} userId - The user ID + * @param {string} emojiId - The emoji ID + * @returns {Promise} + */ +module.exports = async (client, message, userId, emojiId) => { + try { + const pollCheck = client.polls.get(message.id); + const collector = client.messageCollector.get(userId); + const editCollector = client.messageEdit.get(userId); + + // Handle message collector + if (collector && collector.messageId === message.id && collector.channelId === message.channelId) { + return await handleMessageCollector(client, message, emojiId, collector); + } + + // Handle edit collector + if (editCollector && editCollector.messageId === message.id && editCollector.channelId === message.channelId) { + return await handleMessageCollector(client, message, emojiId, editCollector); + } + + // Handle poll + if (pollCheck) { + return await handlePoll(client, message, pollCheck, userId, emojiId); + } + + // Handle giveaway + const giveaway = await Giveaways.findOne({ messageId: message.id }); + if (giveaway && !giveaway.ended) { + return await handleGiveaway(client, message, giveaway, userId, emojiId); + } + + // Handle role reaction + const db = await client.database.getGuild(message.server.id, true); + if (db?.roles.find(e => e.msgId === message.id)?.roles.find(e => e.emoji === emojiId)) { + return await handleRoleReaction(client, message, db, userId, emojiId); + } + } catch (error) { + console.error('Error in messageReactionRemove:', error); + } +}; \ No newline at end of file diff --git a/events/serverCreate.js b/events/serverCreate.js index 8d3f0c5..8a1dd52 100644 --- a/events/serverCreate.js +++ b/events/serverCreate.js @@ -1,4 +1,5 @@ -const Embed = require("../functions/embed") +const path = require("path"); +const Embed = require(path.join(__dirname, "../functions/embed")); module.exports = async (client, server) => { //console.log("join", server) // await client.database.getGuild(server.id, true) diff --git a/events/serverDelete.js b/events/serverDelete.js index e4dde77..5efccaf 100644 --- a/events/serverDelete.js +++ b/events/serverDelete.js @@ -1,4 +1,5 @@ -const Embed = require("../functions/embed") +const path = require("path"); +const Embed = require(path.join(__dirname, "../functions/embed")); module.exports = async (client, server) => { //console.log("delete",server) // await client.database.deleteGuild(server.id) diff --git a/events/serverMemberUpdate.js b/events/serverMemberUpdate.js index 18f5a6d..c669574 100644 --- a/events/serverMemberUpdate.js +++ b/events/serverMemberUpdate.js @@ -1,3 +1,4 @@ +const path = require("path"); let type; module.exports = async (client, member, memberOld) => { // Work in progress diff --git a/functions/audit.js b/functions/audit.js index ba48964..831832b 100644 --- a/functions/audit.js +++ b/functions/audit.js @@ -1,4 +1,5 @@ -const db = require("../models/logging"); +const path = require("path"); +const db = require(path.join(__dirname, "../models/logging")); async function audit(type, message, cmd) { const audit = new db(); diff --git a/functions/checkPolls.js b/functions/checkPolls.js index 3618366..9e8cc56 100644 --- a/functions/checkPolls.js +++ b/functions/checkPolls.js @@ -1,23 +1,69 @@ -const db = require("../models/polls"); -const Polls = require("./poll"); +const path = require("path"); +const db = require(path.join(__dirname, "../models/polls")); +const Polls = require(path.join(__dirname, "./poll")); + +/** + * Checks and processes all active polls in the database + * @param {Object} client - The Discord client instance + * @returns {Promise} + */ async function checkPolls(client) { - let polls = await db.find(); - if (!polls || polls.length === 0) return; - let i = 0; - for (let poll of polls) { - i++ - setTimeout(async () => { - const time = poll.now - (Date.now() - poll.time), users = poll.users, avatars = poll.avatars, votes = poll.votes, desc = poll.desc, name = poll.name, names = poll.options, owner = poll.owner, lang = poll.lang; - const newPoll = new Polls({ time, client, name: { name: name, description: desc }, options: names, votes: votes, users: users, avatars: avatars, owner: owner, lang: lang }) + try { + const polls = await db.find(); + if (!polls?.length) return; + // Process polls concurrently with a small delay between each + await Promise.all(polls.map(async (poll, index) => { try { - await client.channels.get(poll.channelId).fetchMessage(poll.messageId).catch(() => { return }); - const msg = await client.messages.get(poll.messageId); - if (msg) newPoll.start(msg, newPoll); - } catch (e) { } + // Add small delay between processing each poll to prevent rate limiting + await new Promise(resolve => setTimeout(resolve, index * 700)); - poll.deleteOne({ messageId: poll.messageId }); - }, i * 700); + const { + now, + time, + users, + avatars, + votes, + desc, + name, + options: names, + owner, + lang, + channelId, + messageId + } = poll; + + const timeRemaining = now - (Date.now() - time); + + const newPoll = new Polls({ + time: timeRemaining, + client, + name: { name, description: desc }, + options: names, + votes, + users, + avatars, + owner, + lang + }); + + // Attempt to fetch and process the message + const channel = client.channels.get(channelId); + if (!channel) return; + + const message = await channel.fetchMessage(messageId).catch(() => null); + if (message) { + await newPoll.start(message, newPoll); + } + + // Clean up the poll from database + await poll.deleteOne({ messageId }); + } catch (error) { + console.error(`Error processing poll ${poll.messageId}:`, error); + } + })); + } catch (error) { + console.error('Error in checkPolls:', error); } } diff --git a/functions/checkRoles.js b/functions/checkRoles.js index 9fa1c4a..1d0c5e7 100644 --- a/functions/checkRoles.js +++ b/functions/checkRoles.js @@ -1,23 +1,72 @@ -const db = require("../models/guilds"); -async function checkRoles(client) { - let rr = await db.find({ $expr: { $gt: [{ $size: "$roles" }, 0] } }); - if (!rr || rr.length === 0) return; - let i = 0; - let ii = 0; - for (let r of rr) { - i++ - setTimeout(async () => { - r.roles.map((role) => { - ii++ - setTimeout(async () => { - if (!client.channels.get(role.chanId) && role.roles.length === 0) { - return await client.database.updateGuild(r.id, { roles: r.roles.filter(e => e.msgId !== role.msgId) }); - } +const path = require("path"); +const db = require(path.join(__dirname, "../models/guilds")); - await client.channels.get(role.chanId)?.fetchMessage(role.msgId).catch(() => { }); - }, ii * 700); - }); - }, i * 600); +/** + * Checks and validates role reaction messages across all guilds + * @param {Object} client - Discord client instance + * @returns {Promise} + */ +async function checkRoles(client) { + try { + // Find all guilds with role reactions + const guildsWithRoles = await db.find({ + $expr: { $gt: [{ $size: "$roles" }, 0] } + }); + + if (!guildsWithRoles?.length) return; + + // Process each guild sequentially to avoid rate limits + for (const guild of guildsWithRoles) { + try { + await processGuildRoles(client, guild); + } catch (error) { + console.error(`Error processing guild ${guild.id}:`, error); + } + } + } catch (error) { + console.error('Error in checkRoles:', error); + } +} + +/** + * Process role reactions for a single guild + * @param {Object} client - Discord client instance + * @param {Object} guild - Guild data from database + * @returns {Promise} + */ +async function processGuildRoles(client, guild) { + const validRoles = []; + const invalidRoles = []; + + // Process each role reaction message + for (const role of guild.roles) { + try { + const channel = client.channels.get(role.chanId); + + // Skip if channel doesn't exist or role array is empty + if (!channel || !role.roles?.length) { + invalidRoles.push(role); + continue; + } + + // Verify message exists + const message = await channel.fetchMessage(role.msgId).catch(() => null); + if (message) { + validRoles.push(role); + } else { + invalidRoles.push(role); + } + } catch (error) { + console.error(`Error processing role ${role.msgId} in guild ${guild.id}:`, error); + invalidRoles.push(role); + } + } + + // Update guild with only valid roles if there were any invalid ones + if (invalidRoles.length > 0) { + await client.database.updateGuild(guild.id, { + roles: validRoles + }); } } diff --git a/functions/colorCodes.js b/functions/colorCodes.js index c2d6977..9053842 100644 --- a/functions/colorCodes.js +++ b/functions/colorCodes.js @@ -1,26 +1,45 @@ -function colors(altColorChar, textToTranslate) { - const colorMap = { - [`${altColorChar}0`]: '\x1b[30m', // Black - [`${altColorChar}1`]: '\x1b[34m', // Dark Blue - [`${altColorChar}2`]: '\x1b[32m', // Dark Green - [`${altColorChar}3`]: '\x1b[36m', // Dark Aqua - [`${altColorChar}4`]: '\x1b[31m', // Dark Red - [`${altColorChar}5`]: '\x1b[35m', // Dark Purple - [`${altColorChar}6`]: '\x1b[33m', // Gold - [`${altColorChar}7`]: '\x1b[37m', // Gray - [`${altColorChar}8`]: '\x1b[90m', // Dark Gray - [`${altColorChar}9`]: '\x1b[94m', // Blue - [`${altColorChar}a`]: '\x1b[92m', // Green - [`${altColorChar}b`]: '\x1b[96m', // Aqua - [`${altColorChar}c`]: '\x1b[91m', // Red - [`${altColorChar}d`]: '\x1b[95m', // Light Purple - [`${altColorChar}e`]: '\x1b[93m', // Yellow - [`${altColorChar}f`]: '\x1b[97m', // White - [`${altColorChar}r`]: '\x1b[0m', // Reset - }; - - const regex = new RegExp(`${altColorChar}([0-9a-fr])`, 'g'); - return textToTranslate.replace(regex, (match, code) => colorMap[`${altColorChar}${code}`] || ''); +/** + * ANSI color codes mapping for terminal text coloring + * @type {Object} + */ +const ANSI_COLORS = { + '0': '\x1b[30m', // Black + '1': '\x1b[34m', // Dark Blue + '2': '\x1b[32m', // Dark Green + '3': '\x1b[36m', // Dark Aqua + '4': '\x1b[31m', // Dark Red + '5': '\x1b[35m', // Dark Purple + '6': '\x1b[33m', // Gold + '7': '\x1b[37m', // Gray + '8': '\x1b[90m', // Dark Gray + '9': '\x1b[94m', // Blue + 'a': '\x1b[92m', // Green + 'b': '\x1b[96m', // Aqua + 'c': '\x1b[91m', // Red + 'd': '\x1b[95m', // Light Purple + 'e': '\x1b[93m', // Yellow + 'f': '\x1b[97m', // White + 'r': '\x1b[0m', // Reset }; +/** + * Translates color codes in text to ANSI color sequences + * @param {string} altColorChar - The character used to prefix color codes (e.g., '&' or '§') + * @param {string} textToTranslate - The text containing color codes to translate + * @returns {string} The text with color codes replaced by ANSI sequences + * @throws {Error} If altColorChar is not a single character + */ +function colors(altColorChar, textToTranslate) { + if (typeof altColorChar !== 'string' || altColorChar.length !== 1) { + throw new Error('altColorChar must be a single character'); + } + + if (typeof textToTranslate !== 'string') { + throw new Error('textToTranslate must be a string'); + } + + const regex = new RegExp(`${altColorChar}([0-9a-fr])`, 'g'); + return textToTranslate.replace(regex, (_, code) => ANSI_COLORS[code] || ''); +} + module.exports = colors; diff --git a/functions/dhms.js b/functions/dhms.js index 69239f3..a0a28ca 100644 --- a/functions/dhms.js +++ b/functions/dhms.js @@ -1,11 +1,40 @@ -function dhms(str, sec = false) { - const x = sec ? 1 : 1000; - if (typeof str !== 'string') return 0; - const fixed = str.replace(/\s/g, ''); - const tail = +fixed.match(/-?\d+$/g) || 0; - const parts = (fixed.match(/-?\d+[^-0-9]+/g) || []) - .map(v => +v.replace(/[^-0-9]+/g, '') * ({ s: x, m: 60 * x, h: 3600 * x, d: 86400 * x }[v.replace(/[-0-9]+/g, '')] || 0)); - return [tail, ...parts].reduce((a, b) => a + b, 0); -}; +/** + * Converts a time string (e.g., "1d2h3m4s") into milliseconds or seconds + * @param {string} timeStr - The time string to convert (e.g., "1d2h3m4s") + * @param {boolean} [inSeconds=false] - If true, returns result in seconds instead of milliseconds + * @returns {number} The converted time in milliseconds or seconds + */ +function dhms(timeStr, inSeconds = false) { + // Return 0 for invalid input + if (typeof timeStr !== 'string' || !timeStr.trim()) { + return 0; + } + + // Define time unit multipliers + const multipliers = { + s: inSeconds ? 1 : 1000, + m: inSeconds ? 60 : 60000, + h: inSeconds ? 3600 : 3600000, + d: inSeconds ? 86400 : 86400000 + }; + + // Remove whitespace and split into parts + const cleanStr = timeStr.replace(/\s/g, ''); + + // Extract the numeric value at the end (if any) + const tailMatch = cleanStr.match(/-?\d+$/); + const tailValue = tailMatch ? parseInt(tailMatch[0], 10) : 0; + + // Extract and convert time parts + const timeParts = (cleanStr.match(/-?\d+[^-0-9]+/g) || []) + .map(part => { + const value = parseInt(part.replace(/[^-0-9]+/g, ''), 10); + const unit = part.replace(/[-0-9]+/g, ''); + return value * (multipliers[unit] || 0); + }); + + // Sum all parts including the tail + return [tailValue, ...timeParts].reduce((sum, value) => sum + value, 0); +} module.exports = dhms; \ No newline at end of file diff --git a/functions/fetchTime.js b/functions/fetchTime.js index a8d40b3..6ba2fb2 100644 --- a/functions/fetchTime.js +++ b/functions/fetchTime.js @@ -1,16 +1,34 @@ -function fetchTime(ms, client, lang) { - var totalSeconds = (ms / 1000); - let years = Math.floor(totalSeconds / 31536000); - totalSeconds %= 31536000; - let days = Math.floor(totalSeconds / 86400); - totalSeconds %= 86400; - let hours = Math.floor(totalSeconds / 3600); - totalSeconds %= 3600; - let minutes = Math.floor(totalSeconds / 60); - let seconds = totalSeconds % 60; - seconds = Math.floor(seconds); +/** + * Converts milliseconds into a human-readable time string with localized units + * @param {number} milliseconds - The time in milliseconds to convert + * @param {Object} client - The client object containing translation functionality + * @param {string} lang - The language code for translations + * @returns {string} Formatted time string with localized units + */ +function fetchTime(milliseconds, client, lang) { + if (!Number.isFinite(milliseconds) || milliseconds < 0) { + throw new Error('Invalid milliseconds value provided'); + } - return `${years ? `${years} ${client.translate.get(lang, "Functions.fetchTime.years")},` : ""} ${days ? `${days} ${client.translate.get(lang, "Functions.fetchTime.days")},` : ""} ${hours ? `${hours} ${client.translate.get(lang, "Functions.fetchTime.hours")},` : ""} ${minutes ? `${minutes} ${client.translate.get(lang, "Functions.fetchTime.minutes")},` : ""} ${seconds} ${client.translate.get(lang, "Functions.fetchTime.seconds")}`; + const timeUnits = [ + { value: 31536000, key: 'years' }, + { value: 86400, key: 'days' }, + { value: 3600, key: 'hours' }, + { value: 60, key: 'minutes' }, + { value: 1, key: 'seconds' } + ]; + + let remainingSeconds = Math.floor(milliseconds / 1000); + + return timeUnits + .map(({ value, key }) => { + const unitValue = Math.floor(remainingSeconds / value); + remainingSeconds %= value; + return unitValue ? `${unitValue} ${client.translate.get(lang, `Functions.fetchTime.${key}`)},` : ''; + }) + .filter(Boolean) + .join(' ') + .trim(); } module.exports = fetchTime; \ No newline at end of file diff --git a/functions/logger.js b/functions/logger.js index 23f332d..ffdb926 100644 --- a/functions/logger.js +++ b/functions/logger.js @@ -1,38 +1,105 @@ /** - * base code taken from https://github.com/Mirasaki/logger + * Enhanced logger utility with date-fns for date formatting + * Original code inspired by https://github.com/Mirasaki/logger */ -const chalk = require('chalk'), - moment = require('moment'); +const chalk = require('chalk'); +const { format, formatInTimeZone } = require('date-fns-tz'); -const tagList = { - SYSLOG: chalk.greenBright('[SYSLOG]'), - SYSERR: chalk.redBright('[SYSERR]'), - SUCCESS: chalk.greenBright('[SUCCESS]'), - INFO: chalk.blueBright('[INFO]'), - DEBUG: chalk.magentaBright('[DEBUG]'), - DATA: chalk.yellowBright('[DATA]'), - COMMAND: chalk.whiteBright('[CMD]'), - EVENT: chalk.cyanBright('[EVENT]'), - ERROR: chalk.redBright('[EVENT]'), - WARN: chalk.yellowBright('[WARN]') +// Define log levels and their corresponding styles +const LOG_LEVELS = { + SYSLOG: { color: 'greenBright', prefix: '[SYSLOG]' }, + SYSERR: { color: 'redBright', prefix: '[SYSERR]' }, + SUCCESS: { color: 'greenBright', prefix: '[SUCCESS]' }, + INFO: { color: 'blueBright', prefix: '[INFO]' }, + DEBUG: { color: 'magentaBright', prefix: '[DEBUG]' }, + DATA: { color: 'yellowBright', prefix: '[DATA]' }, + COMMAND: { color: 'whiteBright', prefix: '[CMD]' }, + EVENT: { color: 'cyanBright', prefix: '[EVENT]' }, + ERROR: { color: 'redBright', prefix: '[ERROR]' }, + WARN: { color: 'yellowBright', prefix: '[WARN]' } }; +// Create tag list with chalk styling +const tagList = Object.entries(LOG_LEVELS).reduce((acc, [key, { color, prefix }]) => ({ + ...acc, + [key]: chalk[color](prefix) +}), {}); + +// Calculate longest tag length for alignment const longestTagLength = Math.max(...Object.values(tagList).map(t => t.length)); -const getTag = (tag) => `${tagList[tag]}${''.repeat(longestTagLength - tagList[tag].length)}`; -const timestamp = () => `${chalk.whiteBright.bold(`[${moment.utc().format('YYYY-MM-DD HH:mm:ss')}]`)}`; + +/** + * Get formatted tag with proper spacing + * @param {string} tag - The log level tag + * @returns {string} Formatted tag with spacing + */ +const getTag = (tag) => `${tagList[tag]}${' '.repeat(longestTagLength - tagList[tag].length)}`; + +/** + * Get formatted timestamp + * @returns {string} Formatted timestamp with styling + */ +const timestamp = () => { + const now = new Date(); + const formattedDate = formatInTimeZone(now, 'UTC', 'yyyy-MM-dd HH:mm:ss'); + return chalk.whiteBright.bold(`[${formattedDate}]`); +}; + +/** + * Create a log message with proper formatting + * @param {string} level - Log level + * @param {string} type - Optional type identifier + * @param {string} message - Log message + * @returns {string} Formatted log message + */ +const createLogMessage = (level, type, message) => { + const typeTag = type ? `${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : ':'; + return `${timestamp()} ${getTag(level)} ${typeTag} ${message}`; +}; + +/** + * Format error stack trace with proper styling + * @param {Error} err - Error object + * @returns {string} Formatted error stack trace + */ +const formatErrorStack = (err) => { + if (!err.stack) return chalk.red(err); + + return err.stack + .split('\n') + .map((msg, index) => { + if (index === 0) return chalk.red(msg); + + const isFailedFunctionCall = index === 1; + const traceStartIndex = msg.indexOf('('); + const traceEndIndex = msg.lastIndexOf(')'); + const hasTrace = traceStartIndex !== -1; + const functionCall = msg.slice( + msg.indexOf('at') + 3, + hasTrace ? traceStartIndex - 1 : msg.length + ); + const trace = msg.slice(traceStartIndex, traceEndIndex + 1); + + return ` ${chalk.grey('at')} ${isFailedFunctionCall + ? `${chalk.redBright(functionCall)} ${chalk.red.underline(trace)}` + : `${chalk.greenBright(functionCall)} ${chalk.grey(trace)}` + }`; + }) + .join('\n'); +}; module.exports = { - syslog: (type, str) => console.info(`${timestamp()} ${type ? `${getTag('SYSLOG')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('SYSLOG')}:`} ${str}`), - syserr: (type, str) => console.error(`${timestamp()} ${type ? `${getTag('SYSERR')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('SYSERR')} :`} ${str}`), - success: (type, str) => console.log(`${timestamp()} ${type ? `${getTag('SUCCESS')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('SUCCESS')}:`} ${str}`), - info: (type, str) => console.info(`${timestamp()} ${type ? `${getTag('INFO')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('INFO')}:`} ${str}`), - debug: (type, str) => console.log(`${timestamp()} ${type ? `${getTag('DEBUG')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('DEBUG')}:`} ${str}`), - data: (type, str) => console.log(`${timestamp()} ${type ? `${getTag('DATA')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('DATA')}:`} ${str}`), - command: (type, str) => console.log(`${timestamp()} ${type ? `${getTag('COMMAND')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('COMMAND')}:`} ${str}`), - event: (type, str) => console.log(`${timestamp()} ${type ? `${getTag('EVENT')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('EVENT')}:`} ${str}`), - error: (type, str) => console.log(`${timestamp()} ${type ? `${getTag('ERROR')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('ERROR')}:`} ${str}`), - warn: (type, str) => console.log(`${timestamp()} ${type ? `${getTag('WARN')} ${chalk.whiteBright.bgBlue.bold(`[${type}]`)}:` : `${getTag('WARN')}:`} ${str}`), + syslog: (type, str) => console.info(createLogMessage('SYSLOG', type, str)), + syserr: (type, str) => console.error(createLogMessage('SYSERR', type, str)), + success: (type, str) => console.log(createLogMessage('SUCCESS', type, str)), + info: (type, str) => console.info(createLogMessage('INFO', type, str)), + debug: (type, str) => console.log(createLogMessage('DEBUG', type, str)), + data: (type, str) => console.log(createLogMessage('DATA', type, str)), + command: (type, str) => console.log(createLogMessage('COMMAND', type, str)), + event: (type, str) => console.log(createLogMessage('EVENT', type, str)), + error: (type, str) => console.log(createLogMessage('ERROR', type, str)), + warn: (type, str) => console.log(createLogMessage('WARN', type, str)), startLog: (identifier) => console.log(`${timestamp()} ${getTag('DEBUG')} ${chalk.greenBright('[START]')} ${identifier}`), endLog: (identifier) => console.log(`${timestamp()} ${getTag('DEBUG')} ${chalk.redBright('[ END ]')} ${identifier}`), @@ -40,46 +107,17 @@ module.exports = { timestamp, getExecutionTime: (hrtime) => { const timeSinceHrMs = ( - process.hrtime(hrtime)[0] * 1000 - + hrtime[1] / 1000000 + process.hrtime(hrtime)[0] * 1000 + + hrtime[1] / 1000000 ).toFixed(2); - return `${chalk.yellowBright( - (timeSinceHrMs / 1000).toFixed(2)) - } seconds (${chalk.yellowBright(timeSinceHrMs)} ms)`; + return `${chalk.yellowBright((timeSinceHrMs / 1000).toFixed(2))} seconds (${chalk.yellowBright(timeSinceHrMs)} ms)`; }, printErr: (err) => { if (!(err instanceof Error)) { - console.error(err) + console.error(err); return; } - - console.error( - !err.stack - ? chalk.red(err) - : err.stack - .split('\n') - .map((msg, index) => { - if (index === 0) { - return chalk.red(msg); - } - - const isFailedFunctionCall = index === 1; - const traceStartIndex = msg.indexOf('('); - const traceEndIndex = msg.lastIndexOf(')'); - const hasTrace = traceStartIndex !== -1; - const functionCall = msg.slice( - msg.indexOf('at') + 3, - hasTrace ? traceStartIndex - 1 : msg.length - ); - const trace = msg.slice(traceStartIndex, traceEndIndex + 1); - - return ` ${chalk.grey('at')} ${isFailedFunctionCall - ? `${chalk.redBright(functionCall)} ${chalk.red.underline(trace)}` - : `${chalk.greenBright(functionCall)} ${chalk.grey(trace)}` - }`; - }) - .join('\n') - ) + console.error(formatErrorStack(err)); } }; \ No newline at end of file diff --git a/functions/messageCollector.js b/functions/messageCollector.js index 5c41fe2..9b6bf8d 100644 --- a/functions/messageCollector.js +++ b/functions/messageCollector.js @@ -1,72 +1,129 @@ -const Embed = require("../functions/embed"); +const path = require("path"); +const Embed = require(path.join(__dirname, "../functions/embed")); + +/** + * Validates and processes role assignments from a message + * @param {Object} client - The client instance + * @param {Object} message - The message object + * @param {Object} db - Database instance + * @returns {Promise} + */ async function Collector(client, message, db) { - const regex = /{role:(.*?)}/; - const regexAll = /{role:(.*?)}/g; + const ROLE_REGEX = /{role:(.*?)}/g; + const MAX_ROLES = 20; + + // Get collector for the message author const collector = client.messageCollector.get(message.authorId); - if (!message.content.match(regexAll) || message.content.match(regexAll)?.length === 0) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(`${client.translate.get(db.language, "Events.messageCreate.noRoles")}: \`{role:Red}\``)] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); + + // Validate message contains role tags + const roleMatches = message.content.match(ROLE_REGEX); + if (!roleMatches?.length) { + await sendErrorResponse(message, client, db, "Events.messageCreate.noRoles", `\`{role:Red}\``); + return; } - const roles = message.content.match(regexAll).map((r) => r?.match(regex)[1]); - if (roles.length > 20) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(client.translate.get(db.language, "Events.messageCreate.maxRoles"))] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); - } - collector.regex = roles - const roleIds = [] - let newRoles = roles.map((r) => { - return [...message.server.roles].map((r) => r).find((role) => r.toLowerCase() === role[1]?.name?.toLowerCase()); - }) - newRoles.map((r) => roleIds.push(r)); - - if (roleIds.map((r) => !r).includes(true)) { - let unknown = []; - roleIds.map((r, i) => { - i++ - if (!r) { - unknown.push(roles[i - 1]); - } - }); - - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(`${client.translate.get(db.language, "Events.messageCreate.unknown")}\n${unknown.map(e => `\`{role:${e}}\``).join(", ")}`)] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); + // Extract role names from message + const roleNames = roleMatches.map(match => match.match(/{role:(.*?)}/)[1]); + + // Check role count limit + if (roleNames.length > MAX_ROLES) { + await sendErrorResponse(message, client, db, "Events.messageCreate.maxRoles"); + return; } - let duplicate = []; - roleIds.map((r, i) => { - i++ - if (roleIds.filter(e => e[0] === r[0]).length > 1) duplicate.push(roleIds[i - 1]); - }); + collector.regex = roleNames; - if (duplicate.length > 0) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(`${client.translate.get(db.language, "Events.messageCreate.duplicate")}\n${duplicate.map(e => `\`{role:${e[1].name}}\``)}`)] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); + // Find matching server roles + const serverRoles = [...message.server.roles].map(([id, role]) => ({ id, role })); + const matchedRoles = roleNames.map(name => + serverRoles.find(({ role }) => role.name.toLowerCase() === name.toLowerCase()) + ); + + // Check for unknown roles + const unknownRoles = roleNames.filter((name, index) => !matchedRoles[index]); + if (unknownRoles.length > 0) { + await sendErrorResponse( + message, + client, + db, + "Events.messageCreate.unknown", + unknownRoles.map(role => `\`{role:${role}}\``).join(", ") + ); + return; } - let positions = []; - const botRole = message.channel.server.member.orderedRoles.reverse()[0] + // Check for duplicate roles + const roleIds = matchedRoles.map(({ id }) => id); + const duplicates = roleIds.filter((id, index) => roleIds.indexOf(id) !== index); + if (duplicates.length > 0) { + const duplicateRoles = duplicates.map(id => + matchedRoles.find(({ id: roleId }) => roleId === id).role.name + ); + await sendErrorResponse( + message, + client, + db, + "Events.messageCreate.duplicate", + duplicateRoles.map(name => `\`{role:${name}}\``).join(", ") + ); + return; + } + + // Check bot role permissions + const botRole = message.channel.server.member.orderedRoles.reverse()[0]; if (!botRole) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(client.translate.get(db.language, "Events.messageCreate.noBotRole"))] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); + await sendErrorResponse(message, client, db, "Events.messageCreate.noBotRole"); + return; } - roleIds.map((r, i) => { - i++ - if (r[1].rank <= botRole.rank) positions.push(roleIds[i - 1]) - }); - - if (positions.length > 0) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(`${client.translate.get(db.language, "Events.messageCreate.positions")}\n${positions.map(e => `\`{role:${e[1].name}}\``)}`)] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); + // Check role positions + const invalidPositions = matchedRoles.filter(({ role }) => role.rank <= botRole.rank); + if (invalidPositions.length > 0) { + await sendErrorResponse( + message, + client, + db, + "Events.messageCreate.positions", + invalidPositions.map(({ role }) => `\`{role:${role.name}}\``).join(", ") + ); + return; } - message.delete().catch(() => { }); - collector.roles = roleIds; + // Process valid role assignment + await message.delete().catch(() => {}); + collector.roles = matchedRoles; + const react = [client.config.emojis.check]; - return message.channel.sendMessage(collector.type === "content" ? { content: message.content, interactions: [react] } : { embeds: [new Embed().setDescription(message.content).setColor("#A52F05")], interactions: [react] }).then((msg) => { - collector.messageId = msg.id; - }); + const messageContent = collector.type === "content" + ? { content: message.content, interactions: [react] } + : { + embeds: [new Embed().setDescription(message.content).setColor("#A52F05")], + interactions: [react] + }; + + const sentMessage = await message.channel.sendMessage(messageContent); + collector.messageId = sentMessage.id; +} + +/** + * Helper function to send error responses + * @param {Object} message - The message object + * @param {Object} client - The client instance + * @param {Object} db - Database instance + * @param {string} translationKey - Translation key for the error message + * @param {string} [additionalInfo] - Additional information to append to the error message + */ +async function sendErrorResponse(message, client, db, translationKey, additionalInfo = "") { + const errorMessage = additionalInfo + ? `${client.translate.get(db.language, translationKey)}\n${additionalInfo}` + : client.translate.get(db.language, translationKey); + + await message.reply( + { embeds: [new Embed().setColor("#FF0000").setDescription(errorMessage)] }, + false + ).catch(() => {}); + + await message.react(client.config.emojis.cross).catch(() => {}); } module.exports = Collector; \ No newline at end of file diff --git a/functions/messageEdit.js b/functions/messageEdit.js index 14f3a49..25a2e8c 100644 --- a/functions/messageEdit.js +++ b/functions/messageEdit.js @@ -1,72 +1,157 @@ -const Embed = require("../functions/embed"); -async function Collector(client, message, db) { - const regex = /{role:(.*?)}/; - const regexAll = /{role:(.*?)}/g; - const collector = client.messageEdit.get(message.authorId); - if (!message.content.match(regexAll) || message.content.match(regexAll)?.length === 0) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(`${client.translate.get(db.language, "Events.messageCreate.noRoles")}: \`{role:Red}\``)] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); +const path = require("path"); +const Embed = require(path.join(__dirname, "../functions/embed")); + +/** + * Validates role mentions in a message + * @param {string} content - Message content to validate + * @param {string} language - User's language setting + * @param {Object} client - Discord client instance + * @returns {Object} Validation result with roles and error message if any + */ +function validateRoleMentions(content, language, client) { + const regex = /{role:(.*?)}/g; + const matches = content.match(regex); + + if (!matches || matches.length === 0) { + return { + isValid: false, + error: client.translate.get(language, "Events.messageCreate.noRoles") + }; } - const roles = message.content.match(regexAll).map((r) => r?.match(regex)[1]); + const roles = matches.map(match => match.match(/{role:(.*?)}/)[1]); + if (roles.length > 20) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(client.translate.get(db.language, "Events.messageCreate.maxRoles"))] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); - } - collector.regex = roles - const roleIds = [] - let newRoles = roles.map((r) => { - return [...message.server.roles].map((r) => r).find((role) => r.toLowerCase() === role[1]?.name?.toLowerCase()); - }) - newRoles.map((r) => roleIds.push(r)); - - if (roleIds.map((r) => !r).includes(true)) { - let unknown = []; - roleIds.map((r, i) => { - i++ - if (!r) { - unknown.push(roles[i - 1]); - } - }); - - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(`${client.translate.get(db.language, "Events.messageCreate.unknown")}\n${unknown.map(e => `\`{role:${e}}\``).join(", ")}`)] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); + return { + isValid: false, + error: client.translate.get(language, "Events.messageCreate.maxRoles") + }; } - let duplicate = []; - roleIds.map((r, i) => { - i++ - if (roleIds.filter(e => e[0] === r[0]).length > 1) duplicate.push(roleIds[i - 1]); - }); + return { isValid: true, roles }; +} - if (duplicate.length > 0) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(`${client.translate.get(db.language, "Events.messageCreate.duplicate")}\n${duplicate.map(e => `\`{role:${e[1].name}}\``)}`)] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); +/** + * Finds server roles by name + * @param {Array} roles - Array of role names + * @param {Object} server - Server object + * @returns {Object} Result containing found roles and unknown roles + */ +function findServerRoles(roles, server) { + const roleIds = roles.map(roleName => + [...server.roles].find(([_, role]) => + roleName.toLowerCase() === role.name.toLowerCase() + ) + ); + + const unknown = roles.filter((_, index) => !roleIds[index]); + + return { roleIds, unknown }; +} + +/** + * Checks for duplicate roles + * @param {Array} roleIds - Array of role IDs + * @returns {Array} Array of duplicate roles + */ +function findDuplicateRoles(roleIds) { + return roleIds.filter((role, index) => + roleIds.findIndex(r => r[0] === role[0]) !== index + ); +} + +/** + * Checks role positions against bot's role + * @param {Array} roleIds - Array of role IDs + * @param {Object} botRole - Bot's highest role + * @returns {Array} Array of roles with invalid positions + */ +function checkRolePositions(roleIds, botRole) { + return roleIds.filter(role => role[1].rank <= botRole.rank); +} + +/** + * Main collector function for message editing + * @param {Object} client - Discord client instance + * @param {Object} message - Message object + * @param {Object} db - Database object + */ +async function Collector(client, message, db) { + const collector = client.messageEdit.get(message.authorId); + + // Validate role mentions + const validation = validateRoleMentions(message.content, db.language, client); + if (!validation.isValid) { + await message.reply({ + embeds: [new Embed().setColor("#FF0000").setDescription(validation.error)] + }, false).catch(() => {}); + return message.react(client.config.emojis.cross).catch(() => {}); } - let positions = []; - const botRole = message.channel.server.member.orderedRoles.reverse()[0] + // Find server roles + const { roleIds, unknown } = findServerRoles(validation.roles, message.server); + if (unknown.length > 0) { + await message.reply({ + embeds: [new Embed().setColor("#FF0000").setDescription( + `${client.translate.get(db.language, "Events.messageCreate.unknown")}\n${ + unknown.map(e => `\`{role:${e}}\``).join(", ") + }` + )] + }, false).catch(() => {}); + return message.react(client.config.emojis.cross).catch(() => {}); + } + + // Check for duplicates + const duplicates = findDuplicateRoles(roleIds); + if (duplicates.length > 0) { + await message.reply({ + embeds: [new Embed().setColor("#FF0000").setDescription( + `${client.translate.get(db.language, "Events.messageCreate.duplicate")}\n${ + duplicates.map(e => `\`{role:${e[1].name}}\``) + }` + )] + }, false).catch(() => {}); + return message.react(client.config.emojis.cross).catch(() => {}); + } + + // Check role positions + const botRole = message.channel.server.member.orderedRoles.reverse()[0]; if (!botRole) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(client.translate.get(db.language, "Events.messageCreate.noBotRole"))] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); + await message.reply({ + embeds: [new Embed().setColor("#FF0000").setDescription( + client.translate.get(db.language, "Events.messageCreate.noBotRole") + )] + }, false).catch(() => {}); + return message.react(client.config.emojis.cross).catch(() => {}); } - roleIds.map((r, i) => { - i++ - if (r[1].rank <= botRole.rank) positions.push(roleIds[i - 1]) - }); - - if (positions.length > 0) { - message.reply({ embeds: [new Embed().setColor("#FF0000").setDescription(`${client.translate.get(db.language, "Events.messageCreate.positions")}\n${positions.map(e => `\`{role:${e[1].name}}\``)}`)] }, false).catch(() => { return }); - return message.react(client.config.emojis.cross).catch(() => { return }); + const invalidPositions = checkRolePositions(roleIds, botRole); + if (invalidPositions.length > 0) { + await message.reply({ + embeds: [new Embed().setColor("#FF0000").setDescription( + `${client.translate.get(db.language, "Events.messageCreate.positions")}\n${ + invalidPositions.map(e => `\`{role:${e[1].name}}\``) + }` + )] + }, false).catch(() => {}); + return message.react(client.config.emojis.cross).catch(() => {}); } - message.delete().catch(() => { }); + // Process valid message + await message.delete().catch(() => {}); collector.roles = roleIds; + collector.regex = validation.roles; + const react = [client.config.emojis.check]; - return message.channel.sendMessage(collector.type === "content" ? { content: message.content, interactions: [react] } : { embeds: [new Embed().setDescription(message.content).setColor("#A52F05")], interactions: [react] }).then((msg) => { - collector.messageId = msg.id; - }); + const messageContent = collector.type === "content" + ? { content: message.content, interactions: [react] } + : { + embeds: [new Embed().setDescription(message.content).setColor("#A52F05")], + interactions: [react] + }; + + const msg = await message.channel.sendMessage(messageContent); + collector.messageId = msg.id; } module.exports = Collector; \ No newline at end of file diff --git a/functions/poll.js b/functions/poll.js index 9a49c70..634343f 100644 --- a/functions/poll.js +++ b/functions/poll.js @@ -1,34 +1,104 @@ -const PollDB = require("../models/polls"); -const Embed = require("./embed"); +const path = require("path"); +const PollDB = require(path.join(__dirname, "../models/polls")); +const Embed = require(path.join(__dirname, "./embed")); const Canvas = require('canvas'); -const format = `${(new Date().getMonth() + 1) < 10 ? `0${new Date().getMonth() + 1}` : new Date().getMonth() + 1}/${new Date().getDate()}/${new Date().getFullYear()} ${new Date().getHours()}:${(new Date().getMinutes() < 10 ? '0' : '') + new Date().getMinutes()}`; + +// Constants +const CANVAS_CONFIG = { + WIDTH: 600, + PADDING: 10, + BAR_HEIGHT: 40, + FONT_SIZES: { + SMALL: '12px', + MEDIUM: '17px' + }, + COLORS: { + BACKGROUND: "#23272A", + TEXT_PRIMARY: "#FFFFFF", + TEXT_SECONDARY: "#4E535A", + BAR_BACKGROUND: "#2C2F33", + BAR_FILL: "#A52F05", + BAR_INACTIVE: "#24282B", + SELECTION: "#717cf4" + } +}; + +// Helper function to format date +const getFormattedDate = () => { + const now = new Date(); + const month = (now.getMonth() + 1).toString().padStart(2, '0'); + const day = now.getDate().toString().padStart(2, '0'); + const hours = now.getHours().toString().padStart(2, '0'); + const minutes = now.getMinutes().toString().padStart(2, '0'); + return `${month}/${day}/${now.getFullYear()} ${hours}:${minutes}`; +}; + +/** + * Polls class for managing and rendering polls + */ class Polls { - constructor({ time, client, name, options, users, avatars, votes, owner, lang }) { + /** + * @param {Object} options - Poll configuration options + * @param {number} options.time - Duration of the poll in milliseconds + * @param {Object} options.client - Discord client instance + * @param {Object} options.name - Poll name and description + * @param {Object} options.options - Poll options configuration + * @param {string[]} [options.users] - Array of user IDs who voted + * @param {string[]} [options.avatars] - Array of user avatar URLs + * @param {number[]} [options.votes] - Array of vote counts + * @param {string} options.owner - Poll owner ID + * @param {string} options.lang - Language code + */ + constructor({ time, client, name, options, users = [], avatars = [], votes, owner, lang }) { this.client = client; this.time = time; - if (votes) this.votes = votes; - else this.votes = options.name.length === 2 ? [0, 0] : options.name.length === 3 ? [0, 0, 0] : options.name.length === 4 ? [0, 0, 0, 0] : options.name.length === 5 ? [0, 0, 0, 0, 0] : options.name.length === 6 ? [0, 0, 0, 0, 0, 0] : options.name.length === 7 ? [0, 0, 0, 0, 0, 0, 0] : options.name.length === 8 ? [0, 0, 0, 0, 0, 0, 0, 0] : options.name.length === 9 ? [0, 0, 0, 0, 0, 0, 0, 0, 0] : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - this.users = users || []; - this.avatars = avatars || []; - this.options = { name: name.name, description: name.description }; - this.voteOptions = options; this.owner = owner; this.lang = lang; - this.size = { canvas: options.name.length === 2 ? 200 : options.name.length === 3 ? 250 : options.name.length === 4 ? 300 : options.name.length === 5 ? 350 : options.name.length === 6 ? 400 : options.name.length === 7 ? 450 : options.name.length === 8 ? 500 : options.name.length === 9 ? 550 : 600, bar: options.name.length === 2 ? 150 : options.name.length === 3 ? 200 : options.name.length === 4 ? 250 : options.name.length === 5 ? 300 : options.name.length === 6 ? 350 : options.name.length === 7 ? 400 : options.name.length === 8 ? 450 : options.name.length === 9 ? 500 : 550 }; + this.users = users; + this.avatars = avatars; + this.options = { name: name.name, description: name.description }; + this.voteOptions = options; + + // Initialize votes array based on number of options + this.votes = votes || new Array(options.name.length).fill(0); + + // Calculate canvas dimensions based on number of options + this.size = this.calculateCanvasSize(options.name.length); } + /** + * Calculate canvas dimensions based on number of options + * @param {number} optionCount - Number of poll options + * @returns {Object} Canvas and bar dimensions + */ + calculateCanvasSize(optionCount) { + const baseSize = 200; + const increment = 50; + const size = baseSize + (optionCount - 2) * increment; + return { + canvas: size, + bar: size - 50 + }; + } + + /** + * Start the poll + * @param {Object} message - Discord message object + * @param {Object} poll - Poll instance + */ async start(message, poll) { - this.client.polls.set(message.id, { poll, messageId: message.id, users: this.users, owner: this.owner, lang: this.lang }) - setTimeout(async () => { - if (!this.client.polls.get(message.id)) return; - await this.update(); - message.edit({ embeds: [new Embed().setMedia(await this.client.Uploader.upload(poll.canvas.toBuffer(), `Poll.png`)).setColor(`#F24646`)], content: this.client.translate.get(this.lang, "Functions.poll.end") }).catch(() => { }) - this.client.polls.delete(message.id); - await PollDB.findOneAndDelete({ messageId: message.id }); - }, this.time); + this.client.polls.set(message.id, { + poll, + messageId: message.id, + users: this.users, + owner: this.owner, + lang: this.lang + }); if (this.time < 0) return; - await (new PollDB({ + + // Save poll to database + await new PollDB({ owner: this.owner, channelId: message.channelId, messageId: message.id, @@ -41,208 +111,325 @@ class Polls { time: this.time, lang: this.lang, now: Date.now(), - }).save()); - } + }).save(); - textHeight(text, ctx, m) { - let metrics = m || ctx.measureText(text); - return metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; - } - - roundRect(ctx, x, y, width, height, radius, fill, stroke) { // Credit to https://stackoverflow.com/users/227299/juan-mendes - if (typeof stroke === 'undefined') { - stroke = true; - } - if (typeof radius === 'undefined') { - radius = 5; - } - if (typeof radius === 'number') { - radius = { tl: radius, tr: radius, br: radius, bl: radius }; - } else { - var defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 }; - for (var side in defaultRadius) { - radius[side] = radius[side] || defaultRadius[side]; + // Set poll end timer + setTimeout(async () => { + if (!this.client.polls.get(message.id)) return; + + await this.update(); + const endMessage = this.client.translate.get(this.lang, "Functions.poll.end"); + + try { + await message.edit({ + embeds: [new Embed() + .setMedia(await this.client.Uploader.upload(poll.canvas.toBuffer(), 'Poll.png')) + .setColor('#F24646')], + content: endMessage + }); + } catch (error) { + console.error('Failed to edit poll message:', error); } - } - ctx.beginPath(); - ctx.moveTo(x + radius.tl, y); - ctx.lineTo(x + width - radius.tr, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr); - ctx.lineTo(x + width, y + height - radius.br); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height); - ctx.lineTo(x + radius.bl, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl); - ctx.lineTo(x, y + radius.tl); - ctx.quadraticCurveTo(x, y, x + radius.tl, y); - ctx.closePath(); - if (fill) { - ctx.fill(); - } - if (stroke) { - ctx.stroke(); - } + + this.client.polls.delete(message.id); + await PollDB.findOneAndDelete({ messageId: message.id }); + }, this.time); } - async update() { - let roundRect = this.roundRect; - let textHeight = this.textHeight; + /** + * Calculate text height for canvas + * @param {string} text - Text to measure + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {TextMetrics} [metrics] - Optional text metrics + * @returns {number} Text height + */ + textHeight(text, ctx, metrics) { + const m = metrics || ctx.measureText(text); + return m.actualBoundingBoxAscent + m.actualBoundingBoxDescent; + } - var width = 600, height = this.size.canvas, padding = 10; - const canvas = Canvas.createCanvas(width, height); + /** + * Draw rounded rectangle on canvas + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {number} width - Rectangle width + * @param {number} height - Rectangle height + * @param {number|Object} radius - Corner radius + * @param {boolean} fill - Whether to fill the rectangle + * @param {boolean} stroke - Whether to stroke the rectangle + */ + roundRect(ctx, x, y, width, height, radius = 5, fill = true, stroke = true) { + const r = typeof radius === 'number' + ? { tl: radius, tr: radius, br: radius, bl: radius } + : { tl: 0, tr: 0, br: 0, bl: 0, ...radius }; + + ctx.beginPath(); + ctx.moveTo(x + r.tl, y); + ctx.lineTo(x + width - r.tr, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + r.tr); + ctx.lineTo(x + width, y + height - r.br); + ctx.quadraticCurveTo(x + width, y + height, x + width - r.br, y + height); + ctx.lineTo(x + r.bl, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - r.bl); + ctx.lineTo(x, y + r.tl); + ctx.quadraticCurveTo(x, y, x + r.tl, y); + ctx.closePath(); + + if (fill) ctx.fill(); + if (stroke) ctx.stroke(); + } + + /** + * Update the poll canvas + */ + async update() { + const { WIDTH, PADDING, BAR_HEIGHT, COLORS, FONT_SIZES } = CANVAS_CONFIG; + const height = this.size.canvas; + + // Create canvas + const canvas = Canvas.createCanvas(WIDTH, height); this.canvas = canvas; const ctx = this.canvas.getContext('2d'); this.ctx = ctx; - let name = this.options.name?.length > 70 ? this.options?.name.slice(0, 67) + "..." : this.options.name; - var nameHeight = textHeight(name, ctx); + // Draw background + ctx.fillStyle = COLORS.BACKGROUND; + this.roundRect(ctx, 0, 0, WIDTH, height, 5, true, false); - let description = this.options.description.length > 80 ? this.options.description.slice(0, 77) + "..." : this.options.description; - var descHeight = textHeight(description, ctx); + // Draw title and description + const name = this.truncateText(this.options.name, 70); + const description = this.truncateText(this.options.description, 80); + + const nameHeight = this.textHeight(name, ctx); + const descHeight = this.textHeight(description, ctx); - ctx.fillStyle = "#23272A"; - roundRect(ctx, 0, 0, width, height, 5, true, false); // background + // Draw title + ctx.fillStyle = COLORS.TEXT_SECONDARY; + ctx.font = `normal ${FONT_SIZES.SMALL} Sans-Serif`; + ctx.fillText(name, PADDING, PADDING + 2 + nameHeight / 2); - ctx.fillStyle = "#4E535A"; - ctx.font = `normal 12px Sans-Serif`; - ctx.fillText(name, padding, padding + 2 + nameHeight / 2); // name + // Draw description + ctx.fillStyle = COLORS.TEXT_PRIMARY; + ctx.font = `normal ${FONT_SIZES.MEDIUM} Sans-Serif`; + ctx.fillText(description, PADDING, PADDING + 15 + nameHeight + descHeight / 2); - ctx.fillStyle = "#FFFFFF"; - ctx.font = `normal 17px Sans-Serif`; - ctx.fillText(description, padding, padding + 15 + nameHeight + descHeight / 2); // description + const headerHeight = PADDING + descHeight + nameHeight + 15; + const dataWidth = WIDTH - PADDING * 2; - var headerHeight = padding + descHeight + nameHeight + 15; - var dataWidth = width - padding * 2; - var barHeight = 40; - var votes = this.votes; - var names = this.voteOptions.name; + // Draw vote bars + this.drawVoteBars(ctx, dataWidth - 20, BAR_HEIGHT, this.votes, + { pad: PADDING, hHeight: headerHeight }, + this.voteOptions.name); - this.drawVoteBars(ctx, dataWidth - 20, barHeight, votes, { pad: padding, hHeight: headerHeight }, names); - await this.drawFooter(ctx, padding, padding + headerHeight + barHeight * 2 + 20, width, height, padding, this.avatars); + // Draw footer + await this.drawFooter(ctx, PADDING, PADDING + headerHeight + BAR_HEIGHT * 2 + 20, + WIDTH, height, PADDING, this.avatars); } + /** + * Truncate text with ellipsis + * @param {string} text - Text to truncate + * @param {number} maxLength - Maximum length + * @returns {string} Truncated text + */ + truncateText(text, maxLength) { + return text?.length > maxLength ? text.slice(0, maxLength - 3) + "..." : text; + } + + /** + * Add a vote to the poll + * @param {number} option - Option index + * @param {string} user - User ID + * @param {string} avatar - User avatar URL + * @param {string} id - Message ID + * @returns {Canvas} Updated canvas + */ async addVote(option, user, avatar, id) { if (this.avatars.length === 6) this.avatars.shift(); this.avatars.push(avatar); this.votes[option]++; - await PollDB.findOneAndUpdate({ messageId: id }, { $push: { users: user, avatars: avatar }, $inc: { [`votes.${option}`]: 1 } }); + + await PollDB.findOneAndUpdate( + { messageId: id }, + { + $push: { users: user, avatars: avatar }, + $inc: { [`votes.${option}`]: 1 } + } + ); + await this.update(); return this.canvas; } + /** + * Remove a vote from the poll + * @param {number} option - Option index + * @param {string} user - User ID + * @param {string} avatar - User avatar URL + * @param {string} id - Message ID + * @returns {Canvas} Updated canvas + */ async removeVote(option, user, avatar, id) { this.avatars.splice(this.avatars.indexOf(avatar), 1); this.votes[option]--; - await PollDB.findOneAndUpdate({ messageId: id }, { $pull: { users: user, avatars: avatar }, $inc: { [`votes.${option}`]: -1 } }); + + await PollDB.findOneAndUpdate( + { messageId: id }, + { + $pull: { users: user, avatars: avatar }, + $inc: { [`votes.${option}`]: -1 } + } + ); + await this.update(); return this.canvas; } + /** + * Draw vote bars on canvas + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {number} width - Bar width + * @param {number} height - Bar height + * @param {number[]} votes - Vote counts + * @param {Object} vars - Position variables + * @param {string[]} names - Option names + * @param {number} [vote] - Selected vote index + */ drawVoteBars(ctx, width, height, votes, vars, names, vote) { - let roundRect = this.roundRect; - let textHeight = this.textHeight; - let padding = vars.pad; - let headerHeight = vars.hHeight; - let sum = votes.reduce((prev, curr) => prev + curr); - let percentages = votes.map((v) => Math.floor(v / (sum / 100) * 10) / 10); + const { PADDING, COLORS } = CANVAS_CONFIG; + const { pad: padding, hHeight: headerHeight } = vars; + + const sum = votes.reduce((prev, curr) => prev + curr, 0); + const percentages = votes.map(v => Math.floor(v / (sum / 100) * 10) / 10); + ctx.save(); ctx.translate(padding, padding + headerHeight); - var barPadding = 5; + const barPadding = 5; percentages.forEach((percentage, i) => { - if (!percentage) percentage = 0; - let paddingLeft = (vote != undefined) ? 30 : 0; + const y = (height + 10) * i; + const paddingLeft = vote !== undefined ? 30 : 0; - ctx.fillStyle = "#2C2F33"; - let y = (height + 10) * i; - roundRect(ctx, 20, y, width, height, 5, true, false); // full bar + // Draw bar background + ctx.fillStyle = COLORS.BAR_BACKGROUND; + this.roundRect(ctx, 20, y, width, height, 5, true, false); - if (vote == i || percentage) { ctx.fillStyle = "#A52F05"; } - else { ctx.fillStyle = "#24282B"; } // percentage display - roundRect(ctx, 20, y, width * (votes[i] / (sum / 100) / 100), height, 5, true, false); + // Draw bar fill + ctx.fillStyle = (vote === i || percentage) ? COLORS.BAR_FILL : COLORS.BAR_INACTIVE; + this.roundRect(ctx, 20, y, width * (votes[i] / (sum / 100) / 100), height, 5, true, false); - ctx.fillStyle = "#4E535A"; - let h = textHeight(i + 1, ctx); - ctx.fillText(i + 1, 0, y + height / 2 + h / 2); + // Draw option number + ctx.fillStyle = COLORS.TEXT_SECONDARY; + const numHeight = this.textHeight(i + 1, ctx); + ctx.fillText(i + 1, 0, y + height / 2 + numHeight / 2); - ctx.fillStyle = "#FFFFFF"; // Option names - h = textHeight(names[i], ctx); - ctx.fillText(names[i].length > 65 ? names[i].slice(0, 62) + "..." : names[i], 30 + paddingLeft, y + 13 + h); + // Draw option name + ctx.fillStyle = COLORS.TEXT_PRIMARY; + const name = this.truncateText(names[i], 65); + const nameHeight = this.textHeight(name, ctx); + ctx.fillText(name, 30 + paddingLeft, y + 13 + nameHeight); - if (vote != undefined) { - ctx.strokeStyle = "#FFFFFF"; // selection circle - ctx.fillStyle = "#717cf4"; + // Draw selection circle if voting + if (vote !== undefined) { + ctx.strokeStyle = COLORS.TEXT_PRIMARY; + ctx.fillStyle = COLORS.SELECTION; ctx.beginPath(); - ctx.arc(35, y + 10 + h * 0.75, 6, 0, 2 * Math.PI); + ctx.arc(35, y + 10 + nameHeight * 0.75, 6, 0, 2 * Math.PI); ctx.closePath(); ctx.stroke(); - if (vote == i) { + + if (vote === i) { ctx.beginPath(); - ctx.arc(35, y + 10 + h * 0.75, 3, 0, 2 * Math.PI); + ctx.arc(35, y + 10 + nameHeight * 0.75, 3, 0, 2 * Math.PI); ctx.closePath(); ctx.fill(); } } - ctx.fillStyle = "#2C2F33"; // percentage and vote count background - let metrics = ctx.measureText(percentage + "% (" + votes[i] + ")"); - let w = metrics.width; - h = textHeight(percentage + "% (" + votes[i] + ")", ctx, metrics); - y = y + (height - h - barPadding * 2) + barPadding * 2.6; - if (vote == i || vote == undefined) roundRect(ctx, width - barPadding - w - 3, y - h - 4, w + 5, h + 12, 5, true, false); + // Draw percentage and vote count + const voteText = `${percentage}% (${votes[i]})`; + const metrics = ctx.measureText(voteText); + const textHeight = this.textHeight(voteText, ctx, metrics); + const textY = y + (height - textHeight - barPadding * 2) + barPadding * 2.6; - ctx.fillStyle = "#A52F05"; // percentage and vote count - ctx.fillText(percentage + "% (" + votes[i] + ")", width - barPadding - w, y); + if (vote === i || vote === undefined) { + ctx.fillStyle = COLORS.BAR_BACKGROUND; + this.roundRect(ctx, width - barPadding - metrics.width - 3, + textY - textHeight - 4, metrics.width + 5, textHeight + 12, 5, true, false); + } + + ctx.fillStyle = COLORS.BAR_FILL; + ctx.fillText(voteText, width - barPadding - metrics.width, textY); }); + ctx.restore(); } + /** + * Draw footer on canvas + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {number} width - Canvas width + * @param {number} height - Canvas height + * @param {number} padding - Padding value + * @param {string[]} users - Array of user avatar URLs + */ async drawFooter(ctx, x, y, width, height, padding, users) { + const { COLORS } = CANVAS_CONFIG; ctx.save(); ctx.translate(10, this.size.bar); - var rad = 18; - ctx.fillStyle = "#4E535A"; + const rad = 18; + ctx.fillStyle = COLORS.TEXT_SECONDARY; ctx.lineWidth = 2; - ctx.strokeStyle = "#4E535A"; + ctx.strokeStyle = COLORS.TEXT_SECONDARY; + + // Draw separator line ctx.beginPath(); ctx.moveTo(0 - padding, 0); ctx.lineTo(width, 0); ctx.stroke(); - let votes = (this.votes.reduce((p, c) => p + c) == 1) ? `${this.votes.reduce((p, c) => p + c)} vote` : `${this.votes.reduce((p, c) => p + c)} votes`; - let metrics = ctx.measureText(votes); - let h = this.textHeight(votes, ctx, metrics); - ctx.fillText(votes, 5, rad + h); + // Draw vote count + const totalVotes = this.votes.reduce((p, c) => p + c, 0); + const voteText = totalVotes === 1 ? '1 vote' : `${totalVotes} votes`; + const metrics = ctx.measureText(voteText); + const textHeight = this.textHeight(voteText, ctx, metrics); + ctx.fillText(voteText, 5, rad + textHeight); - // Avatars - var pos = rad * users.length + 10 + metrics.width; - var yPos = 6; + // Draw avatars + let pos = rad * users.length + 10 + metrics.width; + const yPos = 6; users.reverse(); - for (let i = 0; i < users.length; i++) { - ctx.beginPath(); - let user = users[i]; - const a = Canvas.createCanvas(rad * 2, rad * 2); - const context = a.getContext("2d"); + for (const avatar of users) { + const avatarCanvas = Canvas.createCanvas(rad * 2, rad * 2); + const avatarCtx = avatarCanvas.getContext('2d'); - context.beginPath(); - context.arc(rad, rad, rad, 0, Math.PI * 2, true); - context.closePath(); - context.clip(); + avatarCtx.beginPath(); + avatarCtx.arc(rad, rad, rad, 0, Math.PI * 2, true); + avatarCtx.closePath(); + avatarCtx.clip(); - const avatar = await Canvas.loadImage(user); - context.drawImage(avatar, 0, 0, rad * 2, rad * 2); - ctx.drawImage(a, pos, yPos); + try { + const avatarImage = await Canvas.loadImage(avatar); + avatarCtx.drawImage(avatarImage, 0, 0, rad * 2, rad * 2); + ctx.drawImage(avatarCanvas, pos, yPos); + } catch (error) { + console.error('Failed to load avatar:', error); + } - ctx.closePath(); pos -= rad; } - // Date - let date = format; - metrics = ctx.measureText(date); - h = this.textHeight(date, ctx, metrics); - ctx.fillText(date, width - 15 - metrics.width, rad + h); + // Draw date + const date = getFormattedDate(); + const dateMetrics = ctx.measureText(date); + const dateHeight = this.textHeight(date, ctx, dateMetrics); + ctx.fillText(date, width - 15 - dateMetrics.width, rad + dateHeight); + ctx.restore(); } } diff --git a/functions/randomStr.js b/functions/randomStr.js index 32bf9ed..8d58b9d 100644 --- a/functions/randomStr.js +++ b/functions/randomStr.js @@ -1,5 +1,4 @@ const crypt = require("crypto"); -// const FlakeId = require('flakeid'); module.exports = { /** diff --git a/functions/reload.js b/functions/reload.js index 339423d..66e2ce7 100644 --- a/functions/reload.js +++ b/functions/reload.js @@ -1,48 +1,109 @@ +/** + * Reloads a module (event, function, or command) in the client + * @param {Object} client - The Discord client instance + * @param {string} category - The category of the module ('events', 'functions', or command name) + * @param {string} [name] - The name of the module (required for events and functions) + * @returns {string} Status message indicating success or failure + */ function Reload(client, category, name) { - if (category === "events") { - if (!name) return 'Provide an event name to reload!' - try { - const evtName = name; - delete require.cache[require.resolve(`../events/${name}.js`)]; - const pull = require(`../events/${name}`); + // Input validation + if (!client) throw new Error('Client instance is required'); + if (!category) return 'Provide a category/command name to reload!'; - client.off(evtName, typeof client._events[evtName] == 'function' ? client._events[evtName] : client._events[evtName][0]) - client.event.delete(evtName) + const reloaders = { + events: () => reloadEvent(client, name), + functions: () => reloadFunction(client, name), + default: () => reloadCommand(client, category) + }; - client.on(evtName, pull.bind(null, client)) - client.event.set(evtName, pull.bind(null, client)) - } catch (e) { - return `Couldn't reload: **${category}/${name}**\n**Error**: ${e.message}` - } - return `Reloaded event: **${name}**.js` - } - - if (category === "functions") { - if (!name) return 'Provide a function name to reload!' - try { - const evtName = name; - delete require.cache[require.resolve(`../functions/${name}.js`)]; - const pull = require(`../functions/${name}`); - - client.functions.delete(evtName) - client.functions.set(evtName, pull) - } catch (e) { - return `Couldn't reload: **functions/${name}**\n**Error**: ${e.message}` - } - return `Reloaded function: **${name}**.js` - } + return (reloaders[category] || reloaders.default)(); +} +/** + * Reloads an event module + * @param {Object} client - The Discord client instance + * @param {string} name - The name of the event + * @returns {string} Status message + */ +function reloadEvent(client, name) { + if (!name) return 'Provide an event name to reload!'; + try { - if (!category) return 'Provide a command name to reload!' - delete require.cache[require.resolve(`../commands/${category}.js`)]; - const pull = require(`../commands/${category}.js`); - if (client.commands.get(category).config.aliases) client.commands.get(category).config.aliases.forEach(a => client.aliases.delete(a)); - client.commands.delete(category); - client.commands.set(category, pull); - if (client.commands.get(category).config.aliases) client.commands.get(category).config.aliases.forEach(a => client.aliases.set(a, category)); - return `Reloaded command: **commands/${category}**.js` - } catch (e) { - return `Couldn't reload: **commands/${category}**\n**Error**: ${e.message}` + const eventPath = `../events/${name}.js`; + delete require.cache[require.resolve(eventPath)]; + const eventModule = require(eventPath); + + // Remove existing event listener + const existingHandler = client._events[name]; + if (existingHandler) { + client.off(name, typeof existingHandler === 'function' ? existingHandler : existingHandler[0]); + } + client.event.delete(name); + + // Add new event listener + const boundHandler = eventModule.bind(null, client); + client.on(name, boundHandler); + client.event.set(name, boundHandler); + + return `Reloaded event: **${name}**.js`; + } catch (error) { + return `Couldn't reload: **events/${name}**\n**Error**: ${error.message}`; + } +} + +/** + * Reloads a function module + * @param {Object} client - The Discord client instance + * @param {string} name - The name of the function + * @returns {string} Status message + */ +function reloadFunction(client, name) { + if (!name) return 'Provide a function name to reload!'; + + try { + const functionPath = `../functions/${name}.js`; + delete require.cache[require.resolve(functionPath)]; + const functionModule = require(functionPath); + + client.functions.delete(name); + client.functions.set(name, functionModule); + + return `Reloaded function: **${name}**.js`; + } catch (error) { + return `Couldn't reload: **functions/${name}**\n**Error**: ${error.message}`; + } +} + +/** + * Reloads a command module + * @param {Object} client - The Discord client instance + * @param {string} commandName - The name of the command + * @returns {string} Status message + */ +function reloadCommand(client, commandName) { + try { + const commandPath = `../commands/${commandName}.js`; + delete require.cache[require.resolve(commandPath)]; + const commandModule = require(commandPath); + + // Handle aliases + const existingCommand = client.commands.get(commandName); + if (existingCommand?.config?.aliases) { + existingCommand.config.aliases.forEach(alias => client.aliases.delete(alias)); + } + + // Update command + client.commands.delete(commandName); + client.commands.set(commandName, commandModule); + + // Update aliases if they exist + if (commandModule.config?.aliases) { + commandModule.config.aliases.forEach(alias => client.aliases.set(alias, commandName)); + } + + return `Reloaded command: **commands/${commandName}**.js`; + } catch (error) { + return `Couldn't reload: **commands/${commandName}**\n**Error**: ${error.message}`; } }