diff --git a/modules/message-quotes/configs/config.json b/modules/message-quotes/configs/config.json new file mode 100644 index 0000000..03fa6b0 --- /dev/null +++ b/modules/message-quotes/configs/config.json @@ -0,0 +1,119 @@ +{ + "description": "Configure the message quoting system", + "humanName": "Configuration", + "filename": "config.json", + "content": [ + { + "name": "roles", + "humanName": "Blacklist roles", + "description": "Roles that are excluded from quoting", + "default": [], + "type": "array", + "content": "roleID" + }, + { + "name": "channels", + "humanName": "Blacklist channels", + "description": "Channels that are excluded from quoting (Channels and categories are supported; a category excludes all its channels)", + "default": [], + "type": "array", + "content": "channelID" + }, + { + "name": "withAttachments", + "humanName": "Attach files?", + "default": false, + "description": "Should all attachments be quoted? (Deactivation recommended if \"Message\" components v2 are used)", + "type": "boolean" + }, + { + "name": "noBots", + "humanName": "Ignore bot messages?", + "default": true, + "description": "Bot messages are not included in the quote when activated", + "type": "boolean" + }, + { + "name": "selfQuote", + "humanName": "Allow Self-quotes?", + "default": true, + "description": "Can users quote their own messages?", + "type": "boolean" + }, + { + "name": "asReply", + "humanName": "Reply to messages?", + "default": true, + "description": "Reply to the message that triggered the quote (Ignored when \"Delete trigger\" is enabled)", + "type": "boolean" + }, + { + "name": "deleteOrigin", + "humanName": "Delete trigger?", + "default": false, + "description": "When enabled, the trigger message will be deleted", + "type": "boolean" + }, + { + "name": "message", + "humanName": "Message", + "description": "Message in which the quote is returned", + "default": { + "title": "Quote from #%channelName%", + "url": "%link%", + "description": ">>> %content%", + "image": "%image%", + "color": "#2ECC71", + "author": { + "name": "%userName%", + "img": "%userAvatar%" + } + }, + "type": "string", + "allowEmbed": true, + "allowGeneratedImage": true, + "params": [ + { + "name": "%userID%", + "description": "Id of the user" + }, + { + "name": "%userName%", + "description": "Username of the user" + }, + { + "name": "%displayName%", + "description": "Displays the user's nickname" + }, + { + "name": "%userAvatar%", + "description": "Avatar of the user" + }, + { + "name": "%channelID%", + "description": "Id of the channel from which the quote originates" + }, + { + "name": "%channelName%", + "description": "Name of the channel from which the quote originates" + }, + { + "name": "%timestamp%", + "description": "Shows when the original message was sent (Used discord timestamp)" + }, + { + "name": "%link%", + "description": "Message-link of the original message" + }, + { + "name": "%image%", + "description": "First image of the message, if available" + }, + { + "name": "%content%", + "description": "Message content of the quote" + } + ] + } + ] +} diff --git a/modules/message-quotes/events/messageCreate.js b/modules/message-quotes/events/messageCreate.js new file mode 100644 index 0000000..445307b --- /dev/null +++ b/modules/message-quotes/events/messageCreate.js @@ -0,0 +1,135 @@ +const { + embedType, + embedTypeV2, + formatDiscordUserName, + archiveDiscordAttachment +} = require('../../../src/functions/helpers'); +const cooldowns = new Map(); + +module.exports.run = async (client, msg) => { + if (!client.botReadyAt) return; + if (!msg.content || msg.author.bot || msg.system) return; + if (!msg.guild || !msg.member) return; + if (msg.guild.id !== client.guildID) return; + + const now = Date.now(); + const cooldownAmount = 5 * 1000; + if (cooldowns.has(msg.author.id)) { + const expirationTime = cooldowns.get(msg.author.id) + cooldownAmount; + if (now < expirationTime) return; + } + + const moduleConfig = client.configurations['message-quotes']['config'] || {}; + + const blacklistedChannels = moduleConfig.channels || []; + const blacklistedRoles = moduleConfig.roles || []; + + if (blacklistedChannels.includes(msg.channel.id) || + blacklistedChannels.includes(msg.channel.parentId) || + (msg.channel.parent?.parentId && blacklistedChannels.includes(msg.channel.parent.parentId))) { + return; + }; + if (msg.member.roles.cache.some(r => blacklistedRoles.some(br => String(br) === r.id))) return; + + const discordLinkRegex = /https:\/\/discord\.com\/channels\/(\d+)\/(\d+)\/(\d+)/i; + const match = msg.content.match(discordLinkRegex); + if (!match) return; + + cooldowns.set(msg.author.id, now); + + const [_, guildId, channelId, messageId] = match; + if (guildId !== msg.guild.id) return; + + try { + const targetChannel = await msg.guild.channels.fetch(channelId).catch(() => null); + if (!targetChannel || !targetChannel.isTextBased()) return; + + const userPerms = targetChannel.permissionsFor(msg.member); + if (!userPerms || !userPerms.has('ViewChannel') || !userPerms.has('ReadMessageHistory')) return; + + const botPerms = targetChannel.permissionsFor(msg.guild.members.me); + if (!botPerms || !botPerms.has('ViewChannel') || !botPerms.has('ReadMessageHistory')) return; + + const targetMsg = await targetChannel.messages.fetch(messageId).catch(() => null); + if (!targetMsg) return; + + if (moduleConfig.noBots === true && targetMsg.author.bot) return; + if (moduleConfig.selfQuote === false && targetMsg.author.id === msg.author.id) return; + + let files = []; + const withAttachments = moduleConfig.withAttachments; + if (withAttachments && targetMsg.attachments.size > 0) { + let count = 0; + for (const [_, att] of targetMsg.attachments) { + if (count >= 3) break; + if (att.size > 8 * 1024 * 1024) continue; + + files.push({ + attachment: att.url, + name: att.name ?? 'attachment' + }); + count++; + } + } + + let finalImage = ''; + const firstAttachment = targetMsg.attachments.first(); + if (firstAttachment) { + finalImage = await archiveDiscordAttachment(client, firstAttachment.url, { + displayName: `Quote by ${formatDiscordUserName(targetMsg.author)} in #${targetChannel.name}`.slice(0, 100), + tags: ['message-quotes'], + uploaderDiscordID: targetMsg.author.id + }); + } else { + const imgMatch = targetMsg.content.match(/https?:\/\/\S+\.(?:png|jpe?g|gif|webp)/i); + if (imgMatch) finalImage = imgMatch[0]; + } + + const userAvatar = targetMsg.author.displayAvatarURL(); + const unixSeconds = Math.floor(targetMsg.createdTimestamp / 1000); + const displayContent = targetMsg.content || + (targetMsg.attachments.size > 0 ? '*[Attachment]*' : '') || + (targetMsg.stickers?.size > 0 ? '*[Sticker]*' : '*[None]*'); + + const quoteMsg = await embedTypeV2(moduleConfig.message, { + '%userID%': targetMsg.author.id, + '%userName%': formatDiscordUserName(targetMsg.author), + '%displayName%': targetMsg.member?.displayName || targetMsg.author.username, + '%userAvatar%': userAvatar, + '%channelID%': targetChannel.id, + '%channelName%': targetChannel.name, + '%link%': match[0], + '%image%': finalImage, + '%timestamp%': ``, + '%content%': displayContent + }); + + let finalFiles = quoteMsg.files && Array.isArray(quoteMsg.files) ? [...quoteMsg.files] : []; + if (files.length > 0) { + finalFiles = finalFiles.concat(files); + } + + const sendOptions = { + ...quoteMsg, + files: finalFiles.length > 0 ? finalFiles : undefined, + allowedMentions: { parse: [], repliedUser: false } + }; + + if (moduleConfig.asReply === true && moduleConfig.deleteOrigin !== true) { + await msg.reply(sendOptions); + } else { + await msg.channel.send(sendOptions); + } + + if (moduleConfig.deleteOrigin === true) { + const currentChannelPerms = msg.channel.permissionsFor(msg.guild.members.me); + if (currentChannelPerms && currentChannelPerms.has('ManageMessages')) { + await msg.delete().catch(() => null); + } else { + client.logger.warn(`[Message-Quotes] Messages cannot deleted, missing Permission: ManageMessages`); + } + } + } catch(error) { + client.logger.error('[Message-Quotes]' + error); + } +}; diff --git a/modules/message-quotes/module.json b/modules/message-quotes/module.json new file mode 100644 index 0000000..077445f --- /dev/null +++ b/modules/message-quotes/module.json @@ -0,0 +1,18 @@ +{ + "name": "message-quotes", + "humanReadableName": "Message quotes", + "description": "Quotes a Discord message when a user pastes a message link.", + "fa-icon": "fas fa-quote-left", + "author": { + "name": "Jean S.", + "link": "https://github.com/JeanCoding16" + }, + "openSourceURL": "https://github.com/ScootKit/CustomDCBot/tree/main/modules/message-quotes", + "tags": [ + "community" + ], + "events-dir": "/events", + "config-example-files": [ + "configs/config.json" + ] +}