From a1c0c12c651cb0ccba57389cf91ff1a22420234c Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sun, 6 Sep 2026 20:02:35 +0200 Subject: [PATCH 1/6] Quick change of AI blob to make a PR --- modules/ping-protection/module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ping-protection/module.json b/modules/ping-protection/module.json index 028391be..3b197e02 100644 --- a/modules/ping-protection/module.json +++ b/modules/ping-protection/module.json @@ -27,7 +27,7 @@ "AutoModerationExecution" ], "intentReasons": { - "GuildMembers": "Enumerates the holders of protected roles to detect unwanted mentions.", + "GuildMembers": "Checks members to see if they are protected from pings and if they are allowed to ping such users.", "MessageContent": "Scans message content and mentions to catch pings of protected roles and users." } } From f8a12d1737850c639a10171e0037cce893c3daef Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sun, 6 Sep 2026 22:05:44 +0200 Subject: [PATCH 2/6] Added user ping preferences model for the last feature in the changelogs list --- .../models/UserPingPreference.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 modules/ping-protection/models/UserPingPreference.js diff --git a/modules/ping-protection/models/UserPingPreference.js b/modules/ping-protection/models/UserPingPreference.js new file mode 100644 index 00000000..55eda178 --- /dev/null +++ b/modules/ping-protection/models/UserPingPreference.js @@ -0,0 +1,30 @@ +const { + DataTypes, + Model +} = require('sequelize'); + +module.exports = class PingProtectionUserPingPreference extends Model { + static init(sequelize) { + return super.init({ + userId: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + disabledUntil: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: null + } + }, { + tableName: 'ping_protection_user_preferences', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'UserPingPreference', + module: 'ping-protection' +}; \ No newline at end of file From 174bfee98bc2bac655644101d28c5e13a0f222cd Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Wed, 16 Sep 2026 22:21:09 +0200 Subject: [PATCH 3/6] Most of the changes in the meantime --- locales/en.json | 19 +- .../configs/configuration.json | 48 ++++- .../ping-protection/configs/moderation.json | 22 +- modules/ping-protection/configs/storage.json | 8 +- .../events/interactionCreate.js | 23 +- .../ping-protection/events/messageCreate.js | 8 +- .../ping-protection_PingHistory__V1.js | 38 ++++ modules/ping-protection/models/PingHistory.js | 5 + modules/ping-protection/module.json | 4 +- modules/ping-protection/ping-protection.js | 198 +++++++++++++++--- 10 files changed, 304 insertions(+), 69 deletions(-) create mode 100644 modules/ping-protection/migrations/ping-protection_PingHistory__V1.js diff --git a/locales/en.json b/locales/en.json index 77df8518..fa98515d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -728,6 +728,7 @@ "log-punish-role-error": "[Ping Protection] Punishment failed: I cannot punish %tag because their role is higher than or equal to my highest role.", "log-mute-error": "[Ping Protection] Punishment failed: I cannot mute %tag: %e", "log-kick-error": "[Ping Protection] Punishment failed: I cannot kick %tag: %e", + "log-kick-dm-failed": "[Ping Protection] Failed to send kick DM to user %u: %e", "log-action-log-failed": "[Ping Protection] Punishment logging failed: %e", "log-data-deletion": "[Ping Protection] All data for the user with ID %u has been deleted successfully.", "log-automod-keyword-limit": "[Ping Protection] Automod keywords exceed 1000 characters limit. Keywords were truncated.", @@ -764,12 +765,9 @@ "field-wl-channels": "Whitelisted Channels", "field-wl-users": "Whitelisted Users", "list-none": "None are configured.", - "modal-title": "Confirm data deletion for this user", - "fallback-modal-title": "Confirm data deletion", - "modal-label": "Confirm data deletion by typing this phrase:", - "fallback-modal-label": "Confirm by typing this phrase:", - "modal-phrase": "I understand that the data of this user will be deleted and that this action cannot be undone.", - "fallback-modal-phrase": "I confirm the data deletion of this user with risks.", + "del-modal-title": "Confirm data deletion", + "del-modal-label": "Confirm by typing this phrase:", + "del-conf-phrase": "I confirm the data deletion for this user.", "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled.", "field-quick-history": "Quick history view (Last %w weeks)", "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m", @@ -781,15 +779,20 @@ "meme-spider": "🕷️ [Is this you?]() - You just pinged yourself.", "meme-rick": "🎵 [Never gonna give you up, never gonna let you down...]() You just Rick Rolled yourself. Also congrats you unlocked the secret easter egg that only has a 1% chance of appearing!!1!1!!", "meme-grind": "Why are you even pinging yourself 5 times in a row? Anyways continue some more to possibly get the secret meme\n-# (good luck grinding, only a 1% chance of getting it and during testing I had it once after 83 pings)", - "label-jump": "Jump to Message", + "label-jump": "Jump to message", + "ping-mention": "Mention", + "ping-reply": "Reply", + "ping-unknown": "Unknown", + "label-type": "Type of ping", "no-message-link": "This ping was blocked by AutoMod", - "list-entry-text": "%index. **Pinged %target** at %time\n%link", + "list-entry-text": "%index. **Pinged %target** at %time\n%type", "punish-log-docs-title": "Troubleshooting", "punish-log-docs-desc": "This issue is documented in the documentation - you can see how to fix this issue [in the documentation](https://docs.scnx.xyz/docs/custom-bot/modules/moderation/ping-protection/#troubleshooting). Please try the steps mentioned there before contacting support as it's very likely the steps mentioned will fix your issue ^^", "log-fetch-mod-history-failed": "[Ping Protection] Failed to fetch moderation history for user %u: %e", "log-warning-build-failed": "[Ping Protection] Failed to build the warning message: %e", "log-warning-reply-failed": "[Ping Protection] Failed to send the warning message as a reply: %e", "log-warning-send-failed": "[Ping Protection] Failed to send the fallback warning message in channel %c: %e", + "log-warning-delete-failed": "[Ping Protection] Failed to automatically delete the warning message: %e", "log-automod-channel-fetch-failed": "[Ping Protection] Failed to refresh the guild channel cache while syncing AutoMod: %e", "log-automod-rule-delete-failed": "[Ping Protection] Failed to delete the native AutoMod rule: %e", "log-automod-sync-failed": "[Ping Protection] AutoMod sync failed: %e", diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 1d773f59..7d0c0642 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -131,10 +131,10 @@ "humanName": "AutoMod Log Channel", "description": "Channel where AutoMod alerts are sent. It is recommended to keep these in a private channel.", "type": "channelID", - "default": "", - "channelTypes": [ + "content": [ "GUILD_TEXT" ], + "default": "", "dependsOn": "enableAutomod" }, { @@ -178,6 +178,50 @@ "image": "https://scnx-cdn.scootkit.net/1769198862209-rJfCVKzAuo6uQLhPUe9o2P6ArJkDBSVUCEyUQM6bqt5WFKWK.gif", "color": "#ed4245" } + }, + { + "name": "kickPunishmentMessage", + "category": "messages", + "humanName": "Warning Message", + "description": "The message that gets sent to the user's DM's when they are kicked for meeting a moderation action's treshold.'", + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "reason", + "description": "The reason why the user was kicked." + }, + { + "name": "pings", + "description": "The amount of pings that triggered the moderation action." + }, + { + "name": "timeframe", + "description": "The time frame in which the pings occurred." + }, + { + "name": "guild-name", + "description": "The name of the server." + } + ], + "default": "You have been kicked from **%guild-name%** for pinging protected users/roles %pings% times within %timeframe% days." + }, + { + "name": "autoDeleteWarningMessage", + "category": "messages", + "humanName": "Automatically delete warning message", + "description": "If enabled, you can set a custom time after which the warning message will automatically be deleted to keep the channel clean.", + "type": "boolean", + "default": true + }, + { + "name": "deleteWarningMessageTime", + "category": "messages", + "humanName": "Automatic message deletion time", + "description": "The amount of time after which the warning message will automatically be deleted, in seconds.", + "type": "integer", + "default": 30, + "minValue": "1" } ] } \ No newline at end of file diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index 82efd99f..c6a4a295 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -15,6 +15,18 @@ "type": "integer", "default": 10 }, + { + "name": "pingsType", + "humanName": "Type of ping", + "description": "The type of pings tht will trigger this moderation action", + "type": "select", + "content": [ + "Mentions", + "Reply pings", + "Both" + ], + "default": "Both" + }, { "name": "enableRolePingThresholds", "humanName": "Enable role-based ping thresholds", @@ -42,11 +54,11 @@ "default": false }, { - "name": "timeframeDays", - "humanName": "Timeframe (Days)", - "description": "In how many days must these pings occur?", - "type": "integer", - "default": 7, + "name": "customTimeFrame", + "humanName": "Custom timeframe", + "description": "In how much time must these pings occur? (use formats like 10s for 10 seconds, 1h for 1 hour or 1d for 1 day)", + "type": "string", + "default": "7d", "dependsOn": "useCustomTimeframe" }, { diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json index 586ba025..3e826437 100644 --- a/modules/ping-protection/configs/storage.json +++ b/modules/ping-protection/configs/storage.json @@ -24,7 +24,7 @@ "name": "enablePingHistory", "category": "pings", "humanName": "Enable Ping History", - "description": "If enabled, the bot will keep a history of pings to enforce moderation actions.", + "description": "If enabled, the bot will keep a history of pings to enforce moderation actions. THIS IS REQUIRED TO USE MODERATION ACTIONS!", "type": "boolean", "default": true }, @@ -69,11 +69,11 @@ "name": "leaverRetention", "category": "leavers", "humanName": "Leaver Data Retention (Days)", - "description": "How long to keep data after a user leaves (1-7 Days).", + "description": "How long to keep data after a user left the server (1-60 Days).", "type": "integer", - "default": 1, + "default": 7, "minValue": "1", - "maxValue": "7", + "maxValue": "60", "dependsOn": "enableLeaverDataRetention" } ] diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index b54ea172..1761ee3e 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -103,21 +103,9 @@ module.exports.run = async function (client, interaction) { }); } - // Checks to ensure modal content fits Discord limits - let modalTitle = localize('ping-protection', 'modal-title'); - if (modalTitle.length > 45) { - modalTitle = localize('ping-protection', 'fallback-modal-title'); - } - - let modalLabel = localize('ping-protection', 'modal-label'); - if (modalLabel.length > 45) { - modalLabel = localize('ping-protection', 'fallback-modal-label'); - } - - let confirmationPhrase = localize('ping-protection', 'modal-phrase'); - if (confirmationPhrase.length > 100) { - confirmationPhrase = localize('ping-protection', 'fallback-modal-phrase'); - } + let modalTitle = localize('ping-protection', 'del-modal-title'); + let modalLabel = localize('ping-protection', 'del-modal-label'); + let confirmationPhrase = localize('ping-protection', 'del-conf-phrase'); const modal = new ModalBuilder() .setCustomId(`ping-protection_del-confirm_${targetId}_${selection}`) @@ -149,10 +137,7 @@ module.exports.run = async function (client, interaction) { const targetId = parts[2]; const selection = parts.slice(3).join('_'); - let confirmPhrase = localize('ping-protection', 'modal-phrase'); - if (confirmPhrase.length > 100) { - confirmPhrase = localize('ping-protection', 'fallback-modal-phrase'); - } + let confirmPhrase = localize('ping-protection', 'del-conf-phrase'); if (interaction.fields.getTextInputValue('confirm').trim() !== confirmPhrase) { return interaction.reply({ diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index 3cc91ba3..8331f17f 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -1,7 +1,8 @@ const { processPing, sendPingWarning, - isWhitelistedChannel + isWhitelistedChannel, + determinePingType } = require('../ping-protection'); const {localize} = require('../../../src/functions/localize'); const {randomElementFromArray} = require('../../../src/functions/helpers'); @@ -126,6 +127,8 @@ module.exports.run = async function (client, message) { } } + const pingType = determinePingType(message, target.id, isRole); + await processPing( client, message.author.id, @@ -133,6 +136,7 @@ module.exports.run = async function (client, message) { isRole, message.url, message.channel, - memberToPunish + message.member, + pingType ); }; \ No newline at end of file diff --git a/modules/ping-protection/migrations/ping-protection_PingHistory__V1.js b/modules/ping-protection/migrations/ping-protection_PingHistory__V1.js new file mode 100644 index 00000000..1c133542 --- /dev/null +++ b/modules/ping-protection/migrations/ping-protection_PingHistory__V1.js @@ -0,0 +1,38 @@ +const {DataTypes} = require('sequelize'); + +const TABLE = 'ping_protection_history'; + +module.exports = { + tables: [TABLE], + up: async ({ + context: { + queryInterface, + sequelize + } + }) => { + await sequelize.transaction(async (transaction) => { + const description = await queryInterface.describeTable(TABLE).catch(() => ({})); + + if (!description.pingType) { + await queryInterface.addColumn(TABLE, 'pingType', { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null + }, {transaction}); + } + }); + }, + down: async ({ + context: { + queryInterface, + sequelize + } + }) => { + await sequelize.transaction(async (transaction) => { + const description = await queryInterface.describeTable(TABLE).catch(() => ({})); + if (description.pingType) { + await queryInterface.removeColumn(TABLE, 'pingType', {transaction}); + } + }); + } +}; \ No newline at end of file diff --git a/modules/ping-protection/models/PingHistory.js b/modules/ping-protection/models/PingHistory.js index 709e26e1..4de6e6b1 100644 --- a/modules/ping-protection/models/PingHistory.js +++ b/modules/ping-protection/models/PingHistory.js @@ -21,6 +21,11 @@ module.exports = class PingProtectionPingHistory extends Model { isRole: { type: DataTypes.BOOLEAN, defaultValue: false + }, + pingType: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null } }, { tableName: 'ping_protection_history', diff --git a/modules/ping-protection/module.json b/modules/ping-protection/module.json index 4178b735..47f09247 100644 --- a/modules/ping-protection/module.json +++ b/modules/ping-protection/module.json @@ -17,9 +17,9 @@ "tags": [ "moderation" ], - "fa-icon": "fa-duotone fa-clock-alarm", + "fa-icon": "fa-duotone fa-solid fa-message-dot", "humanReadableName": "Ping-Protection", - "description": "Powerful and highly customizable ping-protection module to protect members/roles from unwanted mentions with moderation capabilities.", + "description": "A powerful and highly customizable module to protect members and roles from unwanted mentions with moderation capabilities.", "intents": [ "GuildMembers", "GuildMessages", diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 2bf3f803..3fa8b3e9 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -21,8 +21,71 @@ const { const {localize} = require('../../src/functions/localize'); const recentPings = new Set(); +// Parses a timeframe to ms +function parseTimeframeToMs(input, defaultDays = 7) { + const fallbackMs = defaultDays * 86400000; + + if (typeof input === 'number') { + return Number.isFinite(input) && input > 0 + ? input * 86400000 + : fallbackMs; + } + + const clean = String(input).trim().toLowerCase(); + + if (/^\d+$/.test(clean)) { + const parsedDays = parseInt(clean, 10); + return Number.isFinite(parsedDays) && parsedDays > 0 + ? parsedDays * 86400000 + : fallbackMs; + } + + const match = clean.match(/^(\d+)\s*(s|m|h|d|w)$/); + if (!match) { + return fallbackMs; + } + + const value = parseInt(match[1], 10); + const unit = match[2]; + + if (!Number.isFinite(value) || value <= 0) { + return fallbackMs; + } + + switch (unit) { + case 's': return value * 1000; + case 'm': return value * 60000; + case 'h': return value * 3600000; + case 'd': return value * 86400000; + case 'w': return value * 7 * 86400000; + default: return fallbackMs; + } +} + +// Checks the type of ping (mention or reply) +function determinePingType(message, targetId, isRole = false) { + if (isRole || !message || !targetId) { + return 'MENTION'; + } + + const isReply = Boolean(message.reference && message.mentions?.repliedUser?.id === targetId); + if (!isReply) { + return 'MENTION'; + } + + const content = message.content || ''; + if (!content) { + return 'REPLY'; // Backup if MessageContent intent is not enabled or content is empty + } + + const hasExplicitMention = content.includes(`<@${targetId}>`) || content.includes(`<@!${targetId}>`); + return hasExplicitMention + ? 'MENTION' + : 'REPLY'; +} + // Data handling -async function addPing(client, userId, messageUrl, targetId, isRole) { +async function addPing(client, userId, messageUrl, targetId, isRole, pingType = 'MENTION') { const config = client.configurations['ping-protection']['configuration']; const duplicateWindow = config.enableAutomod ? 5000 : 2000; const debounceKey = `${userId}_${targetId}`; @@ -46,20 +109,27 @@ async function addPing(client, userId, messageUrl, targetId, isRole) { userId: userId, messageUrl: messageUrl || 'Blocked by AutoMod', targetId: targetId, - isRole: isRole + isRole: isRole, + pingType: pingType || 'MENTION' }); } // Gets ping count in timeframe -async function getPingCountInWindow(client, userId, days) { - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - days); +async function getPingCountInWindow(client, userId, timeframe, pingType = null) { + const timeframeMs = parseTimeframeToMs(timeframe); + const cutoffDate = new Date(Date.now() - timeframeMs); + + const whereClause = { + userId: userId, + createdAt: {[Op.gt]: cutoffDate} + }; + + if (pingType === 'MENTION' || pingType === 'REPLY') { + whereClause.pingType = pingType; + } return await client.models['ping-protection']['PingHistory'].count({ - where: { - userId: userId, - createdAt: {[Op.gt]: cutoffDate} - } + where: whereClause }); } @@ -404,14 +474,25 @@ async function generatePanelHistory(client, targetUser, page = 1) { ? `[${localize('ping-protection', 'label-jump')}](${entry.messageUrl})` : localize('ping-protection', 'no-message-link'); - return localize('ping-protection', 'list-entry-text', { + let typeString; + if (entry.pingType === 'MENTION') { + typeString = localize('ping-protection', 'ping-mention'); + } else if (entry.pingType === 'REPLY') { + typeString = localize('ping-protection', 'ping-reply'); + } else { + typeString = localize('ping-protection', 'ping-unknown'); + } + + const typeLine = `• **${localize('ping-protection', 'label-type')}:** ${typeString} (${linkText})`; + const mainEntry = localize('ping-protection', 'list-entry-text', { index: (page - 1) * limit + index + 1, target: targetString, time: timeString, - link: linkText + type: typeLine }); - }); + return `${mainEntry}`; + }); description += lines.join('\n\n'); } @@ -475,7 +556,7 @@ async function generatePanelActions(client, targetUser, page = 1) { return `${(page - 1) * limit + index + 1}. **${entry.type}${duration}** - ${formatDate(entry.createdAt)}\n${localize('ping-protection', 'label-reason')}: ${reasonText}`; }); - description += lines.join('\n\n') + `\n\n*${localize('ping-protection', 'actions-retention-note')}*`; + description += lines.join('\n\n') + `\n\n-# ${localize('ping-protection', 'actions-retention-note')}`; } const row = new ActionRowBuilder().addComponents( @@ -563,16 +644,17 @@ async function sendPingWarning(client, message, target, moduleConfig) { try { const messageOptions = await embedTypeV2(warnMsg, placeholders); + let sentMessage = null; try { - return await message.reply(messageOptions); + sentMessage = await message.reply(messageOptions); } catch (replyError) { client.logger.warn(localize('ping-protection', 'log-warning-reply-failed', { e: replyError.message })); try { - return await message.channel.send(messageOptions); + sentMessage = await message.channel.send(messageOptions); } catch (sendError) { client.logger.warn(localize('ping-protection', 'log-warning-send-failed', { c: message.channel.id, @@ -581,8 +663,21 @@ async function sendPingWarning(client, message, target, moduleConfig) { return null; } } + + // Automatically deletes warning message, if enabled + if (sentMessage && moduleConfig.autoDeleteWarningMessage) { + setTimeout(() => { + sentMessage.delete().catch((deleteError) => { + client.logger.warn(localize('ping-protection', 'log-warning-delete-failed', { + e: deleteError.message + })); + }); + }, moduleConfig.deleteWarningMessageTime * 1000); + } + + return sentMessage; } catch (error) { - client.logger.warn(localize('ping-protection', 'log-warning-build-failed', { + client.logger.error(localize('ping-protection', 'log-warning-build-failed', { e: error.message })); return null; @@ -756,12 +851,24 @@ async function generateHistoryResponse(client, userId, page = 1) { ? `[${localize('ping-protection', 'label-jump')}](${entry.messageUrl})` : localize('ping-protection', 'no-message-link'); - return localize('ping-protection', 'list-entry-text', { + let typeString; + if (entry.pingType === 'MENTION') { + typeString = localize('ping-protection', 'ping-mention'); + } else if (entry.pingType === 'REPLY') { + typeString = localize('ping-protection', 'ping-reply'); + } else { + typeString = localize('ping-protection', 'ping-unknown'); + } + + const typeLine = `• **${localize('ping-protection', 'label-type')}:** ${typeString} (${linkText})`; + const mainEntry = localize('ping-protection', 'list-entry-text', { index: (page - 1) * limit + index + 1, target: targetString, time: timeString, - link: linkText + type: typeLine }); + + return `${mainEntry}`; }); description += lines.join('\n\n'); } @@ -831,7 +938,7 @@ async function generateActionsResponse(client, userId, page = 1) { const reasonText = entry.reason || localize('ping-protection', 'no-reason') || 'No reason'; return `${(page - 1) * limit + index + 1}. **${entry.type}${duration}** - ${formatDate(entry.createdAt)}\n${localize('ping-protection', 'label-reason')}: ${reasonText}`; }); - description += lines.join('\n\n') + `\n\n*${localize('ping-protection', 'actions-retention-note')}*`; + description += lines.join('\n\n') + `\n\n-# ${localize('ping-protection', 'actions-retention-note')}`; } const row = new ActionRowBuilder().addComponents( @@ -968,7 +1075,7 @@ async function executeAction(client, member, rule, reason, storageConfig, origin '%action%': rule.actionType, '%duration%': rule.muteDuration || 'N/A', '%pings%': stats.pingCount || 'N/A', - '%timeframe%': stats.timeframeDays || 'N/A' + '%timeframe%': stats.customTimeFrame || 'N/A' }; try { @@ -1065,6 +1172,27 @@ async function executeAction(client, member, rule, reason, storageConfig, origin } } else if (actionType === 'KICK') { + const moduleConfig = client.configurations['ping-protection']['configuration']; + + if (moduleConfig?.kickPunishmentMessage) { + const placeholders = { + '%guild-name%': member.guild.name, + '%reason%': reason, + '%pings%': stats.pingCount || 'N/A', + '%timeframe%': stats.customTimeFrame || stats.timeframeDays || 'N/A' + }; + + try { + const dmPayload = await embedTypeV2(moduleConfig.kickPunishmentMessage, placeholders); + await member.send(dmPayload); + } catch (dmError) { + client.logger.warn(localize('ping-protection', 'log-kick-dm-failed', { + u: member.user.tag, + e: dmError.message + })); + } + } + await logDb('KICK'); try { await member.kick(reason); @@ -1083,14 +1211,14 @@ async function executeAction(client, member, rule, reason, storageConfig, origin } // Processes a ping event -async function processPing(client, userId, targetId, isRole, messageUrl, originChannel, memberToPunish) { +async function processPing(client, userId, targetId, isRole, messageUrl, originChannel, memberToPunish, pingType = 'MENTION') { const config = client.configurations['ping-protection']['configuration']; const storageConfig = client.configurations['ping-protection']['storage']; const moderationRules = client.configurations['ping-protection']['moderation']; if (storageConfig?.enablePingHistory) { try { - await addPing(client, userId, messageUrl, targetId, isRole); + await addPing(client, userId, messageUrl, targetId, isRole, pingType); } catch (e) { client.logger.error(localize('ping-protection', 'log-ping-history-create-failed', { u: userId, @@ -1103,13 +1231,26 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC for (let i = moderationRules.length - 1; i >= 0; i--) { const rule = moderationRules[i]; + const rulePingType = rule.pingsType || 'Both'; + + if (rulePingType === 'Mentions' && pingType === 'REPLY') { + continue; + } + if (rulePingType === 'Reply pings' && pingType === 'MENTION') { + continue; + } + + let countFilter = null; + if (rulePingType === 'Mentions') countFilter = 'MENTION'; + else if (rulePingType === 'Reply pings') countFilter = 'REPLY'; const retentionWeeks = storageConfig?.pingHistoryRetention || 12; - const timeframeDays = rule.useCustomTimeframe - ? (rule.timeframeDays || 7) + const timeframeRaw = rule.useCustomTimeframe + ? (rule.customTimeFrame || rule.timeframeDays || '7d') : (retentionWeeks * 7); - const pingCount = await getPingCountInWindow(client, userId, timeframeDays); + const timeframeDisplay = String(timeframeRaw); + const pingCount = await getPingCountInWindow(client, userId, timeframeRaw, countFilter); const requiredCount = getRequiredPingCountForMember(rule, memberToPunish); if (requiredCount === EXEMPT_THRESHOLD) { @@ -1140,7 +1281,7 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC const generatedReason = rule.useCustomTimeframe ? localize('ping-protection', 'reason-advanced', { c: pingCount, - d: timeframeDays + d: timeframeDisplay }) : localize('ping-protection', 'reason-basic', { c: pingCount, @@ -1157,7 +1298,8 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC originChannel, { pingCount, - timeframeDays + timeframeDays: timeframeDisplay, + customTimeFrame: timeframeDisplay } ); @@ -1168,6 +1310,8 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC } module.exports = { + parseTimeframeToMs, + determinePingType, addPing, getPingCountInWindow, getSafeChannelId, From f3a0d63ee11f04b28e21ca4cc9b7ed83e77bc62c Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 17 Sep 2026 08:42:04 +0200 Subject: [PATCH 4/6] Attempt at fixing node failures --- modules/ping-protection/events/interactionCreate.js | 4 ++-- modules/ping-protection/ping-protection.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index 1761ee3e..56d9b1fe 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -227,8 +227,8 @@ module.exports.run = async function (client, interaction) { } if (btnInt.customId.includes('confirm')) { - await executeDataDeletion(client, targetId, 'del_all'); - const blockedUntil = await setDeletionCooldown(client, targetId, 'del_all', btnInt.user.id); + await executeDataDeletion(client, targetId, selection); + const blockedUntil = await setDeletionCooldown(client, targetId, selection, btnInt.user.id); client.logger.info(localize('ping-protection', 'log-del-all', { target: targetId, diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 3fa8b3e9..c3985bb8 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -1321,6 +1321,9 @@ module.exports = { sendPingWarning, syncNativeAutoMod, processPing, + sendPingWarning, + isWhitelistedChannel, + determinePingType, fetchPingHistory, fetchModHistory, executeAction, From ce3de49fa6e22b69e4af259383e4b6f86647e1f2 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 17 Sep 2026 21:22:45 +0200 Subject: [PATCH 5/6] Another attempt to fixing the issues thanks to Copilot --- tests/ping-protection/interactionCreate.test.js | 3 ++- tests/ping-protection/messageCreate.test.js | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/ping-protection/interactionCreate.test.js b/tests/ping-protection/interactionCreate.test.js index fe20b630..93ed8821 100644 --- a/tests/ping-protection/interactionCreate.test.js +++ b/tests/ping-protection/interactionCreate.test.js @@ -28,6 +28,7 @@ const mockG = { jest.mock('../../modules/ping-protection/ping-protection', () => mockG); const handler = require('../../modules/ping-protection/events/interactionCreate'); +const {localize} = require('../../src/functions/localize'); function makeClient({ user = { @@ -172,7 +173,7 @@ describe('del-confirm modal submit', () => { test('runs a partial deletion and sets the cooldown on the correct phrase', async () => { // the stub localize returns "ping-protection.modal-phrase"; confirm must equal it - const interaction = modalInteraction('ping-protection.modal-phrase'); + const interaction = modalInteraction(localize('ping-protection', 'del-conf-phrase')); await handler.run(makeClient(), interaction); expect(mockG.executeDataDeletion).toHaveBeenCalledWith(expect.anything(), 'target', 'del_ping_history'); expect(mockG.setDeletionCooldown).toHaveBeenCalledWith(expect.anything(), 'target', 'del_ping_history', 'admin1'); diff --git a/tests/ping-protection/messageCreate.test.js b/tests/ping-protection/messageCreate.test.js index 0ce59ac9..96ddccb8 100644 --- a/tests/ping-protection/messageCreate.test.js +++ b/tests/ping-protection/messageCreate.test.js @@ -9,11 +9,13 @@ */ const mockProcessPing = jest.fn().mockResolvedValue(); const mockSendWarning = jest.fn().mockResolvedValue(); +const mockDeterminePingType = jest.fn(() => 'MENTION'); const mockIsWhitelisted = jest.fn(() => false); jest.mock('../../modules/ping-protection/ping-protection', () => ({ processPing: (...a) => mockProcessPing(...a), sendPingWarning: (...a) => mockSendWarning(...a), - isWhitelistedChannel: (...a) => mockIsWhitelisted(...a) + isWhitelistedChannel: (...a) => mockIsWhitelisted(...a), + determinePingType: (...a) => mockDeterminePingType(...a) })); const handler = require('../../modules/ping-protection/events/messageCreate'); @@ -91,6 +93,7 @@ beforeEach(() => { mockProcessPing.mockClear(); mockSendWarning.mockClear(); mockIsWhitelisted.mockClear(); + mockDeterminePingType.mockClear(); mockIsWhitelisted.mockReturnValue(false); }); @@ -165,7 +168,7 @@ describe('protected ping dispatch', () => { await handler.run(client, msg); expect(mockSendWarning).toHaveBeenCalledWith(client, msg, victimUser, expect.any(Object)); expect(mockProcessPing).toHaveBeenCalledWith( - client, 'pinger', 'victim', false, 'http://msg', msg.channel, msg.member + client, 'pinger', 'victim', false, 'http://msg', msg.channel, msg.member, 'MENTION' ); }); From 7155543d7c72352205d503c7440b9144722e433c Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 17 Sep 2026 21:29:05 +0200 Subject: [PATCH 6/6] 1 annoying little fix, I am ragebaited --- tests/ping-protection/messageCreate.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ping-protection/messageCreate.test.js b/tests/ping-protection/messageCreate.test.js index 96ddccb8..10ee0407 100644 --- a/tests/ping-protection/messageCreate.test.js +++ b/tests/ping-protection/messageCreate.test.js @@ -178,7 +178,7 @@ describe('protected ping dispatch', () => { const msg = makeMessage({roles: [role]}); await handler.run(client, msg); expect(mockProcessPing).toHaveBeenCalledWith( - client, 'pinger', 'roleP', true, 'http://msg', msg.channel, msg.member + client, 'pinger', 'roleP', true, 'http://msg', msg.channel, msg.member, 'MENTION' ); });