diff --git a/locales/en.json b/locales/en.json index b71ae7f..ececcf8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1001,10 +1001,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", @@ -1017,9 +1014,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 ^^", @@ -1032,7 +1028,49 @@ "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." }, "betterstatus": { "command-description": "Change the bot's status", diff --git a/modules/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js index 4d61d83..0d47c21 100644 --- a/modules/ping-protection/commands/ping-protection.js +++ b/modules/ping-protection/commands/ping-protection.js @@ -1,18 +1,11 @@ -const { - fetchModHistory, - getPingCountInWindow, - generateHistoryResponse, - generateActionsResponse +const { + generateHistoryResponse, + generateActionsResponse, + generateUserPanel } = require('../ping-protection'); -const {localize} = require('../../../src/functions/localize'); -const {truncate, safeSetFooter} = require('../../../src/functions/helpers'); -const { - ActionRowBuilder, - ButtonBuilder, - EmbedBuilder, - ButtonStyle, - MessageFlags -} = require('discord.js'); +const { localize } = require('../../../src/functions/localize'); +const { truncate } = require('../../../src/functions/helpers'); +const { EmbedBuilder, MessageFlags } = require('discord.js'); module.exports.run = async function (interaction) { const group = interaction.options.getSubcommandGroup(false); @@ -26,76 +19,30 @@ module.exports.run = async function (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, - flags: MessageFlags.Ephemeral - }); - }, - 'actions-history': async function (interaction) { - const user = interaction.options.getUser('user'); - const payload = await generateActionsResponse(interaction.client, user.id, 1); - await interaction.reply({ - ...payload, - flags: MessageFlags.Ephemeral - }); - }, - '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 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 - }]); - - safeSetFooter(embed, interaction.client); - if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); - - await interaction.reply({ - embeds: [embed.toJSON()], - components: [row.toJSON()], - flags: MessageFlags.Ephemeral - }); - } + 'user': { + 'history': async function (interaction) { + const user = interaction.options.getUser('user'); + const payload = await generateHistoryResponse(interaction.client, user.id, 1); + await interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); + }, + 'actions-history': async function (interaction) { + const user = interaction.options.getUser('user'); + const payload = await generateActionsResponse(interaction.client, user.id, 1); + await interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); + }, + 'panel': async function (interaction) { + const user = interaction.options.getUser('user'); + const payload = await generateUserPanel(interaction.client, user); + + await interaction.reply({ + ...payload, + flags: MessageFlags.Ephemeral + }); + } + }, + 'list': { + 'protected': async function (interaction) { + await listHandler(interaction, 'protected'); }, 'list': { 'protected': async function (interaction) { diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index 53a7ea0..fb5db8b 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -80,7 +80,12 @@ "description": "Pings in these channels are ignored.", "type": "array", "content": "channelID", - "default": [] + "default": [], + "channelTypes": [ + "GUILD_TEXT", + "GUILD_NEWS", + "GUILD_CATEGORY" + ] }, { "name": "ignoredUsers", @@ -115,16 +120,24 @@ { "name": "enableAutomod", "category": "automod", - "humanName": "Enable automod", - "description": "If enabled, the bot will utilise Discord's native AutoMod to block the message with a ping of a protected user/role.", + "humanName": { + "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. 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": true }, { "name": "autoModLogChannel", "category": "automod", - "humanName": "AutoMod Log Channel", - "description": "Channel where AutoMod alerts are sent.", + "humanName": { + "en": "AutoMod Log Channel" + }, + "description": { + "en": "Channel where AutoMod alerts are sent. It is recommended to keep these in a private channel." + }, "type": "channelID", "default": "", "channelTypes": [ diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index 9bf55ec..f2bca0b 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -15,6 +15,33 @@ "type": "integer", "default": 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": 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": {}, + "dependsOn": "enableRolePingThresholds" + }, { "name": "useCustomTimeframe", "humanName": "Use a custom timeframe", @@ -60,6 +87,7 @@ "humanName": "Action log message", "description": "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 76d03d2..1ab9e9a 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 f6288ae..0b19b55 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -1,104 +1,341 @@ 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, - MessageFlags + ButtonBuilder, + ButtonStyle, + ComponentType, + EmbedBuilder } = 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; + const isAdmin = interaction.member?.permissions?.has('Administrator') - if (interaction.isButton() && interaction.customId.startsWith('ping-protection_')) { + if (interaction.isStringSelectMenu() && interaction.customId.startsWith('ping-protection_panel-menu_')) { + if (!isAdmin) { + return interaction.reply({ + content: localize('ping-protection', 'no-permission'), + flags: MessageFlags.Ephemeral + }); + } - // 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 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 replyOptions = await generateHistoryResponse(client, userId, targetPage); - await interaction.update(replyOptions); - return; + 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 + }); } - if (interaction.customId.startsWith('ping-protection_mod-page_')) { - const parts = interaction.customId.split('_'); - const userId = parts[2]; - const targetPage = parseInt(parts[3]); + 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 replyOptions = await generateActionsResponse(client, userId, targetPage); - await interaction.update(replyOptions); - return; + 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 + }); } - // Panel buttons - const [prefix, action, userId] = interaction.customId.split('_'); + if (selection === 'del_all' && !isAdmin) { + return interaction.reply({ + content: localize('ping-protection', 'del-all-admin-only'), + flags: MessageFlags.Ephemeral + }); + } - const isAdmin = interaction.member.permissions.has('Administrator') || - (client.config.admins || []).includes(interaction.user.id); + 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 (['history', 'actions', 'delete'].includes(action)) { - if (!isAdmin) return interaction.reply({ + if (interaction.isModalSubmit() && interaction.customId.startsWith('ping-protection_del-confirm_')) { + if (!isAdmin) { + return interaction.reply({ content: localize('ping-protection', 'no-permission'), flags: MessageFlags.Ephemeral }); } - if (action === 'history') { - const replyOptions = await generateHistoryResponse(client, userId, 1); - await interaction.reply({ - ...replyOptions, + 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-failed'), flags: MessageFlags.Ephemeral }); - } else if (action === 'actions') { - const replyOptions = await generateActionsResponse(client, userId, 1); - await interaction.reply({ - ...replyOptions, + } + + 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 }); - } 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); + if (selection === 'del_all') { + if (!isAdmin) { + return interaction.reply({ + content: localize('ping-protection', 'del-all-admin-only'), + flags: MessageFlags.Ephemeral + }); + } - const row = new ActionRowBuilder().addComponents(input); - modal.addComponents(row); + 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 + }); - await interaction.showModal(modal); - } - } + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); - 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}); + 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) + ); - if (userInput === requiredPhrase) { - await deleteAllUserData(client, userId); await interaction.reply({ - content: `✅ ${localize('ping-protection', 'modal-success-data-deletion', {u: userId})}`, + embeds: [embed.toJSON()], + components: [row.toJSON()], flags: MessageFlags.Ephemeral }); - } else { - await interaction.reply({ - content: `❌ ${localize('ping-protection', 'modal-failed')}`, - 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_')) { + + if (interaction.customId.startsWith('ping-protection_hist-page_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3], 10); + + const replyOptions = await generateHistoryResponse(client, userId, targetPage); + await interaction.update(replyOptions); + return; + } + + if (interaction.customId.startsWith('ping-protection_mod-page_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3], 10); + const replyOptions = await generateActionsResponse(client, userId, targetPage); + await interaction.update(replyOptions); + return; + } + + if (interaction.customId.startsWith('ping-protection_panel-hist_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3], 10); + + const targetUser = await client.users.fetch(userId).catch(() => null); + if (!targetUser) { + return interaction.reply({ + content: localize('ping-protection', 'no-data-found'), + flags: MessageFlags.Ephemeral + }); + } + + const payload = await generatePanelHistory(client, targetUser, targetPage); + return interaction.update(payload); + } + + if (interaction.customId.startsWith('ping-protection_panel-actions_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3], 10); + + const targetUser = await client.users.fetch(userId).catch(() => null); + if (!targetUser) { + return interaction.reply({ + content: localize('ping-protection', 'no-data-found'), + 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 6de69cd..3cc91ba 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 b0adb37..224ee1e 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -3,21 +3,10 @@ * @module ping-protection * @author itskevinnn */ -const {Op} = require('sequelize'); -const { - ActionRowBuilder, - ButtonBuilder, - EmbedBuilder, - ButtonStyle -} = require('discord.js'); -const { - embedType, - embedTypeV2, - formatDate, - safeSetFooter -} = require('../../src/functions/helpers'); -const {localize} = require('../../src/functions/localize'); - +const { Op } = require('sequelize'); +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 @@ -48,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(); @@ -60,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, @@ -77,12 +68,13 @@ async function fetchPingHistory(client, userId, page = 1, limit = 8) { 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 { @@ -99,12 +91,14 @@ async function fetchModHistory(client, userId, page = 1, limit = 8) { history: rows }; } catch (e) { - return { - total: 0, - history: [] - }; + 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); @@ -123,6 +117,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; @@ -137,13 +555,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; } } @@ -153,13 +588,22 @@ 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; @@ -218,6 +662,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, @@ -225,10 +674,10 @@ async function syncNativeAutoMod(client) { triggerMetadata: { keywordFilter: keywords }, - actions: actions, + actions, enabled: true, exemptRoles: config.ignoredRoles || [], - exemptChannels: config.ignoredChannels || [] + exemptChannels: exactIgnoredChannels }; if (existingRule) { @@ -237,14 +686,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; @@ -340,7 +791,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; @@ -410,15 +861,7 @@ 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} - }); + await executeDataDeletion(client, userId, 'del_all'); client.logger.info(localize('ping-protection', 'log-data-deletion', { u: userId })); @@ -447,7 +890,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} @@ -526,26 +969,36 @@ async function executeAction(client, member, rule, reason, storageConfig, origin // 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('#ed4245'); - - safeSetFooter(errorEmbed, client); - if (!client.strings.disableFooterTimestamp) errorEmbed.setTimestamp(); + 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 + })}` + ) + .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 + }); - 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 + })); }); }; @@ -570,9 +1023,16 @@ 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) { + client.logger.error(localize('ping-protection', 'log-modlog-create-failed', { + u: member.id, + e: dbError.message + })); } }; @@ -620,6 +1080,10 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC try { await addPing(client, userId, messageUrl, targetId, isRole); } catch (e) { + client.logger.error(localize('ping-protection', 'log-ping-history-create-failed', { + u: userId, + e: e.message + })); } } @@ -634,12 +1098,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; + const requiredCount = getRequiredPingCountForMember(rule, memberToPunish); + + if (requiredCount === EXEMPT_THRESHOLD) { + continue; + } - // Skip this rule if no valid threshold is configured if (typeof requiredCount !== 'number' || !Number.isFinite(requiredCount)) { continue; } @@ -655,6 +1119,10 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC }); if (recentLog) break; } catch (e) { + client.logger.warn(localize('ping-protection', 'log-recent-mod-check-failed', { + u: userId, + e: e.message + })); } const generatedReason = rule.useCustomTimeframe @@ -690,6 +1158,10 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC module.exports = { addPing, getPingCountInWindow, + getSafeChannelId, + isWhitelistedChannel, + getRequiredPingCountForMember, + EXEMPT_THRESHOLD, sendPingWarning, syncNativeAutoMod, processPing, @@ -697,11 +1169,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