From 376c75cd6ba374c2ed61aa0666a1b08c2a47da64 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 31 Mar 2026 19:21:54 +0200 Subject: [PATCH] Ping Protection V2 --- locales/en.json | 82 ++- .../commands/ping-protection.js | 53 +- .../configs/configuration.json | 13 +- .../ping-protection/configs/moderation.json | 32 + .../events/autoModerationActionExecution.js | 4 +- .../events/interactionCreate.js | 369 +++++++++-- .../ping-protection/events/messageCreate.js | 5 +- .../models/DeletionCooldown.js | 34 + modules/ping-protection/ping-protection.js | 587 ++++++++++++++++-- 9 files changed, 1013 insertions(+), 166 deletions(-) create mode 100644 modules/ping-protection/models/DeletionCooldown.js diff --git a/locales/en.json b/locales/en.json index a0637a9..f3ba92f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -423,6 +423,36 @@ "edit-modal-username-placeholder": "Username of the user", "user-not-found": "User not found" }, + "guess-the-number": { + "command-description": "Manage your guess-the-number-games", + "status-command-description": "Shows the current status of a guess-the-number-game in this channel", + "create-command-description": "Create a new guess-the-number-game in this channel", + "create-min-description": "Minimal value users can guess", + "create-max-description": "Maximal value users can guess", + "create-number-description": "Number users should guess to win", + "end-command-description": "Ends the current game", + "session-already-running": "There is a session already running in this channel. Please end it with /guess-the-number end", + "session-not-running": "There is currently no session running.", + "gamechannel-modus": "You can't use this command in a gamechannel. To end a game, disable the gamechannel modus and try using the end command.", + "session-ended-successfully": "Ended session successfully. Locked channel successfully.", + "current-session": "Current session", + "number": "Number", + "min-val": "Min-Value", + "max-val": "Max-Value", + "owner": "Owner", + "guess-count": "Count of guesses", + "min-max-discrepancy": "`min` can't be bigger or equal to `max`", + "max-discrepancy": "`number` can't be bigger than `max`.", + "min-discrepancy": "`number` can't be smaller than `min`.", + "emoji-guide-button": "What does the reaction under my guess mean?", + "guide-wrong-guess": "Your guess was wrong (but your entry was valid)", + "guide-win": "You guessed correctly - you win :tada:", + "guide-admin-guess": "Your guess was invalid, because you are an admin - admins can't participate because they can see the correct number", + "guide-invalid-guess": "Your guess was invalid (e.g. below the minimal / over the maximal number, not a number, …)", + "created-successfully": "Created game successfully. Users can now start guessing in this channel. The winning number is **%n**. You can always check the status by running `/guess-the-number-status`. Note that you as an admin can not guess.", + "game-ended": "Game ended", + "game-started": "Game started" + }, "massrole": { "command-description": "Manage roles for all members", "add-subcommand-description": "Add a role to all members", @@ -961,10 +991,7 @@ "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)", + "panel-description": "Manage and view data for %u (%i). You can see the user's ping history, moderation actions, quick recap of both, and view data deletion options for this user.", "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 when pinged by someone with a whitelisted role/as a whitelisted user or when it's sent in a whitelisted channel.", "field-protected-users": "Protected Users", @@ -977,9 +1004,8 @@ "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.", + "modal-phrase": "I understand that the 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.", - "modal-success-data-deletion": "All data for the user <@%u> (%u) has been deleted successfully", "field-quick-history": "Quick history view (Last %w weeks)", "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 \"Data Storage\" tab in the 'ping-protection' module ^^", @@ -992,6 +1018,48 @@ "meme-grind": "Why are you even pinging yourself 5 times in a row? Anyways continue some more to possibly get the secret meme\n-# (good luck grinding, only a 1% chance of getting it and during testing I had it once after 83 pings)", "label-jump": "Jump to Message", "no-message-link": "This ping was blocked by AutoMod", - "list-entry-text": "%index. **Pinged %target** at %time\n%link" + "list-entry-text": "%index. **Pinged %target** at %time\n%link", + "punish-log-docs-title": "Troubleshooting", + "punish-log-docs-desc": "This issue is documented in the documentation - you can see how to fix this issue [in the documentation](https://docs.scnx.xyz/docs/custom-bot/modules/moderation/ping-protection/#troubleshooting). Please try the steps mentioned there before contacting support as it's very likely the steps mentioned will fix your issue ^^", + "log-fetch-mod-history-failed": "[Ping Protection] Failed to fetch moderation history for user %u: %e", + "log-warning-build-failed": "[Ping Protection] Failed to build the warning message: %e", + "log-warning-reply-failed": "[Ping Protection] Failed to send the warning message as a reply: %e", + "log-warning-send-failed": "[Ping Protection] Failed to send the fallback warning message in channel %c: %e", + "log-automod-channel-fetch-failed": "[Ping Protection] Failed to refresh the guild channel cache while syncing AutoMod: %e", + "log-automod-rule-delete-failed": "[Ping Protection] Failed to delete the native AutoMod rule: %e", + "log-automod-sync-failed": "[Ping Protection] AutoMod sync failed: %e", + "log-punish-log-send-failed": "[Ping Protection] Failed to send the punishment failure message: %e", + "log-modlog-create-failed": "[Ping Protection] Failed to store the moderation log for user %u: %e", + "log-ping-history-create-failed": "[Ping Protection] Failed to store ping history for user %u: %e", + "log-recent-mod-check-failed": "[Ping Protection] Failed to check recent moderation actions for user %u: %e", + "panel-ph": "Select an option", + "panel-opt-over": "Overview", + "panel-opt-hist": "Ping History", + "panel-opt-actions": "Moderation History", + "panel-opt-delete": "Data Deletion", + "panel-deletion-title": "Data Deletion: %u", + "panel-deletion-desc": "⚠️ DANGEROUS AREA ⚠️\nYou are now entering a dangerous zone. At this place, you are able to delete specific or all data for the selected user. These actions ***CANNOT BE UNDONE*** and should only be used if you are absolutely sure about what you are doing.\nIf you are unsure, click 'Go Back' from the dropdown now.\nUse the dropdown below to choose which data you want to delete or delete all data. Choose wisely and gracefully.", + "panel-deletion-placeholder": "Select a deletion option", + "panel-opt-back": "Go back", + "panel-opt-del-pings": "Ping History Deletion", + "panel-opt-del-actions": "Moderation History Deletion", + "panel-opt-del-all": "Delete All Data", + "panel-deletion-cooldown-active": "Data deletion is currently blocked for this user because of a recent %type deletion. Deletion will be available again at %time.", + "del-type-pings": "ping history", + "del-type-actions": "moderation history", + "del-type-all": "full data", + "del-type-unknown": "data", + "del-all-admin-only": "Only users with Administrator permissions can delete all stored data for a user.", + "err-del-cooldown": "Data deletion for this user is currently on cooldown because of a recent %time deletion. You can delete data again at %until.", + "del-all-title": "Confirm full data deletion", + "del-all-desc": "You are about to delete ALL data for this user. Reminder that this ***cannot be undone***. This is the last chance to back out. If you are sure, click the button below.\nThis action will automatically cancel in 30 seconds.", + "btn-conf-del": "Confirm deletion", + "btn-cancel": "Cancel", + "succ-del-canc": "Data deletion cancelled.", + "err-del-time": "⏳ Data deletion timed out and was cancelled. Please try again if you still want to delete data for this user.", + "succ-del-tgt": "The selected %type data was deleted successfully. Deletion for this user is now on cooldown until %until.", + "succ-del-all": "All stored Ping Protection data for this user was deleted successfully. Deletion for this user is now on cooldown until %until.", + "log-del-type": "[Ping Protection] Deleted %type data for user %target by %admin.", + "log-del-all": "[Ping Protection] Deleted all stored data for user %target by %admin." } } diff --git a/modules/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js index d8ac43c..a96b0c2 100644 --- a/modules/ping-protection/commands/ping-protection.js +++ b/modules/ping-protection/commands/ping-protection.js @@ -1,12 +1,11 @@ const { - fetchModHistory, - getPingCountInWindow, generateHistoryResponse, - generateActionsResponse + generateActionsResponse, + generateUserPanel } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); const { truncate } = require('../../../src/functions/helpers'); -const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle, MessageFlags } = require('discord.js'); +const { EmbedBuilder, MessageFlags } = require('discord.js'); module.exports.run = async function (interaction) { const group = interaction.options.getSubcommandGroup(false); @@ -33,50 +32,10 @@ module.exports.subcommands = { }, 'panel': async function (interaction) { 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 timeframeDays = retentionWeeks * 7; - - const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeDays); - const modData = await fetchModHistory(interaction.client, pingerId, 1, 1000); + const payload = await generateUserPanel(interaction.client, user); - 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 EmbedBuilder() - .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: retentionWeeks }), - value: localize('ping-protection', 'field-quick-desc', { p: pingCount, m: modData.total }), - inline: false - }]) - .setFooter({ - text: interaction.client.strings.footer, - iconURL: interaction.client.strings.footerImgUrl - }); - if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); - - await interaction.reply({ - embeds: [embed.toJSON()], - components: [row.toJSON()], + await interaction.reply({ + ...payload, flags: MessageFlags.Ephemeral }); } diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index acd5b7d..517df2b 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -125,7 +125,12 @@ "content": "channelID", "default": { "en": [] - } + }, + "channelTypes": [ + "GUILD_TEXT", + "GUILD_NEWS", + "GUILD_CATEGORY" + ] }, { "name": "ignoredUsers", @@ -179,10 +184,10 @@ "name": "enableAutomod", "category": "automod", "humanName": { - "en": "Enable automod" + "en": "Enable AutoMod" }, "description": { - "en": "If enabled, the bot will utilise Discord's native AutoMod to block the message with a ping of 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. Warning: AutoMod does not support whitelisted categories due to limitations in Discord's AutoMod system - instead, it will still block the message but not log it in the history." }, "type": "boolean", "default": { @@ -196,7 +201,7 @@ "en": "AutoMod Log Channel" }, "description": { - "en": "Channel where AutoMod alerts are sent." + "en": "Channel where AutoMod alerts are sent. It is recommended to keep these in a private channel." }, "type": "channelID", "default": { diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index 1c15ed6..22b8fda 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -27,6 +27,37 @@ "en": 10 } }, + { + "name": "enableRolePingThresholds", + "humanName": { + "en": "Enable role-based ping thresholds" + }, + "description": { + "en": "If enabled, specific roles can have custom ping thresholds for this moderation action. This also allows specific roles to be exempted from this specific action." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "name": "rolePingThresholds", + "humanName": { + "en": "Role-based ping thresholds" + }, + "description": { + "en": "Set custom ping thresholds per role for this moderation action. If a user has multiple configured roles, the value of their highest configured role is used. Setting a role to 0 exempts that role from this action - exempted roles also override any other role's threshold." + }, + "type": "keyed", + "content": { + "key": "roleID", + "value": "integer" + }, + "default": { + "en": {} + }, + "dependsOn": "enableRolePingThresholds" + }, { "name": "useCustomTimeframe", "humanName": { @@ -106,6 +137,7 @@ "en": "The message that will be sent when a user is punished for pinging protected users/roles." }, "type": "string", + "dependsOn": "enableActionLogging", "allowEmbed": true, "params": [ { diff --git a/modules/ping-protection/events/autoModerationActionExecution.js b/modules/ping-protection/events/autoModerationActionExecution.js index 22f80fa..a7bfc49 100644 --- a/modules/ping-protection/events/autoModerationActionExecution.js +++ b/modules/ping-protection/events/autoModerationActionExecution.js @@ -1,4 +1,4 @@ -const { processPing } = require('../ping-protection'); +const { processPing, isWhitelistedChannel } = require('../ping-protection'); // Handles auto mod actions module.exports.run = async function (client, execution) { @@ -16,6 +16,8 @@ module.exports.run = async function (client, execution) { if (!originChannel && execution.channelId) { originChannel = await execution.guild.channels.fetch(execution.channelId).catch(() => null); } + if (isWhitelistedChannel(config, originChannel)) return; + const memberToPunish = await execution.guild.members.fetch(execution.userId).catch(() => null); if (!isProtected && config.protectAllUsersWithProtectedRole) { diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index 042de12..e964b29 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -1,18 +1,294 @@ -const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, MessageFlags } = require('discord.js'); -const { deleteAllUserData, generateHistoryResponse, generateActionsResponse } = require('../ping-protection'); +const { + generateHistoryResponse, + generateActionsResponse, + generateUserPanel, + generatePanelHistory, + generatePanelActions, + generatePanelDeletion, + executeDataDeletion, + getDeletionCooldown, + setDeletionCooldown, + getDeletionTypeLocaleKey +} = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); +const { + MessageFlags, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, + EmbedBuilder +} = require('discord.js'); // Interaction handler module.exports.run = async function (client, interaction) { if (!client.botReadyAt) return; - + const isAdmin = interaction.member?.permissions?.has('Administrator') + + if (interaction.isStringSelectMenu() && interaction.customId.startsWith('ping-protection_panel-menu_')) { + if (!isAdmin) { + return interaction.reply({ + content: localize('ping-protection', 'no-permission'), + flags: MessageFlags.Ephemeral + }); + } + + const targetId = interaction.customId.split('_')[2]; + const targetUser = await client.users.fetch(targetId).catch(() => null); + if (!targetUser) { + return interaction.reply({ + content: localize('ping-protection', 'no-data-found'), + flags: MessageFlags.Ephemeral + }); + } + + const selection = interaction.values[0]; + + let payload; + if (selection === 'overview') payload = await generateUserPanel(client, targetUser); + else if (selection === 'history') payload = await generatePanelHistory(client, targetUser, 1); + else if (selection === 'actions') payload = await generatePanelActions(client, targetUser, 1); + else if (selection === 'deletion') payload = await generatePanelDeletion(client, targetUser); + + if (payload) return interaction.update(payload); + return; + } + + if (interaction.isStringSelectMenu() && interaction.customId.startsWith('ping-protection_delete-menu_')) { + if (!isAdmin) { + return interaction.reply({ + content: localize('ping-protection', 'no-permission'), + flags: MessageFlags.Ephemeral + }); + } + + const targetId = interaction.customId.split('_')[2]; + const targetUser = await client.users.fetch(targetId).catch(() => null); + if (!targetUser) { + return interaction.reply({ + content: localize('ping-protection', 'no-data-found'), + flags: MessageFlags.Ephemeral + }); + } + + const selection = interaction.values[0]; + + if (selection === 'back') { + const payload = await generateUserPanel(client, targetUser); + return interaction.update(payload); + } + + const cooldown = await getDeletionCooldown(client, targetId); + if (cooldown) { + return interaction.reply({ + content: localize('ping-protection', 'err-del-cooldown', { + time: localize('ping-protection', getDeletionTypeLocaleKey(cooldown.lastDeletionType)), + until: `` + }), + flags: MessageFlags.Ephemeral + }); + } + + if (selection === 'del_all' && !isAdmin) { + return interaction.reply({ + content: localize('ping-protection', 'del-all-admin-only'), + flags: MessageFlags.Ephemeral + }); + } + + const modal = new ModalBuilder() + .setCustomId(`ping-protection_del-confirm_${targetId}_${selection}`) + .setTitle(localize('ping-protection', 'modal-title')); + + modal.addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('confirm') + .setLabel(localize('ping-protection', 'modal-label')) + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder(localize('ping-protection', 'modal-phrase')) + .setRequired(true) + ) + ); + + return interaction.showModal(modal); + } + + if (interaction.isModalSubmit() && interaction.customId.startsWith('ping-protection_del-confirm_')) { + if (!isAdmin) { + return interaction.reply({ + content: localize('ping-protection', 'no-permission'), + flags: MessageFlags.Ephemeral + }); + } + + const parts = interaction.customId.split('_'); + const targetId = parts[2]; + const selection = parts.slice(3).join('_'); + + const confirmPhrase = localize('ping-protection', 'modal-phrase'); + if (interaction.fields.getTextInputValue('confirm').trim() !== confirmPhrase) { + return interaction.reply({ + content: localize('ping-protection', 'modal-fail'), + flags: MessageFlags.Ephemeral + }); + } + + const cooldown = await getDeletionCooldown(client, targetId); + if (cooldown) { + return interaction.reply({ + content: localize('ping-protection', 'err-del-cooldown', { + time: localize('ping-protection', getDeletionTypeLocaleKey(cooldown.lastDeletionType)), + until: `` + }), + flags: MessageFlags.Ephemeral + }); + } + + if (selection === 'del_all') { + if (!isAdmin) { + return interaction.reply({ + content: localize('ping-protection', 'del-all-admin-only'), + flags: MessageFlags.Ephemeral + }); + } + + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'del-all-title')) + .setDescription(localize('ping-protection', 'del-all-desc')) + .setColor('DarkRed') + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_del-all-confirm_${targetId}`) + .setLabel(localize('ping-protection', 'btn-conf-del')) + .setStyle(ButtonStyle.Danger), + new ButtonBuilder() + .setCustomId(`ping-protection_del-all-cancel_${targetId}`) + .setLabel(localize('ping-protection', 'btn-cancel')) + .setStyle(ButtonStyle.Secondary) + ); + + await interaction.reply({ + embeds: [embed.toJSON()], + components: [row.toJSON()], + flags: MessageFlags.Ephemeral + }); + + const reply = await interaction.fetchReply(); + const collector = reply.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 30000, + max: 1, + filter: (btnInt) => btnInt.user.id === interaction.user.id + }); + + collector.on('collect', async (btnInt) => { + if (!btnInt.member?.permissions?.has('Administrator')) { + return btnInt.reply({ + content: localize('ping-protection', 'del-all-admin-only'), + flags: MessageFlags.Ephemeral + }); + } + + const liveCooldown = await getDeletionCooldown(client, targetId); + if (liveCooldown) { + return btnInt.reply({ + content: localize('ping-protection', 'err-del-cooldown', { + time: localize('ping-protection', getDeletionTypeLocaleKey(liveCooldown.lastDeletionType)), + until: `` + }), + flags: MessageFlags.Ephemeral + }); + } + + if (btnInt.customId.includes('cancel')) { + await btnInt.update({ + content: localize('ping-protection', 'succ-del-canc'), + embeds: [], + components: [] + }); + return; + } + + if (btnInt.customId.includes('confirm')) { + await executeDataDeletion(client, targetId, 'del_all'); + const blockedUntil = await setDeletionCooldown(client, targetId, 'del_all', btnInt.user.id); + + client.logger.info(localize('ping-protection', 'log-del-all', { + target: targetId, + admin: btnInt.user.id + })); + + const targetUser = await client.users.fetch(targetId).catch(() => null); + if (targetUser && interaction.message) { + const payload = await generateUserPanel(client, targetUser); + await interaction.message.edit(payload).catch(() => {}); + } + + await btnInt.update({ + content: localize('ping-protection', 'succ-del-all', { + until: `` + }), + embeds: [], + components: [] + }); + } + }); + + collector.on('end', async (_collected, reason) => { + if (reason === 'time') { + await interaction.editReply({ + content: localize('ping-protection', 'err-del-time'), + embeds: [], + components: [] + }).catch(() => {}); + } + }); + + return; + } + + await executeDataDeletion(client, targetId, selection); + const blockedUntil = await setDeletionCooldown(client, targetId, selection, interaction.user.id); + + client.logger.info(localize('ping-protection', 'log-del-type', { + type: selection, + target: targetId, + admin: interaction.user.id + })); + + const targetUser = await client.users.fetch(targetId).catch(() => null); + if (targetUser && interaction.message) { + const payload = await generateUserPanel(client, targetUser); + await interaction.message.edit(payload).catch(() => {}); + } + + return interaction.reply({ + content: localize('ping-protection', 'succ-del-tgt', { + type: localize('ping-protection', getDeletionTypeLocaleKey(selection)), + until: `` + }), + flags: MessageFlags.Ephemeral + }); + } + + // User panel dropdown and pages handler 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 targetPage = parseInt(parts[3], 10); const replyOptions = await generateHistoryResponse(client, userId, targetPage); await interaction.update(replyOptions); @@ -22,73 +298,44 @@ module.exports.run = async function (client, interaction) { if (interaction.customId.startsWith('ping-protection_mod-page_')) { const parts = interaction.customId.split('_'); const userId = parts[2]; - const targetPage = parseInt(parts[3]); - + const targetPage = parseInt(parts[3], 10); const replyOptions = await generateActionsResponse(client, userId, targetPage); await interaction.update(replyOptions); return; } - // Panel buttons - const [prefix, action, userId] = interaction.customId.split('_'); - - 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'), - flags: MessageFlags.Ephemeral }); - } + if (interaction.customId.startsWith('ping-protection_panel-hist_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3], 10); - if (action === 'history') { - const replyOptions = await generateHistoryResponse(client, userId, 1); - await interaction.reply({ - ...replyOptions, - flags: MessageFlags.Ephemeral - }); - } + const targetUser = await client.users.fetch(userId).catch(() => null); + if (!targetUser) { + return interaction.reply({ + content: localize('ping-protection', 'no-data-found'), + flags: MessageFlags.Ephemeral + }); + } - else if (action === 'actions') { - const replyOptions = await generateActionsResponse(client, userId, 1); - await interaction.reply({ - ...replyOptions, - flags: MessageFlags.Ephemeral - }); + const payload = await generatePanelHistory(client, targetUser, targetPage); + return interaction.update(payload); } - else if (action === 'delete') { - const modal = new ModalBuilder() - .setCustomId(`ping-protection_confirm-delete_${userId}`) - .setTitle(localize('ping-protection', 'modal-title')); - const input = new TextInputBuilder() - .setCustomId('confirmation_text') - .setLabel(localize('ping-protection', 'modal-label')) - .setStyle(TextInputStyle.Paragraph) - .setPlaceholder(localize('ping-protection', 'modal-phrase')) - .setRequired(true); - - const row = new ActionRowBuilder().addComponents(input); - modal.addComponents(row); + if (interaction.customId.startsWith('ping-protection_panel-actions_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3], 10); - await interaction.showModal(modal); - } - } + const targetUser = await client.users.fetch(userId).catch(() => null); + if (!targetUser) { + return interaction.reply({ + content: localize('ping-protection', 'no-data-found'), + flags: MessageFlags.Ephemeral + }); + } - 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 }); - - if (userInput === requiredPhrase) { - await deleteAllUserData(client, userId); - await interaction.reply({ - content: `✅ ${localize('ping-protection', 'modal-success-data-deletion', {u: userId})}`, - flags: MessageFlags.Ephemeral }); - } else { - await interaction.reply({ - content: `❌ ${localize('ping-protection', 'modal-failed')}`, - flags: MessageFlags.Ephemeral }); + const payload = await generatePanelActions(client, targetUser, targetPage); + return interaction.update(payload); } } }; \ No newline at end of file diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index e551fb0..c9c4f8c 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -1,6 +1,7 @@ const { processPing, - sendPingWarning + sendPingWarning, + isWhitelistedChannel } = require('../ping-protection'); const { localize } = require('../../../src/functions/localize'); const { randomElementFromArray } = require('../../../src/functions/helpers'); @@ -19,7 +20,7 @@ module.exports.run = async function (client, message) { if (message.author.bot) return; - if (config.ignoredChannels.includes(message.channel.id)) return; + if (isWhitelistedChannel(config, message.channel)) return; if (config.ignoredUsers.includes(message.author.id)) return; if (message.member.roles.cache.some(role => config.ignoredRoles.includes(role.id))) return; diff --git a/modules/ping-protection/models/DeletionCooldown.js b/modules/ping-protection/models/DeletionCooldown.js new file mode 100644 index 0000000..d119af9 --- /dev/null +++ b/modules/ping-protection/models/DeletionCooldown.js @@ -0,0 +1,34 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class PingProtectionDeletionCooldown extends Model { + static init(sequelize) { + return super.init({ + userId: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false + }, + blockedUntil: { + type: DataTypes.DATE, + allowNull: false + }, + lastDeletionType: { + type: DataTypes.STRING, + allowNull: false + }, + lastDeletedBy: { + type: DataTypes.STRING, + allowNull: true + } + }, { + tableName: 'ping_protection_deletion_cooldowns', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'DeletionCooldown', + module: 'ping-protection' +}; \ No newline at end of file diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 012143d..c5d9258 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -4,10 +4,9 @@ * @author itskevinnn */ const { Op } = require('sequelize'); -const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle } = require('discord.js'); +const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } = require('discord.js'); const { embedType, embedTypeV2, formatDate } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); - const recentPings = new Set(); // Data handling @@ -38,6 +37,7 @@ async function addPing(client, userId, messageUrl, targetId, isRole) { isRole: isRole }); } + // Gets ping count in timeframe async function getPingCountInWindow(client, userId, days) { const cutoffDate = new Date(); @@ -50,8 +50,9 @@ async function getPingCountInWindow(client, userId, days) { } }); } + // Fetches ping history -async function fetchPingHistory(client, userId, page = 1, limit = 8) { +async function fetchPingHistory(client, userId, page = 1, limit = 5) { const offset = (page - 1) * limit; const { count, rows } = await client.models['ping-protection']['PingHistory'].findAndCountAll({ where: { userId: userId }, @@ -61,9 +62,13 @@ async function fetchPingHistory(client, userId, page = 1, limit = 8) { }); return { total: count, history: rows }; } + // Fetches moderation history -async function fetchModHistory(client, userId, page = 1, limit = 8) { - if (!client.models['ping-protection'] || !client.models['ping-protection']['ModerationLog']) return { total: 0, history: [] }; +async function fetchModHistory(client, userId, page = 1, limit = 5) { + if (!client.models['ping-protection'] || !client.models['ping-protection']['ModerationLog']) { + return { total: 0, history: [] }; + } + try { const offset = (page - 1) * limit; const { count, rows } = await client.models['ping-protection']['ModerationLog'].findAndCountAll({ @@ -74,9 +79,14 @@ async function fetchModHistory(client, userId, page = 1, limit = 8) { }); return { total: count, history: rows }; } catch (e) { + client.logger.warn(localize('ping-protection', 'log-fetch-mod-history-failed', { + u: userId, + e: e.message + })); return { total: 0, history: [] }; } } + // Gets leaver status async function getLeaverStatus(client, userId) { return await client.models['ping-protection']['LeaverData'].findByPk(userId); @@ -95,6 +105,430 @@ function getSafeChannelId(configValue) { } return null; } + +function getWhitelistedChannelIds(channel) { + if (!channel) return []; + const ids = new Set(); + if (channel.id) ids.add(channel.id); + if (channel.parentId) ids.add(channel.parentId); + return [...ids]; +} + +function isWhitelistedChannel(config, channel) { + if (!channel || !config || !Array.isArray(config.ignoredChannels) || config.ignoredChannels.length === 0) { + return false; + } + const ignoredIds = new Set(config.ignoredChannels.map(id => id.toString())); + return getWhitelistedChannelIds(channel).some(id => ignoredIds.has(id.toString())); +} + +const EXEMPT_THRESHOLD = 'exempt'; +const PARTIAL_DELETION_COOLDOWN_HOURS = 24; +const FULL_DELETION_COOLDOWN_HOURS = 168; + +function getRequiredPingCountForMember(rule, member) { + const baseCount = + rule.pingsCount ?? + rule.pingsCountAdvanced ?? + rule.pingsCountBasic; + + if (typeof baseCount !== 'number' || !Number.isFinite(baseCount)) { + return null; + } + if (!rule.enableRolePingThresholds) { + return baseCount; + } + + const thresholds = rule.rolePingThresholds; + if (!thresholds || typeof thresholds !== 'object' || Array.isArray(thresholds)) { + return baseCount; + } + if (!member || !member.roles?.cache) { + return baseCount; + } + + const matchingRoles = member.roles.cache + .filter(role => Object.prototype.hasOwnProperty.call(thresholds, role.id)) + .sort((a, b) => b.position - a.position); + + if (matchingRoles.size === 0) { + return baseCount; + } + + for (const role of matchingRoles.values()) { + const parsedValue = Number(thresholds[role.id]); + if (!Number.isFinite(parsedValue)) continue; + + if (parsedValue === 0) { + return EXEMPT_THRESHOLD; + } + } + + const highestRole = matchingRoles.first(); + const highestRoleValue = Number(thresholds[highestRole.id]); + if (!Number.isFinite(highestRoleValue)) { + return baseCount; + } + + return highestRoleValue; +} + +function getDeletionCooldownHours(dataType) { + return dataType === 'del_all' + ? FULL_DELETION_COOLDOWN_HOURS + : PARTIAL_DELETION_COOLDOWN_HOURS; +} + +function getDeletionTypeLocaleKey(dataType) { + if (dataType === 'del_ping_history') return 'del-type-pings'; + if (dataType === 'del_moderation_history') return 'del-type-actions'; + if (dataType === 'del_all') return 'del-type-all'; + return 'del-type-unknown'; +} + +async function getDeletionCooldown(client, userId) { + const model = client.models['ping-protection']?.['DeletionCooldown']; + if (!model) return null; + + const cooldown = await model.findByPk(userId); + if (!cooldown) return null; + if (new Date(cooldown.blockedUntil) <= new Date()) { + await cooldown.destroy().catch(() => {}); + return null; + } + + return cooldown; +} + +async function setDeletionCooldown(client, userId, dataType, deletedBy = null) { + const model = client.models['ping-protection']?.['DeletionCooldown']; + if (!model) return null; + + const hours = getDeletionCooldownHours(dataType); + const blockedUntil = new Date(Date.now() + hours * 60 * 60 * 1000); + await model.upsert({ + userId, + blockedUntil, + lastDeletionType: dataType, + lastDeletedBy: deletedBy || null + }); + + return blockedUntil; +} + +async function executeDataDeletion(client, userId, dataType) { + const models = client.models['ping-protection']; + + if (['del_ping_history', 'del_all'].includes(dataType)) { + await models.PingHistory.destroy({ + where: { userId } + }); + } + + if (['del_moderation_history', 'del_all'].includes(dataType)) { + await models.ModerationLog.destroy({ + where: { victimID: userId } + }); + } + + if (dataType === 'del_all') { + await models.LeaverData.destroy({ + where: { userId } + }); + } +} + +function buildPanelMenu(userId, selected = 'overview') { + const menu = new StringSelectMenuBuilder() + .setCustomId(`ping-protection_panel-menu_${userId}`) + .setPlaceholder(localize('ping-protection', 'panel-ph')) + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel(localize('ping-protection', 'panel-opt-over')) + .setValue('overview') + .setEmoji('🏠') + .setDefault(selected === 'overview'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('ping-protection', 'panel-opt-hist')) + .setValue('history') + .setEmoji('📜') + .setDefault(selected === 'history'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('ping-protection', 'panel-opt-actions')) + .setValue('actions') + .setEmoji('⚠️') + .setDefault(selected === 'actions'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('ping-protection', 'panel-opt-delete')) + .setValue('deletion') + .setEmoji('🗑️') + .setDefault(selected === 'deletion') + ); + + return new ActionRowBuilder().addComponents(menu); +} + +function buildDeletionMenu(userId) { + const menu = new StringSelectMenuBuilder() + .setCustomId(`ping-protection_delete-menu_${userId}`) + .setPlaceholder(localize('ping-protection', 'panel-deletion-placeholder')) + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel(localize('ping-protection', 'panel-opt-back')) + .setValue('back') + .setEmoji('◀️'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('ping-protection', 'panel-opt-del-pings')) + .setValue('del_ping_history') + .setEmoji('📜'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('ping-protection', 'panel-opt-del-actions')) + .setValue('del_moderation_history') + .setEmoji('⚠️'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('ping-protection', 'panel-opt-del-all')) + .setValue('del_all') + .setEmoji('💥') + ); + + return new ActionRowBuilder().addComponents(menu); +} + +async function generateUserPanel(client, targetUser) { + const storageConfig = client.configurations['ping-protection']['storage']; + const retentionWeeks = storageConfig?.pingHistoryRetention || 12; + const timeframeDays = retentionWeeks * 7; + + const pingCount = await getPingCountInWindow(client, targetUser.id, timeframeDays); + const modData = await fetchModHistory(client, targetUser.id, 1, 1); + + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'panel-title', { + u: targetUser.tag || targetUser.username + })) + .setDescription(localize('ping-protection', 'panel-description', { + u: targetUser.toString(), + i: targetUser.id + })) + .setColor('Blue') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .addFields([{ + name: localize('ping-protection', 'field-quick-history', { w: retentionWeeks }), + value: localize('ping-protection', 'field-quick-desc', { + p: pingCount, + m: modData.total + }), + inline: false + }]) + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + return { + embeds: [embed.toJSON()], + components: [buildPanelMenu(targetUser.id, 'overview').toJSON()] + }; +} + +async function generatePanelHistory(client, targetUser, page = 1) { + const storageConfig = client.configurations['ping-protection']['storage']; + const limit = 5; + const isEnabled = !!storageConfig.enablePingHistory; + + let total = 0; + let history = []; + let totalPages = 1; + + if (isEnabled) { + const data = await fetchPingHistory(client, targetUser.id, page, limit); + total = data.total; + history = data.history; + totalPages = Math.ceil(total / limit) || 1; + } + + const leaverData = await getLeaverStatus(client, targetUser.id); + let description = ''; + + if (leaverData) { + const dateStr = formatDate(leaverData.leftAt); + const warningKey = history.length > 0 ? 'leaver-warning-long' : 'leaver-warning-short'; + description += `⚠️ ${localize('ping-protection', warningKey, { d: dateStr })}\n\n`; + } + + if (!isEnabled) { + 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 = 'Detected'; + if (entry.targetId) { + targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; + } + + const hasValidLink = entry.messageUrl && entry.messageUrl !== 'Blocked by AutoMod'; + const linkText = hasValidLink + ? `[${localize('ping-protection', 'label-jump')}](${entry.messageUrl})` + : localize('ping-protection', 'no-message-link'); + + return localize('ping-protection', 'list-entry-text', { + index: (page - 1) * limit + index + 1, + target: targetString, + time: timeString, + link: linkText + }); + }); + + description += lines.join('\n\n'); + } + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_panel-hist_${targetUser.id}_${page - 1}`) + .setLabel(localize('helpers', 'back')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page <= 1), + new ButtonBuilder() + .setCustomId('ping_protection_panel_hist_count') + .setLabel(`${page}/${totalPages}`) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`ping-protection_panel-hist_${targetUser.id}_${page + 1}`) + .setLabel(localize('helpers', 'next')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page >= totalPages || !isEnabled) + ); + + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'embed-history-title', { + u: targetUser.username + })) + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setDescription(description) + .setColor('Orange') + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + return { + embeds: [embed.toJSON()], + components: [ + buildPanelMenu(targetUser.id, 'history').toJSON(), + row.toJSON() + ] + }; +} + +async function generatePanelActions(client, targetUser, page = 1) { + const moderationConfig = client.configurations['ping-protection']['moderation']; + const limit = 5; + const isEnabled = moderationConfig && Array.isArray(moderationConfig) && moderationConfig.length > 0; + + const data = await fetchModHistory(client, targetUser.id, page, limit); + const total = data.total; + const history = data.history; + const totalPages = Math.ceil(total / limit) || 1; + + 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 `${(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')}*`; + } + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_panel-actions_${targetUser.id}_${page - 1}`) + .setLabel(localize('helpers', 'back')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page <= 1), + new ButtonBuilder() + .setCustomId('ping_protection_panel_actions_count') + .setLabel(`${page}/${totalPages}`) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`ping-protection_panel-actions_${targetUser.id}_${page + 1}`) + .setLabel(localize('helpers', 'next')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page >= totalPages || (!isEnabled && history.length === 0)) + ); + + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'embed-actions-title', { + u: targetUser.username + })) + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setDescription(description) + .setColor(isEnabled ? 'Red' : 'Grey') + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + return { + embeds: [embed.toJSON()], + components: [ + buildPanelMenu(targetUser.id, 'actions').toJSON(), + row.toJSON() + ] + }; +} + +async function generatePanelDeletion(client, targetUser) { + const cooldown = await getDeletionCooldown(client, targetUser.id); + + let description = localize('ping-protection', 'panel-deletion-desc', { + u: targetUser.toString(), + i: targetUser.id + }); + + if (cooldown) { + description += `\n\n⚠️ ${localize('ping-protection', 'panel-deletion-cooldown-active', { + time: formatDate(new Date(cooldown.blockedUntil)), + type: localize('ping-protection', getDeletionTypeLocaleKey(cooldown.lastDeletionType)) + })}`; + } + + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'panel-deletion-title', { + u: targetUser.tag || targetUser.username + })) + .setDescription(description) + .setColor('DarkRed') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + + return { + embeds: [embed.toJSON()], + components: [buildDeletionMenu(targetUser.id).toJSON()] + }; +} + // Sends ping warning message async function sendPingWarning(client, message, target, moduleConfig) { const warningMsg = moduleConfig.pingWarningMessage; @@ -109,12 +543,30 @@ async function sendPingWarning(client, message, target, moduleConfig) { }; try { - let messageOptions = await embedTypeV2(warnMsg, placeholders); - return message.reply(messageOptions).catch(async () => { - return message.channel.send(messageOptions).catch(() => {}); - }); + const messageOptions = await embedTypeV2(warnMsg, placeholders); + + try { + return await message.reply(messageOptions); + } catch (replyError) { + client.logger.warn(localize('ping-protection', 'log-warning-reply-failed', { + e: replyError.message + })); + + try { + return await message.channel.send(messageOptions); + } catch (sendError) { + client.logger.warn(localize('ping-protection', 'log-warning-send-failed', { + c: message.channel.id, + e: sendError.message + })); + return null; + } + } } catch (error) { - client.logger.warn(`[Ping Protection] ${error.message}`); + client.logger.warn(localize('ping-protection', 'log-warning-build-failed', { + e: error.message + })); + return null; } } @@ -124,13 +576,23 @@ async function syncNativeAutoMod(client) { try { const guild = await client.guilds.fetch(client.guildID); + await guild.channels.fetch().catch((error) => { + client.logger.warn(localize('ping-protection', 'log-automod-channel-fetch-failed', { + e: error.message + })); + }); + const rules = await guild.autoModerationRules.fetch(); const existingRule = rules.find(r => r.name === 'Ping Protection System'); // Logic to disable/delete the rule if (!config || !config.enableAutomod) { if (existingRule) { - await existingRule.delete().catch(() => {}); + await existingRule.delete().catch((error) => { + client.logger.warn(localize('ping-protection', 'log-automod-rule-delete-failed', { + e: error.message + })); + }); } return; } @@ -144,7 +606,7 @@ async function syncNativeAutoMod(client) { const protectedIdsSet = new Set(config.protectedUsers || []); if (config.protectAllUsersWithProtectedRole && config.protectedRoles && config.protectedRoles.length > 0) { - guild.members.cache.forEach(member => { + guild.members.cache.forEach(member => { if (member.roles.cache.some(r => config.protectedRoles.includes(r.id))) { protectedIdsSet.add(member.id); } @@ -184,6 +646,11 @@ async function syncNativeAutoMod(client) { }); } + const exactIgnoredChannels = (config.ignoredChannels || []).filter(channelId => { + const channel = guild.channels.cache.get(channelId); + return channel && channel.type !== 4; + }); + const ruleData = { name: 'Ping Protection System', eventType: 1, @@ -191,10 +658,10 @@ async function syncNativeAutoMod(client) { triggerMetadata: { keywordFilter: keywords }, - actions: actions, + actions, enabled: true, exemptRoles: config.ignoredRoles || [], - exemptChannels: config.ignoredChannels || [] + exemptChannels: exactIgnoredChannels }; if (existingRule) { @@ -203,14 +670,16 @@ async function syncNativeAutoMod(client) { await guild.autoModerationRules.create(ruleData); } } catch (error) { - client.logger.error(`[ping-protection] AutoMod Sync/Cleanup Failed: ${error.message}`); + client.logger.error(localize('ping-protection', 'log-automod-sync-failed', { + e: error.message + })); } } // Makes the history embed async function generateHistoryResponse(client, userId, page = 1) { const storageConfig = client.configurations['ping-protection']['storage']; - const limit = 8; + const limit = 5; const isEnabled = !!storageConfig.enablePingHistory; let total = 0, history = [], totalPages = 1; @@ -308,7 +777,7 @@ async function generateHistoryResponse(client, userId, page = 1) { // Makes the moderation actions history embed async function generateActionsResponse(client, userId, page = 1) { const moderationConfig = client.configurations['ping-protection']['moderation']; - const limit = 8; + const limit = 5; const isEnabled = moderationConfig && Array.isArray(moderationConfig) && moderationConfig.length > 0; let total = 0, history = [], totalPages = 1; @@ -380,17 +849,9 @@ async function generateActionsResponse(client, userId, page = 1) { // 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(localize('ping-protection', 'log-data-deletion', { - u: userId + await executeDataDeletion(client, userId, 'del_all'); + client.logger.info(localize('ping-protection', 'log-data-deletion', { + u: userId })); } @@ -417,7 +878,7 @@ async function enforceRetention(client) { const retentionWeeks = storageConfig.pingHistoryRetention || 12; historyCutoff.setDate(historyCutoff.getDate() - (retentionWeeks * 7)); - if (storageConfig.DeleteAllPingHistoryAfterTimeframe) { + if (storageConfig.deleteAllPingHistoryAfterTimeframe) { const usersWithExpiredData = await client.models['ping-protection']['PingHistory'].findAll({ where: { createdAt: { [Op.lt]: historyCutoff } @@ -496,7 +957,7 @@ async function executeAction(client, member, rule, reason, storageConfig, origin // Sends error message if action fails const sendErrorLog = async (error) => { - if (!originChannel) return; + if (!originChannel) return; const errorEmbed = new EmbedBuilder() .setTitle(localize('ping-protection', 'punish-log-failed-title', { @@ -510,14 +971,23 @@ async function executeAction(client, member, rule, reason, storageConfig, origin e: error.message })}` ) - .setColor("#ed4245") + .addFields({ + name: localize('ping-protection', 'punish-log-docs-title'), + value: localize('ping-protection', 'punish-log-docs-desc'), + inline: false + }) + .setColor('#ed4245') .setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); - if (!client.strings.disableFooterTimestamp) errorEmbed.setTimestamp(); - await originChannel.send({ embeds: [errorEmbed.toJSON()] }).catch(() => {}); + if (!client.strings.disableFooterTimestamp) errorEmbed.setTimestamp(); + await originChannel.send({ embeds: [errorEmbed.toJSON()] }).catch((sendError) => { + client.logger.warn(localize('ping-protection', 'log-punish-log-send-failed', { + e: sendError.message + })); + }); }; if (!member) { @@ -541,9 +1011,17 @@ async function executeAction(client, member, rule, reason, storageConfig, origin const logDb = async (type, duration = null) => { try { await client.models['ping-protection']['ModerationLog'].create({ - victimID: member.id, type, actionDuration: duration, reason + victimID: member.id, + type, + actionDuration: duration, + reason }); - } catch (dbError) {} + } catch (dbError) { + client.logger.error(localize('ping-protection', 'log-modlog-create-failed', { + u: member.id, + e: dbError.message + })); + } }; if (actionType === 'MUTE') { @@ -590,7 +1068,12 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC if (storageConfig?.enablePingHistory) { try { await addPing(client, userId, messageUrl, targetId, isRole); - } catch (e) {} + } catch (e) { + client.logger.error(localize('ping-protection', 'log-ping-history-create-failed', { + u: userId, + e: e.message + })); + } } if (!moderationRules || !Array.isArray(moderationRules) || moderationRules.length === 0) return; @@ -604,12 +1087,12 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC : (retentionWeeks * 7); const pingCount = await getPingCountInWindow(client, userId, timeframeDays); - const requiredCount = - rule.pingsCount ?? - rule.pingsCountAdvanced ?? - rule.pingsCountBasic; - - // Skip this rule if no valid threshold is configured + const requiredCount = getRequiredPingCountForMember(rule, memberToPunish); + + if (requiredCount === EXEMPT_THRESHOLD) { + continue; + } + if (typeof requiredCount !== 'number' || !Number.isFinite(requiredCount)) { continue; } @@ -624,7 +1107,12 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC } }); if (recentLog) break; - } catch (e) {} + } catch (e) { + client.logger.warn(localize('ping-protection', 'log-recent-mod-check-failed', { + u: userId, + e: e.message + })); + } const generatedReason = rule.useCustomTimeframe ? localize('ping-protection', 'reason-advanced', { @@ -654,6 +1142,10 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC module.exports = { addPing, getPingCountInWindow, + getSafeChannelId, + isWhitelistedChannel, + getRequiredPingCountForMember, + EXEMPT_THRESHOLD, sendPingWarning, syncNativeAutoMod, processPing, @@ -661,11 +1153,18 @@ module.exports = { fetchModHistory, executeAction, deleteAllUserData, + executeDataDeletion, + getDeletionCooldown, + setDeletionCooldown, + getDeletionTypeLocaleKey, getLeaverStatus, markUserAsLeft, markUserAsRejoined, enforceRetention, generateHistoryResponse, generateActionsResponse, - getSafeChannelId + generateUserPanel, + generatePanelHistory, + generatePanelActions, + generatePanelDeletion }; \ No newline at end of file