From 35021131f023d9fb30e5bf34cf8e0a9faa648d2f Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 11 Dec 2025 19:46:24 +0100 Subject: [PATCH 01/31] Added the base module folders and module.json --- modules/ping-protection/module.json | 22 ++++++++++++++++++++++ modules/ping-protection/ping-protection.js | 0 2 files changed, 22 insertions(+) create mode 100644 modules/ping-protection/module.json create mode 100644 modules/ping-protection/ping-protection.js diff --git a/modules/ping-protection/module.json b/modules/ping-protection/module.json new file mode 100644 index 00000000..e8bef742 --- /dev/null +++ b/modules/ping-protection/module.json @@ -0,0 +1,22 @@ +{ + "name": "ping-protection", + "author": { + "scnxOrgID": "148", + "name": "Kevin", + "link": "https://github.com/Kevinking500" + }, + "openSourceURL": "https://github.com/Kevinking500/CustomDCBot/tree/main/modules/ping-protection", + "commands-dir": "/commands", + "events-dir": "/events", + "tags": [ + "community" + ], + "humanReadableName": { + "en": "Ping-Protection", + "de": "Ping-Schutz" + }, + "description": { + "en": "Powerful and highly customizable ping-protection module to protect members/roles from unwanted mentions with moderation capabilities.", + "de": "Leistungsstarkes und hochgradig anpassbares Ping-Schutz-Modul zum Schutz von Mitgliedern/Rollen vor unerwünschten Erwähnungen mit Moderationsfunktionen." + } +} \ No newline at end of file diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js new file mode 100644 index 00000000..e69de29b From 9bd863dc4c1622735511851ca8956b8d93fecd62 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 11 Dec 2025 21:14:24 +0100 Subject: [PATCH 02/31] Added all folders necessary and the configuration files for each folder --- modules/ping-protection/commands/manage.js | 0 modules/ping-protection/configs/actions.js | 0 modules/ping-protection/configs/configuration.js | 0 modules/ping-protection/configs/storage.js | 0 modules/ping-protection/events/guildMemberRemove.js | 0 modules/ping-protection/events/messageCreate.js | 0 modules/ping-protection/models/ModerationLog.js | 0 modules/ping-protection/models/PingHistory.js | 0 modules/ping-protection/models/UserCache.js | 0 9 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 modules/ping-protection/commands/manage.js create mode 100644 modules/ping-protection/configs/actions.js create mode 100644 modules/ping-protection/configs/configuration.js create mode 100644 modules/ping-protection/configs/storage.js create mode 100644 modules/ping-protection/events/guildMemberRemove.js create mode 100644 modules/ping-protection/events/messageCreate.js create mode 100644 modules/ping-protection/models/ModerationLog.js create mode 100644 modules/ping-protection/models/PingHistory.js create mode 100644 modules/ping-protection/models/UserCache.js diff --git a/modules/ping-protection/commands/manage.js b/modules/ping-protection/commands/manage.js new file mode 100644 index 00000000..e69de29b diff --git a/modules/ping-protection/configs/actions.js b/modules/ping-protection/configs/actions.js new file mode 100644 index 00000000..e69de29b diff --git a/modules/ping-protection/configs/configuration.js b/modules/ping-protection/configs/configuration.js new file mode 100644 index 00000000..e69de29b diff --git a/modules/ping-protection/configs/storage.js b/modules/ping-protection/configs/storage.js new file mode 100644 index 00000000..e69de29b diff --git a/modules/ping-protection/events/guildMemberRemove.js b/modules/ping-protection/events/guildMemberRemove.js new file mode 100644 index 00000000..e69de29b diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js new file mode 100644 index 00000000..e69de29b diff --git a/modules/ping-protection/models/ModerationLog.js b/modules/ping-protection/models/ModerationLog.js new file mode 100644 index 00000000..e69de29b diff --git a/modules/ping-protection/models/PingHistory.js b/modules/ping-protection/models/PingHistory.js new file mode 100644 index 00000000..e69de29b diff --git a/modules/ping-protection/models/UserCache.js b/modules/ping-protection/models/UserCache.js new file mode 100644 index 00000000..e69de29b From 804ceed62c68da7dda256bf687f340798983b2f3 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Fri, 12 Dec 2025 08:30:25 +0100 Subject: [PATCH 03/31] Added a test command --- modules/ping-protection/commands/test.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 modules/ping-protection/commands/test.js diff --git a/modules/ping-protection/commands/test.js b/modules/ping-protection/commands/test.js new file mode 100644 index 00000000..a2a3d942 --- /dev/null +++ b/modules/ping-protection/commands/test.js @@ -0,0 +1,22 @@ +module.exports = { + name: 'test', + description: 'Replies with a test message', + + options: [], + + /** + * @param {import('discord.js').ChatInputCommandInteraction} interaction + */ + async execute(interaction) { + const embed = { + title: 'Test Message', + description: 'This is a test', + color: 0x2b2d31 + }; + + await interaction.reply({ + content: 'testy', + embeds: [embed] + }); + } +}; From 13733f7c180e273e70dbe00bc6b0a854a11eb728 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Fri, 12 Dec 2025 08:33:08 +0100 Subject: [PATCH 04/31] removed the manage file --- modules/ping-protection/commands/manage.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 modules/ping-protection/commands/manage.js diff --git a/modules/ping-protection/commands/manage.js b/modules/ping-protection/commands/manage.js deleted file mode 100644 index e69de29b..00000000 From 88322e356307f739cd07612bcd6311b7462880e9 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 13 Dec 2025 10:53:41 +0100 Subject: [PATCH 05/31] Added, renamed and deleted some files as necessary and coded the models. --- modules/ping-protection/commands/test.js | 22 ----------- .../configs/{actions.js => action.json} | 0 .../{configuration.js => configuration.json} | 0 .../configs/{storage.js => storage.json} | 0 .../UserCache.js => events/botReady.js} | 0 modules/ping-protection/models/LeaverData.js | 26 +++++++++++++ .../ping-protection/models/ModerationLog.js | 37 +++++++++++++++++++ modules/ping-protection/models/PingHistory.js | 28 ++++++++++++++ modules/ping-protection/module.json | 6 +++ 9 files changed, 97 insertions(+), 22 deletions(-) delete mode 100644 modules/ping-protection/commands/test.js rename modules/ping-protection/configs/{actions.js => action.json} (100%) rename modules/ping-protection/configs/{configuration.js => configuration.json} (100%) rename modules/ping-protection/configs/{storage.js => storage.json} (100%) rename modules/ping-protection/{models/UserCache.js => events/botReady.js} (100%) create mode 100644 modules/ping-protection/models/LeaverData.js diff --git a/modules/ping-protection/commands/test.js b/modules/ping-protection/commands/test.js deleted file mode 100644 index a2a3d942..00000000 --- a/modules/ping-protection/commands/test.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - name: 'test', - description: 'Replies with a test message', - - options: [], - - /** - * @param {import('discord.js').ChatInputCommandInteraction} interaction - */ - async execute(interaction) { - const embed = { - title: 'Test Message', - description: 'This is a test', - color: 0x2b2d31 - }; - - await interaction.reply({ - content: 'testy', - embeds: [embed] - }); - } -}; diff --git a/modules/ping-protection/configs/actions.js b/modules/ping-protection/configs/action.json similarity index 100% rename from modules/ping-protection/configs/actions.js rename to modules/ping-protection/configs/action.json diff --git a/modules/ping-protection/configs/configuration.js b/modules/ping-protection/configs/configuration.json similarity index 100% rename from modules/ping-protection/configs/configuration.js rename to modules/ping-protection/configs/configuration.json diff --git a/modules/ping-protection/configs/storage.js b/modules/ping-protection/configs/storage.json similarity index 100% rename from modules/ping-protection/configs/storage.js rename to modules/ping-protection/configs/storage.json diff --git a/modules/ping-protection/models/UserCache.js b/modules/ping-protection/events/botReady.js similarity index 100% rename from modules/ping-protection/models/UserCache.js rename to modules/ping-protection/events/botReady.js diff --git a/modules/ping-protection/models/LeaverData.js b/modules/ping-protection/models/LeaverData.js new file mode 100644 index 00000000..6d88488e --- /dev/null +++ b/modules/ping-protection/models/LeaverData.js @@ -0,0 +1,26 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class LeaverData extends Model { + static init(sequelize) { + return super.init({ + userId: { + type: DataTypes.STRING, + primaryKey: true + }, + // Timestamp of when the user left, used for cooldown calculations + leftAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW + } + }, { + tableName: 'ping_protection_leaver_data', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'LeaverData', + module: 'ping-protection' +}; \ No newline at end of file diff --git a/modules/ping-protection/models/ModerationLog.js b/modules/ping-protection/models/ModerationLog.js index e69de29b..e7d68564 100644 --- a/modules/ping-protection/models/ModerationLog.js +++ b/modules/ping-protection/models/ModerationLog.js @@ -0,0 +1,37 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class ModerationLog extends Model { + static init(sequelize) { + return super.init({ + userId: { + type: DataTypes.STRING, + allowNull: false + }, + actionType: { + type: DataTypes.STRING, + allowNull: false + }, + actionDuration: { + type: DataTypes.INTEGER, + allowNull: true + }, + reason: { + type: DataTypes.TEXT, + allowNull: false + }, + timestamp: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW + } + }, { + tableName: 'ping_protection_mod_log', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'ModerationLog', + module: 'ping-protection' +}; \ No newline at end of file diff --git a/modules/ping-protection/models/PingHistory.js b/modules/ping-protection/models/PingHistory.js index e69de29b..637fcd8e 100644 --- a/modules/ping-protection/models/PingHistory.js +++ b/modules/ping-protection/models/PingHistory.js @@ -0,0 +1,28 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class PingHistory extends Model { + static init(sequelize) { + return super.init({ + + userId: { + type: DataTypes.STRING, + allowNull: false + }, + // Timestasmp of the ping event for date comparisons + // This is explicitly defined to track when the ping occurred + timestamp: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW + } + }, { + tableName: 'ping_protection_history', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'PingHistory', + module: 'ping-protection' +}; \ No newline at end of file diff --git a/modules/ping-protection/module.json b/modules/ping-protection/module.json index e8bef742..a1ac5271 100644 --- a/modules/ping-protection/module.json +++ b/modules/ping-protection/module.json @@ -8,6 +8,12 @@ "openSourceURL": "https://github.com/Kevinking500/CustomDCBot/tree/main/modules/ping-protection", "commands-dir": "/commands", "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/configuration.js", + "configs/actions.json", + "configs/storage.json" + ], "tags": [ "community" ], From 843ebbdf70a7ed8fbaa8b4818b8e1cbcdbd2598a Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 13 Dec 2025 13:13:41 +0100 Subject: [PATCH 06/31] Renamed action.js to moderation.js, coded multiple things, added a new file for correct tracking. --- .../configs/configuration.json | 106 ++++++++++ .../ping-protection/configs/moderation.json | 79 ++++++++ modules/ping-protection/configs/storage.json | 43 ++++ .../action.json => events/guildMemberAdd.js} | 0 modules/ping-protection/models/PingHistory.js | 7 +- modules/ping-protection/ping-protection.js | 191 ++++++++++++++++++ 6 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 modules/ping-protection/configs/moderation.json rename modules/ping-protection/{configs/action.json => events/guildMemberAdd.js} (100%) diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index e69de29b..aed76c3b 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -0,0 +1,106 @@ +{ + "filename": "configuration.json", + "humanName": { + "en": "General Configuration", + "de": "Allgemeine Konfiguration" + }, + "description": { + "en": "Configure protected users/roles, whitelisted roles/members, ignored channels and the notification message.", + "de": "Konfiguriere geschützte Benutzer/Rollen, erlaubte Rollen/Mitglieder, ignorierte Kanäle und die Benachrichtigungsnachricht." + }, + "content": [ + { + "name": "protectedRoles", + "humanName": { + "en": "Protected Roles", + "de": "Geschützte Rollen" + }, + "description": { + "en": "Members with these roles will trigger protection when pinged.", + "de": "Mitglieder mit diesen Rollen lösen den Schutz aus." + }, + "type": "array", + "content": "roleID", + "default": { "en": [] } + }, + { + "name": "protectedUsers", + "humanName": { + "en": "Protected Users", + "de": "Geschützte Benutzer" + }, + "description": { + "en": "Specific users who are protected from pings.", + "de": "Spezifische Benutzer, die geschützt sind." + }, + "type": "array", + "content": "userID", + "default": { "en": [] } + }, + { + "name": "ignoredRoles", + "humanName": { + "en": "Whitelisted Roles", + "de": "Erlaubte Rollen" + }, + "description": { + "en": "Roles allowed to ping protected members or roles.", + "de": "Rollen, die pingen dürfen." + }, + "type": "array", + "content": "roleID", + "default": { "en": [] } + }, + { + "name": "ignoredChannels", + "humanName": { + "en": "Ignored Channels", + "de": "Ignorierte Kanäle" + }, + "description": { + "en": "Pings in these channels are ignored.", + "de": "Pings hier werden ignoriert." }, + "type": "array", + "content": "channelID", + "default": { "en": [] } + }, + { + "name": "allowReplyPings", + "humanName": { + "en": "Allow Reply Pings", + "de": "Antwort-Pings erlauben" + }, + "description": { + "en": "If enabled, replying to a protected user (with mention ON) is allowed.", + "de": "Wenn aktiviert, sind Antworten (mit Mention) an geschützte Benutzer erlaubt." + }, + "type": "boolean", + "default": { "en": true } + }, + { + "name": "pingWarningMessage", + "humanName": { + "en": "Warning Message", + "de": "Warnnachricht" + }, + "description": { + "en": "The message that gets sent to the user when they ping someone.", + "de": "Die Nachricht, die an den Benutzer gesendet wird." + }, + "type": "string", + "allowEmbed": true, + "default": { + "en": { + "title": "You are not allowed to ping %target_name%!", + "description": "You are not allowed to ping %target_mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping protection list roles` or `/ping protection list members`.\n\nIf you were replying, make sure to turn off the mention in the reply.", + "color": "RED" + }, + "de": { + "title": "Du darfst %target_name% nicht pingen!", + "description": "Du darfst %target_mention% aufgrund deiner Rolle nicht pingen. Du kannst sehen, welche Rollen/Mitglieder du nicht pingen darfst, indem du `/ping protection list roles` oder `/ping protection list members` benutzt.\n\nFalls du geantwortet hast, stelle sicher, dass die Erwähnung in der Antwort deaktiviert ist.", + "color": "RED" + } + } + } + ] +} \ No newline at end of file diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json new file mode 100644 index 00000000..b9f73831 --- /dev/null +++ b/modules/ping-protection/configs/moderation.json @@ -0,0 +1,79 @@ +{ + "filename": "moderation.json", + "humanName": { + "en": "Moderation Actions", + "de": "Bestrafungsaktionen" +}, + "description": { + "en": "Define triggers for punishments.", + "de": "Definiere Auslöser für Bestrafungen." + }, + "configElements": true, + "content": [ + { + "name": "pingsCountBasic", + "humanName": { + "en": "Pings to trigger moderation", + "de": "Pings für Bestrafung" + }, + "description": { + "en": "The amount of pings required to trigger a moderation action (Uses 'Ping History Retention' timeframe).", + "de": "Anzahl Pings für Bestrafung (Nutzt 'Ping-Verlauf Speicherzeit')." + }, + "type": "integer", + "default": { "en": 5 } + }, + { + "name": "pingsCountAdvanced", + "humanName": { + "en": "Pings to trigger (Advanced)", + "de": "Pings für Bestrafung (Erweitert)" + }, + "description": { + "en": "The amount of pings required in the custom timeframe below.", + "de": "Anzahl Pings im unten gewählten Zeitraum." + }, + "type": "integer", + "default": { "en": 5 } + }, + { + "name": "timeframeWeeks", + "humanName": { + "en": "Timeframe (Weeks)", + "de": "Zeitraum (Wochen)" + }, + "description": { + "en": "In how many weeks must these pings occur?", + "de": "In wie vielen Wochen müssen diese Pings passieren?" + }, + "type": "integer", + "default": { "en": 1 } + }, + { + "name": "actionType", + "humanName": { + "en": "Action", + "de": "Aktion" }, + "description": { + "en": "What punishment should be applied?", + "de": "Welche Strafe soll verhängt werden?" + }, + "type": "select", + "content": ["MUTE", "KICK"], + "default": { "en": "MUTE" } + }, + { + "name": "muteDuration", + "humanName": { + "en": "Mute Duration (Only if Action is MUTE)", + "de": "Mute-Dauer (Nur bei MUTE Aktion)" + }, + "description": { + "en": "How long to mute the user? (in minutes)", + "de": "Wie lange soll der User gemutet werden? (in Minuten)" + }, + "type": "integer", + "default": { "en": 60 } + } + ] +} \ No newline at end of file diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json index e69de29b..903aacb0 100644 --- a/modules/ping-protection/configs/storage.json +++ b/modules/ping-protection/configs/storage.json @@ -0,0 +1,43 @@ +{ + "filename": "storage.json", + "humanName": { "en": "Data Storage", "de": "Datenspeicherung" }, + "description": { + "en": "Configure how long moderation logs and leaver data are kept.", + "de": "Konfiguriere, wie lange Mod-Logs und Leaver-Daten gespeichert werden." + }, + "content": [ + { + "name": "pingHistoryRetention", + "humanName": { + "en": "Ping History Retention", + "de": "Ping-Verlauf Speicherzeit" + }, + "description": { + "en": "Decides on how long to keep ping logs. Minimum is 12 weeks (3 months) with a maximum of 24 weeks (6 months). This is the length factor of the 'Basic' punishment timeframe.", + "de": "Entscheidet, wie lange Ping-Logs gespeichert werden. Minimum 12 Wochen, Maximum 24 Wochen. Dies ist der Faktor für den 'Basis'-Bestrafungszeitraum." + }, + "type": "integer", + "default": { "en": 12 } + }, + { + "name": "modLogRetention", + "humanName": { "en": "Moderation Log Retention (Months)", "de": "Mod-Log Speicherzeit (Monate)" }, + "description": { + "en": "How long to keep records of punishments (6-12 Months).", + "de": "Wie lange Bestrafungsprotokolle gespeichert werden (6-12 Monate)." + }, + "type": "integer", + "default": { "en": 6 } + }, + { + "name": "leaverRetention", + "humanName": { "en": "Leaver Data Retention (Days)", "de": "Leaver-Daten Speicherzeit (Tage)" }, + "description": { + "en": "How long to keep data after a user leaves (1-3 Days).", + "de": "Wie lange Daten gespeichert bleiben, nachdem ein User den Server verlässt." + }, + "type": "integer", + "default": { "en": 1 } + } + ] +} \ No newline at end of file diff --git a/modules/ping-protection/configs/action.json b/modules/ping-protection/events/guildMemberAdd.js similarity index 100% rename from modules/ping-protection/configs/action.json rename to modules/ping-protection/events/guildMemberAdd.js diff --git a/modules/ping-protection/models/PingHistory.js b/modules/ping-protection/models/PingHistory.js index 637fcd8e..c0f15393 100644 --- a/modules/ping-protection/models/PingHistory.js +++ b/modules/ping-protection/models/PingHistory.js @@ -3,13 +3,14 @@ const { DataTypes, Model } = require('sequelize'); module.exports = class PingHistory extends Model { static init(sequelize) { return super.init({ - userId: { type: DataTypes.STRING, allowNull: false }, - // Timestasmp of the ping event for date comparisons - // This is explicitly defined to track when the ping occurred + messageUrl: { + type: DataTypes.STRING, + allowNull: false + }, timestamp: { type: DataTypes.DATE, defaultValue: DataTypes.NOW diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index e69de29b..9b4e8e4f 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -0,0 +1,191 @@ +/** + * Logic for the Ping Protection module + * @module ping-protection + */ +const { Op } = require('sequelize'); +const { embedType } = require('../../src/functions/helpers'); + +/** + * Adds a ping record to the database. + * @param {Client} client + * @param {Message} message + */ +async function addPing(client, message) { + await client.models['ping-protection']['PingHistory'].create({ + userId: message.author.id, + messageUrl: message.url + }); +} + +/** + * Counts pings within a specific timeframe (in weeks). + * @param {Client} client + * @param {string} userId + * @param {number} weeks + * @returns {Promise} + */ +async function getPingCountInWindow(client, userId, weeks) { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - (weeks * 7)); + + return await client.models['ping-protection']['PingHistory'].count({ + where: { + userId: userId, + timestamp: { + [Op.gt]: cutoffDate + } + } + }); +} + +/** + * Sends the warning message to the user who pinged. + * @param {Client} client + * @param {Message} message + * @param {Role|User} target + * @param {Object} moduleConfig + */ +async function sendPingWarning(client, message, target, moduleConfig) { + const warningMsg = moduleConfig.pingWarningMessage; + if (!warningMsg) return; + + const targetName = target.name || target.tag || target.username || 'Unknown'; + + const targetMention = target.toString(); + + const placeholders = { + '%target_name%': targetName, + '%target_mention%': targetMention + }; + + const replyOptions = embedType(warningMsg, placeholders); + + await message.reply(replyOptions).catch((e) => { + client.logger.debug(`[ping-protection] Failed to send warning to ${message.author.tag}: ${e.message}`); + }); +} + +/** + * Fetches the last X pings. + * @param {Client} client + * @param {string} userId + * @param {number} limit + * @returns {Promise} + */ +async function fetchPingHistory(client, userId, limit = 10) { + return await client.models['ping-protection']['PingHistory'].findAll({ + where: { userId: userId }, + order: [['timestamp', 'DESC']], + limit: limit + }); +} + +/** + * Fetches the moderation log history. + * @param {Client} client + * @param {string} userId + * @param {number} limit + * @returns {Promise} + */ +async function fetchModHistory(client, userId, limit = 10) { + return await client.models['ping-protection']['ModerationLog'].findAll({ + where: { userId: userId }, + order: [['timestamp', 'DESC']], + limit: limit + }); +} + +/** + * Executes a punishment and logs it. + * @param {Client} client + * @param {GuildMember} member + * @param {Object} actionConfig + * @param {string} reason + */ +async function executeAction(client, member, actionConfig, reason) { + const ModLog = client.models['ping-protection']['ModerationLog']; + + try { + if (actionConfig.type === 'MUTE') { + const durationMs = (actionConfig.muteDuration || 60) * 60 * 1000; + + await member.timeout(durationMs, reason); + + await ModLog.create({ + userId: member.id, + actionType: 'MUTE', + actionDuration: durationMs, + reason: reason + }); + client.logger.info(`[ping-protection] Muted ${member.user.tag} for ${actionConfig.muteDuration} mins. Reason: ${reason}`); + + } else if (actionConfig.type === 'KICK') { + await member.kick(reason); + + await ModLog.create({ + userId: member.id, + actionType: 'KICK', + actionDuration: null, + reason: reason + }); + client.logger.info(`[ping-protection] Kicked ${member.user.tag}. Reason: ${reason}`); + } + } catch (error) { + client.logger.error(`[ping-protection] Failed to execute ${actionConfig.type} on ${member.user.tag}: ${error.message}`); + } +} + +/** + * Deletes ALL database information for a user. + * @param {Client} client + * @param {string} userId + */ +async function deleteAllUserData(client, userId) { + await client.models['ping-protection']['PingHistory'].destroy({ where: { userId: userId } }); + await client.models['ping-protection']['ModerationLog'].destroy({ where: { userId: userId } }); + await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); + + client.logger.info(`[ping-protection] Manually deleted all data for user ${userId}`); +} + +/** + * Checks if a user is currently marked as left. + * @param {Client} client + * @param {string} userId + * @returns {Promise} + */ +async function getLeaverStatus(client, userId) { + return await client.models['ping-protection']['LeaverData'].findByPk(userId); +} + +/** + * Marks user as left. + */ +async function markUserAsLeft(client, userId) { + await client.models['ping-protection']['LeaverData'].upsert({ + userId: userId, + leftAt: new Date() + }); +} + +/** + * Handles rejoin. + */ +async function markUserAsRejoined(client, userId) { + await client.models['ping-protection']['LeaverData'].destroy({ + where: { userId: userId } + }); +} + +module.exports = { + addPing, + getPingCountInWindow, + sendPingWarning, + fetchPingHistory, + fetchModHistory, + executeAction, + deleteAllUserData, + getLeaverStatus, + markUserAsLeft, + markUserAsRejoined +}; \ No newline at end of file From 225abb7c804f291201f68e6d7e7db5523d723020 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 13 Dec 2025 13:16:49 +0100 Subject: [PATCH 07/31] Forgot to update module.json, now updated aswell --- modules/ping-protection/module.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ping-protection/module.json b/modules/ping-protection/module.json index a1ac5271..4fbce23c 100644 --- a/modules/ping-protection/module.json +++ b/modules/ping-protection/module.json @@ -11,11 +11,11 @@ "models-dir": "/models", "config-example-files": [ "configs/configuration.js", - "configs/actions.json", + "configs/moderation.json", "configs/storage.json" ], "tags": [ - "community" + "moderation" ], "humanReadableName": { "en": "Ping-Protection", From 1602a165fd022d35f56861ca54676373ef0ee34f Mon Sep 17 00:00:00 2001 From: Kevin Date: Sat, 13 Dec 2025 13:47:24 +0100 Subject: [PATCH 08/31] Added additional information in ping-protection.js --- modules/ping-protection/ping-protection.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 9b4e8e4f..f6e0372d 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -1,6 +1,7 @@ /** - * Logic for the Ping Protection module + * Logic and functions for the Ping Protection module * @module ping-protection + * @author itskevinnn */ const { Op } = require('sequelize'); const { embedType } = require('../../src/functions/helpers'); @@ -188,4 +189,4 @@ module.exports = { getLeaverStatus, markUserAsLeft, markUserAsRejoined -}; \ No newline at end of file +}; From d285aee0b2d6efceb78c4abeb85a7562c478ed4a Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 13 Dec 2025 17:03:23 +0100 Subject: [PATCH 09/31] Disabled allowing reply pings, added the enable moderation and enable advanced configuration in moderation.json and made the choices inside depend on it because I forgot to :/ Added the options to enable/disable pings/modlogs/logs kept after leave and made the choices depend on it + made those choices with numbers select instead of integers for almost 0 user-error issues. --- .../configs/configuration.json | 2 +- .../ping-protection/configs/moderation.json | 60 ++++++++++++++-- modules/ping-protection/configs/storage.json | 70 ++++++++++++++++--- 3 files changed, 114 insertions(+), 18 deletions(-) diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index aed76c3b..33b147a9 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -75,7 +75,7 @@ "de": "Wenn aktiviert, sind Antworten (mit Mention) an geschützte Benutzer erlaubt." }, "type": "boolean", - "default": { "en": true } + "default": { "en": false } }, { "name": "pingWarningMessage", diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index b9f73831..302f365a 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -10,6 +10,32 @@ }, "configElements": true, "content": [ + { + "name": "enableModeration", + "humanName": { + "en": "Enable Moderation Actions", + "de": "Bestrafungsaktionen aktivieren" + }, + "description": { + "en": "If enabled, members who ping protected users/roles repeatedly will be punished.", + "de": "Wenn aktiviert, werden Mitglieder, die geschützte Benutzer/Rollen wiederholt pingen, bestraft." + }, + "type": "boolean", + "default": { "en": false } + }, + { + "name": "advancedConfiguration", + "humanName": { + "en": "Use Advanced Configuration", + "de": "Erweiterte Konfiguration verwenden" + }, + "description": { + "en": "If enabled, the advanced configuration below will be used and the basic configuration will be ignored.", + "de": "Wenn aktiviert, wird die untenstehende erweiterte Konfiguration verwendet und die Basis-Konfiguration ignoriert." + }, + "type": "boolean", + "default": { "en": false } + }, { "name": "pingsCountBasic", "humanName": { @@ -21,7 +47,11 @@ "de": "Anzahl Pings für Bestrafung (Nutzt 'Ping-Verlauf Speicherzeit')." }, "type": "integer", - "default": { "en": 5 } + "default": { "en": 5 }, + "dependsOn": { + "name": "enableModeration", + "value": true + } }, { "name": "pingsCountAdvanced", @@ -34,7 +64,11 @@ "de": "Anzahl Pings im unten gewählten Zeitraum." }, "type": "integer", - "default": { "en": 5 } + "default": { "en": 5 }, + "dependsOn": { + "name": "advancedConfiguration", + "value": true + } }, { "name": "timeframeWeeks", @@ -47,7 +81,11 @@ "de": "In wie vielen Wochen müssen diese Pings passieren?" }, "type": "integer", - "default": { "en": 1 } + "default": { "en": 1 }, + "dependsOn": { + "name": "advancedConfiguration", + "value": true + } }, { "name": "actionType", @@ -60,20 +98,28 @@ }, "type": "select", "content": ["MUTE", "KICK"], - "default": { "en": "MUTE" } + "default": { "en": "MUTE" }, + "dependsOn": { + "name": "enableModeration", + "value": true + } }, { "name": "muteDuration", "humanName": { - "en": "Mute Duration (Only if Action is MUTE)", - "de": "Mute-Dauer (Nur bei MUTE Aktion)" + "en": "Mute Duration", + "de": "Mute-Dauer" }, "description": { "en": "How long to mute the user? (in minutes)", "de": "Wie lange soll der User gemutet werden? (in Minuten)" }, "type": "integer", - "default": { "en": 60 } + "default": { "en": 60 }, + "dependsOn": { + "name": "actionType", + "value": "MUTE" + } } ] } \ No newline at end of file diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json index 903aacb0..3e926141 100644 --- a/modules/ping-protection/configs/storage.json +++ b/modules/ping-protection/configs/storage.json @@ -6,6 +6,19 @@ "de": "Konfiguriere, wie lange Mod-Logs und Leaver-Daten gespeichert werden." }, "content": [ + { + "name": "enablePingHistory", + "humanName": { + "en": "Enable Ping History", + "de": "Ping-Verlauf aktivieren" + }, + "description": { + "en": "If enabled, the bot will keep a history of pings to enforce moderation actions.", + "de": "Wenn aktiviert, speichert der Bot einen Ping-Verlauf, um Bestrafungsaktionen durchzusetzen." + }, + "type": "boolean", + "default": { "en": true } + }, { "name": "pingHistoryRetention", "humanName": { @@ -13,30 +26,67 @@ "de": "Ping-Verlauf Speicherzeit" }, "description": { - "en": "Decides on how long to keep ping logs. Minimum is 12 weeks (3 months) with a maximum of 24 weeks (6 months). This is the length factor of the 'Basic' punishment timeframe.", - "de": "Entscheidet, wie lange Ping-Logs gespeichert werden. Minimum 12 Wochen, Maximum 24 Wochen. Dies ist der Faktor für den 'Basis'-Bestrafungszeitraum." + "en": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 24 weeks (6 months). This is the length factor of the 'Basic' punishment timeframe.", + "de": "Entscheidet, wie lange Ping-Logs gespeichert werden. Minimum 1 Woche, Maximum 24 Wochen. Dies ist der Faktor für den 'Basis'-Bestrafungszeitraum." }, - "type": "integer", - "default": { "en": 12 } + "type": "select", + "content": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], + "default": { "en": 12 }, + "dependsOn": { + "name": "enablePingHistory", + "value": true + } + }, + { + "name": "enableModLogHistory", + "humanName": { + "en": "Enable Moderation Log History", + "de": "Mod-Log Verlauf aktivieren" + }, + "description": { + "en": "If enabled, the bot will keep a history of moderation actions taken by the ping-protection module.", + "de": "Wenn aktiviert, speichert der Bot einen Verlauf der Bestrafungsaktionen des Ping-Schutz Moduls." + }, + "type": "boolean", + "default": { "en": false } }, { "name": "modLogRetention", "humanName": { "en": "Moderation Log Retention (Months)", "de": "Mod-Log Speicherzeit (Monate)" }, "description": { - "en": "How long to keep records of punishments (6-12 Months).", - "de": "Wie lange Bestrafungsprotokolle gespeichert werden (6-12 Monate)." + "en": "How long to keep records of punishments (3-12 Months).", + "de": "Wie lange Bestrafungsprotokolle gespeichert werden (3-12 Monate)." }, - "type": "integer", - "default": { "en": 6 } + "type": "select", + "content": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + "default": { "en": 6 }, + "dependsOn": { + "name": "enableModLogHistory", + "value": true + } + }, + { + "name": "enableLeaverDataRetention", + "humanName": { + "en": "Keep user logs after they leave", + "de": "Benutzerprotokolle nach Verlassen speichern" + }, + "description": { + "en": "If enabled, the bot will keep a history of the user after they leave.", + "de": "Wenn aktiviert, speichert der Bot einen Verlauf des Benutzers, nachdem er den Server verlassen hat." + }, + "type": "boolean", + "default": { "en": true } }, { "name": "leaverRetention", "humanName": { "en": "Leaver Data Retention (Days)", "de": "Leaver-Daten Speicherzeit (Tage)" }, "description": { - "en": "How long to keep data after a user leaves (1-3 Days).", + "en": "How long to keep data after a user leaves (1-7 Days).", "de": "Wie lange Daten gespeichert bleiben, nachdem ein User den Server verlässt." }, - "type": "integer", + "type": "select", + "content": [1, 2, 3, 4, 5, 6, 7], "default": { "en": 1 } } ] From cdf6f70f8117a98b921c55451fec9b442ea88757 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 13 Dec 2025 19:23:55 +0100 Subject: [PATCH 10/31] Added support for actually correct parameters and those parameters added into the message editor for the warning message --- .../configs/configuration.json | 71 ++++++++----------- modules/ping-protection/ping-protection.js | 43 ++++++----- 2 files changed, 52 insertions(+), 62 deletions(-) diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 33b147a9..5b2e1104 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -11,65 +11,39 @@ "content": [ { "name": "protectedRoles", - "humanName": { - "en": "Protected Roles", - "de": "Geschützte Rollen" - }, - "description": { - "en": "Members with these roles will trigger protection when pinged.", - "de": "Mitglieder mit diesen Rollen lösen den Schutz aus." - }, + "humanName": { "en": "Protected Roles", "de": "Geschützte Rollen" }, + "description": { "en": "Members with these roles will trigger protection when pinged.", "de": "Mitglieder mit diesen Rollen lösen den Schutz aus." }, "type": "array", "content": "roleID", "default": { "en": [] } }, { "name": "protectedUsers", - "humanName": { - "en": "Protected Users", - "de": "Geschützte Benutzer" - }, - "description": { - "en": "Specific users who are protected from pings.", - "de": "Spezifische Benutzer, die geschützt sind." - }, + "humanName": { "en": "Protected Users", "de": "Geschützte Benutzer" }, + "description": { "en": "Specific users who are protected from pings.", "de": "Spezifische Benutzer, die geschützt sind." }, "type": "array", "content": "userID", "default": { "en": [] } }, { "name": "ignoredRoles", - "humanName": { - "en": "Whitelisted Roles", - "de": "Erlaubte Rollen" - }, - "description": { - "en": "Roles allowed to ping protected members or roles.", - "de": "Rollen, die pingen dürfen." - }, + "humanName": { "en": "Whitelisted Roles", "de": "Erlaubte Rollen" }, + "description": { "en": "Roles allowed to ping protected members or roles.", "de": "Rollen, die pingen dürfen." }, "type": "array", "content": "roleID", "default": { "en": [] } }, { "name": "ignoredChannels", - "humanName": { - "en": "Ignored Channels", - "de": "Ignorierte Kanäle" - }, - "description": { - "en": "Pings in these channels are ignored.", - "de": "Pings hier werden ignoriert." }, + "humanName": { "en": "Ignored Channels", "de": "Ignorierte Kanäle" }, + "description": { "en": "Pings in these channels are ignored.", "de": "Pings hier werden ignoriert." }, "type": "array", "content": "channelID", "default": { "en": [] } }, { "name": "allowReplyPings", - "humanName": { - "en": "Allow Reply Pings", - "de": "Antwort-Pings erlauben" - }, + "humanName": { "en": "Allow Reply Pings", "de": "Antwort-Pings erlauben" }, "description": { "en": "If enabled, replying to a protected user (with mention ON) is allowed.", "de": "Wenn aktiviert, sind Antworten (mit Mention) an geschützte Benutzer erlaubt." @@ -79,25 +53,36 @@ }, { "name": "pingWarningMessage", - "humanName": { - "en": "Warning Message", - "de": "Warnnachricht" - }, + "humanName": { "en": "Warning Message", "de": "Warnnachricht" }, "description": { "en": "The message that gets sent to the user when they ping someone.", "de": "Die Nachricht, die an den Benutzer gesendet wird." }, "type": "string", "allowEmbed": true, + "params": [ + { + "name": "target-name", + "description": { "en": "Name of the pinged user/role", "de": "Name des gepingten Benutzers/der Rolle" } + }, + { + "name": "target-mention", + "description": { "en": "Mention of the pinged user/role", "de": "Erwähnung des gepingten Benutzers/der Rolle" } + }, + { + "name": "target-id", + "description": { "en": "ID of the pinged user/role", "de": "ID des gepingten Benutzers/der Rolle" } + } + ], "default": { "en": { - "title": "You are not allowed to ping %target_name%!", - "description": "You are not allowed to ping %target_mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping protection list roles` or `/ping protection list members`.\n\nIf you were replying, make sure to turn off the mention in the reply.", + "title": "You are not allowed to ping %target-name%!", + "description": "You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping protection list roles` or `/ping protection list members`.\n\nIf you were replying, make sure to turn off the mention in the reply.", "color": "RED" }, "de": { - "title": "Du darfst %target_name% nicht pingen!", - "description": "Du darfst %target_mention% aufgrund deiner Rolle nicht pingen. Du kannst sehen, welche Rollen/Mitglieder du nicht pingen darfst, indem du `/ping protection list roles` oder `/ping protection list members` benutzt.\n\nFalls du geantwortet hast, stelle sicher, dass die Erwähnung in der Antwort deaktiviert ist.", + "title": "Du darfst %target-name% nicht pingen!", + "description": "Du darfst %target-mention% aufgrund deiner Rolle nicht pingen. Du kannst sehen, welche Rollen/Mitglieder du nicht pingen darfst, indem du `/ping protection list roles` oder `/ping protection list members` benutzt.\n\nFalls du geantwortet hast, stelle sicher, dass die Erwähnung in der Antwort deaktiviert ist.", "color": "RED" } } diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index f6e0372d..a71c96e9 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -1,5 +1,5 @@ /** - * Logic and functions for the Ping Protection module + * Logic for the Ping Protection module * @module ping-protection * @author itskevinnn */ @@ -51,12 +51,12 @@ async function sendPingWarning(client, message, target, moduleConfig) { if (!warningMsg) return; const targetName = target.name || target.tag || target.username || 'Unknown'; - const targetMention = target.toString(); const placeholders = { - '%target_name%': targetName, - '%target_mention%': targetMention + '%target-name%': targetName, + '%target-mention%': targetMention, + '%target-id%': target.id }; const replyOptions = embedType(warningMsg, placeholders); @@ -97,13 +97,14 @@ async function fetchModHistory(client, userId, limit = 10) { } /** - * Executes a punishment and logs it. + * Executes a punishment and optionally logs it. * @param {Client} client * @param {GuildMember} member * @param {Object} actionConfig * @param {string} reason + * @param {Object} storageConfig */ -async function executeAction(client, member, actionConfig, reason) { +async function executeAction(client, member, actionConfig, reason, storageConfig) { const ModLog = client.models['ping-protection']['ModerationLog']; try { @@ -112,23 +113,27 @@ async function executeAction(client, member, actionConfig, reason) { await member.timeout(durationMs, reason); - await ModLog.create({ - userId: member.id, - actionType: 'MUTE', - actionDuration: durationMs, - reason: reason - }); + if (storageConfig && storageConfig.enableModLogHistory) { + await ModLog.create({ + userId: member.id, + actionType: 'MUTE', + actionDuration: durationMs, + reason: reason + }); + } client.logger.info(`[ping-protection] Muted ${member.user.tag} for ${actionConfig.muteDuration} mins. Reason: ${reason}`); } else if (actionConfig.type === 'KICK') { await member.kick(reason); - await ModLog.create({ - userId: member.id, - actionType: 'KICK', - actionDuration: null, - reason: reason - }); + if (storageConfig && storageConfig.enableModLogHistory) { + await ModLog.create({ + userId: member.id, + actionType: 'KICK', + actionDuration: null, + reason: reason + }); + } client.logger.info(`[ping-protection] Kicked ${member.user.tag}. Reason: ${reason}`); } } catch (error) { @@ -189,4 +194,4 @@ module.exports = { getLeaverStatus, markUserAsLeft, markUserAsRejoined -}; +}; \ No newline at end of file From 0e4cafb22550eb9341776e3878c2d529d05b998e Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 13 Dec 2025 20:33:06 +0100 Subject: [PATCH 11/31] Added proper support for localization, and coded the events --- modules/ping-protection/events/botReady.js | 24 +++++ .../ping-protection/events/guildMemberAdd.js | 12 +++ .../events/guildMemberRemove.js | 18 ++++ .../ping-protection/events/messageCreate.js | 95 +++++++++++++++++++ modules/ping-protection/ping-protection.js | 68 ++++++++++++- 5 files changed, 212 insertions(+), 5 deletions(-) diff --git a/modules/ping-protection/events/botReady.js b/modules/ping-protection/events/botReady.js index e69de29b..cd2c4b3f 100644 --- a/modules/ping-protection/events/botReady.js +++ b/modules/ping-protection/events/botReady.js @@ -0,0 +1,24 @@ +const { enforceRetention } = require('../ping-protection'); +const schedule = require('node-schedule'); +const { localize } = require('../../src/functions/localize'); + +module.exports.run = async function (client) { + try { + await client.models['ping-protection']['PingHistory'].sync(); + await client.models['ping-protection']['ModerationLog'].sync(); + await client.models['ping-protection']['LeaverData'].sync(); + + client.logger.debug('[ping-protection] ' + localize('ping-protection', 'log-db-synced')); + } catch (e) { + client.logger.error('[ping-protection] Failed to sync database models: ' + e); + } + + // Run Retention Checks + await enforceRetention(client); + + // Schedule Retention Job (03:00 daily via cronjob) + const job = schedule.scheduleJob('0 3 * * *', async () => { + await enforceRetention(client); + }); + client.jobs.push(job); +}; \ No newline at end of file diff --git a/modules/ping-protection/events/guildMemberAdd.js b/modules/ping-protection/events/guildMemberAdd.js index e69de29b..f81a1000 100644 --- a/modules/ping-protection/events/guildMemberAdd.js +++ b/modules/ping-protection/events/guildMemberAdd.js @@ -0,0 +1,12 @@ +/** + * Checks when a member rejoins the server and updates their leaver status + */ + +const { markUserAsRejoined } = require('../ping-protection'); + +module.exports.run = async function (client, member) { + if (!client.botReadyAt) return; + if (member.guild.id !== client.config.guildID) return; + + await markUserAsRejoined(client, member.id); +}; \ No newline at end of file diff --git a/modules/ping-protection/events/guildMemberRemove.js b/modules/ping-protection/events/guildMemberRemove.js index e69de29b..3bcd73cc 100644 --- a/modules/ping-protection/events/guildMemberRemove.js +++ b/modules/ping-protection/events/guildMemberRemove.js @@ -0,0 +1,18 @@ +/** + * Checks when a member leaves the server and handles data retention and/or deletion + */ + +const { markUserAsLeft, deleteAllUserData } = require('../ping-protection'); + +module.exports.run = async function (client, member) { + if (!client.botReadyAt) return; + if (member.guild.id !== client.config.guildID) return; + + const storageConfig = client.configurations['ping-protection']['storage']; + + if (storageConfig && storageConfig.enableLeaverDataRetention) { + await markUserAsLeft(client, member.id); + } else { + await deleteAllUserData(client, member.id); + } +}; \ No newline at end of file diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index e69de29b..a110155a 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -0,0 +1,95 @@ +const { + addPing, + getPingCountInWindow, + executeAction, + sendPingWarning +} = require('../ping-protection'); +const { localize } = require('../../src/functions/localize'); + +module.exports.run = async function (client, message) { + if (!client.botReadyAt) return; + if (!message.guild) return; + if (message.author.bot) return; + if (message.guild.id !== client.config.guildID) return; + + const config = client.configurations['ping-protection']['config']; + const storageConfig = client.configurations['ping-protection']['storage']; + const moderationRules = client.configurations['ping-protection']['moderation']; + + if (!config) return; + + // Checks ignored channels + if (config.ignoredChannels.includes(message.channel.id)) return; + + // Checks whitelisted roles + const hasIgnoredRole = message.member.roles.cache.some(role => + config.ignoredRoles.includes(role.id) + ); + if (hasIgnoredRole) return; + + // Reply logic + if (message.type === 'REPLY' && config.allowReplyPings) { + } + + // Detect pings + const pingedProtectedRole = message.mentions.roles.some(role => + config.protectedRoles.includes(role.id) + ); + + const pingedProtectedUser = message.mentions.users.some(user => + config.protectedUsers.includes(user.id) + ); + + if (!pingedProtectedRole && !pingedProtectedUser) return; + + // Log pings if enabled + if (storageConfig && storageConfig.enablePingHistory) { + await addPing(client, message); + } + + // Send warning + const target = message.mentions.users.find(u => config.protectedUsers.includes(u.id)) + || message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); + + await sendPingWarning(client, message, target, config); + + // Moderation logic + if (!moderationRules || !Array.isArray(moderationRules)) return; + + for (const rule of moderationRules) { + if (!rule.enableModeration) continue; + + let triggerHit = false; + let generatedReason = ""; + + if (rule.advancedConfiguration) { + // Advanced configuration + const count = await getPingCountInWindow(client, message.author.id, rule.timeframeWeeks); + + if (count >= rule.pingsCountAdvanced) { + triggerHit = true; + generatedReason = localize('ping-protection', 'reason-advanced', { + c: count, + w: rule.timeframeWeeks + }); + } + } else { + // Basic configuration + const globalWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + const count = await getPingCountInWindow(client, message.author.id, globalWeeks); + + if (count >= rule.pingsCountBasic) { + triggerHit = true; + generatedReason = localize('ping-protection', 'reason-basic', { + c: count, + w: globalWeeks + }); + } + } + + if (triggerHit) { + await executeAction(client, message.member, rule, generatedReason, storageConfig); + break; + } + } +}; \ No newline at end of file diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index a71c96e9..957f5ded 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -4,7 +4,8 @@ * @author itskevinnn */ const { Op } = require('sequelize'); -const { embedType } = require('../../src/functions/helpers'); +const { embedType, formatDiscordUserName } = require('../../src/functions/helpers'); +const { localize } = require('../../src/functions/localize'); /** * Adds a ping record to the database. @@ -121,7 +122,12 @@ async function executeAction(client, member, actionConfig, reason, storageConfig reason: reason }); } - client.logger.info(`[ping-protection] Muted ${member.user.tag} for ${actionConfig.muteDuration} mins. Reason: ${reason}`); + + client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-action-mute', { + u: member.user.tag, + t: actionConfig.muteDuration, + r: reason + })); } else if (actionConfig.type === 'KICK') { await member.kick(reason); @@ -134,7 +140,11 @@ async function executeAction(client, member, actionConfig, reason, storageConfig reason: reason }); } - client.logger.info(`[ping-protection] Kicked ${member.user.tag}. Reason: ${reason}`); + + client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-action-kick', { + u: member.user.tag, + r: reason + })); } } catch (error) { client.logger.error(`[ping-protection] Failed to execute ${actionConfig.type} on ${member.user.tag}: ${error.message}`); @@ -151,7 +161,7 @@ async function deleteAllUserData(client, userId) { await client.models['ping-protection']['ModerationLog'].destroy({ where: { userId: userId } }); await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); - client.logger.info(`[ping-protection] Manually deleted all data for user ${userId}`); + client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-manual-delete', { u: userId })); } /** @@ -183,6 +193,53 @@ async function markUserAsRejoined(client, userId) { }); } +/** + * Enforces retention policies + */ +async function enforceRetention(client) { + const storageConfig = client.configurations['ping-protection']['storage']; + if (!storageConfig) return; + + if (storageConfig.enablePingHistory) { + const historyWeeks = storageConfig.pingHistoryRetention || 12; + const historyCutoff = new Date(); + historyCutoff.setDate(historyCutoff.getDate() - (historyWeeks * 7)); + + await client.models['ping-protection']['PingHistory'].destroy({ + where: { timestamp: { [Op.lt]: historyCutoff } } + }); + } + + if (storageConfig.enableModLogHistory) { + const modMonths = storageConfig.modLogRetention || 6; + const modCutoff = new Date(); + modCutoff.setMonth(modCutoff.getMonth() - modMonths); + + await client.models['ping-protection']['ModerationLog'].destroy({ + where: { timestamp: { [Op.lt]: modCutoff } } + }); + } + + if (storageConfig.enableLeaverDataRetention) { + const leaverDays = storageConfig.leaverRetention || 1; + const leaverCutoff = new Date(); + leaverCutoff.setDate(leaverCutoff.getDate() - leaverDays); + + const leaversToDelete = await client.models['ping-protection']['LeaverData'].findAll({ + where: { leftAt: { [Op.lt]: leaverCutoff } } + }); + + for (const leaver of leaversToDelete) { + const userId = leaver.userId; + await client.models['ping-protection']['PingHistory'].destroy({ where: { userId } }); + await client.models['ping-protection']['ModerationLog'].destroy({ where: { userId } }); + await leaver.destroy(); + + client.logger.debug('[ping-protection] ' + localize('ping-protection', 'log-cleanup-finished', { u: userId })); + } + } +} + module.exports = { addPing, getPingCountInWindow, @@ -193,5 +250,6 @@ module.exports = { deleteAllUserData, getLeaverStatus, markUserAsLeft, - markUserAsRejoined + markUserAsRejoined, + enforceRetention }; \ No newline at end of file From 24dc3b2a63e3d5739e3d1b22311f8bbc7a001042 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sun, 14 Dec 2025 10:15:17 +0100 Subject: [PATCH 12/31] Completed the full module and fixed some critical bugs that caused the bot to crash --- locales/de.json | 40 +++- locales/en.json | 40 +++- .../commands/ping-protection-cmd.js | 226 ++++++++++++++++++ .../ping-protection/configs/moderation.json | 96 ++------ modules/ping-protection/configs/storage.json | 48 +--- modules/ping-protection/events/botReady.js | 7 +- .../events/interactionCreate.js | 113 +++++++++ .../ping-protection/events/messageCreate.js | 4 +- modules/ping-protection/module.json | 2 +- 9 files changed, 453 insertions(+), 123 deletions(-) create mode 100644 modules/ping-protection/commands/ping-protection-cmd.js create mode 100644 modules/ping-protection/events/interactionCreate.js diff --git a/locales/de.json b/locales/de.json index 320d9b3e..022fdc78 100644 --- a/locales/de.json +++ b/locales/de.json @@ -982,5 +982,43 @@ "nicknames": { "owner-cannot-be-renamed": "Der Serverbesitzer (%u) kann nicht umbenannt werden.", "nickname-error": "Fehler beim Ändern des Nicknamens von %u: %e" - } + }, + "ping-protection": { + "log-cleanup-finished": "Datenschutz-Bereinigung: Daten für abgelaufenen Leaver %u gelöscht", + "log-action-mute": "%u für %t Min gemutet. Grund: %r", + "log-action-kick": "%u gekickt. Grund: %r", + "log-manual-delete": "Alle Daten für %u (%i) wurden erfolgreich gelöscht.", + "reason-basic": "Benutzer hat %c Pings in den letzten %w Wochen erreicht (Basis-Limit).", + "reason-advanced": "Benutzer hat %c daily Pings in den letzten %w Wochen erreicht (Erweitertes Limit).", + "cmd-desc-module": "Verwalte das Ping-Schutz-System", + "cmd-desc-group-user": "Alle Befehle bezüglich Benutzer", + "cmd-desc-history": "Sieh dir den Ping-Verlauf eines Benutzers an", + "cmd-opt-user": "Der zu prüfende Benutzer", + "cmd-desc-actions": "Sieh dir den Verlauf der Moderationsaktionen eines Benutzers an", + "cmd-desc-panel": "Admin: Öffne das Benutzerverwaltungs-Panel", + "cmd-desc-group-list": "Listet geschützte oder geweißlistete Entitäten auf", + "cmd-desc-list-users": "Listet alle geschützten Mitglieder auf", + "cmd-desc-list-roles": "Listet alle geschützten Rollen auf", + "cmd-desc-list-white": "Listet alle erlaubten Rollen/Mitglieder auf", + "embed-history-title": "Ping-Verlauf: %u", + "embed-leaver-warning": "Dieser Benutzer hat den Server um %t verlassen. Diese Logs bleiben bis zur automatischen Löschung.", + "no-data-found": "Keine Logs gefunden.", + "embed-actions-title": "Moderations-Verlauf: %u", + "label-reason": "Grund", + "actions-retention-note": "ACHTUNG! MODERATIONSAKTIONEN WERDEN FÜR 3 - 12 MONATE GESPEICHERT!", + "no-permission": "Du hast keine Berechtigung, dies zu nutzen.", + "panel-title": "Benutzer-Panel: %u", + "panel-description": "Verwalte Daten für %u (%i). Benutze die Buttons unten.", + "btn-history": "Ping-Verlauf", + "btn-actions": "Aktions-Verlauf", + "btn-delete": "Alle daten löschen (Riskant)", + "list-roles-title": "Geschützte rollen", + "list-members-title": "Geschützte mitglieder", + "list-whitelist-title": "Erlaubte rollen", + "list-empty": "Keine konfiguriert.", + "modal-title": "Bestätigen Sie die Löschung der Daten für diesen Benutzer", + "modal-label": "Bestätigen Sie das Löschen der Daten, indem Sie diesen Satz eingeben:", + "modal-phrase": "Ich verstehe, dass alle Daten für diesen Benutzer gelöscht werden und dass diese Aktion unumkehrbar ist.", + "modal-failed": "Der von Ihnen eingegebene Satz ist falsch. Datenlöschung abgebrochen." + } } diff --git a/locales/en.json b/locales/en.json index 4cf4437e..bb482d57 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1017,5 +1017,43 @@ "nicknames": { "owner-cannot-be-renamed": "The owner of the server (%u) cannot be renamed.", "nickname-error": "An error occurred while trying to change the nickname of %u: %e" - } + }, + "ping-protection": { + "log-cleanup-finished": "History Cleanup: Deleted data for expired leaver %u", + "log-action-mute": "Muted %u for %t mins. Reason: %r", + "log-action-kick": "Kicked %u. Reason: %r", + "log-manual-delete": "All data for %u (%i) has been deleted successfully.", + "reason-basic": "User reached %c pings in the last %w weeks (Basic Configuration).", + "reason-advanced": "User reached %c pings in the last %w weeks (Advanced Configuration).", + "cmd-desc-module": "Manage the ping protection system", + "cmd-desc-group-user": "Every command related to the users", + "cmd-desc-history": "View the ping history of a user", + "cmd-opt-user": "The user to check", + "cmd-desc-actions": "View the moderation action history of a user", + "cmd-desc-panel": "Admin: Open the user management panel", + "cmd-desc-group-list": "Lists protected or whitelisted entities", + "cmd-desc-list-users": "List all protected members", + "cmd-desc-list-roles": "List all protected roles", + "cmd-desc-list-white": "List all whitelisted roles/members", + "embed-history-title": "Ping History: %u", + "embed-leaver-warning": "This user left the server at %t. These logs will stay until automatic deletion.", + "no-data-found": "No logs found.", + "embed-actions-title": "Moderation History: %u", + "label-reason": "Reason", + "actions-retention-note": "REMEMBER! MODERATION ACTIONS ARE KEPT IN STORAGE FOR 3 - 12 MONTHS!", + "no-permission": "You do not have permission to use this.", + "panel-title": "User Panel: %u", + "panel-description": "Manage data for %u (%i). Use the buttons below.", + "btn-history": "Ping history", + "btn-actions": "Actions history", + "btn-delete": "Delete all data (Risky)", + "list-roles-title": "Protected roles", + "list-members-title": "Protected members", + "list-whitelist-title": "Whitelisted roles", + "list-empty": "None configured.", + "modal-title": "Confirm data deletion for this user", + "modal-label": "Confirm data deletion by typing this phrase:", + "modal-phrase": "I understand that all data will be deleted for this user and that this action is irreversible.", + "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled." + } } \ No newline at end of file diff --git a/modules/ping-protection/commands/ping-protection-cmd.js b/modules/ping-protection/commands/ping-protection-cmd.js new file mode 100644 index 00000000..8cff4389 --- /dev/null +++ b/modules/ping-protection/commands/ping-protection-cmd.js @@ -0,0 +1,226 @@ +const { + fetchPingHistory, + fetchModHistory, + getLeaverStatus +} = require('../ping-protection'); +const { + formatDate +} = require('../../../src/functions/helpers'); +const { localize } = require('../../../src/functions/localize'); +const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); + +// 1. Command Configuration (for SCNX internal loader) +// We embed the entire command structure (groups/options) inside the 'config' object. +module.exports.config = { + name: 'ping-protection', + description: localize('ping-protection', 'cmd-desc-module'), + usage: '/ping-protection', + type: 'slash', + + // The entire Discord API command structure goes here, mirroring the 'options' array from before. + options: [ + // --- GROUP: USER --- + { + type: 'SUB_COMMAND_GROUP', + name: 'user', + description: localize('ping-protection', 'cmd-desc-group-user'), + options: [ + { + type: 'SUB_COMMAND', + name: 'history', + description: localize('ping-protection', 'cmd-desc-history'), + options: [{ + type: 'USER', + name: 'user', + description: localize('ping-protection', 'cmd-opt-user'), + required: true + }] + }, + { + type: 'SUB_COMMAND', + name: 'actions-history', + description: localize('ping-protection', 'cmd-desc-actions'), + options: [{ + type: 'USER', + name: 'user', + description: localize('ping-protection', 'cmd-opt-user'), + required: true + }] + }, + { + type: 'SUB_COMMAND', + name: 'panel', + description: localize('ping-protection', 'cmd-desc-panel'), + options: [{ + type: 'USER', + name: 'user', + description: localize('ping-protection', 'cmd-opt-user'), + required: true + }] + } + ] + }, + // --- GROUP: LIST --- + { + type: 'SUB_COMMAND_GROUP', + name: 'list', + description: localize('ping-protection', 'cmd-desc-group-list'), + options: [ + { + type: 'SUB_COMMAND', + name: 'users', + description: localize('ping-protection', 'cmd-desc-list-users') + }, + { + type: 'SUB_COMMAND', + name: 'roles', + description: localize('ping-protection', 'cmd-desc-list-roles') + }, + { + type: 'SUB_COMMAND', + name: 'whitelisted', + description: localize('ping-protection', 'cmd-desc-list-white') + } + ] + } + ] +}; + +// 2. Execution Function - Must be named 'run' for SCNX +module.exports.run = async function (interaction) { + if (!interaction.guild) return; + + // FIX: Get subcommand group and subcommand without crashing if they don't exist + const group = interaction.options.getSubcommandGroup(false); + const subCmd = interaction.options.getSubcommand(false); + + // FIX: Correct the config file name key + const config = interaction.client.configurations['ping-protection']['configuration']; + + // FINAL FALLBACK: If command structure is missing, inform the user (better than silence) + if (!config || !group || !subCmd) { + return interaction.reply({ + content: "⚠️ Command Structure Error (Synching issue). Please ensure the module is enabled and try reloading your Discord client (CTRL+R).", + ephemeral: true + }); + } + + const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || + (interaction.client.config.admins || []).includes(interaction.user.id); + + // GROUP: USER + if (group === 'user') { + const user = interaction.options.getUser('user'); + + if (subCmd === 'history') { + const history = await fetchPingHistory(interaction.client, user.id, 10); + const leaverData = await getLeaverStatus(interaction.client, user.id); + + const embed = new MessageEmbed() + .setTitle(localize('ping-protection', 'embed-history-title', { u: user.username })) + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .setColor('ORANGE'); + + let description = ""; + + if (leaverData) { + description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { + t: formatDate(leaverData.leftAt) + })}\n\n`; + } + + if (history.length === 0) { + description += localize('ping-protection', 'no-data-found'); + } else { + const lines = history.map((entry, index) => { + return `${index + 1}. **[${formatDate(entry.timestamp)}]** [Jump to Message](${entry.messageUrl})`; + }); + description += lines.join('\n'); + } + + embed.setDescription(description); + await interaction.reply({ embeds: [embed], ephemeral: true }); + } + + else if (subCmd === 'actions-history') { + const history = await fetchModHistory(interaction.client, user.id, 10); + + const embed = new MessageEmbed() + .setTitle(localize('ping-protection', 'embed-actions-title', { u: user.username })) + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .setColor('RED'); + + if (history.length === 0) { + embed.setDescription(localize('ping-protection', 'no-data-found')); + } else { + const lines = history.map((entry, index) => { + const duration = entry.actionDuration ? ` (${Math.round(entry.actionDuration / 60000)}m)` : ''; + return `${index + 1}. **${entry.actionType}${duration}** - ${formatDate(entry.timestamp)}\n${localize('ping-protection', 'label-reason')}: ${entry.reason}`; + }); + embed.setDescription(lines.join('\n\n') + `\n\n*${localize('ping-protection', 'actions-retention-note')}*`); + } + + await interaction.reply({ embeds: [embed], ephemeral: true }); + } + + else if (subCmd === 'panel') { + if (!isAdmin) { + return interaction.reply({ + content: localize('ping-protection', 'no-permission'), + ephemeral: true + }); + } + + const user = interaction.options.getUser('user'); + + const embed = new MessageEmbed() + .setTitle(localize('ping-protection', 'panel-title', { u: user.tag })) + .setDescription(localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id })) + .setColor('BLUE') + .setThumbnail(user.displayAvatarURL({ dynamic: true })); + + const row = new MessageActionRow().addComponents( + new MessageButton() + .setCustomId(`ping-protection_history_${user.id}`) + .setLabel(localize('ping-protection', 'btn-history')) + .setStyle('SECONDARY'), + new MessageButton() + .setCustomId(`ping-protection_actions_${user.id}`) + .setLabel(localize('ping-protection', 'btn-actions')) + .setStyle('SECONDARY'), + new MessageButton() + .setCustomId(`ping-protection_delete_${user.id}`) + .setLabel(localize('ping-protection', 'btn-delete')) + .setStyle('DANGER') + ); + + await interaction.reply({ embeds: [embed], components: [row], ephemeral: true }); + } + } + + // GROUP: LIST + else if (group === 'list') { + let contentList = []; + let title = ""; + + if (subCmd === 'roles') { + title = localize('ping-protection', 'list-roles-title'); + contentList = config.protectedRoles.map(id => `<@&${id}>`); + } else if (subCmd === 'users') { + title = localize('ping-protection', 'list-members-title'); + contentList = config.protectedUsers.map(id => `<@${id}>`); + } else if (subCmd === 'whitelisted') { + title = localize('ping-protection', 'list-whitelist-title'); + contentList = config.ignoredRoles.map(id => `<@&${id}>`); + } + + if (contentList.length === 0) contentList = [localize('ping-protection', 'list-empty')]; + + const embed = new MessageEmbed() + .setTitle(title) + .setDescription(contentList.join('\n')) + .setColor('GREEN'); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + } +}; \ No newline at end of file diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index 302f365a..ba3a82d4 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -3,7 +3,7 @@ "humanName": { "en": "Moderation Actions", "de": "Bestrafungsaktionen" -}, + }, "description": { "en": "Define triggers for punishments.", "de": "Definiere Auslöser für Bestrafungen." @@ -12,114 +12,58 @@ "content": [ { "name": "enableModeration", - "humanName": { - "en": "Enable Moderation Actions", - "de": "Bestrafungsaktionen aktivieren" - }, - "description": { - "en": "If enabled, members who ping protected users/roles repeatedly will be punished.", - "de": "Wenn aktiviert, werden Mitglieder, die geschützte Benutzer/Rollen wiederholt pingen, bestraft." - }, + "humanName": { "en": "Enable Moderation Actions", "de": "Bestrafungsaktionen aktivieren" }, + "description": { "en": "If enabled, members who ping protected users/roles repeatedly will be punished.", "de": "Wenn aktiviert, werden Mitglieder, die geschützte Benutzer/Rollen wiederholt pingen, bestraft." }, "type": "boolean", "default": { "en": false } }, { "name": "advancedConfiguration", - "humanName": { - "en": "Use Advanced Configuration", - "de": "Erweiterte Konfiguration verwenden" - }, - "description": { - "en": "If enabled, the advanced configuration below will be used and the basic configuration will be ignored.", - "de": "Wenn aktiviert, wird die untenstehende erweiterte Konfiguration verwendet und die Basis-Konfiguration ignoriert." - }, + "humanName": { "en": "Use Advanced Configuration", "de": "Erweiterte Konfiguration verwenden" }, + "description": { "en": "If enabled, the advanced configuration below will be used and the basic configuration will be ignored.", "de": "Wenn aktiviert, wird die untenstehende erweiterte Konfiguration verwendet und die Basis-Konfiguration ignoriert." }, "type": "boolean", "default": { "en": false } }, { "name": "pingsCountBasic", - "humanName": { - "en": "Pings to trigger moderation", - "de": "Pings für Bestrafung" - }, - "description": { - "en": "The amount of pings required to trigger a moderation action (Uses 'Ping History Retention' timeframe).", - "de": "Anzahl Pings für Bestrafung (Nutzt 'Ping-Verlauf Speicherzeit')." - }, + "humanName": { "en": "Pings to trigger moderation", "de": "Pings für Bestrafung" }, + "description": { "en": "The amount of pings required to trigger a moderation action (Uses 'Ping History Retention' timeframe).", "de": "Anzahl Pings für Bestrafung (Nutzt 'Ping-Verlauf Speicherzeit')." }, "type": "integer", "default": { "en": 5 }, - "dependsOn": { - "name": "enableModeration", - "value": true - } + "dependsOn": "enableModeration" }, { "name": "pingsCountAdvanced", - "humanName": { - "en": "Pings to trigger (Advanced)", - "de": "Pings für Bestrafung (Erweitert)" - }, - "description": { - "en": "The amount of pings required in the custom timeframe below.", - "de": "Anzahl Pings im unten gewählten Zeitraum." - }, + "humanName": { "en": "Pings to trigger (Advanced)", "de": "Pings für Bestrafung (Erweitert)" }, + "description": { "en": "The amount of pings required in the custom timeframe below.", "de": "Anzahl Pings im unten gewählten Zeitraum." }, "type": "integer", "default": { "en": 5 }, - "dependsOn": { - "name": "advancedConfiguration", - "value": true - } + "dependsOn": "advancedConfiguration" }, { "name": "timeframeWeeks", - "humanName": { - "en": "Timeframe (Weeks)", - "de": "Zeitraum (Wochen)" - }, - "description": { - "en": "In how many weeks must these pings occur?", - "de": "In wie vielen Wochen müssen diese Pings passieren?" - }, + "humanName": { "en": "Timeframe (Weeks)", "de": "Zeitraum (Wochen)" }, + "description": { "en": "In how many weeks must these pings occur?", "de": "In wie vielen Wochen müssen diese Pings passieren?" }, "type": "integer", "default": { "en": 1 }, - "dependsOn": { - "name": "advancedConfiguration", - "value": true - } + "dependsOn": "advancedConfiguration" }, { "name": "actionType", - "humanName": { - "en": "Action", - "de": "Aktion" }, - "description": { - "en": "What punishment should be applied?", - "de": "Welche Strafe soll verhängt werden?" - }, + "humanName": { "en": "Action", "de": "Aktion" }, + "description": { "en": "What punishment should be applied?", "de": "Welche Strafe soll verhängt werden?" }, "type": "select", "content": ["MUTE", "KICK"], "default": { "en": "MUTE" }, - "dependsOn": { - "name": "enableModeration", - "value": true - } + "dependsOn": "enableModeration" }, { "name": "muteDuration", - "humanName": { - "en": "Mute Duration", - "de": "Mute-Dauer" - }, - "description": { - "en": "How long to mute the user? (in minutes)", - "de": "Wie lange soll der User gemutet werden? (in Minuten)" - }, + "humanName": { "en": "Mute Duration (only if action type is MUTE)", "de": "Mute-Dauer (nur wenn Aktionstyp MUTE ist)" }, + "description": { "en": "How long to mute the user? (in minutes)", "de": "Wie lange soll der User gemutet werden? (in Minuten)" }, "type": "integer", "default": { "en": 60 }, - "dependsOn": { - "name": "actionType", - "value": "MUTE" - } + "dependsOn": "enableModeration" } ] } \ No newline at end of file diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json index 3e926141..4c698bdc 100644 --- a/modules/ping-protection/configs/storage.json +++ b/modules/ping-protection/configs/storage.json @@ -8,23 +8,14 @@ "content": [ { "name": "enablePingHistory", - "humanName": { - "en": "Enable Ping History", - "de": "Ping-Verlauf aktivieren" - }, - "description": { - "en": "If enabled, the bot will keep a history of pings to enforce moderation actions.", - "de": "Wenn aktiviert, speichert der Bot einen Ping-Verlauf, um Bestrafungsaktionen durchzusetzen." - }, + "humanName": { "en": "Enable Ping History", "de": "Ping-Verlauf aktivieren" }, + "description": { "en": "If enabled, the bot will keep a history of pings to enforce moderation actions.", "de": "Wenn aktiviert, speichert der Bot einen Ping-Verlauf, um Bestrafungsaktionen durchzusetzen." }, "type": "boolean", "default": { "en": true } }, { "name": "pingHistoryRetention", - "humanName": { - "en": "Ping History Retention", - "de": "Ping-Verlauf Speicherzeit" - }, + "humanName": { "en": "Ping History Retention", "de": "Ping-Verlauf Speicherzeit" }, "description": { "en": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 24 weeks (6 months). This is the length factor of the 'Basic' punishment timeframe.", "de": "Entscheidet, wie lange Ping-Logs gespeichert werden. Minimum 1 Woche, Maximum 24 Wochen. Dies ist der Faktor für den 'Basis'-Bestrafungszeitraum." @@ -32,21 +23,12 @@ "type": "select", "content": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], "default": { "en": 12 }, - "dependsOn": { - "name": "enablePingHistory", - "value": true - } + "dependsOn": "enablePingHistory" }, { "name": "enableModLogHistory", - "humanName": { - "en": "Enable Moderation Log History", - "de": "Mod-Log Verlauf aktivieren" - }, - "description": { - "en": "If enabled, the bot will keep a history of moderation actions taken by the ping-protection module.", - "de": "Wenn aktiviert, speichert der Bot einen Verlauf der Bestrafungsaktionen des Ping-Schutz Moduls." - }, + "humanName": { "en": "Enable Moderation Log History", "de": "Mod-Log Verlauf aktivieren" }, + "description": { "en": "If enabled, the bot will keep a history of moderation actions taken by the ping-protection module.", "de": "Wenn aktiviert, speichert der Bot einen Verlauf der Bestrafungsaktionen des Ping-Schutz Moduls." }, "type": "boolean", "default": { "en": false } }, @@ -60,21 +42,12 @@ "type": "select", "content": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "default": { "en": 6 }, - "dependsOn": { - "name": "enableModLogHistory", - "value": true - } + "dependsOn": "enableModLogHistory" }, { "name": "enableLeaverDataRetention", - "humanName": { - "en": "Keep user logs after they leave", - "de": "Benutzerprotokolle nach Verlassen speichern" - }, - "description": { - "en": "If enabled, the bot will keep a history of the user after they leave.", - "de": "Wenn aktiviert, speichert der Bot einen Verlauf des Benutzers, nachdem er den Server verlassen hat." - }, + "humanName": { "en": "Keep user logs after they leave", "de": "Benutzerprotokolle nach Verlassen speichern" }, + "description": { "en": "If enabled, the bot will keep a history of the user after they leave.", "de": "Wenn aktiviert, speichert der Bot einen Verlauf des Benutzers, nachdem er den Server verlassen hat." }, "type": "boolean", "default": { "en": true } }, @@ -87,7 +60,8 @@ }, "type": "select", "content": [1, 2, 3, 4, 5, 6, 7], - "default": { "en": 1 } + "default": { "en": 1 }, + "dependsOn": "enableLeaverDataRetention" } ] } \ No newline at end of file diff --git a/modules/ping-protection/events/botReady.js b/modules/ping-protection/events/botReady.js index cd2c4b3f..72250b6b 100644 --- a/modules/ping-protection/events/botReady.js +++ b/modules/ping-protection/events/botReady.js @@ -1,6 +1,6 @@ const { enforceRetention } = require('../ping-protection'); const schedule = require('node-schedule'); -const { localize } = require('../../src/functions/localize'); +const { localize } = require('../../../src/functions/localize'); module.exports.run = async function (client) { try { @@ -8,15 +8,12 @@ module.exports.run = async function (client) { await client.models['ping-protection']['ModerationLog'].sync(); await client.models['ping-protection']['LeaverData'].sync(); - client.logger.debug('[ping-protection] ' + localize('ping-protection', 'log-db-synced')); } catch (e) { - client.logger.error('[ping-protection] Failed to sync database models: ' + e); } - // Run Retention Checks await enforceRetention(client); - // Schedule Retention Job (03:00 daily via cronjob) + // Schedules daily retention at 03:00 local bot time with cronjob const job = schedule.scheduleJob('0 3 * * *', async () => { await enforceRetention(client); }); diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js new file mode 100644 index 00000000..d7c02070 --- /dev/null +++ b/modules/ping-protection/events/interactionCreate.js @@ -0,0 +1,113 @@ +const { Modal, TextInputComponent, MessageActionRow, MessageEmbed } = require('discord.js'); +const { fetchPingHistory, fetchModHistory, deleteAllUserData, getLeaverStatus } = require('../ping-protection'); +const { localize } = require('../../../src/functions/localize'); +const { formatDate } = require('../../../src/functions/helpers'); + +module.exports.run = async function (client, interaction) { + if (!client.botReadyAt) return; + + // Handles embed buttons + if (interaction.isButton() && interaction.customId.startsWith('ping-protection_')) { + const [prefix, action, userId] = interaction.customId.split('_'); + + const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || + (client.config.admins || []).includes(interaction.user.id); + + if (!isAdmin) { + return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); + } + + if (action === 'history') { + const history = await fetchPingHistory(client, userId, 10); + const leaverData = await getLeaverStatus(client, userId); + const user = interaction.client.users.cache.get(userId) || { username: userId, displayAvatarURL: () => null }; // Fallback user object + + const embed = new MessageEmbed() + .setTitle(localize('ping-protection', 'embed-history-title', { u: user.username })) + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .setColor('ORANGE'); + + let description = ""; + + if (leaverData) { + description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { + t: formatDate(leaverData.leftAt) + })}\n\n`; + } + + if (history.length === 0) { + description += localize('ping-protection', 'no-data-found'); + } else { + const lines = history.map((entry, index) => { + return `${index + 1}. **[${formatDate(entry.timestamp)}]** [Jump to Message](${entry.messageUrl})`; + }); + description += lines.join('\n'); + } + + embed.setDescription(description); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + } + + else if (action === 'actions') { + const history = await fetchModHistory(client, userId, 10); + const user = interaction.client.users.cache.get(userId) || { username: userId, displayAvatarURL: () => null }; // Fallback user object + + const embed = new MessageEmbed() + .setTitle(localize('ping-protection', 'embed-actions-title', { u: user.username })) + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .setColor('RED'); + + if (history.length === 0) { + embed.setDescription(localize('ping-protection', 'no-data-found')); + } else { + const lines = history.map((entry, index) => { + const duration = entry.actionDuration ? ` (${Math.round(entry.actionDuration / 60000)}m)` : ''; + return `${index + 1}. **${entry.actionType}${duration}** - ${formatDate(entry.timestamp)}\n${localize('ping-protection', 'label-reason')}: ${entry.reason}`; + }); + embed.setDescription(lines.join('\n\n') + `\n\n*${localize('ping-protection', 'actions-retention-note')}*`); + } + + await interaction.reply({ embeds: [embed], ephemeral: true }); + } + + else if (action === 'delete') { + const modal = new Modal() + .setCustomId(`ping-protection_confirm-delete_${userId}`) + .setTitle(localize('ping-protection', 'modal-title')); + + const input = new TextInputComponent() + .setCustomId('confirmation_text') + .setLabel(localize('ping-protection', 'modal-label')) + .setStyle('PARAGRAPH') + .setPlaceholder(localize('ping-protection', 'modal-phrase')) + .setRequired(true); + + const row = new MessageActionRow().addComponents(input); + modal.addComponents(row); + + await interaction.showModal(modal); + } + } + + // Handles modal submission + if (interaction.isModalSubmit() && interaction.customId.startsWith('ping-protection_confirm-delete_')) { + const userId = interaction.customId.split('_')[2]; + const userInput = interaction.fields.getTextInputValue('confirmation_text'); + + const requiredPhrase = localize('ping-protection', 'modal-phrase', { locale: interaction.locale }); // IMPORTANT: Use user's locale + + if (userInput === requiredPhrase) { + await deleteAllUserData(client, userId); + await interaction.reply({ + content: `✅ ${localize('ping-protection', 'log-manual-delete', {u: userId})}`, + ephemeral: true + }); + } else { + await interaction.reply({ + content: `❌ ${localize('ping-protection', 'modal-failed')}`, + ephemeral: true + }); + } + } +}; \ No newline at end of file diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index a110155a..c2af2b07 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -4,7 +4,7 @@ const { executeAction, sendPingWarning } = require('../ping-protection'); -const { localize } = require('../../src/functions/localize'); +const { localize } = require('../../../src/functions/localize'); module.exports.run = async function (client, message) { if (!client.botReadyAt) return; @@ -12,7 +12,7 @@ module.exports.run = async function (client, message) { if (message.author.bot) return; if (message.guild.id !== client.config.guildID) return; - const config = client.configurations['ping-protection']['config']; + const config = client.configurations['ping-protection']['configuration']; const storageConfig = client.configurations['ping-protection']['storage']; const moderationRules = client.configurations['ping-protection']['moderation']; diff --git a/modules/ping-protection/module.json b/modules/ping-protection/module.json index 4fbce23c..b945a1c7 100644 --- a/modules/ping-protection/module.json +++ b/modules/ping-protection/module.json @@ -10,7 +10,7 @@ "events-dir": "/events", "models-dir": "/models", "config-example-files": [ - "configs/configuration.js", + "configs/configuration.json", "configs/moderation.json", "configs/storage.json" ], From 01660ad2ac333c0896907dae4c5ed72f4ea5ee86 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sun, 14 Dec 2025 10:24:36 +0100 Subject: [PATCH 13/31] Cleaned up some code notes I used for debugging --- .../commands/ping-protection-cmd.js | 24 +++++-------------- modules/ping-protection/ping-protection.js | 20 ++++++++-------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/modules/ping-protection/commands/ping-protection-cmd.js b/modules/ping-protection/commands/ping-protection-cmd.js index 8cff4389..0165fb8a 100644 --- a/modules/ping-protection/commands/ping-protection-cmd.js +++ b/modules/ping-protection/commands/ping-protection-cmd.js @@ -9,17 +9,15 @@ const { const { localize } = require('../../../src/functions/localize'); const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); -// 1. Command Configuration (for SCNX internal loader) -// We embed the entire command structure (groups/options) inside the 'config' object. +// Command configuration module.exports.config = { name: 'ping-protection', description: localize('ping-protection', 'cmd-desc-module'), usage: '/ping-protection', type: 'slash', - // The entire Discord API command structure goes here, mirroring the 'options' array from before. options: [ - // --- GROUP: USER --- + // Group: user { type: 'SUB_COMMAND_GROUP', name: 'user', @@ -60,7 +58,7 @@ module.exports.config = { } ] }, - // --- GROUP: LIST --- + // Group: list { type: 'SUB_COMMAND_GROUP', name: 'list', @@ -86,29 +84,19 @@ module.exports.config = { ] }; -// 2. Execution Function - Must be named 'run' for SCNX +// Execution function module.exports.run = async function (interaction) { if (!interaction.guild) return; - // FIX: Get subcommand group and subcommand without crashing if they don't exist const group = interaction.options.getSubcommandGroup(false); const subCmd = interaction.options.getSubcommand(false); - // FIX: Correct the config file name key const config = interaction.client.configurations['ping-protection']['configuration']; - // FINAL FALLBACK: If command structure is missing, inform the user (better than silence) - if (!config || !group || !subCmd) { - return interaction.reply({ - content: "⚠️ Command Structure Error (Synching issue). Please ensure the module is enabled and try reloading your Discord client (CTRL+R).", - ephemeral: true - }); - } - const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || (interaction.client.config.admins || []).includes(interaction.user.id); - // GROUP: USER + // Group: user if (group === 'user') { const user = interaction.options.getUser('user'); @@ -198,7 +186,7 @@ module.exports.run = async function (interaction) { } } - // GROUP: LIST + // Group: list else if (group === 'list') { let contentList = []; let title = ""; diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 957f5ded..04334a20 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -8,7 +8,7 @@ const { embedType, formatDiscordUserName } = require('../../src/functions/helper const { localize } = require('../../src/functions/localize'); /** - * Adds a ping record to the database. + * Adds a ping record to the database * @param {Client} client * @param {Message} message */ @@ -20,7 +20,7 @@ async function addPing(client, message) { } /** - * Counts pings within a specific timeframe (in weeks). + * Counts pings within a specific timeframe * @param {Client} client * @param {string} userId * @param {number} weeks @@ -41,7 +41,7 @@ async function getPingCountInWindow(client, userId, weeks) { } /** - * Sends the warning message to the user who pinged. + * Sends the warning message * @param {Client} client * @param {Message} message * @param {Role|User} target @@ -68,7 +68,7 @@ async function sendPingWarning(client, message, target, moduleConfig) { } /** - * Fetches the last X pings. + * Fetches the last X pings * @param {Client} client * @param {string} userId * @param {number} limit @@ -83,7 +83,7 @@ async function fetchPingHistory(client, userId, limit = 10) { } /** - * Fetches the moderation log history. + * Fetches the moderation log history * @param {Client} client * @param {string} userId * @param {number} limit @@ -98,7 +98,7 @@ async function fetchModHistory(client, userId, limit = 10) { } /** - * Executes a punishment and optionally logs it. + * Executes a punishment and logs it if configured * @param {Client} client * @param {GuildMember} member * @param {Object} actionConfig @@ -152,7 +152,7 @@ async function executeAction(client, member, actionConfig, reason, storageConfig } /** - * Deletes ALL database information for a user. + * Deletes ALL database information from a user * @param {Client} client * @param {string} userId */ @@ -165,7 +165,7 @@ async function deleteAllUserData(client, userId) { } /** - * Checks if a user is currently marked as left. + * Checks if a user is currently marked as left * @param {Client} client * @param {string} userId * @returns {Promise} @@ -175,7 +175,7 @@ async function getLeaverStatus(client, userId) { } /** - * Marks user as left. + * Marks user as left */ async function markUserAsLeft(client, userId) { await client.models['ping-protection']['LeaverData'].upsert({ @@ -185,7 +185,7 @@ async function markUserAsLeft(client, userId) { } /** - * Handles rejoin. + * Handles rejoin */ async function markUserAsRejoined(client, userId) { await client.models['ping-protection']['LeaverData'].destroy({ From 93eb5a3356c603882f9d03ef2fd094476df8cab8 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sun, 14 Dec 2025 22:00:30 +0100 Subject: [PATCH 14/31] Completely finished the module and worked tirelessly for many hours to debug code, has been tested and is currently ongoing extensive testing to ensure absolutely everythig works as supposed to --- locales/de.json | 9 +- locales/en.json | 9 +- .../commands/ping-protection-cmd.js | 216 ++++++++---------- .../configs/configuration.json | 2 + .../ping-protection/configs/moderation.json | 3 +- .../events/interactionCreate.js | 156 +++++++++---- .../ping-protection/events/messageCreate.js | 125 +++++----- .../ping-protection/models/ModerationLog.js | 26 ++- modules/ping-protection/models/PingHistory.js | 10 +- modules/ping-protection/ping-protection.js | 210 ++++++++--------- 10 files changed, 400 insertions(+), 366 deletions(-) diff --git a/locales/de.json b/locales/de.json index 022fdc78..48a6726d 100644 --- a/locales/de.json +++ b/locales/de.json @@ -987,7 +987,8 @@ "log-cleanup-finished": "Datenschutz-Bereinigung: Daten für abgelaufenen Leaver %u gelöscht", "log-action-mute": "%u für %t Min gemutet. Grund: %r", "log-action-kick": "%u gekickt. Grund: %r", - "log-manual-delete": "Alle Daten für %u (%i) wurden erfolgreich gelöscht.", + "log-manual-delete": "Alle Daten für <@%u> (%u) wurden erfolgreich gelöscht.", + "log-manual-delete-logs": "Alle Daten für Benutzer mit der ID %u wurden erfolgreich gelöscht.", "reason-basic": "Benutzer hat %c Pings in den letzten %w Wochen erreicht (Basis-Limit).", "reason-advanced": "Benutzer hat %c daily Pings in den letzten %w Wochen erreicht (Erweitertes Limit).", "cmd-desc-module": "Verwalte das Ping-Schutz-System", @@ -1008,7 +1009,7 @@ "actions-retention-note": "ACHTUNG! MODERATIONSAKTIONEN WERDEN FÜR 3 - 12 MONATE GESPEICHERT!", "no-permission": "Du hast keine Berechtigung, dies zu nutzen.", "panel-title": "Benutzer-Panel: %u", - "panel-description": "Verwalte Daten für %u (%i). Benutze die Buttons unten.", + "panel-description": "Daten für %u (%i) verwalten und anzeigen. Sehen Sie sich eine kurze Zusammenfassung ihres Ping- und Moderationsverlaufs an oder löschen Sie alle für diesen Benutzer gespeicherten Daten (Risky).", "btn-history": "Ping-Verlauf", "btn-actions": "Aktions-Verlauf", "btn-delete": "Alle daten löschen (Riskant)", @@ -1019,6 +1020,8 @@ "modal-title": "Bestätigen Sie die Löschung der Daten für diesen Benutzer", "modal-label": "Bestätigen Sie das Löschen der Daten, indem Sie diesen Satz eingeben:", "modal-phrase": "Ich verstehe, dass alle Daten für diesen Benutzer gelöscht werden und dass diese Aktion unumkehrbar ist.", - "modal-failed": "Der von Ihnen eingegebene Satz ist falsch. Datenlöschung abgebrochen." + "modal-failed": "Der von Ihnen eingegebene Satz ist falsch. Datenlöschung abgebrochen.", + "field-quick-history": "Schnelle Verlaufsansicht (Letzte %w Wochen)", + "field-quick-desc": "Betrag des Pings-Verlaufs: %p\nBetrag der Moderationsaktionen: %m" } } diff --git a/locales/en.json b/locales/en.json index bb482d57..53a497d3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1022,7 +1022,8 @@ "log-cleanup-finished": "History Cleanup: Deleted data for expired leaver %u", "log-action-mute": "Muted %u for %t mins. Reason: %r", "log-action-kick": "Kicked %u. Reason: %r", - "log-manual-delete": "All data for %u (%i) has been deleted successfully.", + "log-manual-delete": "All data for <@%u> (%u) has been deleted successfully.", + "log-manual-delete-logs": "All data for user with ID %u has been deleted successfully.", "reason-basic": "User reached %c pings in the last %w weeks (Basic Configuration).", "reason-advanced": "User reached %c pings in the last %w weeks (Advanced Configuration).", "cmd-desc-module": "Manage the ping protection system", @@ -1043,7 +1044,7 @@ "actions-retention-note": "REMEMBER! MODERATION ACTIONS ARE KEPT IN STORAGE FOR 3 - 12 MONTHS!", "no-permission": "You do not have permission to use this.", "panel-title": "User Panel: %u", - "panel-description": "Manage data for %u (%i). Use the buttons below.", + "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", "btn-history": "Ping history", "btn-actions": "Actions history", "btn-delete": "Delete all data (Risky)", @@ -1054,6 +1055,8 @@ "modal-title": "Confirm data deletion for this user", "modal-label": "Confirm data deletion by typing this phrase:", "modal-phrase": "I understand that all data will be deleted for this user and that this action is irreversible.", - "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled." + "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" } } \ No newline at end of file diff --git a/modules/ping-protection/commands/ping-protection-cmd.js b/modules/ping-protection/commands/ping-protection-cmd.js index 0165fb8a..055c3fce 100644 --- a/modules/ping-protection/commands/ping-protection-cmd.js +++ b/modules/ping-protection/commands/ping-protection-cmd.js @@ -1,192 +1,170 @@ const { fetchPingHistory, fetchModHistory, - getLeaverStatus + getLeaverStatus, + getPingCountInWindow, } = require('../ping-protection'); -const { - formatDate -} = require('../../../src/functions/helpers'); +const { formatDate, embedType } = require('../../../src/functions/helpers'); const { localize } = require('../../../src/functions/localize'); -const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); +const { MessageActionRow, MessageButton } = require('discord.js'); -// Command configuration module.exports.config = { name: 'ping-protection', description: localize('ping-protection', 'cmd-desc-module'), usage: '/ping-protection', type: 'slash', - options: [ - // Group: user { type: 'SUB_COMMAND_GROUP', name: 'user', description: localize('ping-protection', 'cmd-desc-group-user'), options: [ - { - type: 'SUB_COMMAND', - name: 'history', - description: localize('ping-protection', 'cmd-desc-history'), - options: [{ - type: 'USER', - name: 'user', - description: localize('ping-protection', 'cmd-opt-user'), - required: true - }] - }, - { - type: 'SUB_COMMAND', - name: 'actions-history', - description: localize('ping-protection', 'cmd-desc-actions'), - options: [{ - type: 'USER', - name: 'user', - description: localize('ping-protection', 'cmd-opt-user'), - required: true - }] - }, - { - type: 'SUB_COMMAND', - name: 'panel', - description: localize('ping-protection', 'cmd-desc-panel'), - options: [{ - type: 'USER', - name: 'user', - description: localize('ping-protection', 'cmd-opt-user'), - required: true - }] - } + { type: 'SUB_COMMAND', name: 'history', description: localize('ping-protection', 'cmd-desc-history'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] }, + { type: 'SUB_COMMAND', name: 'actions-history', description: localize('ping-protection', 'cmd-desc-actions'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] }, + { type: 'SUB_COMMAND', name: 'panel', description: localize('ping-protection', 'cmd-desc-panel'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] } ] }, - // Group: list { type: 'SUB_COMMAND_GROUP', name: 'list', description: localize('ping-protection', 'cmd-desc-group-list'), options: [ - { - type: 'SUB_COMMAND', - name: 'users', - description: localize('ping-protection', 'cmd-desc-list-users') - }, - { - type: 'SUB_COMMAND', - name: 'roles', - description: localize('ping-protection', 'cmd-desc-list-roles') - }, - { - type: 'SUB_COMMAND', - name: 'whitelisted', - description: localize('ping-protection', 'cmd-desc-list-white') - } + { type: 'SUB_COMMAND', name: 'users', description: localize('ping-protection', 'cmd-desc-list-users') }, + { type: 'SUB_COMMAND', name: 'roles', description: localize('ping-protection', 'cmd-desc-list-roles') }, + { type: 'SUB_COMMAND', name: 'whitelisted', description: localize('ping-protection', 'cmd-desc-list-white') } ] } ] }; -// Execution function +// Commands handler module.exports.run = async function (interaction) { if (!interaction.guild) return; const group = interaction.options.getSubcommandGroup(false); const subCmd = interaction.options.getSubcommand(false); - const config = interaction.client.configurations['ping-protection']['configuration']; - const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || (interaction.client.config.admins || []).includes(interaction.user.id); - // Group: user if (group === 'user') { const user = interaction.options.getUser('user'); + // Subcommand history if (subCmd === 'history') { - const history = await fetchPingHistory(interaction.client, user.id, 10); + const page = 1; + const limit = 8; + const { total, history } = await fetchPingHistory(interaction.client, user.id, page, limit); const leaverData = await getLeaverStatus(interaction.client, user.id); - - const embed = new MessageEmbed() - .setTitle(localize('ping-protection', 'embed-history-title', { u: user.username })) - .setThumbnail(user.displayAvatarURL({ dynamic: true })) - .setColor('ORANGE'); + const totalPages = Math.ceil(total / limit) || 1; let description = ""; - if (leaverData) { - description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { - t: formatDate(leaverData.leftAt) - })}\n\n`; + description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { t: formatDate(leaverData.leftAt) })}\n\n`; } if (history.length === 0) { description += localize('ping-protection', 'no-data-found'); } else { const lines = history.map((entry, index) => { - return `${index + 1}. **[${formatDate(entry.timestamp)}]** [Jump to Message](${entry.messageUrl})`; + const ts = Math.floor(new Date(entry.createdAt).getTime() / 1000); + let targetString = "Unknown"; + if (entry.targetId) { + targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; + } else { + targetString = "Detected"; + } + return `${(page - 1) * limit + index + 1}. **Pinged ${targetString}** at ()\n[Jump to Message](${entry.messageUrl})`; }); - description += lines.join('\n'); + description += lines.join('\n\n'); } - embed.setDescription(description); - await interaction.reply({ embeds: [embed], ephemeral: true }); + // Buttons + const row = new MessageActionRow().addComponents( + new MessageButton().setCustomId(`ping-protection_hist-page_${user.id}_${page - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(true), + new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${page}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), + new MessageButton().setCustomId(`ping-protection_hist-page_${user.id}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(totalPages <= 1) + ); + + const replyOptions = embedType({ + title: localize('ping-protection', 'embed-history-title', { u: user.username }), + thumbnail: user.displayAvatarURL({ dynamic: true }), + description: description, + color: 'ORANGE' + }); + + replyOptions.components = [row]; + replyOptions.ephemeral = false; + + await interaction.reply(replyOptions); } + // Subcommand actions history else if (subCmd === 'actions-history') { - const history = await fetchModHistory(interaction.client, user.id, 10); + const history = await fetchModHistory(interaction.client, user.id, 15); - const embed = new MessageEmbed() - .setTitle(localize('ping-protection', 'embed-actions-title', { u: user.username })) - .setThumbnail(user.displayAvatarURL({ dynamic: true })) - .setColor('RED'); - + let description = ""; if (history.length === 0) { - embed.setDescription(localize('ping-protection', 'no-data-found')); + description = localize('ping-protection', 'no-data-found'); } else { const lines = history.map((entry, index) => { - const duration = entry.actionDuration ? ` (${Math.round(entry.actionDuration / 60000)}m)` : ''; - return `${index + 1}. **${entry.actionType}${duration}** - ${formatDate(entry.timestamp)}\n${localize('ping-protection', 'label-reason')}: ${entry.reason}`; + const duration = entry.actionDuration ? ` (${entry.actionDuration}m)` : ''; + const reasonText = entry.reason || localize('ping-protection', 'no-reason') || 'No reason'; + return `${index + 1}. **${entry.type}${duration}** - ${formatDate(entry.createdAt)}\n${localize('ping-protection', 'label-reason')}: ${reasonText}`; }); - embed.setDescription(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')}*`; } - await interaction.reply({ embeds: [embed], ephemeral: true }); + const replyOptions = embedType({ + title: localize('ping-protection', 'embed-actions-title', { u: user.username }), + thumbnail: user.displayAvatarURL({ dynamic: true }), + description: description, + color: 'RED' + }); + + replyOptions.ephemeral = false; + await interaction.reply(replyOptions); } + // Subcammand panel else if (subCmd === 'panel') { - if (!isAdmin) { - return interaction.reply({ - content: localize('ping-protection', 'no-permission'), - ephemeral: true - }); - } - - const user = interaction.options.getUser('user'); + if (!isAdmin) return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); - const embed = new MessageEmbed() - .setTitle(localize('ping-protection', 'panel-title', { u: user.tag })) - .setDescription(localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id })) - .setColor('BLUE') - .setThumbnail(user.displayAvatarURL({ dynamic: true })); + const user = interaction.options.getUser('user'); + const pingerId = user.id; + const storageConfig = interaction.client.configurations['ping-protection']['storage']; + const timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + + const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeWeeks); + const modHistory = await fetchModHistory(interaction.client, pingerId, 1000); + const modActionCount = modHistory.length; const row = new MessageActionRow().addComponents( - new MessageButton() - .setCustomId(`ping-protection_history_${user.id}`) - .setLabel(localize('ping-protection', 'btn-history')) - .setStyle('SECONDARY'), - new MessageButton() - .setCustomId(`ping-protection_actions_${user.id}`) - .setLabel(localize('ping-protection', 'btn-actions')) - .setStyle('SECONDARY'), - new MessageButton() - .setCustomId(`ping-protection_delete_${user.id}`) - .setLabel(localize('ping-protection', 'btn-delete')) - .setStyle('DANGER') + new MessageButton().setCustomId(`ping-protection_history_${user.id}`).setLabel(localize('ping-protection', 'btn-history')).setStyle('SECONDARY'), + new MessageButton().setCustomId(`ping-protection_actions_${user.id}`).setLabel(localize('ping-protection', 'btn-actions')).setStyle('SECONDARY'), + new MessageButton().setCustomId(`ping-protection_delete_${user.id}`).setLabel(localize('ping-protection', 'btn-delete')).setStyle('DANGER') ); - await interaction.reply({ embeds: [embed], components: [row], ephemeral: true }); + const replyOptions = embedType({ + title: localize('ping-protection', 'panel-title', { u: user.tag }), + description: localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id }), + color: 'BLUE', + thumbnail: user.displayAvatarURL({ dynamic: true }), + fields: [{ + name: localize('ping-protection', 'field-quick-history', {w: timeframeWeeks}), + value: localize('ping-protection', 'field-quick-desc', { p: pingCount, m: modActionCount }), + inline: false + }] + }); + + replyOptions.components = [row]; + replyOptions.ephemeral = false; + + await interaction.reply(replyOptions); } } - // Group: list + // Subcommand group list else if (group === 'list') { let contentList = []; let title = ""; @@ -204,11 +182,13 @@ module.exports.run = async function (interaction) { if (contentList.length === 0) contentList = [localize('ping-protection', 'list-empty')]; - const embed = new MessageEmbed() - .setTitle(title) - .setDescription(contentList.join('\n')) - .setColor('GREEN'); + const replyOptions = embedType({ + title: title, + description: contentList.join('\n'), + color: 'GREEN' + }); - await interaction.reply({ embeds: [embed], ephemeral: true }); + replyOptions.ephemeral = false; + await interaction.reply(replyOptions); } }; \ No newline at end of file diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 5b2e1104..83e67604 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -78,11 +78,13 @@ "en": { "title": "You are not allowed to ping %target-name%!", "description": "You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping protection list roles` or `/ping protection list members`.\n\nIf you were replying, make sure to turn off the mention in the reply.", + "image": "https://c.tenor.com/zRR5cYKSIV0AAAAd/tenor.gif", "color": "RED" }, "de": { "title": "Du darfst %target-name% nicht pingen!", "description": "Du darfst %target-mention% aufgrund deiner Rolle nicht pingen. Du kannst sehen, welche Rollen/Mitglieder du nicht pingen darfst, indem du `/ping protection list roles` oder `/ping protection list members` benutzt.\n\nFalls du geantwortet hast, stelle sicher, dass die Erwähnung in der Antwort deaktiviert ist.", + "image": "https://c.tenor.com/zRR5cYKSIV0AAAAd/tenor.gif", "color": "RED" } } diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index ba3a82d4..56690f4d 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -22,7 +22,8 @@ "humanName": { "en": "Use Advanced Configuration", "de": "Erweiterte Konfiguration verwenden" }, "description": { "en": "If enabled, the advanced configuration below will be used and the basic configuration will be ignored.", "de": "Wenn aktiviert, wird die untenstehende erweiterte Konfiguration verwendet und die Basis-Konfiguration ignoriert." }, "type": "boolean", - "default": { "en": false } + "default": { "en": false }, + "dependsOn": "enableModeration" }, { "name": "pingsCountBasic", diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index d7c02070..1c9ac5c9 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -1,76 +1,148 @@ -const { Modal, TextInputComponent, MessageActionRow, MessageEmbed } = require('discord.js'); +const { Modal, TextInputComponent, MessageActionRow, MessageButton } = require('discord.js'); const { fetchPingHistory, fetchModHistory, deleteAllUserData, getLeaverStatus } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); -const { formatDate } = require('../../../src/functions/helpers'); +const { formatDate, embedType } = require('../../../src/functions/helpers'); +// Interactions handler module.exports.run = async function (client, interaction) { if (!client.botReadyAt) return; - // Handles embed buttons if (interaction.isButton() && interaction.customId.startsWith('ping-protection_')) { - const [prefix, action, userId] = interaction.customId.split('_'); + + if (interaction.customId.startsWith('ping-protection_hist-page_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3]); + const limit = 8; + + const { total, history } = await fetchPingHistory(client, userId, targetPage, limit); + const totalPages = Math.ceil(total / limit) || 1; + + const user = await client.users.fetch(userId).catch(() => ({ username: 'Unknown User', displayAvatarURL: () => null })); + + let description = ""; + const leaverData = await getLeaverStatus(client, userId); + if (leaverData) { + description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { t: formatDate(leaverData.leftAt) })}\n\n`; + } + + if (history.length === 0) { + description += localize('ping-protection', 'no-data-found'); + } else { + const lines = history.map((entry, index) => { + const ts = Math.floor(new Date(entry.createdAt).getTime() / 1000); + let targetString = "Unknown"; + if (entry.targetId) { + targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; + } else { + targetString = "Detected"; + } + return `${(targetPage - 1) * limit + index + 1}. **Pinged ${targetString}** at ()\n[Jump to Message](${entry.messageUrl})`; + }); + description += lines.join('\n\n'); + } + + const row = new MessageActionRow().addComponents( + new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${targetPage - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(targetPage <= 1), + new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${targetPage}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), + new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${targetPage + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(targetPage >= totalPages) + ); + + const replyOptions = embedType({ + title: localize('ping-protection', 'embed-history-title', { u: user.username }), + thumbnail: user.displayAvatarURL({ dynamic: true }), + description: description, + color: 'ORANGE' + }); + replyOptions.components = [row]; + await interaction.update(replyOptions); + return; + } + + // Handles panel buttons + const [prefix, action, userId] = interaction.customId.split('_'); const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || (client.config.admins || []).includes(interaction.user.id); if (!isAdmin) { return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); } - - if (action === 'history') { - const history = await fetchPingHistory(client, userId, 10); + // Handles history button + if (action === 'history') { + const page = 1; + const limit = 15; + const { total, history } = await fetchPingHistory(client, userId, page, limit); + const totalPages = Math.ceil(total / limit) || 1; const leaverData = await getLeaverStatus(client, userId); - const user = interaction.client.users.cache.get(userId) || { username: userId, displayAvatarURL: () => null }; // Fallback user object - - const embed = new MessageEmbed() - .setTitle(localize('ping-protection', 'embed-history-title', { u: user.username })) - .setThumbnail(user.displayAvatarURL({ dynamic: true })) - .setColor('ORANGE'); + const user = client.users.cache.get(userId) || await client.users.fetch(userId).catch(() => ({ username: userId, displayAvatarURL: () => null })); let description = ""; - if (leaverData) { - description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { - t: formatDate(leaverData.leftAt) - })}\n\n`; + description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { t: formatDate(leaverData.leftAt) })}\n\n`; } if (history.length === 0) { description += localize('ping-protection', 'no-data-found'); } else { const lines = history.map((entry, index) => { - return `${index + 1}. **[${formatDate(entry.timestamp)}]** [Jump to Message](${entry.messageUrl})`; + const ts = Math.floor(new Date(entry.createdAt).getTime() / 1000); + let targetString = "Unknown"; + if (entry.targetId) { + targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; + } else { + targetString = "Detected"; + } + return `${(page - 1) * limit + index + 1}. **Pinged ${targetString}** at ()\n[Jump to Message](${entry.messageUrl})`; }); - description += lines.join('\n'); + description += lines.join('\n\n'); } - embed.setDescription(description); + const row = new MessageActionRow().addComponents( + new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${page - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(true), + new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${page}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), + new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(totalPages <= 1) + ); + + const replyOptions = embedType({ + title: localize('ping-protection', 'embed-history-title', { u: user.username }), + thumbnail: user.displayAvatarURL({ dynamic: true }), + description: description, + color: 'ORANGE' + }); - await interaction.reply({ embeds: [embed], ephemeral: true }); + replyOptions.components = [row]; + replyOptions.ephemeral = false; + await interaction.reply(replyOptions); } - + // Handled actions history button else if (action === 'actions') { - const history = await fetchModHistory(client, userId, 10); - const user = interaction.client.users.cache.get(userId) || { username: userId, displayAvatarURL: () => null }; // Fallback user object + const history = await fetchModHistory(client, userId, 15); + const user = client.users.cache.get(userId) || await client.users.fetch(userId).catch(() => ({ username: userId, displayAvatarURL: () => null })); - const embed = new MessageEmbed() - .setTitle(localize('ping-protection', 'embed-actions-title', { u: user.username })) - .setThumbnail(user.displayAvatarURL({ dynamic: true })) - .setColor('RED'); - + let description = ""; if (history.length === 0) { - embed.setDescription(localize('ping-protection', 'no-data-found')); + description = localize('ping-protection', 'no-data-found'); } else { const lines = history.map((entry, index) => { - const duration = entry.actionDuration ? ` (${Math.round(entry.actionDuration / 60000)}m)` : ''; - return `${index + 1}. **${entry.actionType}${duration}** - ${formatDate(entry.timestamp)}\n${localize('ping-protection', 'label-reason')}: ${entry.reason}`; + const duration = entry.actionDuration ? ` (${entry.actionDuration}m)` : ''; + const reasonText = entry.reason || localize('ping-protection', 'no-reason') || 'No reason'; + return `${index + 1}. **${entry.type}${duration}** - ${formatDate(entry.createdAt)}\n${localize('ping-protection', 'label-reason')}: ${reasonText}`; }); - embed.setDescription(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')}*`; } - await interaction.reply({ embeds: [embed], ephemeral: true }); - } + const replyOptions = embedType({ + title: localize('ping-protection', 'embed-actions-title', { u: user.username }), + thumbnail: user.displayAvatarURL({ dynamic: true }), + description: description, + color: 'RED' + }); + replyOptions.ephemeral = false; + await interaction.reply(replyOptions); + } + // Handles delete data button & confirmation else if (action === 'delete') { const modal = new Modal() .setCustomId(`ping-protection_confirm-delete_${userId}`) @@ -90,24 +162,16 @@ module.exports.run = async function (client, interaction) { } } - // Handles modal submission if (interaction.isModalSubmit() && interaction.customId.startsWith('ping-protection_confirm-delete_')) { const userId = interaction.customId.split('_')[2]; const userInput = interaction.fields.getTextInputValue('confirmation_text'); - - const requiredPhrase = localize('ping-protection', 'modal-phrase', { locale: interaction.locale }); // IMPORTANT: Use user's locale + const requiredPhrase = localize('ping-protection', 'modal-phrase', { locale: interaction.locale }); if (userInput === requiredPhrase) { await deleteAllUserData(client, userId); - await interaction.reply({ - content: `✅ ${localize('ping-protection', 'log-manual-delete', {u: userId})}`, - ephemeral: true - }); + await interaction.reply({ content: `✅ ${localize('ping-protection', 'log-manual-delete', {u: userId})}`, ephemeral: true }); } else { - await interaction.reply({ - content: `❌ ${localize('ping-protection', 'modal-failed')}`, - ephemeral: true - }); + await interaction.reply({ content: `❌ ${localize('ping-protection', 'modal-failed')}`, ephemeral: true }); } } }; \ No newline at end of file diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index c2af2b07..7cc1fd9e 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -6,6 +6,7 @@ const { } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); +// Messages handler module.exports.run = async function (client, message) { if (!client.botReadyAt) return; if (!message.guild) return; @@ -16,80 +17,84 @@ module.exports.run = async function (client, message) { const storageConfig = client.configurations['ping-protection']['storage']; const moderationRules = client.configurations['ping-protection']['moderation']; - if (!config) return; + if (!config || !moderationRules || !Array.isArray(moderationRules) || moderationRules.length === 0) return; - // Checks ignored channels + const rule1 = moderationRules[0]; + + // Checks ignored channels and roles if (config.ignoredChannels.includes(message.channel.id)) return; + if (message.member.roles.cache.some(role => config.ignoredRoles.includes(role.id))) return; - // Checks whitelisted roles - const hasIgnoredRole = message.member.roles.cache.some(role => - config.ignoredRoles.includes(role.id) - ); - if (hasIgnoredRole) return; + // Detects pings + const pingedProtectedRole = message.mentions.roles.some(role => config.protectedRoles.includes(role.id)); + const pingedProtectedUser = message.mentions.users.some(user => config.protectedUsers.includes(user.id)); + if (!pingedProtectedRole && !pingedProtectedUser) return; + + let pingCount = 0; + const pingerId = message.author.id; + const targetUser = message.mentions.users.find(u => config.protectedUsers.includes(u.id)); + const targetRole = message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); + const target = targetUser || targetRole; + + let requiredCount = 0; + let generatedReason = ""; + let timeframeWeeks = 12; - // Reply logic - if (message.type === 'REPLY' && config.allowReplyPings) { - } + try { + if (storageConfig && storageConfig.enablePingHistory) { + await addPing(client, message, target); + } - // Detect pings - const pingedProtectedRole = message.mentions.roles.some(role => - config.protectedRoles.includes(role.id) - ); - - const pingedProtectedUser = message.mentions.users.some(user => - config.protectedUsers.includes(user.id) - ); + if (rule1.advancedConfiguration) { + timeframeWeeks = rule1.timeframeWeeks; + } else { + timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + } - if (!pingedProtectedRole && !pingedProtectedUser) return; + pingCount = await getPingCountInWindow(client, pingerId, timeframeWeeks); - // Log pings if enabled - if (storageConfig && storageConfig.enablePingHistory) { - await addPing(client, message); + } catch (e) { + client.logger.error(`[ping-protection] Database interaction FAILED for ${message.author.tag}: ${e}`); } - - // Send warning - const target = message.mentions.users.find(u => config.protectedUsers.includes(u.id)) - || message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); - + await sendPingWarning(client, message, target, config); + + if (!rule1.enableModeration) return; + + if (rule1.advancedConfiguration) { + requiredCount = rule1.pingsCountAdvanced; + generatedReason = localize('ping-protection', 'reason-advanced', { c: pingCount, w: rule1.timeframeWeeks }); + } else { + requiredCount = rule1.pingsCountBasic; + generatedReason = localize('ping-protection', 'reason-basic', { c: pingCount, w: timeframeWeeks }); + } + + // Logs the ping status + client.logger.info(`[ping-protection] User ${message.author.tag} pinged ${target.id}. Count: ${pingCount}/${requiredCount}`); - // Moderation logic - if (!moderationRules || !Array.isArray(moderationRules)) return; - - for (const rule of moderationRules) { - if (!rule.enableModeration) continue; - - let triggerHit = false; - let generatedReason = ""; + if (pingCount >= requiredCount) { - if (rule.advancedConfiguration) { - // Advanced configuration - const count = await getPingCountInWindow(client, message.author.id, rule.timeframeWeeks); - - if (count >= rule.pingsCountAdvanced) { - triggerHit = true; - generatedReason = localize('ping-protection', 'reason-advanced', { - c: count, - w: rule.timeframeWeeks - }); + const { Op } = require('sequelize'); + const oneMinuteAgo = new Date(new Date() - 60000); + const recentLog = await client.models['ping-protection']['ModerationLog'].findOne({ + where: { + victimID: message.author.id, + createdAt: { [Op.gt]: oneMinuteAgo } } - } else { - // Basic configuration - const globalWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; - const count = await getPingCountInWindow(client, message.author.id, globalWeeks); + }); - if (count >= rule.pingsCountBasic) { - triggerHit = true; - generatedReason = localize('ping-protection', 'reason-basic', { - c: count, - w: globalWeeks - }); - } - } + if (recentLog) return; - if (triggerHit) { - await executeAction(client, message.member, rule, generatedReason, storageConfig); - break; + let memberToPunish = message.member; + if (!memberToPunish) { + try { + memberToPunish = await message.guild.members.fetch(message.author.id); + } catch (fetchError) { + client.logger.error(`[ping-protection] Failed to fetch member ${message.author.tag} for punishment.`); + return; + } } + + await executeAction(client, memberToPunish, rule1, generatedReason, storageConfig); } }; \ No newline at end of file diff --git a/modules/ping-protection/models/ModerationLog.js b/modules/ping-protection/models/ModerationLog.js index e7d68564..892caaad 100644 --- a/modules/ping-protection/models/ModerationLog.js +++ b/modules/ping-protection/models/ModerationLog.js @@ -3,26 +3,28 @@ const { DataTypes, Model } = require('sequelize'); module.exports = class ModerationLog extends Model { static init(sequelize) { return super.init({ - userId: { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false + }, + victimID: { type: DataTypes.STRING, allowNull: false }, - actionType: { + type: { type: DataTypes.STRING, allowNull: false }, + reason: { + type: DataTypes.STRING, + allowNull: true + }, actionDuration: { type: DataTypes.INTEGER, allowNull: true }, - reason: { - type: DataTypes.TEXT, - allowNull: false - }, - timestamp: { - type: DataTypes.DATE, - defaultValue: DataTypes.NOW - } }, { tableName: 'ping_protection_mod_log', timestamps: true, @@ -32,6 +34,6 @@ module.exports = class ModerationLog extends Model { }; module.exports.config = { - name: 'ModerationLog', - module: 'ping-protection' + 'name': 'ModerationLog', + 'module': 'ping-protection' }; \ No newline at end of file diff --git a/modules/ping-protection/models/PingHistory.js b/modules/ping-protection/models/PingHistory.js index c0f15393..2f6c8a74 100644 --- a/modules/ping-protection/models/PingHistory.js +++ b/modules/ping-protection/models/PingHistory.js @@ -11,9 +11,13 @@ module.exports = class PingHistory extends Model { type: DataTypes.STRING, allowNull: false }, - timestamp: { - type: DataTypes.DATE, - defaultValue: DataTypes.NOW + targetId: { + type: DataTypes.STRING, + allowNull: true + }, + isRole: { + type: DataTypes.BOOLEAN, + defaultValue: false } }, { tableName: 'ping_protection_history', diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 04334a20..927c2234 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -7,25 +7,18 @@ const { Op } = require('sequelize'); const { embedType, formatDiscordUserName } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); -/** - * Adds a ping record to the database - * @param {Client} client - * @param {Message} message - */ -async function addPing(client, message) { +// Adds a ping entry to the database +async function addPing(client, message, target) { + const isRole = !target.username; await client.models['ping-protection']['PingHistory'].create({ userId: message.author.id, - messageUrl: message.url + messageUrl: message.url, + targetId: target.id, + isRole: isRole }); } -/** - * Counts pings within a specific timeframe - * @param {Client} client - * @param {string} userId - * @param {number} weeks - * @returns {Promise} - */ +// Gets the number of pings in the specified timeframe async function getPingCountInWindow(client, userId, weeks) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - (weeks * 7)); @@ -33,20 +26,11 @@ async function getPingCountInWindow(client, userId, weeks) { return await client.models['ping-protection']['PingHistory'].count({ where: { userId: userId, - timestamp: { - [Op.gt]: cutoffDate - } + createdAt: { [Op.gt]: cutoffDate } } }); } - -/** - * Sends the warning message - * @param {Client} client - * @param {Message} message - * @param {Role|User} target - * @param {Object} moduleConfig - */ +// Sends the ping warning message async function sendPingWarning(client, message, target, moduleConfig) { const warningMsg = moduleConfig.pingWarningMessage; if (!warningMsg) return; @@ -61,122 +45,116 @@ async function sendPingWarning(client, message, target, moduleConfig) { }; const replyOptions = embedType(warningMsg, placeholders); - await message.reply(replyOptions).catch((e) => { - client.logger.debug(`[ping-protection] Failed to send warning to ${message.author.tag}: ${e.message}`); + client.logger.debug(`[ping-protection] Failed to send warning: ${e.message}`); }); } -/** - * Fetches the last X pings - * @param {Client} client - * @param {string} userId - * @param {number} limit - * @returns {Promise} - */ -async function fetchPingHistory(client, userId, limit = 10) { - return await client.models['ping-protection']['PingHistory'].findAll({ + // Fetches ping history +async function fetchPingHistory(client, userId, page = 1, limit = 8) { + const offset = (page - 1) * limit; + + const { count, rows } = await client.models['ping-protection']['PingHistory'].findAndCountAll({ where: { userId: userId }, - order: [['timestamp', 'DESC']], - limit: limit + order: [['createdAt', 'DESC']], + limit: limit, + offset: offset }); + + return { total: count, history: rows }; } -/** - * Fetches the moderation log history - * @param {Client} client - * @param {string} userId - * @param {number} limit - * @returns {Promise} - */ +// Fetches moderation action history async function fetchModHistory(client, userId, limit = 10) { - return await client.models['ping-protection']['ModerationLog'].findAll({ - where: { userId: userId }, - order: [['timestamp', 'DESC']], - limit: limit - }); + if (!client.models['ping-protection'] || !client.models['ping-protection']['ModerationLog']) return []; + + try { + return await client.models['ping-protection']['ModerationLog'].findAll({ + where: { victimID: userId }, + order: [['createdAt', 'DESC']], + limit: limit + }); + } catch (e) { + client.logger.error(`[MOD-FETCH-ERROR] Failed to query ModerationLog: ${e.message}`); + return []; + } } -/** - * Executes a punishment and logs it if configured - * @param {Client} client - * @param {GuildMember} member - * @param {Object} actionConfig - * @param {string} reason - * @param {Object} storageConfig - */ -async function executeAction(client, member, actionConfig, reason, storageConfig) { - const ModLog = client.models['ping-protection']['ModerationLog']; +// Executes the configured moderation action +async function executeAction(client, member, rule, reason, storageConfig) { + const actionType = rule.actionType; + + if (!member) return false; + + const botMember = await member.guild.members.fetch(client.user.id); + if (botMember.roles.highest.position <= member.roles.highest.position) { + client.logger.warn(`[ping-protection] Hierarchy Failure: Cannot moderate ${member.user.tag}.`); + return false; + } - try { - if (actionConfig.type === 'MUTE') { - const durationMs = (actionConfig.muteDuration || 60) * 60 * 1000; - - await member.timeout(durationMs, reason); - - if (storageConfig && storageConfig.enableModLogHistory) { - await ModLog.create({ - userId: member.id, - actionType: 'MUTE', - actionDuration: durationMs, + if (actionType === 'MUTE') { + const durationMs = rule.muteDuration * 60000; + + if (storageConfig.enableModLogHistory) { + try { + await client.models['ping-protection']['ModerationLog'].create({ + victimID: member.id, + type: 'MUTE', + actionDuration: rule.muteDuration, reason: reason }); + } catch (dbError) { + client.logger.error(`[ping-protection] DB Insert Failed: ${dbError.message}`); } - - client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-action-mute', { - u: member.user.tag, - t: actionConfig.muteDuration, - r: reason - })); - - } else if (actionConfig.type === 'KICK') { - await member.kick(reason); + } + + try { + await member.timeout(durationMs, reason); + client.logger.info(`[MODERATION] Muted ${member.user.tag} for ${rule.muteDuration}m.`); + return true; + } catch (error) { + client.logger.error(`[ping-protection] Mute failed: ${error.message}`); + return false; + } - if (storageConfig && storageConfig.enableModLogHistory) { - await ModLog.create({ - userId: member.id, - actionType: 'KICK', - actionDuration: null, + } else if (actionType === 'KICK') { + + if (storageConfig.enableModLogHistory) { + try { + await client.models['ping-protection']['ModerationLog'].create({ + victimID: member.id, + type: 'KICK', reason: reason }); + } catch (dbError) { + client.logger.error(`[ping-protection] DB Insert Failed: ${dbError.message}`); } - - client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-action-kick', { - u: member.user.tag, - r: reason - })); } - } catch (error) { - client.logger.error(`[ping-protection] Failed to execute ${actionConfig.type} on ${member.user.tag}: ${error.message}`); + + try { + await member.kick(reason); + client.logger.info(`[MODERATION] Kicked ${member.user.tag}.`); + return true; + } catch (error) { + client.logger.error(`[ping-protection] Kick failed: ${error.message}`); + return false; + } } + return false; } - -/** - * Deletes ALL database information from a user - * @param {Client} client - * @param {string} userId - */ +// Handles deletion of all data from a user async function deleteAllUserData(client, userId) { await client.models['ping-protection']['PingHistory'].destroy({ where: { userId: userId } }); - await client.models['ping-protection']['ModerationLog'].destroy({ where: { userId: userId } }); + await client.models['ping-protection']['ModerationLog'].destroy({ where: { victimID: userId } }); await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-manual-delete', { u: userId })); } -/** - * Checks if a user is currently marked as left - * @param {Client} client - * @param {string} userId - * @returns {Promise} - */ async function getLeaverStatus(client, userId) { return await client.models['ping-protection']['LeaverData'].findByPk(userId); } -/** - * Marks user as left - */ async function markUserAsLeft(client, userId) { await client.models['ping-protection']['LeaverData'].upsert({ userId: userId, @@ -184,18 +162,12 @@ async function markUserAsLeft(client, userId) { }); } -/** - * Handles rejoin - */ async function markUserAsRejoined(client, userId) { await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); } - -/** - * Enforces retention policies - */ +// Enforces data retention policies async function enforceRetention(client) { const storageConfig = client.configurations['ping-protection']['storage']; if (!storageConfig) return; @@ -206,7 +178,7 @@ async function enforceRetention(client) { historyCutoff.setDate(historyCutoff.getDate() - (historyWeeks * 7)); await client.models['ping-protection']['PingHistory'].destroy({ - where: { timestamp: { [Op.lt]: historyCutoff } } + where: { createdAt: { [Op.lt]: historyCutoff } } }); } @@ -216,7 +188,7 @@ async function enforceRetention(client) { modCutoff.setMonth(modCutoff.getMonth() - modMonths); await client.models['ping-protection']['ModerationLog'].destroy({ - where: { timestamp: { [Op.lt]: modCutoff } } + where: { createdAt: { [Op.lt]: modCutoff } } }); } @@ -232,10 +204,8 @@ async function enforceRetention(client) { for (const leaver of leaversToDelete) { const userId = leaver.userId; await client.models['ping-protection']['PingHistory'].destroy({ where: { userId } }); - await client.models['ping-protection']['ModerationLog'].destroy({ where: { userId } }); + await client.models['ping-protection']['ModerationLog'].destroy({ where: { userId: userId } }); await leaver.destroy(); - - client.logger.debug('[ping-protection] ' + localize('ping-protection', 'log-cleanup-finished', { u: userId })); } } } From f464555f2264f08f7f94934cd4d6ddc63936cd76 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Mon, 15 Dec 2025 12:15:44 +0100 Subject: [PATCH 15/31] Debugged absolutely everything, removed like 300 lines of code for polish while remaining the same functions. Removed a few locales that are unused and updated some locales for better understanding. Fully tested extensively. Not verified by GitHub because I code in VSCode. --- locales/de.json | 15 +- locales/en.json | 21 +- .../commands/ping-protection-cmd.js | 121 ++----- .../events/interactionCreate.js | 147 ++------- .../ping-protection/events/messageCreate.js | 47 ++- modules/ping-protection/ping-protection.js | 294 ++++++++++++------ 6 files changed, 294 insertions(+), 351 deletions(-) diff --git a/locales/de.json b/locales/de.json index 48a6726d..341658c9 100644 --- a/locales/de.json +++ b/locales/de.json @@ -984,9 +984,6 @@ "nickname-error": "Fehler beim Ändern des Nicknamens von %u: %e" }, "ping-protection": { - "log-cleanup-finished": "Datenschutz-Bereinigung: Daten für abgelaufenen Leaver %u gelöscht", - "log-action-mute": "%u für %t Min gemutet. Grund: %r", - "log-action-kick": "%u gekickt. Grund: %r", "log-manual-delete": "Alle Daten für <@%u> (%u) wurden erfolgreich gelöscht.", "log-manual-delete-logs": "Alle Daten für Benutzer mit der ID %u wurden erfolgreich gelöscht.", "reason-basic": "Benutzer hat %c Pings in den letzten %w Wochen erreicht (Basis-Limit).", @@ -1000,14 +997,14 @@ "cmd-desc-group-list": "Listet geschützte oder geweißlistete Entitäten auf", "cmd-desc-list-users": "Listet alle geschützten Mitglieder auf", "cmd-desc-list-roles": "Listet alle geschützten Rollen auf", - "cmd-desc-list-white": "Listet alle erlaubten Rollen/Mitglieder auf", + "cmd-desc-list-wl": "Listet alle erlaubten Rollen auf", "embed-history-title": "Ping-Verlauf: %u", "embed-leaver-warning": "Dieser Benutzer hat den Server um %t verlassen. Diese Logs bleiben bis zur automatischen Löschung.", - "no-data-found": "Keine Logs gefunden.", + "no-data-found": "Keine Logs gefunden für diesen Benutzer.", "embed-actions-title": "Moderations-Verlauf: %u", "label-reason": "Grund", - "actions-retention-note": "ACHTUNG! MODERATIONSAKTIONEN WERDEN FÜR 3 - 12 MONATE GESPEICHERT!", - "no-permission": "Du hast keine Berechtigung, dies zu nutzen.", + "actions-retention-note": "Denken Sie daran: Moderationsmaßnahmen werden je nach Konfiguration 3 - 12 Monate lang beibehalten.", + "no-permission": "Sie verfügen nicht über ausreichende Berechtigungen, um diesen Befehl zu verwenden.", "panel-title": "Benutzer-Panel: %u", "panel-description": "Daten für %u (%i) verwalten und anzeigen. Sehen Sie sich eine kurze Zusammenfassung ihres Ping- und Moderationsverlaufs an oder löschen Sie alle für diesen Benutzer gespeicherten Daten (Risky).", "btn-history": "Ping-Verlauf", @@ -1019,9 +1016,9 @@ "list-empty": "Keine konfiguriert.", "modal-title": "Bestätigen Sie die Löschung der Daten für diesen Benutzer", "modal-label": "Bestätigen Sie das Löschen der Daten, indem Sie diesen Satz eingeben:", - "modal-phrase": "Ich verstehe, dass alle Daten für diesen Benutzer gelöscht werden und dass diese Aktion unumkehrbar ist.", + "modal-phrase": "Ich verstehe, dass alle Daten dieses Benutzers gelöscht werden und dass diese Aktion nicht rückgängig gemacht werden kann.", "modal-failed": "Der von Ihnen eingegebene Satz ist falsch. Datenlöschung abgebrochen.", - "field-quick-history": "Schnelle Verlaufsansicht (Letzte %w Wochen)", + "field-quick-history": "Schnelle verlaufsansicht (Letzte %w wochen)", "field-quick-desc": "Betrag des Pings-Verlaufs: %p\nBetrag der Moderationsaktionen: %m" } } diff --git a/locales/en.json b/locales/en.json index 53a497d3..009f07d7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1019,14 +1019,11 @@ "nickname-error": "An error occurred while trying to change the nickname of %u: %e" }, "ping-protection": { - "log-cleanup-finished": "History Cleanup: Deleted data for expired leaver %u", - "log-action-mute": "Muted %u for %t mins. Reason: %r", - "log-action-kick": "Kicked %u. Reason: %r", "log-manual-delete": "All data for <@%u> (%u) has been deleted successfully.", "log-manual-delete-logs": "All data for user with ID %u has been deleted successfully.", "reason-basic": "User reached %c pings in the last %w weeks (Basic Configuration).", "reason-advanced": "User reached %c pings in the last %w weeks (Advanced Configuration).", - "cmd-desc-module": "Manage the ping protection system", + "cmd-desc-module": "Ping protection related commands", "cmd-desc-group-user": "Every command related to the users", "cmd-desc-history": "View the ping history of a user", "cmd-opt-user": "The user to check", @@ -1035,14 +1032,14 @@ "cmd-desc-group-list": "Lists protected or whitelisted entities", "cmd-desc-list-users": "List all protected members", "cmd-desc-list-roles": "List all protected roles", - "cmd-desc-list-white": "List all whitelisted roles/members", - "embed-history-title": "Ping History: %u", + "cmd-desc-list-wl": "List all whitelisted roles", + "embed-history-title": "Ping history of %u", "embed-leaver-warning": "This user left the server at %t. These logs will stay until automatic deletion.", - "no-data-found": "No logs found.", - "embed-actions-title": "Moderation History: %u", + "no-data-found": "No logs found for this user.", + "embed-actions-title": "Moderation history of %u", "label-reason": "Reason", - "actions-retention-note": "REMEMBER! MODERATION ACTIONS ARE KEPT IN STORAGE FOR 3 - 12 MONTHS!", - "no-permission": "You do not have permission to use this.", + "actions-retention-note": "Remember: Moderation actions are retained for 3 - 12 months based on the configuration.", + "no-permission": "You don't have sufficient permissions to use this command.", "panel-title": "User Panel: %u", "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", "btn-history": "Ping history", @@ -1054,9 +1051,9 @@ "list-empty": "None configured.", "modal-title": "Confirm data deletion for this user", "modal-label": "Confirm data deletion by typing this phrase:", - "modal-phrase": "I understand that all data will be deleted for this user and that this action is irreversible.", + "modal-phrase": "I understand that all data of this user will be deleted and that this action cannot be undone.", "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled.", - "field-quick-history": "Quick History View (Last %w weeks)", + "field-quick-history": "Quick history view (Last %w weeks)", "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m" } } \ No newline at end of file diff --git a/modules/ping-protection/commands/ping-protection-cmd.js b/modules/ping-protection/commands/ping-protection-cmd.js index 055c3fce..fb2c9e9e 100644 --- a/modules/ping-protection/commands/ping-protection-cmd.js +++ b/modules/ping-protection/commands/ping-protection-cmd.js @@ -1,13 +1,13 @@ const { - fetchPingHistory, fetchModHistory, - getLeaverStatus, getPingCountInWindow, + generateHistoryResponse, + generateActionsResponse } = require('../ping-protection'); -const { formatDate, embedType } = require('../../../src/functions/helpers'); +const { embedType } = require('../../../src/functions/helpers'); const { localize } = require('../../../src/functions/localize'); const { MessageActionRow, MessageButton } = require('discord.js'); - +// Commands list and info module.exports.config = { name: 'ping-protection', description: localize('ping-protection', 'cmd-desc-module'), @@ -31,13 +31,12 @@ module.exports.config = { options: [ { type: 'SUB_COMMAND', name: 'users', description: localize('ping-protection', 'cmd-desc-list-users') }, { type: 'SUB_COMMAND', name: 'roles', description: localize('ping-protection', 'cmd-desc-list-roles') }, - { type: 'SUB_COMMAND', name: 'whitelisted', description: localize('ping-protection', 'cmd-desc-list-white') } + { type: 'SUB_COMMAND', name: 'whitelisted', description: localize('ping-protection', 'cmd-desc-list-wl') } ] } ] }; - -// Commands handler +// Main commands handler module.exports.run = async function (interaction) { if (!interaction.guild) return; @@ -46,98 +45,32 @@ module.exports.run = async function (interaction) { const config = interaction.client.configurations['ping-protection']['configuration']; const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || (interaction.client.config.admins || []).includes(interaction.user.id); - +// Handles subcommands + // Subcommand user if (group === 'user') { const user = interaction.options.getUser('user'); - // Subcommand history if (subCmd === 'history') { - const page = 1; - const limit = 8; - const { total, history } = await fetchPingHistory(interaction.client, user.id, page, limit); - const leaverData = await getLeaverStatus(interaction.client, user.id); - const totalPages = Math.ceil(total / limit) || 1; - - let description = ""; - if (leaverData) { - description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { t: formatDate(leaverData.leftAt) })}\n\n`; - } - - if (history.length === 0) { - description += localize('ping-protection', 'no-data-found'); - } else { - const lines = history.map((entry, index) => { - const ts = Math.floor(new Date(entry.createdAt).getTime() / 1000); - let targetString = "Unknown"; - if (entry.targetId) { - targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; - } else { - targetString = "Detected"; - } - return `${(page - 1) * limit + index + 1}. **Pinged ${targetString}** at ()\n[Jump to Message](${entry.messageUrl})`; - }); - description += lines.join('\n\n'); - } - - // Buttons - const row = new MessageActionRow().addComponents( - new MessageButton().setCustomId(`ping-protection_hist-page_${user.id}_${page - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(true), - new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${page}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), - new MessageButton().setCustomId(`ping-protection_hist-page_${user.id}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(totalPages <= 1) - ); - - const replyOptions = embedType({ - title: localize('ping-protection', 'embed-history-title', { u: user.username }), - thumbnail: user.displayAvatarURL({ dynamic: true }), - description: description, - color: 'ORANGE' - }); - - replyOptions.components = [row]; + const replyOptions = await generateHistoryResponse(interaction.client, user.id, 1); replyOptions.ephemeral = false; - await interaction.reply(replyOptions); } - // Subcommand actions history else if (subCmd === 'actions-history') { - const history = await fetchModHistory(interaction.client, user.id, 15); - - let description = ""; - if (history.length === 0) { - description = localize('ping-protection', 'no-data-found'); - } else { - const lines = history.map((entry, index) => { - const duration = entry.actionDuration ? ` (${entry.actionDuration}m)` : ''; - const reasonText = entry.reason || localize('ping-protection', 'no-reason') || 'No reason'; - return `${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')}*`; - } - - const replyOptions = embedType({ - title: localize('ping-protection', 'embed-actions-title', { u: user.username }), - thumbnail: user.displayAvatarURL({ dynamic: true }), - description: description, - color: 'RED' - }); - + const replyOptions = await generateActionsResponse(interaction.client, user.id, 1); replyOptions.ephemeral = false; await interaction.reply(replyOptions); } - // Subcammand panel else if (subCmd === 'panel') { if (!isAdmin) return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); - const user = interaction.options.getUser('user'); const pingerId = user.id; const storageConfig = interaction.client.configurations['ping-protection']['storage']; const timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeWeeks); - const modHistory = await fetchModHistory(interaction.client, pingerId, 1000); - const modActionCount = modHistory.length; + const modData = await fetchModHistory(interaction.client, pingerId, 1, 1000); const row = new MessageActionRow().addComponents( new MessageButton().setCustomId(`ping-protection_history_${user.id}`).setLabel(localize('ping-protection', 'btn-history')).setStyle('SECONDARY'), @@ -146,25 +79,26 @@ module.exports.run = async function (interaction) { ); const replyOptions = embedType({ - title: localize('ping-protection', 'panel-title', { u: user.tag }), - description: localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id }), - color: 'BLUE', - thumbnail: user.displayAvatarURL({ dynamic: true }), - fields: [{ - name: localize('ping-protection', 'field-quick-history', {w: timeframeWeeks}), - value: localize('ping-protection', 'field-quick-desc', { p: pingCount, m: modActionCount }), - inline: false + _schema: 'v3', + embeds: [{ + title: localize('ping-protection', 'panel-title', { u: user.tag }), + description: localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id }), + color: 'BLUE', + thumbnailURL: user.displayAvatarURL({ dynamic: true }), + fields: [{ + name: localize('ping-protection', 'field-quick-history', {w: timeframeWeeks}), + value: localize('ping-protection', 'field-quick-desc', { p: pingCount, m: modData.total }), + inline: false + }] }] }); replyOptions.components = [row]; replyOptions.ephemeral = false; - await interaction.reply(replyOptions); } } - - // Subcommand group list + // Subcommand list else if (group === 'list') { let contentList = []; let title = ""; @@ -183,9 +117,12 @@ module.exports.run = async function (interaction) { if (contentList.length === 0) contentList = [localize('ping-protection', 'list-empty')]; const replyOptions = embedType({ - title: title, - description: contentList.join('\n'), - color: 'GREEN' + _schema: 'v3', + embeds: [{ + title: title, + description: contentList.join('\n'), + color: 'GREEN' + }] }); replyOptions.ephemeral = false; diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index 1c9ac5c9..e7ba7f64 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -1,148 +1,58 @@ -const { Modal, TextInputComponent, MessageActionRow, MessageButton } = require('discord.js'); -const { fetchPingHistory, fetchModHistory, deleteAllUserData, getLeaverStatus } = require('../ping-protection'); +const { Modal, TextInputComponent, MessageActionRow } = require('discord.js'); +const { deleteAllUserData, generateHistoryResponse, generateActionsResponse } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); -const { formatDate, embedType } = require('../../../src/functions/helpers'); - -// Interactions handler +// Interaction handler module.exports.run = async function (client, interaction) { if (!client.botReadyAt) return; if (interaction.isButton() && interaction.customId.startsWith('ping-protection_')) { + // Ping history pagination if (interaction.customId.startsWith('ping-protection_hist-page_')) { const parts = interaction.customId.split('_'); const userId = parts[2]; const targetPage = parseInt(parts[3]); - const limit = 8; - - const { total, history } = await fetchPingHistory(client, userId, targetPage, limit); - const totalPages = Math.ceil(total / limit) || 1; - - const user = await client.users.fetch(userId).catch(() => ({ username: 'Unknown User', displayAvatarURL: () => null })); - - let description = ""; - const leaverData = await getLeaverStatus(client, userId); - if (leaverData) { - description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { t: formatDate(leaverData.leftAt) })}\n\n`; - } - - if (history.length === 0) { - description += localize('ping-protection', 'no-data-found'); - } else { - const lines = history.map((entry, index) => { - const ts = Math.floor(new Date(entry.createdAt).getTime() / 1000); - let targetString = "Unknown"; - if (entry.targetId) { - targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; - } else { - targetString = "Detected"; - } - return `${(targetPage - 1) * limit + index + 1}. **Pinged ${targetString}** at ()\n[Jump to Message](${entry.messageUrl})`; - }); - description += lines.join('\n\n'); - } - - const row = new MessageActionRow().addComponents( - new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${targetPage - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(targetPage <= 1), - new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${targetPage}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), - new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${targetPage + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(targetPage >= totalPages) - ); - - const replyOptions = embedType({ - title: localize('ping-protection', 'embed-history-title', { u: user.username }), - thumbnail: user.displayAvatarURL({ dynamic: true }), - description: description, - color: 'ORANGE' - }); - replyOptions.components = [row]; + const replyOptions = await generateHistoryResponse(client, userId, targetPage); await interaction.update(replyOptions); return; } - // Handles panel buttons + // Moderation history pagination + if (interaction.customId.startsWith('ping-protection_mod-page_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3]); + + const replyOptions = await generateActionsResponse(client, userId, targetPage); + await interaction.update(replyOptions); + return; + } + + // Panel buttons const [prefix, action, userId] = interaction.customId.split('_'); + + // Checks for permissions const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || (client.config.admins || []).includes(interaction.user.id); - - if (!isAdmin) { - return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); + if (action !== 'delete' && !isAdmin && !prefix.includes('page')) { + if (['history', 'actions', 'delete'].includes(action)) { + if (!isAdmin) return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); + } } - // Handles history button - if (action === 'history') { - const page = 1; - const limit = 15; - const { total, history } = await fetchPingHistory(client, userId, page, limit); - const totalPages = Math.ceil(total / limit) || 1; - const leaverData = await getLeaverStatus(client, userId); - const user = client.users.cache.get(userId) || await client.users.fetch(userId).catch(() => ({ username: userId, displayAvatarURL: () => null })); - - let description = ""; - if (leaverData) { - description += `⚠️ ${localize('ping-protection', 'embed-leaver-warning', { t: formatDate(leaverData.leftAt) })}\n\n`; - } - - if (history.length === 0) { - description += localize('ping-protection', 'no-data-found'); - } else { - const lines = history.map((entry, index) => { - const ts = Math.floor(new Date(entry.createdAt).getTime() / 1000); - let targetString = "Unknown"; - if (entry.targetId) { - targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; - } else { - targetString = "Detected"; - } - return `${(page - 1) * limit + index + 1}. **Pinged ${targetString}** at ()\n[Jump to Message](${entry.messageUrl})`; - }); - description += lines.join('\n\n'); - } - - const row = new MessageActionRow().addComponents( - new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${page - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(true), - new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${page}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), - new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(totalPages <= 1) - ); - const replyOptions = embedType({ - title: localize('ping-protection', 'embed-history-title', { u: user.username }), - thumbnail: user.displayAvatarURL({ dynamic: true }), - description: description, - color: 'ORANGE' - }); - - replyOptions.components = [row]; + if (action === 'history') { + const replyOptions = await generateHistoryResponse(client, userId, 1); replyOptions.ephemeral = false; await interaction.reply(replyOptions); } - // Handled actions history button - else if (action === 'actions') { - const history = await fetchModHistory(client, userId, 15); - const user = client.users.cache.get(userId) || await client.users.fetch(userId).catch(() => ({ username: userId, displayAvatarURL: () => null })); - - let description = ""; - if (history.length === 0) { - description = localize('ping-protection', 'no-data-found'); - } else { - const lines = history.map((entry, index) => { - const duration = entry.actionDuration ? ` (${entry.actionDuration}m)` : ''; - const reasonText = entry.reason || localize('ping-protection', 'no-reason') || 'No reason'; - return `${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')}*`; - } - - const replyOptions = embedType({ - title: localize('ping-protection', 'embed-actions-title', { u: user.username }), - thumbnail: user.displayAvatarURL({ dynamic: true }), - description: description, - color: 'RED' - }); + else if (action === 'actions') { + const replyOptions = await generateActionsResponse(client, userId, 1); replyOptions.ephemeral = false; await interaction.reply(replyOptions); } - // Handles delete data button & confirmation + // Handles data deletion button else if (action === 'delete') { const modal = new Modal() .setCustomId(`ping-protection_confirm-delete_${userId}`) @@ -162,6 +72,7 @@ module.exports.run = async function (client, interaction) { } } + // Modal Handling if (interaction.isModalSubmit() && interaction.customId.startsWith('ping-protection_confirm-delete_')) { const userId = interaction.customId.split('_')[2]; const userInput = interaction.fields.getTextInputValue('confirmation_text'); diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index 7cc1fd9e..01691d4e 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -5,7 +5,6 @@ const { sendPingWarning } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); - // Messages handler module.exports.run = async function (client, message) { if (!client.botReadyAt) return; @@ -21,7 +20,7 @@ module.exports.run = async function (client, message) { const rule1 = moderationRules[0]; - // Checks ignored channels and roles + // Checks for ignored channels and roles if (config.ignoredChannels.includes(message.channel.id)) return; if (message.member.roles.cache.some(role => config.ignoredRoles.includes(role.id))) return; @@ -30,20 +29,29 @@ module.exports.run = async function (client, message) { const pingedProtectedUser = message.mentions.users.some(user => config.protectedUsers.includes(user.id)); if (!pingedProtectedRole && !pingedProtectedUser) return; - let pingCount = 0; - const pingerId = message.author.id; + // Identifies target const targetUser = message.mentions.users.find(u => config.protectedUsers.includes(u.id)); const targetRole = message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); const target = targetUser || targetRole; + const targetName = target.tag || target.name || target.id; + // Checks if ping history logging is enabled + if (!storageConfig || !storageConfig.enablePingHistory) { + client.logger.info(`[ping-protection] User ${message.author.tag} pinged ${targetName}. Pings history logging is disabled, moderation actions cannot be done.`); + + await sendPingWarning(client, message, target, config); + return; + } + + // Processes the ping + let pingCount = 0; + const pingerId = message.author.id; let requiredCount = 0; let generatedReason = ""; let timeframeWeeks = 12; try { - if (storageConfig && storageConfig.enablePingHistory) { - await addPing(client, message, target); - } + await addPing(client, message, target); if (rule1.advancedConfiguration) { timeframeWeeks = rule1.timeframeWeeks; @@ -54,9 +62,10 @@ module.exports.run = async function (client, message) { pingCount = await getPingCountInWindow(client, pingerId, timeframeWeeks); } catch (e) { - client.logger.error(`[ping-protection] Database interaction FAILED for ${message.author.tag}: ${e}`); + client.logger.error(`[ping-protection] Database interaction failed for ${message.author.tag}: ${e}`); } + // Sends warning message await sendPingWarning(client, message, target, config); if (!rule1.enableModeration) return; @@ -69,21 +78,25 @@ module.exports.run = async function (client, message) { generatedReason = localize('ping-protection', 'reason-basic', { c: pingCount, w: timeframeWeeks }); } - // Logs the ping status - client.logger.info(`[ping-protection] User ${message.author.tag} pinged ${target.id}. Count: ${pingCount}/${requiredCount}`); + client.logger.info(`[ping-protection] User ${message.author.tag} pinged ${targetName}. Count: ${pingCount}/${requiredCount}`); if (pingCount >= requiredCount) { + // Checks for recent moderation to prevent spam actions const { Op } = require('sequelize'); const oneMinuteAgo = new Date(new Date() - 60000); - const recentLog = await client.models['ping-protection']['ModerationLog'].findOne({ - where: { - victimID: message.author.id, - createdAt: { [Op.gt]: oneMinuteAgo } - } - }); + + try { + const recentLog = await client.models['ping-protection']['ModerationLog'].findOne({ + where: { + victimID: message.author.id, + createdAt: { [Op.gt]: oneMinuteAgo } + } + }); - if (recentLog) return; + if (recentLog) return; + } catch (e) { + } let memberToPunish = message.member; if (!memberToPunish) { diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 927c2234..a4ef9255 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -4,12 +4,14 @@ * @author itskevinnn */ const { Op } = require('sequelize'); -const { embedType, formatDiscordUserName } = require('../../src/functions/helpers'); +const { MessageActionRow, MessageButton } = require('discord.js'); +const { embedType, formatDiscordUserName, formatDate } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); -// Adds a ping entry to the database +const DISABLED_MSG = "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"storage\" tab in the 'ping-protection' module ^^"; +// Core functions and logic async function addPing(client, message, target) { - const isRole = !target.username; + const isRole = !target.username; await client.models['ping-protection']['PingHistory'].create({ userId: message.author.id, messageUrl: message.url, @@ -18,7 +20,6 @@ async function addPing(client, message, target) { }); } -// Gets the number of pings in the specified timeframe async function getPingCountInWindow(client, userId, weeks) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - (weeks * 7)); @@ -30,60 +31,62 @@ async function getPingCountInWindow(client, userId, weeks) { } }); } -// Sends the ping warning message -async function sendPingWarning(client, message, target, moduleConfig) { - const warningMsg = moduleConfig.pingWarningMessage; - if (!warningMsg) return; - - const targetName = target.name || target.tag || target.username || 'Unknown'; - const targetMention = target.toString(); - - const placeholders = { - '%target-name%': targetName, - '%target-mention%': targetMention, - '%target-id%': target.id - }; - const replyOptions = embedType(warningMsg, placeholders); - await message.reply(replyOptions).catch((e) => { - client.logger.debug(`[ping-protection] Failed to send warning: ${e.message}`); - }); -} - - // Fetches ping history async function fetchPingHistory(client, userId, page = 1, limit = 8) { const offset = (page - 1) * limit; - const { count, rows } = await client.models['ping-protection']['PingHistory'].findAndCountAll({ where: { userId: userId }, order: [['createdAt', 'DESC']], limit: limit, offset: offset }); - return { total: count, history: rows }; } -// Fetches moderation action history -async function fetchModHistory(client, userId, limit = 10) { - if (!client.models['ping-protection'] || !client.models['ping-protection']['ModerationLog']) return []; - +async function fetchModHistory(client, userId, page = 1, limit = 8) { + if (!client.models['ping-protection'] || !client.models['ping-protection']['ModerationLog']) return { total: 0, history: [] }; try { - return await client.models['ping-protection']['ModerationLog'].findAll({ + const offset = (page - 1) * limit; + const { count, rows } = await client.models['ping-protection']['ModerationLog'].findAndCountAll({ where: { victimID: userId }, order: [['createdAt', 'DESC']], - limit: limit + limit: limit, + offset: offset }); + return { total: count, history: rows }; } catch (e) { - client.logger.error(`[MOD-FETCH-ERROR] Failed to query ModerationLog: ${e.message}`); - return []; + client.logger.error(`[ping-protection] Failed to query ModerationLog: ${e.message}`); + return { total: 0, history: [] }; } } -// Executes the configured moderation action +async function getLeaverStatus(client, userId) { + return await client.models['ping-protection']['LeaverData'].findByPk(userId); +} + +// Action logic + +async function sendPingWarning(client, message, target, moduleConfig) { + const warningMsg = moduleConfig.pingWarningMessage; + if (!warningMsg) return; + + const targetName = target.name || target.tag || target.username || 'Unknown'; + const targetMention = target.toString(); + + const placeholders = { + '%target-name%': targetName, + '%target-mention%': targetMention, + '%target-id%': target.id + }; + + const replyOptions = embedType(warningMsg, placeholders); + await message.reply(replyOptions).catch((e) => { + client.logger.debug(`[ping-protection] Failed to send warning: ${e.message}`); + }); +} + async function executeAction(client, member, rule, reason, storageConfig) { const actionType = rule.actionType; - if (!member) return false; const botMember = await member.guild.members.fetch(client.user.id); @@ -92,48 +95,38 @@ async function executeAction(client, member, rule, reason, storageConfig) { return false; } + // Database logging + const logDb = async (type, duration = null) => { + if (!storageConfig.enableModLogHistory) return; + try { + await client.models['ping-protection']['ModerationLog'].create({ + victimID: member.id, + type, + actionDuration: duration, + reason + }); + } catch (dbError) { + client.logger.error(`[ping-protection] DB Insert Failed: ${dbError.message}`); + } + }; + if (actionType === 'MUTE') { const durationMs = rule.muteDuration * 60000; - - if (storageConfig.enableModLogHistory) { - try { - await client.models['ping-protection']['ModerationLog'].create({ - victimID: member.id, - type: 'MUTE', - actionDuration: rule.muteDuration, - reason: reason - }); - } catch (dbError) { - client.logger.error(`[ping-protection] DB Insert Failed: ${dbError.message}`); - } - } + await logDb('MUTE', rule.muteDuration); try { await member.timeout(durationMs, reason); - client.logger.info(`[MODERATION] Muted ${member.user.tag} for ${rule.muteDuration}m.`); + client.logger.info(`[ping-protection] Muted ${member.user.tag} for ${rule.muteDuration}m.`); return true; } catch (error) { client.logger.error(`[ping-protection] Mute failed: ${error.message}`); return false; } - } else if (actionType === 'KICK') { - - if (storageConfig.enableModLogHistory) { - try { - await client.models['ping-protection']['ModerationLog'].create({ - victimID: member.id, - type: 'KICK', - reason: reason - }); - } catch (dbError) { - client.logger.error(`[ping-protection] DB Insert Failed: ${dbError.message}`); - } - } - + await logDb('KICK'); try { await member.kick(reason); - client.logger.info(`[MODERATION] Kicked ${member.user.tag}.`); + client.logger.info(`[ping-protection] Kicked ${member.user.tag}.`); return true; } catch (error) { client.logger.error(`[ping-protection] Kick failed: ${error.message}`); @@ -142,69 +135,162 @@ async function executeAction(client, member, rule, reason, storageConfig) { } return false; } -// Handles deletion of all data from a user + +// View generations + +async function generateHistoryResponse(client, userId, page = 1) { + const storageConfig = client.configurations['ping-protection']['storage']; + const limit = 8; + const isEnabled = storageConfig.enablePingHistory; + + let total = 0, history = [], totalPages = 1; + + if (isEnabled) { + const data = await fetchPingHistory(client, userId, page, limit); + total = data.total; + history = data.history; + totalPages = Math.ceil(total / limit) || 1; + } + + const user = await client.users.fetch(userId).catch(() => ({ username: 'Unknown User', displayAvatarURL: () => null })); + const leaverData = await getLeaverStatus(client, userId); + let description = ""; + + if (leaverData) { + const dateStr = formatDate(leaverData.leftAt); + if (history.length > 0) { + description += `⚠️ User left at ${dateStr}. These logs will stay until automatic deletion.\n\n`; + } else { + description += `⚠️ User left at ${dateStr}.\n\n`; + } + } + + if (!isEnabled) { + description += DISABLED_MSG; + } else if (history.length === 0) { + description += localize('ping-protection', 'no-data-found'); + } else { + const lines = history.map((entry, index) => { + const timeString = formatDate(entry.createdAt); + let targetString = "Unknown"; + if (entry.targetId) { + targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; + } else { + targetString = "Detected"; + } + return `${(page - 1) * limit + index + 1}. **Pinged ${targetString}** at ${timeString}\n[Jump to Message](${entry.messageUrl})`; + }); + description += lines.join('\n\n'); + } + + const row = new MessageActionRow().addComponents( + new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${page - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(page <= 1), + new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${page}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), + new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(page >= totalPages || !isEnabled) + ); + + const replyOptions = embedType({ + _schema: 'v3', + embeds: [{ + title: localize('ping-protection', 'embed-history-title', { u: user.username }), + thumbnailURL: user.displayAvatarURL({ dynamic: true }), + description: description, + color: 'ORANGE' + }] + }); + replyOptions.components = [row]; + return replyOptions; +} +// Generates the actions view +async function generateActionsResponse(client, userId, page = 1) { + const storageConfig = client.configurations['ping-protection']['storage']; + const limit = 8; + const isEnabled = storageConfig.enableModLogHistory; + + let total = 0, history = [], totalPages = 1; + + if (isEnabled) { + const data = await fetchModHistory(client, userId, page, limit); + total = data.total; + history = data.history; + totalPages = Math.ceil(total / limit) || 1; + } + + const user = await client.users.fetch(userId).catch(() => ({ username: 'Unknown User', displayAvatarURL: () => null })); + let description = ""; + + if (!isEnabled) { + description = DISABLED_MSG; + } else if (history.length === 0) { + description = localize('ping-protection', 'no-data-found'); + } else { + const lines = history.map((entry, index) => { + const duration = entry.actionDuration ? ` (${entry.actionDuration}m)` : ''; + const reasonText = entry.reason || localize('ping-protection', 'no-reason') || 'No reason'; + const timeString = formatDate(entry.createdAt); + return `${(page - 1) * limit + index + 1}. **${entry.type}${duration}** - ${timeString}\n${localize('ping-protection', 'label-reason')}: ${reasonText}`; + }); + description = lines.join('\n\n') + `\n\n*${localize('ping-protection', 'actions-retention-note')}*`; + } + + const row = new MessageActionRow().addComponents( + new MessageButton().setCustomId(`ping-protection_mod-page_${userId}_${page - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(page <= 1), + new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${page}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), + new MessageButton().setCustomId(`ping-protection_mod-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(page >= totalPages || !isEnabled) + ); + + const replyOptions = embedType({ + _schema: 'v3', + embeds: [{ + title: localize('ping-protection', 'embed-actions-title', { u: user.username }), + thumbnailURL: user.displayAvatarURL({ dynamic: true }), + description: description, + color: 'RED' + }] + }); + replyOptions.components = [row]; + return replyOptions; +} + +// Manages data deletion + async function deleteAllUserData(client, userId) { await client.models['ping-protection']['PingHistory'].destroy({ where: { userId: userId } }); await client.models['ping-protection']['ModerationLog'].destroy({ where: { victimID: userId } }); await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); - - client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-manual-delete', { u: userId })); -} - -async function getLeaverStatus(client, userId) { - return await client.models['ping-protection']['LeaverData'].findByPk(userId); + client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-manual-delete-logs', { u: userId })); } async function markUserAsLeft(client, userId) { - await client.models['ping-protection']['LeaverData'].upsert({ - userId: userId, - leftAt: new Date() - }); + await client.models['ping-protection']['LeaverData'].upsert({ userId: userId, leftAt: new Date() }); } async function markUserAsRejoined(client, userId) { - await client.models['ping-protection']['LeaverData'].destroy({ - where: { userId: userId } - }); + await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); } -// Enforces data retention policies + async function enforceRetention(client) { const storageConfig = client.configurations['ping-protection']['storage']; if (!storageConfig) return; if (storageConfig.enablePingHistory) { - const historyWeeks = storageConfig.pingHistoryRetention || 12; const historyCutoff = new Date(); - historyCutoff.setDate(historyCutoff.getDate() - (historyWeeks * 7)); - - await client.models['ping-protection']['PingHistory'].destroy({ - where: { createdAt: { [Op.lt]: historyCutoff } } - }); + historyCutoff.setDate(historyCutoff.getDate() - ((storageConfig.pingHistoryRetention || 12) * 7)); + await client.models['ping-protection']['PingHistory'].destroy({ where: { createdAt: { [Op.lt]: historyCutoff } } }); } if (storageConfig.enableModLogHistory) { - const modMonths = storageConfig.modLogRetention || 6; const modCutoff = new Date(); - modCutoff.setMonth(modCutoff.getMonth() - modMonths); - - await client.models['ping-protection']['ModerationLog'].destroy({ - where: { createdAt: { [Op.lt]: modCutoff } } - }); + modCutoff.setMonth(modCutoff.getMonth() - (storageConfig.modLogRetention || 6)); + await client.models['ping-protection']['ModerationLog'].destroy({ where: { createdAt: { [Op.lt]: modCutoff } } }); } if (storageConfig.enableLeaverDataRetention) { - const leaverDays = storageConfig.leaverRetention || 1; const leaverCutoff = new Date(); - leaverCutoff.setDate(leaverCutoff.getDate() - leaverDays); - - const leaversToDelete = await client.models['ping-protection']['LeaverData'].findAll({ - where: { leftAt: { [Op.lt]: leaverCutoff } } - }); - + leaverCutoff.setDate(leaverCutoff.getDate() - (storageConfig.leaverRetention || 1)); + const leaversToDelete = await client.models['ping-protection']['LeaverData'].findAll({ where: { leftAt: { [Op.lt]: leaverCutoff } } }); for (const leaver of leaversToDelete) { - const userId = leaver.userId; - await client.models['ping-protection']['PingHistory'].destroy({ where: { userId } }); - await client.models['ping-protection']['ModerationLog'].destroy({ where: { userId: userId } }); + await deleteAllUserData(client, leaver.userId); await leaver.destroy(); } } @@ -221,5 +307,7 @@ module.exports = { getLeaverStatus, markUserAsLeft, markUserAsRejoined, - enforceRetention + enforceRetention, + generateHistoryResponse, + generateActionsResponse }; \ No newline at end of file From 35afa5a7ea7a75d49513dccd6bf1fa72fb84ef54 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Mon, 15 Dec 2025 12:27:45 +0100 Subject: [PATCH 16/31] Added the option to lower mod actions history --- locales/de.json | 2 +- locales/en.json | 2 +- modules/ping-protection/configs/storage.json | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/de.json b/locales/de.json index 341658c9..ef85a026 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1003,7 +1003,7 @@ "no-data-found": "Keine Logs gefunden für diesen Benutzer.", "embed-actions-title": "Moderations-Verlauf: %u", "label-reason": "Grund", - "actions-retention-note": "Denken Sie daran: Moderationsmaßnahmen werden je nach Konfiguration 3 - 12 Monate lang beibehalten.", + "actions-retention-note": "Denken Sie daran: Moderationsmaßnahmen werden je nach Konfiguration 1 - 12 Monate lang beibehalten.", "no-permission": "Sie verfügen nicht über ausreichende Berechtigungen, um diesen Befehl zu verwenden.", "panel-title": "Benutzer-Panel: %u", "panel-description": "Daten für %u (%i) verwalten und anzeigen. Sehen Sie sich eine kurze Zusammenfassung ihres Ping- und Moderationsverlaufs an oder löschen Sie alle für diesen Benutzer gespeicherten Daten (Risky).", diff --git a/locales/en.json b/locales/en.json index 009f07d7..7ba530d5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1038,7 +1038,7 @@ "no-data-found": "No logs found for this user.", "embed-actions-title": "Moderation history of %u", "label-reason": "Reason", - "actions-retention-note": "Remember: Moderation actions are retained for 3 - 12 months based on the configuration.", + "actions-retention-note": "Remember: Moderation actions are retained for 1 - 12 months based on the configuration.", "no-permission": "You don't have sufficient permissions to use this command.", "panel-title": "User Panel: %u", "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json index 4c698bdc..3dfa6245 100644 --- a/modules/ping-protection/configs/storage.json +++ b/modules/ping-protection/configs/storage.json @@ -36,11 +36,11 @@ "name": "modLogRetention", "humanName": { "en": "Moderation Log Retention (Months)", "de": "Mod-Log Speicherzeit (Monate)" }, "description": { - "en": "How long to keep records of punishments (3-12 Months).", - "de": "Wie lange Bestrafungsprotokolle gespeichert werden (3-12 Monate)." + "en": "How long to keep records of punishments (1-12 Months).", + "de": "Wie lange Bestrafungsprotokolle gespeichert werden (1-12 Monate)." }, "type": "select", - "content": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + "content": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "default": { "en": 6 }, "dependsOn": "enableModLogHistory" }, From 9cf2c367961ec27cd8b644f822d744c790f40441 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Mon, 15 Dec 2025 12:32:06 +0100 Subject: [PATCH 17/31] Made the deault value of pings to trigger action 10 instead of 5 in basic pings count config --- modules/ping-protection/configs/moderation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index 56690f4d..fb04f392 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -30,7 +30,7 @@ "humanName": { "en": "Pings to trigger moderation", "de": "Pings für Bestrafung" }, "description": { "en": "The amount of pings required to trigger a moderation action (Uses 'Ping History Retention' timeframe).", "de": "Anzahl Pings für Bestrafung (Nutzt 'Ping-Verlauf Speicherzeit')." }, "type": "integer", - "default": { "en": 5 }, + "default": { "en": 10 }, "dependsOn": "enableModeration" }, { From b5ca02ff4d1b006a2c4a6801a362ce22cbf07785 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 15 Dec 2025 15:11:14 +0100 Subject: [PATCH 18/31] Added the commands warnings for most commands Listed the warnings for all commands except the panel command as the bot already checks for administrator perms. --- modules/ping-protection/configs/configuration.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 83e67604..a15dedf9 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -2,7 +2,16 @@ "filename": "configuration.json", "humanName": { "en": "General Configuration", - "de": "Allgemeine Konfiguration" + "de": "Allgemeine Konfiguration", + }, + "commandsWarnings": { + "normal": [ + "/ping-protection user history", + "/ping-protection user actions-history", + "/ping-protection list roles", + "/ping-protection list users", + "/ping-protection list whitelisted" + ] }, "description": { "en": "Configure protected users/roles, whitelisted roles/members, ignored channels and the notification message.", @@ -90,4 +99,4 @@ } } ] -} \ No newline at end of file +} From edaa5d16ef09d5d7d5c66cf489e53ff9fd6c3cb4 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Wed, 17 Dec 2025 15:48:08 +0100 Subject: [PATCH 19/31] Almost completely rewrote the module to make sure the modules works as supposed to with SCNX. --- locales/de.json | 40 +----- locales/en.json | 5 +- .../commands/ping-protection-cmd.js | 131 ----------------- .../commands/ping-protection.js | 133 ++++++++++++++++++ .../configs/configuration.json | 78 ++++++---- .../ping-protection/configs/moderation.json | 102 ++++++++++---- modules/ping-protection/configs/storage.json | 87 +++++++----- modules/ping-protection/events/botReady.js | 9 -- .../ping-protection/events/guildMemberAdd.js | 2 +- .../events/guildMemberRemove.js | 2 +- .../events/interactionCreate.js | 13 +- .../ping-protection/events/messageCreate.js | 47 +++---- modules/ping-protection/models/LeaverData.js | 2 +- .../ping-protection/models/ModerationLog.js | 2 +- modules/ping-protection/models/PingHistory.js | 2 +- modules/ping-protection/ping-protection.js | 85 +++++------ 16 files changed, 368 insertions(+), 372 deletions(-) delete mode 100644 modules/ping-protection/commands/ping-protection-cmd.js create mode 100644 modules/ping-protection/commands/ping-protection.js diff --git a/locales/de.json b/locales/de.json index ef85a026..320d9b3e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -982,43 +982,5 @@ "nicknames": { "owner-cannot-be-renamed": "Der Serverbesitzer (%u) kann nicht umbenannt werden.", "nickname-error": "Fehler beim Ändern des Nicknamens von %u: %e" - }, - "ping-protection": { - "log-manual-delete": "Alle Daten für <@%u> (%u) wurden erfolgreich gelöscht.", - "log-manual-delete-logs": "Alle Daten für Benutzer mit der ID %u wurden erfolgreich gelöscht.", - "reason-basic": "Benutzer hat %c Pings in den letzten %w Wochen erreicht (Basis-Limit).", - "reason-advanced": "Benutzer hat %c daily Pings in den letzten %w Wochen erreicht (Erweitertes Limit).", - "cmd-desc-module": "Verwalte das Ping-Schutz-System", - "cmd-desc-group-user": "Alle Befehle bezüglich Benutzer", - "cmd-desc-history": "Sieh dir den Ping-Verlauf eines Benutzers an", - "cmd-opt-user": "Der zu prüfende Benutzer", - "cmd-desc-actions": "Sieh dir den Verlauf der Moderationsaktionen eines Benutzers an", - "cmd-desc-panel": "Admin: Öffne das Benutzerverwaltungs-Panel", - "cmd-desc-group-list": "Listet geschützte oder geweißlistete Entitäten auf", - "cmd-desc-list-users": "Listet alle geschützten Mitglieder auf", - "cmd-desc-list-roles": "Listet alle geschützten Rollen auf", - "cmd-desc-list-wl": "Listet alle erlaubten Rollen auf", - "embed-history-title": "Ping-Verlauf: %u", - "embed-leaver-warning": "Dieser Benutzer hat den Server um %t verlassen. Diese Logs bleiben bis zur automatischen Löschung.", - "no-data-found": "Keine Logs gefunden für diesen Benutzer.", - "embed-actions-title": "Moderations-Verlauf: %u", - "label-reason": "Grund", - "actions-retention-note": "Denken Sie daran: Moderationsmaßnahmen werden je nach Konfiguration 1 - 12 Monate lang beibehalten.", - "no-permission": "Sie verfügen nicht über ausreichende Berechtigungen, um diesen Befehl zu verwenden.", - "panel-title": "Benutzer-Panel: %u", - "panel-description": "Daten für %u (%i) verwalten und anzeigen. Sehen Sie sich eine kurze Zusammenfassung ihres Ping- und Moderationsverlaufs an oder löschen Sie alle für diesen Benutzer gespeicherten Daten (Risky).", - "btn-history": "Ping-Verlauf", - "btn-actions": "Aktions-Verlauf", - "btn-delete": "Alle daten löschen (Riskant)", - "list-roles-title": "Geschützte rollen", - "list-members-title": "Geschützte mitglieder", - "list-whitelist-title": "Erlaubte rollen", - "list-empty": "Keine konfiguriert.", - "modal-title": "Bestätigen Sie die Löschung der Daten für diesen Benutzer", - "modal-label": "Bestätigen Sie das Löschen der Daten, indem Sie diesen Satz eingeben:", - "modal-phrase": "Ich verstehe, dass alle Daten dieses Benutzers gelöscht werden und dass diese Aktion nicht rückgängig gemacht werden kann.", - "modal-failed": "Der von Ihnen eingegebene Satz ist falsch. Datenlöschung abgebrochen.", - "field-quick-history": "Schnelle verlaufsansicht (Letzte %w wochen)", - "field-quick-desc": "Betrag des Pings-Verlaufs: %p\nBetrag der Moderationsaktionen: %m" - } + } } diff --git a/locales/en.json b/locales/en.json index 7ba530d5..040e4439 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1054,6 +1054,9 @@ "modal-phrase": "I understand that all data of this user will be deleted and that this action cannot be undone.", "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" + "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m", + "history-disabled": "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"storage\" tab in the 'ping-protection' module ^^", + "leaver-warning-long": "User left at %d. These logs will stay until automatic deletion.", + "leaver-warning-short": "User left at %d." } } \ No newline at end of file diff --git a/modules/ping-protection/commands/ping-protection-cmd.js b/modules/ping-protection/commands/ping-protection-cmd.js deleted file mode 100644 index fb2c9e9e..00000000 --- a/modules/ping-protection/commands/ping-protection-cmd.js +++ /dev/null @@ -1,131 +0,0 @@ -const { - fetchModHistory, - getPingCountInWindow, - generateHistoryResponse, - generateActionsResponse -} = require('../ping-protection'); -const { embedType } = require('../../../src/functions/helpers'); -const { localize } = require('../../../src/functions/localize'); -const { MessageActionRow, MessageButton } = require('discord.js'); -// Commands list and info -module.exports.config = { - name: 'ping-protection', - description: localize('ping-protection', 'cmd-desc-module'), - usage: '/ping-protection', - type: 'slash', - options: [ - { - type: 'SUB_COMMAND_GROUP', - name: 'user', - description: localize('ping-protection', 'cmd-desc-group-user'), - options: [ - { type: 'SUB_COMMAND', name: 'history', description: localize('ping-protection', 'cmd-desc-history'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] }, - { type: 'SUB_COMMAND', name: 'actions-history', description: localize('ping-protection', 'cmd-desc-actions'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] }, - { type: 'SUB_COMMAND', name: 'panel', description: localize('ping-protection', 'cmd-desc-panel'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] } - ] - }, - { - type: 'SUB_COMMAND_GROUP', - name: 'list', - description: localize('ping-protection', 'cmd-desc-group-list'), - options: [ - { type: 'SUB_COMMAND', name: 'users', description: localize('ping-protection', 'cmd-desc-list-users') }, - { type: 'SUB_COMMAND', name: 'roles', description: localize('ping-protection', 'cmd-desc-list-roles') }, - { type: 'SUB_COMMAND', name: 'whitelisted', description: localize('ping-protection', 'cmd-desc-list-wl') } - ] - } - ] -}; -// Main commands handler -module.exports.run = async function (interaction) { - if (!interaction.guild) return; - - const group = interaction.options.getSubcommandGroup(false); - const subCmd = interaction.options.getSubcommand(false); - const config = interaction.client.configurations['ping-protection']['configuration']; - const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || - (interaction.client.config.admins || []).includes(interaction.user.id); -// Handles subcommands - // Subcommand user - if (group === 'user') { - const user = interaction.options.getUser('user'); - - if (subCmd === 'history') { - const replyOptions = await generateHistoryResponse(interaction.client, user.id, 1); - replyOptions.ephemeral = false; - await interaction.reply(replyOptions); - } - - else if (subCmd === 'actions-history') { - const replyOptions = await generateActionsResponse(interaction.client, user.id, 1); - replyOptions.ephemeral = false; - await interaction.reply(replyOptions); - } - - else if (subCmd === 'panel') { - if (!isAdmin) return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); - - const pingerId = user.id; - const storageConfig = interaction.client.configurations['ping-protection']['storage']; - const timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; - - const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeWeeks); - const modData = await fetchModHistory(interaction.client, pingerId, 1, 1000); - - const row = new MessageActionRow().addComponents( - new MessageButton().setCustomId(`ping-protection_history_${user.id}`).setLabel(localize('ping-protection', 'btn-history')).setStyle('SECONDARY'), - new MessageButton().setCustomId(`ping-protection_actions_${user.id}`).setLabel(localize('ping-protection', 'btn-actions')).setStyle('SECONDARY'), - new MessageButton().setCustomId(`ping-protection_delete_${user.id}`).setLabel(localize('ping-protection', 'btn-delete')).setStyle('DANGER') - ); - - const replyOptions = embedType({ - _schema: 'v3', - embeds: [{ - title: localize('ping-protection', 'panel-title', { u: user.tag }), - description: localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id }), - color: 'BLUE', - thumbnailURL: user.displayAvatarURL({ dynamic: true }), - fields: [{ - name: localize('ping-protection', 'field-quick-history', {w: timeframeWeeks}), - value: localize('ping-protection', 'field-quick-desc', { p: pingCount, m: modData.total }), - inline: false - }] - }] - }); - - replyOptions.components = [row]; - replyOptions.ephemeral = false; - await interaction.reply(replyOptions); - } - } - // Subcommand list - else if (group === 'list') { - let contentList = []; - let title = ""; - - if (subCmd === 'roles') { - title = localize('ping-protection', 'list-roles-title'); - contentList = config.protectedRoles.map(id => `<@&${id}>`); - } else if (subCmd === 'users') { - title = localize('ping-protection', 'list-members-title'); - contentList = config.protectedUsers.map(id => `<@${id}>`); - } else if (subCmd === 'whitelisted') { - title = localize('ping-protection', 'list-whitelist-title'); - contentList = config.ignoredRoles.map(id => `<@&${id}>`); - } - - if (contentList.length === 0) contentList = [localize('ping-protection', 'list-empty')]; - - const replyOptions = embedType({ - _schema: 'v3', - embeds: [{ - title: title, - description: contentList.join('\n'), - color: 'GREEN' - }] - }); - - replyOptions.ephemeral = false; - await interaction.reply(replyOptions); - } -}; \ No newline at end of file diff --git a/modules/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js new file mode 100644 index 00000000..56c86f3a --- /dev/null +++ b/modules/ping-protection/commands/ping-protection.js @@ -0,0 +1,133 @@ +const { + fetchModHistory, + getPingCountInWindow, + generateHistoryResponse, + generateActionsResponse +} = require('../ping-protection'); +const { localize } = require('../../../src/functions/localize'); +const { MessageActionRow, MessageButton, MessageEmbed } = require('discord.js'); +// Command definition +module.exports.config = { + name: 'ping-protection', + description: localize('ping-protection', 'cmd-desc-module'), + usage: '/ping-protection', + type: 'slash', + options: [ + { + type: 'SUB_COMMAND_GROUP', + name: 'user', + description: localize('ping-protection', 'cmd-desc-group-user'), + options: [ + { type: 'SUB_COMMAND', name: 'history', description: localize('ping-protection', 'cmd-desc-history'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] }, + { type: 'SUB_COMMAND', name: 'actions-history', description: localize('ping-protection', 'cmd-desc-actions'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] }, + { type: 'SUB_COMMAND', name: 'panel', description: localize('ping-protection', 'cmd-desc-panel'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'list', + description: localize('ping-protection', 'cmd-desc-group-list'), + options: [ + { type: 'SUB_COMMAND', name: 'users', description: localize('ping-protection', 'cmd-desc-list-users') }, + { type: 'SUB_COMMAND', name: 'roles', description: localize('ping-protection', 'cmd-desc-list-roles') }, + { type: 'SUB_COMMAND', name: 'whitelisted', description: localize('ping-protection', 'cmd-desc-list-wl') } + ] + } + ] +}; + +module.exports.run = async function (interaction) { + const group = interaction.options.getSubcommandGroup(false); + const sub = interaction.options.getSubcommand(false); + + if (group) { + return module.exports.subcommands[group][sub](interaction); + } + return module.exports.subcommands[sub](interaction); +}; + +// Handles subcommands +module.exports.subcommands = { + 'user': { + 'history': async function (interaction) { + const user = interaction.options.getUser('user'); + const payload = await generateHistoryResponse(interaction.client, user.id, 1); + await interaction.reply(payload); + }, + 'actions-history': async function (interaction) { + const user = interaction.options.getUser('user'); + const payload = await generateActionsResponse(interaction.client, user.id, 1); + await interaction.reply(payload); + }, + 'panel': async function (interaction) { + const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || + (interaction.client.config.admins || []).includes(interaction.user.id); + if (!isAdmin) return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); + + const user = interaction.options.getUser('user'); + const pingerId = user.id; + const storageConfig = interaction.client.configurations['ping-protection']['storage']; + const timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + + const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeWeeks); + const modData = await fetchModHistory(interaction.client, pingerId, 1, 1000); + + const row = new MessageActionRow().addComponents( + new MessageButton().setCustomId(`ping-protection_history_${user.id}`).setLabel(localize('ping-protection', 'btn-history')).setStyle('SECONDARY'), + new MessageButton().setCustomId(`ping-protection_actions_${user.id}`).setLabel(localize('ping-protection', 'btn-actions')).setStyle('SECONDARY'), + new MessageButton().setCustomId(`ping-protection_delete_${user.id}`).setLabel(localize('ping-protection', 'btn-delete')).setStyle('DANGER') + ); + + const embed = new MessageEmbed() + .setTitle(localize('ping-protection', 'panel-title', { u: user.tag })) + .setDescription(localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id })) + .setColor('BLUE') + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .addFields([{ + name: localize('ping-protection', 'field-quick-history', {w: timeframeWeeks}), + value: localize('ping-protection', 'field-quick-desc', { p: pingCount, m: modData.total }), + inline: false + }]); + + await interaction.reply({ embeds: [embed], components: [row], ephemeral: false }); + } + }, + 'list': { + 'users': async function (interaction) { + await listHandler(interaction, 'users'); + }, + 'roles': async function (interaction) { + await listHandler(interaction, 'roles'); + }, + 'whitelisted': async function (interaction) { + await listHandler(interaction, 'whitelisted'); + } + } +}; + +// Handles list subcommands +async function listHandler(interaction, type) { + const config = interaction.client.configurations['ping-protection']['configuration']; + let contentList = []; + let title = ""; + + if (type === 'roles') { + title = localize('ping-protection', 'list-roles-title'); + contentList = config.protectedRoles.map(id => `<@&${id}>`); + } else if (type === 'users') { + title = localize('ping-protection', 'list-members-title'); + contentList = config.protectedUsers.map(id => `<@${id}>`); + } else if (type === 'whitelisted') { + title = localize('ping-protection', 'list-whitelist-title'); + contentList = config.ignoredRoles.map(id => `<@&${id}>`); + } + + if (contentList.length === 0) contentList = [localize('ping-protection', 'list-empty')]; + + const embed = new MessageEmbed() + .setTitle(title) + .setDescription(contentList.join('\n')) + .setColor('GREEN'); + + await interaction.reply({ embeds: [embed], ephemeral: false }); +} \ No newline at end of file diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 83e67604..7ba367aa 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -1,62 +1,88 @@ { "filename": "configuration.json", "humanName": { - "en": "General Configuration", - "de": "Allgemeine Konfiguration" + "en": "General Configuration" }, "description": { - "en": "Configure protected users/roles, whitelisted roles/members, ignored channels and the notification message.", - "de": "Konfiguriere geschützte Benutzer/Rollen, erlaubte Rollen/Mitglieder, ignorierte Kanäle und die Benachrichtigungsnachricht." + "en": "Configure protected users/roles, whitelisted roles/members, ignored channels and the notification message." }, "content": [ { "name": "protectedRoles", - "humanName": { "en": "Protected Roles", "de": "Geschützte Rollen" }, - "description": { "en": "Members with these roles will trigger protection when pinged.", "de": "Mitglieder mit diesen Rollen lösen den Schutz aus." }, + "humanName": { + "en": "Protected Roles" + }, + "description": { + "en": "Members with these roles will trigger protection when pinged." + }, "type": "array", "content": "roleID", - "default": { "en": [] } + "default": { + "en": [] + } }, { "name": "protectedUsers", - "humanName": { "en": "Protected Users", "de": "Geschützte Benutzer" }, - "description": { "en": "Specific users who are protected from pings.", "de": "Spezifische Benutzer, die geschützt sind." }, + "humanName": { + "en": "Protected Users" + }, + "description": { + "en": "Specific users who are protected from pings." + }, "type": "array", "content": "userID", - "default": { "en": [] } + "default": { + "en": [] + } }, { "name": "ignoredRoles", - "humanName": { "en": "Whitelisted Roles", "de": "Erlaubte Rollen" }, - "description": { "en": "Roles allowed to ping protected members or roles.", "de": "Rollen, die pingen dürfen." }, + "humanName": { + "en": "Whitelisted Roles" + }, + "description": { + "en": "Roles allowed to ping protected members or roles." + }, "type": "array", "content": "roleID", - "default": { "en": [] } + "default": { + "en": [] + } }, { "name": "ignoredChannels", - "humanName": { "en": "Ignored Channels", "de": "Ignorierte Kanäle" }, - "description": { "en": "Pings in these channels are ignored.", "de": "Pings hier werden ignoriert." }, + "humanName": { + "en": "Ignored Channels" + }, + "description": { + "en": "Pings in these channels are ignored." + }, "type": "array", "content": "channelID", - "default": { "en": [] } + "default": { + "en": [] + } }, { "name": "allowReplyPings", - "humanName": { "en": "Allow Reply Pings", "de": "Antwort-Pings erlauben" }, + "humanName": { + "en": "Allow Reply Pings" + }, "description": { - "en": "If enabled, replying to a protected user (with mention ON) is allowed.", - "de": "Wenn aktiviert, sind Antworten (mit Mention) an geschützte Benutzer erlaubt." + "en": "If enabled, replying to a protected user (with mention ON) is allowed." }, "type": "boolean", - "default": { "en": false } + "default": { + "en": false + } }, { "name": "pingWarningMessage", - "humanName": { "en": "Warning Message", "de": "Warnnachricht" }, + "humanName": { + "en": "Warning Message" + }, "description": { - "en": "The message that gets sent to the user when they ping someone.", - "de": "Die Nachricht, die an den Benutzer gesendet wird." + "en": "The message that gets sent to the user when they ping someone." }, "type": "string", "allowEmbed": true, @@ -80,12 +106,6 @@ "description": "You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping protection list roles` or `/ping protection list members`.\n\nIf you were replying, make sure to turn off the mention in the reply.", "image": "https://c.tenor.com/zRR5cYKSIV0AAAAd/tenor.gif", "color": "RED" - }, - "de": { - "title": "Du darfst %target-name% nicht pingen!", - "description": "Du darfst %target-mention% aufgrund deiner Rolle nicht pingen. Du kannst sehen, welche Rollen/Mitglieder du nicht pingen darfst, indem du `/ping protection list roles` oder `/ping protection list members` benutzt.\n\nFalls du geantwortet hast, stelle sicher, dass die Erwähnung in der Antwort deaktiviert ist.", - "image": "https://c.tenor.com/zRR5cYKSIV0AAAAd/tenor.gif", - "color": "RED" } } } diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index fb04f392..fbb3b6b9 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -1,70 +1,110 @@ { "filename": "moderation.json", - "humanName": { - "en": "Moderation Actions", - "de": "Bestrafungsaktionen" + "humanName": { + "en": "Moderation Actions" }, "description": { - "en": "Define triggers for punishments.", - "de": "Definiere Auslöser für Bestrafungen." + "en": "Define triggers for punishments." }, "configElements": true, "content": [ { "name": "enableModeration", - "humanName": { "en": "Enable Moderation Actions", "de": "Bestrafungsaktionen aktivieren" }, - "description": { "en": "If enabled, members who ping protected users/roles repeatedly will be punished.", "de": "Wenn aktiviert, werden Mitglieder, die geschützte Benutzer/Rollen wiederholt pingen, bestraft." }, + "humanName": { + "en": "Enable Moderation Actions" + }, + "description": { + "en": "If enabled, members who ping protected users/roles repeatedly will be punished. NOTE: THIS WILL NOT WORK WHEN PINGS HISTORY LOGGING IS DISABLED!" + }, "type": "boolean", - "default": { "en": false } + "default": { + "en": false + }, + "elementToggle": true }, { "name": "advancedConfiguration", - "humanName": { "en": "Use Advanced Configuration", "de": "Erweiterte Konfiguration verwenden" }, - "description": { "en": "If enabled, the advanced configuration below will be used and the basic configuration will be ignored.", "de": "Wenn aktiviert, wird die untenstehende erweiterte Konfiguration verwendet und die Basis-Konfiguration ignoriert." }, + "humanName": { + "en": "Use Advanced Configuration" + }, + "description": { + "en": "If enabled, the advanced configuration below will be used and the basic configuration will be ignored. This allows you to customize the custom timeframe. In the future, this will allow for almost full customization." + }, "type": "boolean", - "default": { "en": false }, - "dependsOn": "enableModeration" + "default": { + "en": false + } }, { "name": "pingsCountBasic", - "humanName": { "en": "Pings to trigger moderation", "de": "Pings für Bestrafung" }, - "description": { "en": "The amount of pings required to trigger a moderation action (Uses 'Ping History Retention' timeframe).", "de": "Anzahl Pings für Bestrafung (Nutzt 'Ping-Verlauf Speicherzeit')." }, + "humanName": { + "en": "Pings to trigger moderation" + }, + "description": { + "en": "The amount of pings required to trigger a moderation action (Uses 'Ping History Retention' timeframe)." + }, "type": "integer", - "default": { "en": 10 }, - "dependsOn": "enableModeration" + "default": { + "en": 10 + } }, { "name": "pingsCountAdvanced", - "humanName": { "en": "Pings to trigger (Advanced)", "de": "Pings für Bestrafung (Erweitert)" }, - "description": { "en": "The amount of pings required in the custom timeframe below.", "de": "Anzahl Pings im unten gewählten Zeitraum." }, + "humanName": { + "en": "Pings to trigger (Advanced)" + }, + "description": { + "en": "The amount of pings required in the custom timeframe below." + }, "type": "integer", - "default": { "en": 5 }, + "default": { + "en": 5 + }, "dependsOn": "advancedConfiguration" }, { "name": "timeframeWeeks", - "humanName": { "en": "Timeframe (Weeks)", "de": "Zeitraum (Wochen)" }, - "description": { "en": "In how many weeks must these pings occur?", "de": "In wie vielen Wochen müssen diese Pings passieren?" }, + "humanName": { + "en": "Timeframe (Weeks)" + }, + "description": { + "en": "In how many weeks must these pings occur?" + }, "type": "integer", - "default": { "en": 1 }, + "default": { + "en": 1 + }, "dependsOn": "advancedConfiguration" }, { "name": "actionType", - "humanName": { "en": "Action", "de": "Aktion" }, - "description": { "en": "What punishment should be applied?", "de": "Welche Strafe soll verhängt werden?" }, + "humanName": { + "en": "Action" + }, + "description": { + "en": "What punishment should be applied?" + }, "type": "select", - "content": ["MUTE", "KICK"], - "default": { "en": "MUTE" }, - "dependsOn": "enableModeration" + "content": [ + "MUTE", + "KICK" + ], + "default": { + "en": "MUTE" + } }, { "name": "muteDuration", - "humanName": { "en": "Mute Duration (only if action type is MUTE)", "de": "Mute-Dauer (nur wenn Aktionstyp MUTE ist)" }, - "description": { "en": "How long to mute the user? (in minutes)", "de": "Wie lange soll der User gemutet werden? (in Minuten)" }, + "humanName": { + "en": "Mute Duration (only if action type is MUTE)" + }, + "description": { + "en": "How long to mute the user? (in minutes)" + }, "type": "integer", - "default": { "en": 60 }, - "dependsOn": "enableModeration" + "default": { + "en": 60 + } } ] } \ No newline at end of file diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json index 3dfa6245..d031eaa9 100644 --- a/modules/ping-protection/configs/storage.json +++ b/modules/ping-protection/configs/storage.json @@ -1,66 +1,83 @@ { "filename": "storage.json", - "humanName": { "en": "Data Storage", "de": "Datenspeicherung" }, + "humanName": { + "en": "Data Storage" + }, "description": { - "en": "Configure how long moderation logs and leaver data are kept.", - "de": "Konfiguriere, wie lange Mod-Logs und Leaver-Daten gespeichert werden." + "en": "Configure how long moderation logs and leaver data are kept." }, "content": [ { "name": "enablePingHistory", - "humanName": { "en": "Enable Ping History", "de": "Ping-Verlauf aktivieren" }, - "description": { "en": "If enabled, the bot will keep a history of pings to enforce moderation actions.", "de": "Wenn aktiviert, speichert der Bot einen Ping-Verlauf, um Bestrafungsaktionen durchzusetzen." }, + "humanName": { + "en": "Enable Ping History" + }, + "description": { + "en": "If enabled, the bot will keep a history of pings to enforce moderation actions." + }, "type": "boolean", - "default": { "en": true } + "default": { + "en": true + } }, { "name": "pingHistoryRetention", - "humanName": { "en": "Ping History Retention", "de": "Ping-Verlauf Speicherzeit" }, + "humanName": { + "en": "Ping History Retention" + }, "description": { - "en": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 24 weeks (6 months). This is the length factor of the 'Basic' punishment timeframe.", - "de": "Entscheidet, wie lange Ping-Logs gespeichert werden. Minimum 1 Woche, Maximum 24 Wochen. Dies ist der Faktor für den 'Basis'-Bestrafungszeitraum." + "en": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 24 weeks (6 months). This is the length factor of the 'Basic' punishment timeframe." + }, + "type": "integer", + "default": { + "en": 12 }, - "type": "select", - "content": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], - "default": { "en": 12 }, + "minValue": "4", + "maxValue": "24", "dependsOn": "enablePingHistory" }, - { - "name": "enableModLogHistory", - "humanName": { "en": "Enable Moderation Log History", "de": "Mod-Log Verlauf aktivieren" }, - "description": { "en": "If enabled, the bot will keep a history of moderation actions taken by the ping-protection module.", "de": "Wenn aktiviert, speichert der Bot einen Verlauf der Bestrafungsaktionen des Ping-Schutz Moduls." }, - "type": "boolean", - "default": { "en": false } - }, { "name": "modLogRetention", - "humanName": { "en": "Moderation Log Retention (Months)", "de": "Mod-Log Speicherzeit (Monate)" }, + "humanName": { + "en": "Moderation Log Retention (Months)" + }, "description": { - "en": "How long to keep records of punishments (1-12 Months).", - "de": "Wie lange Bestrafungsprotokolle gespeichert werden (1-12 Monate)." + "en": "How long to keep records of punishments (1-12 Months). This is applied when moderation actions are enabled." }, - "type": "select", - "content": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], - "default": { "en": 6 }, - "dependsOn": "enableModLogHistory" + "type": "integer", + "default": { + "en": 6 + }, + "minValue": "1", + "maxValue": "12" }, { "name": "enableLeaverDataRetention", - "humanName": { "en": "Keep user logs after they leave", "de": "Benutzerprotokolle nach Verlassen speichern" }, - "description": { "en": "If enabled, the bot will keep a history of the user after they leave.", "de": "Wenn aktiviert, speichert der Bot einen Verlauf des Benutzers, nachdem er den Server verlassen hat." }, + "humanName": { + "en": "Keep user logs after they leave" + }, + "description": { + "en": "If enabled, the bot will keep a history of the user after they leave." + }, "type": "boolean", - "default": { "en": true } + "default": { + "en": true + } }, { "name": "leaverRetention", - "humanName": { "en": "Leaver Data Retention (Days)", "de": "Leaver-Daten Speicherzeit (Tage)" }, + "humanName": { + "en": "Leaver Data Retention (Days)" + }, "description": { - "en": "How long to keep data after a user leaves (1-7 Days).", - "de": "Wie lange Daten gespeichert bleiben, nachdem ein User den Server verlässt." + "en": "How long to keep data after a user leaves (1-7 Days)." + }, + "type": "integer", + "default": { + "en": 1 }, - "type": "select", - "content": [1, 2, 3, 4, 5, 6, 7], - "default": { "en": 1 }, + "minValue": "1", + "maxValue": "7", "dependsOn": "enableLeaverDataRetention" } ] diff --git a/modules/ping-protection/events/botReady.js b/modules/ping-protection/events/botReady.js index 72250b6b..dd32aa39 100644 --- a/modules/ping-protection/events/botReady.js +++ b/modules/ping-protection/events/botReady.js @@ -1,16 +1,7 @@ const { enforceRetention } = require('../ping-protection'); const schedule = require('node-schedule'); -const { localize } = require('../../../src/functions/localize'); module.exports.run = async function (client) { - try { - await client.models['ping-protection']['PingHistory'].sync(); - await client.models['ping-protection']['ModerationLog'].sync(); - await client.models['ping-protection']['LeaverData'].sync(); - - } catch (e) { - } - await enforceRetention(client); // Schedules daily retention at 03:00 local bot time with cronjob diff --git a/modules/ping-protection/events/guildMemberAdd.js b/modules/ping-protection/events/guildMemberAdd.js index f81a1000..8420f997 100644 --- a/modules/ping-protection/events/guildMemberAdd.js +++ b/modules/ping-protection/events/guildMemberAdd.js @@ -6,7 +6,7 @@ const { markUserAsRejoined } = require('../ping-protection'); module.exports.run = async function (client, member) { if (!client.botReadyAt) return; - if (member.guild.id !== client.config.guildID) return; + if (member.guild.id !== client.guildID) return; await markUserAsRejoined(client, member.id); }; \ No newline at end of file diff --git a/modules/ping-protection/events/guildMemberRemove.js b/modules/ping-protection/events/guildMemberRemove.js index 3bcd73cc..58fa7704 100644 --- a/modules/ping-protection/events/guildMemberRemove.js +++ b/modules/ping-protection/events/guildMemberRemove.js @@ -6,7 +6,7 @@ const { markUserAsLeft, deleteAllUserData } = require('../ping-protection'); module.exports.run = async function (client, member) { if (!client.botReadyAt) return; - if (member.guild.id !== client.config.guildID) return; + if (member.guild.id !== client.guildID) return; const storageConfig = client.configurations['ping-protection']['storage']; diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index e7ba7f64..41a944db 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -18,7 +18,6 @@ module.exports.run = async function (client, interaction) { return; } - // Moderation history pagination if (interaction.customId.startsWith('ping-protection_mod-page_')) { const parts = interaction.customId.split('_'); const userId = parts[2]; @@ -32,13 +31,11 @@ module.exports.run = async function (client, interaction) { // Panel buttons const [prefix, action, userId] = interaction.customId.split('_'); - // Checks for permissions const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || (client.config.admins || []).includes(interaction.user.id); - if (action !== 'delete' && !isAdmin && !prefix.includes('page')) { - if (['history', 'actions', 'delete'].includes(action)) { - if (!isAdmin) return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); - } + + if (['history', 'actions', 'delete'].includes(action)) { + if (!isAdmin) return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); } if (action === 'history') { @@ -52,7 +49,6 @@ module.exports.run = async function (client, interaction) { replyOptions.ephemeral = false; await interaction.reply(replyOptions); } - // Handles data deletion button else if (action === 'delete') { const modal = new Modal() .setCustomId(`ping-protection_confirm-delete_${userId}`) @@ -72,7 +68,6 @@ module.exports.run = async function (client, interaction) { } } - // Modal Handling if (interaction.isModalSubmit() && interaction.customId.startsWith('ping-protection_confirm-delete_')) { const userId = interaction.customId.split('_')[2]; const userInput = interaction.fields.getTextInputValue('confirmation_text'); @@ -80,7 +75,7 @@ module.exports.run = async function (client, interaction) { if (userInput === requiredPhrase) { await deleteAllUserData(client, userId); - await interaction.reply({ content: `✅ ${localize('ping-protection', 'log-manual-delete', {u: userId})}`, ephemeral: true }); + await interaction.reply({ content: `✅ ${localize('ping-protection', 'log-manual-delete-logs', {u: userId})}`, ephemeral: true }); } else { await interaction.reply({ content: `❌ ${localize('ping-protection', 'modal-failed')}`, ephemeral: true }); } diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index 01691d4e..ddb916a3 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -5,55 +5,48 @@ const { sendPingWarning } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); -// Messages handler +// Handles messages module.exports.run = async function (client, message) { if (!client.botReadyAt) return; if (!message.guild) return; if (message.author.bot) return; - if (message.guild.id !== client.config.guildID) return; + if (message.guild.id !== client.guildID) return; const config = client.configurations['ping-protection']['configuration']; const storageConfig = client.configurations['ping-protection']['storage']; const moderationRules = client.configurations['ping-protection']['moderation']; - if (!config || !moderationRules || !Array.isArray(moderationRules) || moderationRules.length === 0) return; + if (!config) return; - const rule1 = moderationRules[0]; - - // Checks for ignored channels and roles if (config.ignoredChannels.includes(message.channel.id)) return; if (message.member.roles.cache.some(role => config.ignoredRoles.includes(role.id))) return; - // Detects pings const pingedProtectedRole = message.mentions.roles.some(role => config.protectedRoles.includes(role.id)); const pingedProtectedUser = message.mentions.users.some(user => config.protectedUsers.includes(user.id)); if (!pingedProtectedRole && !pingedProtectedUser) return; - // Identifies target const targetUser = message.mentions.users.find(u => config.protectedUsers.includes(u.id)); const targetRole = message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); const target = targetUser || targetRole; - const targetName = target.tag || target.name || target.id; - // Checks if ping history logging is enabled - if (!storageConfig || !storageConfig.enablePingHistory) { - client.logger.info(`[ping-protection] User ${message.author.tag} pinged ${targetName}. Pings history logging is disabled, moderation actions cannot be done.`); - + if (!storageConfig || !storageConfig.enablePingHistory) { await sendPingWarning(client, message, target, config); return; } - // Processes the ping let pingCount = 0; const pingerId = message.author.id; - let requiredCount = 0; - let generatedReason = ""; let timeframeWeeks = 12; + let rule1 = null; + + if (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) { + rule1 = moderationRules[0]; + } try { await addPing(client, message, target); - if (rule1.advancedConfiguration) { + if (rule1 && !!rule1.advancedConfiguration) { timeframeWeeks = rule1.timeframeWeeks; } else { timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; @@ -61,28 +54,24 @@ module.exports.run = async function (client, message) { pingCount = await getPingCountInWindow(client, pingerId, timeframeWeeks); - } catch (e) { - client.logger.error(`[ping-protection] Database interaction failed for ${message.author.tag}: ${e}`); - } + } catch (e) {} - // Sends warning message await sendPingWarning(client, message, target, config); - if (!rule1.enableModeration) return; + if (!rule1 || !rule1.enableModeration) return; - if (rule1.advancedConfiguration) { + let requiredCount = 0; + let generatedReason = ""; + + if (!!rule1.advancedConfiguration) { requiredCount = rule1.pingsCountAdvanced; generatedReason = localize('ping-protection', 'reason-advanced', { c: pingCount, w: rule1.timeframeWeeks }); } else { requiredCount = rule1.pingsCountBasic; generatedReason = localize('ping-protection', 'reason-basic', { c: pingCount, w: timeframeWeeks }); } - - client.logger.info(`[ping-protection] User ${message.author.tag} pinged ${targetName}. Count: ${pingCount}/${requiredCount}`); if (pingCount >= requiredCount) { - - // Checks for recent moderation to prevent spam actions const { Op } = require('sequelize'); const oneMinuteAgo = new Date(new Date() - 60000); @@ -95,15 +84,13 @@ module.exports.run = async function (client, message) { }); if (recentLog) return; - } catch (e) { - } + } catch (e) {} let memberToPunish = message.member; if (!memberToPunish) { try { memberToPunish = await message.guild.members.fetch(message.author.id); } catch (fetchError) { - client.logger.error(`[ping-protection] Failed to fetch member ${message.author.tag} for punishment.`); return; } } diff --git a/modules/ping-protection/models/LeaverData.js b/modules/ping-protection/models/LeaverData.js index 6d88488e..f70b6c9b 100644 --- a/modules/ping-protection/models/LeaverData.js +++ b/modules/ping-protection/models/LeaverData.js @@ -1,6 +1,6 @@ const { DataTypes, Model } = require('sequelize'); -module.exports = class LeaverData extends Model { +module.exports = class PingProtectionLeaverData extends Model { static init(sequelize) { return super.init({ userId: { diff --git a/modules/ping-protection/models/ModerationLog.js b/modules/ping-protection/models/ModerationLog.js index 892caaad..c90099f8 100644 --- a/modules/ping-protection/models/ModerationLog.js +++ b/modules/ping-protection/models/ModerationLog.js @@ -1,6 +1,6 @@ const { DataTypes, Model } = require('sequelize'); -module.exports = class ModerationLog extends Model { +module.exports = class PingProtectionModerationLog extends Model { static init(sequelize) { return super.init({ id: { diff --git a/modules/ping-protection/models/PingHistory.js b/modules/ping-protection/models/PingHistory.js index 2f6c8a74..4e1a2b0f 100644 --- a/modules/ping-protection/models/PingHistory.js +++ b/modules/ping-protection/models/PingHistory.js @@ -1,6 +1,6 @@ const { DataTypes, Model } = require('sequelize'); -module.exports = class PingHistory extends Model { +module.exports = class PingProtectionPingHistory extends Model { static init(sequelize) { return super.init({ userId: { diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index a4ef9255..b49bf799 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -4,11 +4,10 @@ * @author itskevinnn */ const { Op } = require('sequelize'); -const { MessageActionRow, MessageButton } = require('discord.js'); -const { embedType, formatDiscordUserName, formatDate } = require('../../src/functions/helpers'); +const { MessageActionRow, MessageButton, MessageEmbed } = require('discord.js'); +const { embedType, formatDate } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); -const DISABLED_MSG = "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"storage\" tab in the 'ping-protection' module ^^"; // Core functions and logic async function addPing(client, message, target) { const isRole = !target.username; @@ -55,7 +54,6 @@ async function fetchModHistory(client, userId, page = 1, limit = 8) { }); return { total: count, history: rows }; } catch (e) { - client.logger.error(`[ping-protection] Failed to query ModerationLog: ${e.message}`); return { total: 0, history: [] }; } } @@ -80,9 +78,7 @@ async function sendPingWarning(client, message, target, moduleConfig) { }; const replyOptions = embedType(warningMsg, placeholders); - await message.reply(replyOptions).catch((e) => { - client.logger.debug(`[ping-protection] Failed to send warning: ${e.message}`); - }); + await message.reply(replyOptions).catch(() => {}); } async function executeAction(client, member, rule, reason, storageConfig) { @@ -91,13 +87,11 @@ async function executeAction(client, member, rule, reason, storageConfig) { const botMember = await member.guild.members.fetch(client.user.id); if (botMember.roles.highest.position <= member.roles.highest.position) { - client.logger.warn(`[ping-protection] Hierarchy Failure: Cannot moderate ${member.user.tag}.`); return false; } // Database logging const logDb = async (type, duration = null) => { - if (!storageConfig.enableModLogHistory) return; try { await client.models['ping-protection']['ModerationLog'].create({ victimID: member.id, @@ -105,9 +99,7 @@ async function executeAction(client, member, rule, reason, storageConfig) { actionDuration: duration, reason }); - } catch (dbError) { - client.logger.error(`[ping-protection] DB Insert Failed: ${dbError.message}`); - } + } catch (dbError) {} }; if (actionType === 'MUTE') { @@ -116,32 +108,27 @@ async function executeAction(client, member, rule, reason, storageConfig) { try { await member.timeout(durationMs, reason); - client.logger.info(`[ping-protection] Muted ${member.user.tag} for ${rule.muteDuration}m.`); return true; } catch (error) { - client.logger.error(`[ping-protection] Mute failed: ${error.message}`); return false; } } else if (actionType === 'KICK') { await logDb('KICK'); try { await member.kick(reason); - client.logger.info(`[ping-protection] Kicked ${member.user.tag}.`); return true; } catch (error) { - client.logger.error(`[ping-protection] Kick failed: ${error.message}`); return false; } } return false; } -// View generations - +// Generates history and actions responses async function generateHistoryResponse(client, userId, page = 1) { const storageConfig = client.configurations['ping-protection']['storage']; const limit = 8; - const isEnabled = storageConfig.enablePingHistory; + const isEnabled = !!storageConfig.enablePingHistory; let total = 0, history = [], totalPages = 1; @@ -155,29 +142,31 @@ async function generateHistoryResponse(client, userId, page = 1) { const user = await client.users.fetch(userId).catch(() => ({ username: 'Unknown User', displayAvatarURL: () => null })); const leaverData = await getLeaverStatus(client, userId); let description = ""; - + if (leaverData) { const dateStr = formatDate(leaverData.leftAt); if (history.length > 0) { - description += `⚠️ User left at ${dateStr}. These logs will stay until automatic deletion.\n\n`; + description += `⚠️ ${localize('ping-protection', 'leaver-warning-long', { d: dateStr })}\n\n`; } else { - description += `⚠️ User left at ${dateStr}.\n\n`; + description += `⚠️ ${localize('ping-protection', 'leaver-warning-short', { d: dateStr })}\n\n`; } } if (!isEnabled) { - description += DISABLED_MSG; + description += localize('ping-protection', 'history-disabled'); } else if (history.length === 0) { description += localize('ping-protection', 'no-data-found'); } else { const lines = history.map((entry, index) => { const timeString = formatDate(entry.createdAt); let targetString = "Unknown"; + if (entry.targetId) { targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; } else { - targetString = "Detected"; + targetString = "Detected"; } + return `${(page - 1) * limit + index + 1}. **Pinged ${targetString}** at ${timeString}\n[Jump to Message](${entry.messageUrl})`; }); description += lines.join('\n\n'); @@ -189,23 +178,19 @@ async function generateHistoryResponse(client, userId, page = 1) { new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(page >= totalPages || !isEnabled) ); - const replyOptions = embedType({ - _schema: 'v3', - embeds: [{ - title: localize('ping-protection', 'embed-history-title', { u: user.username }), - thumbnailURL: user.displayAvatarURL({ dynamic: true }), - description: description, - color: 'ORANGE' - }] - }); - replyOptions.components = [row]; - return replyOptions; + const embed = new MessageEmbed() + .setTitle(localize('ping-protection', 'embed-history-title', { u: user.username })) + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .setDescription(description) + .setColor('ORANGE'); + + return { embeds: [embed], components: [row], ephemeral: false }; } -// Generates the actions view + async function generateActionsResponse(client, userId, page = 1) { const storageConfig = client.configurations['ping-protection']['storage']; const limit = 8; - const isEnabled = storageConfig.enableModLogHistory; + const isEnabled = true; let total = 0, history = [], totalPages = 1; @@ -219,15 +204,14 @@ async function generateActionsResponse(client, userId, page = 1) { const user = await client.users.fetch(userId).catch(() => ({ username: 'Unknown User', displayAvatarURL: () => null })); let description = ""; - if (!isEnabled) { - description = DISABLED_MSG; - } else if (history.length === 0) { + if (history.length === 0) { description = localize('ping-protection', 'no-data-found'); } else { const lines = history.map((entry, index) => { const duration = entry.actionDuration ? ` (${entry.actionDuration}m)` : ''; const reasonText = entry.reason || localize('ping-protection', 'no-reason') || 'No reason'; const timeString = formatDate(entry.createdAt); + return `${(page - 1) * limit + index + 1}. **${entry.type}${duration}** - ${timeString}\n${localize('ping-protection', 'label-reason')}: ${reasonText}`; }); description = lines.join('\n\n') + `\n\n*${localize('ping-protection', 'actions-retention-note')}*`; @@ -239,21 +223,16 @@ async function generateActionsResponse(client, userId, page = 1) { new MessageButton().setCustomId(`ping-protection_mod-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(page >= totalPages || !isEnabled) ); - const replyOptions = embedType({ - _schema: 'v3', - embeds: [{ - title: localize('ping-protection', 'embed-actions-title', { u: user.username }), - thumbnailURL: user.displayAvatarURL({ dynamic: true }), - description: description, - color: 'RED' - }] - }); - replyOptions.components = [row]; - return replyOptions; + const embed = new MessageEmbed() + .setTitle(localize('ping-protection', 'embed-actions-title', { u: user.username })) + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .setDescription(description) + .setColor('RED'); + + return { embeds: [embed], components: [row], ephemeral: false }; } // Manages data deletion - async function deleteAllUserData(client, userId) { await client.models['ping-protection']['PingHistory'].destroy({ where: { userId: userId } }); await client.models['ping-protection']['ModerationLog'].destroy({ where: { victimID: userId } }); @@ -279,7 +258,7 @@ async function enforceRetention(client) { await client.models['ping-protection']['PingHistory'].destroy({ where: { createdAt: { [Op.lt]: historyCutoff } } }); } - if (storageConfig.enableModLogHistory) { + if (storageConfig.modLogRetention) { const modCutoff = new Date(); modCutoff.setMonth(modCutoff.getMonth() - (storageConfig.modLogRetention || 6)); await client.models['ping-protection']['ModerationLog'].destroy({ where: { createdAt: { [Op.lt]: modCutoff } } }); From 0b4d87b8c9aa458af7f1a056548f4c5254e337a8 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Wed, 17 Dec 2025 17:37:11 +0100 Subject: [PATCH 20/31] Added "automod" abilities - Will now delete the original message by default with the message content and allows to configure both options --- .../configs/configuration.json | 39 ++++++++++++++-- .../ping-protection/events/messageCreate.js | 46 ++++++++++--------- modules/ping-protection/ping-protection.js | 22 +++++++-- 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index a01ccf24..2531db76 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -85,6 +85,31 @@ "en": false } }, + { + "name": "enableAutomod", + "humanName": { + "en": "Enable automod (Deletes original message)" + }, + "description": { + "en": "If enabled, the bot will automatically delete the original message which pinged a protected user/role." + }, + "type": "boolean", + "default": { "en": true } + }, + { + "name": "includeOriginalContent", + "humanName": { + "en": "Allow original message content parameter" + }, + "description": { + "en": "If enabled, you can use the %message-content% parameter in your warning message to show the original message content." + }, + "type": "boolean", + "default": { + "en": true + }, + "dependsOn": "enableAutomod" + }, { "name": "pingWarningMessage", "humanName": { @@ -98,21 +123,27 @@ "params": [ { "name": "target-name", - "description": { "en": "Name of the pinged user/role", "de": "Name des gepingten Benutzers/der Rolle" } + "description": { + "en": "Name of the pinged user/role" + } }, { "name": "target-mention", - "description": { "en": "Mention of the pinged user/role", "de": "Erwähnung des gepingten Benutzers/der Rolle" } + "description": { + "en": "Mention of the pinged user/role" + } }, { "name": "target-id", - "description": { "en": "ID of the pinged user/role", "de": "ID des gepingten Benutzers/der Rolle" } + "description": { + "en": "ID of the pinged user/role" + } } ], "default": { "en": { "title": "You are not allowed to ping %target-name%!", - "description": "You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping protection list roles` or `/ping protection list members`.\n\nIf you were replying, make sure to turn off the mention in the reply.", + "description": "<@%user-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping protection list roles` or `/ping protection list members`.\n\nIf you were replying, make sure to turn off the mention in the reply.", "image": "https://c.tenor.com/zRR5cYKSIV0AAAAd/tenor.gif", "color": "RED" } diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index ddb916a3..d4c2ee22 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -5,6 +5,7 @@ const { sendPingWarning } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); + // Handles messages module.exports.run = async function (client, message) { if (!client.botReadyAt) return; @@ -18,46 +19,48 @@ module.exports.run = async function (client, message) { if (!config) return; + // Checks for ignored channels and roles if (config.ignoredChannels.includes(message.channel.id)) return; if (message.member.roles.cache.some(role => config.ignoredRoles.includes(role.id))) return; const pingedProtectedRole = message.mentions.roles.some(role => config.protectedRoles.includes(role.id)); const pingedProtectedUser = message.mentions.users.some(user => config.protectedUsers.includes(user.id)); + if (!pingedProtectedRole && !pingedProtectedUser) return; const targetUser = message.mentions.users.find(u => config.protectedUsers.includes(u.id)); const targetRole = message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); const target = targetUser || targetRole; - if (!storageConfig || !storageConfig.enablePingHistory) { - await sendPingWarning(client, message, target, config); - return; - } - +// Processes the ping let pingCount = 0; const pingerId = message.author.id; let timeframeWeeks = 12; - let rule1 = null; - - if (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) { - rule1 = moderationRules[0]; - } + let rule1 = (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) ? moderationRules[0] : null; - try { - await addPing(client, message, target); + if (!!storageConfig && !!storageConfig.enablePingHistory) { + try { + await addPing(client, message, target); - if (rule1 && !!rule1.advancedConfiguration) { - timeframeWeeks = rule1.timeframeWeeks; - } else { - timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; - } + if (rule1 && !!rule1.advancedConfiguration) { + timeframeWeeks = rule1.timeframeWeeks; + } else { + timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + } - pingCount = await getPingCountInWindow(client, pingerId, timeframeWeeks); + pingCount = await getPingCountInWindow(client, pingerId, timeframeWeeks); + } catch (e) { + } + } - } catch (e) {} - await sendPingWarning(client, message, target, config); + + if (!!config.enableAutomod) { + await message.delete().catch(() => { + }); + } + // Moderation action logic if (!rule1 || !rule1.enableModeration) return; let requiredCount = 0; @@ -71,6 +74,7 @@ module.exports.run = async function (client, message) { generatedReason = localize('ping-protection', 'reason-basic', { c: pingCount, w: timeframeWeeks }); } + // Checks for recent punishments to prevent duplicate actions if (pingCount >= requiredCount) { const { Op } = require('sequelize'); const oneMinuteAgo = new Date(new Date() - 60000); @@ -94,7 +98,7 @@ module.exports.run = async function (client, message) { return; } } - + await executeAction(client, memberToPunish, rule1, generatedReason, storageConfig); } }; \ No newline at end of file diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index b49bf799..cc6d435f 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -63,22 +63,34 @@ async function getLeaverStatus(client, userId) { } // Action logic - async function sendPingWarning(client, message, target, moduleConfig) { const warningMsg = moduleConfig.pingWarningMessage; if (!warningMsg) return; const targetName = target.name || target.tag || target.username || 'Unknown'; const targetMention = target.toString(); - + const placeholders = { '%target-name%': targetName, '%target-mention%': targetMention, - '%target-id%': target.id + '%target-id%': target.id, + '%user-id%': message.author.id }; - const replyOptions = embedType(warningMsg, placeholders); - await message.reply(replyOptions).catch(() => {}); + let replyOptions = embedType(warningMsg, placeholders); + + if (!!moduleConfig.enableAutomod && !!moduleConfig.includeOriginalContent && message.content) { + const contentHeader = `\n\n**Original message content:**\n${message.content}`; + if (replyOptions.embeds && replyOptions.embeds.length > 0) { + replyOptions.embeds[0].description = (replyOptions.embeds[0].description || "") + contentHeader; + } else { + replyOptions.content = (replyOptions.content || "") + contentHeader; + } + } + + return message.reply(replyOptions).catch(async () => { + return message.channel.send(replyOptions).catch(() => {}); + }); } async function executeAction(client, member, rule, reason, storageConfig) { From 347b67437251e0efefade625d29d73e1db429b00 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 18 Dec 2025 16:59:46 +0100 Subject: [PATCH 21/31] (not working correctly) added automod integration and some small changes --- locales/en.json | 3 +- .../commands/ping-protection.js | 14 +- .../configs/configuration.json | 29 +- .../events/autoModerationActionExecution.js | 63 ++++ modules/ping-protection/events/botReady.js | 7 +- .../ping-protection/events/messageCreate.js | 43 +-- modules/ping-protection/ping-protection.js | 349 +++++++++++++----- 7 files changed, 371 insertions(+), 137 deletions(-) create mode 100644 modules/ping-protection/events/autoModerationActionExecution.js diff --git a/locales/en.json b/locales/en.json index 040e4439..fda63439 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1057,6 +1057,7 @@ "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m", "history-disabled": "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"storage\" tab in the 'ping-protection' module ^^", "leaver-warning-long": "User left at %d. These logs will stay until automatic deletion.", - "leaver-warning-short": "User left at %d." + "leaver-warning-short": "User left at %d.", + "warning-mod-disabled": "⚠️ **Moderation Actions are disabled!**\nYou can enable them in the dashboard under 'Moderation' to start logging punishments automatically." } } \ No newline at end of file diff --git a/modules/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js index 56c86f3a..69a8462b 100644 --- a/modules/ping-protection/commands/ping-protection.js +++ b/modules/ping-protection/commands/ping-protection.js @@ -125,9 +125,15 @@ async function listHandler(interaction, type) { if (contentList.length === 0) contentList = [localize('ping-protection', 'list-empty')]; const embed = new MessageEmbed() - .setTitle(title) - .setDescription(contentList.join('\n')) - .setColor('GREEN'); + .setTitle(title) + .setDescription(contentList.join('\n')) + .setColor('GREEN') + .setFooter({ + text: interaction.client.strings.footer, + iconURL: interaction.client.strings.footerImgUrl + }); - await interaction.reply({ embeds: [embed], ephemeral: false }); + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: false }); } \ No newline at end of file diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 2531db76..50b26594 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -88,21 +88,38 @@ { "name": "enableAutomod", "humanName": { - "en": "Enable automod (Deletes original message)" + "en": "Enable automod" }, "description": { - "en": "If enabled, the bot will automatically delete the original message which pinged a protected user/role." + "en": "If enabled, the bot will utilise Discord's native AutoMod to block the message with a ping of a protected user/role." }, "type": "boolean", - "default": { "en": true } + "default": { + "en": true + } + }, + { + "name": "autoModLogChannel", + "humanName": { + "en": "AutoMod Log Channel" + }, + "description": { + "en": "Channel where AutoMod alerts are sent." + }, + "type": "array", + "content": "channelID", + "default": { + "en": [] + }, + "dependsOn": "enableAutomod" }, { - "name": "includeOriginalContent", + "name": "sendContentAsBot", "humanName": { - "en": "Allow original message content parameter" + "en": "Repost blocked content" }, "description": { - "en": "If enabled, you can use the %message-content% parameter in your warning message to show the original message content." + "en": "If enabled, the bot will repost the blocked message content in the original channel without the ping." }, "type": "boolean", "default": { diff --git a/modules/ping-protection/events/autoModerationActionExecution.js b/modules/ping-protection/events/autoModerationActionExecution.js new file mode 100644 index 00000000..35f9bbc5 --- /dev/null +++ b/modules/ping-protection/events/autoModerationActionExecution.js @@ -0,0 +1,63 @@ +/** + * Event: autoModerationActionExecution + */ +const { + addPing, + getPingCountInWindow, + executeAction +} = require('../ping-protection'); +const { localize } = require('../../../src/functions/localize'); + +module.exports.run = async function (client, execution) { + if (execution.ruleTriggerType !== 1) return; + + const config = client.configurations['ping-protection']['configuration']; + const storageConfig = client.configurations['ping-protection']['storage']; + const moderationRules = client.configurations['ping-protection']['moderation']; + + if (!config) return; + + const matchedKeyword = execution.matchedKeyword || ""; + const rawId = matchedKeyword.replace(/\*/g, ''); + + // Check if ID is in the protected lists + const isProtected = config.protectedRoles.includes(rawId) || config.protectedUsers.includes(rawId); + if (!isProtected) return; + + let pingCount = 0; + let timeframeWeeks = 12; + let rule1 = (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) ? moderationRules[0] : null; + + if (!!storageConfig && !!storageConfig.enablePingHistory) { + const mockAuthor = { id: execution.userId }; + const mockMessage = { author: mockAuthor, url: 'Blocked by AutoMod' }; + const mockTarget = { id: rawId }; + + try { + await addPing(client, mockMessage, mockTarget); + if (rule1 && !!rule1.advancedConfiguration) { + timeframeWeeks = rule1.timeframeWeeks; + } else { + timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + } + pingCount = await getPingCountInWindow(client, execution.userId, timeframeWeeks); + } catch (e) { + client.logger.error(`[ping-protection] DB Log Failed: ${e.message}`); + } + } + + // Moderation actions + if (!rule1 || !rule1.enableModeration) return; + + let requiredCount = (rule1.advancedConfiguration) ? rule1.pingsCountAdvanced : rule1.pingsCountBasic; + let generatedReason = (rule1.advancedConfiguration) + ? localize('ping-protection', 'reason-advanced', { c: pingCount, w: rule1.timeframeWeeks }) + : localize('ping-protection', 'reason-basic', { c: pingCount, w: timeframeWeeks }); + + if (pingCount >= requiredCount) { + const memberToPunish = await execution.guild.members.fetch(execution.userId).catch(() => null); + if (memberToPunish) { + await executeAction(client, memberToPunish, rule1, generatedReason, storageConfig); + } + } +}; \ No newline at end of file diff --git a/modules/ping-protection/events/botReady.js b/modules/ping-protection/events/botReady.js index dd32aa39..c639a019 100644 --- a/modules/ping-protection/events/botReady.js +++ b/modules/ping-protection/events/botReady.js @@ -1,12 +1,15 @@ -const { enforceRetention } = require('../ping-protection'); +const { enforceRetention, syncNativeAutoMod } = require('../ping-protection'); const schedule = require('node-schedule'); module.exports.run = async function (client) { await enforceRetention(client); + await syncNativeAutoMod(client); + client.logger.info('[ping-protection] Native AutoMod rule synced.'); - // Schedules daily retention at 03:00 local bot time with cronjob + // Daily job const job = schedule.scheduleJob('0 3 * * *', async () => { await enforceRetention(client); + await syncNativeAutoMod(client); }); client.jobs.push(job); }; \ No newline at end of file diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index d4c2ee22..48b73a9d 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -2,15 +2,14 @@ const { addPing, getPingCountInWindow, executeAction, - sendPingWarning + sendPingWarning, + handleAutoModAlert } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); -// Handles messages module.exports.run = async function (client, message) { if (!client.botReadyAt) return; if (!message.guild) return; - if (message.author.bot) return; if (message.guild.id !== client.guildID) return; const config = client.configurations['ping-protection']['configuration']; @@ -18,21 +17,25 @@ module.exports.run = async function (client, message) { const moderationRules = client.configurations['ping-protection']['moderation']; if (!config) return; + + if ((message.system || message.author.system) && message.embeds.length > 0) { + await handleAutoModAlert(client, message); + return; + } + + if (message.author.bot) return; - // Checks for ignored channels and roles if (config.ignoredChannels.includes(message.channel.id)) return; if (message.member.roles.cache.some(role => config.ignoredRoles.includes(role.id))) return; const pingedProtectedRole = message.mentions.roles.some(role => config.protectedRoles.includes(role.id)); const pingedProtectedUser = message.mentions.users.some(user => config.protectedUsers.includes(user.id)); - if (!pingedProtectedRole && !pingedProtectedUser) return; const targetUser = message.mentions.users.find(u => config.protectedUsers.includes(u.id)); const targetRole = message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); const target = targetUser || targetRole; - -// Processes the ping + let pingCount = 0; const pingerId = message.author.id; let timeframeWeeks = 12; @@ -41,26 +44,17 @@ module.exports.run = async function (client, message) { if (!!storageConfig && !!storageConfig.enablePingHistory) { try { await addPing(client, message, target); - if (rule1 && !!rule1.advancedConfiguration) { timeframeWeeks = rule1.timeframeWeeks; } else { timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; } - pingCount = await getPingCountInWindow(client, pingerId, timeframeWeeks); - } catch (e) { - } + } catch (e) {} } await sendPingWarning(client, message, target, config); - if (!!config.enableAutomod) { - await message.delete().catch(() => { - }); - } - - // Moderation action logic if (!rule1 || !rule1.enableModeration) return; let requiredCount = 0; @@ -74,31 +68,20 @@ module.exports.run = async function (client, message) { generatedReason = localize('ping-protection', 'reason-basic', { c: pingCount, w: timeframeWeeks }); } - // Checks for recent punishments to prevent duplicate actions if (pingCount >= requiredCount) { const { Op } = require('sequelize'); const oneMinuteAgo = new Date(new Date() - 60000); - try { const recentLog = await client.models['ping-protection']['ModerationLog'].findOne({ - where: { - victimID: message.author.id, - createdAt: { [Op.gt]: oneMinuteAgo } - } + where: { victimID: message.author.id, createdAt: { [Op.gt]: oneMinuteAgo } } }); - if (recentLog) return; } catch (e) {} let memberToPunish = message.member; if (!memberToPunish) { - try { - memberToPunish = await message.guild.members.fetch(message.author.id); - } catch (fetchError) { - return; - } + try { memberToPunish = await message.guild.members.fetch(message.author.id); } catch (e) { return; } } - await executeAction(client, memberToPunish, rule1, generatedReason, storageConfig); } }; \ No newline at end of file diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index cc6d435f..c846b218 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -5,15 +5,14 @@ */ const { Op } = require('sequelize'); const { MessageActionRow, MessageButton, MessageEmbed } = require('discord.js'); -const { embedType, formatDate } = require('../../src/functions/helpers'); +const { embedType, embedTypeV2, formatDate } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); -// Core functions and logic async function addPing(client, message, target) { const isRole = !target.username; await client.models['ping-protection']['PingHistory'].create({ userId: message.author.id, - messageUrl: message.url, + messageUrl: message.url || 'Blocked by AutoMod', targetId: target.id, isRole: isRole }); @@ -62,81 +61,227 @@ async function getLeaverStatus(client, userId) { return await client.models['ping-protection']['LeaverData'].findByPk(userId); } -// Action logic +function getSafeChannelId(configValue) { + if (!configValue) return null; + let rawId = null; + if (Array.isArray(configValue) && configValue.length > 0) rawId = configValue[0]; + else if (typeof configValue === 'string') rawId = configValue; + + if (rawId && (typeof rawId === 'string' || typeof rawId === 'number')) { + const finalId = rawId.toString(); + if (finalId.length > 5) return finalId; + } + return null; +} + async function sendPingWarning(client, message, target, moduleConfig) { const warningMsg = moduleConfig.pingWarningMessage; if (!warningMsg) return; - const targetName = target.name || target.tag || target.username || 'Unknown'; - const targetMention = target.toString(); - const placeholders = { - '%target-name%': targetName, - '%target-mention%': targetMention, + '%target-name%': target.name || target.tag || target.username || 'Unknown', + '%target-mention%': target.toString(), '%target-id%': target.id, '%user-id%': message.author.id }; - let replyOptions = embedType(warningMsg, placeholders); + let messageOptions = await embedTypeV2(warningMsg, placeholders); + return message.reply(messageOptions).catch(async () => { + return message.channel.send(messageOptions).catch(() => {}); + }); +} + +async function sendAutoModRepost(client, channel, author, content, targetId, moduleConfig) { + if (!moduleConfig.sendContentAsBot) return null; - if (!!moduleConfig.enableAutomod && !!moduleConfig.includeOriginalContent && message.content) { - const contentHeader = `\n\n**Original message content:**\n${message.content}`; - if (replyOptions.embeds && replyOptions.embeds.length > 0) { - replyOptions.embeds[0].description = (replyOptions.embeds[0].description || "") + contentHeader; - } else { - replyOptions.content = (replyOptions.content || "") + contentHeader; - } + const warningMsg = moduleConfig.pingWarningMessage; + const targetMention = `<@${targetId}>`; + + const placeholders = { + '%target-name%': 'Protected Target', + '%target-mention%': targetMention, + '%target-id%': targetId, + '%user-id%': author.id + }; + + let warningOptions = await embedTypeV2(warningMsg, placeholders); + let warningText = ""; + + if (warningOptions.embeds && warningOptions.embeds.length > 0) { + warningText = warningOptions.embeds[0].description || ""; + } else { + warningText = warningOptions.content || ""; } - return message.reply(replyOptions).catch(async () => { - return message.channel.send(replyOptions).catch(() => {}); + const embed = new MessageEmbed() + .setAuthor({ name: author.tag, iconURL: author.displayAvatarURL({ dynamic: true }) }) + .setTitle(localize('ping-protection', 'automod-block-title') || "New message with a blocked ping") + .setDescription(`${content}\n\n${warningText}`) + .setColor('RED') + .setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + return await channel.send({ embeds: [embed] }).catch((err) => { + client.logger.error(`[ping-protection] Repost Failed: ${err.message}`); + return null; }); } -async function executeAction(client, member, rule, reason, storageConfig) { - const actionType = rule.actionType; - if (!member) return false; - - const botMember = await member.guild.members.fetch(client.user.id); - if (botMember.roles.highest.position <= member.roles.highest.position) { - return false; +async function syncNativeAutoMod(client) { + const config = client.configurations['ping-protection']['configuration']; + if (!config || !config.enableAutomod) return; + + try { + const guild = await client.guilds.fetch(client.guildID); + const rules = await guild.autoModerationRules.fetch(); + const existingRule = rules.find(r => r.name === 'SCNX Ping Protection'); + const protectedIds = [...(config.protectedRoles || []), ...(config.protectedUsers || [])]; + + if (protectedIds.length === 0) { + if (existingRule) await existingRule.delete().catch(() => {}); + return; + } + + const baseData = { + name: 'SCNX Ping Protection', + eventType: 1, + triggerType: 1, + triggerMetadata: { + keywordFilter: protectedIds.map(id => `*${id}*`) + }, + enabled: true, + exemptRoles: config.ignoredRoles || [], + exemptChannels: config.ignoredChannels || [] + }; + + if (existingRule) { + const hasBlockAction = existingRule.actions.some(a => a.type === 1); + if (!hasBlockAction) { + const newActions = existingRule.actions.map(a => ({ type: a.type, metadata: a.metadata })); + newActions.push({ type: 1 }); + baseData.actions = newActions; + client.logger.info('[ping-protection] Adding missing Block Action to rule.'); + } else { + client.logger.info('[ping-protection] Updating keywords only (Preserving Manual Actions).'); + } + await guild.autoModerationRules.edit(existingRule.id, baseData); + } else { + baseData.actions = [{ type: 1 }]; + await guild.autoModerationRules.create(baseData); + client.logger.info('[ping-protection] Created new AutoMod rule (Block Only - Add Alert Manually).'); + } + + } catch (e) { + client.logger.error(`[ping-protection] AutoMod Sync Failed: ${e.message}`); } +} - // Database logging - const logDb = async (type, duration = null) => { - try { - await client.models['ping-protection']['ModerationLog'].create({ - victimID: member.id, - type, - actionDuration: duration, - reason - }); - } catch (dbError) {} - }; +async function handleAutoModAlert(client, alertMessage) { + const config = client.configurations['ping-protection']['configuration']; + if (!config) return; + + let fullText = alertMessage.content || ""; + if (alertMessage.embeds.length > 0) { + const embed = alertMessage.embeds[0]; + fullText += " " + (embed.title || ""); + fullText += " " + (embed.description || ""); + fullText += " " + (embed.footer ? embed.footer.text : ""); + if (embed.fields) { + embed.fields.forEach(f => fullText += " " + f.value + " " + f.name); + } + } - if (actionType === 'MUTE') { - const durationMs = rule.muteDuration * 60000; - await logDb('MUTE', rule.muteDuration); - - try { - await member.timeout(durationMs, reason); - return true; - } catch (error) { - return false; + let originalChannelId = null; + + if (alertMessage.mentions.channels.size > 0) { + originalChannelId = alertMessage.mentions.channels.first().id; + } + + if (!originalChannelId && alertMessage.components) { + for (const row of alertMessage.components) { + for (const component of row.components) { + if (component.url && component.url.includes('/channels/')) { + const match = component.url.match(/channels\/\d+\/(\d{17,19})/); + if (match) { + originalChannelId = match[1]; + break; + } + } + } + if (originalChannelId) break; } - } else if (actionType === 'KICK') { - await logDb('KICK'); - try { - await member.kick(reason); - return true; - } catch (error) { - return false; + } + + if (!originalChannelId) { + const mentionMatch = fullText.match(/<#(\d{17,19})>/); + if (mentionMatch) originalChannelId = mentionMatch[1]; + } + + if (!originalChannelId) { + const urlMatch = fullText.match(/channels\/\d+\/(\d{17,19})/); + if (urlMatch) originalChannelId = urlMatch[1]; + } + + if (!originalChannelId) { + client.logger.warn('[ping-protection] Repost Failed: Could not extract Channel ID.'); + return; + } + + let userId = null; + + if (alertMessage.mentions.users.size > 0) { + const found = alertMessage.mentions.users.find(u => u.id !== client.user.id); + if (found) userId = found.id; + } + + if (!userId) { + const parenMatch = fullText.match(/\((\d{17,19})\)/); + if (parenMatch) userId = parenMatch[1]; + } + + if (!userId) { + const mentionMatch = fullText.match(/<@!?(\d{17,19})>/); + if (mentionMatch) userId = mentionMatch[1]; + } + + if (!userId) { + client.logger.warn('[ping-protection] Repost Failed: Could not extract User ID.'); + return; + } + + let content = "*[Content Hidden]*"; + if (alertMessage.embeds.length > 0) { + const embed = alertMessage.embeds[0]; + if (embed.description && embed.description.length > 20) { + content = embed.description; } + const contentField = embed.fields.find(f => f.name && (f.name.includes('Content') || f.name.includes('Message'))); + if (contentField) content = contentField.value; + } + + let targetId = "Protected User"; + const keywordMatch = fullText.match(/Keyword:\s*\*?(\d+)\*?/); + if (keywordMatch) targetId = keywordMatch[1]; + + const author = await client.users.fetch(userId).catch(() => null); + const originalChannel = await client.channels.fetch(originalChannelId).catch(() => null); + + if (author && originalChannel) { + const reposted = await sendAutoModRepost(client, originalChannel, author, content, targetId, config); + + const storageConfig = client.configurations['ping-protection']['storage']; + if (!!storageConfig && !!storageConfig.enablePingHistory) { + const logUrl = reposted ? reposted.url : 'Blocked by AutoMod'; + const mockMessage = { author: { id: userId }, url: logUrl }; + const mockTarget = { id: targetId }; + await addPing(client, mockMessage, mockTarget); + } + } else { + client.logger.error(`[ping-protection] Resolution Failed: Author: ${!!author}, Channel: ${!!originalChannel}`); } - return false; } -// Generates history and actions responses async function generateHistoryResponse(client, userId, page = 1) { const storageConfig = client.configurations['ping-protection']['storage']; const limit = 8; @@ -157,11 +302,7 @@ async function generateHistoryResponse(client, userId, page = 1) { if (leaverData) { const dateStr = formatDate(leaverData.leftAt); - if (history.length > 0) { - description += `⚠️ ${localize('ping-protection', 'leaver-warning-long', { d: dateStr })}\n\n`; - } else { - description += `⚠️ ${localize('ping-protection', 'leaver-warning-short', { d: dateStr })}\n\n`; - } + description += `⚠️ ${localize('ping-protection', history.length > 0 ? 'leaver-warning-long' : 'leaver-warning-short', { d: dateStr })}\n\n`; } if (!isEnabled) { @@ -171,14 +312,7 @@ async function generateHistoryResponse(client, userId, page = 1) { } else { const lines = history.map((entry, index) => { const timeString = formatDate(entry.createdAt); - let targetString = "Unknown"; - - if (entry.targetId) { - targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; - } else { - targetString = "Detected"; - } - + const targetString = entry.targetId ? (entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`) : "Detected"; return `${(page - 1) * limit + index + 1}. **Pinged ${targetString}** at ${timeString}\n[Jump to Message](${entry.messageUrl})`; }); description += lines.join('\n\n'); @@ -194,88 +328,89 @@ async function generateHistoryResponse(client, userId, page = 1) { .setTitle(localize('ping-protection', 'embed-history-title', { u: user.username })) .setThumbnail(user.displayAvatarURL({ dynamic: true })) .setDescription(description) - .setColor('ORANGE'); + .setColor('ORANGE') + .setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); - return { embeds: [embed], components: [row], ephemeral: false }; + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + return { embeds: [embed], components: [row] }; } async function generateActionsResponse(client, userId, page = 1) { - const storageConfig = client.configurations['ping-protection']['storage']; + const moderationConfig = client.configurations['ping-protection']['moderation']; const limit = 8; - const isEnabled = true; + + const rule1 = (moderationConfig && Array.isArray(moderationConfig) && moderationConfig.length > 0) ? moderationConfig[0] : null; + const isEnabled = rule1 ? rule1.enableModeration : false; let total = 0, history = [], totalPages = 1; - if (isEnabled) { - const data = await fetchModHistory(client, userId, page, limit); - total = data.total; - history = data.history; - totalPages = Math.ceil(total / limit) || 1; - } + const data = await fetchModHistory(client, userId, page, limit); + total = data.total; + history = data.history; + totalPages = Math.ceil(total / limit) || 1; const user = await client.users.fetch(userId).catch(() => ({ username: 'Unknown User', displayAvatarURL: () => null })); let description = ""; + if (!isEnabled) { + description += `${localize('ping-protection', 'warning-mod-disabled')}\n\n`; + } + if (history.length === 0) { - description = localize('ping-protection', 'no-data-found'); + description += localize('ping-protection', 'no-data-found'); } else { const lines = history.map((entry, index) => { const duration = entry.actionDuration ? ` (${entry.actionDuration}m)` : ''; const reasonText = entry.reason || localize('ping-protection', 'no-reason') || 'No reason'; - const timeString = formatDate(entry.createdAt); - - return `${(page - 1) * limit + index + 1}. **${entry.type}${duration}** - ${timeString}\n${localize('ping-protection', 'label-reason')}: ${reasonText}`; + 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 MessageActionRow().addComponents( new MessageButton().setCustomId(`ping-protection_mod-page_${userId}_${page - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(page <= 1), new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${page}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), - new MessageButton().setCustomId(`ping-protection_mod-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(page >= totalPages || !isEnabled) + new MessageButton().setCustomId(`ping-protection_mod-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(page >= totalPages || (!isEnabled && history.length === 0)) ); const embed = new MessageEmbed() .setTitle(localize('ping-protection', 'embed-actions-title', { u: user.username })) .setThumbnail(user.displayAvatarURL({ dynamic: true })) .setDescription(description) - .setColor('RED'); + .setColor(isEnabled ? 'RED' : 'GREY') + .setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); - return { embeds: [embed], components: [row], ephemeral: false }; + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + return { embeds: [embed], components: [row] }; } -// Manages data deletion async function deleteAllUserData(client, userId) { await client.models['ping-protection']['PingHistory'].destroy({ where: { userId: userId } }); await client.models['ping-protection']['ModerationLog'].destroy({ where: { victimID: userId } }); await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-manual-delete-logs', { u: userId })); } - async function markUserAsLeft(client, userId) { await client.models['ping-protection']['LeaverData'].upsert({ userId: userId, leftAt: new Date() }); } - async function markUserAsRejoined(client, userId) { await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); } - async function enforceRetention(client) { const storageConfig = client.configurations['ping-protection']['storage']; if (!storageConfig) return; - if (storageConfig.enablePingHistory) { const historyCutoff = new Date(); historyCutoff.setDate(historyCutoff.getDate() - ((storageConfig.pingHistoryRetention || 12) * 7)); await client.models['ping-protection']['PingHistory'].destroy({ where: { createdAt: { [Op.lt]: historyCutoff } } }); } - if (storageConfig.modLogRetention) { const modCutoff = new Date(); modCutoff.setMonth(modCutoff.getMonth() - (storageConfig.modLogRetention || 6)); await client.models['ping-protection']['ModerationLog'].destroy({ where: { createdAt: { [Op.lt]: modCutoff } } }); } - if (storageConfig.enableLeaverDataRetention) { const leaverCutoff = new Date(); leaverCutoff.setDate(leaverCutoff.getDate() - (storageConfig.leaverRetention || 1)); @@ -286,11 +421,35 @@ async function enforceRetention(client) { } } } +async function executeAction(client, member, rule, reason, storageConfig) { + const actionType = rule.actionType; + if (!member) return false; + const botMember = await member.guild.members.fetch(client.user.id); + if (botMember.roles.highest.position <= member.roles.highest.position) return false; + const logDb = async (type, duration = null) => { + try { + await client.models['ping-protection']['ModerationLog'].create({ + victimID: member.id, type, actionDuration: duration, reason + }); + } catch (dbError) {} + }; + if (actionType === 'MUTE') { + const durationMs = rule.muteDuration * 60000; + await logDb('MUTE', rule.muteDuration); + try { await member.timeout(durationMs, reason); return true; } catch (error) { return false; } + } else if (actionType === 'KICK') { + await logDb('KICK'); + try { await member.kick(reason); return true; } catch (error) { return false; } + } + return false; +} module.exports = { addPing, getPingCountInWindow, sendPingWarning, + sendAutoModRepost, + syncNativeAutoMod, fetchPingHistory, fetchModHistory, executeAction, @@ -300,5 +459,7 @@ module.exports = { markUserAsRejoined, enforceRetention, generateHistoryResponse, - generateActionsResponse + generateActionsResponse, + handleAutoModAlert, + getSafeChannelId }; \ No newline at end of file From bc3737a6ecd156b27f3309b50e4680bd77ce6e6e Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 18 Dec 2025 17:20:19 +0100 Subject: [PATCH 22/31] Fixed the --- .../events/autoModerationActionExecution.js | 4 +- modules/ping-protection/ping-protection.js | 57 ++++++++++--------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/modules/ping-protection/events/autoModerationActionExecution.js b/modules/ping-protection/events/autoModerationActionExecution.js index 35f9bbc5..e3f957ba 100644 --- a/modules/ping-protection/events/autoModerationActionExecution.js +++ b/modules/ping-protection/events/autoModerationActionExecution.js @@ -9,7 +9,7 @@ const { const { localize } = require('../../../src/functions/localize'); module.exports.run = async function (client, execution) { - if (execution.ruleTriggerType !== 1) return; + if (execution.ruleTriggerType !== 'KEYWORD') return; const config = client.configurations['ping-protection']['configuration']; const storageConfig = client.configurations['ping-protection']['storage']; @@ -20,7 +20,6 @@ module.exports.run = async function (client, execution) { const matchedKeyword = execution.matchedKeyword || ""; const rawId = matchedKeyword.replace(/\*/g, ''); - // Check if ID is in the protected lists const isProtected = config.protectedRoles.includes(rawId) || config.protectedUsers.includes(rawId); if (!isProtected) return; @@ -46,7 +45,6 @@ module.exports.run = async function (client, execution) { } } - // Moderation actions if (!rule1 || !rule1.enableModeration) return; let requiredCount = (rule1.advancedConfiguration) ? rule1.pingsCountAdvanced : rule1.pingsCountBasic; diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index c846b218..04f5fbb9 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -136,44 +136,50 @@ async function syncNativeAutoMod(client) { const guild = await client.guilds.fetch(client.guildID); const rules = await guild.autoModerationRules.fetch(); const existingRule = rules.find(r => r.name === 'SCNX Ping Protection'); - const protectedIds = [...(config.protectedRoles || []), ...(config.protectedUsers || [])]; + const protectedIds = [...(config.protectedRoles || []), ...(config.protectedUsers || [])]; + if (protectedIds.length === 0) { if (existingRule) await existingRule.delete().catch(() => {}); return; } - const baseData = { + const actions = [{ type: 1 }]; + + const alertChannelId = getSafeChannelId(config.autoModLogChannel); + + if (alertChannelId) { + actions.push({ + type: 2, + metadata: { + channel: alertChannelId + } + }); + } + + const ruleData = { name: 'SCNX Ping Protection', eventType: 1, triggerType: 1, triggerMetadata: { - keywordFilter: protectedIds.map(id => `*${id}*`) + keywordFilter: protectedIds.map(id => `*${id}*`) }, + actions: actions, enabled: true, exemptRoles: config.ignoredRoles || [], exemptChannels: config.ignoredChannels || [] }; if (existingRule) { - const hasBlockAction = existingRule.actions.some(a => a.type === 1); - if (!hasBlockAction) { - const newActions = existingRule.actions.map(a => ({ type: a.type, metadata: a.metadata })); - newActions.push({ type: 1 }); - baseData.actions = newActions; - client.logger.info('[ping-protection] Adding missing Block Action to rule.'); - } else { - client.logger.info('[ping-protection] Updating keywords only (Preserving Manual Actions).'); - } - await guild.autoModerationRules.edit(existingRule.id, baseData); + await guild.autoModerationRules.edit(existingRule.id, ruleData); + client.logger.info(`[ping-protection] AutoMod synced. Actions: ${actions.length}`); } else { - baseData.actions = [{ type: 1 }]; - await guild.autoModerationRules.create(baseData); - client.logger.info('[ping-protection] Created new AutoMod rule (Block Only - Add Alert Manually).'); + await guild.autoModerationRules.create(ruleData); + client.logger.info(`[ping-protection] AutoMod created. Actions: ${actions.length}`); } - } catch (e) { client.logger.error(`[ping-protection] AutoMod Sync Failed: ${e.message}`); + if (e.rawError) client.logger.error(JSON.stringify(e.rawError, null, 2)); } } @@ -201,10 +207,10 @@ async function handleAutoModAlert(client, alertMessage) { if (!originalChannelId && alertMessage.components) { for (const row of alertMessage.components) { for (const component of row.components) { - if (component.url && component.url.includes('/channels/')) { - const match = component.url.match(/channels\/\d+\/(\d{17,19})/); - if (match) { - originalChannelId = match[1]; + if (component.style === 5 && component.url && component.url.includes('/channels/')) { + const parts = component.url.split('/'); + if (parts.length >= 2) { + originalChannelId = parts[parts.length - 2]; break; } } @@ -219,11 +225,10 @@ async function handleAutoModAlert(client, alertMessage) { } if (!originalChannelId) { - const urlMatch = fullText.match(/channels\/\d+\/(\d{17,19})/); - if (urlMatch) originalChannelId = urlMatch[1]; - } - - if (!originalChannelId) { + // DEBUG: Log the components to see what's failing + if (alertMessage.components.length > 0) { + client.logger.info(`[ping-protection] Debug Components: ${JSON.stringify(alertMessage.components)}`); + } client.logger.warn('[ping-protection] Repost Failed: Could not extract Channel ID.'); return; } From d1d0271a4c2e4f4331b0a1a5c5ebaa07b25c0282 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 18 Dec 2025 18:18:11 +0100 Subject: [PATCH 23/31] Removed the feature that didn't work (reposting), adds a custom message to the automod message block. Also the bot now deletes the rule it created if automod enabled = false --- .../configs/configuration.json | 14 +- .../events/autoModerationActionExecution.js | 6 +- modules/ping-protection/events/botReady.js | 1 - .../events/interactionCreate.js | 2 +- .../ping-protection/events/messageCreate.js | 16 +- modules/ping-protection/models/LeaverData.js | 1 - modules/ping-protection/ping-protection.js | 196 +++--------------- 7 files changed, 47 insertions(+), 189 deletions(-) diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 50b26594..2258c531 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -114,16 +114,16 @@ "dependsOn": "enableAutomod" }, { - "name": "sendContentAsBot", + "name": "autoModBlockMessage", "humanName": { - "en": "Repost blocked content" - }, + "en": "AutoMod custom message for message block" }, "description": { - "en": "If enabled, the bot will repost the blocked message content in the original channel without the ping." + "en": "Custom text shown to the user when blocked (Max 150 characters)." }, - "type": "boolean", + "type": "string", + "maxLength": 150, "default": { - "en": true + "en": "Protected User Ping: Your message was blocked but the content was sent to the log channel." }, "dependsOn": "enableAutomod" }, @@ -167,4 +167,4 @@ } } ] -} +} \ No newline at end of file diff --git a/modules/ping-protection/events/autoModerationActionExecution.js b/modules/ping-protection/events/autoModerationActionExecution.js index e3f957ba..ce434513 100644 --- a/modules/ping-protection/events/autoModerationActionExecution.js +++ b/modules/ping-protection/events/autoModerationActionExecution.js @@ -1,13 +1,10 @@ -/** - * Event: autoModerationActionExecution - */ const { addPing, getPingCountInWindow, executeAction } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); - +// Handles auto mod actions module.exports.run = async function (client, execution) { if (execution.ruleTriggerType !== 'KEYWORD') return; @@ -41,7 +38,6 @@ module.exports.run = async function (client, execution) { } pingCount = await getPingCountInWindow(client, execution.userId, timeframeWeeks); } catch (e) { - client.logger.error(`[ping-protection] DB Log Failed: ${e.message}`); } } diff --git a/modules/ping-protection/events/botReady.js b/modules/ping-protection/events/botReady.js index c639a019..6e43412d 100644 --- a/modules/ping-protection/events/botReady.js +++ b/modules/ping-protection/events/botReady.js @@ -4,7 +4,6 @@ const schedule = require('node-schedule'); module.exports.run = async function (client) { await enforceRetention(client); await syncNativeAutoMod(client); - client.logger.info('[ping-protection] Native AutoMod rule synced.'); // Daily job const job = schedule.scheduleJob('0 3 * * *', async () => { diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index 41a944db..08d99bdf 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -75,7 +75,7 @@ module.exports.run = async function (client, interaction) { if (userInput === requiredPhrase) { await deleteAllUserData(client, userId); - await interaction.reply({ content: `✅ ${localize('ping-protection', 'log-manual-delete-logs', {u: userId})}`, ephemeral: true }); + await interaction.reply({ content: `✅ ${localize('ping-protection', 'log-manual-delete', {u: userId})}`, ephemeral: true }); } else { await interaction.reply({ content: `❌ ${localize('ping-protection', 'modal-failed')}`, ephemeral: true }); } diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index 48b73a9d..2abdb6dc 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -2,11 +2,10 @@ const { addPing, getPingCountInWindow, executeAction, - sendPingWarning, - handleAutoModAlert + sendPingWarning } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); - +// Handles messages module.exports.run = async function (client, message) { if (!client.botReadyAt) return; if (!message.guild) return; @@ -17,12 +16,7 @@ module.exports.run = async function (client, message) { const moderationRules = client.configurations['ping-protection']['moderation']; if (!config) return; - - if ((message.system || message.author.system) && message.embeds.length > 0) { - await handleAutoModAlert(client, message); - return; - } - + if (message.author.bot) return; if (config.ignoredChannels.includes(message.channel.id)) return; @@ -52,9 +46,9 @@ module.exports.run = async function (client, message) { pingCount = await getPingCountInWindow(client, pingerId, timeframeWeeks); } catch (e) {} } - + // Send warning if enabled and moderation actions await sendPingWarning(client, message, target, config); - + if (!rule1 || !rule1.enableModeration) return; let requiredCount = 0; diff --git a/modules/ping-protection/models/LeaverData.js b/modules/ping-protection/models/LeaverData.js index f70b6c9b..1727dcff 100644 --- a/modules/ping-protection/models/LeaverData.js +++ b/modules/ping-protection/models/LeaverData.js @@ -7,7 +7,6 @@ module.exports = class PingProtectionLeaverData extends Model { type: DataTypes.STRING, primaryKey: true }, - // Timestamp of when the user left, used for cooldown calculations leftAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 04f5fbb9..1216e569 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -8,11 +8,12 @@ const { MessageActionRow, MessageButton, MessageEmbed } = require('discord.js'); const { embedType, embedTypeV2, formatDate } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); -async function addPing(client, message, target) { +// Data handling +async function addPing(client, messageObj, target) { const isRole = !target.username; await client.models['ping-protection']['PingHistory'].create({ - userId: message.author.id, - messageUrl: message.url || 'Blocked by AutoMod', + userId: messageObj.author.id, + messageUrl: messageObj.url || 'Blocked by AutoMod', targetId: target.id, isRole: isRole }); @@ -61,6 +62,7 @@ async function getLeaverStatus(client, userId) { return await client.models['ping-protection']['LeaverData'].findByPk(userId); } +// Makes sure the channel ID from config is valid for Discord function getSafeChannelId(configValue) { if (!configValue) return null; let rawId = null; @@ -90,70 +92,46 @@ async function sendPingWarning(client, message, target, moduleConfig) { return message.channel.send(messageOptions).catch(() => {}); }); } - -async function sendAutoModRepost(client, channel, author, content, targetId, moduleConfig) { - if (!moduleConfig.sendContentAsBot) return null; - - const warningMsg = moduleConfig.pingWarningMessage; - const targetMention = `<@${targetId}>`; - - const placeholders = { - '%target-name%': 'Protected Target', - '%target-mention%': targetMention, - '%target-id%': targetId, - '%user-id%': author.id - }; - - let warningOptions = await embedTypeV2(warningMsg, placeholders); - let warningText = ""; - - if (warningOptions.embeds && warningOptions.embeds.length > 0) { - warningText = warningOptions.embeds[0].description || ""; - } else { - warningText = warningOptions.content || ""; - } - - const embed = new MessageEmbed() - .setAuthor({ name: author.tag, iconURL: author.displayAvatarURL({ dynamic: true }) }) - .setTitle(localize('ping-protection', 'automod-block-title') || "New message with a blocked ping") - .setDescription(`${content}\n\n${warningText}`) - .setColor('RED') - .setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); - - if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); - - return await channel.send({ embeds: [embed] }).catch((err) => { - client.logger.error(`[ping-protection] Repost Failed: ${err.message}`); - return null; - }); -} - +// Syncs the native AutoMod rule based on configuration async function syncNativeAutoMod(client) { const config = client.configurations['ping-protection']['configuration']; - if (!config || !config.enableAutomod) return; - + try { const guild = await client.guilds.fetch(client.guildID); const rules = await guild.autoModerationRules.fetch(); const existingRule = rules.find(r => r.name === 'SCNX Ping Protection'); + // Logic to disable/delete the rule + if (!config || !config.enableAutomod) { + if (existingRule) { + await existingRule.delete().catch(() => {}); + } + return; + } + const protectedIds = [...(config.protectedRoles || []), ...(config.protectedUsers || [])]; + // Deletest the rule if there are no protected IDs if (protectedIds.length === 0) { - if (existingRule) await existingRule.delete().catch(() => {}); + if (existingRule) { + await existingRule.delete().catch(() => {}); + } return; } - - const actions = [{ type: 1 }]; + + // AutoMod rule data + const actions = []; + const blockMetadata = {}; + if (config.autoModBlockMessage) { + blockMetadata.customMessage = config.autoModBlockMessage; + } + actions.push({ type: 1, metadata: blockMetadata }); const alertChannelId = getSafeChannelId(config.autoModLogChannel); - if (alertChannelId) { actions.push({ type: 2, - metadata: { - channel: alertChannelId - } + metadata: { channel: alertChannelId } }); } @@ -172,121 +150,14 @@ async function syncNativeAutoMod(client) { if (existingRule) { await guild.autoModerationRules.edit(existingRule.id, ruleData); - client.logger.info(`[ping-protection] AutoMod synced. Actions: ${actions.length}`); } else { await guild.autoModerationRules.create(ruleData); - client.logger.info(`[ping-protection] AutoMod created. Actions: ${actions.length}`); } } catch (e) { - client.logger.error(`[ping-protection] AutoMod Sync Failed: ${e.message}`); - if (e.rawError) client.logger.error(JSON.stringify(e.rawError, null, 2)); - } -} - -async function handleAutoModAlert(client, alertMessage) { - const config = client.configurations['ping-protection']['configuration']; - if (!config) return; - - let fullText = alertMessage.content || ""; - if (alertMessage.embeds.length > 0) { - const embed = alertMessage.embeds[0]; - fullText += " " + (embed.title || ""); - fullText += " " + (embed.description || ""); - fullText += " " + (embed.footer ? embed.footer.text : ""); - if (embed.fields) { - embed.fields.forEach(f => fullText += " " + f.value + " " + f.name); - } - } - - let originalChannelId = null; - - if (alertMessage.mentions.channels.size > 0) { - originalChannelId = alertMessage.mentions.channels.first().id; - } - - if (!originalChannelId && alertMessage.components) { - for (const row of alertMessage.components) { - for (const component of row.components) { - if (component.style === 5 && component.url && component.url.includes('/channels/')) { - const parts = component.url.split('/'); - if (parts.length >= 2) { - originalChannelId = parts[parts.length - 2]; - break; - } - } - } - if (originalChannelId) break; - } - } - - if (!originalChannelId) { - const mentionMatch = fullText.match(/<#(\d{17,19})>/); - if (mentionMatch) originalChannelId = mentionMatch[1]; - } - - if (!originalChannelId) { - // DEBUG: Log the components to see what's failing - if (alertMessage.components.length > 0) { - client.logger.info(`[ping-protection] Debug Components: ${JSON.stringify(alertMessage.components)}`); - } - client.logger.warn('[ping-protection] Repost Failed: Could not extract Channel ID.'); - return; - } - - let userId = null; - - if (alertMessage.mentions.users.size > 0) { - const found = alertMessage.mentions.users.find(u => u.id !== client.user.id); - if (found) userId = found.id; - } - - if (!userId) { - const parenMatch = fullText.match(/\((\d{17,19})\)/); - if (parenMatch) userId = parenMatch[1]; - } - - if (!userId) { - const mentionMatch = fullText.match(/<@!?(\d{17,19})>/); - if (mentionMatch) userId = mentionMatch[1]; - } - - if (!userId) { - client.logger.warn('[ping-protection] Repost Failed: Could not extract User ID.'); - return; - } - - let content = "*[Content Hidden]*"; - if (alertMessage.embeds.length > 0) { - const embed = alertMessage.embeds[0]; - if (embed.description && embed.description.length > 20) { - content = embed.description; - } - const contentField = embed.fields.find(f => f.name && (f.name.includes('Content') || f.name.includes('Message'))); - if (contentField) content = contentField.value; - } - - let targetId = "Protected User"; - const keywordMatch = fullText.match(/Keyword:\s*\*?(\d+)\*?/); - if (keywordMatch) targetId = keywordMatch[1]; - - const author = await client.users.fetch(userId).catch(() => null); - const originalChannel = await client.channels.fetch(originalChannelId).catch(() => null); - - if (author && originalChannel) { - const reposted = await sendAutoModRepost(client, originalChannel, author, content, targetId, config); - - const storageConfig = client.configurations['ping-protection']['storage']; - if (!!storageConfig && !!storageConfig.enablePingHistory) { - const logUrl = reposted ? reposted.url : 'Blocked by AutoMod'; - const mockMessage = { author: { id: userId }, url: logUrl }; - const mockTarget = { id: targetId }; - await addPing(client, mockMessage, mockTarget); - } - } else { - client.logger.error(`[ping-protection] Resolution Failed: Author: ${!!author}, Channel: ${!!originalChannel}`); + client.logger.error(`[ping-protection] AutoMod Sync/Cleanup Failed: ${e.message}`); } } - +// Generates history response async function generateHistoryResponse(client, userId, page = 1) { const storageConfig = client.configurations['ping-protection']['storage']; const limit = 8; @@ -340,7 +211,7 @@ async function generateHistoryResponse(client, userId, page = 1) { return { embeds: [embed], components: [row] }; } - +// Generates actions response async function generateActionsResponse(client, userId, page = 1) { const moderationConfig = client.configurations['ping-protection']['moderation']; const limit = 8; @@ -390,7 +261,7 @@ async function generateActionsResponse(client, userId, page = 1) { return { embeds: [embed], components: [row] }; } - +// Handles data deletion async function deleteAllUserData(client, userId) { await client.models['ping-protection']['PingHistory'].destroy({ where: { userId: userId } }); await client.models['ping-protection']['ModerationLog'].destroy({ where: { victimID: userId } }); @@ -426,6 +297,7 @@ async function enforceRetention(client) { } } } + async function executeAction(client, member, rule, reason, storageConfig) { const actionType = rule.actionType; if (!member) return false; @@ -453,7 +325,6 @@ module.exports = { addPing, getPingCountInWindow, sendPingWarning, - sendAutoModRepost, syncNativeAutoMod, fetchPingHistory, fetchModHistory, @@ -465,6 +336,5 @@ module.exports = { enforceRetention, generateHistoryResponse, generateActionsResponse, - handleAutoModAlert, getSafeChannelId }; \ No newline at end of file From aa79dd40824a920ad9a5f8ec934eb813c2e1cb60 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 20 Dec 2025 14:57:41 +0100 Subject: [PATCH 24/31] Fixed the bug of the bot still sending the warning and punishing if limit reached with reply pings even when it's allowed in config --- .../ping-protection/events/messageCreate.js | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index 2abdb6dc..14662026 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -22,14 +22,35 @@ module.exports.run = async function (client, message) { if (config.ignoredChannels.includes(message.channel.id)) return; if (message.member.roles.cache.some(role => config.ignoredRoles.includes(role.id))) return; + // Check for protected pings const pingedProtectedRole = message.mentions.roles.some(role => config.protectedRoles.includes(role.id)); - const pingedProtectedUser = message.mentions.users.some(user => config.protectedUsers.includes(user.id)); + let protectedMentions = message.mentions.users.filter(user => config.protectedUsers.includes(user.id)); + // Handles reply pings + if (config.allowReplyPings && message.type === 'REPLY' && message.mentions.repliedUser) { + const repliedId = message.mentions.repliedUser.id; + + if (protectedMentions.has(repliedId)) { + const manualMentionRegex = new RegExp(`<@!?${repliedId}>`); + const isManualPing = manualMentionRegex.test(message.content); + + if (!isManualPing) { + protectedMentions.delete(repliedId); + } + } + } + // Determines if any protected entities were pinged + const pingedProtectedUser = protectedMentions.size > 0; + if (!pingedProtectedRole && !pingedProtectedUser) return; - const targetUser = message.mentions.users.find(u => config.protectedUsers.includes(u.id)); - const targetRole = message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); - const target = targetUser || targetRole; + let target = null; + if (pingedProtectedUser) { + target = protectedMentions.first(); + } else if (pingedProtectedRole) { + target = message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); + } + if (!target) return; let pingCount = 0; const pingerId = message.author.id; let timeframeWeeks = 12; From 324437126914e98fec349b6bb0643b3a1bc84bcb Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sun, 28 Dec 2025 11:09:02 +0100 Subject: [PATCH 25/31] Added a funny easter egg --- .../ping-protection/events/messageCreate.js | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index 14662026..c069e444 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -5,6 +5,10 @@ const { sendPingWarning } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); +// Tracks the last meme to prevent many duplicates +const lastMemeMap = new Map(); +// Tracks ping counts for the grind message +const selfPingCountMap = new Map(); // Handles messages module.exports.run = async function (client, message) { if (!client.botReadyAt) return; @@ -51,6 +55,48 @@ module.exports.run = async function (client, message) { } if (!target) return; + + // Funny easter egg when they ping themselves + if (target.id === message.author.id) { + const secretChance = 0.01; // Secret for a reason.. (1% chance) + const standardMemes = [ + '[Why are you the way that you are?]() - You just pinged yourself..', + '🔑 [Congratulations, you played yourself.]()', + '🕷️ [Is this you?]() - You just pinged yourself.' + ]; + const secretMeme = '🎵 [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!!'; + const currentCount = (selfPingCountMap.get(message.author.id) || 0) + 1; + selfPingCountMap.set(message.author.id, currentCount); + + setTimeout(() => { + selfPingCountMap.delete(message.author.id); + }, 300000); + + const roll = Math.random(); + let content = ''; + + if (roll < secretChance) { + content = secretMeme; + lastMemeMap.set(message.author.id, -1); + selfPingCountMap.delete(message.author.id); // Reset on secret unlock + } else if (currentCount === 5) { + content = '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)'; + } else { + const lastIndex = lastMemeMap.get(message.author.id); + + let possibleMemes = standardMemes.map((_, index) => index); + if (lastIndex !== undefined && lastIndex !== -1 && standardMemes.length > 1) { + possibleMemes = possibleMemes.filter(i => i !== lastIndex); + } + + const randomIndex = possibleMemes[Math.floor(Math.random() * possibleMemes.length)]; + content = standardMemes[randomIndex]; + lastMemeMap.set(message.author.id, randomIndex); + } + await message.reply({ content: content }).catch(() => {}); + return; + } + let pingCount = 0; const pingerId = message.author.id; let timeframeWeeks = 12; From e310ac8539dbede38d17accdc9fe5431364ef4ee Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Fri, 2 Jan 2026 22:53:08 +0100 Subject: [PATCH 26/31] Some QOL improvements, including merging the list commands --- locales/en.json | 30 +++++--- .../commands/ping-protection.js | 73 ++++++++++--------- .../configs/configuration.json | 2 +- .../ping-protection/configs/moderation.json | 18 ++--- .../events/autoModerationActionExecution.js | 20 ++--- .../ping-protection/events/messageCreate.js | 19 +++-- modules/ping-protection/ping-protection.js | 37 ++++++++-- 7 files changed, 120 insertions(+), 79 deletions(-) diff --git a/locales/en.json b/locales/en.json index fda63439..e03fc124 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1019,10 +1019,16 @@ "nickname-error": "An error occurred while trying to change the nickname of %u: %e" }, "ping-protection": { + "not-a-member": "Punishment failed: The pinger is not a member.", + "punish-role-error": "Punishment failed: I cannot punish %tag because their role is higher than or equal to my highest role.", + "log-mute-success": "Muted %tag for %dur minutes because they exceeded the ping limit.", + "log-kick-success": "Kicked %tag because they exceeded the ping limit.", + "log-mute-error": "Punishment failed: I cannot mute %tag: %e", + "log-kick-error": "Punishment failed: I cannot kick %tag: %e", "log-manual-delete": "All data for <@%u> (%u) has been deleted successfully.", "log-manual-delete-logs": "All data for user with ID %u has been deleted successfully.", - "reason-basic": "User reached %c pings in the last %w weeks (Basic Configuration).", - "reason-advanced": "User reached %c pings in the last %w weeks (Advanced Configuration).", + "reason-basic": "User reached %c pings in the last %w weeks.", + "reason-advanced": "User reached %c pings in the last %d days (Custom timeframe).", "cmd-desc-module": "Ping protection related commands", "cmd-desc-group-user": "Every command related to the users", "cmd-desc-history": "View the ping history of a user", @@ -1030,25 +1036,29 @@ "cmd-desc-actions": "View the moderation action history of a user", "cmd-desc-panel": "Admin: Open the user management panel", "cmd-desc-group-list": "Lists protected or whitelisted entities", - "cmd-desc-list-users": "List all protected members", - "cmd-desc-list-roles": "List all protected roles", - "cmd-desc-list-wl": "List all whitelisted roles", + "cmd-desc-list-protected": "List all protected users and roles", + "cmd-desc-list-wl": "List all whitelisted roles and channels", "embed-history-title": "Ping history of %u", "embed-leaver-warning": "This user left the server at %t. These logs will stay until automatic deletion.", "no-data-found": "No logs found for this user.", "embed-actions-title": "Moderation history of %u", "label-reason": "Reason", - "actions-retention-note": "Remember: Moderation actions are retained for 1 - 12 months based on the configuration.", + "actions-retention-note": "Note: Moderation actions are retained for 1 - 12 months based on the configuration.", "no-permission": "You don't have sufficient permissions to use this command.", "panel-title": "User Panel: %u", "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", "btn-history": "Ping history", "btn-actions": "Actions history", "btn-delete": "Delete all data (Risky)", - "list-roles-title": "Protected roles", - "list-members-title": "Protected members", - "list-whitelist-title": "Whitelisted roles", - "list-empty": "None configured.", + "list-protected-title": "Protected Users and Roles", + "list-protected-desc": "View all protected users and roles here. When someone pings one of these protected user(s)/role(s), a warning will be sent. Exceptions are whitelisted roles, and when sent in a whitelisted channel.", + "field-prot-users": "Protected Users", + "field-prot-roles": "Protected Roles", + "list-whitelist-title": "Whitelisted Roles and Channels", + "list-whitelist-desc": "View all whitelisted roles and channels here. Whitelisted roles will not get a warning for pinging a protected entity, and pings will be ignored in whitelisted channels.", + "field-wl-roles": "Whitelisted Roles", + "field-wl-channels": "Whitelisted Channels", + "list-none": "None are configured.", "modal-title": "Confirm data deletion for this user", "modal-label": "Confirm data deletion by typing this phrase:", "modal-phrase": "I understand that all data of this user will be deleted and that this action cannot be undone.", diff --git a/modules/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js index 69a8462b..767ec05d 100644 --- a/modules/ping-protection/commands/ping-protection.js +++ b/modules/ping-protection/commands/ping-protection.js @@ -6,6 +6,7 @@ const { } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); const { MessageActionRow, MessageButton, MessageEmbed } = require('discord.js'); + // Command definition module.exports.config = { name: 'ping-protection', @@ -28,8 +29,7 @@ module.exports.config = { name: 'list', description: localize('ping-protection', 'cmd-desc-group-list'), options: [ - { type: 'SUB_COMMAND', name: 'users', description: localize('ping-protection', 'cmd-desc-list-users') }, - { type: 'SUB_COMMAND', name: 'roles', description: localize('ping-protection', 'cmd-desc-list-roles') }, + { type: 'SUB_COMMAND', name: 'protected', description: localize('ping-protection', 'cmd-desc-list-protected') }, { type: 'SUB_COMMAND', name: 'whitelisted', description: localize('ping-protection', 'cmd-desc-list-wl') } ] } @@ -67,9 +67,10 @@ module.exports.subcommands = { const user = interaction.options.getUser('user'); const pingerId = user.id; const storageConfig = interaction.client.configurations['ping-protection']['storage']; - const timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + const timeframeDays = retentionWeeks * 7; - const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeWeeks); + const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeDays); const modData = await fetchModHistory(interaction.client, pingerId, 1, 1000); const row = new MessageActionRow().addComponents( @@ -84,7 +85,7 @@ module.exports.subcommands = { .setColor('BLUE') .setThumbnail(user.displayAvatarURL({ dynamic: true })) .addFields([{ - name: localize('ping-protection', 'field-quick-history', {w: timeframeWeeks}), + name: localize('ping-protection', 'field-quick-history', { w: retentionWeeks }), value: localize('ping-protection', 'field-quick-desc', { p: pingCount, m: modData.total }), inline: false }]); @@ -93,11 +94,8 @@ module.exports.subcommands = { } }, 'list': { - 'users': async function (interaction) { - await listHandler(interaction, 'users'); - }, - 'roles': async function (interaction) { - await listHandler(interaction, 'roles'); + 'protected': async function (interaction) { + await listHandler(interaction, 'protected'); }, 'whitelisted': async function (interaction) { await listHandler(interaction, 'whitelisted'); @@ -108,32 +106,39 @@ module.exports.subcommands = { // Handles list subcommands async function listHandler(interaction, type) { const config = interaction.client.configurations['ping-protection']['configuration']; - let contentList = []; - let title = ""; - - if (type === 'roles') { - title = localize('ping-protection', 'list-roles-title'); - contentList = config.protectedRoles.map(id => `<@&${id}>`); - } else if (type === 'users') { - title = localize('ping-protection', 'list-members-title'); - contentList = config.protectedUsers.map(id => `<@${id}>`); - } else if (type === 'whitelisted') { - title = localize('ping-protection', 'list-whitelist-title'); - contentList = config.ignoredRoles.map(id => `<@&${id}>`); - } + const embed = new MessageEmbed() + .setColor('GREEN') + .setFooter({ + text: interaction.client.strings.footer, + iconURL: interaction.client.strings.footerImgUrl + }); - if (contentList.length === 0) contentList = [localize('ping-protection', 'list-empty')]; + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); - const embed = new MessageEmbed() - .setTitle(title) - .setDescription(contentList.join('\n')) - .setColor('GREEN') - .setFooter({ - text: interaction.client.strings.footer, - iconURL: interaction.client.strings.footerImgUrl - }); + if (type === 'protected') { + embed.setTitle(localize('ping-protection', 'list-protected-title')); + embed.setDescription(localize('ping-protection', 'list-protected-desc')); - if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + const usersList = config.protectedUsers.length > 0 ? config.protectedUsers.map(id => `<@${id}>`).join('\n') : localize('ping-protection', 'list-none'); + const rolesList = config.protectedRoles.length > 0 ? config.protectedRoles.map(id => `<@&${id}>`).join('\n') : localize('ping-protection', 'list-none'); + + embed.addFields([ + { name: localize('ping-protection', 'field-prot-users'), value: usersList, inline: true }, + { name: localize('ping-protection', 'field-prot-roles'), value: rolesList, inline: true } + ]); + + } else if (type === 'whitelisted') { + embed.setTitle(localize('ping-protection', 'list-whitelist-title')); + embed.setDescription(localize('ping-protection', 'list-whitelist-desc')); + + const rolesList = config.ignoredRoles.length > 0 ? config.ignoredRoles.map(id => `<@&${id}>`).join('\n') : localize('ping-protection', 'list-none'); + const channelsList = config.ignoredChannels.length > 0 ? config.ignoredChannels.map(id => `<#${id}>`).join('\n') : localize('ping-protection', 'list-none'); + + embed.addFields([ + { name: localize('ping-protection', 'field-wl-roles'), value: rolesList, inline: true }, + { name: localize('ping-protection', 'field-wl-channels'), value: channelsList, inline: true } + ]); + } - await interaction.reply({ embeds: [embed], ephemeral: false }); + await interaction.reply({ embeds: [embed], ephemeral: false }); } \ No newline at end of file diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 2258c531..a26b1a46 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -160,7 +160,7 @@ "default": { "en": { "title": "You are not allowed to ping %target-name%!", - "description": "<@%user-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping protection list roles` or `/ping protection list members`.\n\nIf you were replying, make sure to turn off the mention in the reply.", + "description": "<@%user-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the command.\n\nIf you were replying, make sure to turn off the mention in the reply.", "image": "https://c.tenor.com/zRR5cYKSIV0AAAAd/tenor.gif", "color": "RED" } diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index fbb3b6b9..447cae1b 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -23,12 +23,12 @@ "elementToggle": true }, { - "name": "advancedConfiguration", + "name": "useCustomTimeframe", "humanName": { - "en": "Use Advanced Configuration" + "en": "Use a custom timeframe" }, "description": { - "en": "If enabled, the advanced configuration below will be used and the basic configuration will be ignored. This allows you to customize the custom timeframe. In the future, this will allow for almost full customization." + "en": "If enabled, you can choose your own custom timeframe and the basic configuration will be ignored." }, "type": "boolean", "default": { @@ -60,21 +60,21 @@ "default": { "en": 5 }, - "dependsOn": "advancedConfiguration" + "dependsOn": "useCustomTimeframe" }, { - "name": "timeframeWeeks", + "name": "timeframeDays", "humanName": { - "en": "Timeframe (Weeks)" + "en": "Timeframe (Days)" }, "description": { - "en": "In how many weeks must these pings occur?" + "en": "In how many days must these pings occur?" }, "type": "integer", "default": { - "en": 1 + "en": 7 }, - "dependsOn": "advancedConfiguration" + "dependsOn": "useCustomTimeframe" }, { "name": "actionType", diff --git a/modules/ping-protection/events/autoModerationActionExecution.js b/modules/ping-protection/events/autoModerationActionExecution.js index ce434513..0c1c0151 100644 --- a/modules/ping-protection/events/autoModerationActionExecution.js +++ b/modules/ping-protection/events/autoModerationActionExecution.js @@ -4,6 +4,7 @@ const { executeAction } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); + // Handles auto mod actions module.exports.run = async function (client, execution) { if (execution.ruleTriggerType !== 'KEYWORD') return; @@ -21,7 +22,7 @@ module.exports.run = async function (client, execution) { if (!isProtected) return; let pingCount = 0; - let timeframeWeeks = 12; + let timeframeDays = 84; let rule1 = (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) ? moderationRules[0] : null; if (!!storageConfig && !!storageConfig.enablePingHistory) { @@ -31,22 +32,23 @@ module.exports.run = async function (client, execution) { try { await addPing(client, mockMessage, mockTarget); - if (rule1 && !!rule1.advancedConfiguration) { - timeframeWeeks = rule1.timeframeWeeks; + if (rule1 && !!rule1.useCustomTimeframe) { + timeframeDays = rule1.timeframeDays; } else { - timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + timeframeDays = retentionWeeks * 7; } - pingCount = await getPingCountInWindow(client, execution.userId, timeframeWeeks); + pingCount = await getPingCountInWindow(client, execution.userId, timeframeDays); } catch (e) { } } if (!rule1 || !rule1.enableModeration) return; - let requiredCount = (rule1.advancedConfiguration) ? rule1.pingsCountAdvanced : rule1.pingsCountBasic; - let generatedReason = (rule1.advancedConfiguration) - ? localize('ping-protection', 'reason-advanced', { c: pingCount, w: rule1.timeframeWeeks }) - : localize('ping-protection', 'reason-basic', { c: pingCount, w: timeframeWeeks }); + let requiredCount = (rule1.useCustomTimeframe) ? rule1.pingsCountAdvanced : rule1.pingsCountBasic; + let generatedReason = (rule1.useCustomTimeframe) + ? localize('ping-protection', 'reason-advanced', { c: pingCount, d: rule1.timeframeDays }) + : localize('ping-protection', 'reason-basic', { c: pingCount, w: (storageConfig.pingHistoryRetention || 12) }); if (pingCount >= requiredCount) { const memberToPunish = await execution.guild.members.fetch(execution.userId).catch(() => null); diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index c069e444..d2725cf1 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -99,20 +99,22 @@ module.exports.run = async function (client, message) { let pingCount = 0; const pingerId = message.author.id; - let timeframeWeeks = 12; + let timeframeDays = 84; let rule1 = (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) ? moderationRules[0] : null; if (!!storageConfig && !!storageConfig.enablePingHistory) { try { await addPing(client, message, target); - if (rule1 && !!rule1.advancedConfiguration) { - timeframeWeeks = rule1.timeframeWeeks; + if (rule1 && !!rule1.useCustomTimeframe) { + timeframeDays = rule1.timeframeDays; } else { - timeframeWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + timeframeDays = retentionWeeks * 7; } - pingCount = await getPingCountInWindow(client, pingerId, timeframeWeeks); + pingCount = await getPingCountInWindow(client, pingerId, timeframeDays); } catch (e) {} } + // Send warning if enabled and moderation actions await sendPingWarning(client, message, target, config); @@ -121,12 +123,13 @@ module.exports.run = async function (client, message) { let requiredCount = 0; let generatedReason = ""; - if (!!rule1.advancedConfiguration) { + if (!!rule1.useCustomTimeframe) { requiredCount = rule1.pingsCountAdvanced; - generatedReason = localize('ping-protection', 'reason-advanced', { c: pingCount, w: rule1.timeframeWeeks }); + generatedReason = localize('ping-protection', 'reason-advanced', { c: pingCount, d: rule1.timeframeDays }); } else { requiredCount = rule1.pingsCountBasic; - generatedReason = localize('ping-protection', 'reason-basic', { c: pingCount, w: timeframeWeeks }); + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + generatedReason = localize('ping-protection', 'reason-basic', { c: pingCount, w: retentionWeeks }); } if (pingCount >= requiredCount) { diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 1216e569..e650225c 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -19,9 +19,9 @@ async function addPing(client, messageObj, target) { }); } -async function getPingCountInWindow(client, userId, weeks) { +async function getPingCountInWindow(client, userId, days) { const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - (weeks * 7)); + cutoffDate.setDate(cutoffDate.getDate() - days); return await client.models['ping-protection']['PingHistory'].count({ where: { @@ -154,7 +154,7 @@ async function syncNativeAutoMod(client) { await guild.autoModerationRules.create(ruleData); } } catch (e) { - client.logger.error(`[ping-protection] AutoMod Sync/Cleanup Failed: ${e.message}`); + client.logger.error(`[ping-protection] AutoMod Sync/Cleanup Failed: ${error.message}`); } } // Generates history response @@ -279,7 +279,8 @@ async function enforceRetention(client) { if (!storageConfig) return; if (storageConfig.enablePingHistory) { const historyCutoff = new Date(); - historyCutoff.setDate(historyCutoff.getDate() - ((storageConfig.pingHistoryRetention || 12) * 7)); + const retentionWeeks = storageConfig.pingHistoryRetention || 12; + historyCutoff.setDate(historyCutoff.getDate() - (retentionWeeks * 7)) await client.models['ping-protection']['PingHistory'].destroy({ where: { createdAt: { [Op.lt]: historyCutoff } } }); } if (storageConfig.modLogRetention) { @@ -300,9 +301,15 @@ async function enforceRetention(client) { async function executeAction(client, member, rule, reason, storageConfig) { const actionType = rule.actionType; - if (!member) return false; + if (!member) { + client.logger.debug('[Ping Protection] ' + localize('ping-protection', 'not-a-member')); + return false; + } const botMember = await member.guild.members.fetch(client.user.id); - if (botMember.roles.highest.position <= member.roles.highest.position) return false; + if (botMember.roles.highest.position <= member.roles.highest.position) { + client.logger.warn('[Ping Protection] ' + localize('ping-protection', 'punish-role-error', {tag: member.user.tag})); + return false; + } const logDb = async (type, duration = null) => { try { await client.models['ping-protection']['ModerationLog'].create({ @@ -313,10 +320,24 @@ async function executeAction(client, member, rule, reason, storageConfig) { if (actionType === 'MUTE') { const durationMs = rule.muteDuration * 60000; await logDb('MUTE', rule.muteDuration); - try { await member.timeout(durationMs, reason); return true; } catch (error) { return false; } + try { + await member.timeout(durationMs, reason); + client.logger.info('[Ping Protection] ' + localize('ping-protection', 'log-mute-success', {tag: member.user.tag, dur: rule.muteDuration})); + return true; + } catch (error) { + client.logger.warn('[Ping Protection] ' + localize('ping-protection', 'log-mute-error', {tag: member.user.tag, e: error.message})); + return false; + } } else if (actionType === 'KICK') { await logDb('KICK'); - try { await member.kick(reason); return true; } catch (error) { return false; } + try { + await member.kick(reason); + client.logger.info('[Ping Protection] ' + localize('ping-protection', 'log-kick-success', {tag: member.user.tag})); + return true; + } catch (error) { + client.logger.warn('[Ping Protection] ' + localize('ping-protection', 'log-kick-error', {tag: member.user.tag, e: error.message})); + return false; + } } return false; } From b61c31b3189a85ce0b54bb31c7b96dbfecec92c2 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 8 Jan 2026 20:20:18 +0100 Subject: [PATCH 27/31] Added some new options in the config --- .../configs/configuration.json | 29 ++++++++++++++++++- modules/ping-protection/configs/storage.json | 13 +++++++++ .../events/autoModerationActionExecution.js | 3 +- .../ping-protection/events/messageCreate.js | 3 +- modules/ping-protection/ping-protection.js | 22 ++++++++++++-- 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index a26b1a46..7791675f 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -61,7 +61,7 @@ { "name": "ignoredChannels", "humanName": { - "en": "Ignored Channels" + "en": "Whitelisted Channels" }, "description": { "en": "Pings in these channels are ignored." @@ -72,6 +72,20 @@ "en": [] } }, + { + "name": "ignoredUsers", + "humanName": { + "en": "Whitelisted Users" + }, + "description": { + "en": "Pings from these users are ignored." + }, + "type": "array", + "content": "userID", + "default": { + "en": [] + } + }, { "name": "allowReplyPings", "humanName": { @@ -85,6 +99,19 @@ "en": false } }, + { + "name": "allowSelfPing", + "humanName": { + "en": "Allow protected users to ping themselves" + }, + "description": { + "en": "If enabled, a protected user is able to ping themselves without getting the chance to be embarrassed." + }, + "type": "boolean", + "default": { + "en": false + } + }, { "name": "enableAutomod", "humanName": { diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json index d031eaa9..976bc5ac 100644 --- a/modules/ping-protection/configs/storage.json +++ b/modules/ping-protection/configs/storage.json @@ -36,6 +36,19 @@ "maxValue": "24", "dependsOn": "enablePingHistory" }, + { + "name": "DeleteAllPingHistoryAfterTimeframe", + "humanName": { + "en": "Delete all the pings in history after the timeframe?" + }, + "description": { + "en": "If enabled, the bot will delete ALL the pings history of an user after the timeframe instead of only the ping(s) exceeding the timeframe in the history." + }, + "type": "boolean", + "default": { + "en": false + } + }, { "name": "modLogRetention", "humanName": { diff --git a/modules/ping-protection/events/autoModerationActionExecution.js b/modules/ping-protection/events/autoModerationActionExecution.js index 0c1c0151..e7aabad4 100644 --- a/modules/ping-protection/events/autoModerationActionExecution.js +++ b/modules/ping-protection/events/autoModerationActionExecution.js @@ -14,6 +14,7 @@ module.exports.run = async function (client, execution) { const moderationRules = client.configurations['ping-protection']['moderation']; if (!config) return; + if (config.ignoredUsers && config.ignoredUsers.includes(execution.userId)) return; const matchedKeyword = execution.matchedKeyword || ""; const rawId = matchedKeyword.replace(/\*/g, ''); @@ -33,7 +34,7 @@ module.exports.run = async function (client, execution) { try { await addPing(client, mockMessage, mockTarget); if (rule1 && !!rule1.useCustomTimeframe) { - timeframeDays = rule1.timeframeDays; + timeframeDays = rule1.timeframeDays || 7; } else { const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; timeframeDays = retentionWeeks * 7; diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index d2725cf1..294ecf89 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -24,6 +24,7 @@ module.exports.run = async function (client, message) { if (message.author.bot) return; if (config.ignoredChannels.includes(message.channel.id)) return; + if (config.ignoredUsers.includes(message.author.id)) return; if (message.member.roles.cache.some(role => config.ignoredRoles.includes(role.id))) return; // Check for protected pings @@ -57,7 +58,7 @@ module.exports.run = async function (client, message) { if (!target) return; // Funny easter egg when they ping themselves - if (target.id === message.author.id) { + if (target.id === message.author.id && !config.allowSelfPing) { const secretChance = 0.01; // Secret for a reason.. (1% chance) const standardMemes = [ '[Why are you the way that you are?]() - You just pinged yourself..', diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index e650225c..83faa413 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -280,8 +280,26 @@ async function enforceRetention(client) { if (storageConfig.enablePingHistory) { const historyCutoff = new Date(); const retentionWeeks = storageConfig.pingHistoryRetention || 12; - historyCutoff.setDate(historyCutoff.getDate() - (retentionWeeks * 7)) - await client.models['ping-protection']['PingHistory'].destroy({ where: { createdAt: { [Op.lt]: historyCutoff } } }); + historyCutoff.setDate(historyCutoff.getDate() - (retentionWeeks * 7)); + if (storageConfig.DeleteAllPingHistoryAfterTimeframe) { + const usersWithExpiredData = await client.models['ping-protection']['PingHistory'].findAll({ + where: { createdAt: { [Op.lt]: historyCutoff } }, + attributes: ['userId'], + group: ['userId'] + }); + + const userIdsToWipe = usersWithExpiredData.map(entry => entry.userId); + if (userIdsToWipe.length > 0) { + await client.models['ping-protection']['PingHistory'].destroy({ + where: { userId: userIdsToWipe } + }); + } + } + else { + await client.models['ping-protection']['PingHistory'].destroy({ + where: { createdAt: { [Op.lt]: historyCutoff } } + }); + } } if (storageConfig.modLogRetention) { const modCutoff = new Date(); From 34f345936f3b9cfef372f70fc9954ae3a2954c59 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 8 Jan 2026 20:34:12 +0100 Subject: [PATCH 28/31] Update configuration.json --- modules/ping-protection/configs/configuration.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 7791675f..3377f62f 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -109,7 +109,7 @@ }, "type": "boolean", "default": { - "en": false + "en": true } }, { @@ -194,4 +194,4 @@ } } ] -} \ No newline at end of file +} From f8427da046378c27f04ffeddc3205cb93a4d8f9b Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 8 Jan 2026 20:35:23 +0100 Subject: [PATCH 29/31] Fix self-ping condition to allow self-pinging --- modules/ping-protection/events/messageCreate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index 294ecf89..2a4fd569 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -58,7 +58,7 @@ module.exports.run = async function (client, message) { if (!target) return; // Funny easter egg when they ping themselves - if (target.id === message.author.id && !config.allowSelfPing) { + if (target.id === message.author.id && config.allowSelfPing) { const secretChance = 0.01; // Secret for a reason.. (1% chance) const standardMemes = [ '[Why are you the way that you are?]() - You just pinged yourself..', @@ -149,4 +149,4 @@ module.exports.run = async function (client, message) { } await executeAction(client, memberToPunish, rule1, generatedReason, storageConfig); } -}; \ No newline at end of file +}; From c2d3cad991535674ac99ef4e994d6b1f79e023de Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 10 Jan 2026 11:33:55 +0100 Subject: [PATCH 30/31] Ping protection V1, in Discord.JS V14 --- .../commands/ping-protection.js | 158 ++++++++++----- .../configs/configuration.json | 4 +- .../events/interactionCreate.js | 27 +-- .../ping-protection/events/messageCreate.js | 121 +++++++----- modules/ping-protection/ping-protection.js | 183 +++++++++++++----- 5 files changed, 340 insertions(+), 153 deletions(-) diff --git a/modules/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js index 767ec05d..a2f68c8f 100644 --- a/modules/ping-protection/commands/ping-protection.js +++ b/modules/ping-protection/commands/ping-protection.js @@ -5,36 +5,7 @@ const { generateActionsResponse } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); -const { MessageActionRow, MessageButton, MessageEmbed } = require('discord.js'); - -// Command definition -module.exports.config = { - name: 'ping-protection', - description: localize('ping-protection', 'cmd-desc-module'), - usage: '/ping-protection', - type: 'slash', - options: [ - { - type: 'SUB_COMMAND_GROUP', - name: 'user', - description: localize('ping-protection', 'cmd-desc-group-user'), - options: [ - { type: 'SUB_COMMAND', name: 'history', description: localize('ping-protection', 'cmd-desc-history'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] }, - { type: 'SUB_COMMAND', name: 'actions-history', description: localize('ping-protection', 'cmd-desc-actions'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] }, - { type: 'SUB_COMMAND', name: 'panel', description: localize('ping-protection', 'cmd-desc-panel'), options: [{ type: 'USER', name: 'user', description: localize('ping-protection', 'cmd-opt-user'), required: true }] } - ] - }, - { - type: 'SUB_COMMAND_GROUP', - name: 'list', - description: localize('ping-protection', 'cmd-desc-group-list'), - options: [ - { type: 'SUB_COMMAND', name: 'protected', description: localize('ping-protection', 'cmd-desc-list-protected') }, - { type: 'SUB_COMMAND', name: 'whitelisted', description: localize('ping-protection', 'cmd-desc-list-wl') } - ] - } - ] -}; +const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle, MessageFlags } = require('discord.js'); module.exports.run = async function (interaction) { const group = interaction.options.getSubcommandGroup(false); @@ -60,29 +31,44 @@ module.exports.subcommands = { await interaction.reply(payload); }, 'panel': async function (interaction) { - const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || + const isAdmin = interaction.member.permissions.has('Administrator') || (interaction.client.config.admins || []).includes(interaction.user.id); - if (!isAdmin) return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); + + if (!isAdmin) return interaction.reply({ + content: localize('ping-protection', 'no-permission'), + flags: MessageFlags.Ephemeral + }); const user = interaction.options.getUser('user'); const pingerId = user.id; const storageConfig = interaction.client.configurations['ping-protection']['storage']; - const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) + ? storageConfig.pingHistoryRetention + : 12; const timeframeDays = retentionWeeks * 7; const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeDays); const modData = await fetchModHistory(interaction.client, pingerId, 1, 1000); - const row = new MessageActionRow().addComponents( - new MessageButton().setCustomId(`ping-protection_history_${user.id}`).setLabel(localize('ping-protection', 'btn-history')).setStyle('SECONDARY'), - new MessageButton().setCustomId(`ping-protection_actions_${user.id}`).setLabel(localize('ping-protection', 'btn-actions')).setStyle('SECONDARY'), - new MessageButton().setCustomId(`ping-protection_delete_${user.id}`).setLabel(localize('ping-protection', 'btn-delete')).setStyle('DANGER') + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_history_${user.id}`) + .setLabel(localize('ping-protection', 'btn-history')) + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId(`ping-protection_actions_${user.id}`) + .setLabel(localize('ping-protection', 'btn-actions')) + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId(`ping-protection_delete_${user.id}`) + .setLabel(localize('ping-protection', 'btn-delete')) + .setStyle(ButtonStyle.Danger) ); - const embed = new MessageEmbed() + const embed = new EmbedBuilder() .setTitle(localize('ping-protection', 'panel-title', { u: user.tag })) .setDescription(localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id })) - .setColor('BLUE') + .setColor('Blue') .setThumbnail(user.displayAvatarURL({ dynamic: true })) .addFields([{ name: localize('ping-protection', 'field-quick-history', { w: retentionWeeks }), @@ -90,7 +76,7 @@ module.exports.subcommands = { inline: false }]); - await interaction.reply({ embeds: [embed], components: [row], ephemeral: false }); + await interaction.reply({ embeds: [embed.toJSON()], components: [row.toJSON()] }); } }, 'list': { @@ -106,8 +92,8 @@ module.exports.subcommands = { // Handles list subcommands async function listHandler(interaction, type) { const config = interaction.client.configurations['ping-protection']['configuration']; - const embed = new MessageEmbed() - .setColor('GREEN') + const embed = new EmbedBuilder() + .setColor('Green') .setFooter({ text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl @@ -119,8 +105,13 @@ async function listHandler(interaction, type) { embed.setTitle(localize('ping-protection', 'list-protected-title')); embed.setDescription(localize('ping-protection', 'list-protected-desc')); - const usersList = config.protectedUsers.length > 0 ? config.protectedUsers.map(id => `<@${id}>`).join('\n') : localize('ping-protection', 'list-none'); - const rolesList = config.protectedRoles.length > 0 ? config.protectedRoles.map(id => `<@&${id}>`).join('\n') : localize('ping-protection', 'list-none'); + const usersList = config.protectedUsers.length > 0 + ? config.protectedUsers.map(id => `<@${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + const rolesList = config.protectedRoles.length > 0 + ? config.protectedRoles.map(id => `<@&${id}>`).join('\n') + : localize('ping-protection', 'list-none'); embed.addFields([ { name: localize('ping-protection', 'field-prot-users'), value: usersList, inline: true }, @@ -131,8 +122,13 @@ async function listHandler(interaction, type) { embed.setTitle(localize('ping-protection', 'list-whitelist-title')); embed.setDescription(localize('ping-protection', 'list-whitelist-desc')); - const rolesList = config.ignoredRoles.length > 0 ? config.ignoredRoles.map(id => `<@&${id}>`).join('\n') : localize('ping-protection', 'list-none'); - const channelsList = config.ignoredChannels.length > 0 ? config.ignoredChannels.map(id => `<#${id}>`).join('\n') : localize('ping-protection', 'list-none'); + const rolesList = config.ignoredRoles.length > 0 + ? config.ignoredRoles.map(id => `<@&${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + const channelsList = config.ignoredChannels.length > 0 + ? config.ignoredChannels.map(id => `<#${id}>`).join('\n') + : localize('ping-protection', 'list-none'); embed.addFields([ { name: localize('ping-protection', 'field-wl-roles'), value: rolesList, inline: true }, @@ -140,5 +136,71 @@ async function listHandler(interaction, type) { ]); } - await interaction.reply({ embeds: [embed], ephemeral: false }); -} \ No newline at end of file + await interaction.reply({ embeds: [embed.toJSON()] }); +} + +module.exports.config = { + name: 'ping-protection', + description: localize('ping-protection', 'cmd-desc-module'), + usage: '/ping-protection', + type: 'slash', + options: [ + { + type: 'SUB_COMMAND_GROUP', + name: 'user', + description: localize('ping-protection', 'cmd-desc-group-user'), + options: [ + { + type: 'SUB_COMMAND', + name: 'history', + description: localize('ping-protection', 'cmd-desc-history'), + options: [{ + type: 'USER', + name: 'user', + description: localize('ping-protection', 'cmd-opt-user'), + required: true + }] + }, + { + type: 'SUB_COMMAND', + name: 'actions-history', + description: localize('ping-protection', 'cmd-desc-actions'), + options: [{ + type: 'USER', + name: 'user', + description: localize('ping-protection', 'cmd-opt-user'), + required: true + }] + }, + { + type: 'SUB_COMMAND', + name: 'panel', + description: localize('ping-protection', 'cmd-desc-panel'), + options: [{ + type: 'USER', + name: 'user', + description: localize('ping-protection', 'cmd-opt-user'), + required: true + }] + } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'list', + description: localize('ping-protection', 'cmd-desc-group-list'), + options: [ + { + type: 'SUB_COMMAND', + name: 'protected', + description: localize('ping-protection', 'cmd-desc-list-protected') + }, + { + type: 'SUB_COMMAND', + name: 'whitelisted', + description: localize('ping-protection', 'cmd-desc-list-wl') + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 3377f62f..871f6959 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -187,9 +187,9 @@ "default": { "en": { "title": "You are not allowed to ping %target-name%!", - "description": "<@%user-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the command.\n\nIf you were replying, make sure to turn off the mention in the reply.", + "description": "<@%user-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the ping-protection list protected` command.\n\nIf you were replying, make sure to turn off the mention in the reply.", "image": "https://c.tenor.com/zRR5cYKSIV0AAAAd/tenor.gif", - "color": "RED" + "color": "Red" } } } diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index 08d99bdf..e1bd6eca 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -1,6 +1,7 @@ -const { Modal, TextInputComponent, MessageActionRow } = require('discord.js'); +const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, MessageFlags } = require('discord.js'); const { deleteAllUserData, generateHistoryResponse, generateActionsResponse } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); + // Interaction handler module.exports.run = async function (client, interaction) { if (!client.botReadyAt) return; @@ -31,37 +32,37 @@ module.exports.run = async function (client, interaction) { // Panel buttons const [prefix, action, userId] = interaction.customId.split('_'); - const isAdmin = interaction.member.permissions.has('ADMINISTRATOR') || + const isAdmin = interaction.member.permissions.has('Administrator') || (client.config.admins || []).includes(interaction.user.id); if (['history', 'actions', 'delete'].includes(action)) { - if (!isAdmin) return interaction.reply({ content: localize('ping-protection', 'no-permission'), ephemeral: true }); + if (!isAdmin) return interaction.reply({ + content: localize('ping-protection', 'no-permission'), + flags: MessageFlags.Ephemeral }); } if (action === 'history') { const replyOptions = await generateHistoryResponse(client, userId, 1); - replyOptions.ephemeral = false; await interaction.reply(replyOptions); } else if (action === 'actions') { const replyOptions = await generateActionsResponse(client, userId, 1); - replyOptions.ephemeral = false; await interaction.reply(replyOptions); } else if (action === 'delete') { - const modal = new Modal() + const modal = new ModalBuilder() .setCustomId(`ping-protection_confirm-delete_${userId}`) .setTitle(localize('ping-protection', 'modal-title')); - const input = new TextInputComponent() + const input = new TextInputBuilder() .setCustomId('confirmation_text') .setLabel(localize('ping-protection', 'modal-label')) - .setStyle('PARAGRAPH') + .setStyle(TextInputStyle.Paragraph) .setPlaceholder(localize('ping-protection', 'modal-phrase')) .setRequired(true); - const row = new MessageActionRow().addComponents(input); + const row = new ActionRowBuilder().addComponents(input); modal.addComponents(row); await interaction.showModal(modal); @@ -75,9 +76,13 @@ module.exports.run = async function (client, interaction) { if (userInput === requiredPhrase) { await deleteAllUserData(client, userId); - await interaction.reply({ content: `✅ ${localize('ping-protection', 'log-manual-delete', {u: userId})}`, ephemeral: true }); + await interaction.reply({ + content: `✅ ${localize('ping-protection', 'log-manual-delete', {u: userId})}`, + flags: MessageFlags.Ephemeral }); } else { - await interaction.reply({ content: `❌ ${localize('ping-protection', 'modal-failed')}`, ephemeral: true }); + await interaction.reply({ + content: `❌ ${localize('ping-protection', 'modal-failed')}`, + flags: MessageFlags.Ephemeral }); } } }; \ No newline at end of file diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index 2a4fd569..9c651938 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -4,11 +4,14 @@ const { executeAction, sendPingWarning } = require('../ping-protection'); +const { Op } = require('sequelize'); const { localize } = require('../../../src/functions/localize'); + // Tracks the last meme to prevent many duplicates const lastMemeMap = new Map(); // Tracks ping counts for the grind message const selfPingCountMap = new Map(); + // Handles messages module.exports.run = async function (client, message) { if (!client.botReadyAt) return; @@ -30,8 +33,9 @@ module.exports.run = async function (client, message) { // Check for protected pings const pingedProtectedRole = message.mentions.roles.some(role => config.protectedRoles.includes(role.id)); let protectedMentions = message.mentions.users.filter(user => config.protectedUsers.includes(user.id)); + // Handles reply pings - if (config.allowReplyPings && message.type === 'REPLY' && message.mentions.repliedUser) { + if (config.allowReplyPings && message.mentions.repliedUser) { const repliedId = message.mentions.repliedUser.id; if (protectedMentions.has(repliedId)) { @@ -58,58 +62,66 @@ module.exports.run = async function (client, message) { if (!target) return; // Funny easter egg when they ping themselves - if (target.id === message.author.id && config.allowSelfPing) { - const secretChance = 0.01; // Secret for a reason.. (1% chance) - const standardMemes = [ - '[Why are you the way that you are?]() - You just pinged yourself..', - '🔑 [Congratulations, you played yourself.]()', - '🕷️ [Is this you?]() - You just pinged yourself.' - ]; - const secretMeme = '🎵 [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!!'; - const currentCount = (selfPingCountMap.get(message.author.id) || 0) + 1; - selfPingCountMap.set(message.author.id, currentCount); - - setTimeout(() => { - selfPingCountMap.delete(message.author.id); - }, 300000); - - const roll = Math.random(); - let content = ''; - - if (roll < secretChance) { - content = secretMeme; - lastMemeMap.set(message.author.id, -1); - selfPingCountMap.delete(message.author.id); // Reset on secret unlock - } else if (currentCount === 5) { - content = '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)'; - } else { - const lastIndex = lastMemeMap.get(message.author.id); - - let possibleMemes = standardMemes.map((_, index) => index); - if (lastIndex !== undefined && lastIndex !== -1 && standardMemes.length > 1) { - possibleMemes = possibleMemes.filter(i => i !== lastIndex); - } + if (target.id === message.author.id && config.selfPingConfiguration === "Allowed, and ignored") return; + if (target.id === message.author.id && config.selfPingConfiguration === "Allowed, but with fun easter eggs") { + const secretChance = 0.01; // Secret for a reason.. (1% chance) + const standardMemes = [ + localize('ping-protection', 'meme-why'), + localize('ping-protection', 'meme-played'), + localize('ping-protection', 'meme-spider') + ]; + const secretMeme = localize('ping-protection', 'meme-rick'); + const currentCount = (selfPingCountMap.get(message.author.id) || 0) + 1; + selfPingCountMap.set(message.author.id, currentCount); + + setTimeout(() => { + selfPingCountMap.delete(message.author.id); + }, 300000); + + const roll = Math.random(); + let content = ''; + + if (roll < secretChance) { + content = secretMeme; + lastMemeMap.set(message.author.id, -1); + selfPingCountMap.delete(message.author.id); + } else if (currentCount === 5) { + content = localize('ping-protection', 'meme-grind'); + } else { + const lastIndex = lastMemeMap.get(message.author.id); - const randomIndex = possibleMemes[Math.floor(Math.random() * possibleMemes.length)]; - content = standardMemes[randomIndex]; - lastMemeMap.set(message.author.id, randomIndex); - } - await message.reply({ content: content }).catch(() => {}); - return; + let possibleMemes = standardMemes.map((_, index) => index); + if (lastIndex !== undefined && lastIndex !== -1 && standardMemes.length > 1) { + possibleMemes = possibleMemes.filter(i => i !== lastIndex); + } + + const randomIndex = possibleMemes[Math.floor(Math.random() * possibleMemes.length)]; + content = standardMemes[randomIndex]; + lastMemeMap.set(message.author.id, randomIndex); + } + await message.reply({ content: content }).catch(() => {}); + return; } let pingCount = 0; const pingerId = message.author.id; let timeframeDays = 84; - let rule1 = (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) ? moderationRules[0] : null; + let rule1 = null; + if (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) { + rule1 = moderationRules[0]; + } if (!!storageConfig && !!storageConfig.enablePingHistory) { try { - await addPing(client, message, target); + const isRole = !target.username; + await addPing(client, pingerId, message.url, target.id, isRole); + if (rule1 && !!rule1.useCustomTimeframe) { timeframeDays = rule1.timeframeDays; } else { - const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) + ? storageConfig.pingHistoryRetention + : 12; timeframeDays = retentionWeeks * 7; } pingCount = await getPingCountInWindow(client, pingerId, timeframeDays); @@ -126,27 +138,40 @@ module.exports.run = async function (client, message) { if (!!rule1.useCustomTimeframe) { requiredCount = rule1.pingsCountAdvanced; - generatedReason = localize('ping-protection', 'reason-advanced', { c: pingCount, d: rule1.timeframeDays }); + generatedReason = localize('ping-protection', 'reason-advanced', { + c: pingCount, + d: rule1.timeframeDays + }); } else { requiredCount = rule1.pingsCountBasic; - const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; - generatedReason = localize('ping-protection', 'reason-basic', { c: pingCount, w: retentionWeeks }); + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) + ? storageConfig.pingHistoryRetention + : 12; + + generatedReason = localize('ping-protection', 'reason-basic', { + c: pingCount, + w: retentionWeeks + }); } if (pingCount >= requiredCount) { - const { Op } = require('sequelize'); const oneMinuteAgo = new Date(new Date() - 60000); try { const recentLog = await client.models['ping-protection']['ModerationLog'].findOne({ - where: { victimID: message.author.id, createdAt: { [Op.gt]: oneMinuteAgo } } + where: { + victimID: message.author.id, + createdAt: { [Op.gt]: oneMinuteAgo } + } }); if (recentLog) return; } catch (e) {} let memberToPunish = message.member; if (!memberToPunish) { - try { memberToPunish = await message.guild.members.fetch(message.author.id); } catch (e) { return; } + try { + memberToPunish = await message.guild.members.fetch(message.author.id); + } catch (e) { return; } } await executeAction(client, memberToPunish, rule1, generatedReason, storageConfig); } -}; +}; \ No newline at end of file diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 83faa413..3e92c31f 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -4,17 +4,16 @@ * @author itskevinnn */ const { Op } = require('sequelize'); -const { MessageActionRow, MessageButton, MessageEmbed } = require('discord.js'); +const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle, resolveColor } = require('discord.js'); const { embedType, embedTypeV2, formatDate } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); // Data handling -async function addPing(client, messageObj, target) { - const isRole = !target.username; +async function addPing(client, userId, messageUrl, targetId, isRole) { await client.models['ping-protection']['PingHistory'].create({ - userId: messageObj.author.id, - messageUrl: messageObj.url || 'Blocked by AutoMod', - targetId: target.id, + userId: userId, + messageUrl: messageUrl || 'Blocked by AutoMod', + targetId: targetId, isRole: isRole }); } @@ -80,6 +79,15 @@ async function sendPingWarning(client, message, target, moduleConfig) { const warningMsg = moduleConfig.pingWarningMessage; if (!warningMsg) return; + let warnMsg = { ...warningMsg }; + if (warnMsg.color) { + try { + safeMsg.color = resolveColor(warnMsg.color); + } catch (err) { + delete warnMsg.color; + } + } + const placeholders = { '%target-name%': target.name || target.tag || target.username || 'Unknown', '%target-mention%': target.toString(), @@ -87,11 +95,16 @@ async function sendPingWarning(client, message, target, moduleConfig) { '%user-id%': message.author.id }; - let messageOptions = await embedTypeV2(warningMsg, placeholders); - return message.reply(messageOptions).catch(async () => { - return message.channel.send(messageOptions).catch(() => {}); - }); + try { + let messageOptions = await embedTypeV2(warnMsg, placeholders); + return message.reply(messageOptions).catch(async () => { + return message.channel.send(messageOptions).catch(() => {}); + }); + } catch (e) { + client.logger.warn(`[Ping Protection] ${error.message}`); + } } + // Syncs the native AutoMod rule based on configuration async function syncNativeAutoMod(client) { const config = client.configurations['ping-protection']['configuration']; @@ -111,7 +124,7 @@ async function syncNativeAutoMod(client) { const protectedIds = [...(config.protectedRoles || []), ...(config.protectedUsers || [])]; - // Deletest the rule if there are no protected IDs + // Deletes the rule if there are no protected IDs if (protectedIds.length === 0) { if (existingRule) { await existingRule.delete().catch(() => {}); @@ -157,6 +170,7 @@ async function syncNativeAutoMod(client) { client.logger.error(`[ping-protection] AutoMod Sync/Cleanup Failed: ${error.message}`); } } + // Generates history response async function generateHistoryResponse(client, userId, page = 1) { const storageConfig = client.configurations['ping-protection']['storage']; @@ -172,13 +186,20 @@ async function generateHistoryResponse(client, userId, page = 1) { totalPages = Math.ceil(total / limit) || 1; } - const user = await client.users.fetch(userId).catch(() => ({ username: 'Unknown User', displayAvatarURL: () => null })); + const user = await client.users.fetch(userId).catch(() => ({ + username: 'Unknown User', + displayAvatarURL: () => null + })); + const leaverData = await getLeaverStatus(client, userId); let description = ""; if (leaverData) { const dateStr = formatDate(leaverData.leftAt); - description += `⚠️ ${localize('ping-protection', history.length > 0 ? 'leaver-warning-long' : 'leaver-warning-short', { d: dateStr })}\n\n`; + const warningKey = history.length > 0 + ? 'leaver-warning-long' + : 'leaver-warning-short'; + description += `⚠️ ${localize('ping-protection', warningKey, { d: dateStr })}\n\n`; } if (!isEnabled) { @@ -188,36 +209,67 @@ async function generateHistoryResponse(client, userId, page = 1) { } else { const lines = history.map((entry, index) => { const timeString = formatDate(entry.createdAt); - const targetString = entry.targetId ? (entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`) : "Detected"; + + let targetString = "Detected"; + if (entry.targetId) { + targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; + } return `${(page - 1) * limit + index + 1}. **Pinged ${targetString}** at ${timeString}\n[Jump to Message](${entry.messageUrl})`; }); description += lines.join('\n\n'); } - const row = new MessageActionRow().addComponents( - new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${page - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(page <= 1), - new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${page}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), - new MessageButton().setCustomId(`ping-protection_hist-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(page >= totalPages || !isEnabled) + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_hist-page_${userId}_${page - 1}`) + .setLabel('Back') + .setStyle(ButtonStyle.Primary) + .setDisabled(page <= 1), + new ButtonBuilder() + .setCustomId('ping_protection_page_count') + .setLabel(`${page}/${totalPages}`) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`ping-protection_hist-page_${userId}_${page + 1}`) + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setDisabled(page >= totalPages || !isEnabled) ); - const embed = new MessageEmbed() - .setTitle(localize('ping-protection', 'embed-history-title', { u: user.username })) - .setThumbnail(user.displayAvatarURL({ dynamic: true })) + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'embed-history-title', { + u: user.username + })) + .setThumbnail(user.displayAvatarURL({ + dynamic: true + })) .setDescription(description) - .setColor('ORANGE') - .setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); + .setColor('Orange') + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); - - return { embeds: [embed], components: [row] }; + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; } + // Generates actions response async function generateActionsResponse(client, userId, page = 1) { const moderationConfig = client.configurations['ping-protection']['moderation']; const limit = 8; - const rule1 = (moderationConfig && Array.isArray(moderationConfig) && moderationConfig.length > 0) ? moderationConfig[0] : null; - const isEnabled = rule1 ? rule1.enableModeration : false; + const rule1 = (moderationConfig && Array.isArray(moderationConfig) && moderationConfig.length > 0) + ? moderationConfig[0] + : null; + + const isEnabled = rule1 + ? rule1.enableModeration + : false; let total = 0, history = [], totalPages = 1; @@ -226,7 +278,11 @@ async function generateActionsResponse(client, userId, page = 1) { history = data.history; totalPages = Math.ceil(total / limit) || 1; - const user = await client.users.fetch(userId).catch(() => ({ username: 'Unknown User', displayAvatarURL: () => null })); + const user = await client.users.fetch(userId).catch(() => ({ + username: 'Unknown User', + displayAvatarURL: () => null + })); + let description = ""; if (!isEnabled) { @@ -244,32 +300,61 @@ async function generateActionsResponse(client, userId, page = 1) { description += lines.join('\n\n') + `\n\n*${localize('ping-protection', 'actions-retention-note')}*`; } - const row = new MessageActionRow().addComponents( - new MessageButton().setCustomId(`ping-protection_mod-page_${userId}_${page - 1}`).setLabel('Back').setStyle('PRIMARY').setDisabled(page <= 1), - new MessageButton().setCustomId('ping_protection_page_count').setLabel(`${page}/${totalPages}`).setStyle('SECONDARY').setDisabled(true), - new MessageButton().setCustomId(`ping-protection_mod-page_${userId}_${page + 1}`).setLabel('Next').setStyle('PRIMARY').setDisabled(page >= totalPages || (!isEnabled && history.length === 0)) + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_mod-page_${userId}_${page - 1}`) + .setLabel('Back') + .setStyle(ButtonStyle.Primary) + .setDisabled(page <= 1), + new ButtonBuilder() + .setCustomId('ping_protection_page_count') + .setLabel(`${page}/${totalPages}`) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`ping-protection_mod-page_${userId}_${page + 1}`) + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setDisabled(page >= totalPages || (!isEnabled && history.length === 0)) ); - const embed = new MessageEmbed() + const embed = new EmbedBuilder() .setTitle(localize('ping-protection', 'embed-actions-title', { u: user.username })) .setThumbnail(user.displayAvatarURL({ dynamic: true })) .setDescription(description) - .setColor(isEnabled ? 'RED' : 'GREY') + .setColor(isEnabled + ? 'Red' + : 'Grey' + ) .setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); - - return { embeds: [embed], components: [row] }; + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; } + // Handles data deletion async function deleteAllUserData(client, userId) { - await client.models['ping-protection']['PingHistory'].destroy({ where: { userId: userId } }); - await client.models['ping-protection']['ModerationLog'].destroy({ where: { victimID: userId } }); - await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); - client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-manual-delete-logs', { u: userId })); + await client.models['ping-protection']['PingHistory'].destroy({ + where: { userId: userId } + }); + await client.models['ping-protection']['ModerationLog'].destroy({ + where: { victimID: userId } + }); + await client.models['ping-protection']['LeaverData'].destroy({ + where: { userId: userId } + }); + client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-manual-delete-logs', { + u: userId + })); } async function markUserAsLeft(client, userId) { - await client.models['ping-protection']['LeaverData'].upsert({ userId: userId, leftAt: new Date() }); + await client.models['ping-protection']['LeaverData'].upsert({ + userId: userId, + leftAt: new Date() + }); } async function markUserAsRejoined(client, userId) { await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); @@ -304,12 +389,20 @@ async function enforceRetention(client) { if (storageConfig.modLogRetention) { const modCutoff = new Date(); modCutoff.setMonth(modCutoff.getMonth() - (storageConfig.modLogRetention || 6)); - await client.models['ping-protection']['ModerationLog'].destroy({ where: { createdAt: { [Op.lt]: modCutoff } } }); + await client.models['ping-protection']['ModerationLog'].destroy({ + where: { + createdAt: { [Op.lt]: modCutoff } + } + }); } if (storageConfig.enableLeaverDataRetention) { const leaverCutoff = new Date(); leaverCutoff.setDate(leaverCutoff.getDate() - (storageConfig.leaverRetention || 1)); - const leaversToDelete = await client.models['ping-protection']['LeaverData'].findAll({ where: { leftAt: { [Op.lt]: leaverCutoff } } }); + const leaversToDelete = await client.models['ping-protection']['LeaverData'].findAll({ + where: { + leftAt: { [Op.lt]: leaverCutoff } + } + }); for (const leaver of leaversToDelete) { await deleteAllUserData(client, leaver.userId); await leaver.destroy(); @@ -325,7 +418,9 @@ async function executeAction(client, member, rule, reason, storageConfig) { } const botMember = await member.guild.members.fetch(client.user.id); if (botMember.roles.highest.position <= member.roles.highest.position) { - client.logger.warn('[Ping Protection] ' + localize('ping-protection', 'punish-role-error', {tag: member.user.tag})); + client.logger.warn('[Ping Protection] ' + localize('ping-protection', 'punish-role-error', { + tag: member.user.tag + })); return false; } const logDb = async (type, duration = null) => { From fecf2e452838379e13a04cbe449a87eec87dd176 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Mon, 19 Jan 2026 17:08:49 +0100 Subject: [PATCH 31/31] Ping Protection V1 --- locales/en.json | 17 ++- .../configs/configuration.json | 2 +- .../ping-protection/configs/moderation.json | 69 ++++++++++ .../events/autoModerationActionExecution.js | 37 ++++-- .../ping-protection/events/messageCreate.js | 9 +- modules/ping-protection/ping-protection.js | 123 ++++++++++++++---- 6 files changed, 214 insertions(+), 43 deletions(-) diff --git a/locales/en.json b/locales/en.json index e2e01e45..b8744e45 100644 --- a/locales/en.json +++ b/locales/en.json @@ -930,14 +930,17 @@ "nickname-error": "An error occurred while trying to change the nickname of %u: %e" }, "ping-protection": { - "not-a-member": "Punishment failed: The pinger is not a member.", - "punish-role-error": "Punishment failed: I cannot punish %tag because their role is higher than or equal to my highest role.", - "log-mute-success": "Muted %tag for %dur minutes because they exceeded the ping limit.", - "log-kick-success": "Kicked %tag because they exceeded the ping limit.", - "log-mute-error": "Punishment failed: I cannot mute %tag: %e", - "log-kick-error": "Punishment failed: I cannot kick %tag: %e", + "log-not-a-member": "[Ping Protection] Punishment failed: The pinger is not a member.", + "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-action-log-failed": "[Ping Protection] Punishment logging failed: %e", "log-manual-delete": "All data for <@%u> (%u) has been deleted successfully.", - "log-manual-delete-logs": "All data for user with ID %u has been deleted successfully.", + "log-manual-delete-logs": "[Ping Protection] All data for user with ID %u has been deleted successfully.", + "punish-log-failed-title": "Punishment failed for user %u", + "punish-log-failed-desc": "An error occured while trying to punish the user %m. Please check the bot's permissions and role hierarchy. See the message below for the error.", + "punish-log-error": "Error: ```%e```", + "punish-role-error": "I cannot punish %tag because their role is higher than or equal to my highest role.", "reason-basic": "User reached %c pings in the last %w weeks.", "reason-advanced": "User reached %c pings in the last %d days (Custom timeframe).", "cmd-desc-module": "Ping protection related commands", diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 871f6959..bc70e401 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -189,7 +189,7 @@ "title": "You are not allowed to ping %target-name%!", "description": "<@%user-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the ping-protection list protected` command.\n\nIf you were replying, make sure to turn off the mention in the reply.", "image": "https://c.tenor.com/zRR5cYKSIV0AAAAd/tenor.gif", - "color": "Red" + "color": "16711680" } } } diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index 447cae1b..1c55dda6 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -105,6 +105,75 @@ "default": { "en": 60 } + }, + { + "name": "enableActionLogging", + "humanName": { + "en": "Enable action logging" + }, + "description": { + "en": "If enabled, moderation actions will be logged in the channel where a protected user/role got pinged." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "actionLogMessage", + "humanName": { + "en": "Action log message" + }, + "description": { + "en": "The message that will be sent when a user is punished for pinging protected users/roles." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "pinger-mention", + "description": { + "en": "Mention of the user who pinged" + } + }, + { + "name": "pinger-name", + "description": { + "en": "Name of the user who pinged" + } + }, + { + "name": "action", + "description": { + "en": "The action that was taken (muted/kicked)" + } + }, + { + "name": "pings", + "description": { + "en": "Number of pings that triggered the action" + } + }, + { + "name": "timeframe", + "description": { + "en": "The timeframe in days in which the pings occurred" + } + }, + { + "name": "duration", + "description": { + "en": "Duration of the mute in minutes (only for the mute action)" + } + } + ], + "default": { + "en": { + "title": "Moderation action taken against %pinger-name%", + "description": "I have taken action against %pinger-mention% for pinging protected users/roles %pings% times within %timeframe% days.\n **Action:** %action%\n**Duration:** %duration% minutes", + "color": 16711680 + } + } } ] } \ No newline at end of file diff --git a/modules/ping-protection/events/autoModerationActionExecution.js b/modules/ping-protection/events/autoModerationActionExecution.js index e7aabad4..5509f476 100644 --- a/modules/ping-protection/events/autoModerationActionExecution.js +++ b/modules/ping-protection/events/autoModerationActionExecution.js @@ -24,7 +24,9 @@ module.exports.run = async function (client, execution) { let pingCount = 0; let timeframeDays = 84; - let rule1 = (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) ? moderationRules[0] : null; + let rule1 = (moderationRules && Array.isArray(moderationRules) && moderationRules.length > 0) + ? moderationRules[0] + : null; if (!!storageConfig && !!storageConfig.enablePingHistory) { const mockAuthor = { id: execution.userId }; @@ -32,11 +34,14 @@ module.exports.run = async function (client, execution) { const mockTarget = { id: rawId }; try { - await addPing(client, mockMessage, mockTarget); + await addPing(client, mockMessage.author.id, mockMessage.url, mockTarget.id, false); if (rule1 && !!rule1.useCustomTimeframe) { timeframeDays = rule1.timeframeDays || 7; - } else { - const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention : 12; + } + else { + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) + ? storageConfig.pingHistoryRetention + : 12; timeframeDays = retentionWeeks * 7; } pingCount = await getPingCountInWindow(client, execution.userId, timeframeDays); @@ -46,15 +51,31 @@ module.exports.run = async function (client, execution) { if (!rule1 || !rule1.enableModeration) return; - let requiredCount = (rule1.useCustomTimeframe) ? rule1.pingsCountAdvanced : rule1.pingsCountBasic; + let requiredCount = (rule1.useCustomTimeframe) + ? rule1.pingsCountAdvanced + : rule1.pingsCountBasic; let generatedReason = (rule1.useCustomTimeframe) - ? localize('ping-protection', 'reason-advanced', { c: pingCount, d: rule1.timeframeDays }) - : localize('ping-protection', 'reason-basic', { c: pingCount, w: (storageConfig.pingHistoryRetention || 12) }); + ? localize('ping-protection', 'reason-advanced', { + c: pingCount, + d: rule1.timeframeDays }) + : localize('ping-protection', 'reason-basic', { + c: pingCount, + w: (storageConfig.pingHistoryRetention || 12) + }); if (pingCount >= requiredCount) { const memberToPunish = await execution.guild.members.fetch(execution.userId).catch(() => null); + + let originChannel = execution.channel; + if (!originChannel && execution.channelId) { + originChannel = await execution.guild.channels.fetch(execution.channelId).catch(() => null); + } + if (memberToPunish) { - await executeAction(client, memberToPunish, rule1, generatedReason, storageConfig); + await executeAction( + client,memberToPunish,rule1,generatedReason,storageConfig,originChannel, + { pingCount, timeframeDays } + ); } } }; \ No newline at end of file diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index 9c651938..bbdc8c07 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -142,7 +142,8 @@ module.exports.run = async function (client, message) { c: pingCount, d: rule1.timeframeDays }); - } else { + } + else { requiredCount = rule1.pingsCountBasic; const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) ? storageConfig.pingHistoryRetention @@ -172,6 +173,10 @@ module.exports.run = async function (client, message) { memberToPunish = await message.guild.members.fetch(message.author.id); } catch (e) { return; } } - await executeAction(client, memberToPunish, rule1, generatedReason, storageConfig); + + await executeAction( + client,memberToPunish,rule1,generatedReason,storageConfig,message.channel, + { pingCount, timeframeDays } + ); } }; \ No newline at end of file diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 3e92c31f..73926e45 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -4,7 +4,7 @@ * @author itskevinnn */ const { Op } = require('sequelize'); -const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle, resolveColor } = require('discord.js'); +const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle } = require('discord.js'); const { embedType, embedTypeV2, formatDate } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); @@ -80,14 +80,6 @@ async function sendPingWarning(client, message, target, moduleConfig) { if (!warningMsg) return; let warnMsg = { ...warningMsg }; - if (warnMsg.color) { - try { - safeMsg.color = resolveColor(warnMsg.color); - } catch (err) { - delete warnMsg.color; - } - } - const placeholders = { '%target-name%': target.name || target.tag || target.username || 'Unknown', '%target-mention%': target.toString(), @@ -100,7 +92,7 @@ async function sendPingWarning(client, message, target, moduleConfig) { return message.reply(messageOptions).catch(async () => { return message.channel.send(messageOptions).catch(() => {}); }); - } catch (e) { + } catch (error) { client.logger.warn(`[Ping Protection] ${error.message}`); } } @@ -166,7 +158,7 @@ async function syncNativeAutoMod(client) { } else { await guild.autoModerationRules.create(ruleData); } - } catch (e) { + } catch (error) { client.logger.error(`[ping-protection] AutoMod Sync/Cleanup Failed: ${error.message}`); } } @@ -319,14 +311,21 @@ async function generateActionsResponse(client, userId, page = 1) { ); const embed = new EmbedBuilder() - .setTitle(localize('ping-protection', 'embed-actions-title', { u: user.username })) - .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .setTitle(localize('ping-protection', 'embed-actions-title', { + u: user.username + })) + .setThumbnail(user.displayAvatarURL({ + dynamic: true + })) .setDescription(description) .setColor(isEnabled ? 'Red' : 'Grey' ) - .setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); return { @@ -346,7 +345,7 @@ async function deleteAllUserData(client, userId) { await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); - client.logger.info('[ping-protection] ' + localize('ping-protection', 'log-manual-delete-logs', { + client.logger.info(localize('ping-protection', 'log-manual-delete-logs', { u: userId })); } @@ -357,18 +356,24 @@ async function markUserAsLeft(client, userId) { }); } async function markUserAsRejoined(client, userId) { - await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); + await client.models['ping-protection']['LeaverData'].destroy({ + where: { userId: userId } + }); } async function enforceRetention(client) { const storageConfig = client.configurations['ping-protection']['storage']; if (!storageConfig) return; + if (storageConfig.enablePingHistory) { const historyCutoff = new Date(); const retentionWeeks = storageConfig.pingHistoryRetention || 12; historyCutoff.setDate(historyCutoff.getDate() - (retentionWeeks * 7)); + if (storageConfig.DeleteAllPingHistoryAfterTimeframe) { const usersWithExpiredData = await client.models['ping-protection']['PingHistory'].findAll({ - where: { createdAt: { [Op.lt]: historyCutoff } }, + where: { + createdAt: { [Op.lt]: historyCutoff } + }, attributes: ['userId'], group: ['userId'] }); @@ -410,19 +415,76 @@ async function enforceRetention(client) { } } -async function executeAction(client, member, rule, reason, storageConfig) { +// Executes moderation action +async function executeAction(client, member, rule, reason, storageConfig, originChannel = null, stats = {}) { const actionType = rule.actionType; + + // Sends action log if enabled + const sendActionLog = async () => { + if (!rule.enableActionLogging || !originChannel) return; + + const logMsgConfig = rule.actionLogMessage; + if (!logMsgConfig) return; + let safeMsg = { ...logMsgConfig }; + + const placeholders = { + '%pinger-mention%': member.toString(), + '%pinger-name%': member.user.tag, + '%action%': rule.actionType, + '%duration%': rule.muteDuration || 'N/A', + '%pings%': stats.pingCount || 'N/A', + '%timeframe%': stats.timeframeDays || 'N/A' + }; + + try { + let messageOptions = await embedTypeV2(safeMsg, placeholders); + await originChannel.send(messageOptions).catch(() => {}); + } catch (error) { + client.logger.warn(localize('ping-protection', 'log-action-log-failed', { + e: error.message + })); + } + }; + + // Sends error message if action fails + const sendErrorLog = async (error) => { + if (!originChannel) return; + + const errorEmbed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'punish-log-failed-title', { + u: member.user.tag + })) + .setDescription( + localize('ping-protection', 'punish-log-failed-desc', { + m: member.toString() + }) + + `\n${localize('ping-protection', 'punish-log-error', { + e: error.message + })}` + ) + .setColor(16711680); + + await originChannel.send({ embeds: [errorEmbed.toJSON()] }).catch(() => {}); + }; + if (!member) { - client.logger.debug('[Ping Protection] ' + localize('ping-protection', 'not-a-member')); + client.logger.debug(localize('ping-protection', 'log-not-a-member')); return false; } + const botMember = await member.guild.members.fetch(client.user.id); if (botMember.roles.highest.position <= member.roles.highest.position) { - client.logger.warn('[Ping Protection] ' + localize('ping-protection', 'punish-role-error', { + await sendErrorLog({ + message: localize('ping-protection', 'punish-role-error', { + tag: member.user.tag + }) + }); + client.logger.warn(localize('ping-protection', 'log-punish-role-error', { tag: member.user.tag })); return false; } + const logDb = async (type, duration = null) => { try { await client.models['ping-protection']['ModerationLog'].create({ @@ -430,25 +492,36 @@ async function executeAction(client, member, rule, reason, storageConfig) { }); } catch (dbError) {} }; + if (actionType === 'MUTE') { const durationMs = rule.muteDuration * 60000; await logDb('MUTE', rule.muteDuration); try { await member.timeout(durationMs, reason); - client.logger.info('[Ping Protection] ' + localize('ping-protection', 'log-mute-success', {tag: member.user.tag, dur: rule.muteDuration})); + await sendActionLog(); return true; } catch (error) { - client.logger.warn('[Ping Protection] ' + localize('ping-protection', 'log-mute-error', {tag: member.user.tag, e: error.message})); + await sendErrorLog(error); + client.logger.warn(localize('ping-protection', 'log-mute-error', { + tag: member.user.tag, + e: error.message + })); return false; } - } else if (actionType === 'KICK') { + + } + else if (actionType === 'KICK') { await logDb('KICK'); try { await member.kick(reason); - client.logger.info('[Ping Protection] ' + localize('ping-protection', 'log-kick-success', {tag: member.user.tag})); + await sendActionLog(); return true; } catch (error) { - client.logger.warn('[Ping Protection] ' + localize('ping-protection', 'log-kick-error', {tag: member.user.tag, e: error.message})); + await sendErrorLog(error); + client.logger.warn(localize('ping-protection', 'log-kick-error', { + tag: member.user.tag, + e: error.message + })); return false; } }