diff --git a/.eslintignore b/.eslintignore index fc708fec..488401c8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -30,10 +30,7 @@ commands/lockdown.js commands/manualVerify.js commands/manualVetVerify.js commands/memes.js -commands/modmail.js commands/modmailBlacklist.js -commands/modmailClose.js -commands/modmailRespond.js commands/mute.js commands/mutes.js commands/noNicknames.js diff --git a/botSetup.js b/botSetup.js index 1174cdfa..0a522708 100644 --- a/botSetup.js +++ b/botSetup.js @@ -127,10 +127,9 @@ async function setup(bot) { // initialize components (eg. modmail, verification) iterServers(bot, (bot, g) => { - vibotChannels.update(g, bot).catch(er => { }); const db = dbSetup.getDB(g.id); + vibotChannels.update(g, bot, db).catch(er => { }); afkCheck.loadBotAfkChecks(g, bot, db); - // if (bot.settings[g.id].backend.modmail) modmail.init(g, bot, db).catch(er => { ErrorLogger.log(er, bot, g); }) if (bot.settings[g.id].backend.verification) verification.init(g, bot, db).catch(er => { ErrorLogger.log(er, bot, g); }); if (bot.settings[g.id].backend.vetverification) vetVerification.init(g, bot, db).catch(er => { ErrorLogger.log(er, bot, g); }); }); diff --git a/commands/modmail.js b/commands/modmail.js deleted file mode 100644 index e4a6691a..00000000 --- a/commands/modmail.js +++ /dev/null @@ -1,335 +0,0 @@ -const Discord = require('discord.js'); -const ErrorLogger = require('../lib/logError') -const { init } = require('./vetVerification'); -const moment = require('moment') -var watchedModMails = [] -const axios = require('axios') -const modmailGPTurl = require('../settings.json').modmailGPTurl - -module.exports = { - name: 'modmail', - description: 'Mod Mail Handler', - role: 'moderator', - args: '', - interactionHandler, - async execute(message, args, bot, db) { - if (args.length > 0) { - switch (args[0].toLowerCase()) { - case 'update': - this.update(message.guild, bot, db) - break; - case 'sendinfo': - this.sendInfo(message) - break; - } - } - }, - async sendModMail(message, guild, bot, db) { - let settings = bot.settings[guild.id] - if (await checkBlacklist(message.author, db)) return await message.author.send('You have been blacklisted from modmailing.') - if (!settings.backend.modmail) return - message.react('📧') - message.channel.send('Message has been sent to mod-mail. If this was a mistake, don\'t worry') - let embed = new Discord.EmbedBuilder() - .setColor('#ff0000') - .setAuthor({ name: message.author.tag, iconURL: message.author.avatarURL() }) - .setDescription(`<@!${message.author.id}> **sent the bot**\n${message.content}`) - .setFooter({ text: `User ID: ${message.author.id} MSG ID: ${message.id}` }) - .setTimestamp() - modmailCloseComponents = new Discord.ActionRowBuilder() - .addComponents([ - new Discord.ButtonBuilder() - .setLabel('🔓 Unlock') - .setStyle(3) - .setCustomId('modmailUnlock') - ]) - let modMailChannel = guild.channels.cache.get(settings.channels.modmail) - let embedMessage = await modMailChannel.send({ embeds: [embed], components: [modmailCloseComponents] }).catch(er => ErrorLogger.log(er, bot, message.guild)) - - modmailInteractionCollector = new Discord.InteractionCollector(bot, { message: embedMessage, interactionType: Discord.InteractionType.MessageComponent, componentType: Discord.ComponentType.Button }) - modmailInteractionCollector.on('collect', (interaction) => interactionHandler(interaction, settings, bot, db)) - if (message.attachments.first()) modMailChannel.send(message.attachments.first().proxyURL) - }, - async init(guild, bot, db) { - guild.channels.cache.get(bot.settings[guild.id].channels.modmail).messages.fetch({ limit: 100 }) - }, -} - -async function interactionHandler(interaction, settings, bot, db) { - if (!interaction.isButton()) return; - if (!settings.backend.modmail) { - interaction.reply(`Modmail is disabled in this server.`) - return - } - - failedEmbed = new Discord.EmbedBuilder() - .setFooter({ text: `Status: ${interaction.customId} MSG ID: ${interaction.message.id}` }) - .setDescription(`Could not figure out what went wrong`) - .setColor('#FF0000') - - confirmationEmbed = new Discord.EmbedBuilder() - .setTitle(`Confirm Action`) - .setDescription(`Are you sure you wanna perform this action?\n`) - .setFooter({ text: `${interaction.customId}` }) - .setColor('#FF0000') - - modmailOpenComponents = new Discord.ActionRowBuilder() - if (settings.modmail.lockModmail) { - modmailOpenComponents.addComponents([ - new Discord.ButtonBuilder() - .setLabel('🔒 Lock') - .setStyle(1) - .setCustomId('modmailLock') - ]) - } - if (settings.modmail.sendMessage) { - modmailOpenComponents.addComponents([ - new Discord.ButtonBuilder() - .setLabel('✉️ Send Message') - .setStyle(3) - .setCustomId('modmailSend') - ]) - } - if (settings.modmail.modmailGPT || true) { // remove the true thing - modmailOpenComponents.addComponents([ - new Discord.ButtonBuilder() - .setLabel('🤖 Generate Response') - .setStyle(2) - .setCustomId('modmailGPT') - ]) - } - if (settings.modmail.forwardMessage) { - modmailOpenComponents.addComponents([ - new Discord.ButtonBuilder() - .setLabel('↪️ Forward ModMail') - .setStyle(2) - .setCustomId('modmailForward') - ]) - } - if (settings.modmail.blacklistUser) { - modmailOpenComponents.addComponents([ - new Discord.ButtonBuilder() - .setLabel('🔨 Blacklist User') - .setStyle(2) - .setCustomId('modmailBlacklist') - ]) - } - if (settings.modmail.closeModmail) { - modmailOpenComponents.addComponents([ - new Discord.ButtonBuilder() - .setLabel('❌ Close ModMail') - .setStyle(4) - .setCustomId('modmailClose') - ]) - } - - // Split row if open components is > 5 - if (modmailOpenComponents.components.length > 5) { - // Split the components into two rows - const splitComponents = modmailOpenComponents.components.reduce((acc, component, index) => { - if (index < 5) { - acc[0].push(component); - } else { - acc[1].push(component); - } - return acc; - }, [[], []]); - - // Create the two rows - modmailOpenComponents = [ - new Discord.ActionRowBuilder().addComponents(splitComponents[0]), - new Discord.ActionRowBuilder().addComponents(splitComponents[1]) - ]; - } else modmailOpenComponents = [modmailOpenComponents] - - let embed = new Discord.EmbedBuilder() - embed.data = interaction.message.embeds[0].data - - let modmailMessage = interaction.message - let guild = interaction.guild - let modmailChannel = guild.channels.cache.get(settings.channels.modmail) - let modmailMessageID = modmailMessage.embeds[0].data.footer.text.split(/ +/g)[5] - let raider = guild.members.cache.get(modmailMessage.embeds[0].data.footer.text.split(/ +/g)[2]) - if (!raider) { - embed.addFields({ name: `This modmail has been closed automatically `, value: `The raider in this modmail is no longer in this server.\nI can no longer proceed with this modmail`, inline: false }) - interaction.update({ embeds: [embed], components: [] }) - return - } - let directMessages = await raider.user.createDM() - - function checkInServer() { - const result = guild.members.cache.get(directMessages.recipient.id); - if (!result) { failedEmbed.setDescription(`${raider} Has left this server and I can no longer continue with this modmail`); interaction.reply({ embeds: [failedEmbed] }); interaction.update({ components: [] }) } - return result; - } - let userModMailMessage = await directMessages.messages.fetch(modmailMessageID) - let security = interaction.member - - if (interaction.customId === "modmailUnlock") { - await interaction.update({ embed: [interaction.message.embed], components: [...modmailOpenComponents] }) - } else if (interaction.customId === "modmailLock") { - modmailCloseComponents = new Discord.ActionRowBuilder() - .addComponents([ - new Discord.ButtonBuilder() - .setLabel('🔓 Unlock') - .setStyle(3) - .setCustomId('modmailUnlock') - ]) - await interaction.update({ embed: [interaction.message.embed], components: [modmailCloseComponents] }) - } else if (interaction.customId === "modmailSend") { - let originalModmail = embed.data.description; - let embedResponse = new Discord.EmbedBuilder() - .setDescription(`__How would you like to respond to ${raider}'s [message](${modmailMessage.url})__\n${originalModmail}`) - let tempResponseMessage = await interaction.reply({ embeds: [embedResponse] }); - - let responseMessageCollector = new Discord.MessageCollector(modmailChannel, { filter: messageToBeSent => messageToBeSent.author.id === interaction.member.id }) - responseMessageCollector.on('collect', async function (message) { - let responseMessage = message.content.trim() - if (responseMessage == '') return await interaction.editReply({ content: 'Invalid response. Please provide text. If you attached an image, please copy the URL and send that', ephemeral: true }) - responseMessageCollector.stop() - await message.delete() - if (!checkInServer) { - await tempResponseMessage.delete() - failedEmbed.setDescription(`${raider} Has left this server and I can no longer continue with this modmail`) - await interaction.reply({ embeds: [failedEmbed] }); - await interaction.update({ components: [] }) - return - } - embedResponse.setDescription(`__Are you sure you want to respond with the following?__\n${responseMessage}`) - await tempResponseMessage.edit({ embeds: [embedResponse] }).then(async confirmMessage => { - if (await confirmMessage.confirmButton(interaction.member.id)) { - if (!checkInServer) { - await tempResponseMessage.delete() - failedEmbed.setDescription(`${raider} Has left this server and I can no longer continue with this modmail`) - await interaction.editReply({ embeds: [failedEmbed] }); - await interaction.update({ components: [] }) - return - } - await directMessages.send(responseMessage) - await tempResponseMessage.delete() - embed.addFields([{ name: `Response by ${interaction.member.nickname} :`, value: responseMessage }]) - await interaction.update({ embeds: [embed], components: [] }) - } else { - await tempResponseMessage.delete() - await interaction.update({ components: [...modmailOpenComponents] }) - } - }) - }) - } else if (interaction.customId === "modmailForward") { - let forwardedMessageChannel = interaction.guild.channels.cache.get(settings.channels.forwardedModmailMessage) - confirmationEmbed.setDescription(`This will forward this modmail over to ${forwardedMessageChannel}`) - await modmailChannel.send({ embeds: [confirmationEmbed] }).then(async confirmMessage => { - if (await confirmMessage.confirmButton(interaction.member.id)) { - await confirmMessage.delete() - if (forwardedMessageChannel) { - let forwardedMessageEmbed = new Discord.EmbedBuilder() - .setColor('#ff0000') - .setDescription(interaction.message.embeds[0].data.description) - let forwardedMessage = await forwardedMessageChannel.send({ embeds: [forwardedMessageEmbed] }) - if (settings.backend.forwadedMessageThumbsUpAndDownReactions) { - await forwardedMessage.react('👍') - await forwardedMessage.react('👎') - } - embed.addFields([{ name: `${interaction.member.nickname} has forwarded this modmail `, value: `This modmail has been forwarded to ${forwardedMessageChannel}` }]) - - await interaction.update({ embeds: [embed], components: [] }) - } else if (forwardedMessageChannel == undefined) { - embed = new Discord.EmbedBuilder() - .setDescription(`${interaction.member} This feature has not been set up.\nIf you would like for this to be set up, then do the following\nContact any Mod+\nHave them do \`\`;setup\`\`\nAnd enable \`\`forwardedModmailMessage\`\` under \`\`channels\`\``) - .setColor('#FF0000') - .setFooter({ text: `${interaction.customId}` }) - await interaction.reply({ embeds: [embed] }) - } - } else { await interaction.update({ components: [...modmailOpenComponents] }); await confirmMessage.delete(); } - }) - } else if (interaction.customId === "modmailClose") { - confirmationEmbed.setDescription(`This will close the modmail permanently.\nIf you wish to send a message after closing, use the \`\`;mmr\`\` command to send a message to this modmail`) - await modmailChannel.send({ embeds: [confirmationEmbed] }).then(async confirmMessage => { - if (await confirmMessage.confirmButton(interaction.member.id)) { - await confirmMessage.delete() - embed.addFields([{ name: `${interaction.member.nickname} has closed this modmail `, value: `This modmail has been closed` }]) - await interaction.update({ embeds: [embed], components: [] }) - } else { await interaction.update({ components: [...modmailOpenComponents] }); await confirmMessage.delete(); } - }) - } else if (interaction.customId === "modmailBlacklist") { - confirmationEmbed.setDescription(`This will blacklist ${raider} and they can no longer send in any future modmails`) - await modmailChannel.send({ embeds: [confirmationEmbed] }).then(async confirmMessage => { - if (await confirmMessage.confirmButton(interaction.member.id)) { - await confirmMessage.delete() - db.query(`INSERT INTO modmailblacklist (id) VALUES ('${raider.id}')`) - embed.addFields([{ name: `${interaction.member.nickname} has blacklisted ${raider.nickname} `, value: `${raider} has been blacklisted by ${interaction.member}` }]) - await interaction.update({ embeds: [embed], components: [] }) - } else { await interaction.update({ components: [...modmailOpenComponents] }); await confirmMessage.delete(); } - }) - } else if (interaction.customId === "modmailGPT") { - // Get the original modmail - let originalModmail = embed.data.description.replace(/<@!\d+?>/g, '').replace(' **sent the bot**\n', '').replace('\t', ''); - - // Respond to interaction - let resp = await interaction.deferReply() - - // Send modmail to flask API - axios.post(modmailGPTurl, { modmail: originalModmail }) - .then(async function (response) { - // Get the generated text from the Flask API - let generatedText = response.data.response; // Assuming Flask responds with a key named "response" - - // Ask for User Confirmation - let approvalEmbed = new Discord.EmbedBuilder() - .setTitle("Generated Response") - .setDescription(`Generated text: ${generatedText}`); - - - let tempResponseMessage = await resp.edit({ embeds: [approvalEmbed] }); - - if (await tempResponseMessage.confirmButton(interaction.member.id)) { - // Check if the user is still in the server - if (!checkInServer) { - await tempResponseMessage.delete(); - failedEmbed.setDescription(`${raider} Has left this server and I can no longer continue with this modmail`); - await interaction.reply({ embeds: [failedEmbed] }); - await interaction.update({ components: [] }); - return; - } - - await directMessages.send(generatedText); - await tempResponseMessage.delete(); - - embed.addFields([{ name: `Generated Response Approved by ${interaction.member.nickname} :`, value: generatedText }]); - await interaction.update({ embeds: [embed], components: [] }); - - } else { - // User rejected the generated response - await tempResponseMessage.delete(); - await interaction.update({ components: [...modmailOpenComponents] }); - } - }) - .catch(function (error) { - // Handle any errors from the Flask API - console.log("Error from Flask API: ", error); - }); - } - else { - embed = new Discord.EmbedBuilder() - .setDescription(`${interaction.member} Something went wrong when trying to handle your interaction\nPlease try again or contact any Upper Staff to get this sorted out.\nThank you for your patience!`) - .setColor('#FF0000') - .setFooter({ text: `${interaction.customId}` }) - await interaction.reply({ embeds: [embed] }) - } -} - -async function checkBlacklist(member, db) { - return new Promise(async (res, rej) => { - db.query(`SELECT * FROM modmailblacklist WHERE id = '${member.id}'`, (err, rows) => { - if (err) return rej(err) - if (rows.length == 0) { - res(false) - } else { - res(true) - } - }) - }) -} - -const keyFilter = (r, u) => !u.bot && r.emoji.name === '🔑' -const choiceFilter = (r, u) => !u.bot && (r.emoji.name === '📧' || r.emoji.name === '👀' || r.emoji.name === '🗑️' || r.emoji.name === '❌' || r.emoji.name === '🔨' || r.emoji.name === '🔒' /*temp, remove later*/ || r.emoji.id === '752368122551337061') \ No newline at end of file diff --git a/commands/modmailClose.js b/commands/modmailClose.js deleted file mode 100644 index ecdd728c..00000000 --- a/commands/modmailClose.js +++ /dev/null @@ -1,31 +0,0 @@ -const Discord = require('discord.js') -const moment = require('moment') - -module.exports = { - name: 'modmailclose', - description: 'Closes a modmail using the message id of the modmail embed', - alias: ['mmc'], - role: 'security', - args: '', - async execute(message, args, bot) { - let settings = bot.settings[message.guild.id] - if (!settings.backend.modmail) return message.reply(`Modmail is disabled in this server.`) - // check if the command is being used in the modmail channel - if (message.channel.id !== settings.channels.modmail) return message.reply (`Must be used in modmail channel.`) - if (!args[0]) return message.reply (`There are no arguments being provided.`) - let m = await message.channel.messages.fetch(args[0]) - if (!m) return message.channel.send(`Could not find message with ID of \`${args[0]}\``) - let components = m.components - let embeds = m.embeds - // check if the message with the given id is a modmail embed - if (!Array.isArray(embeds) || !embeds.length) return message.reply (`Did not recognize as a modmail.`) - // check if the modmail isn't already closed/responded to - if (!Array.isArray(components) || !components.length) return message.reply (`This Modmail is already closed.`) - // close the modmail - let embed = new Discord.EmbedBuilder() - embed.data = embeds[0].data; - embed.addFields([{ name: `${message.member.displayName} has closed this modmail `, value: `This modmail has been closed` }]) - await m.edit({ embeds: [embed], components: [] }) - message.delete() - } -} \ No newline at end of file diff --git a/commands/modmailRespond.js b/commands/modmailRespond.js index 9ce7de31..978b75f7 100644 --- a/commands/modmailRespond.js +++ b/commands/modmailRespond.js @@ -1,68 +1,50 @@ -const Discord = require('discord.js') -const moment = require('moment') +const { EmbedBuilder, Colors } = require('discord.js'); +const { Modmail } = require('../lib/modmail.js'); module.exports = { name: 'modmailrespond', alias: ['mmr'], role: 'security', args: '', + requiredArgs: 1, + /** + * @param {import('discord.js').Message} message + * @param {string[]} args + * @param {import('discord.js').Client} bot + * @param {import('mysql2').Pool} db + */ async execute(message, args, bot, db) { - let settings = bot.settings[message.guild.id] - if (!settings.backend.modmail) { - messsage.reply(`Modmail is disabled in this server.`) - return - } - if (message.channel.id !== settings.channels.modmail) return - if (!args[0]) return - let m = await message.channel.messages.fetch(args[0]) - if (!m) return message.channel.send(`Could not find message with ID of \`${args[0]}\``) - let embed = new Discord.EmbedBuilder() - embed.data = m.embeds[0].data; - let raider; - if (!raider) - try { - raider = message.guild.members.cache.get(embed.data.footer.text.split(/ +/g)[2]); - if (!raider) - raider = await message.guild.members.fetch({ user: embed.data.footer.text.split(/ +/g)[2], force: true }); - } catch (e) { return message.channel.send(`User is not currently in the server.`); } - if (!raider) - return message.channel.send(`User is not currently in the server.`); - let dms = await raider.user.createDM() + const settings = bot.settings[message.guild.id]; - function checkInServer() { - const result = message.guild.members.cache.get(dms.recipient.id); - if (!result) - message.channel.send(`User ${dms.recipient} is no longer in the server.`); - return result; - } + const embed = new EmbedBuilder() + .setTitle('Modmail Respond') + .setAuthor({ name: message.member.displayName, iconURL: message.member.displayAvatarURL() }) + .setColor(Colors.Red) + .setTimestamp(); - let originalMessage = embed.data.description; - // originalMessage = originalMessage.substring(originalMessage.indexOf(':') + 3, originalMessage.length - 1) - let responseEmbed = new Discord.EmbedBuilder() - .setDescription(`__How would you like to respond to ${raider}'s [message](${m.url})__\n${originalMessage}`) - let responseEmbedMessage = await message.channel.send({ embeds: [responseEmbed] }) - let responseCollector = new Discord.MessageCollector(message.channel,{filter: m => m.author.id === message.author.id}) - responseCollector.on('collect', async function (mes) { - let response = mes.content.trim() - if (response == '') return mes.channel.send(`Invalid response. Please provide text. If you attached an image, please copy the URL and send that`) - responseCollector.stop() - await mes.delete() - if (!checkInServer()) - return responseEmbedMessage.delete(); - responseEmbed.setDescription(`__Are you sure you want to respond with the following?__\n${response}`) - await responseEmbedMessage.edit({ embeds: [responseEmbed] }).then(async confirmMessage => { - if (await confirmMessage.confirmButton(message.author.id)) { - if (!checkInServer()) - return responseEmbedMessage.delete(); - await dms.send(response) - responseEmbedMessage.delete() - embed.addFields([{ name: `Response by ${message.member.displayName} :`, value: response }]) - m.edit({ embeds: [embed] }) - } else { - await responseEmbedMessage.delete() - } - }) - }) - message.delete() + if (!settings.backend.modmail) return await message.reply({ embeds: [embed.setDescription('Modmail is disabled in this server.')] }); + if (message.channel.id !== settings.channels.modmail) return await message.reply({ embeds: [embed.setDescription('This is not the modmail channel.')] }); + /** @type {Discord.Message} */ + const modmailMessage = await message.channel.messages.fetch(args[0]); + if (!modmailMessage) return await message.reply({ embeds: [embed.setDescription(`Could not find message with ID of \`${args[0]}\``)] }); + const modmailEmbed = EmbedBuilder.from(modmailMessage.embeds[0]); + + const raiderId = modmailEmbed.data.footer.text.split(/ +/g)[2]; + const raider = await message.guild.members.fetch({ user: raiderId, force: true }); + if (!raider) return await message.reply({ embeds: [embed.setDescription(`User <@!${raiderId}> is no longer in the server.`)] }); + + const dms = await raider.user.createDM(); + if (!dms) return await message.reply({ embeds: [embed.setDescription(`Cannot send messages to ${raider}.`)] }); + + message.message = message; + const dummyInteraction = { + message: modmailMessage, + member: message.member, + guild: message.guild, + channel: message.channel, + reply: message.reply.bind(message) + }; + await Modmail.send({ settings, interaction: dummyInteraction, embed: modmailEmbed, raider, db, bot }); + message.delete(); } -} \ No newline at end of file +}; diff --git a/commands/vibotChannels.js b/commands/vibotChannels.js index a7c8b0f4..33f2058c 100644 --- a/commands/vibotChannels.js +++ b/commands/vibotChannels.js @@ -3,7 +3,7 @@ const fs = require('fs') const botSettings = require('../settings.json') const ErrorLogger = require('../lib/logError') const vibotChannel = require('./vibotChannels.js') -const modmail = require('./modmail.js') +const modmail = require('../lib/modmail.js') const roleassignment = require('./roleAssignment.js') var watchedMessages = [] var watchedButtons = {}; //the keys for this are the id of a VC @@ -25,26 +25,7 @@ module.exports = { }, async update(guild, bot, db) { let settings = bot.settings[guild.id] - await updateModmailListeners(guild.channels.cache.get(settings.channels.modmail), settings, bot, db) await updateRoleAssignmentListeners(guild.channels.cache.get(settings.channels.roleassignment), settings, bot, db) - async function updateModmailListeners(modmailChannel, settings, bot, db) { - if (!modmailChannel) { return } // If there is no modmail channel it will not continue - let modmailChannelMessages = await modmailChannel.messages.fetch() // This fetches all the messages in the modmail channel - modmailChannelMessages.each(async modmailMessage => { // This will loop through the modmail channel messages - if (modmailMessage.author.id !== bot.user.id) return; // If the modmail message author is not the same id as ViBot it will not continue with this message - if (modmailMessage.embeds.length == 0) return; // If the message has no embeds it will not continue - let embed = new Discord.EmbedBuilder() // This creates a empty embed, able to be edited later - embed.data = modmailMessage.embeds[0].data // This will change the empty embed to have the modmailMessage embed data - - /* We have a message -> check if it has no components - **EXPLANATION** When the modmail is done, its not supposed to have any components. - If it has any components at all, we will revert them to the basic "unlock" modmail - */ - if (modmailMessage.components == 0) { return } - // Anything below this code inside this function is for open modmails, and we need to reset them - module.exports.addModmailUnlockButton(modmailMessage, settings, bot, db) // This will add a modmail "unlock" button to the modmailMessage - }) - } async function updateRoleAssignmentListeners(roleassignmentChannel, settings, bot, db) { if (!settings.backend.roleassignment) return; if (!roleassignmentChannel) { return } // If there is no roleassignment channel it will not continue @@ -146,8 +127,6 @@ module.exports = { .setCustomId('modmailUnlock')) message = await message.edit({ components: [components] }) } - modmailInteractionCollector = new Discord.InteractionCollector(bot, { message: message, interactionType: Discord.InteractionType.MessageComponent, componentType: Discord.ComponentType.Button }) - modmailInteractionCollector.on('collect', (interaction) => modmail.interactionHandler(interaction, settings, bot, db)) }, async addCloseChannelButtons(bot, m, rsaMessage) { diff --git a/index.js b/index.js index 7ddcefc4..449ea197 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,7 @@ const dbSetup = require('./dbSetup.js'); const memberHandler = require('./memberHandler.js'); const { logWrapper } = require('./metrics.js'); const { handleReactionRow } = require('./redis.js'); - +const Modmail = require('./lib/modmail.js'); // Specific Commands const verification = require('./commands/verification'); @@ -51,7 +51,14 @@ bot.on('interactionCreate', logWrapper('message', async (logger, interaction) => // Validate the interaction is a command if (interaction.isChatInputCommand()) return await messageManager.handleCommand(interaction, true); if (interaction.isUserContextMenuCommand()) return await messageManager.handleCommand(interaction, true); - if (interaction.isButton()) return await handleReactionRow(bot, interaction); + if (interaction.isButton()) { + if (interaction.customId.startsWith('modmail')) { + const settings = bot.settings[interaction.guild.id]; + const db = dbSetup.getDB(interaction.guild.id); + return await Modmail.interactionHandler(interaction, settings, bot, db); + } + return await handleReactionRow(bot, interaction); + } })); bot.on('ready', async () => { diff --git a/lib/extensions.js b/lib/extensions.js index 9bc4f20d..af72a4aa 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -51,38 +51,33 @@ Promise.wait = Promise.wait || function Wait(time) { * @param {string?} requirementMessage Message to provide when filter fails * @param {Discord.Snowflake?} author_id Id of the user to watch for. If not provided, will accept any message that isn't from a bot user. */ -Discord.BaseChannel.prototype.next = function Next(filter, requirementMessage, author_id) { +Discord.TextChannel.prototype.next = function Next(filter, requirementMessage, author_id) { return new Promise((resolve, reject) => { const collector = this.createMessageCollector({ filter: (message) => !message.author.bot && (author_id ? message.author.id == author_id : true), time: MAX_WAIT }); let resolved = false; let error; collector.on('collect', async (message) => { resolved = true; + if (message.deletable) await message.delete(); + if (error?.deletable) error.then(err => err.delete()); + if (message.content.toLowerCase() === 'cancel') { collector.stop(); - reject('Manually cancelled.'); + message.error = 'manually cancelled'; + reject(message); return; } - if (error) - error.then(err => err.delete()); - - let result = message; - if (message.deletable) - result = await message.delete(); - if (filter && !filter(result)) { + if (filter && !filter(message)) { resolved = false; - error = message.channel.send(`${result.content} is not a valid input.\r\n${requirementMessage}\r\nType cancel to cancel.`) + error = message.channel.send(`${message.content} is not a valid input.\r\n${requirementMessage}\r\nType cancel to cancel.`) return; } collector.stop(); - resolve(result); + resolve(message); }) collector.on('end', () => { - const err = 'timed out'; - err.stack = new Error().stack; - if (!resolved) - reject(err) + if (!resolved) reject({ error: 'timed out' }) }); }); }; diff --git a/lib/logCommand.js b/lib/logCommand.js index 951faf0b..3d29c551 100644 --- a/lib/logCommand.js +++ b/lib/logCommand.js @@ -1,21 +1,21 @@ const Discord = require('discord.js') const loggingInfo = require('../data/loggingInfo.json'); - +const { developerId } = require('../settings.json') module.exports = { async log(message, bot) { if (!bot) return let guildHub = bot.guilds.cache.get(loggingInfo.info.guildid); - let vi = bot.users.fetch(loggingInfo.info.vi) + const developer = await bot.users.fetch(developerId) if (!guildHub) { console.log("ViBot Info not found. ``logCommand.js``") - await vi.send("ViBot Info not found. ``logCommand.js``") + await developer?.send("ViBot Info not found. ``logCommand.js``") return } let channel = guildHub.channels.cache.get(loggingInfo[message.guild.id].channelCommand) if (!channel) channel = guildHub.channels.cache.get(loggingInfo.info.channelCommand) if (!channel) { console.log("ViBot Info Channel not found. ``logCommand.js``") - await vi.send("ViBot Info Channel not found. ``logCommand.js``") + await developer?.send("ViBot Info Channel not found. ``logCommand.js``") return } let embed = new Discord.EmbedBuilder() @@ -33,17 +33,17 @@ module.exports = { async logInteractionCommand(interaction, bot) { if (!bot) return let guildHub = bot.guilds.cache.get(loggingInfo.info.guildid); - let vi = bot.users.fetch(loggingInfo.info.vi) + let developer = await bot.users.fetch(developerId) if (!guildHub) { console.log("ViBot Info not found. ``logCommand.js``") - await vi.send("ViBot Info not found. ``logCommand.js``") + await developer?.send("ViBot Info not found. ``logCommand.js``") return } let channel = guildHub.channels.cache.get(loggingInfo[interaction.guild.id].channelCommand) if (!channel) channel = guildHub.channels.cache.get(loggingInfo.info.channelCommand) if (!channel) { console.log("ViBot Info Channel not found. ``logCommand.js``") - await vi.send("ViBot Info Channel not found. ``logCommand.js``") + await developer?.send("ViBot Info Channel not found. ``logCommand.js``") return } let embed = new Discord.EmbedBuilder() diff --git a/lib/modmail.js b/lib/modmail.js new file mode 100644 index 00000000..5d1f8771 --- /dev/null +++ b/lib/modmail.js @@ -0,0 +1,306 @@ +const ErrorLogger = require('./logError'); +const moment = require('moment'); +const axios = require('axios'); +const { modmailGPTurl } = require('../settings.json'); +const { EmbedBuilder, Colors, ButtonBuilder, ActionRowBuilder, Collection } = require('discord.js'); +/** @typedef {import('../data/guildSettings.701483950559985705.cache.json')} Settings */ +/** + * @typedef ModmailData + * @property {import('discord.js').ButtonInteraction} interaction + * @property {Settings} settings + * @property {EmbedBuilder} embed + * @property {import('discord.js').GuildMember} raider + * @property {import('mysql2').Pool} db + * @property {import('discord.js').Client} bot + */ +async function performModmailReply(guild, attachments, content, raider, messageId) { + const atmtInfo = attachments.map(a => `[${a.name}](${a.proxyURL})`).join('\n'); + const userEmbed = new EmbedBuilder() + .setTitle('Modmail Response') + .setColor(Colors.Red) + .setAuthor({ name: guild.name, iconURL: guild.iconURL() }); + + if (attachments.size == 1 && attachments.first().contentType?.toLowerCase().startsWith('image')) { + userEmbed.setImage(attachments.first().proxyURL); + } else if (attachments.size >= 1) { + userEmbed.addFields({ name: 'Attachments', value: atmtInfo }); + userEmbed.setDescription('See attachments'); + } + + if (content.trim()) userEmbed.setDescription(content.trim()); + + const directMessages = await raider.user.createDM(); + const userModmailMessage = await directMessages.messages.fetch(messageId); + + if (userModmailMessage) await userModmailMessage.reply({ embeds: [userEmbed] }); + else await directMessages?.send({ embeds: [userEmbed] }); +} + +const Modmail = { + /** @param {ModmailData} options */ + async unlock({ settings, interaction }) { + await interaction.update({ components: getOpenModmailComponents(settings) }); + }, + + /** @param {ModmailData} options */ + async lock({ interaction }) { + await interaction.update({ components: getCloseModmailComponents() }); + }, + + /** @param {ModmailData} options */ + async send({ settings, interaction, interaction: { guild, channel, message, member }, embed, raider }) { + const confirmEmbed = new EmbedBuilder() + .setDescription(`__How would you like to respond to ${raider}'s [message](${message.url})__\n${embed.data.description}`) + .setFooter({ text: 'Type \'cancel\' to cancel' }) + .setColor(Colors.Blue); + + const confirmResponse = await interaction.reply({ embeds: [confirmEmbed], fetchReply: true }); + + /** @type {import('discord.js').Message} */ + const { attachments, content, error } = await channel.next(null, null, member.id).catch(issue => issue); + + if (error) { + await confirmResponse.delete(); + return await message.edit({ components: getOpenModmailComponents(settings) }); + } + + delete confirmEmbed.data.footer; + + const atmtInfo = attachments.map(a => `[${a.name}](${a.proxyURL})`).join('\n'); + confirmEmbed.setDescription(`__Are you sure you want to respond with the following?__\n${content.trim()}`); + + if (attachments.size == 1 && attachments.first().contentType?.toLowerCase().startsWith('image')) { + confirmEmbed.setImage(attachments.first().proxyURL); + } else if (attachments.size >= 1) { + confirmEmbed.addFields({ name: 'Attachments', value: atmtInfo }); + } + await confirmResponse.edit({ embeds: [confirmEmbed] }); + + const performReply = await confirmResponse.confirmButton(member.id); + confirmResponse.delete(); + if (!performReply) return await message.edit({ components: getOpenModmailComponents(settings) }); + + await performModmailReply(guild, attachments, content, raider, embed.data.footer.text.split(/ +/g)[5]); + const respInfo = content.trim() + (attachments.size ? `\n**Attachments:**\n${atmtInfo}` : ''); + + embed.addFields({ name: `Response by ${member.displayName} :`, value: respInfo }); + await message.edit({ embeds: [embed], components: [] }); + }, + + /** @param {ModmailData} options */ + async forward({ settings, interaction, interaction: { guild, message, member }, embed, raider }) { + const forwardChannel = guild.channels.cache.get(settings.channels.forwardedModmailMessage); + + const confirmationEmbed = new EmbedBuilder() + .setTitle('Modmail Forward') + .setColor(Colors.Blue); + + if (!forwardChannel) { + confirmationEmbed.setDescription('There is no modmail forward channel configured for this server.') + .setColor(Colors.Red) + .setFooter({ text: interaction.customId }); + + await interaction.reply({ embeds: [confirmationEmbed] }); + await message.edit({ components: getOpenModmailComponents(settings) }); + return; + } + confirmationEmbed.setDescription(`__Are you sure you want to forward ${raider}'s [message](${message.url}) to ${forwardChannel}?__\n${embed.data.description}`); + const confirmMessage = await interaction.reply({ embeds: [confirmationEmbed], fetchReply: true }); + const result = await confirmMessage.confirmButton(member.id); + confirmMessage.delete(); + + if (!result) return await message.edit({ components: getOpenModmailComponents(settings) }); + + const forwardEmbed = new EmbedBuilder() + .setColor(Colors.Red) + .setDescription(message.embeds[0].data.description) + .setFooter({ text: `Forwarded by ${member.displayName} • Modmail send at` }) + .setTimestamp(new Date(message.embeds[0].data.timestamp)); + const forwardMessage = await forwardChannel.send({ embeds: [forwardEmbed] }); + if (settings.backend.forwadedMessageThumbsUpAndDownReactions) { + await forwardMessage.react('👍'); + await forwardMessage.react('👎'); + } + embed.addFields({ name: `${member.displayName} forwarded this modmail `, value: `This modmail has been forwarded to ${forwardChannel}` }); + await message.edit({ embeds: [embed], components: [] }); + }, + + /** @param {ModmailData} options */ + async close({ settings, interaction, interaction: { message, member }, embed }) { + const confirmEmbed = new EmbedBuilder() + .setTitle('Modmail Close') + .setDescription('This will close the modmail permanently.\nIf you wish to send a message after closing, use the `;mmr` command to send a message to this modmail') + .setColor(Colors.Blue); + + const confirmMessage = await interaction.reply({ embeds: [confirmEmbed], fetchReply: true }); + const result = await confirmMessage.confirmButton(member.id); + await confirmMessage.delete(); + if (!result) return await message.edit({ components: getOpenModmailComponents(settings) }); + + embed.addFields({ name: `${member.displayName} has closed this modmail `, value: 'This modmail has been closed' }); + await message.edit({ embeds: [embed], components: [] }); + }, + + /** @param {ModmailData} options */ + async blacklist({ settings, db, interaction, interaction: { message, member }, raider, embed }) { + const confirmEmbed = new EmbedBuilder() + .setTitle('Modmail Blacklist') + .setDescription(`This will blacklist ${raider}. They will no longer be able to send any modmails. Are you sure you want to do this?`) + .setColor(Colors.Blue); + + const confirmMessage = await interaction.reply({ embeds: [confirmEmbed], fetchReply: true }); + const result = await confirmMessage.confirmButton(member.id); + await confirmMessage.delete(); + if (!result) return await message.edit({ components: getOpenModmailComponents(settings) }); + + await db.promise().query('INSERT INTO modmailblacklist (id) VALUES (?)', [raider.id]); + embed.addFields({ name: `${member.displayName} has blacklisted ${raider.nickname} `, value: `${raider} has been blacklisted by ${member}` }); + await message.edit({ embeds: [embed], components: [] }); + }, + + /** @param {ModmailData} options */ + async gpt({ settings, interaction, interaction: { guild, message, member }, embed, raider }) { + const originalModmail = embed.data.description.replace(/<@!\d+?>/g, '').replace(' **sent the bot**\n', '').replace('\t', ''); + + const reply = await interaction.deferReply(); + const { response: { data: { response } } } = await axios.post(modmailGPTurl, { modmail: originalModmail }); + + const confirmEmbed = new EmbedBuilder() + .setTitle('Modmail GPT Generated Response') + .setDescription(`Generated text: ${response}`) + .setColor(Colors.Blue); + + const confirmMessage = await reply.edit({ embeds: [confirmEmbed] }); + const result = await confirmMessage.confirmButton(member.id); + await confirmMessage.delete(); + + if (!result) return await message.edit({ components: getOpenModmailComponents(settings) }); + + await performModmailReply(guild, new Collection(), response, raider, embed.data.footer.text.split(/ +/g)[5]); + + embed.addFields({ name: `Generated Response Approved by ${member.displayName} :`, value: response }); + await message.edit({ embeds: [embed], components: [] }); + } +}; + +function getCloseModmailComponents() { + return [new ActionRowBuilder().addComponents(new ButtonBuilder().setLabel('🔓 Unlock').setStyle(3).setCustomId('modmailUnlock'))]; +} +function getOpenModmailComponents(settings) { + const components = []; + if (settings.modmail.lockModmail) { + components.push(new ButtonBuilder().setLabel('🔒 Lock').setStyle(1).setCustomId('modmailLock')); + } + if (settings.modmail.sendMessage) { + components.push(new ButtonBuilder().setLabel('✉️ Send Message').setStyle(3).setCustomId('modmailSend')); + } + if (settings.modmail.modmailGPT) { + components.push(new ButtonBuilder().setLabel('🤖 Generate Response').setStyle(2).setCustomId('modmailGPT')); + } + if (settings.modmail.forwardMessage) { + components.push(new ButtonBuilder().setLabel('↪️ Forward ModMail').setStyle(2).setCustomId('modmailForward')); + } + if (settings.modmail.blacklistUser) { + components.push(new ButtonBuilder().setLabel('🔨 Blacklist User').setStyle(2).setCustomId('modmailBlacklist')); + } + if (settings.modmail.closeModmail) { + components.push(new ButtonBuilder().setLabel('❌ Close ModMail').setStyle(4).setCustomId('modmailClose')); + } + return components.reduce((rows, btn, idx) => { + if (idx % 5 == 0) rows.push(new ActionRowBuilder()); + rows[rows.length - 1].addComponents(btn); + return rows; + }, []); +} + +module.exports = { + /** + * @param {import('discord.js').ButtonInteraction} interaction + * @param {Settings} settings + * @param {import('discord.js').Client} bot + * @param {import('mysql2').Pool} db + */ + async interactionHandler(interaction, settings, bot, db) { + if (!interaction.isButton()) return; + if (!settings.backend.modmail) return interaction.reply('Modmail is disabled in this server.'); + + const embed = EmbedBuilder.from(interaction.message.embeds[0]); + const raider = interaction.guild.members.cache.get(embed.data.footer.text.split(/ +/g)[2]); + + if (!raider) { + embed.addFields({ name: `This modmail has been closed automatically `, value: 'The raider in this modmail is no longer in this server.\nI can no longer proceed with this modmail', inline: false }); + interaction.update({ embeds: [embed], components: [] }); + return; + } + + /** @type {ModmailData} */ + const modmailData = { interaction, settings, embed, raider, db, bot }; + + switch (interaction.customId) { + case 'modmailUnlock': await Modmail.unlock(modmailData); break; + case 'modmailLock': await Modmail.lock(modmailData); break; + case 'modmailSend': await Modmail.send(modmailData); break; + case 'modmailForward': await Modmail.forward(modmailData); break; + case 'modmailClose': await Modmail.close(modmailData); break; + case 'modmailBlacklist': await Modmail.blacklist(modmailData); break; + case 'modmailGPT': await Modmail.gpt(modmailData); break; + default: { + const failEmbed = new EmbedBuilder() + .setTitle('Modmail Interaction Failure') + .setDescription(`${interaction.member} Something went wrong when trying to handle your interaction\nPlease try again or contact any Upper Staff to get this sorted out.\nThank you for your patience!`) + .setColor(Colors.Red) + .setFooter({ text: `${interaction.customId}` }); + await interaction.reply({ embeds: [failEmbed] }); + } + } + }, + + /** + * @param {import('discord.js').Message} message + * @param {import('discord.js').Guild} guild + * @param {import('discord.js').Client} bot + * @param {import('mysql2').Pool} db + */ + async sendModMail(message, guild, bot, db) { + const settings = bot.settings[guild.id]; + /** @type {import('discord.js').GuildTextBasedChannel} */ + const modmailChannel = guild.channels.cache.get(settings.channels.modmail); + + const embed = new EmbedBuilder() + .setColor(Colors.Red) + .setAuthor({ name: guild.name, iconURL: guild.iconURL() }) + .setTimestamp(); + + if (!settings.backend.modmail || !modmailChannel) { + embed.setDescription(`Modmail through ${bot.user} is currently disabled.`); + return await message.reply({ embeds: [embed] }); + } + + const [rows] = await db.promise().query('SELECT * FROM modmailblacklist WHERE id = ?', [message.author.id]); + if (rows.length) { + embed.setDescription(`You are currently blacklisted from sending modmails through ${bot.user}.`); + return await message.reply({ embeds: [embed] }); + } + + message.react('📧'); + embed.setDescription(`Message has been sent to \`${guild.name}\` mod-mail. If this was a mistake, don't worry.`); + message.reply({ embeds: [embed] }); + + const modmailEmbed = new EmbedBuilder() + .setColor(Colors.Red) + .setAuthor({ name: message.author.tag, iconURL: message.author.avatarURL() }) + .setDescription(`<@!${message.author.id}> **sent the bot**\n${message.content}`) + .setFooter({ text: `User ID: ${message.author.id} MSG ID: ${message.id}` }) + .setTimestamp(); + + if (message.attachments.size) modmailEmbed.addFields({ name: 'Attachments', value: `This modmail was sent with ${message.attachments.size} attachments, listed below.` }); + + const modmailMessage = await modmailChannel.send({ + embeds: [modmailEmbed], + components: getCloseModmailComponents() + }).catch(er => ErrorLogger.log(er, bot, message.guild)); + + if (message.attachments.size) await modmailMessage.reply({ files: message.attachments.map(atmt => atmt) }); + }, + Modmail +}; diff --git a/messageManager.js b/messageManager.js index 4465dee0..9104d943 100644 --- a/messageManager.js +++ b/messageManager.js @@ -8,7 +8,7 @@ const { logMessage, genPoint, writePoint } = require('./metrics.js') const restarting = require('./commands/restart') const verification = require('./commands/verification') const stats = require('./commands/stats') -const modmail = require('./commands/modmail') +const modmail = require('./lib/modmail.js') const { argString } = require('./commands/commands.js'); const { getDB } = require('./dbSetup.js') const { LegacyCommandOptions, LegacyParserError } = require('./utils.js') diff --git a/package.json b/package.json index 1e2b7180..448283d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vibot", - "version": "8.13.4", + "version": "8.14.0", "description": "ViBot", "main": "index.js", "dependencies": {