From eaceea0aee3dc383a82ea715d3cedcccf670da1e Mon Sep 17 00:00:00 2001 From: JeanCoding16 Date: Thu, 4 Jun 2026 09:07:50 +0200 Subject: [PATCH 1/3] New module: Message-Qoutes --- modules/message-quotes/configs/config.json | 108 ++++++++++++++++++ .../message-quotes/events/messageCreate.js | 87 ++++++++++++++ modules/message-quotes/module.json | 18 +++ 3 files changed, 213 insertions(+) create mode 100644 modules/message-quotes/configs/config.json create mode 100644 modules/message-quotes/events/messageCreate.js create mode 100644 modules/message-quotes/module.json diff --git a/modules/message-quotes/configs/config.json b/modules/message-quotes/configs/config.json new file mode 100644 index 0000000..614b379 --- /dev/null +++ b/modules/message-quotes/configs/config.json @@ -0,0 +1,108 @@ +{ + "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 worked)", + "default": [], + "type": "array", + "content": "channelID" + }, + { + "name": "withAttachments", + "humanName": "Attach files?", + "default": false, + "description": "Should all attachments be quoted? (Must be disabled for components v2!)", + "type": "boolean" + }, + { + "name": "noBots", + "humanName": "Ignore bot messages?", + "default": true, + "description": "Bot messages are not included in the quote when activated", + "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": "Od of the user" + }, + { + "name": "%userName%", + "description": "Username of the user" + }, + { + "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..027711b --- /dev/null +++ b/modules/message-quotes/events/messageCreate.js @@ -0,0 +1,87 @@ +const { localize } = require('../../../src/functions/localize'); +const { embedType, embedTypeV2 } = require('../../../src/functions/helpers'); + +module.exports.run = async (client, msg) => { + if (!client.botReadyAt) return; + if (!msg.guild || !msg.member) return; + if (msg.author.bot || msg.system) return; + if (msg.guild.id !== client.guildID) return; + + const moduleConfig = client.configurations['message-quotes']['config']; + + if (moduleConfig.channels.includes(msg.channel.id) || moduleConfig.channels.includes(msg.channel.parentId) || moduleConfig.channels.includes(msg.channel.parent?.parentId)) return; + if (msg.member.roles.cache.some(r => moduleConfig.roles.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; + + 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 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; + + let files = []; + const withAttachments = moduleConfig.withAttachments; + if (withAttachments && targetMsg.attachments.size > 0) { + targetMsg.attachments.forEach(att => { + files.push({ + attachment: att.url, + name: att.name + }); + }); + } + + const firstAttachment = targetMsg.attachments.first()?.url || ''; + const userAvatar = targetMsg.author.displayAvatarURL({ dynamic: true }); + const unixSeconds = Math.floor(targetMsg.createdTimestamp / 1000); + const quoteMsg = await embedTypeV2(moduleConfig.message, { + '%userID%': targetMsg.author.id, + '%userName%': targetMsg.author.tag, + '%userAvatar%': userAvatar, + '%channelID%': targetChannel.id, + '%channelName%': targetChannel.name, + '%link%': match[0], + '%image%': firstAttachment, + '%timestamp%': ``, + '%content%': targetMsg.content || '' + }); + + 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 + }; + + if (moduleConfig.asReply === true && moduleConfig.deleteOrigin !== true) { + sendOptions.allowedMentions = { repliedUser: false }; + 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); + } + } + } 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..19a15c6 --- /dev/null +++ b/modules/message-quotes/module.json @@ -0,0 +1,18 @@ +{ + "name": "message-quotes", + "humanReadableName": "Message quotes", + "description": "", + "fa-icon": "fas fa-chat", + "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" + ] +} From d8c73518deb107e3411b0300472b6cff4f3f79e6 Mon Sep 17 00:00:00 2001 From: JeanCoding16 Date: Tue, 9 Jun 2026 09:12:42 +0200 Subject: [PATCH 2/3] All changes made from the preview and stabilized with fallback values --- modules/message-quotes/configs/config.json | 13 +++- .../message-quotes/events/messageCreate.js | 75 ++++++++++++++----- modules/message-quotes/module.json | 4 +- 3 files changed, 69 insertions(+), 23 deletions(-) diff --git a/modules/message-quotes/configs/config.json b/modules/message-quotes/configs/config.json index 614b379..05b39aa 100644 --- a/modules/message-quotes/configs/config.json +++ b/modules/message-quotes/configs/config.json @@ -14,7 +14,7 @@ { "name": "channels", "humanName": "Blacklist channels", - "description": "Channels that are excluded from quoting (Channels and categories worked)", + "description": "Channels that are excluded from quoting (Channels and categories are supported; a category excludes all its channels)", "default": [], "type": "array", "content": "channelID" @@ -23,7 +23,7 @@ "name": "withAttachments", "humanName": "Attach files?", "default": false, - "description": "Should all attachments be quoted? (Must be disabled for components v2!)", + "description": "Should all attachments be quoted? (Deactivation recommended if \"Message\" components v2 are used)", "type": "boolean" }, { @@ -33,6 +33,13 @@ "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?", @@ -68,7 +75,7 @@ "params": [ { "name": "%userID%", - "description": "Od of the user" + "description": "Id of the user" }, { "name": "%userName%", diff --git a/modules/message-quotes/events/messageCreate.js b/modules/message-quotes/events/messageCreate.js index 027711b..3d61ad8 100644 --- a/modules/message-quotes/events/messageCreate.js +++ b/modules/message-quotes/events/messageCreate.js @@ -1,16 +1,35 @@ -const { localize } = require('../../../src/functions/localize'); -const { embedType, embedTypeV2 } = require('../../../src/functions/helpers'); +const { + embedType, + embedTypeV2, + formatDiscordUserName +} = 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.author.bot || msg.system) return; if (msg.guild.id !== client.guildID) return; - const moduleConfig = client.configurations['message-quotes']['config']; + 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; + } + cooldowns.set(msg.author.id, now); - if (moduleConfig.channels.includes(msg.channel.id) || moduleConfig.channels.includes(msg.channel.parentId) || moduleConfig.channels.includes(msg.channel.parent?.parentId)) return; - if (msg.member.roles.cache.some(r => moduleConfig.roles.some(br => String(br) === r.id))) 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); @@ -23,6 +42,9 @@ module.exports.run = async (client, msg) => { 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; @@ -30,46 +52,61 @@ module.exports.run = async (client, msg) => { 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) { - targetMsg.attachments.forEach(att => { + 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 + name: att.name ?? 'attachment' }); - }); + count++; + } } - const firstAttachment = targetMsg.attachments.first()?.url || ''; - const userAvatar = targetMsg.author.displayAvatarURL({ dynamic: true }); + const firstAttachment = targetMsg.attachments.first(); + let finalImage = ''; + if (firstAttachment) { + finalImage = firstAttachment.url; + } + + 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%': targetMsg.author.tag, + '%userName%': formatDiscordUserName(targetMsg.author), + '%displayName%': targetMsg.member?.displayName || targetMsg.author.username, '%userAvatar%': userAvatar, '%channelID%': targetChannel.id, '%channelName%': targetChannel.name, '%link%': match[0], - '%image%': firstAttachment, + '%image%': finalImage, '%timestamp%': ``, - '%content%': targetMsg.content || '' + '%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 + files: finalFiles.length > 0 ? finalFiles : undefined, + allowedMentions: { parse: [], repliedUser: false } }; if (moduleConfig.asReply === true && moduleConfig.deleteOrigin !== true) { - sendOptions.allowedMentions = { repliedUser: false }; await msg.reply(sendOptions); } else { await msg.channel.send(sendOptions); @@ -79,9 +116,11 @@ module.exports.run = async (client, msg) => { 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 index 19a15c6..488b6d7 100644 --- a/modules/message-quotes/module.json +++ b/modules/message-quotes/module.json @@ -1,8 +1,8 @@ { "name": "message-quotes", "humanReadableName": "Message quotes", - "description": "", - "fa-icon": "fas fa-chat", + "description": "Quotes a Discord message when a user has pastes a message link.", + "fa-icon": "fas fa-quote-left", "author": { "name": "Jean S.", "link": "https://github.com/JeanCoding16" From 0f0e503f9789e8e5bae9d34a985284734409f84c Mon Sep 17 00:00:00 2001 From: JeanCoding16 Date: Tue, 9 Jun 2026 14:57:07 +0200 Subject: [PATCH 3/3] cooldowns.set() moved, archiveDiscordAttachment() implemented, typo fixed and parameter %displayName% added to configuration --- modules/message-quotes/configs/config.json | 4 ++++ .../message-quotes/events/messageCreate.js | 19 ++++++++++++++----- modules/message-quotes/module.json | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/modules/message-quotes/configs/config.json b/modules/message-quotes/configs/config.json index 05b39aa..03fa6b0 100644 --- a/modules/message-quotes/configs/config.json +++ b/modules/message-quotes/configs/config.json @@ -81,6 +81,10 @@ "name": "%userName%", "description": "Username of the user" }, + { + "name": "%displayName%", + "description": "Displays the user's nickname" + }, { "name": "%userAvatar%", "description": "Avatar of the user" diff --git a/modules/message-quotes/events/messageCreate.js b/modules/message-quotes/events/messageCreate.js index 3d61ad8..445307b 100644 --- a/modules/message-quotes/events/messageCreate.js +++ b/modules/message-quotes/events/messageCreate.js @@ -1,7 +1,8 @@ const { embedType, embedTypeV2, - formatDiscordUserName + formatDiscordUserName, + archiveDiscordAttachment } = require('../../../src/functions/helpers'); const cooldowns = new Map(); @@ -10,14 +11,13 @@ module.exports.run = async (client, msg) => { 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; } - cooldowns.set(msg.author.id, now); const moduleConfig = client.configurations['message-quotes']['config'] || {}; @@ -35,6 +35,8 @@ module.exports.run = async (client, msg) => { 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; @@ -70,10 +72,17 @@ module.exports.run = async (client, msg) => { } } - const firstAttachment = targetMsg.attachments.first(); let finalImage = ''; + const firstAttachment = targetMsg.attachments.first(); if (firstAttachment) { - finalImage = firstAttachment.url; + 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(); diff --git a/modules/message-quotes/module.json b/modules/message-quotes/module.json index 488b6d7..077445f 100644 --- a/modules/message-quotes/module.json +++ b/modules/message-quotes/module.json @@ -1,7 +1,7 @@ { "name": "message-quotes", "humanReadableName": "Message quotes", - "description": "Quotes a Discord message when a user has pastes a message link.", + "description": "Quotes a Discord message when a user pastes a message link.", "fa-icon": "fas fa-quote-left", "author": { "name": "Jean S.",