From c3bf7f08b170d293c13d035b9e79fc5927b1d23e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 1 Aug 2026 08:40:50 +0300 Subject: [PATCH 001/219] Update bot.js --- src/config/bot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/bot.js b/src/config/bot.js index 86dc861cc3..68cb6259ae 100644 --- a/src/config/bot.js +++ b/src/config/bot.js @@ -24,7 +24,7 @@ export const botConfig = { activities: [ { name: "Custom Status", // required by Discord API, not shown in the client - state: "stalking", // this is what people actually see + state: "Watching You", // this is what people actually see type: 4, // Custom }, ], @@ -648,4 +648,4 @@ export function getRandomColor() { return colors[Math.floor(Math.random() * colors.length)]; } -export default botConfig; \ No newline at end of file +export default botConfig; From e5d6a433c7da315d8870964bab66c9b37cea642c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 8 Aug 2026 07:03:00 +0300 Subject: [PATCH 002/219] Update nodes.json --- lavalink/nodes.json | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/lavalink/nodes.json b/lavalink/nodes.json index 78e107cec4..9092a8d94d 100644 --- a/lavalink/nodes.json +++ b/lavalink/nodes.json @@ -1,10 +1,10 @@ [ { - "host": "lavalinkv4.serenetia.com", + "host": "lavalink.lavalink.is", "port": 443, - "password": "https://seretia.link/discord", + "password": "password", "secure": true, - "name": "Serenetia" + "name": "Lavalink.is" }, { "host": "lavalink.jirayu.net", @@ -12,19 +12,5 @@ "password": "youshallnotpass", "secure": true, "name": "Jirayu" - }, - { - "host": "lava-v4.millohost.my.id", - "port": 443, - "password": "https://discord.gg/mjS5J2K3ep", - "secure": true, - "name": "MilloHost" - }, - { - "host": "lavalink-v4.triniumhost.com", - "port": 443, - "password": "free", - "secure": true, - "name": "TriniumHost" } ] From bd155bdad7236018c9a9f848e892179af2dcd331 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 8 Aug 2026 07:06:07 +0300 Subject: [PATCH 003/219] Update nodes.json --- lavalink/nodes.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lavalink/nodes.json b/lavalink/nodes.json index 9092a8d94d..489119a989 100644 --- a/lavalink/nodes.json +++ b/lavalink/nodes.json @@ -1,16 +1,16 @@ [ { - "host": "lavalink.lavalink.is", + "host": "lava-v4.ajiehospitality.com", "port": 443, - "password": "password", + "password": "https://discord.gg/ajiehospitality", "secure": true, - "name": "Lavalink.is" + "name": "AjieHospitality" }, { - "host": "lavalink.jirayu.net", + "host": "lavalink.lavalink.is", "port": 443, - "password": "youshallnotpass", + "password": "password", "secure": true, - "name": "Jirayu" + "name": "Lavalink.is" } ] From e074d12951686cc0e9b8e7999052e8666cd1efdd Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 8 Aug 2026 07:09:31 +0300 Subject: [PATCH 004/219] Update nodes.json --- lavalink/nodes.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lavalink/nodes.json b/lavalink/nodes.json index 489119a989..4b06b8dcbb 100644 --- a/lavalink/nodes.json +++ b/lavalink/nodes.json @@ -1,16 +1,16 @@ [ { - "host": "lava-v4.ajiehospitality.com", + "host": "lavalink.frix.xyz", "port": 443, - "password": "https://discord.gg/ajiehospitality", + "password": "youshallnotpass", "secure": true, - "name": "AjieHospitality" + "name": "Frix" }, { - "host": "lavalink.lavalink.is", + "host": "lava-v4.yupra.my.id", "port": 443, - "password": "password", + "password": "youshallnotpass", "secure": true, - "name": "Lavalink.is" + "name": "Yupra" } ] From d21f85389965be122153fba146ea31deb8ef20e5 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 8 Aug 2026 07:12:26 +0300 Subject: [PATCH 005/219] Update nodes.json --- lavalink/nodes.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lavalink/nodes.json b/lavalink/nodes.json index 4b06b8dcbb..952e917b36 100644 --- a/lavalink/nodes.json +++ b/lavalink/nodes.json @@ -1,16 +1,16 @@ [ { - "host": "lavalink.frix.xyz", + "host": "lavalink.devamop.in", "port": 443, - "password": "youshallnotpass", + "password": "DevamopLavalink", "secure": true, - "name": "Frix" + "name": "Devamop" }, { - "host": "lava-v4.yupra.my.id", - "port": 443, + "host": "lava.link", + "port": 80, "password": "youshallnotpass", - "secure": true, - "name": "Yupra" + "secure": false, + "name": "Lavalink-Official" } ] From be9f00b2a4bedf611a1de38b3f052be8d42fa56a Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 8 Aug 2026 07:15:21 +0300 Subject: [PATCH 006/219] Update nodes.json --- lavalink/nodes.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lavalink/nodes.json b/lavalink/nodes.json index 952e917b36..92def0551e 100644 --- a/lavalink/nodes.json +++ b/lavalink/nodes.json @@ -1,16 +1,16 @@ [ { - "host": "lavalink.devamop.in", + "host": "lavalink.jirayu.net", "port": 443, - "password": "DevamopLavalink", + "password": "youshallnotpass", "secure": true, - "name": "Devamop" + "name": "Jirayu" }, { - "host": "lava.link", - "port": 80, - "password": "youshallnotpass", - "secure": false, - "name": "Lavalink-Official" + "host": "lava-v4.ajiehospitality.com", + "port": 443, + "password": "https://discord.gg/ajiehospitality", + "secure": true, + "name": "Ajie" } ] From f5fea066d61064db8f7a1778f5a37660fa17064f Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 8 Aug 2026 07:18:58 +0300 Subject: [PATCH 007/219] Update nodes.json --- lavalink/nodes.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lavalink/nodes.json b/lavalink/nodes.json index 92def0551e..5b27147637 100644 --- a/lavalink/nodes.json +++ b/lavalink/nodes.json @@ -1,16 +1,16 @@ [ { - "host": "lavalink.jirayu.net", + "host": "lava-v4.yupra.my.id", "port": 443, "password": "youshallnotpass", "secure": true, - "name": "Jirayu" + "name": "Yupra" }, { - "host": "lava-v4.ajiehospitality.com", + "host": "lavalink.jirayu.net", "port": 443, - "password": "https://discord.gg/ajiehospitality", + "password": "youshallnotpass", "secure": true, - "name": "Ajie" + "name": "Jirayu" } ] From e3daa80b2992692efdb49be503ea9899924d7d33 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 8 Aug 2026 07:29:36 +0300 Subject: [PATCH 008/219] Update nodes.json --- lavalink/nodes.json | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lavalink/nodes.json b/lavalink/nodes.json index 5b27147637..9df29b5206 100644 --- a/lavalink/nodes.json +++ b/lavalink/nodes.json @@ -1,16 +1,9 @@ [ { - "host": "lava-v4.yupra.my.id", + "host": "lavalink.lavalink.is", "port": 443, - "password": "youshallnotpass", + "password": "password", "secure": true, - "name": "Yupra" - }, - { - "host": "lavalink.jirayu.net", - "port": 443, - "password": "youshallnotpass", - "secure": true, - "name": "Jirayu" + "name": "Public-Node" } ] From 9133d1f1e079bab21efb6abe03b808a53db20e60 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 03:32:57 +0300 Subject: [PATCH 009/219] Update lavalink.js --- src/config/music/lavalink.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/music/lavalink.js b/src/config/music/lavalink.js index e464557a9e..7889b6c6bd 100644 --- a/src/config/music/lavalink.js +++ b/src/config/music/lavalink.js @@ -78,7 +78,7 @@ export function getLavalinkNodes() { export const lavalinkConfig = { nodes: getLavalinkNodes(), - defaultSearchPlatform: process.env.LAVALINK_SEARCH_PLATFORM || 'ytmsearch', + defaultSearchPlatform: process.env.LAVALINK_SEARCH_PLATFORM || 'ytsearch', restVersion: process.env.LAVALINK_REST_VERSION || 'v4', }; From bae597cb865c02dea7d630a89cd75b9d718ea9cd Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:11:39 +0300 Subject: [PATCH 010/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index 4dc15a6fd1..da996973e9 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -98,7 +98,7 @@ export default { { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true }, { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false } ) - .setColor('primary'); + .setColor('0xFF0000'); const row = new ActionRowBuilder() .addComponents( @@ -190,4 +190,4 @@ export default { interaction.editReply({ components: [disabledRow] }).catch(console.error); }); }, -}; \ No newline at end of file +}; From 96a165b15cc60901c5e710e5e93d08193bd631d1 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:13:42 +0300 Subject: [PATCH 011/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index da996973e9..3fa87800a9 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -98,7 +98,7 @@ export default { { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true }, { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false } ) - .setColor('0xFF0000'); + .setColor('#FF0000'); const row = new ActionRowBuilder() .addComponents( From 57a3ef007b8e0afc6965f30748024a73110a34f1 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:17:52 +0300 Subject: [PATCH 012/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 34 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index 3fa87800a9..d7068212dc 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -49,6 +49,9 @@ export default { const onlineOnly = interaction.options.getBoolean('online') || false; const shouldMention = interaction.options.getBoolean('mention') || false; + // Fetch all server members before selecting + await interaction.guild.members.fetch(); + let members = interaction.guild.members.cache.filter(member => { if (member.user.bot && !includeBots) return false; @@ -92,13 +95,13 @@ export default { '🎲 Random User Selected', shouldMention ? `${selectedMember}` : `**${user.username}**` ) - .setThumbnail(user.displayAvatarURL({ dynamic: true, size: 256 })) - .addFields( - { name: 'Username', value: user.username, inline: true }, - { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true }, - { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false } - ) - .setColor('#FF0000'); + .setThumbnail(user.displayAvatarURL({ dynamic: true, size: 256 })) + .addFields( + { name: 'Username', value: user.username, inline: true }, + { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true }, + { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false } + ) + .setColor('#FF0000'); const row = new ActionRowBuilder() .addComponents( @@ -120,6 +123,9 @@ export default { collector.on('collect', async (i) => { try { + // Fetch all server members before selecting again + await interaction.guild.members.fetch(); + let newMembers = interaction.guild.members.cache.filter(member => { if (member.user.bot && !includeBots) return false; @@ -158,13 +164,13 @@ export default { '🎲 Random User Selected', shouldMention ? `${newSelectedMember}` : `**${newUser.username}**` ) - .setThumbnail(newUser.displayAvatarURL({ dynamic: true, size: 256 })) - .addFields( - { name: 'Username', value: newUser.username, inline: true }, - { name: 'Bot', value: newUser.bot ? 'Yes' : 'No', inline: true }, - { name: `Roles (${newRoles.length})`, value: newRoles.length > 0 ? newRoles.slice(0, 5).join('') + (newRoles.length > 5 ? `+${newRoles.length - 5} more` : '') : 'No roles', inline: false } - ) - .setColor(newSelectedMember.displayHexColor || '#3498db'); + .setThumbnail(newUser.displayAvatarURL({ dynamic: true, size: 256 })) + .addFields( + { name: 'Username', value: newUser.username, inline: true }, + { name: 'Bot', value: newUser.bot ? 'Yes' : 'No', inline: true }, + { name: `Roles (${newRoles.length})`, value: newRoles.length > 0 ? newRoles.slice(0, 5).join('') + (newRoles.length > 5 ? `+${newRoles.length - 5} more` : '') : 'No roles', inline: false } + ) + .setColor(newSelectedMember.displayHexColor || '#3498db'); await i.update({ content: shouldMention ? `${newSelectedMember}, you've been chosen!` : null, From 7a4d9bf33c03412e05fda29841cb491f97eaa522 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:19:54 +0300 Subject: [PATCH 013/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index d7068212dc..26a5e94c9d 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -49,8 +49,14 @@ export default { const onlineOnly = interaction.options.getBoolean('online') || false; const shouldMention = interaction.options.getBoolean('mention') || false; - // Fetch all server members before selecting - await interaction.guild.members.fetch(); + // Fetch members only once when the command starts + if (interaction.guild.members.cache.size <= 1) { + try { + await interaction.guild.members.fetch(); + } catch (error) { + logger.error('Failed to fetch guild members:', error); + } + } let members = interaction.guild.members.cache.filter(member => { if (member.user.bot && !includeBots) return false; @@ -123,9 +129,6 @@ export default { collector.on('collect', async (i) => { try { - // Fetch all server members before selecting again - await interaction.guild.members.fetch(); - let newMembers = interaction.guild.members.cache.filter(member => { if (member.user.bot && !includeBots) return false; From aaaf8a8ed57075d2cea855b31254dab41851ec05 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:20:57 +0300 Subject: [PATCH 014/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index 26a5e94c9d..d7068212dc 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -49,14 +49,8 @@ export default { const onlineOnly = interaction.options.getBoolean('online') || false; const shouldMention = interaction.options.getBoolean('mention') || false; - // Fetch members only once when the command starts - if (interaction.guild.members.cache.size <= 1) { - try { - await interaction.guild.members.fetch(); - } catch (error) { - logger.error('Failed to fetch guild members:', error); - } - } + // Fetch all server members before selecting + await interaction.guild.members.fetch(); let members = interaction.guild.members.cache.filter(member => { if (member.user.bot && !includeBots) return false; @@ -129,6 +123,9 @@ export default { collector.on('collect', async (i) => { try { + // Fetch all server members before selecting again + await interaction.guild.members.fetch(); + let newMembers = interaction.guild.members.cache.filter(member => { if (member.user.bot && !includeBots) return false; From 420d8eb92b1b552810556ddcdf2fce2e48acf84c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:25:12 +0300 Subject: [PATCH 015/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 173 +++++++++++++++++++++++-------- 1 file changed, 132 insertions(+), 41 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index d7068212dc..1ab61b3802 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -1,3 +1,4 @@ +```js import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; @@ -28,6 +29,7 @@ export default { async execute(interaction) { const deferSuccess = await InteractionHelper.safeDefer(interaction); + if (!deferSuccess) { logger.warn(`RandomUser interaction defer failed`, { userId: interaction.user.id, @@ -49,10 +51,21 @@ export default { const onlineOnly = interaction.options.getBoolean('online') || false; const shouldMention = interaction.options.getBoolean('mention') || false; - // Fetch all server members before selecting - await interaction.guild.members.fetch(); + // Get up to 1000 members without using Gateway Opcode 8 + let members; + + try { + members = await interaction.guild.members.list({ limit: 1000 }); + } catch (error) { + logger.error('Failed to list guild members:', error); + + return replyUserError(interaction, { + type: ErrorTypes.SYSTEM, + message: 'Unable to get the server members right now. Please try again later.', + }); + } - let members = interaction.guild.members.cache.filter(member => { + let memberArray = Array.from(members.values()).filter(member => { if (member.user.bot && !includeBots) return false; if (onlineOnly && member.presence?.status === 'offline') return false; @@ -62,17 +75,20 @@ export default { return true; }); - let memberArray = Array.from(members.values()); - - if (!includeBots) { - memberArray = memberArray.filter(member => !member.user.bot); - } - if (memberArray.length === 0) { let errorMessage = 'Could not find any users matching your filters:'; - if (role) errorMessage = `No users have the **${role.name}** role.`; - if (onlineOnly) errorMessage = 'No users are currently online.'; - if (role && onlineOnly) errorMessage = `No **${role.name}** members are online.`; + + if (role) { + errorMessage = `No users have the **${role.name}** role.`; + } + + if (onlineOnly) { + errorMessage = 'No users are currently online.'; + } + + if (role && onlineOnly) { + errorMessage = `No **${role.name}** members are online.`; + } return replyUserError(interaction, { type: ErrorTypes.USER_INPUT, @@ -84,7 +100,7 @@ export default { const selectedMember = memberArray[randomIndex]; const user = selectedMember.user; - const joinDate = selectedMember.joinedAt; + const roles = selectedMember.roles.cache .filter(role => role.id !== interaction.guild.id) .sort((a, b) => b.position - a.position) @@ -97,9 +113,23 @@ export default { ) .setThumbnail(user.displayAvatarURL({ dynamic: true, size: 256 })) .addFields( - { name: 'Username', value: user.username, inline: true }, - { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true }, - { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false } + { + name: 'Username', + value: user.username, + inline: true + }, + { + name: 'Bot', + value: user.bot ? 'Yes' : 'No', + inline: true + }, + { + name: `Roles (${roles.length})`, + value: roles.length > 0 + ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') + : 'No roles', + inline: false + } ) .setColor('#FF0000'); @@ -115,18 +145,41 @@ export default { content: shouldMention ? `${selectedMember}, you've been chosen!` : null, embeds: [embed], components: [row], - allowedMentions: { users: shouldMention ? [user.id] : [] } + allowedMentions: { + users: shouldMention ? [user.id] : [] + } }); - const filter = (i) => i.customId === `randomuser_${interaction.user.id}_again` && i.user.id === interaction.user.id; - const collector = response.createMessageComponentCollector({ filter, time: 300000 }); + const filter = (i) => + i.customId === `randomuser_${interaction.user.id}_again` && + i.user.id === interaction.user.id; + + const collector = response.createMessageComponentCollector({ + filter, + time: 300000 + }); collector.on('collect', async (i) => { try { - // Fetch all server members before selecting again - await interaction.guild.members.fetch(); + // Get members without using Gateway Opcode 8 + let newMembers; + + try { + newMembers = await interaction.guild.members.list({ + limit: 1000 + }); + } catch (error) { + logger.error('Failed to list guild members again:', error); + + await i.reply({ + content: 'Unable to get the server members right now. Please try again later.', + flags: ['Ephemeral'] + }); - let newMembers = interaction.guild.members.cache.filter(member => { + return; + } + + let newMemberArray = Array.from(newMembers.values()).filter(member => { if (member.user.bot && !includeBots) return false; if (onlineOnly && member.presence?.status === 'offline') return false; @@ -136,20 +189,26 @@ export default { return true; }); - let newMemberArray = Array.from(newMembers.values()); - - if (!includeBots) { - newMemberArray = newMemberArray.filter(member => !member.user.bot); - } - if (newMemberArray.length === 0) { await replyUserError(i, { type: ErrorTypes.USER_INPUT, message: 'No users found matching the criteria.', }); + return; } + // Avoid selecting the exact same user when possible + if (newMemberArray.length > 1) { + const filteredDifferentUsers = newMemberArray.filter( + member => member.id !== selectedMember.id + ); + + if (filteredDifferentUsers.length > 0) { + newMemberArray = filteredDifferentUsers; + } + } + const newRandomIndex = Math.floor(Math.random() * newMemberArray.length); const newSelectedMember = newMemberArray[newRandomIndex]; const newUser = newSelectedMember.user; @@ -162,29 +221,58 @@ export default { const newEmbed = successEmbed( '🎲 Random User Selected', - shouldMention ? `${newSelectedMember}` : `**${newUser.username}**` + shouldMention + ? `${newSelectedMember}` + : `**${newUser.username}**` ) - .setThumbnail(newUser.displayAvatarURL({ dynamic: true, size: 256 })) + .setThumbnail(newUser.displayAvatarURL({ + dynamic: true, + size: 256 + })) .addFields( - { name: 'Username', value: newUser.username, inline: true }, - { name: 'Bot', value: newUser.bot ? 'Yes' : 'No', inline: true }, - { name: `Roles (${newRoles.length})`, value: newRoles.length > 0 ? newRoles.slice(0, 5).join('') + (newRoles.length > 5 ? `+${newRoles.length - 5} more` : '') : 'No roles', inline: false } + { + name: 'Username', + value: newUser.username, + inline: true + }, + { + name: 'Bot', + value: newUser.bot ? 'Yes' : 'No', + inline: true + }, + { + name: `Roles (${newRoles.length})`, + value: newRoles.length > 0 + ? newRoles.slice(0, 5).join('') + + (newRoles.length > 5 + ? `+${newRoles.length - 5} more` + : '') + : 'No roles', + inline: false + } ) - .setColor(newSelectedMember.displayHexColor || '#3498db'); + .setColor('#FF0000'); await i.update({ - content: shouldMention ? `${newSelectedMember}, you've been chosen!` : null, + content: shouldMention + ? `${newSelectedMember}, you've been chosen!` + : null, embeds: [newEmbed], components: [row], - allowedMentions: { users: shouldMention ? [newUser.id] : [] } + allowedMentions: { + users: shouldMention ? [newUser.id] : [] + } }); } catch (error) { logger.error('Button interaction error:', error); - await i.reply({ - content: 'An error occurred while selecting another user.', - flags: ['Ephemeral'] - }); + + if (!i.replied && !i.deferred) { + await i.reply({ + content: 'An error occurred while selecting another user.', + flags: ['Ephemeral'] + }); + } } }); @@ -193,7 +281,10 @@ export default { ButtonBuilder.from(row.components[0]).setDisabled(true) ); - interaction.editReply({ components: [disabledRow] }).catch(console.error); + interaction.editReply({ + components: [disabledRow] + }).catch(console.error); }); }, }; +``` From 216504c498172d3b1d7c45f67f0e159efff7faf5 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:27:36 +0300 Subject: [PATCH 016/219] Update randomuser.js From 50ddc31ed32eb12c29e18ae901a10768579cff72 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:29:53 +0300 Subject: [PATCH 017/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index 1ab61b3802..100397a38e 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -1,4 +1,4 @@ -```js +js import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; @@ -287,4 +287,4 @@ export default { }); }, }; -``` + From fd0f69126d2c9ea9aa9e1efd046a6bf686f69d09 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:31:04 +0300 Subject: [PATCH 018/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 342 ++++++++++++++++++++----------- 1 file changed, 225 insertions(+), 117 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index 100397a38e..7aadca16dd 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -1,8 +1,23 @@ -js -import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; -import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; +import { + SlashCommandBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle +} from 'discord.js'; + +import { + createEmbed, + successEmbed, + infoEmbed, + warningEmbed +} from '../../utils/embeds.js'; + import { logger } from '../../utils/logger.js'; -import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; +import { + replyUserError, + ErrorTypes +} from '../../utils/errorHandler.js'; + import { getColor } from '../../config/bot.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -10,22 +25,34 @@ export default { data: new SlashCommandBuilder() .setName('randomuser') .setDescription('Select a random user from the server') + .addRoleOption(option => - option.setName('role') + option + .setName('role') .setDescription('Limit selection to users with this role') - .setRequired(false)) + .setRequired(false) + ) + .addBooleanOption(option => - option.setName('bots') + option + .setName('bots') .setDescription('Include bots in the selection (default: false)') - .setRequired(false)) + .setRequired(false) + ) + .addBooleanOption(option => - option.setName('online') + option + .setName('online') .setDescription('Only select from online users (default: false)') - .setRequired(false)) + .setRequired(false) + ) + .addBooleanOption(option => - option.setName('mention') + option + .setName('mention') .setDescription('Mention the selected user (default: false)') - .setRequired(false)), + .setRequired(false) + ), async execute(interaction) { const deferSuccess = await InteractionHelper.safeDefer(interaction); @@ -47,58 +74,79 @@ export default { } const role = interaction.options.getRole('role'); - const includeBots = interaction.options.getBoolean('bots') || false; - const onlineOnly = interaction.options.getBoolean('online') || false; - const shouldMention = interaction.options.getBoolean('mention') || false; - - // Get up to 1000 members without using Gateway Opcode 8 - let members; - - try { - members = await interaction.guild.members.list({ limit: 1000 }); - } catch (error) { - logger.error('Failed to list guild members:', error); - - return replyUserError(interaction, { - type: ErrorTypes.SYSTEM, - message: 'Unable to get the server members right now. Please try again later.', - }); - } - - let memberArray = Array.from(members.values()).filter(member => { - if (member.user.bot && !includeBots) return false; + const includeBots = + interaction.options.getBoolean('bots') || false; + const onlineOnly = + interaction.options.getBoolean('online') || false; + const shouldMention = + interaction.options.getBoolean('mention') || false; + + /* + * Use the cached members. + * Do NOT call guild.members.fetch() here because that sends + * Gateway Opcode 8 and can trigger Discord rate limits. + */ + + let memberArray = Array.from( + interaction.guild.members.cache.values() + ).filter(member => { + if (member.user.bot && !includeBots) { + return false; + } - if (onlineOnly && member.presence?.status === 'offline') return false; + if ( + onlineOnly && + (!member.presence || + member.presence.status === 'offline') + ) { + return false; + } - if (role && !member.roles.cache.has(role.id)) return false; + if (role && !member.roles.cache.has(role.id)) { + return false; + } return true; }); + if (!includeBots) { + memberArray = memberArray.filter( + member => !member.user.bot + ); + } + if (memberArray.length === 0) { - let errorMessage = 'Could not find any users matching your filters:'; + let errorMessage = + 'Could not find any users matching your filters:'; if (role) { - errorMessage = `No users have the **${role.name}** role.`; + errorMessage = + `No users have the **${role.name}** role.`; } if (onlineOnly) { - errorMessage = 'No users are currently online.'; + errorMessage = + 'No users are currently online.'; } if (role && onlineOnly) { - errorMessage = `No **${role.name}** members are online.`; + errorMessage = + `No **${role.name}** members are online.`; } return replyUserError(interaction, { type: ErrorTypes.USER_INPUT, - message: errorMessage + '\n\nTry adjusting your filters.', + message: + errorMessage + + '\n\nTry adjusting your filters.', }); } - const randomIndex = Math.floor(Math.random() * memberArray.length); - const selectedMember = memberArray[randomIndex]; + const randomIndex = Math.floor( + Math.random() * memberArray.length + ); + const selectedMember = memberArray[randomIndex]; const user = selectedMember.user; const roles = selectedMember.roles.cache @@ -109,9 +157,16 @@ export default { const embed = successEmbed( '🎲 Random User Selected', - shouldMention ? `${selectedMember}` : `**${user.username}**` + shouldMention + ? `${selectedMember}` + : `**${user.username}**` ) - .setThumbnail(user.displayAvatarURL({ dynamic: true, size: 256 })) + .setThumbnail( + user.displayAvatarURL({ + dynamic: true, + size: 256 + }) + ) .addFields( { name: 'Username', @@ -125,97 +180,133 @@ export default { }, { name: `Roles (${roles.length})`, - value: roles.length > 0 - ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') - : 'No roles', + value: + roles.length > 0 + ? roles.slice(0, 5).join('') + + (roles.length > 5 + ? `+${roles.length - 5} more` + : '') + : 'No roles', inline: false } ) .setColor('#FF0000'); - const row = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(`randomuser_${interaction.user.id}_again`) - .setLabel('🎲 Pick Another User') - .setStyle(ButtonStyle.Primary) - ); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId( + `randomuser_${interaction.user.id}_again` + ) + .setLabel('🎲 Pick Another User') + .setStyle(ButtonStyle.Primary) + ); const response = await interaction.editReply({ - content: shouldMention ? `${selectedMember}, you've been chosen!` : null, + content: shouldMention + ? `${selectedMember}, you've been chosen!` + : null, + embeds: [embed], + components: [row], + allowedMentions: { users: shouldMention ? [user.id] : [] } }); - const filter = (i) => - i.customId === `randomuser_${interaction.user.id}_again` && + const filter = i => + i.customId === + `randomuser_${interaction.user.id}_again` && i.user.id === interaction.user.id; - const collector = response.createMessageComponentCollector({ - filter, - time: 300000 - }); + const collector = + response.createMessageComponentCollector({ + filter, + time: 300000 + }); - collector.on('collect', async (i) => { + collector.on('collect', async i => { try { - // Get members without using Gateway Opcode 8 - let newMembers; - - try { - newMembers = await interaction.guild.members.list({ - limit: 1000 - }); - } catch (error) { - logger.error('Failed to list guild members again:', error); - - await i.reply({ - content: 'Unable to get the server members right now. Please try again later.', - flags: ['Ephemeral'] - }); - - return; - } - - let newMemberArray = Array.from(newMembers.values()).filter(member => { - if (member.user.bot && !includeBots) return false; + /* + * Use the current cache again. + * No members.fetch() and no members.list(). + */ + + let newMemberArray = Array.from( + interaction.guild.members.cache.values() + ).filter(member => { + if (member.user.bot && !includeBots) { + return false; + } - if (onlineOnly && member.presence?.status === 'offline') return false; + if ( + onlineOnly && + (!member.presence || + member.presence.status === 'offline') + ) { + return false; + } - if (role && !member.roles.cache.has(role.id)) return false; + if ( + role && + !member.roles.cache.has(role.id) + ) { + return false; + } return true; }); + if (!includeBots) { + newMemberArray = newMemberArray.filter( + member => !member.user.bot + ); + } + if (newMemberArray.length === 0) { await replyUserError(i, { type: ErrorTypes.USER_INPUT, - message: 'No users found matching the criteria.', + message: + 'No users found matching the criteria.', }); return; } - // Avoid selecting the exact same user when possible - if (newMemberArray.length > 1) { - const filteredDifferentUsers = newMemberArray.filter( - member => member.id !== selectedMember.id - ); + /* + * Avoid selecting the same user when another + * eligible member exists. + */ - if (filteredDifferentUsers.length > 0) { - newMemberArray = filteredDifferentUsers; + if (newMemberArray.length > 1) { + const differentUsers = + newMemberArray.filter( + member => + member.id !== selectedMember.id + ); + + if (differentUsers.length > 0) { + newMemberArray = differentUsers; } } - const newRandomIndex = Math.floor(Math.random() * newMemberArray.length); - const newSelectedMember = newMemberArray[newRandomIndex]; + const newRandomIndex = Math.floor( + Math.random() * newMemberArray.length + ); + + const newSelectedMember = + newMemberArray[newRandomIndex]; + const newUser = newSelectedMember.user; const newRoles = newSelectedMember.roles.cache - .filter(r => r.id !== interaction.guild.id) - .sort((a, b) => b.position - a.position) + .filter( + r => r.id !== interaction.guild.id + ) + .sort( + (a, b) => b.position - a.position + ) .map(r => r.toString()) .slice(0, 10); @@ -225,10 +316,12 @@ export default { ? `${newSelectedMember}` : `**${newUser.username}**` ) - .setThumbnail(newUser.displayAvatarURL({ - dynamic: true, - size: 256 - })) + .setThumbnail( + newUser.displayAvatarURL({ + dynamic: true, + size: 256 + }) + ) .addFields( { name: 'Username', @@ -242,12 +335,15 @@ export default { }, { name: `Roles (${newRoles.length})`, - value: newRoles.length > 0 - ? newRoles.slice(0, 5).join('') + - (newRoles.length > 5 - ? `+${newRoles.length - 5} more` - : '') - : 'No roles', + value: + newRoles.length > 0 + ? newRoles + .slice(0, 5) + .join('') + + (newRoles.length > 5 + ? `+${newRoles.length - 5} more` + : '') + : 'No roles', inline: false } ) @@ -257,19 +353,27 @@ export default { content: shouldMention ? `${newSelectedMember}, you've been chosen!` : null, + embeds: [newEmbed], + components: [row], + allowedMentions: { - users: shouldMention ? [newUser.id] : [] + users: shouldMention + ? [newUser.id] + : [] } }); - } catch (error) { - logger.error('Button interaction error:', error); + logger.error( + 'Button interaction error:', + error + ); if (!i.replied && !i.deferred) { await i.reply({ - content: 'An error occurred while selecting another user.', + content: + 'An error occurred while selecting another user.', flags: ['Ephemeral'] }); } @@ -277,14 +381,18 @@ export default { }); collector.on('end', () => { - const disabledRow = ActionRowBuilder.from(row).setComponents( - ButtonBuilder.from(row.components[0]).setDisabled(true) - ); - - interaction.editReply({ - components: [disabledRow] - }).catch(console.error); + const disabledRow = + ActionRowBuilder.from(row).setComponents( + ButtonBuilder.from( + row.components[0] + ).setDisabled(true) + ); + + interaction + .editReply({ + components: [disabledRow] + }) + .catch(console.error); }); }, }; - From 2a0541b16aaa9abd82eb77878bc69c355b0ae9d4 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:32:41 +0300 Subject: [PATCH 019/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 371 +++++++------------------------ 1 file changed, 86 insertions(+), 285 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index 7aadca16dd..d7068212dc 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -1,23 +1,7 @@ -import { - SlashCommandBuilder, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle -} from 'discord.js'; - -import { - createEmbed, - successEmbed, - infoEmbed, - warningEmbed -} from '../../utils/embeds.js'; - +import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; +import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -import { - replyUserError, - ErrorTypes -} from '../../utils/errorHandler.js'; - +import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; import { getColor } from '../../config/bot.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -25,38 +9,25 @@ export default { data: new SlashCommandBuilder() .setName('randomuser') .setDescription('Select a random user from the server') - .addRoleOption(option => - option - .setName('role') + option.setName('role') .setDescription('Limit selection to users with this role') - .setRequired(false) - ) - + .setRequired(false)) .addBooleanOption(option => - option - .setName('bots') + option.setName('bots') .setDescription('Include bots in the selection (default: false)') - .setRequired(false) - ) - + .setRequired(false)) .addBooleanOption(option => - option - .setName('online') + option.setName('online') .setDescription('Only select from online users (default: false)') - .setRequired(false) - ) - + .setRequired(false)) .addBooleanOption(option => - option - .setName('mention') + option.setName('mention') .setDescription('Mention the selected user (default: false)') - .setRequired(false) - ), + .setRequired(false)), async execute(interaction) { const deferSuccess = await InteractionHelper.safeDefer(interaction); - if (!deferSuccess) { logger.warn(`RandomUser interaction defer failed`, { userId: interaction.user.id, @@ -74,81 +45,46 @@ export default { } const role = interaction.options.getRole('role'); - const includeBots = - interaction.options.getBoolean('bots') || false; - const onlineOnly = - interaction.options.getBoolean('online') || false; - const shouldMention = - interaction.options.getBoolean('mention') || false; - - /* - * Use the cached members. - * Do NOT call guild.members.fetch() here because that sends - * Gateway Opcode 8 and can trigger Discord rate limits. - */ - - let memberArray = Array.from( - interaction.guild.members.cache.values() - ).filter(member => { - if (member.user.bot && !includeBots) { - return false; - } + const includeBots = interaction.options.getBoolean('bots') || false; + const onlineOnly = interaction.options.getBoolean('online') || false; + const shouldMention = interaction.options.getBoolean('mention') || false; - if ( - onlineOnly && - (!member.presence || - member.presence.status === 'offline') - ) { - return false; - } + // Fetch all server members before selecting + await interaction.guild.members.fetch(); - if (role && !member.roles.cache.has(role.id)) { - return false; - } + let members = interaction.guild.members.cache.filter(member => { + if (member.user.bot && !includeBots) return false; + + if (onlineOnly && member.presence?.status === 'offline') return false; + + if (role && !member.roles.cache.has(role.id)) return false; return true; }); + let memberArray = Array.from(members.values()); + if (!includeBots) { - memberArray = memberArray.filter( - member => !member.user.bot - ); + memberArray = memberArray.filter(member => !member.user.bot); } if (memberArray.length === 0) { - let errorMessage = - 'Could not find any users matching your filters:'; - - if (role) { - errorMessage = - `No users have the **${role.name}** role.`; - } - - if (onlineOnly) { - errorMessage = - 'No users are currently online.'; - } - - if (role && onlineOnly) { - errorMessage = - `No **${role.name}** members are online.`; - } + let errorMessage = 'Could not find any users matching your filters:'; + if (role) errorMessage = `No users have the **${role.name}** role.`; + if (onlineOnly) errorMessage = 'No users are currently online.'; + if (role && onlineOnly) errorMessage = `No **${role.name}** members are online.`; return replyUserError(interaction, { type: ErrorTypes.USER_INPUT, - message: - errorMessage + - '\n\nTry adjusting your filters.', + message: errorMessage + '\n\nTry adjusting your filters.', }); } - const randomIndex = Math.floor( - Math.random() * memberArray.length - ); - + const randomIndex = Math.floor(Math.random() * memberArray.length); const selectedMember = memberArray[randomIndex]; - const user = selectedMember.user; + const user = selectedMember.user; + const joinDate = selectedMember.joinedAt; const roles = selectedMember.roles.cache .filter(role => role.id !== interaction.guild.id) .sort((a, b) => b.position - a.position) @@ -157,242 +93,107 @@ export default { const embed = successEmbed( '🎲 Random User Selected', - shouldMention - ? `${selectedMember}` - : `**${user.username}**` + shouldMention ? `${selectedMember}` : `**${user.username}**` ) - .setThumbnail( - user.displayAvatarURL({ - dynamic: true, - size: 256 - }) - ) + .setThumbnail(user.displayAvatarURL({ dynamic: true, size: 256 })) .addFields( - { - name: 'Username', - value: user.username, - inline: true - }, - { - name: 'Bot', - value: user.bot ? 'Yes' : 'No', - inline: true - }, - { - name: `Roles (${roles.length})`, - value: - roles.length > 0 - ? roles.slice(0, 5).join('') + - (roles.length > 5 - ? `+${roles.length - 5} more` - : '') - : 'No roles', - inline: false - } + { name: 'Username', value: user.username, inline: true }, + { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true }, + { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false } ) .setColor('#FF0000'); - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId( - `randomuser_${interaction.user.id}_again` - ) - .setLabel('🎲 Pick Another User') - .setStyle(ButtonStyle.Primary) - ); + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`randomuser_${interaction.user.id}_again`) + .setLabel('🎲 Pick Another User') + .setStyle(ButtonStyle.Primary) + ); const response = await interaction.editReply({ - content: shouldMention - ? `${selectedMember}, you've been chosen!` - : null, - + content: shouldMention ? `${selectedMember}, you've been chosen!` : null, embeds: [embed], - components: [row], - - allowedMentions: { - users: shouldMention ? [user.id] : [] - } + allowedMentions: { users: shouldMention ? [user.id] : [] } }); - const filter = i => - i.customId === - `randomuser_${interaction.user.id}_again` && - i.user.id === interaction.user.id; - - const collector = - response.createMessageComponentCollector({ - filter, - time: 300000 - }); + const filter = (i) => i.customId === `randomuser_${interaction.user.id}_again` && i.user.id === interaction.user.id; + const collector = response.createMessageComponentCollector({ filter, time: 300000 }); - collector.on('collect', async i => { + collector.on('collect', async (i) => { try { - /* - * Use the current cache again. - * No members.fetch() and no members.list(). - */ - - let newMemberArray = Array.from( - interaction.guild.members.cache.values() - ).filter(member => { - if (member.user.bot && !includeBots) { - return false; - } - - if ( - onlineOnly && - (!member.presence || - member.presence.status === 'offline') - ) { - return false; - } - - if ( - role && - !member.roles.cache.has(role.id) - ) { - return false; - } + // Fetch all server members before selecting again + await interaction.guild.members.fetch(); + + let newMembers = interaction.guild.members.cache.filter(member => { + if (member.user.bot && !includeBots) return false; + + if (onlineOnly && member.presence?.status === 'offline') return false; + + if (role && !member.roles.cache.has(role.id)) return false; return true; }); + let newMemberArray = Array.from(newMembers.values()); + if (!includeBots) { - newMemberArray = newMemberArray.filter( - member => !member.user.bot - ); + newMemberArray = newMemberArray.filter(member => !member.user.bot); } if (newMemberArray.length === 0) { await replyUserError(i, { type: ErrorTypes.USER_INPUT, - message: - 'No users found matching the criteria.', + message: 'No users found matching the criteria.', }); - return; } - /* - * Avoid selecting the same user when another - * eligible member exists. - */ - - if (newMemberArray.length > 1) { - const differentUsers = - newMemberArray.filter( - member => - member.id !== selectedMember.id - ); - - if (differentUsers.length > 0) { - newMemberArray = differentUsers; - } - } - - const newRandomIndex = Math.floor( - Math.random() * newMemberArray.length - ); - - const newSelectedMember = - newMemberArray[newRandomIndex]; - + const newRandomIndex = Math.floor(Math.random() * newMemberArray.length); + const newSelectedMember = newMemberArray[newRandomIndex]; const newUser = newSelectedMember.user; const newRoles = newSelectedMember.roles.cache - .filter( - r => r.id !== interaction.guild.id - ) - .sort( - (a, b) => b.position - a.position - ) + .filter(r => r.id !== interaction.guild.id) + .sort((a, b) => b.position - a.position) .map(r => r.toString()) .slice(0, 10); const newEmbed = successEmbed( '🎲 Random User Selected', - shouldMention - ? `${newSelectedMember}` - : `**${newUser.username}**` + shouldMention ? `${newSelectedMember}` : `**${newUser.username}**` ) - .setThumbnail( - newUser.displayAvatarURL({ - dynamic: true, - size: 256 - }) - ) + .setThumbnail(newUser.displayAvatarURL({ dynamic: true, size: 256 })) .addFields( - { - name: 'Username', - value: newUser.username, - inline: true - }, - { - name: 'Bot', - value: newUser.bot ? 'Yes' : 'No', - inline: true - }, - { - name: `Roles (${newRoles.length})`, - value: - newRoles.length > 0 - ? newRoles - .slice(0, 5) - .join('') + - (newRoles.length > 5 - ? `+${newRoles.length - 5} more` - : '') - : 'No roles', - inline: false - } + { name: 'Username', value: newUser.username, inline: true }, + { name: 'Bot', value: newUser.bot ? 'Yes' : 'No', inline: true }, + { name: `Roles (${newRoles.length})`, value: newRoles.length > 0 ? newRoles.slice(0, 5).join('') + (newRoles.length > 5 ? `+${newRoles.length - 5} more` : '') : 'No roles', inline: false } ) - .setColor('#FF0000'); + .setColor(newSelectedMember.displayHexColor || '#3498db'); await i.update({ - content: shouldMention - ? `${newSelectedMember}, you've been chosen!` - : null, - + content: shouldMention ? `${newSelectedMember}, you've been chosen!` : null, embeds: [newEmbed], - components: [row], - - allowedMentions: { - users: shouldMention - ? [newUser.id] - : [] - } + allowedMentions: { users: shouldMention ? [newUser.id] : [] } }); + } catch (error) { - logger.error( - 'Button interaction error:', - error - ); - - if (!i.replied && !i.deferred) { - await i.reply({ - content: - 'An error occurred while selecting another user.', - flags: ['Ephemeral'] - }); - } + logger.error('Button interaction error:', error); + await i.reply({ + content: 'An error occurred while selecting another user.', + flags: ['Ephemeral'] + }); } }); collector.on('end', () => { - const disabledRow = - ActionRowBuilder.from(row).setComponents( - ButtonBuilder.from( - row.components[0] - ).setDisabled(true) - ); - - interaction - .editReply({ - components: [disabledRow] - }) - .catch(console.error); + const disabledRow = ActionRowBuilder.from(row).setComponents( + ButtonBuilder.from(row.components[0]).setDisabled(true) + ); + + interaction.editReply({ components: [disabledRow] }).catch(console.error); }); }, }; From 3f2389ddfc57ba5ce761f628fd6bbf40faa57407 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:38:02 +0300 Subject: [PATCH 020/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 196 ++++++++++++++++++++++--------- 1 file changed, 139 insertions(+), 57 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index d7068212dc..1eb0e6181c 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -28,6 +28,7 @@ export default { async execute(interaction) { const deferSuccess = await InteractionHelper.safeDefer(interaction); + if (!deferSuccess) { logger.warn(`RandomUser interaction defer failed`, { userId: interaction.user.id, @@ -49,30 +50,46 @@ export default { const onlineOnly = interaction.options.getBoolean('online') || false; const shouldMention = interaction.options.getBoolean('mention') || false; - // Fetch all server members before selecting - await interaction.guild.members.fetch(); + // Use the members already available in cache. + // This avoids Gateway Opcode 8 rate limits. + const getAvailableMembers = () => { + let memberArray = Array.from(interaction.guild.members.cache.values()); - let members = interaction.guild.members.cache.filter(member => { - if (member.user.bot && !includeBots) return false; + memberArray = memberArray.filter(member => { + if (member.user.bot && !includeBots) { + return false; + } - if (onlineOnly && member.presence?.status === 'offline') return false; + if (onlineOnly && member.presence?.status === 'offline') { + return false; + } - if (role && !member.roles.cache.has(role.id)) return false; + if (role && !member.roles.cache.has(role.id)) { + return false; + } - return true; - }); + return true; + }); - let memberArray = Array.from(members.values()); + return memberArray; + }; - if (!includeBots) { - memberArray = memberArray.filter(member => !member.user.bot); - } + let memberArray = getAvailableMembers(); if (memberArray.length === 0) { let errorMessage = 'Could not find any users matching your filters:'; - if (role) errorMessage = `No users have the **${role.name}** role.`; - if (onlineOnly) errorMessage = 'No users are currently online.'; - if (role && onlineOnly) errorMessage = `No **${role.name}** members are online.`; + + if (role) { + errorMessage = `No users have the **${role.name}** role.`; + } + + if (onlineOnly) { + errorMessage = 'No users are currently online.'; + } + + if (role && onlineOnly) { + errorMessage = `No **${role.name}** members are online.`; + } return replyUserError(interaction, { type: ErrorTypes.USER_INPUT, @@ -84,7 +101,7 @@ export default { const selectedMember = memberArray[randomIndex]; const user = selectedMember.user; - const joinDate = selectedMember.joinedAt; + const roles = selectedMember.roles.cache .filter(role => role.id !== interaction.guild.id) .sort((a, b) => b.position - a.position) @@ -93,13 +110,37 @@ export default { const embed = successEmbed( '🎲 Random User Selected', - shouldMention ? `${selectedMember}` : `**${user.username}**` + shouldMention + ? `${selectedMember}` + : `**${user.username}**` ) - .setThumbnail(user.displayAvatarURL({ dynamic: true, size: 256 })) + .setThumbnail( + user.displayAvatarURL({ + dynamic: true, + size: 256 + }) + ) .addFields( - { name: 'Username', value: user.username, inline: true }, - { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true }, - { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false } + { + name: 'Username', + value: user.username, + inline: true + }, + { + name: 'Bot', + value: user.bot ? 'Yes' : 'No', + inline: true + }, + { + name: `Roles (${roles.length})`, + value: roles.length > 0 + ? roles.slice(0, 5).join('') + + (roles.length > 5 + ? `+${roles.length - 5} more` + : '') + : 'No roles', + inline: false + } ) .setColor('#FF0000'); @@ -112,45 +153,53 @@ export default { ); const response = await interaction.editReply({ - content: shouldMention ? `${selectedMember}, you've been chosen!` : null, + content: shouldMention + ? `${selectedMember}, you've been chosen!` + : null, embeds: [embed], components: [row], - allowedMentions: { users: shouldMention ? [user.id] : [] } + allowedMentions: { + users: shouldMention ? [user.id] : [] + } }); - const filter = (i) => i.customId === `randomuser_${interaction.user.id}_again` && i.user.id === interaction.user.id; - const collector = response.createMessageComponentCollector({ filter, time: 300000 }); + const filter = (i) => + i.customId === `randomuser_${interaction.user.id}_again` && + i.user.id === interaction.user.id; + + const collector = response.createMessageComponentCollector({ + filter, + time: 300000 + }); collector.on('collect', async (i) => { try { - // Fetch all server members before selecting again - await interaction.guild.members.fetch(); - - let newMembers = interaction.guild.members.cache.filter(member => { - if (member.user.bot && !includeBots) return false; - - if (onlineOnly && member.presence?.status === 'offline') return false; - - if (role && !member.roles.cache.has(role.id)) return false; - - return true; - }); - - let newMemberArray = Array.from(newMembers.values()); - - if (!includeBots) { - newMemberArray = newMemberArray.filter(member => !member.user.bot); - } + let newMemberArray = getAvailableMembers(); if (newMemberArray.length === 0) { await replyUserError(i, { type: ErrorTypes.USER_INPUT, message: 'No users found matching the criteria.', }); + return; } - const newRandomIndex = Math.floor(Math.random() * newMemberArray.length); + // Avoid selecting the same user again when possible. + if (newMemberArray.length > 1) { + const differentUsers = newMemberArray.filter( + member => member.id !== selectedMember.id + ); + + if (differentUsers.length > 0) { + newMemberArray = differentUsers; + } + } + + const newRandomIndex = Math.floor( + Math.random() * newMemberArray.length + ); + const newSelectedMember = newMemberArray[newRandomIndex]; const newUser = newSelectedMember.user; @@ -162,29 +211,60 @@ export default { const newEmbed = successEmbed( '🎲 Random User Selected', - shouldMention ? `${newSelectedMember}` : `**${newUser.username}**` + shouldMention + ? `${newSelectedMember}` + : `**${newUser.username}**` ) - .setThumbnail(newUser.displayAvatarURL({ dynamic: true, size: 256 })) + .setThumbnail( + newUser.displayAvatarURL({ + dynamic: true, + size: 256 + }) + ) .addFields( - { name: 'Username', value: newUser.username, inline: true }, - { name: 'Bot', value: newUser.bot ? 'Yes' : 'No', inline: true }, - { name: `Roles (${newRoles.length})`, value: newRoles.length > 0 ? newRoles.slice(0, 5).join('') + (newRoles.length > 5 ? `+${newRoles.length - 5} more` : '') : 'No roles', inline: false } + { + name: 'Username', + value: newUser.username, + inline: true + }, + { + name: 'Bot', + value: newUser.bot ? 'Yes' : 'No', + inline: true + }, + { + name: `Roles (${newRoles.length})`, + value: newRoles.length > 0 + ? newRoles.slice(0, 5).join('') + + (newRoles.length > 5 + ? `+${newRoles.length - 5} more` + : '') + : 'No roles', + inline: false + } ) - .setColor(newSelectedMember.displayHexColor || '#3498db'); + .setColor('#FF0000'); await i.update({ - content: shouldMention ? `${newSelectedMember}, you've been chosen!` : null, + content: shouldMention + ? `${newSelectedMember}, you've been chosen!` + : null, embeds: [newEmbed], components: [row], - allowedMentions: { users: shouldMention ? [newUser.id] : [] } + allowedMentions: { + users: shouldMention ? [newUser.id] : [] + } }); } catch (error) { logger.error('Button interaction error:', error); - await i.reply({ - content: 'An error occurred while selecting another user.', - flags: ['Ephemeral'] - }); + + if (!i.replied && !i.deferred) { + await i.reply({ + content: 'An error occurred while selecting another user.', + flags: ['Ephemeral'] + }); + } } }); @@ -193,7 +273,9 @@ export default { ButtonBuilder.from(row.components[0]).setDisabled(true) ); - interaction.editReply({ components: [disabledRow] }).catch(console.error); + interaction.editReply({ + components: [disabledRow] + }).catch(console.error); }); }, }; From 1594a3a7c4ede05fa76b1c2adcb5a8f9d7333841 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 06:39:50 +0300 Subject: [PATCH 021/219] Update randomuser.js --- src/commands/Tools/randomuser.js | 196 +++++++++---------------------- 1 file changed, 57 insertions(+), 139 deletions(-) diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index 1eb0e6181c..d7068212dc 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -28,7 +28,6 @@ export default { async execute(interaction) { const deferSuccess = await InteractionHelper.safeDefer(interaction); - if (!deferSuccess) { logger.warn(`RandomUser interaction defer failed`, { userId: interaction.user.id, @@ -50,46 +49,30 @@ export default { const onlineOnly = interaction.options.getBoolean('online') || false; const shouldMention = interaction.options.getBoolean('mention') || false; - // Use the members already available in cache. - // This avoids Gateway Opcode 8 rate limits. - const getAvailableMembers = () => { - let memberArray = Array.from(interaction.guild.members.cache.values()); + // Fetch all server members before selecting + await interaction.guild.members.fetch(); - memberArray = memberArray.filter(member => { - if (member.user.bot && !includeBots) { - return false; - } + let members = interaction.guild.members.cache.filter(member => { + if (member.user.bot && !includeBots) return false; - if (onlineOnly && member.presence?.status === 'offline') { - return false; - } + if (onlineOnly && member.presence?.status === 'offline') return false; - if (role && !member.roles.cache.has(role.id)) { - return false; - } + if (role && !member.roles.cache.has(role.id)) return false; - return true; - }); + return true; + }); - return memberArray; - }; + let memberArray = Array.from(members.values()); - let memberArray = getAvailableMembers(); + if (!includeBots) { + memberArray = memberArray.filter(member => !member.user.bot); + } if (memberArray.length === 0) { let errorMessage = 'Could not find any users matching your filters:'; - - if (role) { - errorMessage = `No users have the **${role.name}** role.`; - } - - if (onlineOnly) { - errorMessage = 'No users are currently online.'; - } - - if (role && onlineOnly) { - errorMessage = `No **${role.name}** members are online.`; - } + if (role) errorMessage = `No users have the **${role.name}** role.`; + if (onlineOnly) errorMessage = 'No users are currently online.'; + if (role && onlineOnly) errorMessage = `No **${role.name}** members are online.`; return replyUserError(interaction, { type: ErrorTypes.USER_INPUT, @@ -101,7 +84,7 @@ export default { const selectedMember = memberArray[randomIndex]; const user = selectedMember.user; - + const joinDate = selectedMember.joinedAt; const roles = selectedMember.roles.cache .filter(role => role.id !== interaction.guild.id) .sort((a, b) => b.position - a.position) @@ -110,37 +93,13 @@ export default { const embed = successEmbed( '🎲 Random User Selected', - shouldMention - ? `${selectedMember}` - : `**${user.username}**` + shouldMention ? `${selectedMember}` : `**${user.username}**` ) - .setThumbnail( - user.displayAvatarURL({ - dynamic: true, - size: 256 - }) - ) + .setThumbnail(user.displayAvatarURL({ dynamic: true, size: 256 })) .addFields( - { - name: 'Username', - value: user.username, - inline: true - }, - { - name: 'Bot', - value: user.bot ? 'Yes' : 'No', - inline: true - }, - { - name: `Roles (${roles.length})`, - value: roles.length > 0 - ? roles.slice(0, 5).join('') + - (roles.length > 5 - ? `+${roles.length - 5} more` - : '') - : 'No roles', - inline: false - } + { name: 'Username', value: user.username, inline: true }, + { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true }, + { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false } ) .setColor('#FF0000'); @@ -153,53 +112,45 @@ export default { ); const response = await interaction.editReply({ - content: shouldMention - ? `${selectedMember}, you've been chosen!` - : null, + content: shouldMention ? `${selectedMember}, you've been chosen!` : null, embeds: [embed], components: [row], - allowedMentions: { - users: shouldMention ? [user.id] : [] - } + allowedMentions: { users: shouldMention ? [user.id] : [] } }); - const filter = (i) => - i.customId === `randomuser_${interaction.user.id}_again` && - i.user.id === interaction.user.id; - - const collector = response.createMessageComponentCollector({ - filter, - time: 300000 - }); + const filter = (i) => i.customId === `randomuser_${interaction.user.id}_again` && i.user.id === interaction.user.id; + const collector = response.createMessageComponentCollector({ filter, time: 300000 }); collector.on('collect', async (i) => { try { - let newMemberArray = getAvailableMembers(); + // Fetch all server members before selecting again + await interaction.guild.members.fetch(); + + let newMembers = interaction.guild.members.cache.filter(member => { + if (member.user.bot && !includeBots) return false; + + if (onlineOnly && member.presence?.status === 'offline') return false; + + if (role && !member.roles.cache.has(role.id)) return false; + + return true; + }); + + let newMemberArray = Array.from(newMembers.values()); + + if (!includeBots) { + newMemberArray = newMemberArray.filter(member => !member.user.bot); + } if (newMemberArray.length === 0) { await replyUserError(i, { type: ErrorTypes.USER_INPUT, message: 'No users found matching the criteria.', }); - return; } - // Avoid selecting the same user again when possible. - if (newMemberArray.length > 1) { - const differentUsers = newMemberArray.filter( - member => member.id !== selectedMember.id - ); - - if (differentUsers.length > 0) { - newMemberArray = differentUsers; - } - } - - const newRandomIndex = Math.floor( - Math.random() * newMemberArray.length - ); - + const newRandomIndex = Math.floor(Math.random() * newMemberArray.length); const newSelectedMember = newMemberArray[newRandomIndex]; const newUser = newSelectedMember.user; @@ -211,60 +162,29 @@ export default { const newEmbed = successEmbed( '🎲 Random User Selected', - shouldMention - ? `${newSelectedMember}` - : `**${newUser.username}**` + shouldMention ? `${newSelectedMember}` : `**${newUser.username}**` ) - .setThumbnail( - newUser.displayAvatarURL({ - dynamic: true, - size: 256 - }) - ) + .setThumbnail(newUser.displayAvatarURL({ dynamic: true, size: 256 })) .addFields( - { - name: 'Username', - value: newUser.username, - inline: true - }, - { - name: 'Bot', - value: newUser.bot ? 'Yes' : 'No', - inline: true - }, - { - name: `Roles (${newRoles.length})`, - value: newRoles.length > 0 - ? newRoles.slice(0, 5).join('') + - (newRoles.length > 5 - ? `+${newRoles.length - 5} more` - : '') - : 'No roles', - inline: false - } + { name: 'Username', value: newUser.username, inline: true }, + { name: 'Bot', value: newUser.bot ? 'Yes' : 'No', inline: true }, + { name: `Roles (${newRoles.length})`, value: newRoles.length > 0 ? newRoles.slice(0, 5).join('') + (newRoles.length > 5 ? `+${newRoles.length - 5} more` : '') : 'No roles', inline: false } ) - .setColor('#FF0000'); + .setColor(newSelectedMember.displayHexColor || '#3498db'); await i.update({ - content: shouldMention - ? `${newSelectedMember}, you've been chosen!` - : null, + content: shouldMention ? `${newSelectedMember}, you've been chosen!` : null, embeds: [newEmbed], components: [row], - allowedMentions: { - users: shouldMention ? [newUser.id] : [] - } + allowedMentions: { users: shouldMention ? [newUser.id] : [] } }); } catch (error) { logger.error('Button interaction error:', error); - - if (!i.replied && !i.deferred) { - await i.reply({ - content: 'An error occurred while selecting another user.', - flags: ['Ephemeral'] - }); - } + await i.reply({ + content: 'An error occurred while selecting another user.', + flags: ['Ephemeral'] + }); } }); @@ -273,9 +193,7 @@ export default { ButtonBuilder.from(row.components[0]).setDisabled(true) ); - interaction.editReply({ - components: [disabledRow] - }).catch(console.error); + interaction.editReply({ components: [disabledRow] }).catch(console.error); }); }, }; From 574fdede87d2001f7697a650f7e3de4f67e71809 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 9 Aug 2026 08:29:33 +0300 Subject: [PATCH 022/219] Update countdown.js --- src/commands/Tools/countdown.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/Tools/countdown.js b/src/commands/Tools/countdown.js index d7561ddbf3..e830f48eb1 100644 --- a/src/commands/Tools/countdown.js +++ b/src/commands/Tools/countdown.js @@ -15,17 +15,17 @@ export default { .addIntegerOption((option) => option .setName("minutes") - .setDescription("Number of minutes to count down (0-1440)") + .setDescription("Number of minutes to count down (0-7200)") .setMinValue(0) - .setMaxValue(1440) + .setMaxValue(7200) .setRequired(false), ) .addIntegerOption((option) => option .setName("seconds") - .setDescription("Number of seconds to count down (0-59)") + .setDescription("Number of seconds to count down (0-432000)") .setMinValue(0) - .setMaxValue(59) + .setMaxValue(432000) .setRequired(false), ) .addStringOption((option) => @@ -56,8 +56,8 @@ export default { throw new Error("Please specify a duration of at least 1 second."); } - if (totalSeconds > 86400) { - throw new Error("Countdown cannot be longer than 24 hours."); + if (totalSeconds > 432000) { + throw new Error("Countdown cannot be longer than 5 days."); } const endTime = Date.now() + totalSeconds * 1000; @@ -93,4 +93,4 @@ export default { flags: MessageFlags.Ephemeral, }); }, -}; \ No newline at end of file +}; From 4d2b1bdc3338a434032ef6ba60aad0c9d5b58f36 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Fri, 21 Aug 2026 20:17:35 +0300 Subject: [PATCH 023/219] Modify lock command to target specific role Updated the lock command to lock the channel for a specific configured role instead of @everyone. Improved error handling and logging for better clarity. --- src/commands/Moderation/lock.js | 79 +++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/src/commands/Moderation/lock.js b/src/commands/Moderation/lock.js index 75b251973f..50cfbe0563 100644 --- a/src/commands/Moderation/lock.js +++ b/src/commands/Moderation/lock.js @@ -1,56 +1,64 @@ -import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType } from 'discord.js'; -import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; +import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { successEmbed } from '../../utils/embeds.js'; import { logEvent } from '../../utils/moderation.js'; import { logger } from '../../utils/logger.js'; -import { getColor } from '../../config/bot.js'; - import { InteractionHelper } from '../../utils/interactionHelper.js'; import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; + +const TARGET_ROLE_ID = '1534935138440314960'; + export default { - data: new SlashCommandBuilder() - .setName("lock") - .setDescription( - "Locks the current channel (prevents @everyone from sending messages).", - ) -.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels), - category: "moderation", + data: new SlashCommandBuilder() + .setName('lock') + .setDescription('Locks the current channel for the configured role.') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels), + + category: 'moderation', async execute(interaction, config, client) { const deferSuccess = await InteractionHelper.safeDefer(interaction); - if (!deferSuccess) { - logger.warn(`Lock interaction defer failed`, { - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'lock' - }); - return; - } + if (!deferSuccess) return; const channel = interaction.channel; - const everyoneRole = interaction.guild.roles.everyone; + const targetRole = interaction.guild.roles.cache.get(TARGET_ROLE_ID); + + if (!targetRole) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: 'The configured role could not be found in this server.' + }); + } try { - const currentPermissions = channel.permissionsFor(everyoneRole); - if (currentPermissions.has(PermissionFlagsBits.SendMessages) === false) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `${channel} is already locked.` }); + const currentPermissions = channel.permissionsFor(targetRole); + + if (!currentPermissions.has(PermissionFlagsBits.SendMessages)) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: `${channel} is already locked for ${targetRole}.` + }); } await channel.permissionOverwrites.edit( - everyoneRole, + targetRole, { SendMessages: false }, -{ type: 0, reason: `Channel locked by ${interaction.user.tag}` }, + { + type: 0, + reason: `Channel locked for ${targetRole.name} by ${interaction.user.tag}` + } ); await logEvent({ client, guild: interaction.guild, event: { - action: "Channel Locked", + action: 'Channel Locked', target: channel.toString(), executor: `${interaction.user.tag} (${interaction.user.id})`, metadata: { channelId: channel.id, - category: channel.parent?.name || 'None', + roleId: targetRole.id, + roleName: targetRole.name, moderatorId: interaction.user.id } } @@ -59,14 +67,19 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [ successEmbed( - `🔒 **Channel Locked**`, - `${channel} is now locked down. No one can speak here now.`, - ), - ], + '🔒 Channel Locked', + `${channel} is now locked for ${targetRole}.` + ) + ] }); + } catch (error) { logger.error('Lock command error:', error); - await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'An unexpected error occurred while trying to lock the channel. Check my permissions (I need \'Manage Channels\').' }); + + await replyUserError(interaction, { + type: ErrorTypes.PERMISSION, + message: 'I could not modify the channel permissions. Make sure I have Manage Channels.' + }); } } -}; \ No newline at end of file +}; From ebd2d581b721f01b1f5b8daf9fcdc1beabefe049 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Fri, 21 Aug 2026 20:17:59 +0300 Subject: [PATCH 024/219] Refactor unlock command for specific role unlocking Updated the unlock command to allow unlocking for a specific configured role instead of everyone. Improved error handling and logging for better debugging. --- src/commands/Moderation/unlock.js | 138 ++++++++++++++++-------------- 1 file changed, 72 insertions(+), 66 deletions(-) diff --git a/src/commands/Moderation/unlock.js b/src/commands/Moderation/unlock.js index 408c2b3b07..e9956f96fc 100644 --- a/src/commands/Moderation/unlock.js +++ b/src/commands/Moderation/unlock.js @@ -1,79 +1,85 @@ -import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType } from 'discord.js'; -import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; +import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { successEmbed } from '../../utils/embeds.js'; import { logEvent } from '../../utils/moderation.js'; import { logger } from '../../utils/logger.js'; -import { getColor } from '../../config/bot.js'; - import { InteractionHelper } from '../../utils/interactionHelper.js'; import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; + +const TARGET_ROLE_ID = '1534935138440314960'; + export default { - data: new SlashCommandBuilder() - .setName("unlock") - .setDescription( - "Unlocks the current channel (allows @everyone to send messages again).", - ) -.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels), - category: "moderation", + data: new SlashCommandBuilder() + .setName('unlock') + .setDescription('Unlocks the current channel for the configured role.') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels), - async execute(interaction, config, client) { - const deferSuccess = await InteractionHelper.safeDefer(interaction); - if (!deferSuccess) { - logger.warn(`Unlock interaction defer failed`, { - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'unlock' - }); - return; - } + category: 'moderation', + + async execute(interaction, config, client) { + const deferSuccess = await InteractionHelper.safeDefer(interaction); + if (!deferSuccess) return; - const channel = interaction.channel; - const everyoneRole = interaction.guild.roles.everyone; + const channel = interaction.channel; + const targetRole = interaction.guild.roles.cache.get(TARGET_ROLE_ID); - try { - const currentPermissions = channel.permissionsFor(everyoneRole); - if ( - currentPermissions.has(PermissionFlagsBits.SendMessages) === - true || - currentPermissions.has(PermissionFlagsBits.SendMessages) === - null - ) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `${channel} is not explicitly locked (everyone can already send messages).` }); - } + if (!targetRole) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: 'The configured role could not be found in this server.' + }); + } - await channel.permissionOverwrites.edit( - everyoneRole, - { SendMessages: true }, - { - type: 0, - reason: `Channel unlocked by ${interaction.user.tag}`, -}, - ); + try { + const currentPermissions = channel.permissionsFor(targetRole); - await logEvent({ - client, - guild: interaction.guild, - event: { - action: "Channel Unlocked", - target: channel.toString(), - executor: `${interaction.user.tag} (${interaction.user.id})`, - metadata: { - channelId: channel.id, - category: channel.parent?.name || 'None' - } - } - }); + if (currentPermissions.has(PermissionFlagsBits.SendMessages)) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: `${channel} is already unlocked for ${targetRole}.` + }); + } - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - `🔓 **Channel Unlocked**`, - `${channel} is now unlocked. You may speak now.`, - ), - ], - }); - } catch (error) { - logger.error('Unlock command error:', error); - await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'An unexpected error occurred while trying to unlock the channel. Check my permissions (I need \'Manage Channels\').' }); + await channel.permissionOverwrites.edit( + targetRole, + { SendMessages: null }, + { + type: 0, + reason: `Channel unlocked for ${targetRole.name} by ${interaction.user.tag}` } + ); + + await logEvent({ + client, + guild: interaction.guild, + event: { + action: 'Channel Unlocked', + target: channel.toString(), + executor: `${interaction.user.tag} (${interaction.user.id})`, + metadata: { + channelId: channel.id, + roleId: targetRole.id, + roleName: targetRole.name, + moderatorId: interaction.user.id + } + } + }); + + await InteractionHelper.safeEditReply(interaction, { + embeds: [ + successEmbed( + '🔓 Channel Unlocked', + `${channel} is now unlocked for ${targetRole}.` + ) + ] + }); + + } catch (error) { + logger.error('Unlock command error:', error); + + await replyUserError(interaction, { + type: ErrorTypes.PERMISSION, + message: 'I could not modify the channel permissions. Make sure I have Manage Channels.' + }); } -}; \ No newline at end of file + } +}; From f7eabca45319846b1b8d124058fa101e3cc5e9e1 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Fri, 21 Aug 2026 20:19:14 +0300 Subject: [PATCH 025/219] Add hide command for channel moderation Implements a 'hide' command to restrict channel visibility for a specific role. --- src/commands/Moderation/hide.js | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/commands/Moderation/hide.js diff --git a/src/commands/Moderation/hide.js b/src/commands/Moderation/hide.js new file mode 100644 index 0000000000..f120aa28a5 --- /dev/null +++ b/src/commands/Moderation/hide.js @@ -0,0 +1,77 @@ +import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { successEmbed } from '../../utils/embeds.js'; +import { logEvent } from '../../utils/moderation.js'; +import { logger } from '../../utils/logger.js'; +import { InteractionHelper } from '../../utils/interactionHelper.js'; +import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; + +const TARGET_ROLE_ID = '1534935138440314960'; + +export default { + data: new SlashCommandBuilder() + .setName('hide') + .setDescription('Hides the current channel from the configured role.') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels), + + category: 'moderation', + + async execute(interaction, config, client) { + const deferSuccess = await InteractionHelper.safeDefer(interaction); + if (!deferSuccess) return; + + const channel = interaction.channel; + const role = interaction.guild.roles.cache.get(TARGET_ROLE_ID); + + if (!role) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: 'The configured role could not be found in this server.' + }); + } + + try { + await channel.permissionOverwrites.edit( + role, + { + ViewChannel: false + }, + { + type: 0, + reason: `Channel hidden from ${role.name} by ${interaction.user.tag}` + } + ); + + await logEvent({ + client, + guild: interaction.guild, + event: { + action: 'Channel Hidden', + target: channel.toString(), + executor: `${interaction.user.tag} (${interaction.user.id})`, + metadata: { + channelId: channel.id, + roleId: role.id, + roleName: role.name, + moderatorId: interaction.user.id + } + } + }); + + await InteractionHelper.safeEditReply(interaction, { + embeds: [ + successEmbed( + '🔒 Channel Hidden', + `${channel} is now hidden from ${role}.` + ) + ] + }); + } catch (error) { + logger.error('Hide command error:', error); + + await replyUserError(interaction, { + type: ErrorTypes.PERMISSION, + message: 'I could not modify the channel permissions. Make sure I have Manage Channels.' + }); + } + } +}; From 10e81b175324826f1e83f9cb70cf9541f8303754 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Fri, 21 Aug 2026 20:19:47 +0300 Subject: [PATCH 026/219] Implement unhide command for channel visibility --- src/commands/Moderation/unhide.js | 77 +++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/commands/Moderation/unhide.js diff --git a/src/commands/Moderation/unhide.js b/src/commands/Moderation/unhide.js new file mode 100644 index 0000000000..4ada738d1e --- /dev/null +++ b/src/commands/Moderation/unhide.js @@ -0,0 +1,77 @@ +import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { successEmbed } from '../../utils/embeds.js'; +import { logEvent } from '../../utils/moderation.js'; +import { logger } from '../../utils/logger.js'; +import { InteractionHelper } from '../../utils/interactionHelper.js'; +import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; + +const TARGET_ROLE_ID = '1534935138440314960'; + +export default { + data: new SlashCommandBuilder() + .setName('unhide') + .setDescription('Unhides the current channel for the configured role.') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels), + + category: 'moderation', + + async execute(interaction, config, client) { + const deferSuccess = await InteractionHelper.safeDefer(interaction); + if (!deferSuccess) return; + + const channel = interaction.channel; + const role = interaction.guild.roles.cache.get(TARGET_ROLE_ID); + + if (!role) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: 'The configured role could not be found in this server.' + }); + } + + try { + await channel.permissionOverwrites.edit( + role, + { + ViewChannel: null + }, + { + type: 0, + reason: `Channel unhidden for ${role.name} by ${interaction.user.tag}` + } + ); + + await logEvent({ + client, + guild: interaction.guild, + event: { + action: 'Channel Unhidden', + target: channel.toString(), + executor: `${interaction.user.tag} (${interaction.user.id})`, + metadata: { + channelId: channel.id, + roleId: role.id, + roleName: role.name, + moderatorId: interaction.user.id + } + } + }); + + await InteractionHelper.safeEditReply(interaction, { + embeds: [ + successEmbed( + '🔓 Channel Unhidden', + `${channel} is now visible to ${role}.` + ) + ] + }); + } catch (error) { + logger.error('Unhide command error:', error); + + await replyUserError(interaction, { + type: ErrorTypes.PERMISSION, + message: 'I could not modify the channel permissions. Make sure I have Manage Channels.' + }); + } + } +}; From e00b0e24823335d885ace0d03024a5a6d66859e6 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Fri, 21 Aug 2026 20:20:30 +0300 Subject: [PATCH 027/219] Add mute command for moderating members --- src/commands/Moderation/mute.js | 104 ++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/commands/Moderation/mute.js diff --git a/src/commands/Moderation/mute.js b/src/commands/Moderation/mute.js new file mode 100644 index 0000000000..a4513748cc --- /dev/null +++ b/src/commands/Moderation/mute.js @@ -0,0 +1,104 @@ +import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { successEmbed } from '../../utils/embeds.js'; +import { logEvent } from '../../utils/moderation.js'; +import { logger } from '../../utils/logger.js'; +import { InteractionHelper } from '../../utils/interactionHelper.js'; +import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; + +const MUTED_ROLE_NAME = 'muted'; + +export default { + data: new SlashCommandBuilder() + .setName('mute') + .setDescription('Mutes a member by giving them the Muted role.') + .addUserOption(option => + option + .setName('user') + .setDescription('The member to mute.') + .setRequired(true) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), + + category: 'moderation', + + async execute(interaction, config, client) { + const deferSuccess = await InteractionHelper.safeDefer(interaction); + if (!deferSuccess) return; + + const user = interaction.options.getUser('user'); + const member = await interaction.guild.members.fetch(user.id); + + const mutedRole = interaction.guild.roles.cache.find( + role => role.name.toLowerCase() === MUTED_ROLE_NAME.toLowerCase() + ); + + if (!mutedRole) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: 'The `Muted` role could not be found.' + }); + } + + if (member.roles.cache.has(mutedRole.id)) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: `${member} is already muted.` + }); + } + + if ( + member.id === interaction.guild.ownerId || + member.roles.highest.position >= interaction.member.roles.highest.position + ) { + return await replyUserError(interaction, { + type: ErrorTypes.PERMISSION, + message: 'You cannot mute this member because their highest role is equal to or higher than yours.' + }); + } + + if (mutedRole.position >= interaction.guild.members.me.roles.highest.position) { + return await replyUserError(interaction, { + type: ErrorTypes.PERMISSION, + message: 'My role must be above the `Muted` role.' + }); + } + + try { + await member.roles.add( + mutedRole, + `Muted by ${interaction.user.tag}` + ); + + await logEvent({ + client, + guild: interaction.guild, + event: { + action: 'Member Muted', + target: `${member.user.tag} (${member.id})`, + executor: `${interaction.user.tag} (${interaction.user.id})`, + metadata: { + userId: member.id, + roleId: mutedRole.id, + moderatorId: interaction.user.id + } + } + }); + + await InteractionHelper.safeEditReply(interaction, { + embeds: [ + successEmbed( + '🔇 Member Muted', + `${member} has been muted successfully.` + ) + ] + }); + } catch (error) { + logger.error('Mute command error:', error); + + await replyUserError(interaction, { + type: ErrorTypes.PERMISSION, + message: 'I could not give the Muted role to this member. Check my Manage Roles permission and role position.' + }); + } + } +}; From 5396a068a4ed27975ba5a5ea34aa8b822c5d249b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Fri, 21 Aug 2026 20:20:54 +0300 Subject: [PATCH 028/219] Add unmute command for moderating members --- src/commands/Moderation/unmute.js | 87 +++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/commands/Moderation/unmute.js diff --git a/src/commands/Moderation/unmute.js b/src/commands/Moderation/unmute.js new file mode 100644 index 0000000000..6b81c2ff57 --- /dev/null +++ b/src/commands/Moderation/unmute.js @@ -0,0 +1,87 @@ +import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { successEmbed } from '../../utils/embeds.js'; +import { logEvent } from '../../utils/moderation.js'; +import { logger } from '../../utils/logger.js'; +import { InteractionHelper } from '../../utils/interactionHelper.js'; +import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; + +const MUTED_ROLE_NAME = 'muted'; + +export default { + data: new SlashCommandBuilder() + .setName('unmute') + .setDescription('Unmutes a member by removing the Muted role.') + .addUserOption(option => + option + .setName('user') + .setDescription('The member to unmute.') + .setRequired(true) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), + + category: 'moderation', + + async execute(interaction, config, client) { + const deferSuccess = await InteractionHelper.safeDefer(interaction); + if (!deferSuccess) return; + + const user = interaction.options.getUser('user'); + const member = await interaction.guild.members.fetch(user.id); + + const mutedRole = interaction.guild.roles.cache.find( + role => role.name.toLowerCase() === MUTED_ROLE_NAME.toLowerCase() + ); + + if (!mutedRole) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: 'The `Muted` role could not be found.' + }); + } + + if (!member.roles.cache.has(mutedRole.id)) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: `${member} is not muted.` + }); + } + + try { + await member.roles.remove( + mutedRole, + `Unmuted by ${interaction.user.tag}` + ); + + await logEvent({ + client, + guild: interaction.guild, + event: { + action: 'Member Unmuted', + target: `${member.user.tag} (${member.id})`, + executor: `${interaction.user.tag} (${interaction.user.id})`, + metadata: { + userId: member.id, + roleId: mutedRole.id, + moderatorId: interaction.user.id + } + } + }); + + await InteractionHelper.safeEditReply(interaction, { + embeds: [ + successEmbed( + '🔊 Member Unmuted', + `${member} has been unmuted successfully.` + ) + ] + }); + } catch (error) { + logger.error('Unmute command error:', error); + + await replyUserError(interaction, { + type: ErrorTypes.PERMISSION, + message: 'I could not remove the Muted role from this member. Check my Manage Roles permission.' + }); + } + } +}; From b69b97437d179d70409e20cbad67382587b153c4 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Fri, 21 Aug 2026 20:25:46 +0300 Subject: [PATCH 029/219] Enhance mute command with duration and file management Refactor mute command to handle mute durations and file storage for mutes. --- src/commands/Moderation/mute.js | 234 +++++++++++++++++++++++++++++++- 1 file changed, 229 insertions(+), 5 deletions(-) diff --git a/src/commands/Moderation/mute.js b/src/commands/Moderation/mute.js index a4513748cc..93b04f3bd0 100644 --- a/src/commands/Moderation/mute.js +++ b/src/commands/Moderation/mute.js @@ -5,18 +5,179 @@ import { logger } from '../../utils/logger.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; -const MUTED_ROLE_NAME = 'muted'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const MUTED_ROLE_NAME = 'Muted'; +const MUTE_FILE = path.join(__dirname, '../../data/mutes.json'); + +function ensureMuteFile() { + const dir = path.dirname(MUTE_FILE); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + if (!fs.existsSync(MUTE_FILE)) { + fs.writeFileSync(MUTE_FILE, '{}', 'utf8'); + } +} + +function loadMutes() { + ensureMuteFile(); + + try { + return JSON.parse(fs.readFileSync(MUTE_FILE, 'utf8')); + } catch { + return {}; + } +} + +function saveMutes(mutes) { + ensureMuteFile(); + fs.writeFileSync( + MUTE_FILE, + JSON.stringify(mutes, null, 2), + 'utf8' + ); +} + +function parseDuration(duration) { + if (!duration) return null; + + const match = duration.toLowerCase().match( + /^(\d+)\s*(s|m|h|d|w)$/ + ); + + if (!match) return null; + + const amount = Number(match[1]); + const unit = match[2]; + + const units = { + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000, + w: 7 * 24 * 60 * 60 * 1000 + }; + + return amount * units[unit]; +} + +function formatDuration(ms) { + const seconds = Math.floor(ms / 1000); + + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + const parts = []; + + if (days) parts.push(`${days}d`); + if (hours) parts.push(`${hours}h`); + if (minutes) parts.push(`${minutes}m`); + if (secs) parts.push(`${secs}s`); + + return parts.join(' ') || '0s'; +} + +async function scheduleUnmute(client, guildId, userId, expiresAt) { + const delay = expiresAt - Date.now(); + + if (delay <= 0) { + try { + const guild = await client.guilds.fetch(guildId); + const member = await guild.members.fetch(userId).catch(() => null); + + const mutedRole = guild.roles.cache.find( + role => role.name.toLowerCase() === MUTED_ROLE_NAME.toLowerCase() + ); + + if (member && mutedRole && member.roles.cache.has(mutedRole.id)) { + await member.roles.remove( + mutedRole, + 'Temporary mute expired' + ); + } + } catch (error) { + logger.error('Automatic unmute error:', error); + } + + const mutes = loadMutes(); + delete mutes[`${guildId}:${userId}`]; + saveMutes(mutes); + + return; + } + + setTimeout(async () => { + try { + const guild = await client.guilds.fetch(guildId); + const member = await guild.members.fetch(userId).catch(() => null); + + const mutedRole = guild.roles.cache.find( + role => role.name.toLowerCase() === MUTED_ROLE_NAME.toLowerCase() + ); + + if (member && mutedRole && member.roles.cache.has(mutedRole.id)) { + await member.roles.remove( + mutedRole, + 'Temporary mute expired' + ); + } + + const mutes = loadMutes(); + delete mutes[`${guildId}:${userId}`]; + saveMutes(mutes); + + logger.info( + `Temporary mute expired for ${userId} in ${guildId}` + ); + } catch (error) { + logger.error('Automatic unmute error:', error); + } + }, delay); +} + +async function restoreMutes(client) { + const mutes = loadMutes(); + + for (const [key, data] of Object.entries(mutes)) { + const [guildId, userId] = key.split(':'); + + if (!data?.expiresAt) continue; + + await scheduleUnmute( + client, + guildId, + userId, + data.expiresAt + ); + } +} export default { data: new SlashCommandBuilder() .setName('mute') - .setDescription('Mutes a member by giving them the Muted role.') + .setDescription('Mutes a member using the Muted role.') .addUserOption(option => option .setName('user') .setDescription('The member to mute.') .setRequired(true) ) + .addStringOption(option => + option + .setName('duration') + .setDescription('Mute duration: 10s, 10m, 2h, 1d, 1w. Leave empty for permanent.') + .setRequired(false) + ) .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), category: 'moderation', @@ -26,7 +187,18 @@ export default { if (!deferSuccess) return; const user = interaction.options.getUser('user'); - const member = await interaction.guild.members.fetch(user.id); + const duration = interaction.options.getString('duration'); + + const member = await interaction.guild.members + .fetch(user.id) + .catch(() => null); + + if (!member) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: 'This user is not in the server.' + }); + } const mutedRole = interaction.guild.roles.cache.find( role => role.name.toLowerCase() === MUTED_ROLE_NAME.toLowerCase() @@ -56,19 +228,59 @@ export default { }); } - if (mutedRole.position >= interaction.guild.members.me.roles.highest.position) { + if ( + mutedRole.position >= + interaction.guild.members.me.roles.highest.position + ) { return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'My role must be above the `Muted` role.' }); } + let durationMs = null; + + if (duration) { + durationMs = parseDuration(duration); + + if (!durationMs) { + return await replyUserError(interaction, { + type: ErrorTypes.UNKNOWN, + message: 'Invalid duration. Use formats like `10s`, `10m`, `2h`, `1d`, or `1w`.' + }); + } + } + try { await member.roles.add( mutedRole, `Muted by ${interaction.user.tag}` ); + const mutes = loadMutes(); + const muteKey = `${interaction.guild.id}:${member.id}`; + + let expiresAt = null; + + if (durationMs) { + expiresAt = Date.now() + durationMs; + + mutes[muteKey] = { + guildId: interaction.guild.id, + userId: member.id, + expiresAt + }; + + saveMutes(mutes); + + await scheduleUnmute( + client, + interaction.guild.id, + member.id, + expiresAt + ); + } + await logEvent({ client, guild: interaction.guild, @@ -79,19 +291,27 @@ export default { metadata: { userId: member.id, roleId: mutedRole.id, + roleName: mutedRole.name, + duration: duration || 'Permanent', + expiresAt, moderatorId: interaction.user.id } } }); + const durationText = durationMs + ? `\nDuration: **${formatDuration(durationMs)}**` + : '\nDuration: **Permanent**'; + await InteractionHelper.safeEditReply(interaction, { embeds: [ successEmbed( '🔇 Member Muted', - `${member} has been muted successfully.` + `${member} has been muted successfully.${durationText}` ) ] }); + } catch (error) { logger.error('Mute command error:', error); @@ -100,5 +320,9 @@ export default { message: 'I could not give the Muted role to this member. Check my Manage Roles permission and role position.' }); } + }, + + async restoreMutes(client) { + await restoreMutes(client); } }; From 45527c193355e8947b084bd56cb0e6cb021c0006 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 09:55:10 +0300 Subject: [PATCH 030/219] Update commandLoader.js --- src/handlers/loaders/commandLoader.js | 318 +++++++++++++++++--------- 1 file changed, 214 insertions(+), 104 deletions(-) diff --git a/src/handlers/loaders/commandLoader.js b/src/handlers/loaders/commandLoader.js index ecb7ad96d4..d9bd6084b8 100644 --- a/src/handlers/loaders/commandLoader.js +++ b/src/handlers/loaders/commandLoader.js @@ -7,20 +7,18 @@ import botConfig from '../../config/bot.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const MAX_COMMANDS = 100; -const COMMAND_COUNT_WARN_THRESHOLD = 90; function getSubcommandInfo(commandData) { const subcommands = []; - + if (commandData.options) { for (const option of commandData.options) { -if (option.type === 1) { + if (option.type === 1) { subcommands.push(option.name); -} else if (option.type === 2) { + } else if (option.type === 2) { if (option.options) { for (const subOption of option.options) { -if (subOption.type === 1) { + if (subOption.type === 1) { subcommands.push(`${option.name}/${subOption.name}`); } } @@ -28,95 +26,92 @@ if (subOption.type === 1) { } } } - + return subcommands; } async function getAllFiles(directory, fileList = []) { const files = await fs.readdir(directory, { withFileTypes: true }); - + for (const file of files) { const filePath = path.join(directory, file.name); - + if (file.isDirectory()) { if (file.name === 'modules') { continue; } + await getAllFiles(filePath, fileList); } else if (file.name.endsWith('.js')) { fileList.push(filePath); } } - + return fileList; } export async function loadCommands(client) { client.commands = new Collection(); + const commandsPath = path.join(__dirname, '../../commands'); const commandFiles = await getAllFiles(commandsPath); - + logger.info(`Found ${commandFiles.length} command files to load`); - + const uniqueCommandNames = new Set(); - + for (const filePath of commandFiles) { try { const normalizedPath = filePath.replace(/\\/g, '/'); - - const commandName = path.basename(filePath, '.js'); - const commandDir = path.dirname(filePath); - const category = path.basename(commandDir); - + const commandModule = await import(`file://${filePath}`); const command = commandModule.default || commandModule; - + if (!command.data || !command.execute) { - logger.warn(`Command at ${filePath} is missing required "data" or "execute" property.`); + logger.warn( + `Command at ${filePath} is missing required "data" or "execute" property.`, + ); continue; } - + + const commandName = command.data.name; + const commandDir = path.dirname(filePath); + const category = path.basename(commandDir); + command.category = category; command.filePath = normalizedPath; - - const primaryCommandName = command.data.name; - - if (!uniqueCommandNames.has(primaryCommandName)) { - uniqueCommandNames.add(primaryCommandName); - - client.commands.set(primaryCommandName, command); + + if (!uniqueCommandNames.has(commandName)) { + uniqueCommandNames.add(commandName); + client.commands.set(commandName, command); } - + const subcommands = getSubcommandInfo(command.data.toJSON()); - - logger.info(`Loaded command: ${primaryCommandName} from ${normalizedPath} (category: ${category})`); - + + logger.info( + `Loaded command: ${commandName} from ${normalizedPath} (category: ${category})`, + ); + if (subcommands.length > 0) { - logger.info(` - Subcommands: ${subcommands.join(', ')}`); + logger.info( + ` - Subcommands: ${subcommands.join(', ')}`, + ); } - } catch (error) { logger.error(`Error loading command from ${filePath}:`, error); } } - - const commandsWithSubcommands = Array.from(client.commands.values()).filter(cmd => { - const subcommands = getSubcommandInfo(cmd.data.toJSON()); - return subcommands.length > 0; - }); - - const totalSubcommands = commandsWithSubcommands.reduce((total, cmd) => { - return total + getSubcommandInfo(cmd.data.toJSON()).length; - }, 0); - + const uniqueCommands = new Set(); + for (const [name, command] of client.commands.entries()) { if (command.data && command.data.name) { uniqueCommands.add(command.data.name); } } - + logger.info(`Loaded ${uniqueCommands.size} commands`); + return client.commands; } @@ -132,7 +127,6 @@ function collectCommandPayloads(client) { } const commandName = command.data.name; - logger.debug(`Processing command for registration: ${commandName}`); if (registeredNames.has(commandName)) { logger.debug(`Skipping duplicate command: ${commandName}`); @@ -140,8 +134,11 @@ function collectCommandPayloads(client) { } registeredNames.add(commandName); + const commandJson = command.data.toJSON(); + commands.push(commandJson); + totalSubcommands += getSubcommandInfo(commandJson).length; if (process.env.NODE_ENV !== 'production') { @@ -149,7 +146,10 @@ function collectCommandPayloads(client) { } } - return { commands, totalSubcommands }; + return { + commands, + totalSubcommands, + }; } function validateCommands(commands) { @@ -157,10 +157,15 @@ function validateCommands(commands) { for (const cmd of commands) { if (cmd.name && cmd.name.length > 32) { - validationErrors.push(`Command ${cmd.name} has name longer than 32 chars: "${cmd.name}" (${cmd.name.length} chars)`); + validationErrors.push( + `Command ${cmd.name} has name longer than 32 chars`, + ); } + if (cmd.description && cmd.description.length > 110) { - validationErrors.push(`Command ${cmd.name} has description longer than 110 chars: "${cmd.description}" (${cmd.description.length} chars)`); + validationErrors.push( + `Command ${cmd.name} has description longer than 110 chars`, + ); } if (!cmd.options) { @@ -169,19 +174,33 @@ function validateCommands(commands) { for (const option of cmd.options) { if (option.name && option.name.length > 32) { - validationErrors.push(`Command ${cmd.name} option ${option.name} has name longer than 32 chars: "${option.name}" (${option.name.length} chars)`); + validationErrors.push( + `Command ${cmd.name} option ${option.name} has name longer than 32 chars`, + ); } + if (option.description && option.description.length > 110) { - validationErrors.push(`Command ${cmd.name} option ${option.name} has description longer than 110 chars: "${option.description}" (${option.description.length} chars)`); + validationErrors.push( + `Command ${cmd.name} option ${option.name} has description longer than 110 chars`, + ); } if (option.choices) { for (const choice of option.choices) { if (choice.name && choice.name.length > 110) { - validationErrors.push(`Command ${cmd.name} option ${option.name} choice ${choice.name} has name longer than 110 chars: "${choice.name}" (${choice.name.length} chars)`); + validationErrors.push( + `Command ${cmd.name} option ${option.name} choice ${choice.name} has name longer than 110 chars`, + ); } - if (choice.value && choice.value.length > 100) { - validationErrors.push(`Command ${cmd.name} option ${option.name} choice ${choice.name} has value longer than 100 chars: "${choice.value}" (${choice.value.length} chars)`); + + if ( + choice.value && + typeof choice.value === 'string' && + choice.value.length > 100 + ) { + validationErrors.push( + `Command ${cmd.name} option ${option.name} choice ${choice.name} has value longer than 100 chars`, + ); } } } @@ -192,10 +211,18 @@ function validateCommands(commands) { for (const subOption of option.options) { if (subOption.name && subOption.name.length > 32) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} has name longer than 32 chars: "${subOption.name}" (${subOption.name.length} chars)`); + validationErrors.push( + `Command ${cmd.name} subcommand ${option.name} option ${subOption.name} has name longer than 32 chars`, + ); } - if (subOption.description && subOption.description.length > 110) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} has description longer than 110 chars: "${subOption.description}" (${subOption.description.length} chars)`); + + if ( + subOption.description && + subOption.description.length > 110 + ) { + validationErrors.push( + `Command ${cmd.name} subcommand ${option.name} option ${subOption.name} has description longer than 110 chars`, + ); } if (!subOption.choices) { @@ -204,10 +231,19 @@ function validateCommands(commands) { for (const choice of subOption.choices) { if (choice.name && choice.name.length > 110) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} choice ${choice.name} has name longer than 110 chars: "${choice.name}" (${choice.name.length} chars)`); + validationErrors.push( + `Command ${cmd.name} subcommand ${option.name} option ${subOption.name} choice ${choice.name} has name longer than 110 chars`, + ); } - if (choice.value && choice.value.length > 100) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} choice ${choice.name} has value longer than 100 chars: "${choice.value}" (${choice.value.length} chars)`); + + if ( + choice.value && + typeof choice.value === 'string' && + choice.value.length > 100 + ) { + validationErrors.push( + `Command ${cmd.name} subcommand ${option.name} option ${subOption.name} choice ${choice.name} has value longer than 100 chars`, + ); } } } @@ -216,59 +252,109 @@ function validateCommands(commands) { if (validationErrors.length > 0) { logger.error('Command validation failed. Errors:'); - validationErrors.forEach((error) => logger.error(` - ${error}`)); - throw new Error(`Command validation failed with ${validationErrors.length} errors`); - } -} -function prepareCommandsForRegistration(commands) { - if (commands.length >= COMMAND_COUNT_WARN_THRESHOLD) { - logger.warn(`Command count (${commands.length}) is near Discord's ${MAX_COMMANDS} global command limit`); - } + validationErrors.forEach((error) => { + logger.error(` - ${error}`); + }); - if (commands.length <= MAX_COMMANDS) { - return commands; + throw new Error( + `Command validation failed with ${validationErrors.length} errors`, + ); } - - logger.warn(`Command count (${commands.length}) exceeds Discord limit (${MAX_COMMANDS}), truncating...`); - const truncated = commands.slice(0, MAX_COMMANDS); - logger.info(`Truncated to ${truncated.length} commands for registration`); - return truncated; } -async function registerGlobalCommands(client, clientId, commands, totalSubcommands) { +async function registerGuildCommands( + client, + clientId, + guildId, + commands, + totalSubcommands, +) { if (!clientId) { - throw new Error('CLIENT_ID is required for slash command registration'); + throw new Error( + 'CLIENT_ID is required for slash command registration', + ); + } + + if (!guildId) { + throw new Error( + 'GUILD_ID is required for guild slash command registration', + ); } if (!client.rest) { - throw new Error('Discord REST client is not available for slash command registration'); + throw new Error( + 'Discord REST client is not available for slash command registration', + ); } - logger.info(`Preparing to register ${totalSubcommands + commands.length} commands globally`); + logger.info( + `Preparing to register ${commands.length} commands for guild ${guildId}`, + ); + + logger.info( + `Total subcommands: ${totalSubcommands}`, + ); + logger.info('Validating commands before registration...'); + validateCommands(commands); - logger.info('Command validation passed'); - const commandsToRegister = prepareCommandsForRegistration(commands); + logger.info('Command validation passed'); if (botConfig.commands?.deleteCommands) { - logger.info('Clearing existing global commands before registration...'); - await client.rest.put(`/applications/${clientId}/commands`, { body: [] }); + logger.info( + `Clearing existing guild commands for ${guildId}...`, + ); + + await client.rest.put( + `/applications/${clientId}/guilds/${guildId}/commands`, + { + body: [], + }, + ); + + logger.info('Existing guild commands cleared'); } - logger.info(`Registering ${commandsToRegister.length} global commands...`); - await client.rest.put(`/applications/${clientId}/commands`, { body: commandsToRegister }); - logger.info(`Successfully registered ${commandsToRegister.length} global commands`); - logger.info('Global commands may take up to an hour to appear in all servers on first deploy'); + logger.info( + `Registering ${commands.length} guild commands...`, + ); + + await client.rest.put( + `/applications/${clientId}/guilds/${guildId}/commands`, + { + body: commands, + }, + ); + + logger.info( + `Successfully registered ${commands.length} guild commands`, + ); } export async function registerCommands(client, options = {}) { - const { clientId = null } = options; + const clientId = + options.clientId || + process.env.CLIENT_ID; + + const guildId = + options.guildId || + process.env.GUILD_ID; try { - const { commands, totalSubcommands } = collectCommandPayloads(client); - await registerGlobalCommands(client, clientId, commands, totalSubcommands); + const { + commands, + totalSubcommands, + } = collectCommandPayloads(client); + + await registerGuildCommands( + client, + clientId, + guildId, + commands, + totalSubcommands, + ); } catch (error) { logger.error('Error registering commands:', error); throw error; @@ -277,24 +363,48 @@ export async function registerCommands(client, options = {}) { export async function reloadCommand(client, commandName) { const command = client.commands.get(commandName); - + if (!command) { - return { success: false, message: `Command "${commandName}" not found` }; + return { + success: false, + message: `Command "${commandName}" not found`, + }; } - + try { const commandPath = path.resolve(command.filePath); const moduleUrl = pathToFileURL(commandPath); - moduleUrl.searchParams.set('t', Date.now().toString()); - - const newCommand = (await import(moduleUrl.href)).default; - - client.commands.set(commandName, newCommand); - - logger.info(`Reloaded command: ${commandName}`); - return { success: true, message: `Successfully reloaded command "${commandName}"` }; + + moduleUrl.searchParams.set( + 't', + Date.now().toString(), + ); + + const newCommand = + (await import(moduleUrl.href)).default; + + client.commands.set( + commandName, + newCommand, + ); + + logger.info( + `Reloaded command: ${commandName}`, + ); + + return { + success: true, + message: `Successfully reloaded command "${commandName}"`, + }; } catch (error) { - logger.error(`Error reloading command "${commandName}":`, error); - return { success: false, message: `Error reloading command: ${error.message}` }; + logger.error( + `Error reloading command "${commandName}":`, + error, + ); + + return { + success: false, + message: `Error reloading command: ${error.message}`, + }; } -} \ No newline at end of file +} From a5ce86fbb7eb6c3ee3a1ddd64be725aaf07b9fd8 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:09:15 +0300 Subject: [PATCH 031/219] Delete src/commands/Economy directory --- src/commands/Economy/balance.js | 87 --- src/commands/Economy/beg.js | 99 ---- src/commands/Economy/buy.js | 160 ------ src/commands/Economy/crime.js | 119 ---- src/commands/Economy/daily.js | 104 ---- src/commands/Economy/deposit.js | 138 ----- src/commands/Economy/economy.js | 32 -- src/commands/Economy/eleaderboard.js | 89 --- src/commands/Economy/fish.js | 132 ----- src/commands/Economy/gamble.js | 131 ----- src/commands/Economy/inventory.js | 70 --- src/commands/Economy/mine.js | 93 ---- .../Economy/modules/economy_dashboard.js | 527 ------------------ src/commands/Economy/modules/shop_browse.js | 94 ---- .../Economy/modules/shop_config_setrole.js | 31 -- src/commands/Economy/pay.js | 149 ----- src/commands/Economy/rob.js | 154 ----- src/commands/Economy/shop-config.js | 28 - src/commands/Economy/shop.js | 13 - src/commands/Economy/slut.js | 188 ------- src/commands/Economy/withdraw.js | 85 --- src/commands/Economy/work.js | 123 ---- 22 files changed, 2646 deletions(-) delete mode 100644 src/commands/Economy/balance.js delete mode 100644 src/commands/Economy/beg.js delete mode 100644 src/commands/Economy/buy.js delete mode 100644 src/commands/Economy/crime.js delete mode 100644 src/commands/Economy/daily.js delete mode 100644 src/commands/Economy/deposit.js delete mode 100644 src/commands/Economy/economy.js delete mode 100644 src/commands/Economy/eleaderboard.js delete mode 100644 src/commands/Economy/fish.js delete mode 100644 src/commands/Economy/gamble.js delete mode 100644 src/commands/Economy/inventory.js delete mode 100644 src/commands/Economy/mine.js delete mode 100644 src/commands/Economy/modules/economy_dashboard.js delete mode 100644 src/commands/Economy/modules/shop_browse.js delete mode 100644 src/commands/Economy/modules/shop_config_setrole.js delete mode 100644 src/commands/Economy/pay.js delete mode 100644 src/commands/Economy/rob.js delete mode 100644 src/commands/Economy/shop-config.js delete mode 100644 src/commands/Economy/shop.js delete mode 100644 src/commands/Economy/slut.js delete mode 100644 src/commands/Economy/withdraw.js delete mode 100644 src/commands/Economy/work.js diff --git a/src/commands/Economy/balance.js b/src/commands/Economy/balance.js deleted file mode 100644 index 95528ecd9c..0000000000 --- a/src/commands/Economy/balance.js +++ /dev/null @@ -1,87 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, getMaxBankCapacity } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -export default { - data: new SlashCommandBuilder() - .setName('balance') - .setDescription("Check your or someone else's balance") - .addUserOption(option => - option - .setName('user') - .setDescription('User to check balance for') - .setRequired(false) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userOption = interaction.options.getUser("user"); - const targetUser = userOption || interaction.user; - const guildId = interaction.guildId; - - logger.info(`[ECONOMY] Balance check - userOption: ${userOption?.id || 'null'}, targetUser: ${targetUser.id}, guildId: ${guildId}, isPrefix: ${!!interaction._commandStartTime}`); - - logger.debug(`[ECONOMY] Balance check for ${targetUser.id}`, { userId: targetUser.id, guildId }); - - if (targetUser.bot) { - throw createError( - "Bot user queried for balance", - ErrorTypes.VALIDATION, - "Bots don't have an economy balance." - ); - } - - const userData = await getEconomyData(client, guildId, targetUser.id); - - logger.info(`[ECONOMY] Economy data retrieved - userData:`, userData); - - if (!userData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load economy data. Please try again later.", - { userId: targetUser.id, guildId } - ); - } - - const maxBank = getMaxBankCapacity(userData); - - const wallet = typeof userData.wallet === 'number' ? userData.wallet : 0; - const bank = typeof userData.bank === 'number' ? userData.bank : 0; - - const embed = createEmbed({ - title: `${targetUser.username}'s Balance`, - description: `Here is the current financial status for ${targetUser.username}.`, - }) - .addFields( - { - name: "💵 Cash", - value: `$${wallet.toLocaleString()}`, - inline: true, - }, - { - name: "🏦 Bank", - value: `$${bank.toLocaleString()} / $${maxBank.toLocaleString()}`, - inline: true, - }, - { - name: "💰 Total", - value: `$${(wallet + bank).toLocaleString()}`, - inline: true, - } - ) - .setFooter({ - text: `Requested by ${interaction.user.tag}`, - iconURL: interaction.user.displayAvatarURL(), - }); - - logger.info(`[ECONOMY] Balance retrieved`, { userId: targetUser.id, wallet, bank }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'balance' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/beg.js b/src/commands/Economy/beg.js deleted file mode 100644 index 9b2fc717f9..0000000000 --- a/src/commands/Economy/beg.js +++ /dev/null @@ -1,99 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { successEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { botConfig } from '../../config/bot.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const COOLDOWN = 30 * 60 * 1000; -const MIN_WIN = Number(botConfig?.economy?.begMin) || 50; -const MAX_WIN = Number(botConfig?.economy?.begMax) || 200; -const SUCCESS_CHANCE = 0.7; - -export default { - data: new SlashCommandBuilder() - .setName('beg') - .setDescription('Beg for a small amount of money'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - - let userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const lastBeg = userData.lastBeg || 0; - const remainingTime = lastBeg + COOLDOWN - Date.now(); - - if (remainingTime > 0) { - const minutes = Math.floor(remainingTime / 60000); - const seconds = Math.floor((remainingTime % 60000) / 1000); - - let timeMessage = - minutes > 0 ? `${minutes} minute(s)` : `${seconds} second(s)`; - - throw createError( - "Beg cooldown active", - ErrorTypes.RATE_LIMIT, - `You are tired from begging! Try again in **${timeMessage}**.`, - { remainingTime, minutes, seconds, cooldownType: 'beg' } - ); - } - - const success = Math.random() < SUCCESS_CHANCE; - - let replyEmbed; - let newCash = userData.wallet; - - if (success) { - const amountWon = - Math.floor(Math.random() * (MAX_WIN - MIN_WIN + 1)) + MIN_WIN; - - newCash += amountWon; - - const successMessages = [ - `A kind stranger drops **$${amountWon.toLocaleString()}** into your cup.`, - `You spotted an unattended wallet! You grab **$${amountWon.toLocaleString()}** and run.`, - `Someone took pity on you and gave you **$${amountWon.toLocaleString()}**!`, - `You found **$${amountWon.toLocaleString()}** under a park bench.`, - ]; - - replyEmbed = successEmbed( - 'Begging Successful', - successMessages[ - Math.floor(Math.random() * successMessages.length) - ] - ); - } else { - const failMessages = [ - "The police chased you off. You got nothing.", - "Someone yelled, 'Get a job!' and walked past.", - "A squirrel stole the single coin you had.", - "You tried to beg, but you were too embarrassed and gave up.", - ]; - - replyEmbed = warningEmbed( - 'Insufficient Funds', - failMessages[Math.floor(Math.random() * failMessages.length)] - ); - } - - userData.wallet = newCash; -userData.lastBeg = Date.now(); - - await setEconomyData(client, guildId, userId, userData); - - await InteractionHelper.safeEditReply(interaction, { embeds: [replyEmbed] }); - }, { command: 'beg' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js deleted file mode 100644 index 1a90387315..0000000000 --- a/src/commands/Economy/buy.js +++ /dev/null @@ -1,160 +0,0 @@ -import { SlashCommandBuilder, MessageFlags } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { shopItems } from '../../config/shop/items.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { getGuildConfig } from '../../services/config/guildConfig.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const SHOP_ITEMS = shopItems; - -export default { - data: new SlashCommandBuilder() - .setName('buy') - .setDescription('Buy an item from the shop') - .addStringOption(option => - option - .setName('item_id') - .setDescription('ID of the item to buy') - .setRequired(true) - ) - .addIntegerOption(option => - option - .setName('quantity') - .setDescription('Quantity to buy (default: 1)') - .setRequired(false) - .setMinValue(1) - .setMaxValue(10) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const itemId = interaction.options.getString("item_id").toLowerCase(); - const quantity = interaction.options.getInteger("quantity") || 1; - - const item = SHOP_ITEMS.find(i => i.id === itemId); - - if (!item) { - throw createError( - `Item ${itemId} not found`, - ErrorTypes.VALIDATION, - `The item ID \`${itemId}\` does not exist in the shop.`, - { itemId } - ); - } - - if (quantity < 1) { - throw createError( - "Invalid quantity", - ErrorTypes.VALIDATION, - "You must purchase a quantity of 1 or more.", - { quantity } - ); - } - - const totalCost = item.price * quantity; - - const guildConfig = await getGuildConfig(client, guildId); - const PREMIUM_ROLE_ID = guildConfig.premiumRoleId; - - const userData = await getEconomyData(client, guildId, userId); - - if (userData.wallet < totalCost) { - throw createError( - "Insufficient funds", - ErrorTypes.VALIDATION, - `You need **$${totalCost.toLocaleString()}** to purchase ${quantity}x **${item.name}**, but you only have **$${userData.wallet.toLocaleString()}** in cash.`, - { required: totalCost, current: userData.wallet, itemId, quantity } - ); - } - - if (item.type === "role" && itemId === "premium_role") { - if (!PREMIUM_ROLE_ID) { - throw createError( - "Premium role not configured", - ErrorTypes.CONFIGURATION, - "The **Premium Shop Role** has not been configured by a server administrator yet.", - { itemId } - ); - } - if (interaction.member.roles.cache.has(PREMIUM_ROLE_ID)) { - throw createError( - "Role already owned", - ErrorTypes.VALIDATION, - `You already have the **${item.name}** role.`, - { itemId, roleId: PREMIUM_ROLE_ID } - ); - } - if (quantity > 1) { - throw createError( - "Invalid quantity for role", - ErrorTypes.VALIDATION, - `You can only purchase the **${item.name}** role once.`, - { itemId, quantity } - ); - } - } - - userData.wallet -= totalCost; - - let successDescription = `You successfully purchased ${quantity}x **${item.name}** for **$${totalCost.toLocaleString()}**!`; - - if (item.type === "role" && itemId === "premium_role") { - const member = interaction.member; - - const role = interaction.guild.roles.cache.get(PREMIUM_ROLE_ID); - - if (!role) { - throw createError( - "Role not found", - ErrorTypes.CONFIGURATION, - "The configured premium role no longer exists in this guild.", - { roleId: PREMIUM_ROLE_ID } - ); - } - - try { - await member.roles.add( - role, - `Purchased role: ${item.name}`, - ); - successDescription += `\n\n**👑 The role ${role.toString()} has been granted to you!**`; - } catch (roleError) { - userData.wallet += totalCost; - await setEconomyData(client, guildId, userId, userData); - throw createError( - "Role assignment failed", - ErrorTypes.DISCORD_API, - "Successfully deducted money, but failed to grant the role. Your cash has been refunded.", - { roleId: PREMIUM_ROLE_ID, originalError: roleError.message } - ); - } - } else if (item.type === "upgrade") { - userData.upgrades[itemId] = true; - successDescription += `\n\n**✨ Your upgrade is now active!**`; - } else if (item.type === "consumable" || item.type === "tool") { - userData.inventory[itemId] = - (userData.inventory[itemId] || 0) + quantity; - if (item.type === "tool") { - successDescription += `\n\n**🛠️ ${item.name} added to your inventory!**`; - } - } - - await setEconomyData(client, guildId, userId, userData); - - const embed = successEmbed( - "💰 Purchase Successful", - successDescription, - ).addFields({ - name: "New Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: [MessageFlags.Ephemeral] }); - }, { command: 'buy' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/crime.js b/src/commands/Economy/crime.js deleted file mode 100644 index 101dc5a575..0000000000 --- a/src/commands/Economy/crime.js +++ /dev/null @@ -1,119 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const CRIME_COOLDOWN = 60 * 60 * 1000; -const JAIL_TIME = 2 * 60 * 60 * 1000; -const FINE_RATE = 0.2; - -const CRIME_TYPES = [ - { name: "Pickpocketing", min: 100, max: 500, risk: 0.3 }, - { name: "Burglary", min: 300, max: 1000, risk: 0.4 }, - { name: "Bank Heist", min: 1000, max: 5000, risk: 0.6 }, - { name: "Art Theft", min: 2000, max: 10000, risk: 0.7 }, - { name: "Cybercrime", min: 5000, max: 20000, risk: 0.8 }, -]; - -export default { - data: new SlashCommandBuilder() - .setName('crime') - .setDescription('Commit a crime to earn money (risky)') - .addStringOption(option => - option - .setName('type') - .setDescription('Type of crime to commit') - .setRequired(true) - .addChoices( - { name: 'Pickpocketing', value: 'pickpocketing' }, - { name: 'Burglary', value: 'burglary' }, - { name: 'Bank Heist', value: 'bank-heist' }, - { name: 'Art Theft', value: 'art-theft' }, - { name: 'Cybercrime', value: 'cybercrime' }, - ) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - await InteractionHelper.safeDefer(interaction); - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - const lastCrime = userData.cooldowns?.crime || 0; - const isJailed = userData.jailedUntil && userData.jailedUntil > now; - - if (isJailed) { - const timeLeft = Math.ceil((userData.jailedUntil - now) / (1000 * 60)); - throw createError( - "User is in jail", - ErrorTypes.RATE_LIMIT, - `You're in jail for ${timeLeft} more minutes!`, - { jailTimeRemaining: userData.jailedUntil - now } - ); - } - - if (now < lastCrime + CRIME_COOLDOWN) { - const timeLeft = Math.ceil((lastCrime + CRIME_COOLDOWN - now) / (1000 * 60)); - throw createError( - "Crime cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to wait ${timeLeft} more minutes before committing another crime.`, - { remaining: lastCrime + CRIME_COOLDOWN - now, cooldownType: 'crime' } - ); - } - - const crimeType = interaction.options.getString("type").toLowerCase(); - const crime = CRIME_TYPES.find( - c => c.name.toLowerCase().replace(/\s+/g, '-') === crimeType - ); - - if (!crime) { - throw createError( - "Invalid crime type", - ErrorTypes.VALIDATION, - "Please select a valid crime type.", - { crimeType } - ); - } - - const isSuccess = Math.random() > crime.risk; - const amountEarned = isSuccess - ? Math.floor(Math.random() * (crime.max - crime.min + 1)) + crime.min - : 0; - - userData.cooldowns = userData.cooldowns || {}; - userData.cooldowns.crime = now; - - if (isSuccess) { - userData.wallet = (userData.wallet || 0) + amountEarned; - - await setEconomyData(client, guildId, userId, userData); - - const embed = successEmbed( - "🕵️ Crime Successful!", - `You successfully committed ${crime.name} and earned **${amountEarned}** coins!` - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - } else { - // Fine is based on the potential haul of the attempted crime - const potentialHaul = Math.floor((crime.min + crime.max) / 2); - const fine = Math.min(Math.floor(potentialHaul * FINE_RATE), userData.wallet || 0); - userData.wallet = Math.max(0, (userData.wallet || 0) - fine); - userData.jailedUntil = now + JAIL_TIME; - - await setEconomyData(client, guildId, userId, userData); - - const embed = warningEmbed( - "🚔 Crime Failed!", - `You were caught while attempting ${crime.name} and have been sent to jail! ` + - `You were fined ${fine.toLocaleString()} coins and will be in jail for 2 hours.` - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - } - }, { command: 'crime' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/daily.js b/src/commands/Economy/daily.js deleted file mode 100644 index 4045d23ed0..0000000000 --- a/src/commands/Economy/daily.js +++ /dev/null @@ -1,104 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { getGuildConfig } from '../../services/config/guildConfig.js'; -import { formatDuration } from '../../utils/embeds.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { botConfig } from '../../config/bot.js'; - -const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; -const DAILY_AMOUNT = botConfig.economy?.dailyAmount ?? 100; -const PREMIUM_BONUS_PERCENTAGE = 0.1; - -export default { - data: new SlashCommandBuilder() - .setName('daily') - .setDescription('Claim your daily cash reward'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - logger.debug(`[ECONOMY] Daily claimed started for ${userId}`, { userId, guildId }); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data for daily", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const lastDaily = userData.lastDaily || 0; - - if (now < lastDaily + DAILY_COOLDOWN) { - const timeRemaining = lastDaily + DAILY_COOLDOWN - now; - throw createError( - "Daily cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to wait before claiming daily again. Try again in **${formatDuration(timeRemaining)}**.`, - { timeRemaining, cooldownType: 'daily' } - ); - } - - const guildConfig = await getGuildConfig(client, guildId); - const PREMIUM_ROLE_ID = guildConfig.premiumRoleId; - - let earned = DAILY_AMOUNT; - let bonusMessage = ""; - let hasPremiumRole = false; - - if ( - PREMIUM_ROLE_ID && - interaction.member && - interaction.member.roles.cache.has(PREMIUM_ROLE_ID) - ) { - const bonusAmount = Math.floor( - DAILY_AMOUNT * PREMIUM_BONUS_PERCENTAGE, - ); - earned += bonusAmount; - bonusMessage = `\n✨ **Premium Bonus:** +$${bonusAmount.toLocaleString()}`; - hasPremiumRole = true; - } - - userData.wallet = (userData.wallet || 0) + earned; - userData.lastDaily = now; - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Daily claimed`, { - userId, - guildId, - amount: earned, - newWallet: userData.wallet, - hasPremium: hasPremiumRole, - timestamp: new Date().toISOString() - }); - - const embed = successEmbed( - "✅ Daily Claimed!", - `You have claimed your daily **$${earned.toLocaleString()}**!${bonusMessage}` - ) - .addFields({ - name: "New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }) - .setFooter({ - text: hasPremiumRole - ? `Next claim in 24 hours. (Premium Active)` - : `Next claim in 24 hours.`, - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'daily' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/deposit.js b/src/commands/Economy/deposit.js deleted file mode 100644 index 56015b5fb4..0000000000 --- a/src/commands/Economy/deposit.js +++ /dev/null @@ -1,138 +0,0 @@ -import { SlashCommandBuilder, MessageFlags } from 'discord.js'; -import { successEmbed, buildUserErrorEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData, getMaxBankCapacity } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -export default { - data: new SlashCommandBuilder() - .setName('deposit') - .setDescription('Deposit money from your wallet into your bank') - .addStringOption(option => - option - .setName('amount') - .setDescription('Amount to deposit (number or "all")') - .setRequired(true) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const amountInput = interaction.options.getString("amount"); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const maxBank = getMaxBankCapacity(userData); - let depositAmount; - - if (amountInput.toLowerCase() === "all") { - depositAmount = userData.wallet; - } else { - depositAmount = parseInt(amountInput); - - if (isNaN(depositAmount) || depositAmount <= 0) { - throw createError( - "Invalid deposit amount", - ErrorTypes.VALIDATION, - `Please enter a valid number or 'all'. You entered: \`${amountInput}\``, - { amountInput, userId } - ); - } - } - - if (depositAmount === 0) { - throw createError( - "Zero deposit amount", - ErrorTypes.VALIDATION, - "You have no cash to deposit.", - { userId, walletBalance: userData.wallet } - ); - } - - if (depositAmount > userData.wallet) { - depositAmount = userData.wallet; - await interaction.followUp({ - embeds: [ - buildUserErrorEmbed( - 'validation', - `You tried to deposit more than you have. Depositing your remaining cash: **$${depositAmount.toLocaleString()}**` - ) - ], - flags: MessageFlags.Ephemeral, - }); - } - - const availableSpace = maxBank - userData.bank; - - if (availableSpace <= 0) { - throw createError( - "Bank is full", - ErrorTypes.VALIDATION, - `Your bank is currently full (Max Capacity: $${maxBank.toLocaleString()}). Purchase a **Bank Upgrade** to increase your limit.`, - { maxBank, currentBank: userData.bank, userId } - ); - } - - if (depositAmount > availableSpace) { - const originalDepositAmount = depositAmount; - depositAmount = availableSpace; - - if (amountInput.toLowerCase() !== "all") { - await interaction.followUp({ - embeds: [ - buildUserErrorEmbed( - 'validation', - `You only had space for **$${depositAmount.toLocaleString()}** in your bank account (Max: $${maxBank.toLocaleString()}). The rest remains in your cash.` - ) - ], - flags: MessageFlags.Ephemeral, - }); - } - } - - if (depositAmount === 0) { - throw createError( - "No space or cash for deposit", - ErrorTypes.VALIDATION, - "The amount you tried to deposit was either 0 or exceeded your bank capacity after checking your cash balance.", - { depositAmount, availableSpace, walletBalance: userData.wallet } - ); - } - - userData.wallet -= depositAmount; - userData.bank += depositAmount; - - await setEconomyData(client, guildId, userId, userData); - - const embed = successEmbed( - 'Deposit Successful', - `You successfully deposited **$${depositAmount.toLocaleString()}** into your bank.` - ) - .addFields( - { - name: "New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: "New Bank Balance", - value: `$${userData.bank.toLocaleString()} / $${maxBank.toLocaleString()}`, - inline: true, - }, - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'deposit' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/economy.js b/src/commands/Economy/economy.js deleted file mode 100644 index d702eb6157..0000000000 --- a/src/commands/Economy/economy.js +++ /dev/null @@ -1,32 +0,0 @@ -import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import economyDashboard from './modules/economy_dashboard.js'; - -export default { - slashOnly: true, - data: new SlashCommandBuilder() - .setName('economy') - .setDescription('Economy management commands') - .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) - .setDMPermission(false) - .addSubcommand(subcommand => - subcommand - .setName('dashboard') - .setDescription('Open the economy management dashboard') - ), - category: 'Economy', - - async execute(interaction, config, client) { - const deferred = await InteractionHelper.safeDefer(interaction, { - flags: MessageFlags.Ephemeral, - }); - if (!deferred) return; - - const subcommand = interaction.options.getSubcommand(); - - if (subcommand === 'dashboard') { - await economyDashboard.execute(interaction, config, client); - } - } -}; \ No newline at end of file diff --git a/src/commands/Economy/eleaderboard.js b/src/commands/Economy/eleaderboard.js deleted file mode 100644 index e4603797a7..0000000000 --- a/src/commands/Economy/eleaderboard.js +++ /dev/null @@ -1,89 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed } from '../../utils/embeds.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { getEconomyPrefix } from '../../utils/database.js'; - -export default { - data: new SlashCommandBuilder() - .setName("eleaderboard") - .setDescription("View the server's top 10 richest users.") - .setDMPermission(false), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const guildId = interaction.guildId; - - logger.debug(`[ECONOMY] Leaderboard requested`, { guildId }); - - const prefix = getEconomyPrefix(guildId); - - let allKeys = await client.db.list(prefix); - - if (!Array.isArray(allKeys)) { - allKeys = []; - } - - if (allKeys.length === 0) { - throw createError( - "No economy data found", - ErrorTypes.VALIDATION, - "No economy data found for this server." - ); - } - - let allUserData = []; - - for (const key of allKeys) { - const userId = key.replace(prefix, ""); - const userData = await client.db.get(key); - - if (userData) { - allUserData.push({ - userId: userId, - net_worth: (userData.wallet || 0) + (userData.bank || 0), - }); - } - } - - allUserData.sort((a, b) => b.net_worth - a.net_worth); - - const topUsers = allUserData.slice(0, 10); - const userRank = - allUserData.findIndex((u) => u.userId === interaction.user.id) + - 1; - const rankEmoji = ["🥇", "🥈", "🥉"]; - const leaderboardEntries = []; - - for (let i = 0; i < topUsers.length; i++) { - const user = topUsers[i]; - const rank = i + 1; - const emoji = rankEmoji[i] || `**#${rank}**`; - - leaderboardEntries.push( - `${emoji} <@${user.userId}> - 🏦 ${user.net_worth.toLocaleString()}`, - ); - } - - logger.info(`[ECONOMY] Leaderboard generated`, { - guildId, - userCount: allUserData.length, - userRank - }); - - const description = leaderboardEntries.length > 0 - ? leaderboardEntries.join("\n") - : "No economy data is available for this server yet."; - - const embed = createEmbed({ - title: `Economy Leaderboard`, - description, - footer: `Your Rank: ${userRank > 0 ?`#${userRank}`: "No ranking data available"}`, - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'eleaderboard' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js deleted file mode 100644 index 5288f048bb..0000000000 --- a/src/commands/Economy/fish.js +++ /dev/null @@ -1,132 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const FISH_COOLDOWN = 45 * 60 * 1000; -const BASE_MIN_REWARD = 300; -const BASE_MAX_REWARD = 900; -const FISHING_ROD_MULTIPLIER = 1.5; - -const FISH_TYPES = [ - { name: 'Bass', emoji: '🐟', rarity: 'common' }, - { name: 'Salmon', emoji: '🐟', rarity: 'common' }, - { name: 'Trout', emoji: '🐟', rarity: 'common' }, - { name: 'Tuna', emoji: '🐠', rarity: 'uncommon' }, - { name: 'Swordfish', emoji: '🐠', rarity: 'uncommon' }, - { name: 'Octopus', emoji: '🐙', rarity: 'rare' }, - { name: 'Lobster', emoji: '🦞', rarity: 'rare' }, - { name: 'Shark', emoji: '🦈', rarity: 'epic' }, - { name: 'Whale', emoji: '🐋', rarity: 'legendary' }, -]; - -const CATCH_MESSAGES = [ - "You cast your line into the crystal clear waters...", - "You wait patiently as your bobber floats...", - "After a few minutes of waiting, you feel a tug...", - "The water ripples as something takes your bait...", - "You reel in your catch with expert precision...", -]; - -export default { - data: new SlashCommandBuilder() - .setName('fish') - .setDescription('Go fishing to catch fish and earn money'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - const lastFish = userData.lastFish || 0; - const hasFishingRod = userData.inventory["fishing_rod"] || 0; - - if (now < lastFish + FISH_COOLDOWN) { - const remaining = lastFish + FISH_COOLDOWN - now; - const hours = Math.floor(remaining / (1000 * 60 * 60)); - const minutes = Math.floor( - (remaining % (1000 * 60 * 60)) / (1000 * 60), - ); - - throw createError( - "Fishing cooldown active", - ErrorTypes.RATE_LIMIT, - `You're too tired to fish right now. Rest for **${hours}h ${minutes}m** before fishing again.`, - { remaining, cooldownType: 'fish' } - ); - } - - const rand = Math.random(); - let fishCaught; - - if (rand < 0.5) { - - fishCaught = FISH_TYPES.filter(f => f.rarity === 'common')[Math.floor(Math.random() * 3)]; - } else if (rand < 0.75) { - - fishCaught = FISH_TYPES.filter(f => f.rarity === 'uncommon')[Math.floor(Math.random() * 2)]; - } else if (rand < 0.9) { - - fishCaught = FISH_TYPES.filter(f => f.rarity === 'rare')[Math.floor(Math.random() * 2)]; - } else if (rand < 0.98) { - - fishCaught = FISH_TYPES.find(f => f.rarity === 'epic'); - } else { - - fishCaught = FISH_TYPES.find(f => f.rarity === 'legendary'); - } - - const baseEarned = Math.floor( - Math.random() * (BASE_MAX_REWARD - BASE_MIN_REWARD + 1) - ) + BASE_MIN_REWARD; - - let finalEarned = baseEarned; - let multiplierMessage = ""; - - if (hasFishingRod > 0) { - finalEarned = Math.floor(baseEarned * FISHING_ROD_MULTIPLIER); - multiplierMessage = `\n🎣 **Fishing Rod Bonus: +50%**`; - } - - const catchMessage = CATCH_MESSAGES[Math.floor(Math.random() * CATCH_MESSAGES.length)]; - - userData.wallet += finalEarned; - userData.lastFish = now; - - await setEconomyData(client, guildId, userId, userData); - - const rarityColors = { - common: '#95A5A6', - uncommon: '#2ECC71', - rare: '#3498DB', - epic: '#9B59B6', - legendary: '#F1C40F' - }; - - const embed = createEmbed({ - title: 'Fishing Success!', - description: `${catchMessage}\n\nYou caught a **${fishCaught.emoji} ${fishCaught.name}**! You sold it for **$${finalEarned.toLocaleString()}**!${multiplierMessage}`, - color: rarityColors[fishCaught.rarity] - }) - .addFields( - { - name: "New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: "Rarity", - value: fishCaught.rarity.charAt(0).toUpperCase() + fishCaught.rarity.slice(1), - inline: true, - } - ) - .setFooter({ text: `Next fishing trip available in 45 minutes.` }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'fish' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/gamble.js b/src/commands/Economy/gamble.js deleted file mode 100644 index b4acf1745d..0000000000 --- a/src/commands/Economy/gamble.js +++ /dev/null @@ -1,131 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const BASE_WIN_CHANCE = 0.4; -const CLOVER_WIN_BONUS = 0.1; -const CHARM_WIN_BONUS = 0.08; -const PAYOUT_MULTIPLIER = 2.0; -const GAMBLE_COOLDOWN = 5 * 60 * 1000; - -export default { - data: new SlashCommandBuilder() - .setName('gamble') - .setDescription('Gamble your money for a chance to win more') - .addIntegerOption(option => - option - .setName('amount') - .setDescription('Amount of cash to gamble') - .setRequired(true) - .setMinValue(1) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const betAmount = interaction.options.getInteger("amount"); - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - const lastGamble = userData.lastGamble || 0; - let cloverCount = userData.inventory["lucky_clover"] || 0; - let charmCount = userData.inventory["lucky_charm"] || 0; - - if (now < lastGamble + GAMBLE_COOLDOWN) { - const remaining = lastGamble + GAMBLE_COOLDOWN - now; - const minutes = Math.floor(remaining / (1000 * 60)); - const seconds = Math.floor((remaining % (1000 * 60)) / 1000); - - throw createError( - "Gamble cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to cool down before gambling again. Wait **${minutes}m ${seconds}s**.`, - { remaining, cooldownType: 'gamble' } - ); - } - - if (userData.wallet < betAmount) { - throw createError( - "Insufficient cash for gamble", - ErrorTypes.VALIDATION, - `You only have $${userData.wallet.toLocaleString()} cash, but you are trying to bet $${betAmount.toLocaleString()}.`, - { required: betAmount, current: userData.wallet } - ); - } - - let winChance = BASE_WIN_CHANCE; - let cloverMessage = ""; - let usedClover = false; - let usedCharm = false; - - if (cloverCount > 0) { - winChance += CLOVER_WIN_BONUS; - userData.inventory["lucky_clover"] -= 1; - cloverMessage = `\n🍀 **Lucky Clover Consumed:** Your win chance was boosted!`; - usedClover = true; - } - - else if (charmCount > 0) { - winChance += CHARM_WIN_BONUS; - userData.inventory["lucky_charm"] -= 1; - cloverMessage = `\n🍀 **Lucky Charm Used (${charmCount - 1} uses remaining):** Your win chance was boosted!`; - usedCharm = true; - } - - const win = Math.random() < winChance; - let cashChange = 0; - let resultEmbed; - - if (win) { - const amountWon = Math.floor(betAmount * PAYOUT_MULTIPLIER); - // Net change: the bet is replaced by the payout (bet was at stake, not pre-deducted) - cashChange = amountWon - betAmount; - - resultEmbed = successEmbed( - "🎉 You Won!", - `You successfully gambled and turned your **$${betAmount.toLocaleString()}** bet into **$${amountWon.toLocaleString()}**!${cloverMessage}`, - ); - } else { -cashChange = -betAmount; - - resultEmbed = warningEmbed( - "💔 You Lost...", - `The dice rolled against you. You lost your **$${betAmount.toLocaleString()}** bet.`, - ); - } - - userData.wallet = (userData.wallet || 0) + cashChange; -userData.lastGamble = now; - - await setEconomyData(client, guildId, userId, userData); - - const newCash = userData.wallet; - - resultEmbed.addFields({ - name: "New Cash Balance", - value: `$${newCash.toLocaleString()}`, - inline: true, - }); - - if (usedClover) { - resultEmbed.setFooter({ - text: `You have ${userData.inventory["lucky_clover"]} Lucky Clovers left. Win chance was ${Math.round(winChance * 100)}%.`, - }); - } else if (usedCharm) { - resultEmbed.setFooter({ - text: `You have ${userData.inventory["lucky_charm"]} Lucky Charm uses left. Win chance was ${Math.round(winChance * 100)}%.`, - }); - } else { - resultEmbed.setFooter({ - text: `Next gamble available in 5 minutes. Base win chance: ${Math.round(BASE_WIN_CHANCE * 100)}%.`, - }); - } - - await InteractionHelper.safeEditReply(interaction, { embeds: [resultEmbed] }); - }, { command: 'gamble' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/inventory.js b/src/commands/Economy/inventory.js deleted file mode 100644 index b7efcfec80..0000000000 --- a/src/commands/Economy/inventory.js +++ /dev/null @@ -1,70 +0,0 @@ -import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { shopItems } from '../../config/shop/items.js'; -import { getEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const SHOP_ITEMS = shopItems; - -export default { - data: new SlashCommandBuilder() - .setName('inventory') - .setDescription('View your economy inventory'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - - logger.debug(`[ECONOMY] Inventory requested for ${userId}`, { userId, guildId }); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data for inventory", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const inventory = userData.inventory || {}; - - let inventoryDescription = "Your inventory is currently empty."; - - if (Object.keys(inventory).length > 0) { - inventoryDescription = Object.entries(inventory) - .filter( - ([itemId, quantity]) => { - const item = SHOP_ITEMS.find(i => i.id === itemId); - return quantity > 0 && item; - } - ) - .map( - ([itemId, quantity]) => { - const item = SHOP_ITEMS.find(i => i.id === itemId); - return `**${item.name}:** ${quantity}x`; - } - ) - .join("\n"); - } - - logger.info(`[ECONOMY] Inventory retrieved`, { - userId, - guildId, - itemCount: Object.keys(inventory).length - }); - - const embed = createEmbed({ - title: `🎒 ${interaction.user.username}'s Inventory`, - description: inventoryDescription, - }).setThumbnail(interaction.user.displayAvatarURL()); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'inventory' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/mine.js b/src/commands/Economy/mine.js deleted file mode 100644 index fda8ba1dda..0000000000 --- a/src/commands/Economy/mine.js +++ /dev/null @@ -1,93 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const MINE_COOLDOWN = 60 * 60 * 1000; -const BASE_MIN_REWARD = 400; -const BASE_MAX_REWARD = 1200; -const PICKAXE_MULTIPLIER = 1.2; -const DIAMOND_PICKAXE_MULTIPLIER = 2.0; - -const MINE_LOCATIONS = [ - "abandoned gold mine", - "dark, damp cave", - "backyard rock quarry", - "volcanic obsidian vent", - "deep-sea mineral trench", -]; - -export default { - data: new SlashCommandBuilder() - .setName('mine') - .setDescription('Go mining to earn money'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - const lastMine = userData.lastMine || 0; - const hasDiamondPickaxe = userData.inventory["diamond_pickaxe"] || 0; - const hasPickaxe = userData.inventory["pickaxe"] || 0; - - if (now < lastMine + MINE_COOLDOWN) { - const remaining = lastMine + MINE_COOLDOWN - now; - const hours = Math.floor(remaining / (1000 * 60 * 60)); - const minutes = Math.floor( - (remaining % (1000 * 60 * 60)) / (1000 * 60), - ); - - throw createError( - "Mining cooldown active", - ErrorTypes.RATE_LIMIT, - `Your pickaxe is cooling down. Wait for **${hours}h ${minutes}m** before mining again.`, - { remaining, cooldownType: 'mine' } - ); - } - - const baseEarned = - Math.floor( - Math.random() * (BASE_MAX_REWARD - BASE_MIN_REWARD + 1), - ) + BASE_MIN_REWARD; - - let finalEarned = baseEarned; - let multiplierMessage = ""; - - if (hasDiamondPickaxe > 0) { - finalEarned = Math.floor(baseEarned * DIAMOND_PICKAXE_MULTIPLIER); - multiplierMessage = `\n💎 **Diamond Pickaxe Bonus: +100%**`; - } else if (hasPickaxe > 0) { - finalEarned = Math.floor(baseEarned * PICKAXE_MULTIPLIER); - multiplierMessage = `\n⛏️ **Pickaxe Bonus: +20%**`; - } - - const location = - MINE_LOCATIONS[ - Math.floor(Math.random() * MINE_LOCATIONS.length) - ]; - - userData.wallet += finalEarned; -userData.lastMine = now; - - await setEconomyData(client, guildId, userId, userData); - - const embed = successEmbed( - "💰 Mining Expedition Successful!", - `You explored a **${location}** and managed to find minerals worth **$${finalEarned.toLocaleString()}**!${multiplierMessage}`, - ) - .addFields({ - name: "New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }) - .setFooter({ text: `Next mine available in 1 hour.` }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'mine' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/modules/economy_dashboard.js b/src/commands/Economy/modules/economy_dashboard.js deleted file mode 100644 index 04f507ff97..0000000000 --- a/src/commands/Economy/modules/economy_dashboard.js +++ /dev/null @@ -1,527 +0,0 @@ -import { - ActionRowBuilder, - StringSelectMenuBuilder, - StringSelectMenuOptionBuilder, - ModalBuilder, - TextInputBuilder, - TextInputStyle, - UserSelectMenuBuilder, - LabelBuilder, - ButtonBuilder, - ButtonStyle, - MessageFlags, - ComponentType, - EmbedBuilder, -} from 'discord.js'; -import { getColor, BotConfig } from '../../../config/bot.js'; -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -import { successEmbed } from '../../../utils/embeds.js'; -import { logger } from '../../../utils/logger.js'; -import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js'; -import { getEconomyPrefix } from '../../../utils/database.js'; -import { getEconomyData, addMoney, removeMoney, getMaxBankCapacity } from '../../../utils/economy.js'; -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -async function buildDashboardEmbed(guild, client) { - const currencySymbol = BotConfig.economy.currency.symbol; - const currencyName = BotConfig.economy.currency.name; - - let totalInCirculation = 0; - let userCount = 0; - - try { - const economyKeys = await client.db.list(getEconomyPrefix(guild.id)); - - if (economyKeys && economyKeys.length > 0) { - for (const key of economyKeys) { - const userId = key.split(':').pop(); - - const member = await guild.members.fetch(userId).catch(() => null); - if (member?.user?.bot) continue; - - const userData = await client.db.get(key, {}); - if (userData) { - totalInCirculation += (userData.wallet || 0) + (userData.bank || 0); - userCount++; - } - } - } - } catch (error) { - logger.error('Error calculating economy stats:', error); - } - - const avgBalance = userCount > 0 ? Math.floor(totalInCirculation / userCount) : 0; - - return new EmbedBuilder() - .setTitle('💰 Economy Dashboard') - .setDescription(`Manage the economy system for **${guild.name}**.\nSelect an option below to perform an action.`) - .setColor(getColor('economy')) - .addFields( - { name: '💰 Total in Circulation', value: `\`${currencySymbol}${totalInCirculation.toLocaleString()}\``, inline: true }, - { name: '👥 Active Users', value: `\`${userCount.toLocaleString()}\``, inline: true }, - { name: '📊 Average Balance', value: `\`${currencySymbol}${avgBalance.toLocaleString()}\``, inline: true }, - { name: '💱 Currency Symbol', value: `\`${currencySymbol}\``, inline: true }, - { name: '📝 Currency Name', value: `\`${currencyName}\``, inline: true }, - ) - .setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' }) - .setTimestamp(); -} - -function buildSelectMenu(guildId) { - return new StringSelectMenuBuilder() - .setCustomId(`economy_dashboard_${guildId}`) - .setPlaceholder('Select an action...') - .addOptions( - new StringSelectMenuOptionBuilder() - .setLabel('Add Currency') - .setDescription('Add currency to a user\'s wallet or bank') - .setValue('add_currency') - .setEmoji('💰'), - new StringSelectMenuOptionBuilder() - .setLabel('Remove Currency') - .setDescription('Remove currency from a user\'s wallet or bank') - .setValue('remove_currency') - .setEmoji('💸'), - new StringSelectMenuOptionBuilder() - .setLabel('Change Currency Symbol') - .setDescription('Change the currency symbol (e.g., $, €, £)') - .setValue('change_currency') - .setEmoji('💱'), - new StringSelectMenuOptionBuilder() - .setLabel('Change Currency Name') - .setDescription('Change the currency name (e.g., coins, credits)') - .setValue('change_name') - .setEmoji('📝'), - ); -} - -async function refreshDashboard(rootInteraction, guild, client) { - const selectMenu = buildSelectMenu(guild.id); - await InteractionHelper.safeEditReply(rootInteraction, { - embeds: [await buildDashboardEmbed(guild, client)], - components: [ - new ActionRowBuilder().addComponents(selectMenu), - ], - }).catch(() => {}); -} - -async function updateConfigFile(currencySymbol, currencyName) { - try { - const configPath = path.join(__dirname, '../../../config/bot.js'); - let configContent = await fs.readFile(configPath, 'utf-8'); - - configContent = configContent.replace( - /symbol:\s*"[^"]*"/, - `symbol: "${currencySymbol}"` - ); - - configContent = configContent.replace( - /name:\s*"[^"]*",\s*\/\/\s*Currency display name/, - `name: "${currencyName}", // Currency display name` - ); - - configContent = configContent.replace( - /namePlural:\s*"[^"]*",\s*\/\/\s*Plural display name/, - `namePlural: "${currencyName}s", // Plural display name` - ); - - await fs.writeFile(configPath, configContent, 'utf-8'); - logger.info('Config file updated successfully'); - return true; - } catch (error) { - logger.error('Error updating config file:', error); - return false; - } -} - -export default { - prefixOnly: false, - async execute(interaction, config, client) { - try { - const guild = interaction.guild; - const selectMenu = buildSelectMenu(guild.id); - const selectRow = new ActionRowBuilder().addComponents(selectMenu); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [await buildDashboardEmbed(guild, client)], - components: [selectRow], - }); - - const collector = interaction.channel.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - filter: i => - i.user.id === interaction.user.id && i.customId === `economy_dashboard_${guild.id}`, - time: 600_000, - }); - - collector.on('collect', async selectInteraction => { - const selectedOption = selectInteraction.values[0]; - try { - switch (selectedOption) { - case 'add_currency': - await handleAddCurrency(selectInteraction, interaction, guild, client); - break; - case 'remove_currency': - await handleRemoveCurrency(selectInteraction, interaction, guild, client); - break; - case 'change_currency': - await handleChangeCurrency(selectInteraction, interaction, guild); - break; - case 'change_name': - await handleChangeName(selectInteraction, interaction, guild); - break; - } - } catch (error) { - if (error instanceof TitanBotError) { - logger.debug(`Economy dashboard validation error: ${error.message}`); - } else { - logger.error('Unexpected economy dashboard error:', error); - } - - const errorMessage = - error instanceof TitanBotError - ? error.userMessage || 'An error occurred while processing your selection.' - : 'An unexpected error occurred while processing your request.'; - - if (!selectInteraction.replied && !selectInteraction.deferred) { - await selectInteraction.deferUpdate().catch(() => {}); - } - - await replyUserError(selectInteraction, { - type: ErrorTypes.UNKNOWN, - message: errorMessage, - }).catch(() => {}); - } - }); - - collector.on('end', async (collected, reason) => { - if (reason === 'time') { - const timeoutEmbed = new EmbedBuilder() - .setTitle('Dashboard Timed Out') - .setDescription('This dashboard has been closed due to inactivity. Please run the command again to continue.') - .setColor(getColor('error')); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [timeoutEmbed], - components: [], - }).catch(() => {}); - } - }); - } catch (error) { - if (error instanceof TitanBotError) throw error; - logger.error('Unexpected error in economy_dashboard:', error); - throw new TitanBotError( - `Economy dashboard failed: ${error.message}`, - ErrorTypes.UNKNOWN, - 'Failed to open the economy dashboard.', - ); - } - }, -}; - -async function handleAddCurrency(selectInteraction, rootInteraction, guild, client) { - const modal = new ModalBuilder() - .setCustomId(`economy_add_currency_${guild.id}`) - .setTitle('Add Currency'); - - const userSelect = new UserSelectMenuBuilder() - .setCustomId('target_user') - .setPlaceholder('Select a user...') - .setMinValues(1) - .setMaxValues(1) - .setRequired(true); - - const userLabel = new LabelBuilder() - .setLabel('Target User') - .setDescription('User to add currency to') - .setUserSelectMenuComponent(userSelect); - - const amountInput = new TextInputBuilder() - .setCustomId('amount') - .setLabel('Amount to add') - .setStyle(TextInputStyle.Short) - .setPlaceholder('100') - .setMinLength(1) - .setMaxLength(10) - .setRequired(true); - - const typeInput = new TextInputBuilder() - .setCustomId('type') - .setLabel('Type (wallet or bank)') - .setStyle(TextInputStyle.Short) - .setPlaceholder('wallet') - .setMinLength(1) - .setMaxLength(5) - .setRequired(true); - - modal.addLabelComponents(userLabel); - modal.addComponents( - new ActionRowBuilder().addComponents(amountInput), - new ActionRowBuilder().addComponents(typeInput), - ); - - await selectInteraction.showModal(modal); - - const submitted = await selectInteraction - .awaitModalSubmit({ - filter: i => i.customId === `economy_add_currency_${guild.id}` && i.user.id === selectInteraction.user.id, - time: 120_000, - }) - .catch(() => null); - - if (!submitted) return; - - const userId = submitted.fields.getField('target_user').values[0]; - const amount = parseInt(submitted.fields.getTextInputValue('amount').trim(), 10); - const type = submitted.fields.getTextInputValue('type').trim().toLowerCase(); - - if (isNaN(amount) || amount <= 0) { - await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Amount must be a positive number.' }); - return; - } - - if (type !== 'wallet' && type !== 'bank') { - await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Type must be either "wallet" or "bank".' }); - return; - } - - const member = await guild.members.fetch(userId).catch(() => null); - if (!member) { - await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'The specified user is not in this server.' }); - return; - } - - if (member.user.bot) { - await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Bots do not have economy accounts.' }); - return; - } - - const { newBalance } = await addMoney(client, guild.id, userId, amount, type); - - const currencySymbol = BotConfig.economy.currency.symbol; - - await submitted.reply({ - embeds: [successEmbed('Currency Added', `Successfully added ${currencySymbol}${amount.toLocaleString()} to ${member.user.tag}'s ${type}.\n**New Balance:** ${currencySymbol}${newBalance.toLocaleString()}`)], - flags: MessageFlags.Ephemeral, - }); - - logger.info(`[ECONOMY_DASHBOARD] Currency added`, { - adminId: submitted.user.id, - targetUserId: userId, - amount, - type, - newBalance, - }); - - await refreshDashboard(rootInteraction, guild, client); -} - -async function handleRemoveCurrency(selectInteraction, rootInteraction, guild, client) { - const modal = new ModalBuilder() - .setCustomId(`economy_remove_currency_${guild.id}`) - .setTitle('Remove Currency'); - - const userSelect = new UserSelectMenuBuilder() - .setCustomId('target_user') - .setPlaceholder('Select a user...') - .setMinValues(1) - .setMaxValues(1) - .setRequired(true); - - const userLabel = new LabelBuilder() - .setLabel('Target User') - .setDescription('User to remove currency from') - .setUserSelectMenuComponent(userSelect); - - const amountInput = new TextInputBuilder() - .setCustomId('amount') - .setLabel('Amount to remove') - .setStyle(TextInputStyle.Short) - .setPlaceholder('100') - .setMinLength(1) - .setMaxLength(10) - .setRequired(true); - - const typeInput = new TextInputBuilder() - .setCustomId('type') - .setLabel('Type (wallet or bank)') - .setStyle(TextInputStyle.Short) - .setPlaceholder('wallet') - .setMinLength(1) - .setMaxLength(5) - .setRequired(true); - - modal.addLabelComponents(userLabel); - modal.addComponents( - new ActionRowBuilder().addComponents(amountInput), - new ActionRowBuilder().addComponents(typeInput), - ); - - await selectInteraction.showModal(modal); - - const submitted = await selectInteraction - .awaitModalSubmit({ - filter: i => i.customId === `economy_remove_currency_${guild.id}` && i.user.id === selectInteraction.user.id, - time: 120_000, - }) - .catch(() => null); - - if (!submitted) return; - - const userId = submitted.fields.getField('target_user').values[0]; - const amount = parseInt(submitted.fields.getTextInputValue('amount').trim(), 10); - const type = submitted.fields.getTextInputValue('type').trim().toLowerCase(); - - if (isNaN(amount) || amount <= 0) { - await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Amount must be a positive number.' }); - return; - } - - if (type !== 'wallet' && type !== 'bank') { - await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Type must be either "wallet" or "bank".' }); - return; - } - - const member = await guild.members.fetch(userId).catch(() => null); - if (!member) { - await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'The specified user is not in this server.' }); - return; - } - - if (member.user.bot) { - await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Bots do not have economy accounts.' }); - return; - } - - const { newBalance } = await removeMoney(client, guild.id, userId, amount, type); - - const currencySymbol = BotConfig.economy.currency.symbol; - - await submitted.reply({ - embeds: [successEmbed('Currency Removed', `Successfully removed ${currencySymbol}${amount.toLocaleString()} from ${member.user.tag}'s ${type}.\n**New Balance:** ${currencySymbol}${newBalance.toLocaleString()}`)], - flags: MessageFlags.Ephemeral, - }); - - logger.info(`[ECONOMY_DASHBOARD] Currency removed`, { - adminId: submitted.user.id, - targetUserId: userId, - amount, - type, - newBalance, - }); - - await refreshDashboard(rootInteraction, guild, client); -} - -async function handleChangeCurrency(selectInteraction, rootInteraction, guild) { - const modal = new ModalBuilder() - .setCustomId(`economy_change_currency_${guild.id}`) - .setTitle('Change Currency Symbol'); - - const symbolInput = new TextInputBuilder() - .setCustomId('currency_symbol') - .setLabel('New Currency Symbol') - .setStyle(TextInputStyle.Short) - .setValue(BotConfig.economy.currency.symbol) - .setPlaceholder('$') - .setMinLength(1) - .setMaxLength(3) - .setRequired(true); - - modal.addComponents(new ActionRowBuilder().addComponents(symbolInput)); - - await selectInteraction.showModal(modal); - - const submitted = await selectInteraction - .awaitModalSubmit({ - filter: i => i.customId === `economy_change_currency_${guild.id}` && i.user.id === selectInteraction.user.id, - time: 120_000, - }) - .catch(() => null); - - if (!submitted) return; - - const newSymbol = submitted.fields.getTextInputValue('currency_symbol').trim(); - - if (newSymbol.length === 0 || newSymbol.length > 3) { - await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Currency symbol must be 1-3 characters long.' }); - return; - } - - const success = await updateConfigFile(newSymbol, BotConfig.economy.currency.name); - - if (!success) { - await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Could not update the config file. Please check the logs.' }); - return; - } - - await submitted.reply({ - embeds: [successEmbed('Currency Symbol Updated', `Currency symbol changed to **${newSymbol}**.\n\n**Note:** The bot needs to be restarted for changes to take effect.`)], - flags: MessageFlags.Ephemeral, - }); - - logger.info(`[ECONOMY_DASHBOARD] Currency symbol changed`, { - adminId: submitted.user.id, - oldSymbol: BotConfig.economy.currency.symbol, - newSymbol - }); -} - -async function handleChangeName(selectInteraction, rootInteraction, guild) { - const modal = new ModalBuilder() - .setCustomId(`economy_change_name_${guild.id}`) - .setTitle('Change Currency Name'); - - const nameInput = new TextInputBuilder() - .setCustomId('currency_name') - .setLabel('New Currency Name') - .setStyle(TextInputStyle.Short) - .setValue(BotConfig.economy.currency.name) - .setPlaceholder('coins') - .setMinLength(1) - .setMaxLength(20) - .setRequired(true); - - modal.addComponents(new ActionRowBuilder().addComponents(nameInput)); - - await selectInteraction.showModal(modal); - - const submitted = await selectInteraction - .awaitModalSubmit({ - filter: i => i.customId === `economy_change_name_${guild.id}` && i.user.id === selectInteraction.user.id, - time: 120_000, - }) - .catch(() => null); - - if (!submitted) return; - - const newName = submitted.fields.getTextInputValue('currency_name').trim(); - - if (newName.length === 0 || newName.length > 20) { - await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Currency name must be 1-20 characters long.' }); - return; - } - - const success = await updateConfigFile(BotConfig.economy.currency.symbol, newName); - - if (!success) { - await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Could not update the config file. Please check the logs.' }); - return; - } - - await submitted.reply({ - embeds: [successEmbed('Currency Name Updated', `Currency name changed to **${newName}**.\n\n**Note:** The bot needs to be restarted for changes to take effect.`)], - flags: MessageFlags.Ephemeral, - }); - - logger.info(`[ECONOMY_DASHBOARD] Currency name changed`, { - adminId: submitted.user.id, - oldName: BotConfig.economy.currency.name, - newName - }); -} \ No newline at end of file diff --git a/src/commands/Economy/modules/shop_browse.js b/src/commands/Economy/modules/shop_browse.js deleted file mode 100644 index b7adcf40dd..0000000000 --- a/src/commands/Economy/modules/shop_browse.js +++ /dev/null @@ -1,94 +0,0 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, EmbedBuilder, MessageFlags } from 'discord.js'; -import { shopItems } from '../../../config/shop/items.js'; -import { getColor } from '../../../config/bot.js'; -import { logger } from '../../../utils/logger.js'; -import { handleInteractionError } from '../../../utils/errorHandler.js'; - -export default { - async execute(interaction, config, client) { - try { - const TARGET_MAX_PAGES = 3; - const ITEMS_PER_PAGE = Math.max(1, Math.ceil(shopItems.length / TARGET_MAX_PAGES)); - const totalPages = Math.ceil(shopItems.length / ITEMS_PER_PAGE); - let currentPage = 1; - - const createShopEmbed = (page) => { - const startIndex = (page - 1) * ITEMS_PER_PAGE; - const pageItems = shopItems.slice(startIndex, startIndex + ITEMS_PER_PAGE); - const embed = new EmbedBuilder() - .setTitle('Store') - .setColor(getColor('primary')) - .setDescription('Use `/buy item_id: quantity:` to purchase an item.'); - pageItems.forEach(item => { - embed.addFields({ - name: `${item.name} (${item.id})`, - value: `**Type:** ${item.type}\n **Price:** $${item.price.toLocaleString()}\n${item.description}`, - inline: false, - }); - }); - embed.setFooter({ text: `Page ${page}/${totalPages}` }); - return embed; - }; - - const createShopComponents = (page) => { - if (totalPages <= 1) return []; - return [ - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId('shop_prev') - .setLabel('⬅️ Previous') - .setStyle(ButtonStyle.Secondary) - .setDisabled(page === 1), - new ButtonBuilder() - .setCustomId('shop_next') - .setLabel('Next ➡️') - .setStyle(ButtonStyle.Secondary) - .setDisabled(page === totalPages), - ), - ]; - }; - - const message = await interaction.reply({ - embeds: [createShopEmbed(currentPage)], - components: createShopComponents(currentPage), - flags: 0, - }); - - const collector = message.createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 300000, - }); - - collector.on('collect', async (buttonInteraction) => { - if (buttonInteraction.user.id !== interaction.user.id) { - await buttonInteraction.reply({ content: '❌ You cannot use these buttons. Run `/shop` to get your own shop view.', flags: 64 }); - return; - } - const { customId } = buttonInteraction; - if (customId === 'shop_prev' || customId === 'shop_next') { - await buttonInteraction.deferUpdate(); - if (customId === 'shop_prev' && currentPage > 1) currentPage--; - else if (customId === 'shop_next' && currentPage < totalPages) currentPage++; - await buttonInteraction.editReply({ - embeds: [createShopEmbed(currentPage)], - components: createShopComponents(currentPage), - }); - } - }); - - collector.on('end', async () => { - try { - const disabledComponents = createShopComponents(currentPage); - disabledComponents.forEach(row => row.components.forEach(btn => btn.setDisabled(true))); - await message.edit({ components: disabledComponents }); - } catch (error) { - logger.debug('shop_browse: could not disable components on collector end', { - error: error.message, - }); - } - }); - } catch (error) { - await handleInteractionError(interaction, error, { command: 'shop_browse' }); - } - }, -}; \ No newline at end of file diff --git a/src/commands/Economy/modules/shop_config_setrole.js b/src/commands/Economy/modules/shop_config_setrole.js deleted file mode 100644 index e917e9e69a..0000000000 --- a/src/commands/Economy/modules/shop_config_setrole.js +++ /dev/null @@ -1,31 +0,0 @@ -import { PermissionsBitField } from 'discord.js'; -import { successEmbed } from '../../../utils/embeds.js'; -import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js'; -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -import { logger } from '../../../utils/logger.js'; - -import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; -export default { - async execute(interaction, config, client) { - if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need **Manage Server** permissions to set the premium role.' }); - } - - const role = interaction.options.getRole('role'); - const guildId = interaction.guildId; - - try { - const currentConfig = await getGuildConfig(client, guildId); - currentConfig.premiumRoleId = role.id; - await setGuildConfig(client, guildId, currentConfig); - - return InteractionHelper.safeReply(interaction, { - embeds: [successEmbed('Premium Role Set', `The **Premium Shop Role** has been set to ${role.toString()}. Members who purchase the Premium Role item will be granted this role.`)], - ephemeral: true, - }); - } catch (error) { - logger.error('shop_config_setrole error:', error); - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Could not save the guild configuration.' }); - } - }, -}; \ No newline at end of file diff --git a/src/commands/Economy/pay.js b/src/commands/Economy/pay.js deleted file mode 100644 index 5e8e904862..0000000000 --- a/src/commands/Economy/pay.js +++ /dev/null @@ -1,149 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, addMoney, removeMoney, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import EconomyService from '../../services/economyService.js'; - -export default { - data: new SlashCommandBuilder() - .setName('pay') - .setDescription('Pay another user some of your cash') - .addUserOption(option => - option - .setName('user') - .setDescription('User to pay') - .setRequired(true) - ) - .addIntegerOption(option => - option - .setName('amount') - .setDescription('Amount to pay') - .setRequired(true) - .setMinValue(1) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const senderId = interaction.user.id; - const receiver = interaction.options.getUser("user"); - const amount = interaction.options.getInteger("amount"); - const guildId = interaction.guildId; - - logger.debug(`[ECONOMY] Pay command initiated`, { - senderId, - receiverId: receiver.id, - amount, - guildId - }); - - if (receiver.bot) { - throw createError( - "Cannot pay bot", - ErrorTypes.VALIDATION, - "You cannot pay a bot.", - { receiverId: receiver.id, isBot: true } - ); - } - - if (receiver.id === senderId) { - throw createError( - "Cannot pay self", - ErrorTypes.VALIDATION, - "You cannot pay yourself.", - { senderId, receiverId: receiver.id } - ); - } - - if (amount <= 0) { - throw createError( - "Invalid payment amount", - ErrorTypes.VALIDATION, - "Amount must be greater than zero.", - { amount, senderId } - ); - } - - const [senderData, receiverData] = await Promise.all([ - getEconomyData(client, guildId, senderId), - getEconomyData(client, guildId, receiver.id) - ]); - - if (!senderData) { - throw createError( - "Failed to load sender economy data", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId: senderId, guildId } - ); - } - - if (!receiverData) { - throw createError( - "Failed to load receiver economy data", - ErrorTypes.DATABASE, - "Failed to load the receiver's economy data. Please try again later.", - { userId: receiver.id, guildId } - ); - } - - const result = await EconomyService.transferMoney( - client, - guildId, - senderId, - receiver.id, - amount - ); - - const updatedSenderData = await getEconomyData(client, guildId, senderId); - const updatedReceiverData = await getEconomyData(client, guildId, receiver.id); - - const embed = successEmbed( - 'Payment Successful', - `You successfully paid **${receiver.username}** the amount of **$${amount.toLocaleString()}**!` - ) - .addFields( - { - name: "Payment Amount", - value: `$${amount.toLocaleString()}`, - inline: true, - }, - { - name: "Your New Balance", - value: `$${updatedSenderData.wallet.toLocaleString()}`, - inline: true, - }, - ) - .setFooter({ - text: `Paid to ${receiver.tag}`, - iconURL: receiver.displayAvatarURL(), - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - - logger.info(`[ECONOMY] Payment sent successfully`, { - senderId, - receiverId: receiver.id, - amount, - senderBalance: updatedSenderData.wallet, - receiverBalance: updatedReceiverData.wallet - }); - - try { - const receiverEmbed = createEmbed({ - title: "Incoming Payment!", - description: `${interaction.user.username} paid you **$${amount.toLocaleString()}**.` - }).addFields({ - name: "Your New Cash", - value: `$${updatedReceiverData.wallet.toLocaleString()}`, - inline: true, - }); - await receiver.send({ embeds: [receiverEmbed] }); - } catch (e) { - logger.warn(`Could not DM user ${receiver.id}: ${e.message}`); - } - }, { command: 'pay' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/rob.js b/src/commands/Economy/rob.js deleted file mode 100644 index 68f74fbfee..0000000000 --- a/src/commands/Economy/rob.js +++ /dev/null @@ -1,154 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { successEmbed, warningEmbed, buildUserErrorEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { BotConfig } from '../../config/bot.js'; - -const ROB_COOLDOWN = BotConfig.economy?.cooldowns?.rob ?? 4 * 60 * 60 * 1000; -const BASE_ROB_SUCCESS_CHANCE = BotConfig.economy?.robSuccessRate ?? 0.4; -const ROB_PERCENTAGE = 0.15; -const FINE_PERCENTAGE = 0.1; - -export default { - data: new SlashCommandBuilder() - .setName('rob') - .setDescription('Attempt to rob another user (very risky)') - .addUserOption(option => - option - .setName('user') - .setDescription('User to rob') - .setRequired(true) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const robberId = interaction.user.id; - const victimUser = interaction.options.getUser("user"); - const guildId = interaction.guildId; - const now = Date.now(); - - if (robberId === victimUser.id) { - throw createError( - "Cannot rob self", - ErrorTypes.VALIDATION, - "You cannot rob yourself.", - { robberId, victimId: victimUser.id } - ); - } - - if (victimUser.bot) { - throw createError( - "Cannot rob bot", - ErrorTypes.VALIDATION, - "You cannot rob a bot.", - { victimId: victimUser.id, isBot: true } - ); - } - - const robberData = await getEconomyData(client, guildId, robberId); - const victimData = await getEconomyData(client, guildId, victimUser.id); - - if (!robberData || !victimData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load economy data. Please try again later.", - { robberId: !!robberData, victimId: !!victimData, guildId } - ); - } - - const lastRob = robberData.lastRob || 0; - - if (now < lastRob + ROB_COOLDOWN) { - const remaining = lastRob + ROB_COOLDOWN - now; - const hours = Math.floor(remaining / (1000 * 60 * 60)); - const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60)); - - throw createError( - "Robbery cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to lay low. Wait **${hours}h ${minutes}m** before attempting another robbery.`, - { remaining, hours, minutes, cooldownType: 'rob' } - ); - } - - if (victimData.wallet < 500) { - throw createError( - "Victim too poor", - ErrorTypes.VALIDATION, - `${victimUser.username} is too poor. They need at least $500 cash to be worth robbing.`, - { victimWallet: victimData.wallet, required: 500 } - ); - } - - const hasSafe = victimData.inventory["personal_safe"] || 0; - - if (hasSafe > 0) { - robberData.lastRob = now; - await setEconomyData(client, guildId, robberId, robberData); - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - warningEmbed( - 'Robbery Blocked', - `${victimUser.username} was prepared! Your attempt failed because they own a **Personal Safe**. You got away clean but didn't gain anything.` - ) - ], - }); - } - - const isSuccessful = Math.random() < BASE_ROB_SUCCESS_CHANCE; - let resultEmbed; - - if (isSuccessful) { - const amountStolen = Math.floor(victimData.wallet * ROB_PERCENTAGE); - - robberData.wallet = (robberData.wallet || 0) + amountStolen; - victimData.wallet = (victimData.wallet || 0) - amountStolen; - - resultEmbed = successEmbed( - 'Robbery Successful', - `You successfully stole **$${amountStolen.toLocaleString()}** from ${victimUser.username}!` - ); - } else { - const fineAmount = Math.floor((robberData.wallet || 0) * FINE_PERCENTAGE); - - if ((robberData.wallet || 0) < fineAmount) { - robberData.wallet = 0; - } else { - robberData.wallet = (robberData.wallet || 0) - fineAmount; - } - - resultEmbed = buildUserErrorEmbed( - 'unknown', - `You failed the robbery and were caught! You were fined **$${fineAmount.toLocaleString()}** of your own cash.`, - { titleOverride: 'Robbery Failed' } - ); - } - - robberData.lastRob = now; - - await setEconomyData(client, guildId, robberId, robberData); - await setEconomyData(client, guildId, victimUser.id, victimData); - - resultEmbed - .addFields( - { - name: `Your New Cash (${interaction.user.username})`, - value: `$${robberData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: `Victim's New Cash (${victimUser.username})`, - value: `$${victimData.wallet.toLocaleString()}`, - inline: true, - }, - ) - .setFooter({ text: `Next robbery available in ${Math.ceil(ROB_COOLDOWN / (60 * 60 * 1000))} hours.` }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [resultEmbed] }); - }, { command: 'rob' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/shop-config.js b/src/commands/Economy/shop-config.js deleted file mode 100644 index fb3be41dc3..0000000000 --- a/src/commands/Economy/shop-config.js +++ /dev/null @@ -1,28 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import shopConfigSetrole from './modules/shop_config_setrole.js'; - -export default { - slashOnly: true, - data: new SlashCommandBuilder() - .setName('shop-config') - .setDescription('Configure shop settings. (Manage Server required)') - .addSubcommand(subcommand => - subcommand - .setName('setrole') - .setDescription('Set the Discord role granted when the Premium Role shop item is purchased.') - .addRoleOption(option => - option - .setName('role') - .setDescription('The role to grant for Premium Role purchases.') - .setRequired(true), - ), - ), - - async execute(interaction, config, client) { - const subcommand = interaction.options.getSubcommand(); - - if (subcommand === 'setrole') { - return shopConfigSetrole.execute(interaction, config, client); - } - }, -}; diff --git a/src/commands/Economy/shop.js b/src/commands/Economy/shop.js deleted file mode 100644 index 16b2b62d96..0000000000 --- a/src/commands/Economy/shop.js +++ /dev/null @@ -1,13 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import shopBrowse from './modules/shop_browse.js'; - -export default { - slashOnly: true, - data: new SlashCommandBuilder() - .setName('shop') - .setDescription('Browse the economy shop.'), - - async execute(interaction, config, client) { - return shopBrowse.execute(interaction, config, client); - }, -}; diff --git a/src/commands/Economy/slut.js b/src/commands/Economy/slut.js deleted file mode 100644 index 98b83e369e..0000000000 --- a/src/commands/Economy/slut.js +++ /dev/null @@ -1,188 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const SLUT_COOLDOWN = 45 * 60 * 1000; - -const SLUT_ACTIVITIES = [ - { name: "Cam Stream", min: 120, max: 450, risk: 0.2 }, - { name: "Private Dance Session", min: 220, max: 700, risk: 0.25 }, - { name: "After-Hours Club Host", min: 320, max: 900, risk: 0.3 }, - { name: "VIP Companion Booking", min: 550, max: 1400, risk: 0.35 }, - { name: "Exclusive Livestream", min: 850, max: 2200, risk: 0.4 }, -]; - -const POSITIVE_OUTCOMES = [ - "Your stream blew up and tips poured in.", - "A VIP booking paid far above average.", - "Your after-hours shift was packed and profitable.", - "Premium requests came through and your payout jumped.", -]; - -const FINE_OUTCOMES = [ - "Venue security issued a compliance fine.", - "A moderation strike triggered a platform fee.", - "You were flagged and had to pay a penalty.", -]; - -const ROBBED_OUTCOMES = [ - "A fake buyer chargeback wiped part of your earnings.", - "A scam booking cleaned out a chunk of your cash.", - "You got baited by a fraud account and lost money.", -]; - -const LOSS_OUTCOMES = [ - "The set flopped and you had to cover operating costs.", - "You burned budget on prep and made no return.", - "The shift went sideways and left you in the red.", -]; - -function randomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -function randomChoice(items) { - return items[Math.floor(Math.random() * items.length)]; -} - -function resolveOutcome(activity, wallet) { - const successChance = Math.max(0.35, 0.55 - activity.risk * 0.2); - const fineChance = 0.22; - const robbedChance = 0.2; - const roll = Math.random(); - - if (roll < successChance) { - const amount = randomInt(activity.min, activity.max); - return { - type: 'payout', - delta: amount, - message: randomChoice(POSITIVE_OUTCOMES), - title: `${activity.name} - Payout` - }; - } - - const remainingAfterSuccess = roll - successChance; - - if (remainingAfterSuccess < fineChance) { - const maxFine = Math.min(wallet, Math.max(150, Math.floor(activity.max * 0.4))); - const minFine = Math.min(maxFine, Math.max(50, Math.floor(activity.min * 0.2))); - const amount = maxFine > 0 ? randomInt(minFine, maxFine) : 0; - return { - type: 'fine', - delta: -amount, - message: randomChoice(FINE_OUTCOMES), - title: `${activity.name} - Fined` - }; - } - - if (remainingAfterSuccess < fineChance + robbedChance) { - const maxRobbed = Math.min(wallet, Math.max(200, Math.floor(wallet * 0.35))); - const minRobbed = Math.min(maxRobbed, Math.max(75, Math.floor(wallet * 0.1))); - const amount = maxRobbed > 0 ? randomInt(minRobbed, maxRobbed) : 0; - return { - type: 'robbed', - delta: -amount, - message: randomChoice(ROBBED_OUTCOMES), - title: `${activity.name} - Robbed` - }; - } - - const maxLoss = Math.min(wallet, Math.max(100, Math.floor(activity.max * 0.3))); - const minLoss = Math.min(maxLoss, Math.max(40, Math.floor(activity.min * 0.15))); - const amount = maxLoss > 0 ? randomInt(minLoss, maxLoss) : 0; - return { - type: 'loss', - delta: -amount, - message: randomChoice(LOSS_OUTCOMES), - title: `${activity.name} - Loss` - }; -} - -export default { - data: new SlashCommandBuilder() - .setName('slut') - .setDescription('Take a risky provocative job for random payout or loss'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - logger.debug(`[ECONOMY] Slut command started for ${userId}`, { userId, guildId }); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data for slut command", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const lastSlut = userData.lastSlut || 0; - - if (now - lastSlut < SLUT_COOLDOWN) { - const remainingTime = lastSlut + SLUT_COOLDOWN - now; - throw createError( - "Slut cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to wait before you can work again! Try again in **${Math.ceil(remainingTime / 60000)}** minutes.`, - { timeRemaining: remainingTime, cooldownType: 'slut' } - ); - } - - const activity = randomChoice(SLUT_ACTIVITIES); - - const outcome = resolveOutcome(activity, userData.wallet || 0); - - userData.lastSlut = now; - userData.totalSluts = (userData.totalSluts || 0) + 1; - userData.totalSlutEarnings = (userData.totalSlutEarnings || 0) + Math.max(0, outcome.delta); - userData.totalSlutLosses = (userData.totalSlutLosses || 0) + Math.max(0, -outcome.delta); - - if (outcome.type !== 'payout') { - userData.failedSluts = (userData.failedSluts || 0) + 1; - } - - userData.wallet = Math.max(0, (userData.wallet || 0) + outcome.delta); - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Slut activity resolved`, { - userId, - guildId, - activity: activity.name, - outcomeType: outcome.type, - amountDelta: outcome.delta, - newWallet: userData.wallet, - timestamp: new Date().toISOString() - }); - - const amountLabel = `${outcome.delta >= 0 ? '+' : '-'}$${Math.abs(outcome.delta).toLocaleString()}`; - const summaryLines = [ - `${outcome.message}`, - `💸 **Net Result:** ${amountLabel}`, - `💳 **Current Balance:** $${userData.wallet.toLocaleString()}`, - `📊 **Total Sessions:** ${userData.totalSluts}`, - `💵 **Total Earned:** $${(userData.totalSlutEarnings || 0).toLocaleString()}`, - `🧾 **Total Lost:** $${(userData.totalSlutLosses || 0).toLocaleString()}` - ]; - - const embed = createEmbed({ - title: outcome.title, - description: summaryLines.join('\n'), - color: outcome.delta >= 0 ? 'success' : 'error', - timestamp: true - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'slut' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/withdraw.js b/src/commands/Economy/withdraw.js deleted file mode 100644 index bf45af02ad..0000000000 --- a/src/commands/Economy/withdraw.js +++ /dev/null @@ -1,85 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData, getMaxBankCapacity } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; - -import { InteractionHelper } from '../../utils/interactionHelper.js'; -export default { - data: new SlashCommandBuilder() - .setName('withdraw') - .setDescription('Withdraw money from your bank to your wallet') - .addIntegerOption(option => - option - .setName('amount') - .setDescription('Amount to withdraw') - .setRequired(true) - .setMinValue(1) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - await InteractionHelper.safeDefer(interaction); - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const amountInput = interaction.options.getInteger("amount"); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - let withdrawAmount = amountInput; - - if (withdrawAmount <= 0) { - throw createError( - "Invalid withdrawal amount", - ErrorTypes.VALIDATION, - "You must withdraw a positive amount.", - { amount: withdrawAmount, userId } - ); - } - - if (withdrawAmount > userData.bank) { - withdrawAmount = userData.bank; - } - - if (withdrawAmount === 0) { - throw createError( - "Empty bank account", - ErrorTypes.VALIDATION, - "Your bank account is empty.", - { userId, bankBalance: userData.bank } - ); - } - - userData.wallet += withdrawAmount; - userData.bank -= withdrawAmount; - - await setEconomyData(client, guildId, userId, userData); - - const embed = successEmbed( - 'Withdrawal Successful', - `You successfully withdrew **$${withdrawAmount.toLocaleString()}** from your bank.` - ) - .addFields( - { - name: "New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: "New Bank Balance", - value: `$${userData.bank.toLocaleString()}`, - inline: true, - }, - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'withdraw' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/work.js b/src/commands/Economy/work.js deleted file mode 100644 index 775be897ad..0000000000 --- a/src/commands/Economy/work.js +++ /dev/null @@ -1,123 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { botConfig } from '../../config/bot.js'; - -const WORK_COOLDOWN = botConfig.economy?.cooldowns?.work ?? 30 * 60 * 1000; -const MIN_WORK_AMOUNT = botConfig.economy?.workMin ?? 10; -const MAX_WORK_AMOUNT = botConfig.economy?.workMax ?? 100; -const LAPTOP_MULTIPLIER = 1.5; -const WORK_JOBS = [ - "Software Developer", - "Barista", - "Janitor", - "YouTuber", - "Discord Bot Developer", - "Cashier", - "Pizza Delivery Driver", - "Librarian", - "Gardener", - "Data Analyst", -]; - -export default { - data: new SlashCommandBuilder() - .setName('work') - .setDescription('Work to earn some money'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data for work", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - logger.debug(`[ECONOMY] Work command started for ${userId}`, { userId, guildId }); - - const lastWork = userData.lastWork || 0; - const inventory = userData.inventory || {}; - const extraWorkShifts = inventory["extra_work"] || 0; - const hasLaptop = inventory["laptop"] || 0; - - let cooldownActive = now < lastWork + WORK_COOLDOWN; - let usedConsumable = false; - - if (cooldownActive) { - if (extraWorkShifts > 0) { - inventory["extra_work"] = (inventory["extra_work"] || 0) - 1; - usedConsumable = true; - } else { - const remaining = lastWork + WORK_COOLDOWN - now; - throw createError( - "Work cooldown active", - ErrorTypes.RATE_LIMIT, - `You're working too fast! Wait **${Math.floor(remaining / 3600000)}h ${Math.floor((remaining % 3600000) / 60000)}m** before working again.`, - { timeRemaining: remaining, cooldownType: 'work' } - ); - } - } - - let earned = Math.floor(Math.random() * (MAX_WORK_AMOUNT - MIN_WORK_AMOUNT + 1)) + MIN_WORK_AMOUNT; - const job = WORK_JOBS[Math.floor(Math.random() * WORK_JOBS.length)]; - - let multiplierMessage = ""; - if (hasLaptop > 0) { - earned = Math.floor(earned * LAPTOP_MULTIPLIER); - multiplierMessage = "\n💻 **Laptop Bonus:** +50% earnings!"; - } - - userData.wallet = (userData.wallet || 0) + earned; - userData.lastWork = now; - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Work completed`, { - userId, - guildId, - amount: earned, - job, - usedConsumable, - hasLaptop: hasLaptop > 0, - newWallet: userData.wallet, - timestamp: new Date().toISOString() - }); - - const embed = successEmbed( - "💼 Work Complete!", - `You worked as a **${job}** and earned **$${earned.toLocaleString()}**!${multiplierMessage}` - ) - .addFields( - { - name: "New Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: "Next Work", - value: ``, - inline: true, - } - ) - .setFooter({ - text: `Requested by ${interaction.user.tag}`, - iconURL: interaction.user.displayAvatarURL(), - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'work' }) -}; \ No newline at end of file From 83e6e03ec3ca551d04a1c032644ab35d7543a081 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:09:32 +0300 Subject: [PATCH 032/219] Delete src/utils/economy.js --- src/utils/economy.js | 399 ------------------------------------------- 1 file changed, 399 deletions(-) delete mode 100644 src/utils/economy.js diff --git a/src/utils/economy.js b/src/utils/economy.js deleted file mode 100644 index c50e9ab1f3..0000000000 --- a/src/utils/economy.js +++ /dev/null @@ -1,399 +0,0 @@ -// economy.js - -import { getColor, getEconomyKey as getEconomyStorageKey } from './database.js'; -import { BotConfig } from '../config/bot.js'; -import { normalizeEconomyData } from './schemas.js'; -import { logger } from './logger.js'; -import { validateDiscordId, validateNumber } from './validation.js'; -import { DEFAULT_ECONOMY_DATA } from './constants.js'; -import { createError, ErrorTypes, wrapServiceBoundary } from './errorHandler.js'; - -const ECONOMY_CONFIG = BotConfig.economy || {}; -const BASE_BANK_CAPACITY = ECONOMY_CONFIG.baseBankCapacity || 10000; -const BANK_CAPACITY_PER_LEVEL = ECONOMY_CONFIG.bankCapacityPerLevel || 5000; -const DAILY_AMOUNT = ECONOMY_CONFIG.dailyAmount || 100; -const WORK_MIN = ECONOMY_CONFIG.workMin || 10; -const WORK_MAX = ECONOMY_CONFIG.workMax || 100; -const COOLDOWNS = ECONOMY_CONFIG.cooldowns || { -daily: 24 * 60 * 60 * 1000, -work: 60 * 60 * 1000, -crime: 2 * 60 * 60 * 1000, -rob: 4 * 60 * 60 * 1000, -}; - -export function getEconomyKey(guildId, userId) { - const validGuildId = validateDiscordId(guildId, 'guildId'); - const validUserId = validateDiscordId(userId, 'userId'); - - if (!validGuildId || !validUserId) { - throw new Error('Invalid guild ID or user ID'); - } - - return getEconomyStorageKey(validGuildId, validUserId); -} - -export function getMaxBankCapacity(userData) { - if (!userData) return BASE_BANK_CAPACITY; - - const bankLevel = userData.bankLevel || 0; - let capacity = BASE_BANK_CAPACITY + (bankLevel * BANK_CAPACITY_PER_LEVEL); - - const upgrades = userData.upgrades || {}; - const inventory = userData.inventory || {}; - - if (upgrades['bank_upgrade_1']) { - capacity = Math.floor(capacity * 1.5); - } - - const bankNotes = inventory['bank_note'] || 0; - capacity += (bankNotes * 10000); - - return capacity; -} - -export function formatCurrency(amount) { - const currencyName = ECONOMY_CONFIG.currency?.name || 'coins'; - return `${amount.toLocaleString()} ${currencyName}`; -} - -export async function getEconomyData(client, guildId, userId) { - try { - if (!client.db || typeof client.db.get !== 'function') { - throw new Error('Database not available'); - } - - const key = getEconomyKey(guildId, userId); - const data = await client.db.get(key, {}); - const defaults = { - ...DEFAULT_ECONOMY_DATA, - wallet: ECONOMY_CONFIG.startingBalance ?? DEFAULT_ECONOMY_DATA.wallet, - }; - - return normalizeEconomyData(data, defaults); - } catch (error) { - logger.error(`Error getting economy data for user ${userId}`, error); - return normalizeEconomyData({}, DEFAULT_ECONOMY_DATA); - } -} - -export async function setEconomyData(client, guildId, userId, data) { - try { - if (!client.db || typeof client.db.set !== 'function') { - throw new Error('Database not available'); - } - - const key = getEconomyKey(guildId, userId); - const normalized = normalizeEconomyData(data, DEFAULT_ECONOMY_DATA); - await client.db.set(key, normalized); - return true; - } catch (error) { - logger.error(`Error saving economy data for user ${userId}`, error); - return false; - } -} - -export async function updateBalance(client, guildId, userId, options = {}) { - const data = await getEconomyData(client, guildId, userId); - - if (options.wallet !== undefined) { - data.wallet = Math.max(0, (data.wallet || 0) + options.wallet); - } - - if (options.bank !== undefined) { - const maxBank = getMaxBankCapacity(data); - data.bank = Math.min(Math.max(0, (data.bank || 0) + options.bank), maxBank); - } - - if (options.xp !== undefined) { - data.xp = Math.max(0, (data.xp || 0) + options.xp); - - const xpNeeded = Math.floor(5 * Math.pow(data.level || 1, 2) + 50 * (data.level || 1) + 100); - if (data.xp >= xpNeeded) { - data.xp -= xpNeeded; - data.level = (data.level || 1) + 1; - data.leveledUp = true; - } - } - - await setEconomyData(client, guildId, userId, data); - return data; -} - -export function checkCooldown(userData, action) { - const cooldownTime = COOLDOWNS[action] || 0; - const lastUsed = userData[`last${action.charAt(0).toUpperCase() + action.slice(1)}`] || 0; - const now = Date.now(); - const remaining = Math.max(0, (lastUsed + cooldownTime) - now); - - return { - onCooldown: remaining > 0, - remaining, - formatted: formatCooldown(remaining) - }; -} - -function formatCooldown(ms) { - if (ms < 1000) return 'now'; - - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 0) return `${days}d ${hours % 24}h`; - if (hours > 0) return `${hours}h ${minutes % 60}m`; - if (minutes > 0) return `${minutes}m ${seconds % 60}s`; - return `${seconds}s`; -} - -export function getWorkReward() { - const amount = Math.floor(Math.random() * (WORK_MAX - WORK_MIN + 1)) + WORK_MIN; - const jobs = [ - 'worked at a fast food restaurant', - 'worked as a programmer', - 'worked as a construction worker', - 'worked as a doctor', - 'worked as a streamer', - 'worked as a YouTuber', - 'worked as a teacher', - 'worked as a cashier', - 'worked as a delivery driver', - 'worked as a freelancer' - ]; - - const job = jobs[Math.floor(Math.random() * jobs.length)]; - - return { - amount, - job, - message: `You ${job} and earned ${formatCurrency(amount)}!` - }; -} - -export function getCrimeOutcome() { - const outcomes = [ - { - success: true, - amount: Math.floor(Math.random() * 200) + 50, - message: 'You successfully robbed a bank and got away with {amount}!' - }, - { - success: true, - amount: Math.floor(Math.random() * 100) + 20, - message: 'You pickpocketed someone and stole {amount}!' - }, - { - success: true, - amount: Math.floor(Math.random() * 150) + 30, - message: 'You hacked into a bank account and transferred {amount} to yourself!' - }, - { - success: false, - fine: Math.floor(Math.random() * 100) + 50, - message: 'You got caught and had to pay a fine of {fine}!' - }, - { - success: false, - fine: Math.floor(Math.random() * 150) + 50, - message: 'The police caught you! You paid {fine} to get out of jail.' - }, - { - success: false, - fine: 0, - message: 'Your attempt failed, but you managed to escape!' - } - ]; - - return outcomes[Math.floor(Math.random() * outcomes.length)]; -} - -export function getRobOutcome(targetBalance) { - if (targetBalance <= 0) { - return { - success: false, - amount: 0, - message: 'The target has no money to steal!' - }; - } - -const success = Math.random() > 0.4; - - if (success) { - const amount = Math.min( -Math.floor(Math.random() * (targetBalance * 0.3)) + 1, - targetBalance - ); - - return { - success: true, - amount, - message: `You successfully robbed them and got away with {amount}!` - }; - } else { - const fine = Math.floor(Math.random() * 200) + 100; - - return { - success: false, - amount: 0, - fine, - message: `You got caught! You had to pay a fine of {fine}.` - }; - } -} - -export function formatShopItem(item, index) { - return `**${index + 1}.** ${item.emoji} **${item.name}** - ${formatCurrency(item.price)}\n${item.description}\n`; -} - -export const addMoney = wrapServiceBoundary(async function addMoney(client, guildId, userId, amount, type = 'wallet') { - const validAmount = validateNumber(amount, 'amount'); - if (validAmount === null || validAmount <= 0) { - throw createError( - 'Invalid amount', - ErrorTypes.VALIDATION, - 'Amount must be a positive number.', - { guildId, userId, amount, operation: 'addMoney' } - ); - } - - if (type !== 'wallet' && type !== 'bank') { - throw createError( - 'Invalid money type', - ErrorTypes.VALIDATION, - 'Type must be "wallet" or "bank".', - { guildId, userId, type, operation: 'addMoney' } - ); - } - - const userData = await getEconomyData(client, guildId, userId); - - if (type === 'bank') { - const maxBank = getMaxBankCapacity(userData); - if ((userData.bank || 0) + validAmount > maxBank) { - throw createError( - 'Bank capacity exceeded', - ErrorTypes.VALIDATION, - `Bank capacity exceeded. Current: ${userData.bank || 0}, Max: ${maxBank}.`, - { guildId, userId, current: userData.bank || 0, max: maxBank, operation: 'addMoney' } - ); - } - userData.bank = (userData.bank || 0) + validAmount; - } else { - userData.wallet = (userData.wallet || 0) + validAmount; - } - - await setEconomyData(client, guildId, userId, userData); - - return { - newBalance: type === 'bank' ? userData.bank : userData.wallet, - ...(type === 'bank' ? { maxBank: getMaxBankCapacity(userData) } : {}), - }; -}, { - service: 'economy', - operation: 'addMoney', - userMessage: 'Failed to add money. Please try again.', -}); - -export const removeMoney = wrapServiceBoundary(async function removeMoney(client, guildId, userId, amount, type = 'wallet') { - const validAmount = validateNumber(amount, 'amount'); - if (validAmount === null || validAmount <= 0) { - throw createError( - 'Invalid amount', - ErrorTypes.VALIDATION, - 'Amount must be a positive number.', - { guildId, userId, amount, operation: 'removeMoney' } - ); - } - - if (type !== 'wallet' && type !== 'bank') { - throw createError( - 'Invalid money type', - ErrorTypes.VALIDATION, - 'Type must be "wallet" or "bank".', - { guildId, userId, type, operation: 'removeMoney' } - ); - } - - const userData = await getEconomyData(client, guildId, userId); - - if (type === 'bank') { - if ((userData.bank || 0) < validAmount) { - throw createError( - 'Insufficient bank funds', - ErrorTypes.VALIDATION, - `Insufficient funds in bank. You have ${userData.bank || 0}, need ${validAmount}.`, - { guildId, userId, current: userData.bank || 0, required: validAmount, operation: 'removeMoney' } - ); - } - userData.bank = (userData.bank || 0) - validAmount; - } else { - if ((userData.wallet || 0) < validAmount) { - throw createError( - 'Insufficient wallet funds', - ErrorTypes.VALIDATION, - `Insufficient funds in wallet. You have ${userData.wallet || 0}, need ${validAmount}.`, - { guildId, userId, current: userData.wallet || 0, required: validAmount, operation: 'removeMoney' } - ); - } - userData.wallet = (userData.wallet || 0) - validAmount; - } - - await setEconomyData(client, guildId, userId, userData); - - return { - newBalance: type === 'bank' ? userData.bank : userData.wallet, - }; -}, { - service: 'economy', - operation: 'removeMoney', - userMessage: 'Failed to remove money. Please try again.', -}); - -export function getShopInventory() { - return [ - { - id: 'fishing_rod', - name: 'Fishing Rod', - emoji: '🎣', - price: 500, - description: 'Catch fish to sell for profit!', - type: 'tool' - }, - { - id: 'hunting_rifle', - name: 'Hunting Rifle', - emoji: '🔫', - price: 1000, - description: 'Hunt animals for meat and fur!', - type: 'tool' - }, - { - id: 'laptop', - name: 'Laptop', - emoji: '💻', - price: 2000, - description: 'Work as a programmer for higher pay!', - type: 'tool', - workMultiplier: 1.5 - }, - { - id: 'bank_loan', - name: 'Bank Loan', - emoji: '🏦', - price: 5000, - description: 'Increases your bank capacity by 50,000!', - type: 'upgrade', - effect: 'bank_capacity', - value: 50000 - }, - { - id: 'lottery_ticket', - name: 'Lottery Ticket', - emoji: '🎫', - price: 100, - description: 'A chance to win big!', - type: 'consumable', - use: 'gamble' - } - ]; -} \ No newline at end of file From 61e46c500384379187192b7eb5547e5ef3913dfb Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:09:43 +0300 Subject: [PATCH 033/219] Delete src/services/economyService.js --- src/services/economyService.js | 463 --------------------------------- 1 file changed, 463 deletions(-) delete mode 100644 src/services/economyService.js diff --git a/src/services/economyService.js b/src/services/economyService.js deleted file mode 100644 index 9203754280..0000000000 --- a/src/services/economyService.js +++ /dev/null @@ -1,463 +0,0 @@ -// economyService.js - -import { logger } from '../utils/logger.js'; -import { getEconomyData, setEconomyData, getMaxBankCapacity } from '../utils/economy.js'; -import { createError, ErrorTypes } from '../utils/errorHandler.js'; -import { wrapServiceClassMethods } from '../utils/serviceErrorBoundary.js'; - -class EconomyService { - - static DAILY_COOLDOWN = 24 * 60 * 60 * 1000; - static WORK_COOLDOWN = 30 * 60 * 1000; - static GAMBLE_COOLDOWN = 5 * 60 * 1000; - static CRIME_COOLDOWN = 60 * 60 * 1000; - static ROB_COOLDOWN = 4 * 60 * 60 * 1000; - static MINE_COOLDOWN = 60 * 60 * 1000; - static FISH_COOLDOWN = 45 * 60 * 1000; - static BEG_COOLDOWN = 30 * 60 * 1000; - - static DAILY_AMOUNT = 1000; - static MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER; - - static assertSafeBalance(value, context = {}) { - if (!Number.isSafeInteger(value) || value < 0 || value > this.MAX_SAFE_INTEGER) { - throw createError( - "Invalid balance state", - ErrorTypes.VALIDATION, - "Operation would create an invalid account balance.", - { value, ...context } - ); - } - } - - static async claimDaily(client, guildId, userId) { - logger.debug(`[ECONOMY_SERVICE] claimDaily requested`, { userId, guildId }); - - const userData = await getEconomyData(client, guildId, userId); - if (!userData) { - logger.error(`[ECONOMY_SERVICE] Failed to load economy data for daily`); - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const now = Date.now(); - const lastDaily = userData.lastDaily || 0; - const remaining = lastDaily + this.DAILY_COOLDOWN - now; - - if (remaining > 0) { - logger.warn(`[ECONOMY_SERVICE] Daily cooldown active`, { - userId, - timeRemaining: remaining - }); - throw createError( - "Daily cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to wait before claiming daily again. Try again in **${this.formatDuration(remaining)}**.`, - { remaining, cooldownType: 'daily' } - ); - } - - const earned = this.DAILY_AMOUNT; - const nextWallet = (userData.wallet || 0) + earned; - this.assertSafeBalance(nextWallet, { operation: 'claimDaily', userId, guildId }); - userData.wallet = nextWallet; - userData.lastDaily = now; - - try { - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Daily claimed`, { - userId, - guildId, - amount: earned, - newWallet: userData.wallet, - timestamp: new Date().toISOString(), - source: 'claim_daily' - }); - - return { - earned, - newWallet: userData.wallet, - nextClaimTime: new Date(now + this.DAILY_COOLDOWN) - }; - } catch (error) { - logger.error(`[ECONOMY_SERVICE] Failed to save daily claim`, error, { - userId, - guildId, - amount: earned - }); - throw createError( - "Failed to save daily claim", - ErrorTypes.DATABASE, - "Failed to process your daily. Please try again.", - { userId, guildId } - ); - } - } - - static async transferMoney(client, guildId, senderId, receiverId, amount) { - logger.debug(`[ECONOMY_SERVICE] transferMoney requested`, { - senderId, - receiverId, - amount, - guildId - }); - - if (amount <= 0) { - throw createError( - "Invalid transfer amount", - ErrorTypes.VALIDATION, - "Amount must be greater than zero.", - { amount, senderId } - ); - } - - if (senderId === receiverId) { - throw createError( - "Cannot pay self", - ErrorTypes.VALIDATION, - "You cannot pay yourself.", - { senderId, receiverId } - ); - } - - this.validateAmount(amount, { operation: 'transfer', senderId, receiverId }); - - const [senderData, receiverData] = await Promise.all([ - getEconomyData(client, guildId, senderId), - getEconomyData(client, guildId, receiverId) - ]); - - if (!senderData || !receiverData) { - logger.error(`[ECONOMY_SERVICE] Failed to load economy data for transfer`, { - senderLoaded: !!senderData, - receiverLoaded: !!receiverData - }); - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load economy data. Please try again later.", - { senderId, receiverId, guildId } - ); - } - - if (senderData.wallet < amount) { - logger.warn(`[ECONOMY_SERVICE] Insufficient funds for transfer`, { - senderId, - required: amount, - available: senderData.wallet - }); - throw createError( - "Insufficient funds", - ErrorTypes.VALIDATION, - `You only have **$${senderData.wallet.toLocaleString()}** in cash.`, - { required: amount, available: senderData.wallet, senderId } - ); - } - - const walletBefore = senderData.wallet; - const senderNext = (senderData.wallet || 0) - amount; - const receiverNext = (receiverData.wallet || 0) + amount; - - this.assertSafeBalance(senderNext, { operation: 'transfer.sender', senderId, amount }); - this.assertSafeBalance(receiverNext, { operation: 'transfer.receiver', receiverId, amount }); - - senderData.wallet = senderNext; - receiverData.wallet = receiverNext; - - try { - - await setEconomyData(client, guildId, senderId, senderData); - - try { - - await setEconomyData(client, guildId, receiverId, receiverData); - } catch (receiverError) { - - logger.error(`[ECONOMY_CRITICAL] Failed to credit receiver ${receiverId}. Attempting rollback for sender ${senderId}...`, receiverError); - - senderData.wallet = walletBefore; - try { - await setEconomyData(client, guildId, senderId, senderData); - logger.info(`[ECONOMY_ROLLBACK] Successfully rolled back sender ${senderId} after receiver credit failure.`); - } catch (rollbackError) { - logger.error(`[ECONOMY_FATAL] ROLLBACK FAILED for sender ${senderId}! Data is now inconsistent.`, rollbackError); - - } - - throw receiverError; - } - - logger.info(`[ECONOMY_TRANSACTION] Money transferred`, { - type: 'transfer', - senderId, - receiverId, - guildId, - amount, - senderNewBalance: senderData.wallet, - receiverNewBalance: receiverData.wallet, - timestamp: new Date().toISOString() - }); - - return { - senderNewBalance: senderData.wallet, - receiverNewBalance: receiverData.wallet - }; - } catch (error) { - logger.error(`[ECONOMY_SERVICE] Transfer execution failed, DATA MAY BE INCONSISTENT`, error, { - senderId, - receiverId, - amount, - guildId, - senderBefore: walletBefore, - senderAfter: senderData.wallet, - receiverAfter: receiverData.wallet - }); - throw createError( - "Failed to save transfer", - ErrorTypes.DATABASE, - "Failed to process transfer. Please try again.", - { senderId, receiverId, amount } - ); - } - } - - static async addMoney(client, guildId, userId, amount, source = 'unknown') { - if (amount <= 0) { - throw createError( - "Invalid amount", - ErrorTypes.VALIDATION, - "Amount must be positive", - { amount, userId, source } - ); - } - - this.validateAmount(amount, { operation: 'addMoney', userId, source }); - - const userData = await getEconomyData(client, guildId, userId); - const balanceBefore = userData.wallet || 0; - const nextWallet = balanceBefore + amount; - this.assertSafeBalance(nextWallet, { operation: 'addMoney', userId, source, amount }); - userData.wallet = nextWallet; - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Money added`, { - userId, - guildId, - amount, - source, - balanceBefore, - balanceAfter: userData.wallet, - delta: amount, - timestamp: new Date().toISOString() - }); - - return userData; - } - - static async removeMoney(client, guildId, userId, amount, reason = 'unknown') { - if (amount <= 0) { - throw createError( - "Invalid amount", - ErrorTypes.VALIDATION, - "Amount must be positive", - { amount, userId, reason } - ); - } - - this.validateAmount(amount, { operation: 'removeMoney', userId, reason }); - - const userData = await getEconomyData(client, guildId, userId); - const balanceBefore = userData.wallet || 0; - - if (balanceBefore < amount) { - throw createError( - "Insufficient funds", - ErrorTypes.VALIDATION, - `You only have **$${balanceBefore.toLocaleString()}**.`, - { required: amount, available: balanceBefore, reason } - ); - } - - userData.wallet = balanceBefore - amount; - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Money removed`, { - userId, - guildId, - amount, - reason, - balanceBefore, - balanceAfter: userData.wallet, - delta: -amount, - timestamp: new Date().toISOString() - }); - - return userData; - } - - static async depositToBank(client, guildId, userId, amount) { - this.validateAmount(amount, { operation: 'deposit', userId }); - - const userData = await getEconomyData(client, guildId, userId); - const maxBank = getMaxBankCapacity(userData); - - if (userData.wallet < amount) { - throw createError( - "Insufficient cash", - ErrorTypes.VALIDATION, - `You only have **$${userData.wallet.toLocaleString()}** in cash.`, - { required: amount, available: userData.wallet } - ); - } - - const currentBank = userData.bank || 0; - if (currentBank + amount > maxBank) { - throw createError( - "Bank capacity exceeded", - ErrorTypes.VALIDATION, - `Your bank can only hold **$${maxBank.toLocaleString()}**. You would exceed capacity by **$${(currentBank + amount - maxBank).toLocaleString()}**.`, - { capacity: maxBank, current: currentBank, requested: amount } - ); - } - - const nextWallet = userData.wallet - amount; - const nextBank = (userData.bank || 0) + amount; - - this.assertSafeBalance(nextWallet, { operation: 'deposit.wallet', userId, amount }); - this.assertSafeBalance(nextBank, { operation: 'deposit.bank', userId, amount }); - - userData.wallet = nextWallet; - userData.bank = nextBank; - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Money deposited to bank`, { - userId, - guildId, - amount, - walletAfter: userData.wallet, - bankAfter: userData.bank, - timestamp: new Date().toISOString() - }); - - return userData; - } - - static async withdrawFromBank(client, guildId, userId, amount) { - this.validateAmount(amount, { operation: 'withdraw', userId }); - - const userData = await getEconomyData(client, guildId, userId); - const bank = userData.bank || 0; - - if (bank < amount) { - throw createError( - "Insufficient bank balance", - ErrorTypes.VALIDATION, - `You only have **$${bank.toLocaleString()}** in your bank.`, - { required: amount, available: bank } - ); - } - - const nextWallet = (userData.wallet || 0) + amount; - const nextBank = bank - amount; - - this.assertSafeBalance(nextWallet, { operation: 'withdraw.wallet', userId, amount }); - this.assertSafeBalance(nextBank, { operation: 'withdraw.bank', userId, amount }); - - userData.wallet = nextWallet; - userData.bank = nextBank; - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Money withdrawn from bank`, { - userId, - guildId, - amount, - walletAfter: userData.wallet, - bankAfter: userData.bank, - timestamp: new Date().toISOString() - }); - - return userData; - } - - static checkCooldown(userData, action, cooldownMs) { - const lastActionField = `last${action.charAt(0).toUpperCase() + action.slice(1)}`; - const lastTime = userData[lastActionField] || 0; - const now = Date.now(); - const remaining = Math.max(0, lastTime + cooldownMs - now); - - return { - isOnCooldown: remaining > 0, - remaining, - formatted: this.formatDuration(remaining), - nextAvailable: new Date(lastTime + cooldownMs) - }; - } - - static validateAmount(amount, context = {}) { - if (!Number.isInteger(amount)) { - throw createError( - "Invalid amount - not an integer", - ErrorTypes.VALIDATION, - "Amount must be a whole number", - context - ); - } - - if (amount <= 0) { - throw createError( - "Invalid amount - not positive", - ErrorTypes.VALIDATION, - "Amount must be positive", - context - ); - } - - if (amount > this.MAX_SAFE_INTEGER) { - logger.error(`[ECONOMY] Amount exceeds MAX_SAFE_INTEGER`, { amount, context }); - throw createError( - "Amount too large", - ErrorTypes.VALIDATION, - "The amount is too large to process", - context - ); - } - } - - static formatDuration(ms) { - const totalSeconds = Math.floor(ms / 1000); - const hours = Math.floor(totalSeconds / 3600); - const minutes = Math.floor((totalSeconds % 3600) / 60); - const seconds = totalSeconds % 60; - - if (hours > 0) { - return `${hours}h ${minutes}m ${seconds}s`; - } - if (minutes > 0) { - return `${minutes}m ${seconds}s`; - } - return `${seconds}s`; - } - - static formatCooldownDisplay(ms) { - const duration = this.formatDuration(ms); - return `**${duration}**`; - } -} - -wrapServiceClassMethods(EconomyService, (methodName) => ({ - service: 'EconomyService', - operation: methodName, - message: `Economy service operation failed: ${methodName}`, - userMessage: 'An economy operation failed. Please try again in a moment.' -})); - -export default EconomyService; \ No newline at end of file From 811b092262b5641df263ba74ca0f9e759178a059 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:14:57 +0300 Subject: [PATCH 034/219] Add security configuration service --- src/services/security/securityService.js | 145 +++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/services/security/securityService.js diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js new file mode 100644 index 0000000000..bdb5b12338 --- /dev/null +++ b/src/services/security/securityService.js @@ -0,0 +1,145 @@ +import { getFromDb, setInDb } from '../../utils/database.js'; +import { logger } from '../../utils/logger.js'; + +export const SECURITY_DEFAULTS = { + enabled: true, + antiNuke: { + enabled: true, + windowMs: 10000, + thresholds: { + channelDelete: 3, + channelCreate: 5, + roleDelete: 3, + roleCreate: 5, + webhookUpdate: 3, + ban: 5, + kick: 5, + botAdd: 2, + }, + action: 'strip', + lockdown: true, + }, + antiRaid: { + enabled: true, + joins: 8, + windowMs: 10000, + minAccountAgeMs: 24 * 60 * 60 * 1000, + action: 'timeout', + timeoutMs: 10 * 60 * 1000, + lockdown: true, + lockdownMs: 10 * 60 * 1000, + }, + autoMod: { + enabled: true, + spam: { enabled: true, maxMessages: 6, windowMs: 5000 }, + duplicate: { enabled: true, maxRepeats: 3, windowMs: 10000 }, + mentions: { enabled: true, max: 6 }, + caps: { enabled: false, ratio: 0.8, minLength: 12 }, + invites: { enabled: true }, + links: { enabled: false }, + badWords: { enabled: false, words: [] }, + action: 'delete', + }, + escalation: [ + { strike: 1, action: 'warn', durationMs: 0 }, + { strike: 2, action: 'timeout', durationMs: 60 * 1000 }, + { strike: 3, action: 'timeout', durationMs: 10 * 60 * 1000 }, + { strike: 4, action: 'timeout', durationMs: 60 * 60 * 1000 }, + { strike: 5, action: 'kick', durationMs: 0 }, + { strike: 6, action: 'ban', durationMs: 0 }, + ], + whitelist: { users: [], roles: [], bots: [] }, + ignoredChannels: [], + logChannelId: null, +}; + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function deepMerge(base, patch) { + const result = { ...base }; + for (const [key, value] of Object.entries(patch || {})) { + if (value && typeof value === 'object' && !Array.isArray(value) && base[key] && typeof base[key] === 'object' && !Array.isArray(base[key])) { + result[key] = deepMerge(base[key], value); + } else { + result[key] = value; + } + } + return result; +} + +function configKey(guildId) { + return `security:config:${guildId}`; +} + +function strikeKey(guildId, userId) { + return `security:strikes:${guildId}:${userId}`; +} + +export async function getSecurityConfig(client, guildId) { + try { + const stored = await getFromDb(configKey(guildId), null); + return deepMerge(clone(SECURITY_DEFAULTS), stored || {}); + } catch (error) { + logger.error('Failed to load security config', { guildId, error: error.message }); + return clone(SECURITY_DEFAULTS); + } +} + +export async function updateSecurityConfig(client, guildId, patch) { + const current = await getSecurityConfig(client, guildId); + const updated = deepMerge(current, patch); + await setInDb(configKey(guildId), updated); + return updated; +} + +export async function getStrikes(client, guildId, userId) { + const value = await getFromDb(strikeKey(guildId, userId), null); + if (!value || typeof value !== 'object') return { count: 0, updatedAt: 0 }; + return value; +} + +export async function addStrike(client, guildId, userId, reason = 'AutoMod violation') { + const current = await getStrikes(client, guildId, userId); + const next = { count: Number(current.count || 0) + 1, updatedAt: Date.now(), lastReason: reason }; + await setInDb(strikeKey(guildId, userId), next); + return next; +} + +export async function clearStrikes(client, guildId, userId) { + await setInDb(strikeKey(guildId, userId), { count: 0, updatedAt: Date.now() }); +} + +export function isWhitelisted(member, config) { + if (!member) return false; + if (config.whitelist.users.includes(member.id)) return true; + if (member.roles?.cache?.some(role => config.whitelist.roles.includes(role.id))) return true; + if (member.user?.bot && config.whitelist.bots.includes(member.id)) return true; + return false; +} + +export async function sendSecurityLog(client, guild, payload) { + try { + const config = await getSecurityConfig(client, guild.id); + if (!config.logChannelId) return; + const channel = guild.channels.cache.get(config.logChannelId) || await guild.channels.fetch(config.logChannelId).catch(() => null); + if (!channel?.isTextBased()) return; + await channel.send({ embeds: [{ + title: payload.title || 'Security Event', + description: payload.description || 'Security event detected.', + color: payload.color || 0xED4245, + fields: payload.fields || [], + timestamp: new Date().toISOString(), + }] }); + } catch (error) { + logger.warn('Failed to send security log', { guildId: guild.id, error: error.message }); + } +} + +export function getRecentCount(store, key, now, windowMs) { + const list = store.get(key) || []; + const recent = list.filter(timestamp => now - timestamp <= windowMs); + store.set(key, recent); + return recent; +} From fc1144982b0766e98ce6bdcf9025b85977289276 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:06 +0300 Subject: [PATCH 035/219] Add anti-nuke protection --- src/services/security/antiNuke.js | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/services/security/antiNuke.js diff --git a/src/services/security/antiNuke.js b/src/services/security/antiNuke.js new file mode 100644 index 0000000000..8b53e54256 --- /dev/null +++ b/src/services/security/antiNuke.js @@ -0,0 +1,94 @@ +import { AuditLogEvent, PermissionFlagsBits } from 'discord.js'; +import { getSecurityConfig, isWhitelisted, sendSecurityLog, getRecentCount } from './securityService.js'; +import { logger } from '../../utils/logger.js'; + +const counters = new Map(); + +const EVENT_MAP = { + channelDelete: AuditLogEvent.ChannelDelete, + channelCreate: AuditLogEvent.ChannelCreate, + roleDelete: AuditLogEvent.RoleDelete, + roleCreate: AuditLogEvent.RoleCreate, + webhookUpdate: AuditLogEvent.WebhookCreate, + ban: AuditLogEvent.MemberBanAdd, + kick: AuditLogEvent.MemberKick, + botAdd: AuditLogEvent.BotAdd, +}; + +function key(guildId, executorId, type) { + return `${guildId}:${executorId}:${type}`; +} + +async function findExecutor(guild, auditType, targetId) { + const logs = await guild.fetchAuditLogs({ type: auditType, limit: 8 }).catch(() => null); + const entry = logs?.entries?.find(e => { + if (Date.now() - e.createdTimestamp > 15000) return false; + if (!targetId) return true; + return e.target?.id === targetId; + }); + return entry?.executor || null; +} + +async function punishExecutor(guild, executor, config, reason) { + const member = await guild.members.fetch(executor.id).catch(() => null); + if (!member || member.id === guild.ownerId || isWhitelisted(member, config)) return false; + + if (config.antiNuke.action === 'ban' && member.bannable) { + await member.ban({ reason: `Anti-Nuke: ${reason}` }).catch(() => {}); + } else if (config.antiNuke.action === 'kick' && member.kickable) { + await member.kick(`Anti-Nuke: ${reason}`).catch(() => {}); + } else if (member.manageable) { + const removable = member.roles.cache.filter(r => r.id !== guild.id && r.editable && r.permissions.bitfield === 0n ? true : r.editable); + await member.roles.remove(removable, `Anti-Nuke: ${reason}`).catch(() => {}); + } + + if (config.antiNuke.lockdown) { + await guild.channels.cache.forEach(channel => { + if (!channel.isTextBased?.() || !channel.permissionsFor(guild.members.me)?.has(PermissionFlagsBits.SendMessages)) return; + }); + } + + await sendSecurityLog(guild.client, guild, { + title: 'Anti-Nuke Triggered', + description: `**${executor.tag || executor.username}** triggered the Anti-Nuke protection.`, + fields: [{ name: 'Reason', value: reason }, { name: 'Action', value: config.antiNuke.action }], + }); + return true; +} + +export async function handleAntiNuke(guild, type, targetId = null) { + const config = await getSecurityConfig(guild.client, guild.id); + if (!config.enabled || !config.antiNuke.enabled) return; + + const auditType = EVENT_MAP[type]; + const threshold = config.antiNuke.thresholds[type]; + if (!auditType || !threshold) return; + + const executor = await findExecutor(guild, auditType, targetId); + if (!executor) return; + const member = await guild.members.fetch(executor.id).catch(() => null); + if (!member || member.id === guild.ownerId || isWhitelisted(member, config)) return; + + const now = Date.now(); + const recent = getRecentCount(counters, key(guild.id, executor.id, type), now, config.antiNuke.windowMs); + recent.push(now); + counters.set(key(guild.id, executor.id, type), recent); + + if (recent.length >= threshold) { + await punishExecutor(guild, executor, config, `${type} threshold exceeded (${recent.length}/${threshold})`); + counters.delete(key(guild.id, executor.id, type)); + } +} + +export function registerAntiNukeEvent(eventName, type) { + return { + name: eventName, + async execute(eventTarget, client) { + const guild = eventTarget?.guild || eventTarget; + const targetId = eventTarget?.id || null; + if (!guild?.id) return; + try { await handleAntiNuke(guild, type, targetId); } + catch (error) { logger.error(`Anti-Nuke ${type} failed`, { error: error.message, guildId: guild.id }); } + }, + }; +} From 954638505692713e6cdc3f227d68d2f3da423493 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:13 +0300 Subject: [PATCH 036/219] Add anti-raid protection --- src/services/security/antiRaid.js | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/services/security/antiRaid.js diff --git a/src/services/security/antiRaid.js b/src/services/security/antiRaid.js new file mode 100644 index 0000000000..713a3dc4e8 --- /dev/null +++ b/src/services/security/antiRaid.js @@ -0,0 +1,51 @@ +import { PermissionFlagsBits } from 'discord.js'; +import { getSecurityConfig, isWhitelisted, sendSecurityLog } from './securityService.js'; + +const joins = new Map(); +const lockdowns = new Map(); + +export async function handleMemberJoin(member) { + const guild = member.guild; + const client = guild.client; + const config = await getSecurityConfig(client, guild.id); + if (!config.enabled || !config.antiRaid.enabled || isWhitelisted(member, config)) return; + + const now = Date.now(); + const listKey = guild.id; + const list = (joins.get(listKey) || []).filter(t => now - t <= config.antiRaid.windowMs); + list.push(now); + joins.set(listKey, list); + + const accountTooNew = now - member.user.createdTimestamp < config.antiRaid.minAccountAgeMs; + const raidDetected = list.length >= config.antiRaid.joins; + + if (raidDetected || accountTooNew) { + if (config.antiRaid.action === 'kick' && member.kickable) { + await member.kick(`Anti-Raid: ${raidDetected ? 'raid detected' : 'new account'}`).catch(() => {}); + } else if (member.moderatable) { + await member.timeout(config.antiRaid.timeoutMs, `Anti-Raid: ${raidDetected ? 'raid detected' : 'new account'}`).catch(() => {}); + } + + await sendSecurityLog(client, guild, { + title: raidDetected ? 'Raid Detected' : 'New Account Protection', + description: `${member} was flagged by Anti-Raid.`, + fields: [ + { name: 'Account Age', value: ``, inline: true }, + { name: 'Joins in Window', value: String(list.length), inline: true }, + { name: 'Action', value: config.antiRaid.action, inline: true }, + ], + }); + } + + if (raidDetected && config.antiRaid.lockdown && !lockdowns.has(guild.id)) { + lockdowns.set(guild.id, Date.now() + config.antiRaid.lockdownMs); + const me = guild.members.me; + if (me?.permissions.has(PermissionFlagsBits.ManageChannels)) { + for (const channel of guild.channels.cache.values()) { + if (!channel.isTextBased?.() || !channel.permissionOverwrites?.edit) continue; + await channel.permissionOverwrites.edit(guild.roles.everyone, { SendMessages: false }, { reason: 'Anti-Raid lockdown' }).catch(() => {}); + } + } + setTimeout(() => lockdowns.delete(guild.id), config.antiRaid.lockdownMs).unref?.(); + } +} From 4e387c47bb9271dd7ad11b6289ace94c8db064b2 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:23 +0300 Subject: [PATCH 037/219] Add advanced automod engine --- src/services/security/autoMod.js | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/services/security/autoMod.js diff --git a/src/services/security/autoMod.js b/src/services/security/autoMod.js new file mode 100644 index 0000000000..8e3a2ab12f --- /dev/null +++ b/src/services/security/autoMod.js @@ -0,0 +1,86 @@ +import { getSecurityConfig, isWhitelisted, addStrike, sendSecurityLog } from './securityService.js'; + +const userMessages = new Map(); +const duplicates = new Map(); +const inviteRegex = /(discord\.gg|discord(?:app)?\.com\/invite)\/[^\s]+/i; +const urlRegex = /https?:\/\/[^\s]+/i; + +function getState(map, key) { + if (!map.has(key)) map.set(key, []); + return map.get(key); +} + +function detect(message, config) { + const text = message.content || ''; + const lower = text.toLowerCase(); + const reasons = []; + const now = Date.now(); + const key = `${message.guild.id}:${message.author.id}`; + + if (config.autoMod.spam.enabled) { + const list = getState(userMessages, key).filter(t => now - t <= config.autoMod.spam.windowMs); + list.push(now); userMessages.set(key, list); + if (list.length >= config.autoMod.spam.maxMessages) reasons.push('message spam'); + } + + if (config.autoMod.duplicate.enabled && text.trim()) { + const list = getState(duplicates, key).filter(x => now - x.time <= config.autoMod.duplicate.windowMs); + list.push({ text: text.trim().slice(0, 500), time: now }); duplicates.set(key, list); + const repeats = list.filter(x => x.text === text.trim().slice(0, 500)).length; + if (repeats >= config.autoMod.duplicate.maxRepeats) reasons.push('duplicate messages'); + } + + if (config.autoMod.mentions.enabled && message.mentions.users.size + message.mentions.roles.size >= config.autoMod.mentions.max) reasons.push('mention spam'); + if (config.autoMod.invites.enabled && inviteRegex.test(text)) reasons.push('Discord invite link'); + if (config.autoMod.links.enabled && urlRegex.test(text)) reasons.push('external link'); + if (config.autoMod.badWords.enabled && config.autoMod.badWords.words.some(word => lower.includes(String(word).toLowerCase()))) reasons.push('blocked word'); + + if (config.autoMod.caps.enabled) { + const letters = text.match(/[A-Za-z]/g) || []; + const upper = text.match(/[A-Z]/g) || []; + if (letters.length >= config.autoMod.caps.minLength && upper.length / letters.length >= config.autoMod.caps.ratio) reasons.push('excessive caps'); + } + + return [...new Set(reasons)]; +} + +async function executeAction(message, reason, config) { + await message.delete().catch(() => {}); + const strike = await addStrike(message.client, message.guild.id, message.author.id, reason); + const level = Math.min(strike.count, config.escalation.length); + const escalation = config.escalation[level - 1]; + let action = config.autoMod.action; + + if (escalation?.action && escalation.action !== 'warn') action = escalation.action; + + const member = await message.guild.members.fetch(message.author.id).catch(() => null); + if (member && !isWhitelisted(member, config)) { + if (action === 'timeout' && member.moderatable) await member.timeout(escalation?.durationMs || 60000, `AutoMod: ${reason}`).catch(() => {}); + else if (action === 'kick' && member.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); + else if (action === 'ban' && member.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); + } + + await sendSecurityLog(message.client, message.guild, { + title: 'AutoMod Action', + description: `${message.author} triggered AutoMod.`, + fields: [ + { name: 'Reason', value: reason, inline: true }, + { name: 'Strike', value: String(strike.count), inline: true }, + { name: 'Action', value: action, inline: true }, + ], + }); +} + +export async function handleAutoMod(message) { + if (!message.guild || message.author.bot) return false; + const config = await getSecurityConfig(message.client, message.guild.id); + if (!config.enabled || !config.autoMod.enabled || config.ignoredChannels.includes(message.channel.id)) return false; + + const member = message.member; + if (!member || member.id === message.guild.ownerId || isWhitelisted(member, config)) return false; + + const reasons = detect(message, config); + if (!reasons.length) return false; + await executeAction(message, reasons.join(', '), config); + return true; +} From ff9de3460110dc10259c00ddb228113213729f4c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:27 +0300 Subject: [PATCH 038/219] Hook AutoMod into message events --- src/events/securityMessageCreate.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/events/securityMessageCreate.js diff --git a/src/events/securityMessageCreate.js b/src/events/securityMessageCreate.js new file mode 100644 index 0000000000..0d07de21c3 --- /dev/null +++ b/src/events/securityMessageCreate.js @@ -0,0 +1,9 @@ +import { Events } from 'discord.js'; +import { handleAutoMod } from '../services/security/autoMod.js'; + +export default { + name: Events.MessageCreate, + async execute(message) { + await handleAutoMod(message); + }, +}; From 49effb69ae801e45909c4cbbf1109b050085e0b2 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:32 +0300 Subject: [PATCH 039/219] Hook Anti-Raid into member joins --- src/events/securityMemberAdd.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/events/securityMemberAdd.js diff --git a/src/events/securityMemberAdd.js b/src/events/securityMemberAdd.js new file mode 100644 index 0000000000..d557a3b30d --- /dev/null +++ b/src/events/securityMemberAdd.js @@ -0,0 +1,9 @@ +import { Events } from 'discord.js'; +import { handleMemberJoin } from '../services/security/antiRaid.js'; + +export default { + name: Events.GuildMemberAdd, + async execute(member) { + await handleMemberJoin(member); + }, +}; From c60cdaa171fe1d194cd8f366fc2dcd2215c5be14 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:35 +0300 Subject: [PATCH 040/219] Hook Anti-Nuke channel deletion detection --- src/events/securityChannelDelete.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/events/securityChannelDelete.js diff --git a/src/events/securityChannelDelete.js b/src/events/securityChannelDelete.js new file mode 100644 index 0000000000..ce14e2c66f --- /dev/null +++ b/src/events/securityChannelDelete.js @@ -0,0 +1,9 @@ +import { Events } from 'discord.js'; +import { handleAntiNuke } from '../services/security/antiNuke.js'; + +export default { + name: Events.ChannelDelete, + async execute(channel) { + if (channel.guild) await handleAntiNuke(channel.guild, 'channelDelete', channel.id); + }, +}; From 9481eb48562f7cd3ee5b8892cc1dbc985a14d242 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:39 +0300 Subject: [PATCH 041/219] Hook Anti-Nuke channel creation detection --- src/events/securityChannelCreate.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/events/securityChannelCreate.js diff --git a/src/events/securityChannelCreate.js b/src/events/securityChannelCreate.js new file mode 100644 index 0000000000..e6aa5292e3 --- /dev/null +++ b/src/events/securityChannelCreate.js @@ -0,0 +1,9 @@ +import { Events } from 'discord.js'; +import { handleAntiNuke } from '../services/security/antiNuke.js'; + +export default { + name: Events.ChannelCreate, + async execute(channel) { + if (channel.guild) await handleAntiNuke(channel.guild, 'channelCreate', channel.id); + }, +}; From da7e096bd7ea702339df02e400025aa9388f394b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:43 +0300 Subject: [PATCH 042/219] Hook Anti-Nuke role deletion detection --- src/events/securityRoleDelete.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/events/securityRoleDelete.js diff --git a/src/events/securityRoleDelete.js b/src/events/securityRoleDelete.js new file mode 100644 index 0000000000..fa68275221 --- /dev/null +++ b/src/events/securityRoleDelete.js @@ -0,0 +1,9 @@ +import { Events } from 'discord.js'; +import { handleAntiNuke } from '../services/security/antiNuke.js'; + +export default { + name: Events.RoleDelete, + async execute(role) { + if (role.guild) await handleAntiNuke(role.guild, 'roleDelete', role.id); + }, +}; From af61e01f84ac076fea821f21c9836cff1afcb4fe Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:48 +0300 Subject: [PATCH 043/219] Hook Anti-Nuke role creation detection --- src/events/securityRoleCreate.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/events/securityRoleCreate.js diff --git a/src/events/securityRoleCreate.js b/src/events/securityRoleCreate.js new file mode 100644 index 0000000000..67c6e7a035 --- /dev/null +++ b/src/events/securityRoleCreate.js @@ -0,0 +1,9 @@ +import { Events } from 'discord.js'; +import { handleAntiNuke } from '../services/security/antiNuke.js'; + +export default { + name: Events.RoleCreate, + async execute(role) { + if (role.guild) await handleAntiNuke(role.guild, 'roleCreate', role.id); + }, +}; From 37cf2b6e468e8b72b3e40267bd0537d5701b30bc Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:53 +0300 Subject: [PATCH 044/219] Hook Anti-Nuke ban detection --- src/events/securityBanAdd.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/events/securityBanAdd.js diff --git a/src/events/securityBanAdd.js b/src/events/securityBanAdd.js new file mode 100644 index 0000000000..23315ce269 --- /dev/null +++ b/src/events/securityBanAdd.js @@ -0,0 +1,9 @@ +import { Events } from 'discord.js'; +import { handleAntiNuke } from '../services/security/antiNuke.js'; + +export default { + name: Events.GuildBanAdd, + async execute(ban) { + await handleAntiNuke(ban.guild, 'ban', ban.user.id); + }, +}; From 40c797e2708286cf04f9449de4c806ffc914afa0 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:15:57 +0300 Subject: [PATCH 045/219] Hook Anti-Nuke kick detection --- src/events/securityMemberRemove.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/events/securityMemberRemove.js diff --git a/src/events/securityMemberRemove.js b/src/events/securityMemberRemove.js new file mode 100644 index 0000000000..5ef8b9a138 --- /dev/null +++ b/src/events/securityMemberRemove.js @@ -0,0 +1,9 @@ +import { Events } from 'discord.js'; +import { handleAntiNuke } from '../services/security/antiNuke.js'; + +export default { + name: Events.GuildMemberRemove, + async execute(member) { + await handleAntiNuke(member.guild, 'kick', member.id); + }, +}; From 50b84e1e5407c69980407bc13ad3de1a09c138c7 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:16:13 +0300 Subject: [PATCH 046/219] Add security configuration dashboard --- src/services/security/securityDashboard.js | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/services/security/securityDashboard.js diff --git a/src/services/security/securityDashboard.js b/src/services/security/securityDashboard.js new file mode 100644 index 0000000000..1db1c6468a --- /dev/null +++ b/src/services/security/securityDashboard.js @@ -0,0 +1,29 @@ +import { getSecurityConfig, updateSecurityConfig } from './securityService.js'; + +const HTML = `Infinity Security Dashboard

Infinity Security

Anti-Nuke / Anti-Raid / AutoMod / Escalation

Dashboard Token

`; + +export function registerSecurityDashboard(app, client) { + const token = process.env.SECURITY_DASHBOARD_TOKEN; + const authorized = req => Boolean(token && req.headers['x-security-token'] && req.headers['x-security-token'] === token); + + app.get('/security', (req, res) => res.type('html').send(HTML)); + + app.get('/api/security/config', async (req, res) => { + if (!authorized(req)) return res.status(401).json({ error: 'Unauthorized' }); + const guild = client.guilds.cache.first(); + if (!guild) return res.status(503).json({ error: 'Bot is not connected to a guild' }); + res.json(await getSecurityConfig(client, guild.id)); + }); + + app.patch('/api/security/config', async (req, res) => { + if (!authorized(req)) return res.status(401).json({ error: 'Unauthorized' }); + const guild = client.guilds.cache.first(); + if (!guild) return res.status(503).json({ error: 'Bot is not connected to a guild' }); + try { + const updated = await updateSecurityConfig(client, guild.id, req.body || {}); + res.json(updated); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); +} From 34be1a86edf4eb394652a3ab2311d411df135087 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:16:51 +0300 Subject: [PATCH 047/219] Add security dashboard command --- src/commands/Security/security.js | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/commands/Security/security.js diff --git a/src/commands/Security/security.js b/src/commands/Security/security.js new file mode 100644 index 0000000000..91514c87cc --- /dev/null +++ b/src/commands/Security/security.js @@ -0,0 +1,46 @@ +import { SlashCommandBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { getSecurityConfig } from '../../services/security/securityService.js'; + +function dashboard(config) { + return new EmbedBuilder() + .setTitle('🛡️ Infinity Security Dashboard') + .setDescription('Use the buttons below to enable/disable protection and manage security settings.') + .setColor(0x5865F2) + .addFields( + { name: '🛡️ Anti-Nuke', value: config.antiNuke.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '🚨 Anti-Raid', value: config.antiRaid.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '🤖 AutoMod', value: config.autoMod.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '⚡ Escalation', value: `${config.escalation.length} strike levels`, inline: true }, + { name: '👤 Whitelist', value: `${config.whitelist.users.length} users / ${config.whitelist.roles.length} roles`, inline: true }, + { name: '📋 Log Channel', value: config.logChannelId ? `<#${config.logChannelId}>` : 'Not configured', inline: true }, + ) + .setFooter({ text: 'Infinity Security • ManageGuild required' }); +} + +export default { + category: 'Security', + slashOnly: true, + data: new SlashCommandBuilder() + .setName('security') + .setDescription('Open the server security dashboard') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild.toString()), + async execute(interaction, config, client) { + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { + return interaction.reply({ content: 'You need Manage Server permission.', ephemeral: true }); + } + + const security = await getSecurityConfig(client, interaction.guildId); + const row1 = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`security_toggle:nuke:${interaction.user.id}`).setLabel('Anti-Nuke').setStyle(security.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), + new ButtonBuilder().setCustomId(`security_toggle:raid:${interaction.user.id}`).setLabel('Anti-Raid').setStyle(security.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), + new ButtonBuilder().setCustomId(`security_toggle:automod:${interaction.user.id}`).setLabel('AutoMod').setStyle(security.autoMod.enabled ? ButtonStyle.Success : ButtonStyle.Danger), + new ButtonBuilder().setCustomId(`security_refresh:${interaction.user.id}`).setLabel('Refresh').setStyle(ButtonStyle.Secondary), + ); + const row2 = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`security_whitelist:${interaction.user.id}`).setLabel('Whitelist').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId(`security_logging:${interaction.user.id}`).setLabel('Logging').setStyle(ButtonStyle.Primary), + ); + + await interaction.reply({ embeds: [dashboard(security)], components: [row1, row2], ephemeral: true }); + }, +}; From 06f6f9201eb59a37f1ccaca6569b03213c1083f1 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:17:08 +0300 Subject: [PATCH 048/219] Add security dashboard interaction handlers --- src/handlers/securityHandlers.js | 102 +++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/handlers/securityHandlers.js diff --git a/src/handlers/securityHandlers.js b/src/handlers/securityHandlers.js new file mode 100644 index 0000000000..52cd4c59d7 --- /dev/null +++ b/src/handlers/securityHandlers.js @@ -0,0 +1,102 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; + +function dashboard(config) { + return new EmbedBuilder().setTitle('🛡️ Infinity Security Dashboard').setColor(0x5865F2).addFields( + { name: '🛡️ Anti-Nuke', value: config.antiNuke.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '🚨 Anti-Raid', value: config.antiRaid.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '🤖 AutoMod', value: config.autoMod.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '⚡ Escalation', value: `${config.escalation.length} strike levels`, inline: true }, + { name: '👤 Whitelist', value: `${config.whitelist.users.length} users / ${config.whitelist.roles.length} roles`, inline: true }, + { name: '📋 Log Channel', value: config.logChannelId ? `<#${config.logChannelId}>` : 'Not configured', inline: true }, + ); +} + +function controls(config, userId) { + return [ + new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`security_toggle:nuke:${userId}`).setLabel('Anti-Nuke').setStyle(config.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), + new ButtonBuilder().setCustomId(`security_toggle:raid:${userId}`).setLabel('Anti-Raid').setStyle(config.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), + new ButtonBuilder().setCustomId(`security_toggle:automod:${userId}`).setLabel('AutoMod').setStyle(config.autoMod.enabled ? ButtonStyle.Success : ButtonStyle.Danger), + new ButtonBuilder().setCustomId(`security_refresh:${userId}`).setLabel('Refresh').setStyle(ButtonStyle.Secondary), + ), + new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`security_whitelist:${userId}`).setLabel('Whitelist').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId(`security_logging:${userId}`).setLabel('Logging').setStyle(ButtonStyle.Primary), + ), + ]; +} + +function ownerOnly(interaction) { + const parts = interaction.customId.split(':'); + return parts.at(-1) === interaction.user.id; +} + +const securityToggleHandler = { + name: 'security_toggle', + async execute(interaction, client) { + if (!ownerOnly(interaction)) return interaction.reply({ content: 'This dashboard belongs to another moderator.', ephemeral: true }); + const [, type] = interaction.customId.split(':'); + const key = type === 'nuke' ? 'antiNuke.enabled' : type === 'raid' ? 'antiRaid.enabled' : 'autoMod.enabled'; + const current = await getSecurityConfig(client, interaction.guildId); + const patch = type === 'nuke' ? { antiNuke: { enabled: !current.antiNuke.enabled } } : type === 'raid' ? { antiRaid: { enabled: !current.antiRaid.enabled } } : { autoMod: { enabled: !current.autoMod.enabled } }; + const updated = await updateSecurityConfig(client, interaction.guildId, patch); + await interaction.update({ embeds: [dashboard(updated)], components: controls(updated, interaction.user.id) }); + }, +}; + +const securityRefreshHandler = { + name: 'security_refresh', + async execute(interaction, client) { + if (!ownerOnly(interaction)) return interaction.reply({ content: 'This dashboard belongs to another moderator.', ephemeral: true }); + const config = await getSecurityConfig(client, interaction.guildId); + await interaction.update({ embeds: [dashboard(config)], components: controls(config, interaction.user.id) }); + }, +}; + +const securityWhitelistHandler = { + name: 'security_whitelist', + async execute(interaction) { + if (!ownerOnly(interaction)) return interaction.reply({ content: 'This dashboard belongs to another moderator.', ephemeral: true }); + const modal = new ModalBuilder().setCustomId(`security_whitelist_modal:${interaction.user.id}`).setTitle('Security Whitelist'); + modal.addComponents( + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('users').setLabel('User IDs (space/comma separated)').setStyle(TextInputStyle.Paragraph).setRequired(false)), + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('roles').setLabel('Role IDs (space/comma separated)').setStyle(TextInputStyle.Paragraph).setRequired(false)), + ); + await interaction.showModal(modal); + }, +}; + +const securityLoggingHandler = { + name: 'security_logging', + async execute(interaction) { + if (!ownerOnly(interaction)) return interaction.reply({ content: 'This dashboard belongs to another moderator.', ephemeral: true }); + const modal = new ModalBuilder().setCustomId(`security_logging_modal:${interaction.user.id}`).setTitle('Security Logging'); + modal.addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('channel').setLabel('Security log channel ID').setPlaceholder('123456789012345678').setStyle(TextInputStyle.Short).setRequired(false))); + await interaction.showModal(modal); + }, +}; + +const securityWhitelistModalHandler = { + name: 'security_whitelist_modal', + async execute(interaction, client) { + if (!ownerOnly(interaction)) return interaction.reply({ content: 'Unauthorized.', ephemeral: true }); + const users = interaction.fields.getTextInputValue('users').split(/[\s,]+/).filter(Boolean); + const roles = interaction.fields.getTextInputValue('roles').split(/[\s,]+/).filter(Boolean); + const updated = await updateSecurityConfig(client, interaction.guildId, { whitelist: { users, roles } }); + await interaction.reply({ embeds: [dashboard(updated)], components: controls(updated, interaction.user.id), ephemeral: true }); + }, +}; + +const securityLoggingModalHandler = { + name: 'security_logging_modal', + async execute(interaction, client) { + if (!ownerOnly(interaction)) return interaction.reply({ content: 'Unauthorized.', ephemeral: true }); + const channel = interaction.fields.getTextInputValue('channel').trim(); + const updated = await updateSecurityConfig(client, interaction.guildId, { logChannelId: channel || null }); + await interaction.reply({ embeds: [dashboard(updated)], components: controls(updated, interaction.user.id), ephemeral: true }); + }, +}; + +export const securityButtonHandlers = [securityToggleHandler, securityRefreshHandler, securityWhitelistHandler, securityLoggingHandler]; +export const securityModalHandlers = [securityWhitelistModalHandler, securityLoggingModalHandler]; From 8f4f1fd68b4977373c13976c06f7691514d9360e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:17:12 +0300 Subject: [PATCH 049/219] Register security dashboard buttons --- src/interactions/buttons/security/security.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/interactions/buttons/security/security.js diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js new file mode 100644 index 0000000000..cf625b44f4 --- /dev/null +++ b/src/interactions/buttons/security/security.js @@ -0,0 +1,2 @@ +import { securityButtonHandlers } from '../../../handlers/securityHandlers.js'; +export default securityButtonHandlers; From 96fffa50ccc1bb21784ba722520959ea0a9d4d84 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:17:15 +0300 Subject: [PATCH 050/219] Register security dashboard modals --- src/interactions/modals/security/security.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/interactions/modals/security/security.js diff --git a/src/interactions/modals/security/security.js b/src/interactions/modals/security/security.js new file mode 100644 index 0000000000..b45cf446db --- /dev/null +++ b/src/interactions/modals/security/security.js @@ -0,0 +1,2 @@ +import { securityModalHandlers } from '../../../handlers/securityHandlers.js'; +export default securityModalHandlers; From d4a61bad2431441c201284e8cf2d5afdb640ae47 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:17:25 +0300 Subject: [PATCH 051/219] Fix Anti-Nuke role stripping --- src/services/security/antiNuke.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/services/security/antiNuke.js b/src/services/security/antiNuke.js index 8b53e54256..68f4b96c9a 100644 --- a/src/services/security/antiNuke.js +++ b/src/services/security/antiNuke.js @@ -1,4 +1,4 @@ -import { AuditLogEvent, PermissionFlagsBits } from 'discord.js'; +import { AuditLogEvent } from 'discord.js'; import { getSecurityConfig, isWhitelisted, sendSecurityLog, getRecentCount } from './securityService.js'; import { logger } from '../../utils/logger.js'; @@ -38,20 +38,17 @@ async function punishExecutor(guild, executor, config, reason) { } else if (config.antiNuke.action === 'kick' && member.kickable) { await member.kick(`Anti-Nuke: ${reason}`).catch(() => {}); } else if (member.manageable) { - const removable = member.roles.cache.filter(r => r.id !== guild.id && r.editable && r.permissions.bitfield === 0n ? true : r.editable); + const removable = member.roles.cache.filter(role => role.id !== guild.id && role.editable); await member.roles.remove(removable, `Anti-Nuke: ${reason}`).catch(() => {}); } - if (config.antiNuke.lockdown) { - await guild.channels.cache.forEach(channel => { - if (!channel.isTextBased?.() || !channel.permissionsFor(guild.members.me)?.has(PermissionFlagsBits.SendMessages)) return; - }); - } - await sendSecurityLog(guild.client, guild, { title: 'Anti-Nuke Triggered', description: `**${executor.tag || executor.username}** triggered the Anti-Nuke protection.`, - fields: [{ name: 'Reason', value: reason }, { name: 'Action', value: config.antiNuke.action }], + fields: [ + { name: 'Reason', value: reason }, + { name: 'Action', value: config.antiNuke.action }, + ], }); return true; } @@ -70,25 +67,29 @@ export async function handleAntiNuke(guild, type, targetId = null) { if (!member || member.id === guild.ownerId || isWhitelisted(member, config)) return; const now = Date.now(); - const recent = getRecentCount(counters, key(guild.id, executor.id, type), now, config.antiNuke.windowMs); + const counterKey = key(guild.id, executor.id, type); + const recent = getRecentCount(counters, counterKey, now, config.antiNuke.windowMs); recent.push(now); - counters.set(key(guild.id, executor.id, type), recent); + counters.set(counterKey, recent); if (recent.length >= threshold) { await punishExecutor(guild, executor, config, `${type} threshold exceeded (${recent.length}/${threshold})`); - counters.delete(key(guild.id, executor.id, type)); + counters.delete(counterKey); } } export function registerAntiNukeEvent(eventName, type) { return { name: eventName, - async execute(eventTarget, client) { + async execute(eventTarget) { const guild = eventTarget?.guild || eventTarget; const targetId = eventTarget?.id || null; if (!guild?.id) return; - try { await handleAntiNuke(guild, type, targetId); } - catch (error) { logger.error(`Anti-Nuke ${type} failed`, { error: error.message, guildId: guild.id }); } + try { + await handleAntiNuke(guild, type, targetId); + } catch (error) { + logger.error(`Anti-Nuke ${type} failed`, { error: error.message, guildId: guild.id }); + } }, }; } From ae3e57f9a6987615587d45ac5e4b60aa40922957 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:41:05 +0300 Subject: [PATCH 052/219] Harden security configuration and strike decay --- src/services/security/securityService.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js index bdb5b12338..70885c8d11 100644 --- a/src/services/security/securityService.js +++ b/src/services/security/securityService.js @@ -11,7 +11,9 @@ export const SECURITY_DEFAULTS = { channelCreate: 5, roleDelete: 3, roleCreate: 5, + roleUpdate: 3, webhookUpdate: 3, + webhookDelete: 3, ban: 5, kick: 5, botAdd: 2, @@ -48,6 +50,7 @@ export const SECURITY_DEFAULTS = { { strike: 5, action: 'kick', durationMs: 0 }, { strike: 6, action: 'ban', durationMs: 0 }, ], + strikeDecayMs: 24 * 60 * 60 * 1000, whitelist: { users: [], roles: [], bots: [] }, ignoredChannels: [], logChannelId: null, @@ -101,8 +104,15 @@ export async function getStrikes(client, guildId, userId) { } export async function addStrike(client, guildId, userId, reason = 'AutoMod violation') { + const config = await getSecurityConfig(client, guildId); const current = await getStrikes(client, guildId, userId); - const next = { count: Number(current.count || 0) + 1, updatedAt: Date.now(), lastReason: reason }; + const now = Date.now(); + const expired = current.updatedAt && now - current.updatedAt > Number(config.strikeDecayMs || 0); + const next = { + count: (expired ? 0 : Number(current.count || 0)) + 1, + updatedAt: now, + lastReason: reason, + }; await setInDb(strikeKey(guildId, userId), next); return next; } @@ -113,9 +123,9 @@ export async function clearStrikes(client, guildId, userId) { export function isWhitelisted(member, config) { if (!member) return false; - if (config.whitelist.users.includes(member.id)) return true; - if (member.roles?.cache?.some(role => config.whitelist.roles.includes(role.id))) return true; - if (member.user?.bot && config.whitelist.bots.includes(member.id)) return true; + if (config.whitelist?.users?.includes(member.id)) return true; + if (member.roles?.cache?.some(role => config.whitelist?.roles?.includes(role.id))) return true; + if (member.user?.bot && config.whitelist?.bots?.includes(member.id)) return true; return false; } From bf34e34d634bdd61c32733043c25c3f8cfe247e5 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:41:13 +0300 Subject: [PATCH 053/219] Fix Anti-Raid lockdown restoration and state handling --- src/services/security/antiRaid.js | 108 ++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 13 deletions(-) diff --git a/src/services/security/antiRaid.js b/src/services/security/antiRaid.js index 713a3dc4e8..476e43a88a 100644 --- a/src/services/security/antiRaid.js +++ b/src/services/security/antiRaid.js @@ -4,6 +4,80 @@ import { getSecurityConfig, isWhitelisted, sendSecurityLog } from './securitySer const joins = new Map(); const lockdowns = new Map(); +async function restoreLockdown(guild, state) { + if (!state) return; + + for (const [channelId, previous] of state.channels) { + const channel = guild.channels.cache.get(channelId) || await guild.channels.fetch(channelId).catch(() => null); + if (!channel?.permissionOverwrites?.edit) continue; + + const everyoneId = guild.roles.everyone.id; + const current = channel.permissionOverwrites.cache.get(everyoneId); + + if (!previous) { + if (current) await current.delete('Anti-Raid lockdown ended').catch(() => {}); + continue; + } + + const sendMessages = previous.sendMessages === null + ? null + : previous.sendMessages; + + await channel.permissionOverwrites.edit( + guild.roles.everyone, + { SendMessages: sendMessages }, + { reason: 'Anti-Raid lockdown ended' }, + ).catch(() => {}); + } +} + +async function startLockdown(guild, config) { + if (lockdowns.has(guild.id)) return; + + const me = guild.members.me; + if (!me?.permissions.has(PermissionFlagsBits.ManageChannels)) return; + + const state = { + expiresAt: Date.now() + config.antiRaid.lockdownMs, + channels: new Map(), + }; + + for (const channel of guild.channels.cache.values()) { + if (!channel.permissionOverwrites?.edit || channel.isThread?.()) continue; + + const everyoneOverwrite = channel.permissionOverwrites.cache.get(guild.roles.everyone.id); + state.channels.set(channel.id, { + sendMessages: everyoneOverwrite?.deny?.has(PermissionFlagsBits.SendMessages) + ? false + : everyoneOverwrite?.allow?.has(PermissionFlagsBits.SendMessages) + ? true + : null, + }); + + await channel.permissionOverwrites.edit( + guild.roles.everyone, + { SendMessages: false }, + { reason: 'Anti-Raid lockdown' }, + ).catch(() => {}); + } + + lockdowns.set(guild.id, state); + + const timer = setTimeout(async () => { + const current = lockdowns.get(guild.id); + if (current !== state) return; + lockdowns.delete(guild.id); + await restoreLockdown(guild, state); + await sendSecurityLog(guild.client, guild, { + title: 'Anti-Raid Lockdown Ended', + description: 'The temporary raid lockdown has ended and previous channel permissions were restored.', + color: 0x57F287, + }); + }, Math.max(1000, config.antiRaid.lockdownMs)); + + timer.unref?.(); +} + export async function handleMemberJoin(member) { const guild = member.guild; const client = guild.client; @@ -18,12 +92,15 @@ export async function handleMemberJoin(member) { const accountTooNew = now - member.user.createdTimestamp < config.antiRaid.minAccountAgeMs; const raidDetected = list.length >= config.antiRaid.joins; + const flagged = raidDetected || accountTooNew; + + if (flagged) { + const reason = raidDetected ? 'raid detected' : 'new account'; - if (raidDetected || accountTooNew) { if (config.antiRaid.action === 'kick' && member.kickable) { - await member.kick(`Anti-Raid: ${raidDetected ? 'raid detected' : 'new account'}`).catch(() => {}); + await member.kick(`Anti-Raid: ${reason}`).catch(() => {}); } else if (member.moderatable) { - await member.timeout(config.antiRaid.timeoutMs, `Anti-Raid: ${raidDetected ? 'raid detected' : 'new account'}`).catch(() => {}); + await member.timeout(config.antiRaid.timeoutMs, `Anti-Raid: ${reason}`).catch(() => {}); } await sendSecurityLog(client, guild, { @@ -37,15 +114,20 @@ export async function handleMemberJoin(member) { }); } - if (raidDetected && config.antiRaid.lockdown && !lockdowns.has(guild.id)) { - lockdowns.set(guild.id, Date.now() + config.antiRaid.lockdownMs); - const me = guild.members.me; - if (me?.permissions.has(PermissionFlagsBits.ManageChannels)) { - for (const channel of guild.channels.cache.values()) { - if (!channel.isTextBased?.() || !channel.permissionOverwrites?.edit) continue; - await channel.permissionOverwrites.edit(guild.roles.everyone, { SendMessages: false }, { reason: 'Anti-Raid lockdown' }).catch(() => {}); - } - } - setTimeout(() => lockdowns.delete(guild.id), config.antiRaid.lockdownMs).unref?.(); + if (raidDetected && config.antiRaid.lockdown) { + await startLockdown(guild, config); } } + +export async function clearRaidLockdown(guild) { + const state = lockdowns.get(guild.id); + if (!state) return false; + lockdowns.delete(guild.id); + await restoreLockdown(guild, state); + return true; +} + +export function isRaidLockdownActive(guildId) { + const state = lockdowns.get(guildId); + return Boolean(state && state.expiresAt > Date.now()); +} From c7e51adce8bd10bbc44ce067cd1ea1a9d793d16d Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:41:21 +0300 Subject: [PATCH 054/219] Harden Anti-Nuke audit detection and dangerous-role stripping --- src/services/security/antiNuke.js | 81 ++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/src/services/security/antiNuke.js b/src/services/security/antiNuke.js index 68f4b96c9a..081bc2837d 100644 --- a/src/services/security/antiNuke.js +++ b/src/services/security/antiNuke.js @@ -1,4 +1,4 @@ -import { AuditLogEvent } from 'discord.js'; +import { AuditLogEvent, PermissionFlagsBits } from 'discord.js'; import { getSecurityConfig, isWhitelisted, sendSecurityLog, getRecentCount } from './securityService.js'; import { logger } from '../../utils/logger.js'; @@ -9,7 +9,9 @@ const EVENT_MAP = { channelCreate: AuditLogEvent.ChannelCreate, roleDelete: AuditLogEvent.RoleDelete, roleCreate: AuditLogEvent.RoleCreate, - webhookUpdate: AuditLogEvent.WebhookCreate, + roleUpdate: AuditLogEvent.RoleUpdate, + webhookUpdate: AuditLogEvent.WebhookUpdate, + webhookDelete: AuditLogEvent.WebhookDelete, ban: AuditLogEvent.MemberBanAdd, kick: AuditLogEvent.MemberKick, botAdd: AuditLogEvent.BotAdd, @@ -20,34 +22,72 @@ function key(guildId, executorId, type) { } async function findExecutor(guild, auditType, targetId) { - const logs = await guild.fetchAuditLogs({ type: auditType, limit: 8 }).catch(() => null); - const entry = logs?.entries?.find(e => { - if (Date.now() - e.createdTimestamp > 15000) return false; + const logs = await guild.fetchAuditLogs({ type: auditType, limit: 10 }).catch(() => null); + const entry = logs?.entries?.find(entry => { + if (Date.now() - entry.createdTimestamp > 15000) return false; if (!targetId) return true; - return e.target?.id === targetId; + return entry.target?.id === targetId || entry.targetId === targetId; }); return entry?.executor || null; } -async function punishExecutor(guild, executor, config, reason) { +async function stripDangerousRoles(member, reason) { + if (!member.manageable) return false; + + const removable = member.roles.cache.filter(role => { + if (role.id === member.guild.id || !role.editable) return false; + return role.permissions.has([ + PermissionFlagsBits.Administrator, + PermissionFlagsBits.ManageGuild, + PermissionFlagsBits.ManageChannels, + PermissionFlagsBits.ManageRoles, + PermissionFlagsBits.BanMembers, + PermissionFlagsBits.KickMembers, + PermissionFlagsBits.ManageWebhooks, + ]); + }); + + if (!removable.size) return false; + await member.roles.remove(removable, `Anti-Nuke: ${reason}`).catch(() => {}); + return true; +} + +async function punishExecutor(guild, executor, config, reason, targetId) { const member = await guild.members.fetch(executor.id).catch(() => null); if (!member || member.id === guild.ownerId || isWhitelisted(member, config)) return false; + let actionTaken = config.antiNuke.action; + if (config.antiNuke.action === 'ban' && member.bannable) { await member.ban({ reason: `Anti-Nuke: ${reason}` }).catch(() => {}); } else if (config.antiNuke.action === 'kick' && member.kickable) { await member.kick(`Anti-Nuke: ${reason}`).catch(() => {}); - } else if (member.manageable) { - const removable = member.roles.cache.filter(role => role.id !== guild.id && role.editable); - await member.roles.remove(removable, `Anti-Nuke: ${reason}`).catch(() => {}); + } else { + const stripped = await stripDangerousRoles(member, reason); + actionTaken = stripped ? 'strip' : 'none'; + } + + if (config.antiNuke.lockdown && guild.members.me?.permissions.has(PermissionFlagsBits.ManageChannels)) { + if (['channelDelete', 'roleDelete', 'webhookDelete', 'botAdd'].includes(reason.split(' ')[0])) { + // The raid system owns timed lockdowns; Anti-Nuke only emits an alert here. + // This prevents an accidental permanent server-wide lockdown from a single event. + } + } + + if (reason.startsWith('botAdd') && targetId) { + const bot = await guild.members.fetch(targetId).catch(() => null); + if (bot?.user?.bot && bot.kickable && bot.id !== guild.client.user.id) { + await bot.kick('Anti-Nuke: unauthorized bot addition').catch(() => {}); + } } await sendSecurityLog(guild.client, guild, { title: 'Anti-Nuke Triggered', - description: `**${executor.tag || executor.username}** triggered the Anti-Nuke protection.`, + description: `**${executor.tag || executor.username}** triggered Anti-Nuke protection.`, fields: [ - { name: 'Reason', value: reason }, - { name: 'Action', value: config.antiNuke.action }, + { name: 'Reason', value: reason.slice(0, 1024), inline: true }, + { name: 'Action', value: actionTaken, inline: true }, + { name: 'Executor', value: `${executor.id}`, inline: true }, ], }); return true; @@ -58,11 +98,12 @@ export async function handleAntiNuke(guild, type, targetId = null) { if (!config.enabled || !config.antiNuke.enabled) return; const auditType = EVENT_MAP[type]; - const threshold = config.antiNuke.thresholds[type]; - if (!auditType || !threshold) return; + const threshold = Number(config.antiNuke.thresholds?.[type] || 0); + if (!auditType || threshold <= 0) return; const executor = await findExecutor(guild, auditType, targetId); - if (!executor) return; + if (!executor || executor.id === guild.client.user?.id) return; + const member = await guild.members.fetch(executor.id).catch(() => null); if (!member || member.id === guild.ownerId || isWhitelisted(member, config)) return; @@ -73,7 +114,13 @@ export async function handleAntiNuke(guild, type, targetId = null) { counters.set(counterKey, recent); if (recent.length >= threshold) { - await punishExecutor(guild, executor, config, `${type} threshold exceeded (${recent.length}/${threshold})`); + await punishExecutor( + guild, + executor, + config, + `${type} threshold exceeded (${recent.length}/${threshold})`, + targetId, + ); counters.delete(counterKey); } } From 4fc734e00d5007875690b110bc53baf1c8c34b2c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:41:31 +0300 Subject: [PATCH 055/219] Upgrade AutoMod detection and escalation behavior --- src/services/security/autoMod.js | 57 ++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/services/security/autoMod.js b/src/services/security/autoMod.js index 8e3a2ab12f..c3f666a47d 100644 --- a/src/services/security/autoMod.js +++ b/src/services/security/autoMod.js @@ -4,6 +4,7 @@ const userMessages = new Map(); const duplicates = new Map(); const inviteRegex = /(discord\.gg|discord(?:app)?\.com\/invite)\/[^\s]+/i; const urlRegex = /https?:\/\/[^\s]+/i; +const repeatedCharRegex = /(.)\1{8,}/u; function getState(map, key) { if (!map.has(key)) map.set(key, []); @@ -19,54 +20,82 @@ function detect(message, config) { if (config.autoMod.spam.enabled) { const list = getState(userMessages, key).filter(t => now - t <= config.autoMod.spam.windowMs); - list.push(now); userMessages.set(key, list); + list.push(now); + userMessages.set(key, list); if (list.length >= config.autoMod.spam.maxMessages) reasons.push('message spam'); } - if (config.autoMod.duplicate.enabled && text.trim()) { + const normalized = text.trim().slice(0, 500); + if (config.autoMod.duplicate.enabled && normalized) { const list = getState(duplicates, key).filter(x => now - x.time <= config.autoMod.duplicate.windowMs); - list.push({ text: text.trim().slice(0, 500), time: now }); duplicates.set(key, list); - const repeats = list.filter(x => x.text === text.trim().slice(0, 500)).length; + list.push({ text: normalized, time: now }); + duplicates.set(key, list); + const repeats = list.filter(x => x.text === normalized).length; if (repeats >= config.autoMod.duplicate.maxRepeats) reasons.push('duplicate messages'); } - if (config.autoMod.mentions.enabled && message.mentions.users.size + message.mentions.roles.size >= config.autoMod.mentions.max) reasons.push('mention spam'); + const mentionCount = message.mentions.users.size + message.mentions.roles.size; + if (config.autoMod.mentions.enabled && mentionCount >= config.autoMod.mentions.max) reasons.push('mention spam'); + if (config.autoMod.mentions.enabled && (message.mentions.everyone || /@(everyone|here)/i.test(text))) reasons.push('everyone/here mention'); if (config.autoMod.invites.enabled && inviteRegex.test(text)) reasons.push('Discord invite link'); if (config.autoMod.links.enabled && urlRegex.test(text)) reasons.push('external link'); - if (config.autoMod.badWords.enabled && config.autoMod.badWords.words.some(word => lower.includes(String(word).toLowerCase()))) reasons.push('blocked word'); + if (config.autoMod.badWords.enabled && config.autoMod.badWords.words.some(word => { + const escaped = String(word).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`(^|\\s)${escaped}(?=$|\\s|[.!?,])`, 'i').test(text); + })) reasons.push('blocked word'); + if (repeatedCharRegex.test(text)) reasons.push('character spam'); if (config.autoMod.caps.enabled) { const letters = text.match(/[A-Za-z]/g) || []; const upper = text.match(/[A-Z]/g) || []; - if (letters.length >= config.autoMod.caps.minLength && upper.length / letters.length >= config.autoMod.caps.ratio) reasons.push('excessive caps'); + if (letters.length >= config.autoMod.caps.minLength && upper.length / letters.length >= config.autoMod.caps.ratio) { + reasons.push('excessive caps'); + } } return [...new Set(reasons)]; } async function executeAction(message, reason, config) { - await message.delete().catch(() => {}); + const deleted = await message.delete().then(() => true).catch(() => false); const strike = await addStrike(message.client, message.guild.id, message.author.id, reason); const level = Math.min(strike.count, config.escalation.length); const escalation = config.escalation[level - 1]; let action = config.autoMod.action; if (escalation?.action && escalation.action !== 'warn') action = escalation.action; + if (escalation?.action === 'warn') action = 'warn'; const member = await message.guild.members.fetch(message.author.id).catch(() => null); if (member && !isWhitelisted(member, config)) { - if (action === 'timeout' && member.moderatable) await member.timeout(escalation?.durationMs || 60000, `AutoMod: ${reason}`).catch(() => {}); - else if (action === 'kick' && member.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); - else if (action === 'ban' && member.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); + if (action === 'timeout' && member.moderatable) { + await member.timeout(escalation?.durationMs || 60000, `AutoMod: ${reason}`).catch(() => {}); + } else if (action === 'kick' && member.kickable) { + await member.kick(`AutoMod: ${reason}`).catch(() => {}); + } else if (action === 'ban' && member.bannable) { + await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); + } + } + + if (action === 'warn' && !message.channel.isDMBased?.()) { + const warning = await message.channel.send({ + content: `${message.author}, warning: **${reason}**. Strike **${strike.count}**.`, + allowedMentions: { users: [message.author.id] }, + }).catch(() => null); + if (warning) { + const timer = setTimeout(() => warning.delete().catch(() => {}), 5000); + timer.unref?.(); + } } await sendSecurityLog(message.client, message.guild, { title: 'AutoMod Action', description: `${message.author} triggered AutoMod.`, fields: [ - { name: 'Reason', value: reason, inline: true }, + { name: 'Reason', value: reason.slice(0, 1024), inline: true }, { name: 'Strike', value: String(strike.count), inline: true }, { name: 'Action', value: action, inline: true }, + { name: 'Message Deleted', value: deleted ? 'Yes' : 'No', inline: true }, ], }); } @@ -74,9 +103,9 @@ async function executeAction(message, reason, config) { export async function handleAutoMod(message) { if (!message.guild || message.author.bot) return false; const config = await getSecurityConfig(message.client, message.guild.id); - if (!config.enabled || !config.autoMod.enabled || config.ignoredChannels.includes(message.channel.id)) return false; + if (!config.enabled || !config.autoMod.enabled || config.ignoredChannels?.includes(message.channel.id)) return false; - const member = message.member; + const member = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); if (!member || member.id === message.guild.ownerId || isWhitelisted(member, config)) return false; const reasons = detect(message, config); From 3d695186dbd11f456b91e641b103a02a09324447 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:41:38 +0300 Subject: [PATCH 056/219] Add Anti-Nuke role permission escalation detection --- src/events/securityRoleUpdate.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/events/securityRoleUpdate.js diff --git a/src/events/securityRoleUpdate.js b/src/events/securityRoleUpdate.js new file mode 100644 index 0000000000..97cefe2131 --- /dev/null +++ b/src/events/securityRoleUpdate.js @@ -0,0 +1,32 @@ +import { Events } from 'discord.js'; +import { handleAntiNuke } from '../services/security/antiNuke.js'; + +export default { + name: Events.RoleUpdate, + async execute(oldRole, newRole) { + if (!newRole?.guild) return; + + const dangerousBefore = oldRole.permissions.has([ + 'Administrator', + 'ManageGuild', + 'ManageChannels', + 'ManageRoles', + 'BanMembers', + 'KickMembers', + 'ManageWebhooks', + ]); + const dangerousAfter = newRole.permissions.has([ + 'Administrator', + 'ManageGuild', + 'ManageChannels', + 'ManageRoles', + 'BanMembers', + 'KickMembers', + 'ManageWebhooks', + ]); + + if (dangerousAfter && !dangerousBefore) { + await handleAntiNuke(newRole.guild, 'roleUpdate', newRole.id); + } + }, +}; From ff3d509bbec0092b6dc58f606dc859ab0adc448d Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:41:42 +0300 Subject: [PATCH 057/219] Add webhook change protection event --- src/events/securityWebhooksUpdate.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/events/securityWebhooksUpdate.js diff --git a/src/events/securityWebhooksUpdate.js b/src/events/securityWebhooksUpdate.js new file mode 100644 index 0000000000..c8aeddc59f --- /dev/null +++ b/src/events/securityWebhooksUpdate.js @@ -0,0 +1,10 @@ +import { Events } from 'discord.js'; +import { handleAntiNuke } from '../services/security/antiNuke.js'; + +export default { + name: Events.WebhooksUpdate, + async execute(channel) { + if (!channel?.guild) return; + await handleAntiNuke(channel.guild, 'webhookUpdate', channel.id); + }, +}; From f9731f04115c9070d8f269b97b213c4d0b1ae4c3 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:41:51 +0300 Subject: [PATCH 058/219] Fix webhook audit matching in Anti-Nuke --- src/services/security/antiNuke.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/services/security/antiNuke.js b/src/services/security/antiNuke.js index 081bc2837d..c1903f862a 100644 --- a/src/services/security/antiNuke.js +++ b/src/services/security/antiNuke.js @@ -21,7 +21,7 @@ function key(guildId, executorId, type) { return `${guildId}:${executorId}:${type}`; } -async function findExecutor(guild, auditType, targetId) { +async function findExecutor(guild, auditType, targetId = null) { const logs = await guild.fetchAuditLogs({ type: auditType, limit: 10 }).catch(() => null); const entry = logs?.entries?.find(entry => { if (Date.now() - entry.createdTimestamp > 15000) return false; @@ -67,16 +67,9 @@ async function punishExecutor(guild, executor, config, reason, targetId) { actionTaken = stripped ? 'strip' : 'none'; } - if (config.antiNuke.lockdown && guild.members.me?.permissions.has(PermissionFlagsBits.ManageChannels)) { - if (['channelDelete', 'roleDelete', 'webhookDelete', 'botAdd'].includes(reason.split(' ')[0])) { - // The raid system owns timed lockdowns; Anti-Nuke only emits an alert here. - // This prevents an accidental permanent server-wide lockdown from a single event. - } - } - if (reason.startsWith('botAdd') && targetId) { const bot = await guild.members.fetch(targetId).catch(() => null); - if (bot?.user?.bot && bot.kickable && bot.id !== guild.client.user.id) { + if (bot?.user?.bot && bot.kickable && bot.id !== guild.client.user?.id) { await bot.kick('Anti-Nuke: unauthorized bot addition').catch(() => {}); } } @@ -101,7 +94,8 @@ export async function handleAntiNuke(guild, type, targetId = null) { const threshold = Number(config.antiNuke.thresholds?.[type] || 0); if (!auditType || threshold <= 0) return; - const executor = await findExecutor(guild, auditType, targetId); + const auditTargetId = type.startsWith('webhook') ? null : targetId; + const executor = await findExecutor(guild, auditType, auditTargetId); if (!executor || executor.id === guild.client.user?.id) return; const member = await guild.members.fetch(executor.id).catch(() => null); From 1cc60c3127696a94bebabad4bbc87030722fb501 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:42:11 +0300 Subject: [PATCH 059/219] Build full security dashboard with escalation and whitelist controls --- src/services/security/securityDashboard.js | 114 +++++++++++++++++++-- 1 file changed, 107 insertions(+), 7 deletions(-) diff --git a/src/services/security/securityDashboard.js b/src/services/security/securityDashboard.js index 1db1c6468a..429524d928 100644 --- a/src/services/security/securityDashboard.js +++ b/src/services/security/securityDashboard.js @@ -1,6 +1,101 @@ import { getSecurityConfig, updateSecurityConfig } from './securityService.js'; -const HTML = `Infinity Security Dashboard

Infinity Security

Anti-Nuke / Anti-Raid / AutoMod / Escalation

Dashboard Token

`; +const HTML = ` + + + + +Infinity Security Dashboard + + +
+

Infinity Security

Anti-Nuke • Anti-Raid • Advanced AutoMod • Escalation • Whitelist
+

Dashboard Access

Enter the SECURITY_DASHBOARD_TOKEN configured on the bot host.

+
+`; + +function getAllowedGuild(client, guildId) { + const configured = process.env.SECURITY_DASHBOARD_GUILD_ID || process.env.GUILD_ID || null; + const selectedId = guildId || configured; + if (selectedId) return client.guilds.cache.get(selectedId) || null; + return client.guilds.cache.first() || null; +} export function registerSecurityDashboard(app, client) { const token = process.env.SECURITY_DASHBOARD_TOKEN; @@ -10,18 +105,23 @@ export function registerSecurityDashboard(app, client) { app.get('/api/security/config', async (req, res) => { if (!authorized(req)) return res.status(401).json({ error: 'Unauthorized' }); - const guild = client.guilds.cache.first(); - if (!guild) return res.status(503).json({ error: 'Bot is not connected to a guild' }); - res.json(await getSecurityConfig(client, guild.id)); + const guild = getAllowedGuild(client, req.query.guildId); + if (!guild) return res.status(503).json({ error: 'Configured guild is not available' }); + try { + const config = await getSecurityConfig(client, guild.id); + res.json({ ...config, guildName: guild.name, guildId: guild.id }); + } catch (error) { + res.status(500).json({ error: error.message }); + } }); app.patch('/api/security/config', async (req, res) => { if (!authorized(req)) return res.status(401).json({ error: 'Unauthorized' }); - const guild = client.guilds.cache.first(); - if (!guild) return res.status(503).json({ error: 'Bot is not connected to a guild' }); + const guild = getAllowedGuild(client, req.query.guildId || req.body?.guildId); + if (!guild) return res.status(503).json({ error: 'Configured guild is not available' }); try { const updated = await updateSecurityConfig(client, guild.id, req.body || {}); - res.json(updated); + res.json({ ...updated, guildName: guild.name, guildId: guild.id }); } catch (error) { res.status(500).json({ error: error.message }); } From 3e24a141058b97fdc37b0b8159e84fb08f6768a4 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:42:17 +0300 Subject: [PATCH 060/219] Attach security dashboard to web server on bot ready --- src/events/securityDashboardReady.js | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/events/securityDashboardReady.js diff --git a/src/events/securityDashboardReady.js b/src/events/securityDashboardReady.js new file mode 100644 index 0000000000..2655731542 --- /dev/null +++ b/src/events/securityDashboardReady.js @@ -0,0 +1,30 @@ +import { Events } from 'discord.js'; +import express from 'express'; +import { registerSecurityDashboard } from '../services/security/securityDashboard.js'; +import { logger, startupLog } from '../utils/logger.js'; + +export default { + name: Events.ClientReady, + once: true, + async execute(client) { + try { + const server = client.webServer; + const app = server?.listeners('request')?.[0]; + if (typeof app !== 'function') { + logger.warn('Security dashboard could not attach: Express app was not found.'); + return; + } + + app.use(express.json({ limit: '128kb' })); + registerSecurityDashboard(app, client); + + if (!process.env.SECURITY_DASHBOARD_TOKEN) { + logger.warn('SECURITY_DASHBOARD_TOKEN is not configured; /security will be inaccessible.'); + } else { + startupLog('Security dashboard enabled at /security'); + } + } catch (error) { + logger.error('Failed to initialize security dashboard:', error); + } + }, +}; From 2ff8aa8077175be45a71c4b26835f63272a4d8ac Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:42:24 +0300 Subject: [PATCH 061/219] Apply AutoMod to edited messages --- src/events/securityMessageUpdate.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/events/securityMessageUpdate.js diff --git a/src/events/securityMessageUpdate.js b/src/events/securityMessageUpdate.js new file mode 100644 index 0000000000..82f2f3eadc --- /dev/null +++ b/src/events/securityMessageUpdate.js @@ -0,0 +1,11 @@ +import { Events } from 'discord.js'; +import { handleAutoMod } from '../services/security/autoMod.js'; + +export default { + name: Events.MessageUpdate, + async execute(oldMessage, newMessage) { + if (!newMessage?.guild || newMessage.author?.bot) return; + if (!newMessage.content || newMessage.content === oldMessage?.content) return; + await handleAutoMod(newMessage); + }, +}; From bb56bb7790ee6e8f1be154e4d6477823edfd79bf Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:42:28 +0300 Subject: [PATCH 062/219] Add Anti-Nuke unauthorized bot addition detection --- src/events/securityBotAdd.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/events/securityBotAdd.js diff --git a/src/events/securityBotAdd.js b/src/events/securityBotAdd.js new file mode 100644 index 0000000000..7e9d486555 --- /dev/null +++ b/src/events/securityBotAdd.js @@ -0,0 +1,10 @@ +import { Events } from 'discord.js'; +import { handleAntiNuke } from '../services/security/antiNuke.js'; + +export default { + name: Events.GuildMemberAdd, + async execute(member) { + if (!member?.user?.bot) return; + await handleAntiNuke(member.guild, 'botAdd', member.id); + }, +}; From 6a57f345b44b5217c58b395f885a3cca9946505b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:42:40 +0300 Subject: [PATCH 063/219] Fix dangerous permission detection for role escalation --- src/events/securityRoleUpdate.js | 38 ++++++++++++++------------------ 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/events/securityRoleUpdate.js b/src/events/securityRoleUpdate.js index 97cefe2131..c9825d7a61 100644 --- a/src/events/securityRoleUpdate.js +++ b/src/events/securityRoleUpdate.js @@ -1,31 +1,25 @@ -import { Events } from 'discord.js'; +import { Events, PermissionFlagsBits } from 'discord.js'; import { handleAntiNuke } from '../services/security/antiNuke.js'; +const DANGEROUS_PERMISSIONS = [ + PermissionFlagsBits.Administrator, + PermissionFlagsBits.ManageGuild, + PermissionFlagsBits.ManageChannels, + PermissionFlagsBits.ManageRoles, + PermissionFlagsBits.BanMembers, + PermissionFlagsBits.KickMembers, + PermissionFlagsBits.ManageWebhooks, +]; + +function hasDangerousPermission(role) { + return DANGEROUS_PERMISSIONS.some(permission => role.permissions.has(permission)); +} + export default { name: Events.RoleUpdate, async execute(oldRole, newRole) { if (!newRole?.guild) return; - - const dangerousBefore = oldRole.permissions.has([ - 'Administrator', - 'ManageGuild', - 'ManageChannels', - 'ManageRoles', - 'BanMembers', - 'KickMembers', - 'ManageWebhooks', - ]); - const dangerousAfter = newRole.permissions.has([ - 'Administrator', - 'ManageGuild', - 'ManageChannels', - 'ManageRoles', - 'BanMembers', - 'KickMembers', - 'ManageWebhooks', - ]); - - if (dangerousAfter && !dangerousBefore) { + if (!hasDangerousPermission(oldRole) && hasDangerousPermission(newRole)) { await handleAntiNuke(newRole.guild, 'roleUpdate', newRole.id); } }, From d9e173e657e911358a7d35dd1dcf74148c22f938 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:42:51 +0300 Subject: [PATCH 064/219] Tune Anti-Nuke defaults for severe permission and bot changes --- src/services/security/securityService.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js index 70885c8d11..e16a78b029 100644 --- a/src/services/security/securityService.js +++ b/src/services/security/securityService.js @@ -11,12 +11,12 @@ export const SECURITY_DEFAULTS = { channelCreate: 5, roleDelete: 3, roleCreate: 5, - roleUpdate: 3, + roleUpdate: 1, webhookUpdate: 3, - webhookDelete: 3, + webhookDelete: 2, ban: 5, kick: 5, - botAdd: 2, + botAdd: 1, }, action: 'strip', lockdown: true, From 294a933a11bbc2fdb60ad5760c84b40e939b85bd Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:43:01 +0300 Subject: [PATCH 065/219] Document security dashboard environment settings --- .env.example | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index c2eae4ad28..d7ecd0a16f 100644 --- a/.env.example +++ b/.env.example @@ -19,10 +19,13 @@ WEB_HOST=0.0.0.0 PORT_RETRY_ATTEMPTS=5 CORS_ORIGIN=* +# Security Dashboard +# Generate a long random value and set it in Railway/environment variables. +SECURITY_DASHBOARD_TOKEN=replace_with_a_long_random_secret +# Optional: restrict dashboard access to one guild. Defaults to GUILD_ID when present. +SECURITY_DASHBOARD_GUILD_ID= + # PostgreSQL Configuration (Primary Database) -# Railway: use the private POSTGRES_URL / DATABASE_URL variable (includes SSL). -# Public proxy logs showing "invalid length of startup packet" or "SSL without ALPN" -# are usually internet scanners — not your bot. POSTGRES_URL=postgresql://postgres:yourpassword@localhost:5432/titanbot POSTGRES_SSL= POSTGRES_HOST=localhost @@ -52,12 +55,8 @@ BACKUP_RETENTION_DAYS=14 POSTGRES_RESTORE_URL= # Music (Lavalink + Riffy) — requires Lavalink v4 nodes -# Default: loads public v4 SSL nodes from lavalink/nodes.json -# Source: https://lavalink.darrennathanael.com/SSL/Lavalink-SSL/ # LAVALINK_NODES_FILE=lavalink/nodes.json -# Optional: override nodes as JSON array (takes priority over nodes file) # LAVALINK_NODES=[{"host":"lavalink","port":2333,"password":"youshallnotpass","secure":false,"name":"Main"}] -# Self-hosted single node fallback (used only if nodes file/env array are absent): # LAVALINK_HOST=localhost # LAVALINK_PORT=2333 # LAVALINK_PASSWORD=youshallnotpass From 447bfa71b90e08abda6d8bebca8c345284f5f1db Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:43:31 +0300 Subject: [PATCH 066/219] Monitor webhook create, update, and delete audit actions --- src/services/security/antiNuke.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/services/security/antiNuke.js b/src/services/security/antiNuke.js index c1903f862a..4c199ed7be 100644 --- a/src/services/security/antiNuke.js +++ b/src/services/security/antiNuke.js @@ -10,7 +10,7 @@ const EVENT_MAP = { roleDelete: AuditLogEvent.RoleDelete, roleCreate: AuditLogEvent.RoleCreate, roleUpdate: AuditLogEvent.RoleUpdate, - webhookUpdate: AuditLogEvent.WebhookUpdate, + webhookUpdate: [AuditLogEvent.WebhookCreate, AuditLogEvent.WebhookUpdate, AuditLogEvent.WebhookDelete], webhookDelete: AuditLogEvent.WebhookDelete, ban: AuditLogEvent.MemberBanAdd, kick: AuditLogEvent.MemberKick, @@ -22,13 +22,22 @@ function key(guildId, executorId, type) { } async function findExecutor(guild, auditType, targetId = null) { - const logs = await guild.fetchAuditLogs({ type: auditType, limit: 10 }).catch(() => null); - const entry = logs?.entries?.find(entry => { - if (Date.now() - entry.createdTimestamp > 15000) return false; - if (!targetId) return true; - return entry.target?.id === targetId || entry.targetId === targetId; - }); - return entry?.executor || null; + const types = Array.isArray(auditType) ? auditType : [auditType]; + const entries = []; + + for (const type of types) { + const logs = await guild.fetchAuditLogs({ type, limit: 10 }).catch(() => null); + if (!logs?.entries) continue; + + for (const entry of logs.entries.values()) { + if (Date.now() - entry.createdTimestamp > 15000) continue; + if (targetId && entry.target?.id !== targetId && entry.targetId !== targetId) continue; + entries.push(entry); + } + } + + entries.sort((a, b) => b.createdTimestamp - a.createdTimestamp); + return entries[0]?.executor || null; } async function stripDangerousRoles(member, reason) { @@ -36,7 +45,7 @@ async function stripDangerousRoles(member, reason) { const removable = member.roles.cache.filter(role => { if (role.id === member.guild.id || !role.editable) return false; - return role.permissions.has([ + return role.permissions.any([ PermissionFlagsBits.Administrator, PermissionFlagsBits.ManageGuild, PermissionFlagsBits.ManageChannels, From 2e88362f4cb123582d8de3fb3684abf457186d5e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 10:43:38 +0300 Subject: [PATCH 067/219] Add security permission health checks on startup --- src/events/securityDashboardReady.js | 29 ++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/events/securityDashboardReady.js b/src/events/securityDashboardReady.js index 2655731542..8e77841d84 100644 --- a/src/events/securityDashboardReady.js +++ b/src/events/securityDashboardReady.js @@ -1,4 +1,4 @@ -import { Events } from 'discord.js'; +import { Events, PermissionFlagsBits } from 'discord.js'; import express from 'express'; import { registerSecurityDashboard } from '../services/security/securityDashboard.js'; import { logger, startupLog } from '../utils/logger.js'; @@ -12,11 +12,32 @@ export default { const app = server?.listeners('request')?.[0]; if (typeof app !== 'function') { logger.warn('Security dashboard could not attach: Express app was not found.'); - return; + } else { + app.use(express.json({ limit: '128kb' })); + registerSecurityDashboard(app, client); } - app.use(express.json({ limit: '128kb' })); - registerSecurityDashboard(app, client); + for (const guild of client.guilds.cache.values()) { + const me = guild.members.me; + const permissions = me?.permissions; + if (!permissions) continue; + + const required = [ + [PermissionFlagsBits.ViewAuditLog, 'View Audit Log'], + [PermissionFlagsBits.ManageChannels, 'Manage Channels'], + [PermissionFlagsBits.ManageRoles, 'Manage Roles'], + [PermissionFlagsBits.ManageWebhooks, 'Manage Webhooks'], + [PermissionFlagsBits.KickMembers, 'Kick Members'], + [PermissionFlagsBits.BanMembers, 'Ban Members'], + [PermissionFlagsBits.ModerateMembers, 'Moderate Members'], + ]; + const missing = required.filter(([bit]) => !permissions.has(bit)).map(([, name]) => name); + if (missing.length) { + logger.warn(`Security permissions missing in ${guild.name}: ${missing.join(', ')}`); + } else { + startupLog(`Security permissions OK in ${guild.name}`); + } + } if (!process.env.SECURITY_DASHBOARD_TOKEN) { logger.warn('SECURITY_DASHBOARD_TOKEN is not configured; /security will be inaccessible.'); From 15c40a221604cc8db5ab5fbb7588ca0713e0f525 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:09:54 +0300 Subject: [PATCH 068/219] Add full in-Discord security dashboard controls --- src/handlers/securityHandlers.js | 166 ++++++++++++++----------------- 1 file changed, 74 insertions(+), 92 deletions(-) diff --git a/src/handlers/securityHandlers.js b/src/handlers/securityHandlers.js index 52cd4c59d7..6461283d5e 100644 --- a/src/handlers/securityHandlers.js +++ b/src/handlers/securityHandlers.js @@ -1,102 +1,84 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; -function dashboard(config) { - return new EmbedBuilder().setTitle('🛡️ Infinity Security Dashboard').setColor(0x5865F2).addFields( - { name: '🛡️ Anti-Nuke', value: config.antiNuke.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '🚨 Anti-Raid', value: config.antiRaid.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '🤖 AutoMod', value: config.autoMod.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '⚡ Escalation', value: `${config.escalation.length} strike levels`, inline: true }, - { name: '👤 Whitelist', value: `${config.whitelist.users.length} users / ${config.whitelist.roles.length} roles`, inline: true }, - { name: '📋 Log Channel', value: config.logChannelId ? `<#${config.logChannelId}>` : 'Not configured', inline: true }, - ); -} +const BUTTONS = [ + ['security_panel_nuke', 'Anti-Nuke'], ['security_panel_raid', 'Anti-Raid'], + ['security_panel_automod', 'AutoMod'], ['security_panel_punishments', 'Punishments'], + ['security_panel_whitelist', 'Whitelist'], ['security_panel_logs', 'Logs'], + ['security_panel_settings', 'Settings'], +]; -function controls(config, userId) { - return [ - new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId(`security_toggle:nuke:${userId}`).setLabel('Anti-Nuke').setStyle(config.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), - new ButtonBuilder().setCustomId(`security_toggle:raid:${userId}`).setLabel('Anti-Raid').setStyle(config.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), - new ButtonBuilder().setCustomId(`security_toggle:automod:${userId}`).setLabel('AutoMod').setStyle(config.autoMod.enabled ? ButtonStyle.Success : ButtonStyle.Danger), - new ButtonBuilder().setCustomId(`security_refresh:${userId}`).setLabel('Refresh').setStyle(ButtonStyle.Secondary), - ), - new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId(`security_whitelist:${userId}`).setLabel('Whitelist').setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId(`security_logging:${userId}`).setLabel('Logging').setStyle(ButtonStyle.Primary), - ), - ]; +function dashboard(config, guild) { + return new EmbedBuilder().setTitle('🛡️ Infinity Security Dashboard') + .setDescription(`Security control center for **${guild.name}**. All settings are stored persistently.`) + .setColor(0x5865F2) + .addFields( + { name: '🛡️ Anti-Nuke', value: config.antiNuke.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '🚨 Anti-Raid', value: config.antiRaid.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '🤖 AutoMod', value: config.autoMod.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '⚡ Punishments', value: `${config.escalation.length} escalation levels`, inline: true }, + { name: '👤 Whitelist', value: `${config.whitelist.users.length} users / ${config.whitelist.roles.length} roles / ${config.whitelist.bots.length} bots`, inline: true }, + { name: '📋 Logs', value: config.logChannelId ? `<#${config.logChannelId}>` : 'Not configured', inline: true }, + { name: '⏱️ Strike Decay', value: `${Math.round((config.strikeDecayMs || 86400000) / 3600000)}h`, inline: true }, + { name: '🔒 Global Security', value: config.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + ).setFooter({ text: 'Only the moderator who opened this panel can use its controls.' }); } -function ownerOnly(interaction) { - const parts = interaction.customId.split(':'); - return parts.at(-1) === interaction.user.id; +function controls(userId, disabled = false) { + const rows = []; + for (let i = 0; i < BUTTONS.length; i += 4) { + rows.push(new ActionRowBuilder().addComponents(...BUTTONS.slice(i, i + 4).map(([id, label]) => new ButtonBuilder() + .setCustomId(`${id}:${userId}`).setLabel(label) + .setStyle(label === 'Punishments' ? ButtonStyle.Danger : label === 'Settings' ? ButtonStyle.Secondary : ButtonStyle.Primary) + .setDisabled(disabled)))); + } + return rows; } -const securityToggleHandler = { - name: 'security_toggle', - async execute(interaction, client) { - if (!ownerOnly(interaction)) return interaction.reply({ content: 'This dashboard belongs to another moderator.', ephemeral: true }); - const [, type] = interaction.customId.split(':'); - const key = type === 'nuke' ? 'antiNuke.enabled' : type === 'raid' ? 'antiRaid.enabled' : 'autoMod.enabled'; - const current = await getSecurityConfig(client, interaction.guildId); - const patch = type === 'nuke' ? { antiNuke: { enabled: !current.antiNuke.enabled } } : type === 'raid' ? { antiRaid: { enabled: !current.antiRaid.enabled } } : { autoMod: { enabled: !current.autoMod.enabled } }; - const updated = await updateSecurityConfig(client, interaction.guildId, patch); - await interaction.update({ embeds: [dashboard(updated)], components: controls(updated, interaction.user.id) }); - }, -}; - -const securityRefreshHandler = { - name: 'security_refresh', - async execute(interaction, client) { - if (!ownerOnly(interaction)) return interaction.reply({ content: 'This dashboard belongs to another moderator.', ephemeral: true }); - const config = await getSecurityConfig(client, interaction.guildId); - await interaction.update({ embeds: [dashboard(config)], components: controls(config, interaction.user.id) }); - }, -}; - -const securityWhitelistHandler = { - name: 'security_whitelist', - async execute(interaction) { - if (!ownerOnly(interaction)) return interaction.reply({ content: 'This dashboard belongs to another moderator.', ephemeral: true }); - const modal = new ModalBuilder().setCustomId(`security_whitelist_modal:${interaction.user.id}`).setTitle('Security Whitelist'); - modal.addComponents( - new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('users').setLabel('User IDs (space/comma separated)').setStyle(TextInputStyle.Paragraph).setRequired(false)), - new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('roles').setLabel('Role IDs (space/comma separated)').setStyle(TextInputStyle.Paragraph).setRequired(false)), - ); - await interaction.showModal(modal); - }, -}; - -const securityLoggingHandler = { - name: 'security_logging', - async execute(interaction) { - if (!ownerOnly(interaction)) return interaction.reply({ content: 'This dashboard belongs to another moderator.', ephemeral: true }); - const modal = new ModalBuilder().setCustomId(`security_logging_modal:${interaction.user.id}`).setTitle('Security Logging'); - modal.addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('channel').setLabel('Security log channel ID').setPlaceholder('123456789012345678').setStyle(TextInputStyle.Short).setRequired(false))); - await interaction.showModal(modal); - }, -}; +function authorized(interaction) { return interaction.customId.split(':').at(-1) === interaction.user.id; } +function reject(interaction) { return interaction.reply({ content: 'This security dashboard belongs to another moderator.', flags: MessageFlags.Ephemeral }); } +function input(id, label, value = '', style = TextInputStyle.Short) { + const x = new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false); + if (value !== undefined && value !== null && String(value)) x.setValue(String(value).slice(0, 4000)); + return new ActionRowBuilder().addComponents(x); +} +function modal(interaction, id, title, rows) { + return interaction.showModal(new ModalBuilder().setCustomId(`${id}:${interaction.user.id}`).setTitle(title).addComponents(...rows)); +} +function lines(value) { return String(value || '').split(/[\s,]+/).map(v => v.trim()).filter(Boolean); } +function num(value, fallback, min = 0) { const n = Number(value); return Number.isFinite(n) ? Math.max(min, n) : fallback; } +function bool(value, fallback) { const v = String(value ?? '').trim().toLowerCase(); return v === 'true' ? true : v === 'false' ? false : fallback; } +function escalationText(config) { return (config.escalation || []).map(e => `${e.strike}:${e.action}:${e.durationMs || 0}`).join('\n'); } +function parseEscalation(value) { + const allowed = new Set(['warn', 'timeout', 'kick', 'ban']); + return String(value || '').split('\n').map(x => x.trim()).filter(Boolean).map(line => { + const [s, a, d] = line.split(':').map(x => x.trim()); + const strike = Number(s); + if (!Number.isInteger(strike) || strike < 1 || strike > 10 || !allowed.has(a)) return null; + return { strike, action: a, durationMs: num(d, 0) }; + }).filter(Boolean).sort((a, b) => a.strike - b.strike).slice(0, 10); +} -const securityWhitelistModalHandler = { - name: 'security_whitelist_modal', - async execute(interaction, client) { - if (!ownerOnly(interaction)) return interaction.reply({ content: 'Unauthorized.', ephemeral: true }); - const users = interaction.fields.getTextInputValue('users').split(/[\s,]+/).filter(Boolean); - const roles = interaction.fields.getTextInputValue('roles').split(/[\s,]+/).filter(Boolean); - const updated = await updateSecurityConfig(client, interaction.guildId, { whitelist: { users, roles } }); - await interaction.reply({ embeds: [dashboard(updated)], components: controls(updated, interaction.user.id), ephemeral: true }); - }, -}; +const buttonHandlers = [ + { name: 'security_panel_nuke', async execute(i, c) { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_nuke_modal', 'Anti-Nuke Settings', [input('enabled','Enabled: true/false',x.antiNuke.enabled),input('window','Detection window ms',x.antiNuke.windowMs),input('thresholds','One threshold per line: key=value',Object.entries(x.antiNuke.thresholds).map(([k,v])=>`${k}=${v}`).join('\n'),TextInputStyle.Paragraph),input('action','Action: strip, kick, ban',x.antiNuke.action),input('lockdown','Lockdown alert: true/false',x.antiNuke.lockdown)]); } }, + { name: 'security_panel_raid', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_raid_modal','Anti-Raid Settings',[input('enabled','Enabled: true/false',x.antiRaid.enabled),input('joins','Joins required',x.antiRaid.joins),input('window','Join window ms',x.antiRaid.windowMs),input('accountAge','Minimum account age hours',Math.round(x.antiRaid.minAccountAgeMs/3600000)),input('action','Action: timeout or kick',x.antiRaid.action)]); } }, + { name: 'security_panel_automod', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_automod_modal','AutoMod Settings',[input('enabled','Enabled: true/false',x.autoMod.enabled),input('spam','Spam: enabled,maxMessages,windowMs',`${x.autoMod.spam.enabled},${x.autoMod.spam.maxMessages},${x.autoMod.spam.windowMs}`),input('duplicate','Duplicate: enabled,maxRepeats,windowMs',`${x.autoMod.duplicate.enabled},${x.autoMod.duplicate.maxRepeats},${x.autoMod.duplicate.windowMs}`),input('content','Mentions,invites,links,caps',''+x.autoMod.mentions.max+','+x.autoMod.invites.enabled+','+x.autoMod.links.enabled+','+x.autoMod.caps.enabled),input('badWords','Bad words, one per line',(x.autoMod.badWords.words||[]).join('\n'),TextInputStyle.Paragraph)]); } }, + { name: 'security_panel_punishments', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_punishments_modal','Punishment Escalation',[input('levels','strike:action:durationMs per line',escalationText(x),TextInputStyle.Paragraph),input('decay','Strike decay hours',Math.round((x.strikeDecayMs||86400000)/3600000))]); } }, + { name: 'security_panel_whitelist', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_whitelist_modal','Security Whitelist',[input('users','User IDs',(x.whitelist.users||[]).join('\n'),TextInputStyle.Paragraph),input('roles','Role IDs',(x.whitelist.roles||[]).join('\n'),TextInputStyle.Paragraph),input('bots','Bot IDs',(x.whitelist.bots||[]).join('\n'),TextInputStyle.Paragraph)]); } }, + { name: 'security_panel_logs', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_logs_modal','Security Logs',[input('channel','Security log channel ID',x.logChannelId||''),input('ignored','Ignored channel IDs',(x.ignoredChannels||[]).join('\n'),TextInputStyle.Paragraph)]); } }, + { name: 'security_panel_settings', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_settings_modal','Security Settings',[input('enabled','Global security enabled: true/false',x.enabled),input('info','Ignored by parser','No value needed')]); } }, + { name: 'security_refresh', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return i.update({embeds:[dashboard(x,i.guild)],components:controls(i.user.id)}); } }, +]; -const securityLoggingModalHandler = { - name: 'security_logging_modal', - async execute(interaction, client) { - if (!ownerOnly(interaction)) return interaction.reply({ content: 'Unauthorized.', ephemeral: true }); - const channel = interaction.fields.getTextInputValue('channel').trim(); - const updated = await updateSecurityConfig(client, interaction.guildId, { logChannelId: channel || null }); - await interaction.reply({ embeds: [dashboard(updated)], components: controls(updated, interaction.user.id), ephemeral: true }); - }, -}; +const modalHandlers = [ + { name:'security_nuke_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const thresholds={...x.antiNuke.thresholds}; for(const line of i.fields.getTextInputValue('thresholds').split('\n')){const [k,v]=line.split('=').map(z=>z.trim());if(Object.prototype.hasOwnProperty.call(thresholds,k))thresholds[k]=num(v,thresholds[k],1);} const action=i.fields.getTextInputValue('action').trim().toLowerCase(); const u=await updateSecurityConfig(c,i.guildId,{antiNuke:{enabled:bool(i.fields.getTextInputValue('enabled'),x.antiNuke.enabled),windowMs:num(i.fields.getTextInputValue('window'),x.antiNuke.windowMs,1000),thresholds,action:['strip','kick','ban'].includes(action)?action:x.antiNuke.action,lockdown:bool(i.fields.getTextInputValue('lockdown'),x.antiNuke.lockdown)}}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, + { name:'security_raid_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const a=i.fields.getTextInputValue('action').trim().toLowerCase(); const u=await updateSecurityConfig(c,i.guildId,{antiRaid:{enabled:bool(i.fields.getTextInputValue('enabled'),x.antiRaid.enabled),joins:num(i.fields.getTextInputValue('joins'),x.antiRaid.joins,2),windowMs:num(i.fields.getTextInputValue('window'),x.antiRaid.windowMs,1000),minAccountAgeMs:num(i.fields.getTextInputValue('accountAge'),x.antiRaid.minAccountAgeMs/3600000,0)*3600000,action:['timeout','kick'].includes(a)?a:x.antiRaid.action}}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, + { name:'security_automod_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const [se,sm,sw]=i.fields.getTextInputValue('spam').split(','); const [de,dm,dw]=i.fields.getTextInputValue('duplicate').split(','); const [mm,inv,ln,cp]=i.fields.getTextInputValue('content').split(','); const u=await updateSecurityConfig(c,i.guildId,{autoMod:{enabled:bool(i.fields.getTextInputValue('enabled'),x.autoMod.enabled),spam:{enabled:bool(se,x.autoMod.spam.enabled),maxMessages:num(sm,x.autoMod.spam.maxMessages,2),windowMs:num(sw,x.autoMod.spam.windowMs,1000)},duplicate:{enabled:bool(de,x.autoMod.duplicate.enabled),maxRepeats:num(dm,x.autoMod.duplicate.maxRepeats,2),windowMs:num(dw,x.autoMod.duplicate.windowMs,1000)},mentions:{enabled:true,max:num(mm,x.autoMod.mentions.max,1)},invites:{enabled:bool(inv,x.autoMod.invites.enabled)},links:{enabled:bool(ln,x.autoMod.links.enabled)},caps:{...x.autoMod.caps,enabled:bool(cp,x.autoMod.caps.enabled)},badWords:{enabled:true,words:lines(i.fields.getTextInputValue('badWords')).slice(0,100)}}}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, + { name:'security_punishments_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const parsed=parseEscalation(i.fields.getTextInputValue('levels')); const u=await updateSecurityConfig(c,i.guildId,{escalation:parsed.length?parsed:x.escalation,strikeDecayMs:num(i.fields.getTextInputValue('decay'),24,1)*3600000}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, + { name:'security_whitelist_modal', async execute(i,c){ if(!authorized(i))return reject(i); const u=await updateSecurityConfig(c,i.guildId,{whitelist:{users:lines(i.fields.getTextInputValue('users')),roles:lines(i.fields.getTextInputValue('roles')),bots:lines(i.fields.getTextInputValue('bots'))}}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, + { name:'security_logs_modal', async execute(i,c){ if(!authorized(i))return reject(i); const u=await updateSecurityConfig(c,i.guildId,{logChannelId:i.fields.getTextInputValue('channel').trim()||null,ignoredChannels:lines(i.fields.getTextInputValue('ignored'))}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, + { name:'security_settings_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const u=await updateSecurityConfig(c,i.guildId,{enabled:bool(i.fields.getTextInputValue('enabled'),x.enabled)}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, +]; -export const securityButtonHandlers = [securityToggleHandler, securityRefreshHandler, securityWhitelistHandler, securityLoggingHandler]; -export const securityModalHandlers = [securityWhitelistModalHandler, securityLoggingModalHandler]; +export const securityButtonHandlers = buttonHandlers; +export const securityModalHandlers = modalHandlers; From 9d25682e41883d62f80cfa4d4f41e15cf391e223 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:10:01 +0300 Subject: [PATCH 069/219] Add private Discord security dashboard command --- src/commands/Core/security.js | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/commands/Core/security.js diff --git a/src/commands/Core/security.js b/src/commands/Core/security.js new file mode 100644 index 0000000000..38fa6f6993 --- /dev/null +++ b/src/commands/Core/security.js @@ -0,0 +1,38 @@ +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; +import { getSecurityConfig } from '../../services/security/securityService.js'; +import { buildSecurityDashboard } from '../../handlers/securityHandlers.js'; +import { InteractionHelper } from '../../utils/interactionHelper.js'; + +export default { + data: new SlashCommandBuilder() + .setName('security') + .setDescription('Open the server security control panel') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .setDMPermission(false), + category: 'Core', + async execute(interaction, config, client) { + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { + await interaction.reply({ content: 'You need the Manage Server permission to use this panel.', flags: MessageFlags.Ephemeral }); + return; + } + + const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); + if (!deferred) return; + + const securityConfig = await getSecurityConfig(client, interaction.guildId); + const view = await buildSecurityDashboard(client, interaction.guild); + view.components = view.components.map((row) => { + const json = row.toJSON(); + json.components = json.components.map((component) => ({ + ...component, + custom_id: component.custom_id.replace(':pending', `:${interaction.user.id}`), + })); + return json; + }); + + await InteractionHelper.safeEditReply(interaction, { + embeds: [view.embeds[0]], + components: view.components, + }); + }, +}; From c217b5600041fbc60207122c0adee07b17d08d8c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:25:22 +0300 Subject: [PATCH 070/219] Fix security dashboard initial UI --- src/commands/Security/security.js | 71 ++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/src/commands/Security/security.js b/src/commands/Security/security.js index 91514c87cc..f4a40ca39b 100644 --- a/src/commands/Security/security.js +++ b/src/commands/Security/security.js @@ -1,20 +1,55 @@ -import { SlashCommandBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { SlashCommandBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageFlags } from 'discord.js'; import { getSecurityConfig } from '../../services/security/securityService.js'; -function dashboard(config) { +const BUTTONS = [ + ['security_panel_nuke', 'Anti-Nuke'], + ['security_panel_raid', 'Anti-Raid'], + ['security_panel_automod', 'AutoMod'], + ['security_panel_punishments', 'Punishments'], + ['security_panel_whitelist', 'Whitelist'], + ['security_panel_logs', 'Logs'], + ['security_panel_settings', 'Settings'], +]; + +function dashboard(config, guild) { return new EmbedBuilder() .setTitle('🛡️ Infinity Security Dashboard') - .setDescription('Use the buttons below to enable/disable protection and manage security settings.') + .setDescription(`Security control center for **${guild.name}**. All settings are stored persistently.`) .setColor(0x5865F2) .addFields( { name: '🛡️ Anti-Nuke', value: config.antiNuke.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, { name: '🚨 Anti-Raid', value: config.antiRaid.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, { name: '🤖 AutoMod', value: config.autoMod.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '⚡ Escalation', value: `${config.escalation.length} strike levels`, inline: true }, - { name: '👤 Whitelist', value: `${config.whitelist.users.length} users / ${config.whitelist.roles.length} roles`, inline: true }, - { name: '📋 Log Channel', value: config.logChannelId ? `<#${config.logChannelId}>` : 'Not configured', inline: true }, + { name: '⚡ Punishments', value: `${config.escalation.length} escalation levels`, inline: true }, + { name: '👤 Whitelist', value: `${config.whitelist.users.length} users / ${config.whitelist.roles.length} roles / ${config.whitelist.bots.length} bots`, inline: true }, + { name: '📋 Logs', value: config.logChannelId ? `<#${config.logChannelId}>` : 'Not configured', inline: true }, + { name: '⏱️ Strike Decay', value: `${Math.round((config.strikeDecayMs || 86400000) / 3600000)}h`, inline: true }, + { name: '🔒 Global Security', value: config.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, ) - .setFooter({ text: 'Infinity Security • ManageGuild required' }); + .setFooter({ text: 'Only the moderator who opened this panel can use its controls.' }); +} + +function controls(userId) { + const rows = []; + for (let i = 0; i < BUTTONS.length; i += 4) { + rows.push( + new ActionRowBuilder().addComponents( + ...BUTTONS.slice(i, i + 4).map(([id, label]) => + new ButtonBuilder() + .setCustomId(`${id}:${userId}`) + .setLabel(label) + .setStyle( + label === 'Punishments' + ? ButtonStyle.Danger + : label === 'Settings' + ? ButtonStyle.Secondary + : ButtonStyle.Primary, + ), + ), + ), + ); + } + return rows; } export default { @@ -24,23 +59,21 @@ export default { .setName('security') .setDescription('Open the server security dashboard') .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild.toString()), + async execute(interaction, config, client) { if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - return interaction.reply({ content: 'You need Manage Server permission.', ephemeral: true }); + return interaction.reply({ + content: 'You need Manage Server permission.', + flags: MessageFlags.Ephemeral, + }); } const security = await getSecurityConfig(client, interaction.guildId); - const row1 = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId(`security_toggle:nuke:${interaction.user.id}`).setLabel('Anti-Nuke').setStyle(security.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), - new ButtonBuilder().setCustomId(`security_toggle:raid:${interaction.user.id}`).setLabel('Anti-Raid').setStyle(security.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), - new ButtonBuilder().setCustomId(`security_toggle:automod:${interaction.user.id}`).setLabel('AutoMod').setStyle(security.autoMod.enabled ? ButtonStyle.Success : ButtonStyle.Danger), - new ButtonBuilder().setCustomId(`security_refresh:${interaction.user.id}`).setLabel('Refresh').setStyle(ButtonStyle.Secondary), - ); - const row2 = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId(`security_whitelist:${interaction.user.id}`).setLabel('Whitelist').setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId(`security_logging:${interaction.user.id}`).setLabel('Logging').setStyle(ButtonStyle.Primary), - ); - await interaction.reply({ embeds: [dashboard(security)], components: [row1, row2], ephemeral: true }); + await interaction.reply({ + embeds: [dashboard(security, interaction.guild)], + components: controls(interaction.user.id), + flags: MessageFlags.Ephemeral, + }); }, }; From cf53468613a5278642e7742864befc845587237d Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:27:36 +0300 Subject: [PATCH 071/219] Redesign security dashboard UI --- src/commands/Security/security.js | 88 ++++++++++++++----------------- 1 file changed, 39 insertions(+), 49 deletions(-) diff --git a/src/commands/Security/security.js b/src/commands/Security/security.js index f4a40ca39b..00a5eb1f0c 100644 --- a/src/commands/Security/security.js +++ b/src/commands/Security/security.js @@ -2,54 +2,52 @@ import { SlashCommandBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuild import { getSecurityConfig } from '../../services/security/securityService.js'; const BUTTONS = [ - ['security_panel_nuke', 'Anti-Nuke'], - ['security_panel_raid', 'Anti-Raid'], - ['security_panel_automod', 'AutoMod'], - ['security_panel_punishments', 'Punishments'], - ['security_panel_whitelist', 'Whitelist'], - ['security_panel_logs', 'Logs'], - ['security_panel_settings', 'Settings'], + ['security_panel_nuke', '🛡️ Anti-Nuke'], + ['security_panel_raid', '🚨 Anti-Raid'], + ['security_panel_automod', '🤖 AutoMod'], + ['security_panel_punishments', '⚖️ Punishments'], + ['security_panel_whitelist', '👤 Whitelist'], + ['security_panel_logs', '📋 Logs'], + ['security_panel_settings', '⚙️ Settings'], ]; +function status(enabled) { + return enabled ? '🟢 **ACTIVE**' : '🔴 **OFF**'; +} + function dashboard(config, guild) { + const systems = [config.antiNuke?.enabled, config.antiRaid?.enabled, config.autoMod?.enabled, config.enabled]; + const active = systems.filter(Boolean).length; + const score = Math.round((active / systems.length) * 100); + const scoreLabel = score >= 100 ? '🟢 Maximum' : score >= 75 ? '🟡 Strong' : score >= 50 ? '🟠 Partial' : '🔴 Weak'; + const whitelistCount = (config.whitelist?.users?.length || 0) + (config.whitelist?.roles?.length || 0) + (config.whitelist?.bots?.length || 0); + const logValue = config.logChannelId ? `<#${config.logChannelId}>` : '`Not configured`'; + return new EmbedBuilder() - .setTitle('🛡️ Infinity Security Dashboard') - .setDescription(`Security control center for **${guild.name}**. All settings are stored persistently.`) - .setColor(0x5865F2) + .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) + .setTitle('🛡️ Server Protection') + .setDescription([`**${guild.name}**`, 'A centralized control center for your server security.', '', `Security health: **${score}%** ${scoreLabel}`].join('\n')) + .setColor(score >= 100 ? 0x57F287 : score >= 75 ? 0xFEE75C : score >= 50 ? 0xF47B67 : 0xED4245) + .setThumbnail(guild.iconURL({ size: 256 }) || null) .addFields( - { name: '🛡️ Anti-Nuke', value: config.antiNuke.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '🚨 Anti-Raid', value: config.antiRaid.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '🤖 AutoMod', value: config.autoMod.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '⚡ Punishments', value: `${config.escalation.length} escalation levels`, inline: true }, - { name: '👤 Whitelist', value: `${config.whitelist.users.length} users / ${config.whitelist.roles.length} roles / ${config.whitelist.bots.length} bots`, inline: true }, - { name: '📋 Logs', value: config.logChannelId ? `<#${config.logChannelId}>` : 'Not configured', inline: true }, - { name: '⏱️ Strike Decay', value: `${Math.round((config.strikeDecayMs || 86400000) / 3600000)}h`, inline: true }, - { name: '🔒 Global Security', value: config.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, + { name: '━━ Core Protection ━━', value: [`🛡️ Anti-Nuke ${status(config.antiNuke?.enabled)}`, `🚨 Anti-Raid ${status(config.antiRaid?.enabled)}`, `🤖 AutoMod ${status(config.autoMod?.enabled)}`, `🔒 Global ${status(config.enabled)}`].join('\n') }, + { name: '━━ Protection Stats ━━', value: [`⚡ **${config.escalation?.length || 0}** punishment levels`, `👤 **${whitelistCount}** whitelist entries`, `⏱️ **${Math.round((config.strikeDecayMs || 86400000) / 3600000)}h** strike decay`, `📋 Logs: ${logValue}`].join('\n') }, + { name: '━━ Current Mode ━━', value: config.enabled ? '🟢 **PROTECTED** — Security systems are monitoring the server.' : '🔴 **DISABLED** — Global security protection is currently off.' }, ) - .setFooter({ text: 'Only the moderator who opened this panel can use its controls.' }); + .setFooter({ text: 'Infinity System • Security Center • Changes are saved automatically' }) + .setTimestamp(); } function controls(userId) { - const rows = []; - for (let i = 0; i < BUTTONS.length; i += 4) { - rows.push( - new ActionRowBuilder().addComponents( - ...BUTTONS.slice(i, i + 4).map(([id, label]) => - new ButtonBuilder() - .setCustomId(`${id}:${userId}`) - .setLabel(label) - .setStyle( - label === 'Punishments' - ? ButtonStyle.Danger - : label === 'Settings' - ? ButtonStyle.Secondary - : ButtonStyle.Primary, - ), - ), - ), - ); - } - return rows; + return [ + new ActionRowBuilder().addComponents( + ...BUTTONS.slice(0, 4).map(([id, label], index) => new ButtonBuilder().setCustomId(`${id}:${userId}`).setLabel(label).setStyle(index === 0 ? ButtonStyle.Danger : ButtonStyle.Primary)), + ), + new ActionRowBuilder().addComponents( + ...BUTTONS.slice(4).map(([id, label]) => new ButtonBuilder().setCustomId(`${id}:${userId}`).setLabel(label).setStyle(label.includes('Settings') ? ButtonStyle.Secondary : ButtonStyle.Primary)), + new ButtonBuilder().setCustomId(`security_refresh:${userId}`).setLabel('🔄 Refresh').setStyle(ButtonStyle.Success), + ), + ]; } export default { @@ -62,18 +60,10 @@ export default { async execute(interaction, config, client) { if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - return interaction.reply({ - content: 'You need Manage Server permission.', - flags: MessageFlags.Ephemeral, - }); + return interaction.reply({ content: 'You need Manage Server permission.', flags: MessageFlags.Ephemeral }); } const security = await getSecurityConfig(client, interaction.guildId); - - await interaction.reply({ - embeds: [dashboard(security, interaction.guild)], - components: controls(interaction.user.id), - flags: MessageFlags.Ephemeral, - }); + await interaction.reply({ embeds: [dashboard(security, interaction.guild)], components: controls(interaction.user.id), flags: MessageFlags.Ephemeral }); }, }; From f7f27ed86b7d37177e39172df907a75fef15685b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:27:47 +0300 Subject: [PATCH 072/219] Export security dashboard builders --- src/commands/Security/security.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/Security/security.js b/src/commands/Security/security.js index 00a5eb1f0c..a0ab5c7ab2 100644 --- a/src/commands/Security/security.js +++ b/src/commands/Security/security.js @@ -15,7 +15,7 @@ function status(enabled) { return enabled ? '🟢 **ACTIVE**' : '🔴 **OFF**'; } -function dashboard(config, guild) { +export function buildSecurityDashboard(config, guild) { const systems = [config.antiNuke?.enabled, config.antiRaid?.enabled, config.autoMod?.enabled, config.enabled]; const active = systems.filter(Boolean).length; const score = Math.round((active / systems.length) * 100); @@ -38,7 +38,7 @@ function dashboard(config, guild) { .setTimestamp(); } -function controls(userId) { +export function buildSecurityControls(userId) { return [ new ActionRowBuilder().addComponents( ...BUTTONS.slice(0, 4).map(([id, label], index) => new ButtonBuilder().setCustomId(`${id}:${userId}`).setLabel(label).setStyle(index === 0 ? ButtonStyle.Danger : ButtonStyle.Primary)), @@ -64,6 +64,6 @@ export default { } const security = await getSecurityConfig(client, interaction.guildId); - await interaction.reply({ embeds: [dashboard(security, interaction.guild)], components: controls(interaction.user.id), flags: MessageFlags.Ephemeral }); + await interaction.reply({ embeds: [buildSecurityDashboard(security, interaction.guild)], components: buildSecurityControls(interaction.user.id), flags: MessageFlags.Ephemeral }); }, }; From 2e0519315d97041be2788731f941d7477463ddc7 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:28:04 +0300 Subject: [PATCH 073/219] Use redesigned security dashboard across interactions --- src/handlers/securityHandlers.js | 305 ++++++++++++++++++++++++------- 1 file changed, 243 insertions(+), 62 deletions(-) diff --git a/src/handlers/securityHandlers.js b/src/handlers/securityHandlers.js index 6461283d5e..9df688e819 100644 --- a/src/handlers/securityHandlers.js +++ b/src/handlers/securityHandlers.js @@ -1,83 +1,264 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; +import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; +import { buildSecurityDashboard, buildSecurityControls } from '../commands/Security/security.js'; const BUTTONS = [ - ['security_panel_nuke', 'Anti-Nuke'], ['security_panel_raid', 'Anti-Raid'], - ['security_panel_automod', 'AutoMod'], ['security_panel_punishments', 'Punishments'], - ['security_panel_whitelist', 'Whitelist'], ['security_panel_logs', 'Logs'], - ['security_panel_settings', 'Settings'], + 'security_panel_nuke', + 'security_panel_raid', + 'security_panel_automod', + 'security_panel_punishments', + 'security_panel_whitelist', + 'security_panel_logs', + 'security_panel_settings', ]; -function dashboard(config, guild) { - return new EmbedBuilder().setTitle('🛡️ Infinity Security Dashboard') - .setDescription(`Security control center for **${guild.name}**. All settings are stored persistently.`) - .setColor(0x5865F2) - .addFields( - { name: '🛡️ Anti-Nuke', value: config.antiNuke.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '🚨 Anti-Raid', value: config.antiRaid.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '🤖 AutoMod', value: config.autoMod.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - { name: '⚡ Punishments', value: `${config.escalation.length} escalation levels`, inline: true }, - { name: '👤 Whitelist', value: `${config.whitelist.users.length} users / ${config.whitelist.roles.length} roles / ${config.whitelist.bots.length} bots`, inline: true }, - { name: '📋 Logs', value: config.logChannelId ? `<#${config.logChannelId}>` : 'Not configured', inline: true }, - { name: '⏱️ Strike Decay', value: `${Math.round((config.strikeDecayMs || 86400000) / 3600000)}h`, inline: true }, - { name: '🔒 Global Security', value: config.enabled ? '🟢 Enabled' : '🔴 Disabled', inline: true }, - ).setFooter({ text: 'Only the moderator who opened this panel can use its controls.' }); +function authorized(interaction) { + return interaction.customId.split(':').at(-1) === interaction.user.id; } -function controls(userId, disabled = false) { - const rows = []; - for (let i = 0; i < BUTTONS.length; i += 4) { - rows.push(new ActionRowBuilder().addComponents(...BUTTONS.slice(i, i + 4).map(([id, label]) => new ButtonBuilder() - .setCustomId(`${id}:${userId}`).setLabel(label) - .setStyle(label === 'Punishments' ? ButtonStyle.Danger : label === 'Settings' ? ButtonStyle.Secondary : ButtonStyle.Primary) - .setDisabled(disabled)))); - } - return rows; +function reject(interaction) { + return interaction.reply({ + content: 'This security dashboard belongs to another moderator.', + flags: MessageFlags.Ephemeral, + }); +} + +function lines(value) { + return String(value || '').split(/[\s,]+/).map(v => v.trim()).filter(Boolean); +} + +function num(value, fallback, min = 0) { + const n = Number(value); + return Number.isFinite(n) ? Math.max(min, n) : fallback; +} + +function bool(value, fallback) { + const v = String(value ?? '').trim().toLowerCase(); + return v === 'true' ? true : v === 'false' ? false : fallback; } -function authorized(interaction) { return interaction.customId.split(':').at(-1) === interaction.user.id; } -function reject(interaction) { return interaction.reply({ content: 'This security dashboard belongs to another moderator.', flags: MessageFlags.Ephemeral }); } function input(id, label, value = '', style = TextInputStyle.Short) { - const x = new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false); - if (value !== undefined && value !== null && String(value)) x.setValue(String(value).slice(0, 4000)); - return new ActionRowBuilder().addComponents(x); + const field = new TextInputBuilder() + .setCustomId(id) + .setLabel(label) + .setStyle(style) + .setRequired(false); + + if (value !== undefined && value !== null && String(value)) { + field.setValue(String(value).slice(0, 4000)); + } + + return new ActionRowBuilder().addComponents(field); } -function modal(interaction, id, title, rows) { - return interaction.showModal(new ModalBuilder().setCustomId(`${id}:${interaction.user.id}`).setTitle(title).addComponents(...rows)); + +function openModal(interaction, id, title, rows) { + return interaction.showModal( + new ModalBuilder() + .setCustomId(`${id}:${interaction.user.id}`) + .setTitle(title) + .addComponents(...rows), + ); } -function lines(value) { return String(value || '').split(/[\s,]+/).map(v => v.trim()).filter(Boolean); } -function num(value, fallback, min = 0) { const n = Number(value); return Number.isFinite(n) ? Math.max(min, n) : fallback; } -function bool(value, fallback) { const v = String(value ?? '').trim().toLowerCase(); return v === 'true' ? true : v === 'false' ? false : fallback; } -function escalationText(config) { return (config.escalation || []).map(e => `${e.strike}:${e.action}:${e.durationMs || 0}`).join('\n'); } + +function escalationText(config) { + return (config.escalation || []) + .map(e => `${e.strike}:${e.action}:${e.durationMs || 0}`) + .join('\n'); +} + function parseEscalation(value) { const allowed = new Set(['warn', 'timeout', 'kick', 'ban']); - return String(value || '').split('\n').map(x => x.trim()).filter(Boolean).map(line => { - const [s, a, d] = line.split(':').map(x => x.trim()); - const strike = Number(s); - if (!Number.isInteger(strike) || strike < 1 || strike > 10 || !allowed.has(a)) return null; - return { strike, action: a, durationMs: num(d, 0) }; - }).filter(Boolean).sort((a, b) => a.strike - b.strike).slice(0, 10); + return String(value || '') + .split('\n') + .map(x => x.trim()) + .filter(Boolean) + .map(line => { + const [strikeValue, action, duration] = line.split(':').map(x => x.trim()); + const strike = Number(strikeValue); + if (!Number.isInteger(strike) || strike < 1 || strike > 10 || !allowed.has(action)) return null; + return { strike, action, durationMs: num(duration, 0) }; + }) + .filter(Boolean) + .sort((a, b) => a.strike - b.strike) + .slice(0, 10); } -const buttonHandlers = [ - { name: 'security_panel_nuke', async execute(i, c) { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_nuke_modal', 'Anti-Nuke Settings', [input('enabled','Enabled: true/false',x.antiNuke.enabled),input('window','Detection window ms',x.antiNuke.windowMs),input('thresholds','One threshold per line: key=value',Object.entries(x.antiNuke.thresholds).map(([k,v])=>`${k}=${v}`).join('\n'),TextInputStyle.Paragraph),input('action','Action: strip, kick, ban',x.antiNuke.action),input('lockdown','Lockdown alert: true/false',x.antiNuke.lockdown)]); } }, - { name: 'security_panel_raid', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_raid_modal','Anti-Raid Settings',[input('enabled','Enabled: true/false',x.antiRaid.enabled),input('joins','Joins required',x.antiRaid.joins),input('window','Join window ms',x.antiRaid.windowMs),input('accountAge','Minimum account age hours',Math.round(x.antiRaid.minAccountAgeMs/3600000)),input('action','Action: timeout or kick',x.antiRaid.action)]); } }, - { name: 'security_panel_automod', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_automod_modal','AutoMod Settings',[input('enabled','Enabled: true/false',x.autoMod.enabled),input('spam','Spam: enabled,maxMessages,windowMs',`${x.autoMod.spam.enabled},${x.autoMod.spam.maxMessages},${x.autoMod.spam.windowMs}`),input('duplicate','Duplicate: enabled,maxRepeats,windowMs',`${x.autoMod.duplicate.enabled},${x.autoMod.duplicate.maxRepeats},${x.autoMod.duplicate.windowMs}`),input('content','Mentions,invites,links,caps',''+x.autoMod.mentions.max+','+x.autoMod.invites.enabled+','+x.autoMod.links.enabled+','+x.autoMod.caps.enabled),input('badWords','Bad words, one per line',(x.autoMod.badWords.words||[]).join('\n'),TextInputStyle.Paragraph)]); } }, - { name: 'security_panel_punishments', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_punishments_modal','Punishment Escalation',[input('levels','strike:action:durationMs per line',escalationText(x),TextInputStyle.Paragraph),input('decay','Strike decay hours',Math.round((x.strikeDecayMs||86400000)/3600000))]); } }, - { name: 'security_panel_whitelist', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_whitelist_modal','Security Whitelist',[input('users','User IDs',(x.whitelist.users||[]).join('\n'),TextInputStyle.Paragraph),input('roles','Role IDs',(x.whitelist.roles||[]).join('\n'),TextInputStyle.Paragraph),input('bots','Bot IDs',(x.whitelist.bots||[]).join('\n'),TextInputStyle.Paragraph)]); } }, - { name: 'security_panel_logs', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_logs_modal','Security Logs',[input('channel','Security log channel ID',x.logChannelId||''),input('ignored','Ignored channel IDs',(x.ignoredChannels||[]).join('\n'),TextInputStyle.Paragraph)]); } }, - { name: 'security_panel_settings', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return modal(i,'security_settings_modal','Security Settings',[input('enabled','Global security enabled: true/false',x.enabled),input('info','Ignored by parser','No value needed')]); } }, - { name: 'security_refresh', async execute(i,c) { if (!authorized(i)) return reject(i); const x=await getSecurityConfig(c,i.guildId); return i.update({embeds:[dashboard(x,i.guild)],components:controls(i.user.id)}); } }, -]; +async function showUpdated(interaction, client, config) { + return interaction.reply({ + embeds: [buildSecurityDashboard(config, interaction.guild)], + components: buildSecurityControls(interaction.user.id), + flags: MessageFlags.Ephemeral, + }); +} + +const buttonHandlers = BUTTONS.map(name => ({ + name, + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + + const config = await getSecurityConfig(client, interaction.guildId); + + const modals = { + security_panel_nuke: ['security_nuke_modal', '🛡️ Anti-Nuke Configuration', [ + input('enabled', 'Enabled: true/false', config.antiNuke.enabled), + input('window', 'Detection window (ms)', config.antiNuke.windowMs), + input('thresholds', 'Thresholds: key=value per line', Object.entries(config.antiNuke.thresholds).map(([k, v]) => `${k}=${v}`).join('\n'), TextInputStyle.Paragraph), + input('action', 'Action: strip, kick, ban', config.antiNuke.action), + input('lockdown', 'Lockdown: true/false', config.antiNuke.lockdown), + ]], + security_panel_raid: ['security_raid_modal', '🚨 Anti-Raid Configuration', [ + input('enabled', 'Enabled: true/false', config.antiRaid.enabled), + input('joins', 'Joins required', config.antiRaid.joins), + input('window', 'Join window (ms)', config.antiRaid.windowMs), + input('accountAge', 'Minimum account age (hours)', Math.round(config.antiRaid.minAccountAgeMs / 3600000)), + input('action', 'Action: timeout or kick', config.antiRaid.action), + ]], + security_panel_automod: ['security_automod_modal', '🤖 AutoMod Configuration', [ + input('enabled', 'Enabled: true/false', config.autoMod.enabled), + input('spam', 'Spam: enabled,maxMessages,windowMs', `${config.autoMod.spam.enabled},${config.autoMod.spam.maxMessages},${config.autoMod.spam.windowMs}`), + input('duplicate', 'Duplicate: enabled,maxRepeats,windowMs', `${config.autoMod.duplicate.enabled},${config.autoMod.duplicate.maxRepeats},${config.autoMod.duplicate.windowMs}`), + input('content', 'Mentions, invites, links, caps', `${config.autoMod.mentions.max},${config.autoMod.invites.enabled},${config.autoMod.links.enabled},${config.autoMod.caps.enabled}`), + input('badWords', 'Blocked words (one per line)', (config.autoMod.badWords.words || []).join('\n'), TextInputStyle.Paragraph), + ]], + security_panel_punishments: ['security_punishments_modal', '⚖️ Punishment Escalation', [ + input('levels', 'strike:action:durationMs per line', escalationText(config), TextInputStyle.Paragraph), + input('decay', 'Strike decay (hours)', Math.round((config.strikeDecayMs || 86400000) / 3600000)), + ]], + security_panel_whitelist: ['security_whitelist_modal', '👤 Security Whitelist', [ + input('users', 'User IDs', (config.whitelist.users || []).join('\n'), TextInputStyle.Paragraph), + input('roles', 'Role IDs', (config.whitelist.roles || []).join('\n'), TextInputStyle.Paragraph), + input('bots', 'Bot IDs', (config.whitelist.bots || []).join('\n'), TextInputStyle.Paragraph), + ]], + security_panel_logs: ['security_logs_modal', '📋 Security Logs', [ + input('channel', 'Security log channel ID', config.logChannelId || ''), + input('ignored', 'Ignored channel IDs', (config.ignoredChannels || []).join('\n'), TextInputStyle.Paragraph), + ]], + security_panel_settings: ['security_settings_modal', '⚙️ Security Settings', [ + input('enabled', 'Global security enabled: true/false', config.enabled), + ]], + }; + + const [id, title, rows] = modals[name]; + return openModal(interaction, id, title, rows); + }, +})); + +buttonHandlers.push({ + name: 'security_refresh', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + return interaction.update({ + embeds: [buildSecurityDashboard(config, interaction.guild)], + components: buildSecurityControls(interaction.user.id), + }); + }, +}); const modalHandlers = [ - { name:'security_nuke_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const thresholds={...x.antiNuke.thresholds}; for(const line of i.fields.getTextInputValue('thresholds').split('\n')){const [k,v]=line.split('=').map(z=>z.trim());if(Object.prototype.hasOwnProperty.call(thresholds,k))thresholds[k]=num(v,thresholds[k],1);} const action=i.fields.getTextInputValue('action').trim().toLowerCase(); const u=await updateSecurityConfig(c,i.guildId,{antiNuke:{enabled:bool(i.fields.getTextInputValue('enabled'),x.antiNuke.enabled),windowMs:num(i.fields.getTextInputValue('window'),x.antiNuke.windowMs,1000),thresholds,action:['strip','kick','ban'].includes(action)?action:x.antiNuke.action,lockdown:bool(i.fields.getTextInputValue('lockdown'),x.antiNuke.lockdown)}}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, - { name:'security_raid_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const a=i.fields.getTextInputValue('action').trim().toLowerCase(); const u=await updateSecurityConfig(c,i.guildId,{antiRaid:{enabled:bool(i.fields.getTextInputValue('enabled'),x.antiRaid.enabled),joins:num(i.fields.getTextInputValue('joins'),x.antiRaid.joins,2),windowMs:num(i.fields.getTextInputValue('window'),x.antiRaid.windowMs,1000),minAccountAgeMs:num(i.fields.getTextInputValue('accountAge'),x.antiRaid.minAccountAgeMs/3600000,0)*3600000,action:['timeout','kick'].includes(a)?a:x.antiRaid.action}}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, - { name:'security_automod_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const [se,sm,sw]=i.fields.getTextInputValue('spam').split(','); const [de,dm,dw]=i.fields.getTextInputValue('duplicate').split(','); const [mm,inv,ln,cp]=i.fields.getTextInputValue('content').split(','); const u=await updateSecurityConfig(c,i.guildId,{autoMod:{enabled:bool(i.fields.getTextInputValue('enabled'),x.autoMod.enabled),spam:{enabled:bool(se,x.autoMod.spam.enabled),maxMessages:num(sm,x.autoMod.spam.maxMessages,2),windowMs:num(sw,x.autoMod.spam.windowMs,1000)},duplicate:{enabled:bool(de,x.autoMod.duplicate.enabled),maxRepeats:num(dm,x.autoMod.duplicate.maxRepeats,2),windowMs:num(dw,x.autoMod.duplicate.windowMs,1000)},mentions:{enabled:true,max:num(mm,x.autoMod.mentions.max,1)},invites:{enabled:bool(inv,x.autoMod.invites.enabled)},links:{enabled:bool(ln,x.autoMod.links.enabled)},caps:{...x.autoMod.caps,enabled:bool(cp,x.autoMod.caps.enabled)},badWords:{enabled:true,words:lines(i.fields.getTextInputValue('badWords')).slice(0,100)}}}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, - { name:'security_punishments_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const parsed=parseEscalation(i.fields.getTextInputValue('levels')); const u=await updateSecurityConfig(c,i.guildId,{escalation:parsed.length?parsed:x.escalation,strikeDecayMs:num(i.fields.getTextInputValue('decay'),24,1)*3600000}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, - { name:'security_whitelist_modal', async execute(i,c){ if(!authorized(i))return reject(i); const u=await updateSecurityConfig(c,i.guildId,{whitelist:{users:lines(i.fields.getTextInputValue('users')),roles:lines(i.fields.getTextInputValue('roles')),bots:lines(i.fields.getTextInputValue('bots'))}}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, - { name:'security_logs_modal', async execute(i,c){ if(!authorized(i))return reject(i); const u=await updateSecurityConfig(c,i.guildId,{logChannelId:i.fields.getTextInputValue('channel').trim()||null,ignoredChannels:lines(i.fields.getTextInputValue('ignored'))}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, - { name:'security_settings_modal', async execute(i,c){ if(!authorized(i))return reject(i); const x=await getSecurityConfig(c,i.guildId); const u=await updateSecurityConfig(c,i.guildId,{enabled:bool(i.fields.getTextInputValue('enabled'),x.enabled)}); return i.reply({embeds:[dashboard(u,i.guild)],components:controls(i.user.id),flags:MessageFlags.Ephemeral}); } }, + { + name: 'security_nuke_modal', + async execute(i, c) { + if (!authorized(i)) return reject(i); + const x = await getSecurityConfig(c, i.guildId); + const thresholds = { ...x.antiNuke.thresholds }; + for (const line of i.fields.getTextInputValue('thresholds').split('\n')) { + const [key, value] = line.split('=').map(v => v.trim()); + if (Object.prototype.hasOwnProperty.call(thresholds, key)) thresholds[key] = num(value, thresholds[key], 1); + } + const action = i.fields.getTextInputValue('action').trim().toLowerCase(); + const u = await updateSecurityConfig(c, i.guildId, { antiNuke: { + enabled: bool(i.fields.getTextInputValue('enabled'), x.antiNuke.enabled), + windowMs: num(i.fields.getTextInputValue('window'), x.antiNuke.windowMs, 1000), + thresholds, + action: ['strip', 'kick', 'ban'].includes(action) ? action : x.antiNuke.action, + lockdown: bool(i.fields.getTextInputValue('lockdown'), x.antiNuke.lockdown), + }}); + return showUpdated(i, c, u); + }, + }, + { + name: 'security_raid_modal', + async execute(i, c) { + if (!authorized(i)) return reject(i); + const x = await getSecurityConfig(c, i.guildId); + const action = i.fields.getTextInputValue('action').trim().toLowerCase(); + const u = await updateSecurityConfig(c, i.guildId, { antiRaid: { + enabled: bool(i.fields.getTextInputValue('enabled'), x.antiRaid.enabled), + joins: num(i.fields.getTextInputValue('joins'), x.antiRaid.joins, 2), + windowMs: num(i.fields.getTextInputValue('window'), x.antiRaid.windowMs, 1000), + minAccountAgeMs: num(i.fields.getTextInputValue('accountAge'), x.antiRaid.minAccountAgeMs / 3600000, 0) * 3600000, + action: ['timeout', 'kick'].includes(action) ? action : x.antiRaid.action, + }}); + return showUpdated(i, c, u); + }, + }, + { + name: 'security_automod_modal', + async execute(i, c) { + if (!authorized(i)) return reject(i); + const x = await getSecurityConfig(c, i.guildId); + const [spamEnabled, spamMax, spamWindow] = i.fields.getTextInputValue('spam').split(','); + const [dupEnabled, dupMax, dupWindow] = i.fields.getTextInputValue('duplicate').split(','); + const [mentionMax, invites, links, caps] = i.fields.getTextInputValue('content').split(','); + const u = await updateSecurityConfig(c, i.guildId, { autoMod: { + enabled: bool(i.fields.getTextInputValue('enabled'), x.autoMod.enabled), + spam: { enabled: bool(spamEnabled, x.autoMod.spam.enabled), maxMessages: num(spamMax, x.autoMod.spam.maxMessages, 2), windowMs: num(spamWindow, x.autoMod.spam.windowMs, 1000) }, + duplicate: { enabled: bool(dupEnabled, x.autoMod.duplicate.enabled), maxRepeats: num(dupMax, x.autoMod.duplicate.maxRepeats, 2), windowMs: num(dupWindow, x.autoMod.duplicate.windowMs, 1000) }, + mentions: { enabled: true, max: num(mentionMax, x.autoMod.mentions.max, 1) }, + invites: { enabled: bool(invites, x.autoMod.invites.enabled) }, + links: { enabled: bool(links, x.autoMod.links.enabled) }, + caps: { ...x.autoMod.caps, enabled: bool(caps, x.autoMod.caps.enabled) }, + badWords: { enabled: true, words: lines(i.fields.getTextInputValue('badWords')).slice(0, 100) }, + }}); + return showUpdated(i, c, u); + }, + }, + { + name: 'security_punishments_modal', + async execute(i, c) { + if (!authorized(i)) return reject(i); + const x = await getSecurityConfig(c, i.guildId); + const parsed = parseEscalation(i.fields.getTextInputValue('levels')); + const u = await updateSecurityConfig(c, i.guildId, { + escalation: parsed.length ? parsed : x.escalation, + strikeDecayMs: num(i.fields.getTextInputValue('decay'), 24, 1) * 3600000, + }); + return showUpdated(i, c, u); + }, + }, + { + name: 'security_whitelist_modal', + async execute(i, c) { + if (!authorized(i)) return reject(i); + const u = await updateSecurityConfig(c, i.guildId, { whitelist: { + users: lines(i.fields.getTextInputValue('users')), + roles: lines(i.fields.getTextInputValue('roles')), + bots: lines(i.fields.getTextInputValue('bots')), + }}); + return showUpdated(i, c, u); + }, + }, + { + name: 'security_logs_modal', + async execute(i, c) { + if (!authorized(i)) return reject(i); + const u = await updateSecurityConfig(c, i.guildId, { + logChannelId: i.fields.getTextInputValue('channel').trim() || null, + ignoredChannels: lines(i.fields.getTextInputValue('ignored')), + }); + return showUpdated(i, c, u); + }, + }, + { + name: 'security_settings_modal', + async execute(i, c) { + if (!authorized(i)) return reject(i); + const x = await getSecurityConfig(c, i.guildId); + const u = await updateSecurityConfig(c, i.guildId, { enabled: bool(i.fields.getTextInputValue('enabled'), x.enabled) }); + return showUpdated(i, c, u); + }, + }, ]; export const securityButtonHandlers = buttonHandlers; From f467a411f8ae0ae1f4d3ccd59b7b7da2014cdc3b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:39:06 +0300 Subject: [PATCH 074/219] Improve security dashboard with inline control panels --- src/commands/Security/security.js | 170 ++++++++++++++++++++++++------ 1 file changed, 135 insertions(+), 35 deletions(-) diff --git a/src/commands/Security/security.js b/src/commands/Security/security.js index a0ab5c7ab2..b6ce055630 100644 --- a/src/commands/Security/security.js +++ b/src/commands/Security/security.js @@ -1,68 +1,168 @@ -import { SlashCommandBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageFlags } from 'discord.js'; +import { + SlashCommandBuilder, + PermissionFlagsBits, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + MessageFlags, +} from 'discord.js'; import { getSecurityConfig } from '../../services/security/securityService.js'; -const BUTTONS = [ - ['security_panel_nuke', '🛡️ Anti-Nuke'], - ['security_panel_raid', '🚨 Anti-Raid'], - ['security_panel_automod', '🤖 AutoMod'], - ['security_panel_punishments', '⚖️ Punishments'], - ['security_panel_whitelist', '👤 Whitelist'], - ['security_panel_logs', '📋 Logs'], - ['security_panel_settings', '⚙️ Settings'], +const PANEL_META = { + nuke: ['🛡️ Anti-Nuke', 'Protects channels, roles, webhooks, bans and dangerous server changes.'], + raid: ['🚨 Anti-Raid', 'Detects rapid joins and suspicious new accounts.'], + automod: ['🤖 AutoMod', 'Stops spam, duplicates, mentions, invites, links and other abuse.'], + punishments: ['⚖️ Punishments', 'Configure escalating strikes and automatic punishments.'], + whitelist: ['👤 Whitelist', 'Trusted users, roles and bots bypass security actions.'], + logs: ['📋 Logs', 'Choose where security incidents are reported.'], + settings: ['⚙️ Settings', 'Global protection and security behavior.'], +}; + +const MAIN_BUTTONS = [ + ['security_panel_nuke', '🛡️ Anti-Nuke', ButtonStyle.Danger], + ['security_panel_raid', '🚨 Anti-Raid', ButtonStyle.Primary], + ['security_panel_automod', '🤖 AutoMod', ButtonStyle.Primary], + ['security_panel_punishments', '⚖️ Punishments', ButtonStyle.Primary], + ['security_panel_whitelist', '👤 Whitelist', ButtonStyle.Secondary], + ['security_panel_logs', '📋 Logs', ButtonStyle.Secondary], + ['security_panel_settings', '⚙️ Settings', ButtonStyle.Secondary], ]; -function status(enabled) { - return enabled ? '🟢 **ACTIVE**' : '🔴 **OFF**'; +function status(enabled) { return enabled ? '🟢 **ACTIVE**' : '🔴 **OFF**'; } +function boolLabel(value) { return value ? '🟢 ON' : '🔴 OFF'; } +function hours(ms) { return Math.max(0, Math.round(Number(ms || 0) / 3600000)); } +function button(id, label, style = ButtonStyle.Secondary, disabled = false) { + return new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style).setDisabled(disabled); } export function buildSecurityDashboard(config, guild) { - const systems = [config.antiNuke?.enabled, config.antiRaid?.enabled, config.autoMod?.enabled, config.enabled]; + const systems = [config.enabled, config.antiNuke?.enabled, config.antiRaid?.enabled, config.autoMod?.enabled]; const active = systems.filter(Boolean).length; const score = Math.round((active / systems.length) * 100); - const scoreLabel = score >= 100 ? '🟢 Maximum' : score >= 75 ? '🟡 Strong' : score >= 50 ? '🟠 Partial' : '🔴 Weak'; + const scoreLabel = score >= 100 ? 'Maximum' : score >= 75 ? 'Strong' : score >= 50 ? 'Partial' : 'Weak'; const whitelistCount = (config.whitelist?.users?.length || 0) + (config.whitelist?.roles?.length || 0) + (config.whitelist?.bots?.length || 0); const logValue = config.logChannelId ? `<#${config.logChannelId}>` : '`Not configured`'; return new EmbedBuilder() .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) .setTitle('🛡️ Server Protection') - .setDescription([`**${guild.name}**`, 'A centralized control center for your server security.', '', `Security health: **${score}%** ${scoreLabel}`].join('\n')) - .setColor(score >= 100 ? 0x57F287 : score >= 75 ? 0xFEE75C : score >= 50 ? 0xF47B67 : 0xED4245) + .setDescription(`**${guild.name}**\n\nCentralized security controls for your server.\n\n**Security Health:** ${score}% • **${scoreLabel}**`) + .setColor(score >= 100 ? 0x57f287 : score >= 75 ? 0xfee75c : score >= 50 ? 0xf47b67 : 0xed4245) .setThumbnail(guild.iconURL({ size: 256 }) || null) .addFields( - { name: '━━ Core Protection ━━', value: [`🛡️ Anti-Nuke ${status(config.antiNuke?.enabled)}`, `🚨 Anti-Raid ${status(config.antiRaid?.enabled)}`, `🤖 AutoMod ${status(config.autoMod?.enabled)}`, `🔒 Global ${status(config.enabled)}`].join('\n') }, - { name: '━━ Protection Stats ━━', value: [`⚡ **${config.escalation?.length || 0}** punishment levels`, `👤 **${whitelistCount}** whitelist entries`, `⏱️ **${Math.round((config.strikeDecayMs || 86400000) / 3600000)}h** strike decay`, `📋 Logs: ${logValue}`].join('\n') }, - { name: '━━ Current Mode ━━', value: config.enabled ? '🟢 **PROTECTED** — Security systems are monitoring the server.' : '🔴 **DISABLED** — Global security protection is currently off.' }, + { name: '━━ Protection ━━', value: [`🛡️ Anti-Nuke ${status(config.antiNuke?.enabled)}`, `🚨 Anti-Raid ${status(config.antiRaid?.enabled)}`, `🤖 AutoMod ${status(config.autoMod?.enabled)}`, `🔒 Global ${status(config.enabled)}`].join('\n'), inline: true }, + { name: '━━ Statistics ━━', value: [`⚡ **${config.escalation?.length || 0}** punishment levels`, `👤 **${whitelistCount}** whitelist entries`, `⏱️ **${hours(config.strikeDecayMs)}h** strike decay`, `📋 Logs: ${logValue}`].join('\n'), inline: true }, + { name: '━━ Current Mode ━━', value: config.enabled ? '🟢 **PROTECTED** — security systems are actively monitoring this server.' : '🔴 **DISABLED** — global security protection is currently off.' }, ) - .setFooter({ text: 'Infinity System • Security Center • Changes are saved automatically' }) + .setFooter({ text: 'Infinity System • Security Center • Changes save automatically' }) .setTimestamp(); } export function buildSecurityControls(userId) { return [ - new ActionRowBuilder().addComponents( - ...BUTTONS.slice(0, 4).map(([id, label], index) => new ButtonBuilder().setCustomId(`${id}:${userId}`).setLabel(label).setStyle(index === 0 ? ButtonStyle.Danger : ButtonStyle.Primary)), - ), - new ActionRowBuilder().addComponents( - ...BUTTONS.slice(4).map(([id, label]) => new ButtonBuilder().setCustomId(`${id}:${userId}`).setLabel(label).setStyle(label.includes('Settings') ? ButtonStyle.Secondary : ButtonStyle.Primary)), - new ButtonBuilder().setCustomId(`security_refresh:${userId}`).setLabel('🔄 Refresh').setStyle(ButtonStyle.Success), - ), + new ActionRowBuilder().addComponents(...MAIN_BUTTONS.slice(0, 4).map(([id, label, style]) => button(`${id}:${userId}`, label, style))), + new ActionRowBuilder().addComponents(...MAIN_BUTTONS.slice(4).map(([id, label, style]) => button(`${id}:${userId}`, label, style)), button(`security_refresh:${userId}`, '🔄 Refresh', ButtonStyle.Success)), ]; } +export function buildSecurityPanel(config, guild, panel) { + const [title, description] = PANEL_META[panel] || PANEL_META.settings; + const data = { + nuke: [ + `**Status:** ${status(config.antiNuke?.enabled)}`, + `**Action:** \`${config.antiNuke?.action || 'strip'}\``, + `**Detection window:** \`${Math.round((config.antiNuke?.windowMs || 10000) / 1000)}s\``, + `**Lockdown:** ${boolLabel(config.antiNuke?.lockdown)}`, + '', '**Thresholds**', + `Channels delete: \`${config.antiNuke?.thresholds?.channelDelete ?? 3}\` • Channels create: \`${config.antiNuke?.thresholds?.channelCreate ?? 5}\``, + `Roles delete: \`${config.antiNuke?.thresholds?.roleDelete ?? 3}\` • Roles create: \`${config.antiNuke?.thresholds?.roleCreate ?? 5}\``, + `Webhooks: \`${config.antiNuke?.thresholds?.webhookUpdate ?? 3}\` • Bot add: \`${config.antiNuke?.thresholds?.botAdd ?? 1}\``, + ], + raid: [ + `**Status:** ${status(config.antiRaid?.enabled)}`, + `**Join threshold:** \`${config.antiRaid?.joins ?? 8}\` members`, + `**Window:** \`${Math.round((config.antiRaid?.windowMs || 10000) / 1000)}s\``, + `**Minimum account age:** \`${hours(config.antiRaid?.minAccountAgeMs)}h\``, + `**Action:** \`${config.antiRaid?.action || 'timeout'}\``, + `**Lockdown:** ${boolLabel(config.antiRaid?.lockdown)} • **Duration:** \`${Math.round((config.antiRaid?.lockdownMs || 600000) / 60000)}m\``, + ], + automod: [ + `**Status:** ${status(config.autoMod?.enabled)}`, + `**Spam:** ${boolLabel(config.autoMod?.spam?.enabled)} • \`${config.autoMod?.spam?.maxMessages ?? 6}\` msgs / \`${Math.round((config.autoMod?.spam?.windowMs || 5000) / 1000)}s\``, + `**Duplicate:** ${boolLabel(config.autoMod?.duplicate?.enabled)} • \`${config.autoMod?.duplicate?.maxRepeats ?? 3}\` repeats`, + `**Mentions:** ${boolLabel(config.autoMod?.mentions?.enabled)} • max \`${config.autoMod?.mentions?.max ?? 6}\``, + `**Invites:** ${boolLabel(config.autoMod?.invites?.enabled)} • **Links:** ${boolLabel(config.autoMod?.links?.enabled)} • **Caps:** ${boolLabel(config.autoMod?.caps?.enabled)}`, + `**Blocked words:** \`${config.autoMod?.badWords?.words?.length || 0}\``, + `**Default action:** \`${config.autoMod?.action || 'delete'}\``, + ], + punishments: [ + `**Strike decay:** \`${hours(config.strikeDecayMs)}h\``, '', + ...(config.escalation || []).slice(0, 10).map(e => `**Strike ${e.strike}:** \`${e.action}\`${e.durationMs ? ` • ${Math.round(e.durationMs / 60000)}m` : ''}`), + ], + whitelist: [ + `**Users:** \`${config.whitelist?.users?.length || 0}\``, + `**Roles:** \`${config.whitelist?.roles?.length || 0}\``, + `**Bots:** \`${config.whitelist?.bots?.length || 0}\``, '', + 'Whitelisted accounts bypass Anti-Nuke and AutoMod enforcement where applicable.', + ], + logs: [ + `**Log channel:** ${config.logChannelId ? `<#${config.logChannelId}>` : '`Not configured`'}`, + `**Ignored channels:** \`${config.ignoredChannels?.length || 0}\``, '', + 'Security incidents include the executor/member, reason and action taken.', + ], + settings: [ + `**Global protection:** ${status(config.enabled)}`, + `**Anti-Nuke:** ${status(config.antiNuke?.enabled)}`, + `**Anti-Raid:** ${status(config.antiRaid?.enabled)}`, + `**AutoMod:** ${status(config.autoMod?.enabled)}`, '', + 'Use the controls below to change protection without leaving this message.', + ], + }[panel] || []; + + return new EmbedBuilder() + .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) + .setTitle(title) + .setDescription(`${description}\n\n${data.join('\n')}`) + .setColor(panel === 'nuke' ? 0xed4245 : panel === 'raid' ? 0xf47b67 : panel === 'automod' ? 0x5865f2 : 0x57f287) + .setFooter({ text: 'Infinity System • Click a control below • Changes save automatically' }) + .setTimestamp(); +} + +export function buildSecurityPanelControls(userId, panel, config) { + const id = (name) => `${name}:${userId}`; + const rows = []; + if (panel === 'nuke') { + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_nuke_toggle'), config.antiNuke.enabled ? '🟢 Disable' : '🔴 Enable', config.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(id('security_nuke_action'), `Action: ${config.antiNuke.action}`, ButtonStyle.Primary), button(id('security_nuke_window'), `Window: ${Math.round(config.antiNuke.windowMs / 1000)}s`))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_nuke_lockdown'), `Lockdown: ${config.antiNuke.lockdown ? 'ON' : 'OFF'}`), button(id('security_nuke_threshold'), 'Threshold Editor', ButtonStyle.Primary))); + } else if (panel === 'raid') { + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_raid_toggle'), config.antiRaid.enabled ? '🟢 Disable' : '🔴 Enable', config.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(id('security_raid_joins_down'), '− Joins'), button(id('security_raid_joins_up'), '+ Joins'), button(id('security_raid_action'), `Action: ${config.antiRaid.action}`, ButtonStyle.Primary))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_raid_window'), `Window: ${Math.round(config.antiRaid.windowMs / 1000)}s`), button(id('security_raid_age'), `Age: ${hours(config.antiRaid.minAccountAgeMs)}h`), button(id('security_raid_lockdown'), `Lockdown: ${config.antiRaid.lockdown ? 'ON' : 'OFF'}`))); + } else if (panel === 'automod') { + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_automod_toggle'), config.autoMod.enabled ? '🟢 Disable' : '🔴 Enable', config.autoMod.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(id('security_automod_spam_toggle'), `Spam ${boolLabel(config.autoMod.spam.enabled)}`, ButtonStyle.Primary), button(id('security_automod_dup_toggle'), `Duplicate ${boolLabel(config.autoMod.duplicate.enabled)}`, ButtonStyle.Primary))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_automod_spam_down'), 'Spam −'), button(id('security_automod_spam_up'), 'Spam +'), button(id('security_automod_dup_down'), 'Duplicate −'), button(id('security_automod_dup_up'), 'Duplicate +'))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_automod_mentions_down'), 'Mentions −'), button(id('security_automod_mentions_up'), 'Mentions +'), button(id('security_automod_invites'), `Invites ${boolLabel(config.autoMod.invites.enabled)}`), button(id('security_automod_links'), `Links ${boolLabel(config.autoMod.links.enabled)}`), button(id('security_automod_caps'), `Caps ${boolLabel(config.autoMod.caps.enabled)}`))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_automod_badwords'), '🚫 Manage Words', ButtonStyle.Primary), button(id('security_automod_action'), `Action: ${config.autoMod.action}`, ButtonStyle.Primary))); + } else if (panel === 'punishments') { + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_pun_decay_down'), 'Decay −'), button(id('security_pun_decay_up'), 'Decay +'))); + const levels = (config.escalation || []).slice(0, 10); + for (let i = 0; i < levels.length; i += 5) rows.push(new ActionRowBuilder().addComponents(...levels.slice(i, i + 5).map(level => button(id(`security_pun_level_${level.strike}`), `#${level.strike}: ${level.action}`, ButtonStyle.Primary)))); + } else if (panel === 'whitelist') { + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_whitelist_users'), '👤 Manage Users', ButtonStyle.Primary), button(id('security_whitelist_roles'), '🎭 Manage Roles', ButtonStyle.Primary), button(id('security_whitelist_bots'), '🤖 Manage Bots', ButtonStyle.Primary))); + } else if (panel === 'logs') { + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_logs_channel'), '📋 Set Log Channel', ButtonStyle.Primary), button(id('security_logs_ignored'), '🚫 Ignored Channels'))); + } else if (panel === 'settings') { + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_settings_toggle'), config.enabled ? '🟢 Disable Protection' : '🔴 Enable Protection', config.enabled ? ButtonStyle.Danger : ButtonStyle.Success), button(id('security_settings_refresh'), '🔄 Refresh'))); + } + return rows.slice(0, 5); +} + export default { category: 'Security', slashOnly: true, - data: new SlashCommandBuilder() - .setName('security') - .setDescription('Open the server security dashboard') - .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild.toString()), - + data: new SlashCommandBuilder().setName('security').setDescription('Open the server security dashboard').setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild.toString()), async execute(interaction, config, client) { - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - return interaction.reply({ content: 'You need Manage Server permission.', flags: MessageFlags.Ephemeral }); - } - + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return interaction.reply({ content: 'You need Manage Server permission.', flags: MessageFlags.Ephemeral }); const security = await getSecurityConfig(client, interaction.guildId); await interaction.reply({ embeds: [buildSecurityDashboard(security, interaction.guild)], components: buildSecurityControls(interaction.user.id), flags: MessageFlags.Ephemeral }); }, From ee0af58bc2e6432564916ca30a9aefc44fcaf7f2 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:39:36 +0300 Subject: [PATCH 075/219] Add inline security dashboard navigation and controls --- src/handlers/securityHandlers.js | 461 +++++++++++++++++++------------ 1 file changed, 283 insertions(+), 178 deletions(-) diff --git a/src/handlers/securityHandlers.js b/src/handlers/securityHandlers.js index 9df688e819..bc2c583cc4 100644 --- a/src/handlers/securityHandlers.js +++ b/src/handlers/securityHandlers.js @@ -1,30 +1,34 @@ -import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; +import { + ActionRowBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + MessageFlags, +} from 'discord.js'; import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; -import { buildSecurityDashboard, buildSecurityControls } from '../commands/Security/security.js'; - -const BUTTONS = [ - 'security_panel_nuke', - 'security_panel_raid', - 'security_panel_automod', - 'security_panel_punishments', - 'security_panel_whitelist', - 'security_panel_logs', - 'security_panel_settings', -]; +import { + buildSecurityDashboard, + buildSecurityControls, + buildSecurityPanel, + buildSecurityPanelControls, +} from '../commands/Security/security.js'; + +const PANELS = { + security_panel_nuke: 'nuke', + security_panel_raid: 'raid', + security_panel_automod: 'automod', + security_panel_punishments: 'punishments', + security_panel_whitelist: 'whitelist', + security_panel_logs: 'logs', + security_panel_settings: 'settings', +}; function authorized(interaction) { return interaction.customId.split(':').at(-1) === interaction.user.id; } function reject(interaction) { - return interaction.reply({ - content: 'This security dashboard belongs to another moderator.', - flags: MessageFlags.Ephemeral, - }); -} - -function lines(value) { - return String(value || '').split(/[\s,]+/).map(v => v.trim()).filter(Boolean); + return interaction.reply({ content: 'This security dashboard belongs to another moderator.', flags: MessageFlags.Ephemeral }); } function num(value, fallback, min = 0) { @@ -37,229 +41,330 @@ function bool(value, fallback) { return v === 'true' ? true : v === 'false' ? false : fallback; } -function input(id, label, value = '', style = TextInputStyle.Short) { - const field = new TextInputBuilder() - .setCustomId(id) - .setLabel(label) - .setStyle(style) - .setRequired(false); - - if (value !== undefined && value !== null && String(value)) { - field.setValue(String(value).slice(0, 4000)); - } - - return new ActionRowBuilder().addComponents(field); +function lines(value) { + return String(value || '').split(/[\s,]+/).map(v => v.trim()).filter(Boolean); } -function openModal(interaction, id, title, rows) { - return interaction.showModal( - new ModalBuilder() - .setCustomId(`${id}:${interaction.user.id}`) - .setTitle(title) - .addComponents(...rows), - ); +function field(id, label, value = '', style = TextInputStyle.Short) { + const input = new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false); + if (value !== undefined && value !== null && String(value)) input.setValue(String(value).slice(0, 4000)); + return new ActionRowBuilder().addComponents(input); } -function escalationText(config) { - return (config.escalation || []) - .map(e => `${e.strike}:${e.action}:${e.durationMs || 0}`) - .join('\n'); +function modal(interaction, id, title, fields) { + return interaction.showModal(new ModalBuilder().setCustomId(`${id}:${interaction.user.id}`).setTitle(title).addComponents(...fields)); } -function parseEscalation(value) { - const allowed = new Set(['warn', 'timeout', 'kick', 'ban']); - return String(value || '') - .split('\n') - .map(x => x.trim()) - .filter(Boolean) - .map(line => { - const [strikeValue, action, duration] = line.split(':').map(x => x.trim()); - const strike = Number(strikeValue); - if (!Number.isInteger(strike) || strike < 1 || strike > 10 || !allowed.has(action)) return null; - return { strike, action, durationMs: num(duration, 0) }; - }) - .filter(Boolean) - .sort((a, b) => a.strike - b.strike) - .slice(0, 10); +async function updatePanel(interaction, client, panel) { + const config = await getSecurityConfig(client, interaction.guildId); + return interaction.update({ + embeds: [buildSecurityPanel(config, interaction.guild, panel)], + components: buildSecurityPanelControls(interaction.user.id, panel, config), + }); } -async function showUpdated(interaction, client, config) { - return interaction.reply({ +async function updateDashboard(interaction, client) { + const config = await getSecurityConfig(client, interaction.guildId); + return interaction.update({ embeds: [buildSecurityDashboard(config, interaction.guild)], components: buildSecurityControls(interaction.user.id), - flags: MessageFlags.Ephemeral, }); } -const buttonHandlers = BUTTONS.map(name => ({ - name, +function changeCycle(current, values) { + const index = values.indexOf(current); + return values[(index + 1) % values.length]; +} + +const handlers = []; + +for (const [name, panel] of Object.entries(PANELS)) { + handlers.push({ + name, + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + return updatePanel(interaction, client, panel); + }, + }); +} + +handlers.push({ + name: 'security_refresh', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + return updateDashboard(interaction, client); + }, +}); + +handlers.push({ + name: 'security_back', async execute(interaction, client) { if (!authorized(interaction)) return reject(interaction); + return updateDashboard(interaction, client); + }, +}); +handlers.push({ + name: 'security_settings_refresh', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + return updatePanel(interaction, client, 'settings'); + }, +}); + +handlers.push({ + name: 'security_settings_toggle', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { enabled: !config.enabled }); + return updatePanel(interaction, client, 'settings'); + }, +}); - const modals = { - security_panel_nuke: ['security_nuke_modal', '🛡️ Anti-Nuke Configuration', [ - input('enabled', 'Enabled: true/false', config.antiNuke.enabled), - input('window', 'Detection window (ms)', config.antiNuke.windowMs), - input('thresholds', 'Thresholds: key=value per line', Object.entries(config.antiNuke.thresholds).map(([k, v]) => `${k}=${v}`).join('\n'), TextInputStyle.Paragraph), - input('action', 'Action: strip, kick, ban', config.antiNuke.action), - input('lockdown', 'Lockdown: true/false', config.antiNuke.lockdown), - ]], - security_panel_raid: ['security_raid_modal', '🚨 Anti-Raid Configuration', [ - input('enabled', 'Enabled: true/false', config.antiRaid.enabled), - input('joins', 'Joins required', config.antiRaid.joins), - input('window', 'Join window (ms)', config.antiRaid.windowMs), - input('accountAge', 'Minimum account age (hours)', Math.round(config.antiRaid.minAccountAgeMs / 3600000)), - input('action', 'Action: timeout or kick', config.antiRaid.action), - ]], - security_panel_automod: ['security_automod_modal', '🤖 AutoMod Configuration', [ - input('enabled', 'Enabled: true/false', config.autoMod.enabled), - input('spam', 'Spam: enabled,maxMessages,windowMs', `${config.autoMod.spam.enabled},${config.autoMod.spam.maxMessages},${config.autoMod.spam.windowMs}`), - input('duplicate', 'Duplicate: enabled,maxRepeats,windowMs', `${config.autoMod.duplicate.enabled},${config.autoMod.duplicate.maxRepeats},${config.autoMod.duplicate.windowMs}`), - input('content', 'Mentions, invites, links, caps', `${config.autoMod.mentions.max},${config.autoMod.invites.enabled},${config.autoMod.links.enabled},${config.autoMod.caps.enabled}`), - input('badWords', 'Blocked words (one per line)', (config.autoMod.badWords.words || []).join('\n'), TextInputStyle.Paragraph), - ]], - security_panel_punishments: ['security_punishments_modal', '⚖️ Punishment Escalation', [ - input('levels', 'strike:action:durationMs per line', escalationText(config), TextInputStyle.Paragraph), - input('decay', 'Strike decay (hours)', Math.round((config.strikeDecayMs || 86400000) / 3600000)), - ]], - security_panel_whitelist: ['security_whitelist_modal', '👤 Security Whitelist', [ - input('users', 'User IDs', (config.whitelist.users || []).join('\n'), TextInputStyle.Paragraph), - input('roles', 'Role IDs', (config.whitelist.roles || []).join('\n'), TextInputStyle.Paragraph), - input('bots', 'Bot IDs', (config.whitelist.bots || []).join('\n'), TextInputStyle.Paragraph), - ]], - security_panel_logs: ['security_logs_modal', '📋 Security Logs', [ - input('channel', 'Security log channel ID', config.logChannelId || ''), - input('ignored', 'Ignored channel IDs', (config.ignoredChannels || []).join('\n'), TextInputStyle.Paragraph), - ]], - security_panel_settings: ['security_settings_modal', '⚙️ Security Settings', [ - input('enabled', 'Global security enabled: true/false', config.enabled), - ]], - }; - - const [id, title, rows] = modals[name]; - return openModal(interaction, id, title, rows); +handlers.push({ + name: 'security_nuke_toggle', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { antiNuke: { enabled: !config.antiNuke.enabled } }); + return updatePanel(interaction, client, 'nuke'); }, -})); +}); -buttonHandlers.push({ - name: 'security_refresh', +handlers.push({ + name: 'security_nuke_action', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { antiNuke: { action: changeCycle(config.antiNuke.action, ['strip', 'kick', 'ban']) } }); + return updatePanel(interaction, client, 'nuke'); + }, +}); + +handlers.push({ + name: 'security_nuke_window', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + const values = [5000, 10000, 15000, 30000, 60000]; + await updateSecurityConfig(client, interaction.guildId, { antiNuke: { windowMs: changeCycle(config.antiNuke.windowMs, values) } }); + return updatePanel(interaction, client, 'nuke'); + }, +}); + +handlers.push({ + name: 'security_nuke_lockdown', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { antiNuke: { lockdown: !config.antiNuke.lockdown } }); + return updatePanel(interaction, client, 'nuke'); + }, +}); + +handlers.push({ + name: 'security_nuke_threshold', + async execute(interaction) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(interaction.client, interaction.guildId); + const t = config.antiNuke.thresholds || {}; + return modal(interaction, 'security_nuke_threshold_modal', 'Anti-Nuke Thresholds', [ + field('channelDelete', 'Channel deletes', t.channelDelete), + field('channelCreate', 'Channel creates', t.channelCreate), + field('roleDelete', 'Role deletes', t.roleDelete), + field('roleCreate', 'Role creates', t.roleCreate), + field('botAdd', 'Bot additions', t.botAdd), + ]); + }, +}); + +handlers.push({ + name: 'security_raid_toggle', async execute(interaction, client) { if (!authorized(interaction)) return reject(interaction); const config = await getSecurityConfig(client, interaction.guildId); - return interaction.update({ - embeds: [buildSecurityDashboard(config, interaction.guild)], - components: buildSecurityControls(interaction.user.id), - }); + await updateSecurityConfig(client, interaction.guildId, { antiRaid: { enabled: !config.antiRaid.enabled } }); + return updatePanel(interaction, client, 'raid'); }, }); +handlers.push({ + name: 'security_raid_joins_down', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { antiRaid: { joins: Math.max(2, config.antiRaid.joins - 1) } }); + return updatePanel(interaction, client, 'raid'); + }, +}); + +handlers.push({ + name: 'security_raid_joins_up', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { antiRaid: { joins: Math.min(100, config.antiRaid.joins + 1) } }); + return updatePanel(interaction, client, 'raid'); + }, +}); + +handlers.push({ + name: 'security_raid_action', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { antiRaid: { action: changeCycle(config.antiRaid.action, ['timeout', 'kick']) } }); + return updatePanel(interaction, client, 'raid'); + }, +}); + +handlers.push({ + name: 'security_raid_window', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { antiRaid: { windowMs: changeCycle(config.antiRaid.windowMs, [5000, 10000, 15000, 30000, 60000]) } }); + return updatePanel(interaction, client, 'raid'); + }, +}); + +handlers.push({ + name: 'security_raid_age', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + const values = [0, 3600000, 21600000, 86400000, 259200000, 604800000, 2592000000]; + await updateSecurityConfig(client, interaction.guildId, { antiRaid: { minAccountAgeMs: changeCycle(config.antiRaid.minAccountAgeMs, values) } }); + return updatePanel(interaction, client, 'raid'); + }, +}); + +handlers.push({ + name: 'security_raid_lockdown', + async execute(interaction, client) { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { antiRaid: { lockdown: !config.antiRaid.lockdown } }); + return updatePanel(interaction, client, 'raid'); + }, +}); + +const automodToggle = async (interaction, client, key) => { + if (!authorized(interaction)) return reject(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + await updateSecurityConfig(client, interaction.guildId, { autoMod: { [key]: { enabled: !config.autoMod[key].enabled } } }); + return updatePanel(interaction, client, 'automod'); +}; + +handlers.push({ name: 'security_automod_toggle', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { enabled: !x.autoMod.enabled } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_spam_toggle', execute: (i, c) => automodToggle(i, c, 'spam') }); +handlers.push({ name: 'security_automod_dup_toggle', execute: (i, c) => automodToggle(i, c, 'duplicate') }); +handlers.push({ name: 'security_automod_spam_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { spam: { maxMessages: Math.max(2, x.autoMod.spam.maxMessages - 1) } } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_spam_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { spam: { maxMessages: Math.min(30, x.autoMod.spam.maxMessages + 1) } } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_dup_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { duplicate: { maxRepeats: Math.max(2, x.autoMod.duplicate.maxRepeats - 1) } } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_dup_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { duplicate: { maxRepeats: Math.min(15, x.autoMod.duplicate.maxRepeats + 1) } } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_mentions_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { mentions: { max: Math.max(1, x.autoMod.mentions.max - 1) } } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_mentions_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { mentions: { max: Math.min(30, x.autoMod.mentions.max + 1) } } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_invites', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { invites: { enabled: !x.autoMod.invites.enabled } } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_links', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { links: { enabled: !x.autoMod.links.enabled } } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_caps', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { caps: { enabled: !x.autoMod.caps.enabled } } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_action', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { action: changeCycle(x.autoMod.action, ['delete', 'warn', 'timeout']) } }); return updatePanel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_badwords', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_automod_badwords_modal', 'AutoMod Blocked Words', [field('words', 'Words, separated by spaces', (x.autoMod.badWords.words || []).join(' '), TextInputStyle.Paragraph)]); } }); + +handlers.push({ name: 'security_pun_decay_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { strikeDecayMs: Math.max(3600000, x.strikeDecayMs - 3600000) }); return updatePanel(i, c, 'punishments'); } }); +handlers.push({ name: 'security_pun_decay_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { strikeDecayMs: Math.min(30 * 86400000, x.strikeDecayMs + 3600000) }); return updatePanel(i, c, 'punishments'); } }); + +handlers.push({ name: 'security_whitelist_users', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_whitelist_users_modal', 'Whitelist Users', [field('users', 'User IDs, one per line', (x.whitelist.users || []).join('\n'), TextInputStyle.Paragraph)]); } }); +handlers.push({ name: 'security_whitelist_roles', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_whitelist_roles_modal', 'Whitelist Roles', [field('roles', 'Role IDs, one per line', (x.whitelist.roles || []).join('\n'), TextInputStyle.Paragraph)]); } }); +handlers.push({ name: 'security_whitelist_bots', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_whitelist_bots_modal', 'Whitelist Bots', [field('bots', 'Bot IDs, one per line', (x.whitelist.bots || []).join('\n'), TextInputStyle.Paragraph)]); } }); +handlers.push({ name: 'security_logs_channel', execute: async (i) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(i.client, i.guildId); return modal(i, 'security_logs_channel_modal', 'Security Log Channel', [field('channel', 'Channel ID', x.logChannelId || '')]); } }); +handlers.push({ name: 'security_logs_ignored', execute: async (i) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(i.client, i.guildId); return modal(i, 'security_logs_ignored_modal', 'Ignored Channels', [field('channels', 'Channel IDs, one per line', (x.ignoredChannels || []).join('\n'), TextInputStyle.Paragraph)]); } }); + +for (let strike = 1; strike <= 10; strike++) { + handlers.push({ + name: `security_pun_level_${strike}`, + async execute(i, c) { + if (!authorized(i)) return reject(i); + const x = await getSecurityConfig(c, i.guildId); + const level = (x.escalation || []).find(e => e.strike === strike); + if (!level) return updatePanel(i, c, 'punishments'); + const actions = ['warn', 'timeout', 'kick', 'ban']; + const nextAction = changeCycle(level.action, actions); + const escalation = x.escalation.map(e => e.strike === strike ? { ...e, action: nextAction } : e); + await updateSecurityConfig(c, i.guildId, { escalation }); + return updatePanel(i, c, 'punishments'); + }, + }); +} + +export const securityButtonHandlers = handlers; + const modalHandlers = [ { - name: 'security_nuke_modal', + name: 'security_nuke_threshold_modal', async execute(i, c) { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); - const thresholds = { ...x.antiNuke.thresholds }; - for (const line of i.fields.getTextInputValue('thresholds').split('\n')) { - const [key, value] = line.split('=').map(v => v.trim()); - if (Object.prototype.hasOwnProperty.call(thresholds, key)) thresholds[key] = num(value, thresholds[key], 1); + const t = { ...x.antiNuke.thresholds }; + for (const key of Object.keys(t)) { + const inputId = key === 'channelDelete' ? 'channelDelete' : key === 'channelCreate' ? 'channelCreate' : key === 'roleDelete' ? 'roleDelete' : key === 'roleCreate' ? 'roleCreate' : key === 'botAdd' ? 'botAdd' : null; + if (inputId) t[key] = num(i.fields.getTextInputValue(inputId), t[key], 1); } - const action = i.fields.getTextInputValue('action').trim().toLowerCase(); - const u = await updateSecurityConfig(c, i.guildId, { antiNuke: { - enabled: bool(i.fields.getTextInputValue('enabled'), x.antiNuke.enabled), - windowMs: num(i.fields.getTextInputValue('window'), x.antiNuke.windowMs, 1000), - thresholds, - action: ['strip', 'kick', 'ban'].includes(action) ? action : x.antiNuke.action, - lockdown: bool(i.fields.getTextInputValue('lockdown'), x.antiNuke.lockdown), - }}); - return showUpdated(i, c, u); + await updateSecurityConfig(c, i.guildId, { antiNuke: { thresholds: t } }); + return i.update({ embeds: [buildSecurityPanel(await getSecurityConfig(c, i.guildId), i.guild, 'nuke')], components: buildSecurityPanelControls(i.user.id, 'nuke', await getSecurityConfig(c, i.guildId)) }); }, }, { - name: 'security_raid_modal', + name: 'security_automod_badwords_modal', async execute(i, c) { if (!authorized(i)) return reject(i); + const words = lines(i.fields.getTextInputValue('words')).slice(0, 100); + await updateSecurityConfig(c, i.guildId, { autoMod: { badWords: { enabled: words.length > 0, words } } }); const x = await getSecurityConfig(c, i.guildId); - const action = i.fields.getTextInputValue('action').trim().toLowerCase(); - const u = await updateSecurityConfig(c, i.guildId, { antiRaid: { - enabled: bool(i.fields.getTextInputValue('enabled'), x.antiRaid.enabled), - joins: num(i.fields.getTextInputValue('joins'), x.antiRaid.joins, 2), - windowMs: num(i.fields.getTextInputValue('window'), x.antiRaid.windowMs, 1000), - minAccountAgeMs: num(i.fields.getTextInputValue('accountAge'), x.antiRaid.minAccountAgeMs / 3600000, 0) * 3600000, - action: ['timeout', 'kick'].includes(action) ? action : x.antiRaid.action, - }}); - return showUpdated(i, c, u); + return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'automod')], components: buildSecurityPanelControls(i.user.id, 'automod', x) }); }, }, { - name: 'security_automod_modal', + name: 'security_whitelist_users_modal', async execute(i, c) { if (!authorized(i)) return reject(i); - const x = await getSecurityConfig(c, i.guildId); - const [spamEnabled, spamMax, spamWindow] = i.fields.getTextInputValue('spam').split(','); - const [dupEnabled, dupMax, dupWindow] = i.fields.getTextInputValue('duplicate').split(','); - const [mentionMax, invites, links, caps] = i.fields.getTextInputValue('content').split(','); - const u = await updateSecurityConfig(c, i.guildId, { autoMod: { - enabled: bool(i.fields.getTextInputValue('enabled'), x.autoMod.enabled), - spam: { enabled: bool(spamEnabled, x.autoMod.spam.enabled), maxMessages: num(spamMax, x.autoMod.spam.maxMessages, 2), windowMs: num(spamWindow, x.autoMod.spam.windowMs, 1000) }, - duplicate: { enabled: bool(dupEnabled, x.autoMod.duplicate.enabled), maxRepeats: num(dupMax, x.autoMod.duplicate.maxRepeats, 2), windowMs: num(dupWindow, x.autoMod.duplicate.windowMs, 1000) }, - mentions: { enabled: true, max: num(mentionMax, x.autoMod.mentions.max, 1) }, - invites: { enabled: bool(invites, x.autoMod.invites.enabled) }, - links: { enabled: bool(links, x.autoMod.links.enabled) }, - caps: { ...x.autoMod.caps, enabled: bool(caps, x.autoMod.caps.enabled) }, - badWords: { enabled: true, words: lines(i.fields.getTextInputValue('badWords')).slice(0, 100) }, - }}); - return showUpdated(i, c, u); + const x = await updateSecurityConfig(c, i.guildId, { whitelist: { users: lines(i.fields.getTextInputValue('users')).slice(0, 100) } }); + return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'whitelist')], components: buildSecurityPanelControls(i.user.id, 'whitelist', x) }); }, }, { - name: 'security_punishments_modal', + name: 'security_whitelist_roles_modal', async execute(i, c) { if (!authorized(i)) return reject(i); - const x = await getSecurityConfig(c, i.guildId); - const parsed = parseEscalation(i.fields.getTextInputValue('levels')); - const u = await updateSecurityConfig(c, i.guildId, { - escalation: parsed.length ? parsed : x.escalation, - strikeDecayMs: num(i.fields.getTextInputValue('decay'), 24, 1) * 3600000, - }); - return showUpdated(i, c, u); + const x = await updateSecurityConfig(c, i.guildId, { whitelist: { roles: lines(i.fields.getTextInputValue('roles')).slice(0, 100) } }); + return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'whitelist')], components: buildSecurityPanelControls(i.user.id, 'whitelist', x) }); }, }, { - name: 'security_whitelist_modal', + name: 'security_whitelist_bots_modal', async execute(i, c) { if (!authorized(i)) return reject(i); - const u = await updateSecurityConfig(c, i.guildId, { whitelist: { - users: lines(i.fields.getTextInputValue('users')), - roles: lines(i.fields.getTextInputValue('roles')), - bots: lines(i.fields.getTextInputValue('bots')), - }}); - return showUpdated(i, c, u); + const x = await updateSecurityConfig(c, i.guildId, { whitelist: { bots: lines(i.fields.getTextInputValue('bots')).slice(0, 100) } }); + return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'whitelist')], components: buildSecurityPanelControls(i.user.id, 'whitelist', x) }); }, }, { - name: 'security_logs_modal', + name: 'security_logs_channel_modal', async execute(i, c) { if (!authorized(i)) return reject(i); - const u = await updateSecurityConfig(c, i.guildId, { - logChannelId: i.fields.getTextInputValue('channel').trim() || null, - ignoredChannels: lines(i.fields.getTextInputValue('ignored')), - }); - return showUpdated(i, c, u); + const x = await updateSecurityConfig(c, i.guildId, { logChannelId: i.fields.getTextInputValue('channel').trim() || null }); + return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'logs')], components: buildSecurityPanelControls(i.user.id, 'logs', x) }); }, }, { - name: 'security_settings_modal', + name: 'security_logs_ignored_modal', async execute(i, c) { if (!authorized(i)) return reject(i); - const x = await getSecurityConfig(c, i.guildId); - const u = await updateSecurityConfig(c, i.guildId, { enabled: bool(i.fields.getTextInputValue('enabled'), x.enabled) }); - return showUpdated(i, c, u); + const x = await updateSecurityConfig(c, i.guildId, { ignoredChannels: lines(i.fields.getTextInputValue('channels')).slice(0, 100) }); + return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'logs')], components: buildSecurityPanelControls(i.user.id, 'logs', x) }); }, }, ]; -export const securityButtonHandlers = buttonHandlers; export const securityModalHandlers = modalHandlers; From 39140a43f523f2bcbf9b9ad516eb0d330cfde28c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:41:01 +0300 Subject: [PATCH 076/219] Add AutoMod spam detection and escalation --- src/services/security/securityService.js | 128 +++++++++++++---------- 1 file changed, 74 insertions(+), 54 deletions(-) diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js index e16a78b029..ffa617b1c4 100644 --- a/src/services/security/securityService.js +++ b/src/services/security/securityService.js @@ -6,18 +6,7 @@ export const SECURITY_DEFAULTS = { antiNuke: { enabled: true, windowMs: 10000, - thresholds: { - channelDelete: 3, - channelCreate: 5, - roleDelete: 3, - roleCreate: 5, - roleUpdate: 1, - webhookUpdate: 3, - webhookDelete: 2, - ban: 5, - kick: 5, - botAdd: 1, - }, + thresholds: { channelDelete: 3, channelCreate: 5, roleDelete: 3, roleCreate: 5, roleUpdate: 1, webhookUpdate: 3, webhookDelete: 2, ban: 5, kick: 5, botAdd: 1 }, action: 'strip', lockdown: true, }, @@ -56,29 +45,17 @@ export const SECURITY_DEFAULTS = { logChannelId: null, }; -function clone(value) { - return JSON.parse(JSON.stringify(value)); -} - +function clone(value) { return JSON.parse(JSON.stringify(value)); } function deepMerge(base, patch) { const result = { ...base }; for (const [key, value] of Object.entries(patch || {})) { - if (value && typeof value === 'object' && !Array.isArray(value) && base[key] && typeof base[key] === 'object' && !Array.isArray(base[key])) { - result[key] = deepMerge(base[key], value); - } else { - result[key] = value; - } + if (value && typeof value === 'object' && !Array.isArray(value) && base[key] && typeof base[key] === 'object' && !Array.isArray(base[key])) result[key] = deepMerge(base[key], value); + else result[key] = value; } return result; } - -function configKey(guildId) { - return `security:config:${guildId}`; -} - -function strikeKey(guildId, userId) { - return `security:strikes:${guildId}:${userId}`; -} +function configKey(guildId) { return `security:config:${guildId}`; } +function strikeKey(guildId, userId) { return `security:strikes:${guildId}:${userId}`; } export async function getSecurityConfig(client, guildId) { try { @@ -89,38 +66,27 @@ export async function getSecurityConfig(client, guildId) { return clone(SECURITY_DEFAULTS); } } - export async function updateSecurityConfig(client, guildId, patch) { const current = await getSecurityConfig(client, guildId); const updated = deepMerge(current, patch); await setInDb(configKey(guildId), updated); return updated; } - export async function getStrikes(client, guildId, userId) { const value = await getFromDb(strikeKey(guildId, userId), null); if (!value || typeof value !== 'object') return { count: 0, updatedAt: 0 }; return value; } - export async function addStrike(client, guildId, userId, reason = 'AutoMod violation') { const config = await getSecurityConfig(client, guildId); const current = await getStrikes(client, guildId, userId); const now = Date.now(); const expired = current.updatedAt && now - current.updatedAt > Number(config.strikeDecayMs || 0); - const next = { - count: (expired ? 0 : Number(current.count || 0)) + 1, - updatedAt: now, - lastReason: reason, - }; + const next = { count: (expired ? 0 : Number(current.count || 0)) + 1, updatedAt: now, lastReason: reason }; await setInDb(strikeKey(guildId, userId), next); return next; } - -export async function clearStrikes(client, guildId, userId) { - await setInDb(strikeKey(guildId, userId), { count: 0, updatedAt: Date.now() }); -} - +export async function clearStrikes(client, guildId, userId) { await setInDb(strikeKey(guildId, userId), { count: 0, updatedAt: Date.now() }); } export function isWhitelisted(member, config) { if (!member) return false; if (config.whitelist?.users?.includes(member.id)) return true; @@ -128,28 +94,82 @@ export function isWhitelisted(member, config) { if (member.user?.bot && config.whitelist?.bots?.includes(member.id)) return true; return false; } - export async function sendSecurityLog(client, guild, payload) { try { const config = await getSecurityConfig(client, guild.id); if (!config.logChannelId) return; const channel = guild.channels.cache.get(config.logChannelId) || await guild.channels.fetch(config.logChannelId).catch(() => null); if (!channel?.isTextBased()) return; - await channel.send({ embeds: [{ - title: payload.title || 'Security Event', - description: payload.description || 'Security event detected.', - color: payload.color || 0xED4245, - fields: payload.fields || [], - timestamp: new Date().toISOString(), - }] }); - } catch (error) { - logger.warn('Failed to send security log', { guildId: guild.id, error: error.message }); - } + await channel.send({ embeds: [{ title: payload.title || 'Security Event', description: payload.description || 'Security event detected.', color: payload.color || 0xED4245, fields: payload.fields || [], timestamp: new Date().toISOString() }] }); + } catch (error) { logger.warn('Failed to send security log', { guildId: guild.id, error: error.message }); } } - export function getRecentCount(store, key, now, windowMs) { const list = store.get(key) || []; const recent = list.filter(timestamp => now - timestamp <= windowMs); store.set(key, recent); return recent; } + +const autoModState = new Map(); +function autoModKey(guildId, userId) { return `${guildId}:${userId}`; } +function autoModData(guildId, userId) { + const key = autoModKey(guildId, userId); + let data = autoModState.get(key); + if (!data) { data = { messages: [], repeats: [] }; autoModState.set(key, data); } + return data; +} +function normalizeMessage(content) { return String(content || '').trim().toLowerCase().replace(/\s+/g, ' '); } +function capRatio(content) { + const letters = String(content || '').match(/[A-Za-z]/g) || []; + const upper = String(content || '').match(/[A-Z]/g) || []; + return letters.length ? upper.length / letters.length : 0; +} + +export async function processAutoMod(message, client) { + if (!message?.guild || message.author?.bot) return false; + const config = await getSecurityConfig(client, message.guild.id); + const a = config.autoMod; + if (!config.enabled || !a?.enabled || config.ignoredChannels?.includes(message.channel.id) || isWhitelisted(message.member, config)) return false; + + const now = Date.now(); + const data = autoModData(message.guild.id, message.author.id); + data.messages = data.messages.filter(t => now - t <= 60000); + data.repeats = data.repeats.filter(x => now - x.time <= 60000); + data.messages.push(now); + data.repeats.push({ time: now, content: normalizeMessage(message.content) }); + + const content = message.content || ''; + const reasons = []; + if (a.spam.enabled && data.messages.filter(t => now - t <= a.spam.windowMs).length >= a.spam.maxMessages) reasons.push(`message spam (${a.spam.maxMessages}/${Math.round(a.spam.windowMs / 1000)}s)`); + if (a.duplicate.enabled && data.repeats.filter(x => now - x.time <= a.duplicate.windowMs && x.content === normalizeMessage(content)).length >= a.duplicate.maxRepeats) reasons.push(`duplicate spam (${a.duplicate.maxRepeats})`); + const mentions = message.mentions.users.size + message.mentions.roles.size; + if (a.mentions.enabled && mentions >= a.mentions.max) reasons.push(`mention spam (${mentions})`); + if (a.invites.enabled && /(?:discord\.gg|discord(?:app)?\.com\/invite)\/\S+/i.test(content)) reasons.push('Discord invite'); + if (a.links.enabled && /https?:\/\/\S+/i.test(content)) reasons.push('link spam'); + if (a.caps.enabled && content.length >= a.caps.minLength && capRatio(content) >= a.caps.ratio) reasons.push('excessive caps'); + if (a.badWords.enabled && a.badWords.words?.some(word => word && content.toLowerCase().includes(String(word).toLowerCase()))) reasons.push('blocked word'); + if (!reasons.length) return false; + + const reason = reasons.join(', '); + const strike = await addStrike(client, message.guild.id, message.author.id, reason); + const escalation = config.escalation?.find(item => item.strike === strike.count); + const action = escalation?.action || a.action || 'delete'; + const duration = escalation?.durationMs || 60000; + await message.delete().catch(() => {}); + const member = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); + + if (action === 'timeout' && member?.moderatable) await member.timeout(Math.min(Math.max(duration, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); + else if (action === 'kick' && member?.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); + else if (action === 'ban' && member?.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); + else if (action === 'warn') { + const warning = await message.channel.send(`⚠️ <@${message.author.id}> your message was removed: **${reason}**.`).catch(() => null); + if (warning) setTimeout(() => warning.delete().catch(() => {}), 5000); + } + + await sendSecurityLog(client, message.guild, { title: 'AutoMod Triggered', description: `AutoMod acted on **${message.author.tag}**.`, fields: [ + { name: 'Reason', value: reason.slice(0, 1024), inline: true }, + { name: 'Strike', value: String(strike.count), inline: true }, + { name: 'Action', value: action, inline: true }, + ] }); + return true; +} From 941e4e0a96d20c3a6ae51e1265f8815669195710 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:41:14 +0300 Subject: [PATCH 077/219] Run AutoMod before message processing --- src/events/messageCreate.js | 152 +++++++----------------------------- 1 file changed, 28 insertions(+), 124 deletions(-) diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js index fb1a165f31..97c6c061f3 100644 --- a/src/events/messageCreate.js +++ b/src/events/messageCreate.js @@ -12,6 +12,7 @@ import { getCommandPrefix, getBotMessage, isBotOwner, isCommandCategoryEnabled, import { enforceAbuseProtection, formatCooldownDuration } from '../utils/abuseProtection.js'; import { createEmbed } from '../utils/embeds.js'; import { isCommandEnabled } from '../services/commandAccessService.js'; +import { processAutoMod } from '../services/security/securityService.js'; import { getCountingGameConfig, saveCountingGameConfig, @@ -28,15 +29,15 @@ export default { try { if (message.author.bot || !message.guild) return; + const autoModTriggered = await processAutoMod(message, client); + if (autoModTriggered) return; + logger.debug(`Message received from ${message.author.tag}: ${message.content}`); const countingProcessed = await handleCountingGame(message, client); - if (countingProcessed) { - return; - } + if (countingProcessed) return; await handlePrefixCommand(message, client); - await handleLeveling(message, client); } catch (error) { logger.error('Error in messageCreate event:', error); @@ -49,10 +50,7 @@ async function handlePrefixCommand(message, client) { const guildConfig = await getGuildConfig(client, message.guild.id); const prefix = guildConfig?.prefix || getCommandPrefix(); const parsed = parsePrefixCommand(message.content, prefix); - - if (!parsed) { - return; - } + if (!parsed) return; let { commandName, args } = parsed; const musicPrefixShortcut = commandName.toLowerCase(); @@ -63,83 +61,39 @@ async function handlePrefixCommand(message, client) { } logger.info(`Prefix command detected: ${commandName}, args: ${args.join(', ')}`); - const resolvedCommandName = resolveCommandAlias(commandName); logger.info(`Resolved command name: ${resolvedCommandName}`); const command = client.commands.get(resolvedCommandName); - - if (!command) { - logger.warn(`Command not found: ${resolvedCommandName}`); - return; - } + if (!command) return; if (isMaintenanceMode() && !isBotOwner(message.author.id)) { - await message.channel.send({ - embeds: [createEmbed({ - title: 'Maintenance Mode', - description: getBotMessage('maintenanceMode'), - color: 'warning', - })], - }).catch(() => {}); + await message.channel.send({ embeds: [createEmbed({ title: 'Maintenance Mode', description: getBotMessage('maintenanceMode'), color: 'warning' })] }).catch(() => {}); return; } - if (!isCommandCategoryEnabled(command.category)) { - await message.channel.send({ - embeds: [createEmbed({ - title: 'Feature Disabled', - description: getBotMessage('commandDisabled'), - color: 'error', - })], - }).catch(() => {}); + await message.channel.send({ embeds: [createEmbed({ title: 'Feature Disabled', description: getBotMessage('commandDisabled'), color: 'error' })] }).catch(() => {}); return; } const restriction = getPrefixRestriction(command, args, resolveSubcommandAlias); if (!supportsPrefixExecution(command) || restriction.blocked) { - if (restriction.blocked && restriction.reason) { - const embed = createEmbed({ - title: 'Slash Command Only', - description: `${restriction.reason}\nUse \`/${resolvedCommandName}\` instead.`, - color: 'info', - }); - await message.channel.send({ embeds: [embed] }).catch(() => {}); - } + if (restriction.blocked && restriction.reason) await message.channel.send({ embeds: [createEmbed({ title: 'Slash Command Only', description: `${restriction.reason}\nUse \`/${resolvedCommandName}\` instead.`, color: 'info' })] }).catch(() => {}); return; } - if (!(await isCommandEnabled(client, message.guild.id, resolvePrefixAccessKey(command.data, args), command.category))) { - const embed = createEmbed({ - title: 'Command Disabled', - description: 'This command has been disabled for this server.', - color: 'error', - }); - await message.channel.send({ embeds: [embed] }).catch(() => {}); + await message.channel.send({ embeds: [createEmbed({ title: 'Command Disabled', description: 'This command has been disabled for this server.', color: 'error' })] }).catch(() => {}); return; } - const mockInteractionForProtection = { - guildId: message.guild.id, - user: message.author, - }; - const abuseProtection = await enforceAbuseProtection( - mockInteractionForProtection, - command, - resolvedCommandName, - ); + const mockInteractionForProtection = { guildId: message.guild.id, user: message.author }; + const abuseProtection = await enforceAbuseProtection(mockInteractionForProtection, command, resolvedCommandName); if (!abuseProtection.allowed) { const formattedCooldown = formatCooldownDuration(abuseProtection.remainingMs); - const embed = createEmbed({ - title: 'Command Cooldown', - description: `This command is on cooldown. Please wait ${formattedCooldown} before trying again.`, - color: 'error', - }); - await message.channel.send({ embeds: [embed] }).catch(() => {}); + await message.channel.send({ embeds: [createEmbed({ title: 'Command Cooldown', description: `This command is on cooldown. Please wait ${formattedCooldown} before trying again.`, color: 'error' })] }).catch(() => {}); return; } logger.info(`Executing prefix command: ${prefix}${commandName} (resolved to ${resolvedCommandName}) by ${message.author.tag}`); - await executePrefixCommand(command, message, args, client, prefix, guildConfig); } catch (error) { logger.error('Error handling prefix command:', error); @@ -149,31 +103,18 @@ async function handlePrefixCommand(message, client) { async function handleCountingGame(message, client) { try { const config = await getCountingGameConfig(client, message.guild.id); - if (!config.enabled || !config.channelId || message.channel.id !== config.channelId) { - return false; - } + if (!config.enabled || !config.channelId || message.channel.id !== config.channelId) return false; const content = message.content.trim(); const validCount = isValidCountingMessage(content, config); const invalidAttempt = !validCount || message.author.id === config.lastUserId; - if (invalidAttempt) { await message.delete().catch(() => {}); - await saveCountingGameConfig(client, message.guild.id, { - ...config, - nextNumber: 1, - lastUserId: null, - currentStreak: 0, - }); - + await saveCountingGameConfig(client, message.guild.id, { ...config, nextNumber: 1, lastUserId: null, currentStreak: 0 }); const failureMessage = await message.channel.send(`❌ Count broken by <@${message.author.id}>. The sequence has been reset to **1**.`); - setTimeout(() => { - failureMessage.delete().catch(() => {}); - }, 10000); - + setTimeout(() => failureMessage.delete().catch(() => {}), 10000); return true; } - await recordCorrectCount(client, message.guild.id, message.author.id); return true; } catch (error) { @@ -186,68 +127,31 @@ async function handleLeveling(message, client) { try { const rateLimitKey = `xp-event:${message.guild.id}:${message.author.id}`; const canProcess = await checkRateLimit(rateLimitKey, MESSAGE_XP_RATE_LIMIT_ATTEMPTS, MESSAGE_XP_RATE_LIMIT_WINDOW_MS); - if (!canProcess) { - return; - } + if (!canProcess) return; const levelingConfig = await getLevelingConfig(client, message.guild.id); - - if (!levelingConfig?.enabled) { - return; - } - - if (levelingConfig.ignoredChannels?.includes(message.channel.id)) { - return; - } - + if (!levelingConfig?.enabled) return; + if (levelingConfig.ignoredChannels?.includes(message.channel.id)) return; if (levelingConfig.ignoredRoles?.length > 0) { - const member = await message.guild.members.fetch(message.author.id).catch(() => { - return null; - }); - if (member && member.roles.cache.some(role => levelingConfig.ignoredRoles.includes(role.id))) { - return; - } - } - - if (levelingConfig.blacklistedUsers?.includes(message.author.id)) { - return; - } - - if (!message.content || message.content.trim().length === 0) { - return; + const member = await message.guild.members.fetch(message.author.id).catch(() => null); + if (member && member.roles.cache.some(role => levelingConfig.ignoredRoles.includes(role.id))) return; } + if (levelingConfig.blacklistedUsers?.includes(message.author.id)) return; + if (!message.content || message.content.trim().length === 0) return; const userData = await getUserLevelData(client, message.guild.id, message.author.id); - const cooldownTime = levelingConfig.xpCooldown || 60; - const now = Date.now(); - const timeSinceLastMessage = now - (userData.lastMessage || 0); - - if (timeSinceLastMessage < cooldownTime * 1000) { - return; - } + if (Date.now() - (userData.lastMessage || 0) < cooldownTime * 1000) return; const minXP = levelingConfig.xpRange?.min || levelingConfig.xpPerMessage?.min || 15; const maxXP = levelingConfig.xpRange?.max || levelingConfig.xpPerMessage?.max || 25; - const safeMinXP = Math.max(1, minXP); const safeMaxXP = Math.max(safeMinXP, maxXP); - const xpToGive = Math.floor(Math.random() * (safeMaxXP - safeMinXP + 1)) + safeMinXP; - - let finalXP = xpToGive; - if (levelingConfig.xpMultiplier && levelingConfig.xpMultiplier > 1) { - finalXP = Math.floor(finalXP * levelingConfig.xpMultiplier); - } - + const finalXP = levelingConfig.xpMultiplier && levelingConfig.xpMultiplier > 1 ? Math.floor(xpToGive * levelingConfig.xpMultiplier) : xpToGive; const result = await addXp(client, message.guild, message.member, finalXP); - - if (result?.leveledUp) { - logger.info( - `${message.author.tag} leveled up to level ${result.level} in ${message.guild.name}` - ); - } + if (result?.leveledUp) logger.info(`${message.author.tag} leveled up to level ${result.level} in ${message.guild.name}`); } catch (error) { logger.error('Error handling leveling for message:', error); } -} \ No newline at end of file +} From cf31c4d527e42e65ca0ec54b3af561bf3ebbe29a Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:57:23 +0300 Subject: [PATCH 078/219] Add per-rule security punishments --- src/services/security/securityService.js | 72 ++++++++++++++---------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js index ffa617b1c4..8da11ea188 100644 --- a/src/services/security/securityService.js +++ b/src/services/security/securityService.js @@ -1,6 +1,9 @@ import { getFromDb, setInDb } from '../../utils/database.js'; import { logger } from '../../utils/logger.js'; +const NukeTypes = ['channelDelete','channelCreate','roleDelete','roleCreate','roleUpdate','webhookUpdate','webhookDelete','ban','kick','botAdd']; +const AutoModTypes = ['spam','duplicate','mentions','invites','links','caps','badWords']; + export const SECURITY_DEFAULTS = { enabled: true, antiNuke: { @@ -8,6 +11,7 @@ export const SECURITY_DEFAULTS = { windowMs: 10000, thresholds: { channelDelete: 3, channelCreate: 5, roleDelete: 3, roleCreate: 5, roleUpdate: 1, webhookUpdate: 3, webhookDelete: 2, ban: 5, kick: 5, botAdd: 1 }, action: 'strip', + punishments: Object.fromEntries(NukeTypes.map(type => [type, ['ban','kick','botAdd'].includes(type) ? 'ban' : 'strip'])), lockdown: true, }, antiRaid: { @@ -16,19 +20,20 @@ export const SECURITY_DEFAULTS = { windowMs: 10000, minAccountAgeMs: 24 * 60 * 60 * 1000, action: 'timeout', + punishment: 'timeout', timeoutMs: 10 * 60 * 1000, lockdown: true, lockdownMs: 10 * 60 * 1000, }, autoMod: { enabled: true, - spam: { enabled: true, maxMessages: 6, windowMs: 5000 }, - duplicate: { enabled: true, maxRepeats: 3, windowMs: 10000 }, - mentions: { enabled: true, max: 6 }, - caps: { enabled: false, ratio: 0.8, minLength: 12 }, - invites: { enabled: true }, - links: { enabled: false }, - badWords: { enabled: false, words: [] }, + spam: { enabled: true, maxMessages: 6, windowMs: 5000, punishment: 'timeout' }, + duplicate: { enabled: true, maxRepeats: 3, windowMs: 10000, punishment: 'timeout' }, + mentions: { enabled: true, max: 6, punishment: 'timeout' }, + caps: { enabled: false, ratio: 0.8, minLength: 12, punishment: 'warn' }, + invites: { enabled: true, punishment: 'delete' }, + links: { enabled: false, punishment: 'delete' }, + badWords: { enabled: false, words: [], punishment: 'timeout' }, action: 'delete', }, escalation: [ @@ -60,7 +65,10 @@ function strikeKey(guildId, userId) { return `security:strikes:${guildId}:${user export async function getSecurityConfig(client, guildId) { try { const stored = await getFromDb(configKey(guildId), null); - return deepMerge(clone(SECURITY_DEFAULTS), stored || {}); + const config = deepMerge(clone(SECURITY_DEFAULTS), stored || {}); + config.antiNuke.punishments = { ...SECURITY_DEFAULTS.antiNuke.punishments, ...(config.antiNuke.punishments || {}) }; + for (const type of AutoModTypes) config.autoMod[type].punishment ||= SECURITY_DEFAULTS.autoMod[type].punishment; + return config; } catch (error) { logger.error('Failed to load security config', { guildId, error: error.message }); return clone(SECURITY_DEFAULTS); @@ -125,6 +133,18 @@ function capRatio(content) { return letters.length ? upper.length / letters.length : 0; } +async function executeAutoModAction(message, action, duration, reason) { + const member = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); + if (action === 'delete') return; + if (action === 'timeout' && member?.moderatable) await member.timeout(Math.min(Math.max(duration || 60000, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); + else if (action === 'kick' && member?.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); + else if (action === 'ban' && member?.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); + else if (action === 'warn') { + const warning = await message.channel.send(`⚠️ <@${message.author.id}> your message was removed: **${reason}**.`).catch(() => null); + if (warning) setTimeout(() => warning.delete().catch(() => {}), 5000); + } +} + export async function processAutoMod(message, client) { if (!message?.guild || message.author?.bot) return false; const config = await getSecurityConfig(client, message.guild.id); @@ -139,34 +159,28 @@ export async function processAutoMod(message, client) { data.repeats.push({ time: now, content: normalizeMessage(message.content) }); const content = message.content || ''; - const reasons = []; - if (a.spam.enabled && data.messages.filter(t => now - t <= a.spam.windowMs).length >= a.spam.maxMessages) reasons.push(`message spam (${a.spam.maxMessages}/${Math.round(a.spam.windowMs / 1000)}s)`); - if (a.duplicate.enabled && data.repeats.filter(x => now - x.time <= a.duplicate.windowMs && x.content === normalizeMessage(content)).length >= a.duplicate.maxRepeats) reasons.push(`duplicate spam (${a.duplicate.maxRepeats})`); + const violations = []; + if (a.spam.enabled && data.messages.filter(t => now - t <= a.spam.windowMs).length >= a.spam.maxMessages) violations.push({ type: 'spam', reason: `message spam (${a.spam.maxMessages}/${Math.round(a.spam.windowMs / 1000)}s)` }); + if (a.duplicate.enabled && data.repeats.filter(x => now - x.time <= a.duplicate.windowMs && x.content === normalizeMessage(content)).length >= a.duplicate.maxRepeats) violations.push({ type: 'duplicate', reason: `duplicate spam (${a.duplicate.maxRepeats})` }); const mentions = message.mentions.users.size + message.mentions.roles.size; - if (a.mentions.enabled && mentions >= a.mentions.max) reasons.push(`mention spam (${mentions})`); - if (a.invites.enabled && /(?:discord\.gg|discord(?:app)?\.com\/invite)\/\S+/i.test(content)) reasons.push('Discord invite'); - if (a.links.enabled && /https?:\/\/\S+/i.test(content)) reasons.push('link spam'); - if (a.caps.enabled && content.length >= a.caps.minLength && capRatio(content) >= a.caps.ratio) reasons.push('excessive caps'); - if (a.badWords.enabled && a.badWords.words?.some(word => word && content.toLowerCase().includes(String(word).toLowerCase()))) reasons.push('blocked word'); - if (!reasons.length) return false; + if (a.mentions.enabled && mentions >= a.mentions.max) violations.push({ type: 'mentions', reason: `mention spam (${mentions})` }); + if (a.invites.enabled && /(?:discord\.gg|discord(?:app)?\.com\/invite)\/\S+/i.test(content)) violations.push({ type: 'invites', reason: 'Discord invite' }); + if (a.links.enabled && /https?:\/\/\S+/i.test(content)) violations.push({ type: 'links', reason: 'link spam' }); + if (a.caps.enabled && content.length >= a.caps.minLength && capRatio(content) >= a.caps.ratio) violations.push({ type: 'caps', reason: 'excessive caps' }); + if (a.badWords.enabled && a.badWords.words?.some(word => word && content.toLowerCase().includes(String(word).toLowerCase()))) violations.push({ type: 'badWords', reason: 'blocked word' }); + if (!violations.length) return false; - const reason = reasons.join(', '); + const reason = violations.map(v => v.reason).join(', '); const strike = await addStrike(client, message.guild.id, message.author.id, reason); const escalation = config.escalation?.find(item => item.strike === strike.count); - const action = escalation?.action || a.action || 'delete'; + const primary = violations[0]; + const action = escalation?.action || a[primary.type]?.punishment || a.action || 'delete'; const duration = escalation?.durationMs || 60000; - await message.delete().catch(() => {}); - const member = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); - - if (action === 'timeout' && member?.moderatable) await member.timeout(Math.min(Math.max(duration, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); - else if (action === 'kick' && member?.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); - else if (action === 'ban' && member?.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); - else if (action === 'warn') { - const warning = await message.channel.send(`⚠️ <@${message.author.id}> your message was removed: **${reason}**.`).catch(() => null); - if (warning) setTimeout(() => warning.delete().catch(() => {}), 5000); - } + await message.delete().catch(() => {}); + await executeAutoModAction(message, action, duration, reason); await sendSecurityLog(client, message.guild, { title: 'AutoMod Triggered', description: `AutoMod acted on **${message.author.tag}**.`, fields: [ + { name: 'Rule', value: primary.type, inline: true }, { name: 'Reason', value: reason.slice(0, 1024), inline: true }, { name: 'Strike', value: String(strike.count), inline: true }, { name: 'Action', value: action, inline: true }, From 41fddf5e3ca1a4faa2675d9c1916d3e1594984cf Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:57:36 +0300 Subject: [PATCH 079/219] Use per-operation Anti-Nuke punishments --- src/services/security/antiNuke.js | 59 +++++++++++-------------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/src/services/security/antiNuke.js b/src/services/security/antiNuke.js index 4c199ed7be..21813b5c67 100644 --- a/src/services/security/antiNuke.js +++ b/src/services/security/antiNuke.js @@ -3,7 +3,6 @@ import { getSecurityConfig, isWhitelisted, sendSecurityLog, getRecentCount } fro import { logger } from '../../utils/logger.js'; const counters = new Map(); - const EVENT_MAP = { channelDelete: AuditLogEvent.ChannelDelete, channelCreate: AuditLogEvent.ChannelCreate, @@ -16,33 +15,26 @@ const EVENT_MAP = { kick: AuditLogEvent.MemberKick, botAdd: AuditLogEvent.BotAdd, }; - -function key(guildId, executorId, type) { - return `${guildId}:${executorId}:${type}`; -} +function key(guildId, executorId, type) { return `${guildId}:${executorId}:${type}`; } async function findExecutor(guild, auditType, targetId = null) { const types = Array.isArray(auditType) ? auditType : [auditType]; const entries = []; - for (const type of types) { const logs = await guild.fetchAuditLogs({ type, limit: 10 }).catch(() => null); if (!logs?.entries) continue; - for (const entry of logs.entries.values()) { if (Date.now() - entry.createdTimestamp > 15000) continue; if (targetId && entry.target?.id !== targetId && entry.targetId !== targetId) continue; entries.push(entry); } } - entries.sort((a, b) => b.createdTimestamp - a.createdTimestamp); return entries[0]?.executor || null; } async function stripDangerousRoles(member, reason) { if (!member.manageable) return false; - const removable = member.roles.cache.filter(role => { if (role.id === member.guild.id || !role.editable) return false; return role.permissions.any([ @@ -55,41 +47,44 @@ async function stripDangerousRoles(member, reason) { PermissionFlagsBits.ManageWebhooks, ]); }); - if (!removable.size) return false; await member.roles.remove(removable, `Anti-Nuke: ${reason}`).catch(() => {}); return true; } -async function punishExecutor(guild, executor, config, reason, targetId) { +async function punishExecutor(guild, executor, config, type, reason, targetId) { const member = await guild.members.fetch(executor.id).catch(() => null); if (!member || member.id === guild.ownerId || isWhitelisted(member, config)) return false; - let actionTaken = config.antiNuke.action; - - if (config.antiNuke.action === 'ban' && member.bannable) { - await member.ban({ reason: `Anti-Nuke: ${reason}` }).catch(() => {}); - } else if (config.antiNuke.action === 'kick' && member.kickable) { - await member.kick(`Anti-Nuke: ${reason}`).catch(() => {}); - } else { + const action = config.antiNuke.punishments?.[type] || config.antiNuke.action || 'strip'; + let actionTaken = action; + if (action === 'ban' && member.bannable) await member.ban({ reason: `Anti-Nuke: ${reason}` }).catch(() => {}); + else if (action === 'kick' && member.kickable) await member.kick(`Anti-Nuke: ${reason}`).catch(() => {}); + else if (action === 'timeout' && member.moderatable) await member.timeout(10 * 60 * 1000, `Anti-Nuke: ${reason}`).catch(() => {}); + else if (action === 'strip') { const stripped = await stripDangerousRoles(member, reason); actionTaken = stripped ? 'strip' : 'none'; + } else if (action === 'warn') { + const channel = guild.systemChannel; + if (channel?.isTextBased()) { + const warning = await channel.send(`⚠️ <@${executor.id}> Anti-Nuke triggered: **${reason}**.`).catch(() => null); + if (warning) setTimeout(() => warning.delete().catch(() => {}), 5000); + } } - if (reason.startsWith('botAdd') && targetId) { + if (type === 'botAdd' && targetId) { const bot = await guild.members.fetch(targetId).catch(() => null); - if (bot?.user?.bot && bot.kickable && bot.id !== guild.client.user?.id) { - await bot.kick('Anti-Nuke: unauthorized bot addition').catch(() => {}); - } + if (bot?.user?.bot && bot.kickable && bot.id !== guild.client.user?.id) await bot.kick('Anti-Nuke: unauthorized bot addition').catch(() => {}); } await sendSecurityLog(guild.client, guild, { title: 'Anti-Nuke Triggered', description: `**${executor.tag || executor.username}** triggered Anti-Nuke protection.`, fields: [ + { name: 'Operation', value: type, inline: true }, { name: 'Reason', value: reason.slice(0, 1024), inline: true }, { name: 'Action', value: actionTaken, inline: true }, - { name: 'Executor', value: `${executor.id}`, inline: true }, + { name: 'Executor', value: executor.id, inline: true }, ], }); return true; @@ -98,7 +93,6 @@ async function punishExecutor(guild, executor, config, reason, targetId) { export async function handleAntiNuke(guild, type, targetId = null) { const config = await getSecurityConfig(guild.client, guild.id); if (!config.enabled || !config.antiNuke.enabled) return; - const auditType = EVENT_MAP[type]; const threshold = Number(config.antiNuke.thresholds?.[type] || 0); if (!auditType || threshold <= 0) return; @@ -106,7 +100,6 @@ export async function handleAntiNuke(guild, type, targetId = null) { const auditTargetId = type.startsWith('webhook') ? null : targetId; const executor = await findExecutor(guild, auditType, auditTargetId); if (!executor || executor.id === guild.client.user?.id) return; - const member = await guild.members.fetch(executor.id).catch(() => null); if (!member || member.id === guild.ownerId || isWhitelisted(member, config)) return; @@ -115,15 +108,8 @@ export async function handleAntiNuke(guild, type, targetId = null) { const recent = getRecentCount(counters, counterKey, now, config.antiNuke.windowMs); recent.push(now); counters.set(counterKey, recent); - if (recent.length >= threshold) { - await punishExecutor( - guild, - executor, - config, - `${type} threshold exceeded (${recent.length}/${threshold})`, - targetId, - ); + await punishExecutor(guild, executor, config, type, `${type} threshold exceeded (${recent.length}/${threshold})`, targetId); counters.delete(counterKey); } } @@ -135,11 +121,8 @@ export function registerAntiNukeEvent(eventName, type) { const guild = eventTarget?.guild || eventTarget; const targetId = eventTarget?.id || null; if (!guild?.id) return; - try { - await handleAntiNuke(guild, type, targetId); - } catch (error) { - logger.error(`Anti-Nuke ${type} failed`, { error: error.message, guildId: guild.id }); - } + try { await handleAntiNuke(guild, type, targetId); } + catch (error) { logger.error(`Anti-Nuke ${type} failed`, { error: error.message, guildId: guild.id }); } }, }; } From 9c8a6d450c2c1cc82c37e3e9427a8e039818f563 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:57:47 +0300 Subject: [PATCH 080/219] Use configurable Anti-Raid punishment --- src/services/security/antiRaid.js | 85 ++++++++----------------------- 1 file changed, 20 insertions(+), 65 deletions(-) diff --git a/src/services/security/antiRaid.js b/src/services/security/antiRaid.js index 476e43a88a..b3bd91cbe4 100644 --- a/src/services/security/antiRaid.js +++ b/src/services/security/antiRaid.js @@ -6,78 +6,44 @@ const lockdowns = new Map(); async function restoreLockdown(guild, state) { if (!state) return; - for (const [channelId, previous] of state.channels) { const channel = guild.channels.cache.get(channelId) || await guild.channels.fetch(channelId).catch(() => null); if (!channel?.permissionOverwrites?.edit) continue; - const everyoneId = guild.roles.everyone.id; const current = channel.permissionOverwrites.cache.get(everyoneId); - - if (!previous) { - if (current) await current.delete('Anti-Raid lockdown ended').catch(() => {}); - continue; - } - - const sendMessages = previous.sendMessages === null - ? null - : previous.sendMessages; - - await channel.permissionOverwrites.edit( - guild.roles.everyone, - { SendMessages: sendMessages }, - { reason: 'Anti-Raid lockdown ended' }, - ).catch(() => {}); + if (!previous) { if (current) await current.delete('Anti-Raid lockdown ended').catch(() => {}); continue; } + await channel.permissionOverwrites.edit(guild.roles.everyone, { SendMessages: previous.sendMessages }, { reason: 'Anti-Raid lockdown ended' }).catch(() => {}); } } async function startLockdown(guild, config) { if (lockdowns.has(guild.id)) return; - const me = guild.members.me; if (!me?.permissions.has(PermissionFlagsBits.ManageChannels)) return; - - const state = { - expiresAt: Date.now() + config.antiRaid.lockdownMs, - channels: new Map(), - }; - + const state = { expiresAt: Date.now() + config.antiRaid.lockdownMs, channels: new Map() }; for (const channel of guild.channels.cache.values()) { if (!channel.permissionOverwrites?.edit || channel.isThread?.()) continue; - - const everyoneOverwrite = channel.permissionOverwrites.cache.get(guild.roles.everyone.id); - state.channels.set(channel.id, { - sendMessages: everyoneOverwrite?.deny?.has(PermissionFlagsBits.SendMessages) - ? false - : everyoneOverwrite?.allow?.has(PermissionFlagsBits.SendMessages) - ? true - : null, - }); - - await channel.permissionOverwrites.edit( - guild.roles.everyone, - { SendMessages: false }, - { reason: 'Anti-Raid lockdown' }, - ).catch(() => {}); + const overwrite = channel.permissionOverwrites.cache.get(guild.roles.everyone.id); + state.channels.set(channel.id, { sendMessages: overwrite?.deny?.has(PermissionFlagsBits.SendMessages) ? false : overwrite?.allow?.has(PermissionFlagsBits.SendMessages) ? true : null }); + await channel.permissionOverwrites.edit(guild.roles.everyone, { SendMessages: false }, { reason: 'Anti-Raid lockdown' }).catch(() => {}); } - lockdowns.set(guild.id, state); - const timer = setTimeout(async () => { const current = lockdowns.get(guild.id); if (current !== state) return; lockdowns.delete(guild.id); await restoreLockdown(guild, state); - await sendSecurityLog(guild.client, guild, { - title: 'Anti-Raid Lockdown Ended', - description: 'The temporary raid lockdown has ended and previous channel permissions were restored.', - color: 0x57F287, - }); + await sendSecurityLog(guild.client, guild, { title: 'Anti-Raid Lockdown Ended', description: 'The temporary raid lockdown has ended and previous channel permissions were restored.', color: 0x57F287 }); }, Math.max(1000, config.antiRaid.lockdownMs)); - timer.unref?.(); } +async function punishMember(member, action, reason, timeoutMs) { + if (action === 'ban' && member.bannable) await member.ban({ reason: `Anti-Raid: ${reason}` }).catch(() => {}); + else if (action === 'kick' && member.kickable) await member.kick(`Anti-Raid: ${reason}`).catch(() => {}); + else if (action === 'timeout' && member.moderatable) await member.timeout(Math.min(Math.max(timeoutMs || 600000, 1000), 2419200000), `Anti-Raid: ${reason}`).catch(() => {}); +} + export async function handleMemberJoin(member) { const guild = member.guild; const client = guild.client; @@ -85,38 +51,28 @@ export async function handleMemberJoin(member) { if (!config.enabled || !config.antiRaid.enabled || isWhitelisted(member, config)) return; const now = Date.now(); - const listKey = guild.id; - const list = (joins.get(listKey) || []).filter(t => now - t <= config.antiRaid.windowMs); + const list = (joins.get(guild.id) || []).filter(t => now - t <= config.antiRaid.windowMs); list.push(now); - joins.set(listKey, list); + joins.set(guild.id, list); const accountTooNew = now - member.user.createdTimestamp < config.antiRaid.minAccountAgeMs; const raidDetected = list.length >= config.antiRaid.joins; - const flagged = raidDetected || accountTooNew; - - if (flagged) { + if (raidDetected || accountTooNew) { const reason = raidDetected ? 'raid detected' : 'new account'; - - if (config.antiRaid.action === 'kick' && member.kickable) { - await member.kick(`Anti-Raid: ${reason}`).catch(() => {}); - } else if (member.moderatable) { - await member.timeout(config.antiRaid.timeoutMs, `Anti-Raid: ${reason}`).catch(() => {}); - } - + const action = config.antiRaid.punishment || config.antiRaid.action || 'timeout'; + await punishMember(member, action, reason, config.antiRaid.timeoutMs); await sendSecurityLog(client, guild, { title: raidDetected ? 'Raid Detected' : 'New Account Protection', description: `${member} was flagged by Anti-Raid.`, fields: [ { name: 'Account Age', value: ``, inline: true }, { name: 'Joins in Window', value: String(list.length), inline: true }, - { name: 'Action', value: config.antiRaid.action, inline: true }, + { name: 'Punishment', value: action, inline: true }, ], }); } - if (raidDetected && config.antiRaid.lockdown) { - await startLockdown(guild, config); - } + if (raidDetected && config.antiRaid.lockdown) await startLockdown(guild, config); } export async function clearRaidLockdown(guild) { @@ -126,7 +82,6 @@ export async function clearRaidLockdown(guild) { await restoreLockdown(guild, state); return true; } - export function isRaidLockdownActive(guildId) { const state = lockdowns.get(guildId); return Boolean(state && state.expiresAt > Date.now()); From e1249663551aad7fc0605896aa14e54d48fd0e40 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:58:14 +0300 Subject: [PATCH 081/219] Add per-rule punishment controls to security dashboard --- src/commands/Security/security.js | 145 +++++++++++++----------------- 1 file changed, 61 insertions(+), 84 deletions(-) diff --git a/src/commands/Security/security.js b/src/commands/Security/security.js index b6ce055630..26e8688336 100644 --- a/src/commands/Security/security.js +++ b/src/commands/Security/security.js @@ -13,12 +13,11 @@ const PANEL_META = { nuke: ['🛡️ Anti-Nuke', 'Protects channels, roles, webhooks, bans and dangerous server changes.'], raid: ['🚨 Anti-Raid', 'Detects rapid joins and suspicious new accounts.'], automod: ['🤖 AutoMod', 'Stops spam, duplicates, mentions, invites, links and other abuse.'], - punishments: ['⚖️ Punishments', 'Configure escalating strikes and automatic punishments.'], + punishments: ['⚖️ Punishments', 'Every protection rule can have its own punishment.'], whitelist: ['👤 Whitelist', 'Trusted users, roles and bots bypass security actions.'], logs: ['📋 Logs', 'Choose where security incidents are reported.'], settings: ['⚙️ Settings', 'Global protection and security behavior.'], }; - const MAIN_BUTTONS = [ ['security_panel_nuke', '🛡️ Anti-Nuke', ButtonStyle.Danger], ['security_panel_raid', '🚨 Anti-Raid', ButtonStyle.Primary], @@ -28,13 +27,12 @@ const MAIN_BUTTONS = [ ['security_panel_logs', '📋 Logs', ButtonStyle.Secondary], ['security_panel_settings', '⚙️ Settings', ButtonStyle.Secondary], ]; - +const NUKE_LABELS = { channelDelete: 'Channel Del', channelCreate: 'Channel Add', roleDelete: 'Role Del', roleCreate: 'Role Add', roleUpdate: 'Role Edit', webhookUpdate: 'Webhook Edit', webhookDelete: 'Webhook Del', ban: 'Ban', kick: 'Kick', botAdd: 'Bot Add' }; +const AUTOMOD_LABELS = { spam: 'Spam', duplicate: 'Duplicate', mentions: 'Mentions', invites: 'Invites', links: 'Links', caps: 'Caps', badWords: 'Bad Words' }; function status(enabled) { return enabled ? '🟢 **ACTIVE**' : '🔴 **OFF**'; } function boolLabel(value) { return value ? '🟢 ON' : '🔴 OFF'; } function hours(ms) { return Math.max(0, Math.round(Number(ms || 0) / 3600000)); } -function button(id, label, style = ButtonStyle.Secondary, disabled = false) { - return new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style).setDisabled(disabled); -} +function button(id, label, style = ButtonStyle.Secondary, disabled = false) { return new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style).setDisabled(disabled); } export function buildSecurityDashboard(config, guild) { const systems = [config.enabled, config.antiNuke?.enabled, config.antiRaid?.enabled, config.autoMod?.enabled]; @@ -43,7 +41,6 @@ export function buildSecurityDashboard(config, guild) { const scoreLabel = score >= 100 ? 'Maximum' : score >= 75 ? 'Strong' : score >= 50 ? 'Partial' : 'Weak'; const whitelistCount = (config.whitelist?.users?.length || 0) + (config.whitelist?.roles?.length || 0) + (config.whitelist?.bots?.length || 0); const logValue = config.logChannelId ? `<#${config.logChannelId}>` : '`Not configured`'; - return new EmbedBuilder() .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) .setTitle('🛡️ Server Protection') @@ -52,7 +49,7 @@ export function buildSecurityDashboard(config, guild) { .setThumbnail(guild.iconURL({ size: 256 }) || null) .addFields( { name: '━━ Protection ━━', value: [`🛡️ Anti-Nuke ${status(config.antiNuke?.enabled)}`, `🚨 Anti-Raid ${status(config.antiRaid?.enabled)}`, `🤖 AutoMod ${status(config.autoMod?.enabled)}`, `🔒 Global ${status(config.enabled)}`].join('\n'), inline: true }, - { name: '━━ Statistics ━━', value: [`⚡ **${config.escalation?.length || 0}** punishment levels`, `👤 **${whitelistCount}** whitelist entries`, `⏱️ **${hours(config.strikeDecayMs)}h** strike decay`, `📋 Logs: ${logValue}`].join('\n'), inline: true }, + { name: '━━ Statistics ━━', value: [`⚡ **${config.escalation?.length || 0}** escalation levels`, `👤 **${whitelistCount}** whitelist entries`, `⏱️ **${hours(config.strikeDecayMs)}h** strike decay`, `📋 Logs: ${logValue}`].join('\n'), inline: true }, { name: '━━ Current Mode ━━', value: config.enabled ? '🟢 **PROTECTED** — security systems are actively monitoring this server.' : '🔴 **DISABLED** — global security protection is currently off.' }, ) .setFooter({ text: 'Infinity System • Security Center • Changes save automatically' }) @@ -68,92 +65,72 @@ export function buildSecurityControls(userId) { export function buildSecurityPanel(config, guild, panel) { const [title, description] = PANEL_META[panel] || PANEL_META.settings; - const data = { - nuke: [ - `**Status:** ${status(config.antiNuke?.enabled)}`, - `**Action:** \`${config.antiNuke?.action || 'strip'}\``, - `**Detection window:** \`${Math.round((config.antiNuke?.windowMs || 10000) / 1000)}s\``, - `**Lockdown:** ${boolLabel(config.antiNuke?.lockdown)}`, - '', '**Thresholds**', - `Channels delete: \`${config.antiNuke?.thresholds?.channelDelete ?? 3}\` • Channels create: \`${config.antiNuke?.thresholds?.channelCreate ?? 5}\``, - `Roles delete: \`${config.antiNuke?.thresholds?.roleDelete ?? 3}\` • Roles create: \`${config.antiNuke?.thresholds?.roleCreate ?? 5}\``, - `Webhooks: \`${config.antiNuke?.thresholds?.webhookUpdate ?? 3}\` • Bot add: \`${config.antiNuke?.thresholds?.botAdd ?? 1}\``, - ], - raid: [ - `**Status:** ${status(config.antiRaid?.enabled)}`, - `**Join threshold:** \`${config.antiRaid?.joins ?? 8}\` members`, - `**Window:** \`${Math.round((config.antiRaid?.windowMs || 10000) / 1000)}s\``, - `**Minimum account age:** \`${hours(config.antiRaid?.minAccountAgeMs)}h\``, - `**Action:** \`${config.antiRaid?.action || 'timeout'}\``, - `**Lockdown:** ${boolLabel(config.antiRaid?.lockdown)} • **Duration:** \`${Math.round((config.antiRaid?.lockdownMs || 600000) / 60000)}m\``, - ], - automod: [ - `**Status:** ${status(config.autoMod?.enabled)}`, - `**Spam:** ${boolLabel(config.autoMod?.spam?.enabled)} • \`${config.autoMod?.spam?.maxMessages ?? 6}\` msgs / \`${Math.round((config.autoMod?.spam?.windowMs || 5000) / 1000)}s\``, - `**Duplicate:** ${boolLabel(config.autoMod?.duplicate?.enabled)} • \`${config.autoMod?.duplicate?.maxRepeats ?? 3}\` repeats`, - `**Mentions:** ${boolLabel(config.autoMod?.mentions?.enabled)} • max \`${config.autoMod?.mentions?.max ?? 6}\``, - `**Invites:** ${boolLabel(config.autoMod?.invites?.enabled)} • **Links:** ${boolLabel(config.autoMod?.links?.enabled)} • **Caps:** ${boolLabel(config.autoMod?.caps?.enabled)}`, - `**Blocked words:** \`${config.autoMod?.badWords?.words?.length || 0}\``, - `**Default action:** \`${config.autoMod?.action || 'delete'}\``, - ], - punishments: [ - `**Strike decay:** \`${hours(config.strikeDecayMs)}h\``, '', - ...(config.escalation || []).slice(0, 10).map(e => `**Strike ${e.strike}:** \`${e.action}\`${e.durationMs ? ` • ${Math.round(e.durationMs / 60000)}m` : ''}`), - ], - whitelist: [ - `**Users:** \`${config.whitelist?.users?.length || 0}\``, - `**Roles:** \`${config.whitelist?.roles?.length || 0}\``, - `**Bots:** \`${config.whitelist?.bots?.length || 0}\``, '', - 'Whitelisted accounts bypass Anti-Nuke and AutoMod enforcement where applicable.', - ], - logs: [ - `**Log channel:** ${config.logChannelId ? `<#${config.logChannelId}>` : '`Not configured`'}`, - `**Ignored channels:** \`${config.ignoredChannels?.length || 0}\``, '', - 'Security incidents include the executor/member, reason and action taken.', - ], - settings: [ - `**Global protection:** ${status(config.enabled)}`, - `**Anti-Nuke:** ${status(config.antiNuke?.enabled)}`, - `**Anti-Raid:** ${status(config.antiRaid?.enabled)}`, - `**AutoMod:** ${status(config.autoMod?.enabled)}`, '', - 'Use the controls below to change protection without leaving this message.', - ], - }[panel] || []; + let data = []; + if (panel === 'nuke') data = [ + `**Status:** ${status(config.antiNuke?.enabled)}`, + `**Default action:** \`${config.antiNuke?.action || 'strip'}\``, + `**Detection window:** \`${Math.round((config.antiNuke?.windowMs || 10000) / 1000)}s\``, + `**Lockdown:** ${boolLabel(config.antiNuke?.lockdown)}`, + '', '**Thresholds**', + `Channels: delete \`${config.antiNuke?.thresholds?.channelDelete ?? 3}\` • create \`${config.antiNuke?.thresholds?.channelCreate ?? 5}\``, + `Roles: delete \`${config.antiNuke?.thresholds?.roleDelete ?? 3}\` • create \`${config.antiNuke?.thresholds?.roleCreate ?? 5}\` • edit \`${config.antiNuke?.thresholds?.roleUpdate ?? 1}\``, + `Webhooks: edit \`${config.antiNuke?.thresholds?.webhookUpdate ?? 3}\` • delete \`${config.antiNuke?.thresholds?.webhookDelete ?? 2}\``, + `Ban \`${config.antiNuke?.thresholds?.ban ?? 5}\` • Kick \`${config.antiNuke?.thresholds?.kick ?? 5}\` • Bot add \`${config.antiNuke?.thresholds?.botAdd ?? 1}\``, + '', 'Use **Punishments** to configure a different punishment for every operation.' + ]; + else if (panel === 'raid') data = [ + `**Status:** ${status(config.antiRaid?.enabled)}`, + `**Punishment:** \`${config.antiRaid?.punishment || config.antiRaid?.action || 'timeout'}\``, + `**Join threshold:** \`${config.antiRaid?.joins ?? 8}\` members`, + `**Window:** \`${Math.round((config.antiRaid?.windowMs || 10000) / 1000)}s\``, + `**Minimum account age:** \`${hours(config.antiRaid?.minAccountAgeMs)}h\``, + `**Lockdown:** ${boolLabel(config.antiRaid?.lockdown)} • **Duration:** \`${Math.round((config.antiRaid?.lockdownMs || 600000) / 60000)}m\``, + ]; + else if (panel === 'automod') data = [ + `**Status:** ${status(config.autoMod?.enabled)}`, + `**Spam:** ${boolLabel(config.autoMod?.spam?.enabled)} • ${config.autoMod?.spam?.maxMessages ?? 6}/${Math.round((config.autoMod?.spam?.windowMs || 5000) / 1000)}s • **${config.autoMod?.spam?.punishment || 'timeout'}**`, + `**Duplicate:** ${boolLabel(config.autoMod?.duplicate?.enabled)} • ${config.autoMod?.duplicate?.maxRepeats ?? 3} repeats • **${config.autoMod?.duplicate?.punishment || 'timeout'}**`, + `**Mentions:** ${boolLabel(config.autoMod?.mentions?.enabled)} • max ${config.autoMod?.mentions?.max ?? 6} • **${config.autoMod?.mentions?.punishment || 'timeout'}**`, + `**Invites:** ${boolLabel(config.autoMod?.invites?.enabled)} • **${config.autoMod?.invites?.punishment || 'delete'}**`, + `**Links:** ${boolLabel(config.autoMod?.links?.enabled)} • **${config.autoMod?.links?.punishment || 'delete'}** • **Caps:** ${boolLabel(config.autoMod?.caps?.enabled)} • **${config.autoMod?.caps?.punishment || 'warn'}**`, + `**Bad words:** ${config.autoMod?.badWords?.words?.length || 0} words • **${config.autoMod?.badWords?.punishment || 'timeout'}**`, + '', 'Escalation can override the rule punishment when a member reaches a configured strike level.' + ]; + else if (panel === 'punishments') { + const nuke = Object.entries(NUKE_LABELS).map(([key, label]) => `${label}: **${config.antiNuke?.punishments?.[key] || config.antiNuke?.action || 'strip'}**`); + const automod = Object.entries(AUTOMOD_LABELS).map(([key, label]) => `${label}: **${config.autoMod?.[key]?.punishment || 'delete'}**`); + data = ['**Per-rule punishments**', '', '**Anti-Nuke**', ...nuke, '', `**Anti-Raid:** **${config.antiRaid?.punishment || config.antiRaid?.action || 'timeout'}**`, '', '**AutoMod**', ...automod, '', '**Escalation**', ...(config.escalation || []).slice(0, 10).map(e => `Strike ${e.strike}: **${e.action}**${e.durationMs ? ` • ${Math.round(e.durationMs / 60000)}m` : ''}`), '', `Strike decay: **${hours(config.strikeDecayMs)}h**`]; + } else if (panel === 'whitelist') data = [`**Users:** \`${config.whitelist?.users?.length || 0}\``, `**Roles:** \`${config.whitelist?.roles?.length || 0}\``, `**Bots:** \`${config.whitelist?.bots?.length || 0}\``, '', 'Whitelisted accounts bypass Anti-Nuke and AutoMod enforcement where applicable.']; + else if (panel === 'logs') data = [`**Log channel:** ${config.logChannelId ? `<#${config.logChannelId}>` : '`Not configured`'}`, `**Ignored channels:** \`${config.ignoredChannels?.length || 0}\``, '', 'Security incidents include the executor/member, reason and action taken.']; + else data = [`**Global protection:** ${status(config.enabled)}`, `**Anti-Nuke:** ${status(config.antiNuke?.enabled)}`, `**Anti-Raid:** ${status(config.antiRaid?.enabled)}`, `**AutoMod:** ${status(config.autoMod?.enabled)}`, '', 'Use the controls below to change protection without leaving this message.']; - return new EmbedBuilder() - .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) - .setTitle(title) - .setDescription(`${description}\n\n${data.join('\n')}`) - .setColor(panel === 'nuke' ? 0xed4245 : panel === 'raid' ? 0xf47b67 : panel === 'automod' ? 0x5865f2 : 0x57f287) - .setFooter({ text: 'Infinity System • Click a control below • Changes save automatically' }) - .setTimestamp(); + return new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }).setTitle(title).setDescription(`${description}\n\n${data.join('\n')}`).setColor(panel === 'nuke' ? 0xed4245 : panel === 'raid' ? 0xf47b67 : panel === 'automod' ? 0x5865f2 : 0x57f287).setFooter({ text: 'Infinity System • Click a control below • Changes save automatically' }).setTimestamp(); } export function buildSecurityPanelControls(userId, panel, config) { - const id = (name) => `${name}:${userId}`; + const id = name => `${name}:${userId}`; const rows = []; if (panel === 'nuke') { - rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_nuke_toggle'), config.antiNuke.enabled ? '🟢 Disable' : '🔴 Enable', config.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(id('security_nuke_action'), `Action: ${config.antiNuke.action}`, ButtonStyle.Primary), button(id('security_nuke_window'), `Window: ${Math.round(config.antiNuke.windowMs / 1000)}s`))); - rows.push(new ActionRowBuilder().addComponents(button(id('security_nuke_lockdown'), `Lockdown: ${config.antiNuke.lockdown ? 'ON' : 'OFF'}`), button(id('security_nuke_threshold'), 'Threshold Editor', ButtonStyle.Primary))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_nuke_toggle'), config.antiNuke.enabled ? '🟢 Disable' : '🔴 Enable', config.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(id('security_nuke_window'), `Window: ${Math.round(config.antiNuke.windowMs / 1000)}s`), button(id('security_nuke_lockdown'), `Lockdown: ${config.antiNuke.lockdown ? 'ON' : 'OFF'}`))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_nuke_threshold'), 'Threshold Editor', ButtonStyle.Primary), button(id('security_panel_punishments'), '⚖️ Punishments', ButtonStyle.Primary))); } else if (panel === 'raid') { - rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_raid_toggle'), config.antiRaid.enabled ? '🟢 Disable' : '🔴 Enable', config.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(id('security_raid_joins_down'), '− Joins'), button(id('security_raid_joins_up'), '+ Joins'), button(id('security_raid_action'), `Action: ${config.antiRaid.action}`, ButtonStyle.Primary))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_raid_toggle'), config.antiRaid.enabled ? '🟢 Disable' : '🔴 Enable', config.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(id('security_raid_joins_down'), '− Joins'), button(id('security_raid_joins_up'), '+ Joins'), button(id('security_raid_punishment'), `Punishment: ${config.antiRaid.punishment || 'timeout'}`, ButtonStyle.Primary))); rows.push(new ActionRowBuilder().addComponents(button(id('security_raid_window'), `Window: ${Math.round(config.antiRaid.windowMs / 1000)}s`), button(id('security_raid_age'), `Age: ${hours(config.antiRaid.minAccountAgeMs)}h`), button(id('security_raid_lockdown'), `Lockdown: ${config.antiRaid.lockdown ? 'ON' : 'OFF'}`))); } else if (panel === 'automod') { - rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_automod_toggle'), config.autoMod.enabled ? '🟢 Disable' : '🔴 Enable', config.autoMod.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(id('security_automod_spam_toggle'), `Spam ${boolLabel(config.autoMod.spam.enabled)}`, ButtonStyle.Primary), button(id('security_automod_dup_toggle'), `Duplicate ${boolLabel(config.autoMod.duplicate.enabled)}`, ButtonStyle.Primary))); - rows.push(new ActionRowBuilder().addComponents(button(id('security_automod_spam_down'), 'Spam −'), button(id('security_automod_spam_up'), 'Spam +'), button(id('security_automod_dup_down'), 'Duplicate −'), button(id('security_automod_dup_up'), 'Duplicate +'))); - rows.push(new ActionRowBuilder().addComponents(button(id('security_automod_mentions_down'), 'Mentions −'), button(id('security_automod_mentions_up'), 'Mentions +'), button(id('security_automod_invites'), `Invites ${boolLabel(config.autoMod.invites.enabled)}`), button(id('security_automod_links'), `Links ${boolLabel(config.autoMod.links.enabled)}`), button(id('security_automod_caps'), `Caps ${boolLabel(config.autoMod.caps.enabled)}`))); - rows.push(new ActionRowBuilder().addComponents(button(id('security_automod_badwords'), '🚫 Manage Words', ButtonStyle.Primary), button(id('security_automod_action'), `Action: ${config.autoMod.action}`, ButtonStyle.Primary))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_automod_toggle'), config.autoMod.enabled ? '🟢 Disable' : '🔴 Enable', config.autoMod.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(id('security_automod_spam_toggle'), `Spam ${boolLabel(config.autoMod.spam.enabled)}`), button(id('security_automod_spam_punishment'), `Spam: ${config.autoMod.spam.punishment}`))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_automod_dup_toggle'), `Duplicate ${boolLabel(config.autoMod.duplicate.enabled)}`), button(id('security_automod_dup_punishment'), `Duplicate: ${config.autoMod.duplicate.punishment}`), button(id('security_automod_mentions_punishment'), `Mentions: ${config.autoMod.mentions.punishment}`), button(id('security_automod_invites'), `Invites ${boolLabel(config.autoMod.invites.enabled)}`), button(id('security_automod_invites_punishment'), `Invites: ${config.autoMod.invites.punishment}`))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_automod_links'), `Links ${boolLabel(config.autoMod.links.enabled)}`), button(id('security_automod_links_punishment'), `Links: ${config.autoMod.links.punishment}`), button(id('security_automod_caps'), `Caps ${boolLabel(config.autoMod.caps.enabled)}`), button(id('security_automod_caps_punishment'), `Caps: ${config.autoMod.caps.punishment}`))); + rows.push(new ActionRowBuilder().addComponents(button(id('security_automod_badwords'), '🚫 Manage Words', ButtonStyle.Primary), button(id('security_automod_badwords_punishment'), `Bad Words: ${config.autoMod.badWords.punishment}`, ButtonStyle.Primary), button(id('security_automod_spam_down'), 'Spam −'), button(id('security_automod_spam_up'), 'Spam +'), button(id('security_automod_action'), `Default: ${config.autoMod.action}`))); } else if (panel === 'punishments') { - rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_pun_decay_down'), 'Decay −'), button(id('security_pun_decay_up'), 'Decay +'))); - const levels = (config.escalation || []).slice(0, 10); - for (let i = 0; i < levels.length; i += 5) rows.push(new ActionRowBuilder().addComponents(...levels.slice(i, i + 5).map(level => button(id(`security_pun_level_${level.strike}`), `#${level.strike}: ${level.action}`, ButtonStyle.Primary)))); - } else if (panel === 'whitelist') { - rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_whitelist_users'), '👤 Manage Users', ButtonStyle.Primary), button(id('security_whitelist_roles'), '🎭 Manage Roles', ButtonStyle.Primary), button(id('security_whitelist_bots'), '🤖 Manage Bots', ButtonStyle.Primary))); - } else if (panel === 'logs') { - rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_logs_channel'), '📋 Set Log Channel', ButtonStyle.Primary), button(id('security_logs_ignored'), '🚫 Ignored Channels'))); - } else if (panel === 'settings') { - rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_settings_toggle'), config.enabled ? '🟢 Disable Protection' : '🔴 Enable Protection', config.enabled ? ButtonStyle.Danger : ButtonStyle.Success), button(id('security_settings_refresh'), '🔄 Refresh'))); - } + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_pun_decay_down'), 'Decay −'), button(id('security_pun_decay_up'), 'Decay +'), button(id('security_pun_raid'), `Raid: ${config.antiRaid.punishment}`, ButtonStyle.Primary))); + const nukeKeys = Object.keys(NUKE_LABELS); + for (let i = 0; i < nukeKeys.length; i += 5) rows.push(new ActionRowBuilder().addComponents(...nukeKeys.slice(i, i + 5).map(k => button(id(`security_pun_nuke_${k}`), `${NUKE_LABELS[k]}: ${config.antiNuke.punishments[k]}`, ButtonStyle.Primary)))); + const autoKeys = Object.keys(AUTOMOD_LABELS); + for (let i = 0; i < autoKeys.length; i += 5) rows.push(new ActionRowBuilder().addComponents(...autoKeys.slice(i, i + 5).map(k => button(id(`security_pun_auto_${k}`), `${AUTOMOD_LABELS[k]}: ${config.autoMod[k].punishment}`, ButtonStyle.Primary)))); + for (let i = 0; i < Math.min(10, (config.escalation || []).length); i += 5) rows.push(new ActionRowBuilder().addComponents(...config.escalation.slice(i, i + 5).map(level => button(id(`security_pun_level_${level.strike}`), `#${level.strike}: ${level.action}`, ButtonStyle.Secondary)))); + } else if (panel === 'whitelist') rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_whitelist_users'), '👤 Manage Users', ButtonStyle.Primary), button(id('security_whitelist_roles'), '🎭 Manage Roles', ButtonStyle.Primary), button(id('security_whitelist_bots'), '🤖 Manage Bots', ButtonStyle.Primary))); + else if (panel === 'logs') rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_logs_channel'), '📋 Set Log Channel', ButtonStyle.Primary), button(id('security_logs_ignored'), '🚫 Ignored Channels'))); + else rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_settings_toggle'), config.enabled ? '🟢 Disable Protection' : '🔴 Enable Protection', config.enabled ? ButtonStyle.Danger : ButtonStyle.Success), button(id('security_settings_refresh'), '🔄 Refresh'))); return rows.slice(0, 5); } From 3fc8bd62514b7262daf1c5460ced36097a1c2a85 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 11:58:40 +0300 Subject: [PATCH 082/219] Add per-rule punishment controls to security dashboard --- src/handlers/securityHandlers.js | 449 ++++++------------------------- 1 file changed, 81 insertions(+), 368 deletions(-) diff --git a/src/handlers/securityHandlers.js b/src/handlers/securityHandlers.js index bc2c583cc4..91bcdc317a 100644 --- a/src/handlers/securityHandlers.js +++ b/src/handlers/securityHandlers.js @@ -1,370 +1,83 @@ -import { - ActionRowBuilder, - ModalBuilder, - TextInputBuilder, - TextInputStyle, - MessageFlags, -} from 'discord.js'; +import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; -import { - buildSecurityDashboard, - buildSecurityControls, - buildSecurityPanel, - buildSecurityPanelControls, -} from '../commands/Security/security.js'; - -const PANELS = { - security_panel_nuke: 'nuke', - security_panel_raid: 'raid', - security_panel_automod: 'automod', - security_panel_punishments: 'punishments', - security_panel_whitelist: 'whitelist', - security_panel_logs: 'logs', - security_panel_settings: 'settings', -}; - -function authorized(interaction) { - return interaction.customId.split(':').at(-1) === interaction.user.id; -} - -function reject(interaction) { - return interaction.reply({ content: 'This security dashboard belongs to another moderator.', flags: MessageFlags.Ephemeral }); -} - -function num(value, fallback, min = 0) { - const n = Number(value); - return Number.isFinite(n) ? Math.max(min, n) : fallback; -} - -function bool(value, fallback) { - const v = String(value ?? '').trim().toLowerCase(); - return v === 'true' ? true : v === 'false' ? false : fallback; -} - -function lines(value) { - return String(value || '').split(/[\s,]+/).map(v => v.trim()).filter(Boolean); -} - -function field(id, label, value = '', style = TextInputStyle.Short) { - const input = new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false); - if (value !== undefined && value !== null && String(value)) input.setValue(String(value).slice(0, 4000)); - return new ActionRowBuilder().addComponents(input); -} - -function modal(interaction, id, title, fields) { - return interaction.showModal(new ModalBuilder().setCustomId(`${id}:${interaction.user.id}`).setTitle(title).addComponents(...fields)); -} - -async function updatePanel(interaction, client, panel) { - const config = await getSecurityConfig(client, interaction.guildId); - return interaction.update({ - embeds: [buildSecurityPanel(config, interaction.guild, panel)], - components: buildSecurityPanelControls(interaction.user.id, panel, config), - }); -} - -async function updateDashboard(interaction, client) { - const config = await getSecurityConfig(client, interaction.guildId); - return interaction.update({ - embeds: [buildSecurityDashboard(config, interaction.guild)], - components: buildSecurityControls(interaction.user.id), - }); -} - -function changeCycle(current, values) { - const index = values.indexOf(current); - return values[(index + 1) % values.length]; -} - -const handlers = []; - -for (const [name, panel] of Object.entries(PANELS)) { - handlers.push({ - name, - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - return updatePanel(interaction, client, panel); - }, - }); -} - -handlers.push({ - name: 'security_refresh', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - return updateDashboard(interaction, client); - }, -}); - -handlers.push({ - name: 'security_back', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - return updateDashboard(interaction, client); - }, -}); - -handlers.push({ - name: 'security_settings_refresh', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - return updatePanel(interaction, client, 'settings'); - }, -}); - -handlers.push({ - name: 'security_settings_toggle', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { enabled: !config.enabled }); - return updatePanel(interaction, client, 'settings'); - }, -}); - -handlers.push({ - name: 'security_nuke_toggle', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { antiNuke: { enabled: !config.antiNuke.enabled } }); - return updatePanel(interaction, client, 'nuke'); - }, -}); - -handlers.push({ - name: 'security_nuke_action', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { antiNuke: { action: changeCycle(config.antiNuke.action, ['strip', 'kick', 'ban']) } }); - return updatePanel(interaction, client, 'nuke'); - }, -}); - -handlers.push({ - name: 'security_nuke_window', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - const values = [5000, 10000, 15000, 30000, 60000]; - await updateSecurityConfig(client, interaction.guildId, { antiNuke: { windowMs: changeCycle(config.antiNuke.windowMs, values) } }); - return updatePanel(interaction, client, 'nuke'); - }, -}); - -handlers.push({ - name: 'security_nuke_lockdown', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { antiNuke: { lockdown: !config.antiNuke.lockdown } }); - return updatePanel(interaction, client, 'nuke'); - }, -}); - -handlers.push({ - name: 'security_nuke_threshold', - async execute(interaction) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(interaction.client, interaction.guildId); - const t = config.antiNuke.thresholds || {}; - return modal(interaction, 'security_nuke_threshold_modal', 'Anti-Nuke Thresholds', [ - field('channelDelete', 'Channel deletes', t.channelDelete), - field('channelCreate', 'Channel creates', t.channelCreate), - field('roleDelete', 'Role deletes', t.roleDelete), - field('roleCreate', 'Role creates', t.roleCreate), - field('botAdd', 'Bot additions', t.botAdd), - ]); - }, -}); - -handlers.push({ - name: 'security_raid_toggle', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { antiRaid: { enabled: !config.antiRaid.enabled } }); - return updatePanel(interaction, client, 'raid'); - }, -}); - -handlers.push({ - name: 'security_raid_joins_down', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { antiRaid: { joins: Math.max(2, config.antiRaid.joins - 1) } }); - return updatePanel(interaction, client, 'raid'); - }, -}); - -handlers.push({ - name: 'security_raid_joins_up', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { antiRaid: { joins: Math.min(100, config.antiRaid.joins + 1) } }); - return updatePanel(interaction, client, 'raid'); - }, -}); - -handlers.push({ - name: 'security_raid_action', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { antiRaid: { action: changeCycle(config.antiRaid.action, ['timeout', 'kick']) } }); - return updatePanel(interaction, client, 'raid'); - }, -}); - -handlers.push({ - name: 'security_raid_window', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { antiRaid: { windowMs: changeCycle(config.antiRaid.windowMs, [5000, 10000, 15000, 30000, 60000]) } }); - return updatePanel(interaction, client, 'raid'); - }, -}); - -handlers.push({ - name: 'security_raid_age', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - const values = [0, 3600000, 21600000, 86400000, 259200000, 604800000, 2592000000]; - await updateSecurityConfig(client, interaction.guildId, { antiRaid: { minAccountAgeMs: changeCycle(config.antiRaid.minAccountAgeMs, values) } }); - return updatePanel(interaction, client, 'raid'); - }, -}); - -handlers.push({ - name: 'security_raid_lockdown', - async execute(interaction, client) { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { antiRaid: { lockdown: !config.antiRaid.lockdown } }); - return updatePanel(interaction, client, 'raid'); - }, -}); - -const automodToggle = async (interaction, client, key) => { - if (!authorized(interaction)) return reject(interaction); - const config = await getSecurityConfig(client, interaction.guildId); - await updateSecurityConfig(client, interaction.guildId, { autoMod: { [key]: { enabled: !config.autoMod[key].enabled } } }); - return updatePanel(interaction, client, 'automod'); -}; - -handlers.push({ name: 'security_automod_toggle', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { enabled: !x.autoMod.enabled } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_spam_toggle', execute: (i, c) => automodToggle(i, c, 'spam') }); -handlers.push({ name: 'security_automod_dup_toggle', execute: (i, c) => automodToggle(i, c, 'duplicate') }); -handlers.push({ name: 'security_automod_spam_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { spam: { maxMessages: Math.max(2, x.autoMod.spam.maxMessages - 1) } } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_spam_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { spam: { maxMessages: Math.min(30, x.autoMod.spam.maxMessages + 1) } } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_dup_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { duplicate: { maxRepeats: Math.max(2, x.autoMod.duplicate.maxRepeats - 1) } } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_dup_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { duplicate: { maxRepeats: Math.min(15, x.autoMod.duplicate.maxRepeats + 1) } } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_mentions_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { mentions: { max: Math.max(1, x.autoMod.mentions.max - 1) } } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_mentions_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { mentions: { max: Math.min(30, x.autoMod.mentions.max + 1) } } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_invites', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { invites: { enabled: !x.autoMod.invites.enabled } } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_links', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { links: { enabled: !x.autoMod.links.enabled } } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_caps', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { caps: { enabled: !x.autoMod.caps.enabled } } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_action', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { action: changeCycle(x.autoMod.action, ['delete', 'warn', 'timeout']) } }); return updatePanel(i, c, 'automod'); } }); -handlers.push({ name: 'security_automod_badwords', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_automod_badwords_modal', 'AutoMod Blocked Words', [field('words', 'Words, separated by spaces', (x.autoMod.badWords.words || []).join(' '), TextInputStyle.Paragraph)]); } }); - -handlers.push({ name: 'security_pun_decay_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { strikeDecayMs: Math.max(3600000, x.strikeDecayMs - 3600000) }); return updatePanel(i, c, 'punishments'); } }); -handlers.push({ name: 'security_pun_decay_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { strikeDecayMs: Math.min(30 * 86400000, x.strikeDecayMs + 3600000) }); return updatePanel(i, c, 'punishments'); } }); - -handlers.push({ name: 'security_whitelist_users', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_whitelist_users_modal', 'Whitelist Users', [field('users', 'User IDs, one per line', (x.whitelist.users || []).join('\n'), TextInputStyle.Paragraph)]); } }); -handlers.push({ name: 'security_whitelist_roles', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_whitelist_roles_modal', 'Whitelist Roles', [field('roles', 'Role IDs, one per line', (x.whitelist.roles || []).join('\n'), TextInputStyle.Paragraph)]); } }); -handlers.push({ name: 'security_whitelist_bots', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_whitelist_bots_modal', 'Whitelist Bots', [field('bots', 'Bot IDs, one per line', (x.whitelist.bots || []).join('\n'), TextInputStyle.Paragraph)]); } }); -handlers.push({ name: 'security_logs_channel', execute: async (i) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(i.client, i.guildId); return modal(i, 'security_logs_channel_modal', 'Security Log Channel', [field('channel', 'Channel ID', x.logChannelId || '')]); } }); -handlers.push({ name: 'security_logs_ignored', execute: async (i) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(i.client, i.guildId); return modal(i, 'security_logs_ignored_modal', 'Ignored Channels', [field('channels', 'Channel IDs, one per line', (x.ignoredChannels || []).join('\n'), TextInputStyle.Paragraph)]); } }); - -for (let strike = 1; strike <= 10; strike++) { - handlers.push({ - name: `security_pun_level_${strike}`, - async execute(i, c) { - if (!authorized(i)) return reject(i); - const x = await getSecurityConfig(c, i.guildId); - const level = (x.escalation || []).find(e => e.strike === strike); - if (!level) return updatePanel(i, c, 'punishments'); - const actions = ['warn', 'timeout', 'kick', 'ban']; - const nextAction = changeCycle(level.action, actions); - const escalation = x.escalation.map(e => e.strike === strike ? { ...e, action: nextAction } : e); - await updateSecurityConfig(c, i.guildId, { escalation }); - return updatePanel(i, c, 'punishments'); - }, - }); -} - -export const securityButtonHandlers = handlers; - -const modalHandlers = [ - { - name: 'security_nuke_threshold_modal', - async execute(i, c) { - if (!authorized(i)) return reject(i); - const x = await getSecurityConfig(c, i.guildId); - const t = { ...x.antiNuke.thresholds }; - for (const key of Object.keys(t)) { - const inputId = key === 'channelDelete' ? 'channelDelete' : key === 'channelCreate' ? 'channelCreate' : key === 'roleDelete' ? 'roleDelete' : key === 'roleCreate' ? 'roleCreate' : key === 'botAdd' ? 'botAdd' : null; - if (inputId) t[key] = num(i.fields.getTextInputValue(inputId), t[key], 1); - } - await updateSecurityConfig(c, i.guildId, { antiNuke: { thresholds: t } }); - return i.update({ embeds: [buildSecurityPanel(await getSecurityConfig(c, i.guildId), i.guild, 'nuke')], components: buildSecurityPanelControls(i.user.id, 'nuke', await getSecurityConfig(c, i.guildId)) }); - }, - }, - { - name: 'security_automod_badwords_modal', - async execute(i, c) { - if (!authorized(i)) return reject(i); - const words = lines(i.fields.getTextInputValue('words')).slice(0, 100); - await updateSecurityConfig(c, i.guildId, { autoMod: { badWords: { enabled: words.length > 0, words } } }); - const x = await getSecurityConfig(c, i.guildId); - return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'automod')], components: buildSecurityPanelControls(i.user.id, 'automod', x) }); - }, - }, - { - name: 'security_whitelist_users_modal', - async execute(i, c) { - if (!authorized(i)) return reject(i); - const x = await updateSecurityConfig(c, i.guildId, { whitelist: { users: lines(i.fields.getTextInputValue('users')).slice(0, 100) } }); - return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'whitelist')], components: buildSecurityPanelControls(i.user.id, 'whitelist', x) }); - }, - }, - { - name: 'security_whitelist_roles_modal', - async execute(i, c) { - if (!authorized(i)) return reject(i); - const x = await updateSecurityConfig(c, i.guildId, { whitelist: { roles: lines(i.fields.getTextInputValue('roles')).slice(0, 100) } }); - return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'whitelist')], components: buildSecurityPanelControls(i.user.id, 'whitelist', x) }); - }, - }, - { - name: 'security_whitelist_bots_modal', - async execute(i, c) { - if (!authorized(i)) return reject(i); - const x = await updateSecurityConfig(c, i.guildId, { whitelist: { bots: lines(i.fields.getTextInputValue('bots')).slice(0, 100) } }); - return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'whitelist')], components: buildSecurityPanelControls(i.user.id, 'whitelist', x) }); - }, - }, - { - name: 'security_logs_channel_modal', - async execute(i, c) { - if (!authorized(i)) return reject(i); - const x = await updateSecurityConfig(c, i.guildId, { logChannelId: i.fields.getTextInputValue('channel').trim() || null }); - return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'logs')], components: buildSecurityPanelControls(i.user.id, 'logs', x) }); - }, - }, - { - name: 'security_logs_ignored_modal', - async execute(i, c) { - if (!authorized(i)) return reject(i); - const x = await updateSecurityConfig(c, i.guildId, { ignoredChannels: lines(i.fields.getTextInputValue('channels')).slice(0, 100) }); - return i.update({ embeds: [buildSecurityPanel(x, i.guild, 'logs')], components: buildSecurityPanelControls(i.user.id, 'logs', x) }); - }, - }, +import { buildSecurityDashboard, buildSecurityControls, buildSecurityPanel, buildSecurityPanelControls } from '../commands/Security/security.js'; + +const PANELS = { security_panel_nuke:'nuke', security_panel_raid:'raid', security_panel_automod:'automod', security_panel_punishments:'punishments', security_panel_whitelist:'whitelist', security_panel_logs:'logs', security_panel_settings:'settings' }; +const NUKE_ACTIONS = ['strip','kick','ban']; +const RAID_ACTIONS = ['timeout','kick','ban']; +const AUTO_ACTIONS = ['delete','warn','timeout','kick','ban']; +const NUKE_KEYS = ['channelDelete','channelCreate','roleDelete','roleCreate','roleUpdate','webhookUpdate','webhookDelete','ban','kick','botAdd']; +const AUTO_KEYS = ['spam','duplicate','mentions','invites','links','caps','badWords']; + +function authorized(i){return i.customId.split(':').at(-1)===i.user.id;} +function reject(i){return i.reply({content:'This security dashboard belongs to another moderator.',flags:MessageFlags.Ephemeral});} +function num(v,f,min=0){const n=Number(v);return Number.isFinite(n)?Math.max(min,n):f;} +function lines(v){return String(v||'').split(/[\s,]+/).map(x=>x.trim()).filter(Boolean);} +function field(id,label,value='',style=TextInputStyle.Short){const input=new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false);if(value!==undefined&&value!==null&&String(value))input.setValue(String(value).slice(0,4000));return new ActionRowBuilder().addComponents(input);} +function modal(i,id,title,fields){return i.showModal(new ModalBuilder().setCustomId(`${id}:${i.user.id}`).setTitle(title).addComponents(...fields));} +function cycle(current,values){const index=values.indexOf(current);return values[(index+1)%values.length];} +async function panel(i,c,p){const x=await getSecurityConfig(c,i.guildId);return i.update({embeds:[buildSecurityPanel(x,i.guild,p)],components:buildSecurityPanelControls(i.user.id,p,x)});} +async function dashboard(i,c){const x=await getSecurityConfig(c,i.guildId);return i.update({embeds:[buildSecurityDashboard(x,i.guild)],components:buildSecurityControls(i.user.id)});} + +const handlers=[]; +for(const [name,p] of Object.entries(PANELS))handlers.push({name,execute:async(i,c)=>authorized(i)?panel(i,c,p):reject(i)}); +handlers.push({name:'security_refresh',execute:async(i,c)=>authorized(i)?dashboard(i,c):reject(i)}); +handlers.push({name:'security_back',execute:async(i,c)=>authorized(i)?dashboard(i,c):reject(i)}); +handlers.push({name:'security_settings_refresh',execute:async(i,c)=>authorized(i)?panel(i,c,'settings'):reject(i)}); +handlers.push({name:'security_settings_toggle',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{enabled:!x.enabled});return panel(i,c,'settings');}}); + +handlers.push({name:'security_nuke_toggle',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiNuke:{enabled:!x.antiNuke.enabled}});return panel(i,c,'nuke');}}); +handlers.push({name:'security_nuke_window',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiNuke:{windowMs:cycle(x.antiNuke.windowMs,[5000,10000,15000,30000,60000])}});return panel(i,c,'nuke');}}); +handlers.push({name:'security_nuke_lockdown',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiNuke:{lockdown:!x.antiNuke.lockdown}});return panel(i,c,'nuke');}}); +handlers.push({name:'security_nuke_threshold',execute:async i=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(i.client,i.guildId),t=x.antiNuke.thresholds||{};return modal(i,'security_nuke_threshold_modal','Anti-Nuke Thresholds',[field('channelDelete','Channel deletes',t.channelDelete),field('channelCreate','Channel creates',t.channelCreate),field('roleDelete','Role deletes',t.roleDelete),field('roleCreate','Role creates',t.roleCreate),field('botAdd','Bot additions',t.botAdd)]);}}); + +handlers.push({name:'security_raid_toggle',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{enabled:!x.antiRaid.enabled}});return panel(i,c,'raid');}}); +handlers.push({name:'security_raid_joins_down',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{joins:Math.max(2,x.antiRaid.joins-1)}});return panel(i,c,'raid');}}); +handlers.push({name:'security_raid_joins_up',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{joins:Math.min(100,x.antiRaid.joins+1)}});return panel(i,c,'raid');}}); +handlers.push({name:'security_raid_window',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{windowMs:cycle(x.antiRaid.windowMs,[5000,10000,15000,30000,60000])}});return panel(i,c,'raid');}}); +handlers.push({name:'security_raid_age',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{minAccountAgeMs:cycle(x.antiRaid.minAccountAgeMs,[0,3600000,21600000,86400000,259200000,604800000,2592000000])}});return panel(i,c,'raid');}}); +handlers.push({name:'security_raid_lockdown',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{lockdown:!x.antiRaid.lockdown}});return panel(i,c,'raid');}}); +handlers.push({name:'security_raid_punishment',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{punishment:cycle(x.antiRaid.punishment||'timeout',RAID_ACTIONS)}});return panel(i,c,'raid');}}); + +const toggleRule=async(i,c,key)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{[key]:{enabled:!x.autoMod[key].enabled}}});return panel(i,c,'automod');}; +const adjust=async(i,c,key,fieldName,delta,min,max)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{[key]:{[fieldName]:Math.min(max,Math.max(min,x.autoMod[key][fieldName]+delta))}}});return panel(i,c,'automod');}; +handlers.push({name:'security_automod_toggle',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{enabled:!x.autoMod.enabled}});return panel(i,c,'automod');}}); +handlers.push({name:'security_automod_spam_toggle',execute:(i,c)=>toggleRule(i,c,'spam')}); +handlers.push({name:'security_automod_dup_toggle',execute:(i,c)=>toggleRule(i,c,'duplicate')}); +handlers.push({name:'security_automod_invites',execute:(i,c)=>toggleRule(i,c,'invites')}); +handlers.push({name:'security_automod_links',execute:(i,c)=>toggleRule(i,c,'links')}); +handlers.push({name:'security_automod_caps',execute:(i,c)=>toggleRule(i,c,'caps')}); +handlers.push({name:'security_automod_spam_down',execute:(i,c)=>adjust(i,c,'spam','maxMessages',-1,2,30)}); +handlers.push({name:'security_automod_spam_up',execute:(i,c)=>adjust(i,c,'spam','maxMessages',1,2,30)}); +handlers.push({name:'security_automod_dup_down',execute:(i,c)=>adjust(i,c,'duplicate','maxRepeats',-1,2,15)}); +handlers.push({name:'security_automod_dup_up',execute:(i,c)=>adjust(i,c,'duplicate','maxRepeats',1,2,15)}); +handlers.push({name:'security_automod_mentions_down',execute:(i,c)=>adjust(i,c,'mentions','max',-1,1,30)}); +handlers.push({name:'security_automod_mentions_up',execute:(i,c)=>adjust(i,c,'mentions','max',1,1,30)}); +handlers.push({name:'security_automod_action',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{action:cycle(x.autoMod.action||'delete',AUTO_ACTIONS)}});return panel(i,c,'automod');}}); +handlers.push({name:'security_automod_badwords',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);return modal(i,'security_automod_badwords_modal','AutoMod Blocked Words',[field('words','Words, separated by spaces',(x.autoMod.badWords.words||[]).join(' '),TextInputStyle.Paragraph)]);}}); +for(const key of AUTO_KEYS)handlers.push({name:`security_automod_${key}_punishment`,execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{[key]:{punishment:cycle(x.autoMod[key].punishment||'delete',AUTO_ACTIONS)}}});return panel(i,c,'automod');}}); + +handlers.push({name:'security_pun_decay_down',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{strikeDecayMs:Math.max(3600000,x.strikeDecayMs-3600000)});return panel(i,c,'punishments');}}); +handlers.push({name:'security_pun_decay_up',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{strikeDecayMs:Math.min(30*86400000,x.strikeDecayMs+3600000)});return panel(i,c,'punishments');}}); +handlers.push({name:'security_pun_raid',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{punishment:cycle(x.antiRaid.punishment||'timeout',RAID_ACTIONS)}});return panel(i,c,'punishments');}}); +for(const key of NUKE_KEYS)handlers.push({name:`security_pun_nuke_${key}`,execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiNuke:{punishments:{[key]:cycle(x.antiNuke.punishments[key]||'strip',NUKE_ACTIONS)}}});return panel(i,c,'punishments');}}); +for(const key of AUTO_KEYS)handlers.push({name:`security_pun_auto_${key}`,execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{[key]:{punishment:cycle(x.autoMod[key].punishment||'delete',AUTO_ACTIONS)}}});return panel(i,c,'punishments');}}); +for(let strike=1;strike<=10;strike++)handlers.push({name:`security_pun_level_${strike}`,execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);const level=x.escalation.find(e=>e.strike===strike);if(!level)return panel(i,c,'punishments');const escalation=x.escalation.map(e=>e.strike===strike?{...e,action:cycle(e.action,['warn','timeout','kick','ban'])}:e);await updateSecurityConfig(c,i.guildId,{escalation});return panel(i,c,'punishments');}}); + +handlers.push({name:'security_whitelist_users',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);return modal(i,'security_whitelist_users_modal','Whitelist Users',[field('users','User IDs, one per line',(x.whitelist.users||[]).join('\n'),TextInputStyle.Paragraph)]);}}); +handlers.push({name:'security_whitelist_roles',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);return modal(i,'security_whitelist_roles_modal','Whitelist Roles',[field('roles','Role IDs, one per line',(x.whitelist.roles||[]).join('\n'),TextInputStyle.Paragraph)]);}}); +handlers.push({name:'security_whitelist_bots',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);return modal(i,'security_whitelist_bots_modal','Whitelist Bots',[field('bots','Bot IDs, one per line',(x.whitelist.bots||[]).join('\n'),TextInputStyle.Paragraph)]);}}); +handlers.push({name:'security_logs_channel',execute:async i=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(i.client,i.guildId);return modal(i,'security_logs_channel_modal','Security Log Channel',[field('channel','Channel ID',x.logChannelId||'')]);}}); +handlers.push({name:'security_logs_ignored',execute:async i=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(i.client,i.guildId);return modal(i,'security_logs_ignored_modal','Ignored Channels',[field('channels','Channel IDs, one per line',(x.ignoredChannels||[]).join('\n'),TextInputStyle.Paragraph)]);}}); + +export const securityButtonHandlers=handlers; + +export const securityModalHandlers=[ +{name:'security_nuke_threshold_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);const t={...x.antiNuke.thresholds};for(const key of ['channelDelete','channelCreate','roleDelete','roleCreate','botAdd'])t[key]=num(i.fields.getTextInputValue(key),t[key],1);await updateSecurityConfig(c,i.guildId,{antiNuke:{thresholds:t}});return panel(i,c,'nuke');}}, +{name:'security_automod_badwords_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);const words=lines(i.fields.getTextInputValue('words')).slice(0,100);await updateSecurityConfig(c,i.guildId,{autoMod:{badWords:{enabled:words.length>0,words}}});return panel(i,c,'automod');}}, +{name:'security_whitelist_users_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{whitelist:{users:lines(i.fields.getTextInputValue('users')).slice(0,100)}});return panel(i,c,'whitelist');}}, +{name:'security_whitelist_roles_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{whitelist:{roles:lines(i.fields.getTextInputValue('roles')).slice(0,100)}});return panel(i,c,'whitelist');}}, +{name:'security_whitelist_bots_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{whitelist:{bots:lines(i.fields.getTextInputValue('bots')).slice(0,100)}});return panel(i,c,'whitelist');}}, +{name:'security_logs_channel_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{logChannelId:i.fields.getTextInputValue('channel').trim()||null});return panel(i,c,'logs');}}, +{name:'security_logs_ignored_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{ignoredChannels:lines(i.fields.getTextInputValue('channels')).slice(0,100)});return panel(i,c,'logs');}}, ]; - -export const securityModalHandlers = modalHandlers; From dc539acee2c0d6bca9ff4044d5d8ebf728504c32 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 12:15:53 +0300 Subject: [PATCH 083/219] Add strikes management to security dashboard --- src/commands/Security/security.js | 45 ++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/commands/Security/security.js b/src/commands/Security/security.js index 26e8688336..319de3818b 100644 --- a/src/commands/Security/security.js +++ b/src/commands/Security/security.js @@ -14,21 +14,26 @@ const PANEL_META = { raid: ['🚨 Anti-Raid', 'Detects rapid joins and suspicious new accounts.'], automod: ['🤖 AutoMod', 'Stops spam, duplicates, mentions, invites, links and other abuse.'], punishments: ['⚖️ Punishments', 'Every protection rule can have its own punishment.'], + strikes: ['🏆 Strike Board', 'See members with the most active security strikes and manage them.'], whitelist: ['👤 Whitelist', 'Trusted users, roles and bots bypass security actions.'], logs: ['📋 Logs', 'Choose where security incidents are reported.'], settings: ['⚙️ Settings', 'Global protection and security behavior.'], }; + const MAIN_BUTTONS = [ ['security_panel_nuke', '🛡️ Anti-Nuke', ButtonStyle.Danger], ['security_panel_raid', '🚨 Anti-Raid', ButtonStyle.Primary], ['security_panel_automod', '🤖 AutoMod', ButtonStyle.Primary], ['security_panel_punishments', '⚖️ Punishments', ButtonStyle.Primary], + ['security_panel_strikes', '🏆 Strikes', ButtonStyle.Danger], ['security_panel_whitelist', '👤 Whitelist', ButtonStyle.Secondary], ['security_panel_logs', '📋 Logs', ButtonStyle.Secondary], ['security_panel_settings', '⚙️ Settings', ButtonStyle.Secondary], ]; + const NUKE_LABELS = { channelDelete: 'Channel Del', channelCreate: 'Channel Add', roleDelete: 'Role Del', roleCreate: 'Role Add', roleUpdate: 'Role Edit', webhookUpdate: 'Webhook Edit', webhookDelete: 'Webhook Del', ban: 'Ban', kick: 'Kick', botAdd: 'Bot Add' }; const AUTOMOD_LABELS = { spam: 'Spam', duplicate: 'Duplicate', mentions: 'Mentions', invites: 'Invites', links: 'Links', caps: 'Caps', badWords: 'Bad Words' }; + function status(enabled) { return enabled ? '🟢 **ACTIVE**' : '🔴 **OFF**'; } function boolLabel(value) { return value ? '🟢 ON' : '🔴 OFF'; } function hours(ms) { return Math.max(0, Math.round(Number(ms || 0) / 3600000)); } @@ -49,7 +54,7 @@ export function buildSecurityDashboard(config, guild) { .setThumbnail(guild.iconURL({ size: 256 }) || null) .addFields( { name: '━━ Protection ━━', value: [`🛡️ Anti-Nuke ${status(config.antiNuke?.enabled)}`, `🚨 Anti-Raid ${status(config.antiRaid?.enabled)}`, `🤖 AutoMod ${status(config.autoMod?.enabled)}`, `🔒 Global ${status(config.enabled)}`].join('\n'), inline: true }, - { name: '━━ Statistics ━━', value: [`⚡ **${config.escalation?.length || 0}** escalation levels`, `👤 **${whitelistCount}** whitelist entries`, `⏱️ **${hours(config.strikeDecayMs)}h** strike decay`, `📋 Logs: ${logValue}`].join('\n'), inline: true }, + { name: '━━ Statistics ━━', value: [`⚡ **${config.escalation?.length || 0}** escalation levels`, `🏆 **Strike management** enabled`, `👤 **${whitelistCount}** whitelist entries`, `📋 Logs: ${logValue}`].join('\n'), inline: true }, { name: '━━ Current Mode ━━', value: config.enabled ? '🟢 **PROTECTED** — security systems are actively monitoring this server.' : '🔴 **DISABLED** — global security protection is currently off.' }, ) .setFooter({ text: 'Infinity System • Security Center • Changes save automatically' }) @@ -76,7 +81,6 @@ export function buildSecurityPanel(config, guild, panel) { `Roles: delete \`${config.antiNuke?.thresholds?.roleDelete ?? 3}\` • create \`${config.antiNuke?.thresholds?.roleCreate ?? 5}\` • edit \`${config.antiNuke?.thresholds?.roleUpdate ?? 1}\``, `Webhooks: edit \`${config.antiNuke?.thresholds?.webhookUpdate ?? 3}\` • delete \`${config.antiNuke?.thresholds?.webhookDelete ?? 2}\``, `Ban \`${config.antiNuke?.thresholds?.ban ?? 5}\` • Kick \`${config.antiNuke?.thresholds?.kick ?? 5}\` • Bot add \`${config.antiNuke?.thresholds?.botAdd ?? 1}\``, - '', 'Use **Punishments** to configure a different punishment for every operation.' ]; else if (panel === 'raid') data = [ `**Status:** ${status(config.antiRaid?.enabled)}`, @@ -94,17 +98,42 @@ export function buildSecurityPanel(config, guild, panel) { `**Invites:** ${boolLabel(config.autoMod?.invites?.enabled)} • **${config.autoMod?.invites?.punishment || 'delete'}**`, `**Links:** ${boolLabel(config.autoMod?.links?.enabled)} • **${config.autoMod?.links?.punishment || 'delete'}** • **Caps:** ${boolLabel(config.autoMod?.caps?.enabled)} • **${config.autoMod?.caps?.punishment || 'warn'}**`, `**Bad words:** ${config.autoMod?.badWords?.words?.length || 0} words • **${config.autoMod?.badWords?.punishment || 'timeout'}**`, - '', 'Escalation can override the rule punishment when a member reaches a configured strike level.' ]; else if (panel === 'punishments') { const nuke = Object.entries(NUKE_LABELS).map(([key, label]) => `${label}: **${config.antiNuke?.punishments?.[key] || config.antiNuke?.action || 'strip'}**`); const automod = Object.entries(AUTOMOD_LABELS).map(([key, label]) => `${label}: **${config.autoMod?.[key]?.punishment || 'delete'}**`); - data = ['**Per-rule punishments**', '', '**Anti-Nuke**', ...nuke, '', `**Anti-Raid:** **${config.antiRaid?.punishment || config.antiRaid?.action || 'timeout'}**`, '', '**AutoMod**', ...automod, '', '**Escalation**', ...(config.escalation || []).slice(0, 10).map(e => `Strike ${e.strike}: **${e.action}**${e.durationMs ? ` • ${Math.round(e.durationMs / 60000)}m` : ''}`), '', `Strike decay: **${hours(config.strikeDecayMs)}h**`]; - } else if (panel === 'whitelist') data = [`**Users:** \`${config.whitelist?.users?.length || 0}\``, `**Roles:** \`${config.whitelist?.roles?.length || 0}\``, `**Bots:** \`${config.whitelist?.bots?.length || 0}\``, '', 'Whitelisted accounts bypass Anti-Nuke and AutoMod enforcement where applicable.']; + data = ['**Per-rule punishments**', '', '**Anti-Nuke**', ...nuke, '', `**Anti-Raid:** **${config.antiRaid?.punishment || 'timeout'}**`, '', '**AutoMod**', ...automod, '', '**Escalation**', ...(config.escalation || []).slice(0, 10).map(e => `Strike ${e.strike}: **${e.action}**${e.durationMs ? ` • ${Math.round(e.durationMs / 60000)}m` : ''}`), '', `Strike decay: **${hours(config.strikeDecayMs)}h**`]; + } else if (panel === 'strikes') data = [ + '🏆 **Top active security strikes**', + '', + 'This board shows members with the highest active Strike count. Expired strikes are ignored automatically.', + '', + 'Use the buttons below to reset a member completely or refresh the board.', + ]; + else if (panel === 'whitelist') data = [`**Users:** \`${config.whitelist?.users?.length || 0}\``, `**Roles:** \`${config.whitelist?.roles?.length || 0}\``, `**Bots:** \`${config.whitelist?.bots?.length || 0}\``, '', 'Whitelisted accounts bypass Anti-Nuke and AutoMod enforcement where applicable.']; else if (panel === 'logs') data = [`**Log channel:** ${config.logChannelId ? `<#${config.logChannelId}>` : '`Not configured`'}`, `**Ignored channels:** \`${config.ignoredChannels?.length || 0}\``, '', 'Security incidents include the executor/member, reason and action taken.']; else data = [`**Global protection:** ${status(config.enabled)}`, `**Anti-Nuke:** ${status(config.antiNuke?.enabled)}`, `**Anti-Raid:** ${status(config.antiRaid?.enabled)}`, `**AutoMod:** ${status(config.autoMod?.enabled)}`, '', 'Use the controls below to change protection without leaving this message.']; - return new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }).setTitle(title).setDescription(`${description}\n\n${data.join('\n')}`).setColor(panel === 'nuke' ? 0xed4245 : panel === 'raid' ? 0xf47b67 : panel === 'automod' ? 0x5865f2 : 0x57f287).setFooter({ text: 'Infinity System • Click a control below • Changes save automatically' }).setTimestamp(); + return new EmbedBuilder() + .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) + .setTitle(title) + .setDescription(`${description}\n\n${data.join('\n')}`) + .setColor(panel === 'nuke' ? 0xed4245 : panel === 'raid' ? 0xf47b67 : panel === 'automod' ? 0x5865f2 : panel === 'strikes' ? 0xfee75c : 0x57f287) + .setFooter({ text: 'Infinity System • Click a control below • Changes save automatically' }) + .setTimestamp(); +} + +export function buildStrikeBoardEmbed(guild, entries) { + const lines = entries.length + ? entries.map((entry, index) => `${index + 1}. <@${entry.userId}> — **${entry.count} strike${entry.count === 1 ? '' : 's'}**${entry.lastReason ? ` • ${String(entry.lastReason).slice(0, 80)}` : ''}`).join('\n') + : '✅ No active strikes found.'; + return new EmbedBuilder() + .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) + .setTitle('🏆 Strike Leaderboard') + .setDescription(`**${guild.name}**\n\n${lines}`) + .setColor(0xfee75c) + .setFooter({ text: 'Resetting a strike only changes the security strike counter; security logs remain intact.' }) + .setTimestamp(); } export function buildSecurityPanelControls(userId, panel, config) { @@ -128,6 +157,8 @@ export function buildSecurityPanelControls(userId, panel, config) { const autoKeys = Object.keys(AUTOMOD_LABELS); for (let i = 0; i < autoKeys.length; i += 5) rows.push(new ActionRowBuilder().addComponents(...autoKeys.slice(i, i + 5).map(k => button(id(`security_pun_auto_${k}`), `${AUTOMOD_LABELS[k]}: ${config.autoMod[k].punishment}`, ButtonStyle.Primary)))); for (let i = 0; i < Math.min(10, (config.escalation || []).length); i += 5) rows.push(new ActionRowBuilder().addComponents(...config.escalation.slice(i, i + 5).map(level => button(id(`security_pun_level_${level.strike}`), `#${level.strike}: ${level.action}`, ButtonStyle.Secondary)))); + } else if (panel === 'strikes') { + rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_strikes_refresh'), '🔄 Refresh', ButtonStyle.Success))); } else if (panel === 'whitelist') rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_whitelist_users'), '👤 Manage Users', ButtonStyle.Primary), button(id('security_whitelist_roles'), '🎭 Manage Roles', ButtonStyle.Primary), button(id('security_whitelist_bots'), '🤖 Manage Bots', ButtonStyle.Primary))); else if (panel === 'logs') rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_logs_channel'), '📋 Set Log Channel', ButtonStyle.Primary), button(id('security_logs_ignored'), '🚫 Ignored Channels'))); else rows.push(new ActionRowBuilder().addComponents(button(id('security_back'), '← Back'), button(id('security_settings_toggle'), config.enabled ? '🟢 Disable Protection' : '🔴 Enable Protection', config.enabled ? ButtonStyle.Danger : ButtonStyle.Success), button(id('security_settings_refresh'), '🔄 Refresh'))); @@ -143,4 +174,4 @@ export default { const security = await getSecurityConfig(client, interaction.guildId); await interaction.reply({ embeds: [buildSecurityDashboard(security, interaction.guild)], components: buildSecurityControls(interaction.user.id), flags: MessageFlags.Ephemeral }); }, -}; +}; \ No newline at end of file From 6118b651c8c05c46711e4372a35da25e1650f634 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 12:16:16 +0300 Subject: [PATCH 084/219] Add security strike leaderboard and reset controls --- src/handlers/securityHandlers.js | 264 +++++++++++++++++++++---------- 1 file changed, 182 insertions(+), 82 deletions(-) diff --git a/src/handlers/securityHandlers.js b/src/handlers/securityHandlers.js index 91bcdc317a..df7343b183 100644 --- a/src/handlers/securityHandlers.js +++ b/src/handlers/securityHandlers.js @@ -1,83 +1,183 @@ import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; -import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; -import { buildSecurityDashboard, buildSecurityControls, buildSecurityPanel, buildSecurityPanelControls } from '../commands/Security/security.js'; - -const PANELS = { security_panel_nuke:'nuke', security_panel_raid:'raid', security_panel_automod:'automod', security_panel_punishments:'punishments', security_panel_whitelist:'whitelist', security_panel_logs:'logs', security_panel_settings:'settings' }; -const NUKE_ACTIONS = ['strip','kick','ban']; -const RAID_ACTIONS = ['timeout','kick','ban']; -const AUTO_ACTIONS = ['delete','warn','timeout','kick','ban']; -const NUKE_KEYS = ['channelDelete','channelCreate','roleDelete','roleCreate','roleUpdate','webhookUpdate','webhookDelete','ban','kick','botAdd']; -const AUTO_KEYS = ['spam','duplicate','mentions','invites','links','caps','badWords']; - -function authorized(i){return i.customId.split(':').at(-1)===i.user.id;} -function reject(i){return i.reply({content:'This security dashboard belongs to another moderator.',flags:MessageFlags.Ephemeral});} -function num(v,f,min=0){const n=Number(v);return Number.isFinite(n)?Math.max(min,n):f;} -function lines(v){return String(v||'').split(/[\s,]+/).map(x=>x.trim()).filter(Boolean);} -function field(id,label,value='',style=TextInputStyle.Short){const input=new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false);if(value!==undefined&&value!==null&&String(value))input.setValue(String(value).slice(0,4000));return new ActionRowBuilder().addComponents(input);} -function modal(i,id,title,fields){return i.showModal(new ModalBuilder().setCustomId(`${id}:${i.user.id}`).setTitle(title).addComponents(...fields));} -function cycle(current,values){const index=values.indexOf(current);return values[(index+1)%values.length];} -async function panel(i,c,p){const x=await getSecurityConfig(c,i.guildId);return i.update({embeds:[buildSecurityPanel(x,i.guild,p)],components:buildSecurityPanelControls(i.user.id,p,x)});} -async function dashboard(i,c){const x=await getSecurityConfig(c,i.guildId);return i.update({embeds:[buildSecurityDashboard(x,i.guild)],components:buildSecurityControls(i.user.id)});} - -const handlers=[]; -for(const [name,p] of Object.entries(PANELS))handlers.push({name,execute:async(i,c)=>authorized(i)?panel(i,c,p):reject(i)}); -handlers.push({name:'security_refresh',execute:async(i,c)=>authorized(i)?dashboard(i,c):reject(i)}); -handlers.push({name:'security_back',execute:async(i,c)=>authorized(i)?dashboard(i,c):reject(i)}); -handlers.push({name:'security_settings_refresh',execute:async(i,c)=>authorized(i)?panel(i,c,'settings'):reject(i)}); -handlers.push({name:'security_settings_toggle',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{enabled:!x.enabled});return panel(i,c,'settings');}}); - -handlers.push({name:'security_nuke_toggle',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiNuke:{enabled:!x.antiNuke.enabled}});return panel(i,c,'nuke');}}); -handlers.push({name:'security_nuke_window',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiNuke:{windowMs:cycle(x.antiNuke.windowMs,[5000,10000,15000,30000,60000])}});return panel(i,c,'nuke');}}); -handlers.push({name:'security_nuke_lockdown',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiNuke:{lockdown:!x.antiNuke.lockdown}});return panel(i,c,'nuke');}}); -handlers.push({name:'security_nuke_threshold',execute:async i=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(i.client,i.guildId),t=x.antiNuke.thresholds||{};return modal(i,'security_nuke_threshold_modal','Anti-Nuke Thresholds',[field('channelDelete','Channel deletes',t.channelDelete),field('channelCreate','Channel creates',t.channelCreate),field('roleDelete','Role deletes',t.roleDelete),field('roleCreate','Role creates',t.roleCreate),field('botAdd','Bot additions',t.botAdd)]);}}); - -handlers.push({name:'security_raid_toggle',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{enabled:!x.antiRaid.enabled}});return panel(i,c,'raid');}}); -handlers.push({name:'security_raid_joins_down',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{joins:Math.max(2,x.antiRaid.joins-1)}});return panel(i,c,'raid');}}); -handlers.push({name:'security_raid_joins_up',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{joins:Math.min(100,x.antiRaid.joins+1)}});return panel(i,c,'raid');}}); -handlers.push({name:'security_raid_window',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{windowMs:cycle(x.antiRaid.windowMs,[5000,10000,15000,30000,60000])}});return panel(i,c,'raid');}}); -handlers.push({name:'security_raid_age',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{minAccountAgeMs:cycle(x.antiRaid.minAccountAgeMs,[0,3600000,21600000,86400000,259200000,604800000,2592000000])}});return panel(i,c,'raid');}}); -handlers.push({name:'security_raid_lockdown',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{lockdown:!x.antiRaid.lockdown}});return panel(i,c,'raid');}}); -handlers.push({name:'security_raid_punishment',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{punishment:cycle(x.antiRaid.punishment||'timeout',RAID_ACTIONS)}});return panel(i,c,'raid');}}); - -const toggleRule=async(i,c,key)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{[key]:{enabled:!x.autoMod[key].enabled}}});return panel(i,c,'automod');}; -const adjust=async(i,c,key,fieldName,delta,min,max)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{[key]:{[fieldName]:Math.min(max,Math.max(min,x.autoMod[key][fieldName]+delta))}}});return panel(i,c,'automod');}; -handlers.push({name:'security_automod_toggle',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{enabled:!x.autoMod.enabled}});return panel(i,c,'automod');}}); -handlers.push({name:'security_automod_spam_toggle',execute:(i,c)=>toggleRule(i,c,'spam')}); -handlers.push({name:'security_automod_dup_toggle',execute:(i,c)=>toggleRule(i,c,'duplicate')}); -handlers.push({name:'security_automod_invites',execute:(i,c)=>toggleRule(i,c,'invites')}); -handlers.push({name:'security_automod_links',execute:(i,c)=>toggleRule(i,c,'links')}); -handlers.push({name:'security_automod_caps',execute:(i,c)=>toggleRule(i,c,'caps')}); -handlers.push({name:'security_automod_spam_down',execute:(i,c)=>adjust(i,c,'spam','maxMessages',-1,2,30)}); -handlers.push({name:'security_automod_spam_up',execute:(i,c)=>adjust(i,c,'spam','maxMessages',1,2,30)}); -handlers.push({name:'security_automod_dup_down',execute:(i,c)=>adjust(i,c,'duplicate','maxRepeats',-1,2,15)}); -handlers.push({name:'security_automod_dup_up',execute:(i,c)=>adjust(i,c,'duplicate','maxRepeats',1,2,15)}); -handlers.push({name:'security_automod_mentions_down',execute:(i,c)=>adjust(i,c,'mentions','max',-1,1,30)}); -handlers.push({name:'security_automod_mentions_up',execute:(i,c)=>adjust(i,c,'mentions','max',1,1,30)}); -handlers.push({name:'security_automod_action',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{action:cycle(x.autoMod.action||'delete',AUTO_ACTIONS)}});return panel(i,c,'automod');}}); -handlers.push({name:'security_automod_badwords',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);return modal(i,'security_automod_badwords_modal','AutoMod Blocked Words',[field('words','Words, separated by spaces',(x.autoMod.badWords.words||[]).join(' '),TextInputStyle.Paragraph)]);}}); -for(const key of AUTO_KEYS)handlers.push({name:`security_automod_${key}_punishment`,execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{[key]:{punishment:cycle(x.autoMod[key].punishment||'delete',AUTO_ACTIONS)}}});return panel(i,c,'automod');}}); - -handlers.push({name:'security_pun_decay_down',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{strikeDecayMs:Math.max(3600000,x.strikeDecayMs-3600000)});return panel(i,c,'punishments');}}); -handlers.push({name:'security_pun_decay_up',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{strikeDecayMs:Math.min(30*86400000,x.strikeDecayMs+3600000)});return panel(i,c,'punishments');}}); -handlers.push({name:'security_pun_raid',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{punishment:cycle(x.antiRaid.punishment||'timeout',RAID_ACTIONS)}});return panel(i,c,'punishments');}}); -for(const key of NUKE_KEYS)handlers.push({name:`security_pun_nuke_${key}`,execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiNuke:{punishments:{[key]:cycle(x.antiNuke.punishments[key]||'strip',NUKE_ACTIONS)}}});return panel(i,c,'punishments');}}); -for(const key of AUTO_KEYS)handlers.push({name:`security_pun_auto_${key}`,execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{[key]:{punishment:cycle(x.autoMod[key].punishment||'delete',AUTO_ACTIONS)}}});return panel(i,c,'punishments');}}); -for(let strike=1;strike<=10;strike++)handlers.push({name:`security_pun_level_${strike}`,execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);const level=x.escalation.find(e=>e.strike===strike);if(!level)return panel(i,c,'punishments');const escalation=x.escalation.map(e=>e.strike===strike?{...e,action:cycle(e.action,['warn','timeout','kick','ban'])}:e);await updateSecurityConfig(c,i.guildId,{escalation});return panel(i,c,'punishments');}}); - -handlers.push({name:'security_whitelist_users',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);return modal(i,'security_whitelist_users_modal','Whitelist Users',[field('users','User IDs, one per line',(x.whitelist.users||[]).join('\n'),TextInputStyle.Paragraph)]);}}); -handlers.push({name:'security_whitelist_roles',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);return modal(i,'security_whitelist_roles_modal','Whitelist Roles',[field('roles','Role IDs, one per line',(x.whitelist.roles||[]).join('\n'),TextInputStyle.Paragraph)]);}}); -handlers.push({name:'security_whitelist_bots',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);return modal(i,'security_whitelist_bots_modal','Whitelist Bots',[field('bots','Bot IDs, one per line',(x.whitelist.bots||[]).join('\n'),TextInputStyle.Paragraph)]);}}); -handlers.push({name:'security_logs_channel',execute:async i=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(i.client,i.guildId);return modal(i,'security_logs_channel_modal','Security Log Channel',[field('channel','Channel ID',x.logChannelId||'')]);}}); -handlers.push({name:'security_logs_ignored',execute:async i=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(i.client,i.guildId);return modal(i,'security_logs_ignored_modal','Ignored Channels',[field('channels','Channel IDs, one per line',(x.ignoredChannels||[]).join('\n'),TextInputStyle.Paragraph)]);}}); - -export const securityButtonHandlers=handlers; - -export const securityModalHandlers=[ -{name:'security_nuke_threshold_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);const x=await getSecurityConfig(c,i.guildId);const t={...x.antiNuke.thresholds};for(const key of ['channelDelete','channelCreate','roleDelete','roleCreate','botAdd'])t[key]=num(i.fields.getTextInputValue(key),t[key],1);await updateSecurityConfig(c,i.guildId,{antiNuke:{thresholds:t}});return panel(i,c,'nuke');}}, -{name:'security_automod_badwords_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);const words=lines(i.fields.getTextInputValue('words')).slice(0,100);await updateSecurityConfig(c,i.guildId,{autoMod:{badWords:{enabled:words.length>0,words}}});return panel(i,c,'automod');}}, -{name:'security_whitelist_users_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{whitelist:{users:lines(i.fields.getTextInputValue('users')).slice(0,100)}});return panel(i,c,'whitelist');}}, -{name:'security_whitelist_roles_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{whitelist:{roles:lines(i.fields.getTextInputValue('roles')).slice(0,100)}});return panel(i,c,'whitelist');}}, -{name:'security_whitelist_bots_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{whitelist:{bots:lines(i.fields.getTextInputValue('bots')).slice(0,100)}});return panel(i,c,'whitelist');}}, -{name:'security_logs_channel_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{logChannelId:i.fields.getTextInputValue('channel').trim()||null});return panel(i,c,'logs');}}, -{name:'security_logs_ignored_modal',execute:async(i,c)=>{if(!authorized(i))return reject(i);await updateSecurityConfig(c,i.guildId,{ignoredChannels:lines(i.fields.getTextInputValue('channels')).slice(0,100)});return panel(i,c,'logs');}}, -]; +import { + getSecurityConfig, + updateSecurityConfig, + getStrikes, + clearStrikes, + sendSecurityLog, +} from '../services/security/securityService.js'; +import { + buildSecurityDashboard, + buildSecurityControls, + buildSecurityPanel, + buildSecurityPanelControls, + buildStrikeBoardEmbed, +} from '../commands/Security/security.js'; + +const PANELS = { + security_panel_nuke: 'nuke', + security_panel_raid: 'raid', + security_panel_automod: 'automod', + security_panel_punishments: 'punishments', + security_panel_strikes: 'strikes', + security_panel_whitelist: 'whitelist', + security_panel_logs: 'logs', + security_panel_settings: 'settings', +}; +const NUKE_ACTIONS = ['strip', 'kick', 'ban']; +const RAID_ACTIONS = ['timeout', 'kick', 'ban']; +const AUTO_ACTIONS = ['delete', 'warn', 'timeout', 'kick', 'ban']; +const NUKE_KEYS = ['channelDelete', 'channelCreate', 'roleDelete', 'roleCreate', 'roleUpdate', 'webhookUpdate', 'webhookDelete', 'ban', 'kick', 'botAdd']; +const AUTO_KEYS = ['spam', 'duplicate', 'mentions', 'invites', 'links', 'caps', 'badWords']; + +function authorized(i) { return i.customId.split(':').at(-1) === i.user.id; } +function reject(i) { return i.reply({ content: 'This security dashboard belongs to another moderator.', flags: MessageFlags.Ephemeral }); } +function num(v, fallback, min = 0) { const n = Number(v); return Number.isFinite(n) ? Math.max(min, n) : fallback; } +function lines(v) { return String(v || '').split(/[\s,]+/).map(x => x.trim()).filter(Boolean); } +function field(id, label, value = '', style = TextInputStyle.Short) { + const input = new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false); + if (value !== undefined && value !== null && String(value)) input.setValue(String(value).slice(0, 4000)); + return new ActionRowBuilder().addComponents(input); +} +function modal(i, id, title, fields) { return i.showModal(new ModalBuilder().setCustomId(`${id}:${i.user.id}`).setTitle(title).addComponents(...fields)); } +function cycle(current, values) { const index = values.indexOf(current); return values[(index + 1) % values.length]; } + +async function panel(i, client, panelName) { + const config = await getSecurityConfig(client, i.guildId); + if (panelName === 'strikes') return strikeBoard(i, client); + return i.update({ embeds: [buildSecurityPanel(config, i.guild, panelName)], components: buildSecurityPanelControls(i.user.id, panelName, config) }); +} + +async function dashboard(i, client) { + const config = await getSecurityConfig(client, i.guildId); + return i.update({ embeds: [buildSecurityDashboard(config, i.guild)], components: buildSecurityControls(i.user.id) }); +} + +async function getStrikeEntries(client, guild) { + const config = await getSecurityConfig(client, guild.id); + const members = await guild.members.fetch().catch(() => guild.members.cache); + const now = Date.now(); + const entries = []; + for (const member of members.values()) { + if (member.user?.bot) continue; + const strike = await getStrikes(client, guild.id, member.id).catch(() => ({ count: 0, updatedAt: 0 })); + if (!strike?.count) continue; + if (config.strikeDecayMs && strike.updatedAt && now - strike.updatedAt > Number(config.strikeDecayMs)) continue; + entries.push({ userId: member.id, count: Number(strike.count || 0), updatedAt: strike.updatedAt || 0, lastReason: strike.lastReason || '' }); + } + return entries.sort((a, b) => b.count - a.count || b.updatedAt - a.updatedAt).slice(0, 10); +} + +async function strikeBoard(i, client) { + const entries = await getStrikeEntries(client, i.guild); + const embed = buildStrikeBoardEmbed(i.guild, entries); + const rows = []; + if (entries.length) { + for (let offset = 0; offset < entries.length; offset += 4) { + rows.push(new ActionRowBuilder().addComponents(...entries.slice(offset, offset + 4).map(entry => { + return new (requireButton())().setCustomId(`security_strike_reset:${entry.userId}:${i.user.id}`).setLabel(`Reset ${entry.userId.slice(-4)}`).setStyle(4); + }))); + } + } + rows.push(new ActionRowBuilder().addComponents( + new (requireButton())().setCustomId(`security_back:${i.user.id}`).setLabel('← Back').setStyle(2), + new (requireButton())().setCustomId(`security_strikes_refresh:${i.user.id}`).setLabel('🔄 Refresh').setStyle(3), + )); + return i.update({ embeds: [embed], components: rows.slice(0, 5) }); +} + +function requireButton() { + return class { + constructor() { this.data = {}; } + setCustomId(value) { this.data.custom_id = value; return this; } + setLabel(value) { this.data.label = value; return this; } + setStyle(value) { this.data.style = value; return this; } + toJSON() { return { type: 2, ...this.data }; } + }; +} + +const handlers = []; +for (const [name, p] of Object.entries(PANELS)) handlers.push({ name, execute: async (i, c) => authorized(i) ? panel(i, c, p) : reject(i) }); +handlers.push({ name: 'security_refresh', execute: async (i, c) => authorized(i) ? dashboard(i, c) : reject(i) }); +handlers.push({ name: 'security_back', execute: async (i, c) => authorized(i) ? dashboard(i, c) : reject(i) }); +handlers.push({ name: 'security_strikes_refresh', execute: async (i, c) => authorized(i) ? strikeBoard(i, c) : reject(i) }); +handlers.push({ + name: 'security_strike_reset', + execute: async (i, c) => { + if (!authorized(i)) return reject(i); + const parts = i.customId.split(':'); + const userId = parts.at(-2); + const member = await i.guild.members.fetch(userId).catch(() => null); + await clearStrikes(c, i.guildId, userId); + await sendSecurityLog(c, i.guild, { + title: 'Security Strikes Reset', + description: `Security strikes were reset for <@${userId}>.`, + color: 0x57f287, + fields: [ + { name: 'Member', value: member ? `${member.user.tag} (${userId})` : userId, inline: true }, + { name: 'Moderator', value: `${i.user.tag} (${i.user.id})`, inline: true }, + { name: 'Action', value: 'Reset all strikes', inline: true }, + ], + }); + return strikeBoard(i, c); + }, +}); + +handlers.push({ name: 'security_settings_refresh', execute: async (i, c) => authorized(i) ? panel(i, c, 'settings') : reject(i) }); +handlers.push({ name: 'security_settings_toggle', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { enabled: !x.enabled }); return panel(i, c, 'settings'); } }); + +handlers.push({ name: 'security_nuke_toggle', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { enabled: !x.antiNuke.enabled } }); return panel(i, c, 'nuke'); } }); +handlers.push({ name: 'security_nuke_window', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { windowMs: cycle(x.antiNuke.windowMs, [5000, 10000, 15000, 30000, 60000]) } }); return panel(i, c, 'nuke'); } }); +handlers.push({ name: 'security_nuke_lockdown', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { lockdown: !x.antiNuke.lockdown } }); return panel(i, c, 'nuke'); } }); +handlers.push({ name: 'security_nuke_threshold', execute: async i => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(i.client, i.guildId); const t = x.antiNuke.thresholds || {}; return modal(i, 'security_nuke_threshold_modal', 'Anti-Nuke Thresholds', [field('channelDelete', 'Channel deletes', t.channelDelete), field('channelCreate', 'Channel creates', t.channelCreate), field('roleDelete', 'Role deletes', t.roleDelete), field('roleCreate', 'Role creates', t.roleCreate), field('botAdd', 'Bot additions', t.botAdd)]); } }); + +handlers.push({ name: 'security_raid_toggle', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { enabled: !x.antiRaid.enabled } }); return panel(i, c, 'raid'); } }); +handlers.push({ name: 'security_raid_joins_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { joins: Math.max(2, x.antiRaid.joins - 1) } }); return panel(i, c, 'raid'); } }); +handlers.push({ name: 'security_raid_joins_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { joins: Math.min(100, x.antiRaid.joins + 1) } }); return panel(i, c, 'raid'); } }); +handlers.push({ name: 'security_raid_window', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { windowMs: cycle(x.antiRaid.windowMs, [5000, 10000, 15000, 30000, 60000]) } }); return panel(i, c, 'raid'); } }); +handlers.push({ name: 'security_raid_age', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { minAccountAgeMs: cycle(x.antiRaid.minAccountAgeMs, [0, 3600000, 21600000, 86400000, 604800000, 2592000000]) } }); return panel(i, c, 'raid'); } }); +handlers.push({ name: 'security_raid_lockdown', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { lockdown: !x.antiRaid.lockdown } }); return panel(i, c, 'raid'); } }); +handlers.push({ name: 'security_raid_punishment', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { punishment: cycle(x.antiRaid.punishment || 'timeout', RAID_ACTIONS) } }); return panel(i, c, 'raid'); } }); + +const toggleRule = async (i, c, key) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { enabled: !x.autoMod[key].enabled } } }); return panel(i, c, 'automod'); }; +const adjust = async (i, c, key, fieldName, delta, min, max) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { [fieldName]: Math.min(max, Math.max(min, x.autoMod[key][fieldName] + delta)) } } }); return panel(i, c, 'automod'); }; +handlers.push({ name: 'security_automod_toggle', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { enabled: !x.autoMod.enabled } }); return panel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_spam_toggle', execute: (i, c) => toggleRule(i, c, 'spam') }); +handlers.push({ name: 'security_automod_dup_toggle', execute: (i, c) => toggleRule(i, c, 'duplicate') }); +handlers.push({ name: 'security_automod_invites', execute: (i, c) => toggleRule(i, c, 'invites') }); +handlers.push({ name: 'security_automod_links', execute: (i, c) => toggleRule(i, c, 'links') }); +handlers.push({ name: 'security_automod_caps', execute: (i, c) => toggleRule(i, c, 'caps') }); +handlers.push({ name: 'security_automod_spam_down', execute: (i, c) => adjust(i, c, 'spam', 'maxMessages', -1, 2, 30) }); +handlers.push({ name: 'security_automod_spam_up', execute: (i, c) => adjust(i, c, 'spam', 'maxMessages', 1, 2, 30) }); +handlers.push({ name: 'security_automod_dup_down', execute: (i, c) => adjust(i, c, 'duplicate', 'maxRepeats', -1, 2, 15) }); +handlers.push({ name: 'security_automod_dup_up', execute: (i, c) => adjust(i, c, 'duplicate', 'maxRepeats', 1, 2, 15) }); +handlers.push({ name: 'security_automod_mentions_down', execute: (i, c) => adjust(i, c, 'mentions', 'max', -1, 1, 30) }); +handlers.push({ name: 'security_automod_mentions_up', execute: (i, c) => adjust(i, c, 'mentions', 'max', 1, 1, 30) }); +handlers.push({ name: 'security_automod_action', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { action: cycle(x.autoMod.action || 'delete', AUTO_ACTIONS) } }); return panel(i, c, 'automod'); } }); +handlers.push({ name: 'security_automod_badwords', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_automod_badwords_modal', 'AutoMod Blocked Words', [field('words', 'Words, separated by spaces', (x.autoMod.badWords.words || []).join(' '), TextInputStyle.Paragraph)]); } }); +for (const key of AUTO_KEYS) handlers.push({ name: `security_automod_${key}_punishment`, execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { punishment: cycle(x.autoMod[key].punishment || 'delete', AUTO_ACTIONS) } } }); return panel(i, c, 'automod'); } }); + +handlers.push({ name: 'security_pun_decay_down', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { strikeDecayMs: Math.max(3600000, x.strikeDecayMs - 3600000) }); return panel(i, c, 'punishments'); } }); +handlers.push({ name: 'security_pun_decay_up', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { strikeDecayMs: Math.min(30 * 86400000, x.strikeDecayMs + 3600000) }); return panel(i, c, 'punishments'); } }); +handlers.push({ name: 'security_pun_raid', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { punishment: cycle(x.antiRaid.punishment || 'timeout', RAID_ACTIONS) } }); return panel(i, c, 'punishments'); } }); +for (const key of NUKE_KEYS) handlers.push({ name: `security_pun_nuke_${key}`, execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { punishments: { [key]: cycle(x.antiNuke.punishments[key] || 'strip', NUKE_ACTIONS) } } }); return panel(i, c, 'punishments'); } }); +for (const key of AUTO_KEYS) handlers.push({ name: `security_pun_auto_${key}`, execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { punishment: cycle(x.autoMod[key].punishment || 'delete', AUTO_ACTIONS) } } }); return panel(i, c, 'punishments'); } }); +for (let strike = 1; strike <= 10; strike++) handlers.push({ name: `security_pun_level_${strike}`, execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); const escalation = x.escalation.map(e => e.strike === strike ? { ...e, action: cycle(e.action, ['warn', 'timeout', 'kick', 'ban']) } : e); await updateSecurityConfig(c, i.guildId, { escalation }); return panel(i, c, 'punishments'); } }); + +handlers.push({ name: 'security_whitelist_users', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_whitelist_users_modal', 'Whitelist Users', [field('users', 'User IDs, one per line', (x.whitelist.users || []).join('\n'), TextInputStyle.Paragraph)]); } }); +handlers.push({ name: 'security_whitelist_roles', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_whitelist_roles_modal', 'Whitelist Roles', [field('roles', 'Role IDs, one per line', (x.whitelist.roles || []).join('\n'), TextInputStyle.Paragraph)]); } }); +handlers.push({ name: 'security_whitelist_bots', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); return modal(i, 'security_whitelist_bots_modal', 'Whitelist Bots', [field('bots', 'Bot IDs, one per line', (x.whitelist.bots || []).join('\n'), TextInputStyle.Paragraph)]); } }); +handlers.push({ name: 'security_logs_channel', execute: async i => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(i.client, i.guildId); return modal(i, 'security_logs_channel_modal', 'Security Log Channel', [field('channel', 'Channel ID', x.logChannelId || '')]); } }); +handlers.push({ name: 'security_logs_ignored', execute: async i => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(i.client, i.guildId); return modal(i, 'security_logs_ignored_modal', 'Ignored Channels', [field('channels', 'Channel IDs, one per line', (x.ignoredChannels || []).join('\n'), TextInputStyle.Paragraph)]); } }); + +export const securityButtonHandlers = handlers; + +export const securityModalHandlers = [ + { name: 'security_nuke_threshold_modal', execute: async (i, c) => { if (!authorized(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); const t = { ...x.antiNuke.thresholds }; for (const key of ['channelDelete', 'channelCreate', 'roleDelete', 'roleCreate', 'botAdd']) t[key] = num(i.fields.getTextInputValue(key), t[key], 1); await updateSecurityConfig(c, i.guildId, { antiNuke: { thresholds: t } }); return panel(i, c, 'nuke'); } }, + { name: 'security_automod_badwords_modal', execute: async (i, c) => { if (!authorized(i)) return reject(i); const words = lines(i.fields.getTextInputValue('words')).slice(0, 100); await updateSecurityConfig(c, i.guildId, { autoMod: { badWords: { enabled: words.length > 0, words } } }); return panel(i, c, 'automod'); } }, + { name: 'security_whitelist_users_modal', execute: async (i, c) => { if (!authorized(i)) return reject(i); await updateSecurityConfig(c, i.guildId, { whitelist: { users: lines(i.fields.getTextInputValue('users')).slice(0, 100) } }); return panel(i, c, 'whitelist'); } }, + { name: 'security_whitelist_roles_modal', execute: async (i, c) => { if (!authorized(i)) return reject(i); await updateSecurityConfig(c, i.guildId, { whitelist: { roles: lines(i.fields.getTextInputValue('roles')).slice(0, 100) } }); return panel(i, c, 'whitelist'); } }, + { name: 'security_whitelist_bots_modal', execute: async (i, c) => { if (!authorized(i)) return reject(i); await updateSecurityConfig(c, i.guildId, { whitelist: { bots: lines(i.fields.getTextInputValue('bots')).slice(0, 100) } }); return panel(i, c, 'whitelist'); } }, + { name: 'security_logs_channel_modal', execute: async (i, c) => { if (!authorized(i)) return reject(i); await updateSecurityConfig(c, i.guildId, { logChannelId: i.fields.getTextInputValue('channel').trim() || null }); return panel(i, c, 'logs'); } }, + { name: 'security_logs_ignored_modal', execute: async (i, c) => { if (!authorized(i)) return reject(i); await updateSecurityConfig(c, i.guildId, { ignoredChannels: lines(i.fields.getTextInputValue('channels')).slice(0, 100) }); return panel(i, c, 'logs'); } }, +]; \ No newline at end of file From 0d9b5fbf2696d65097cd28cf0003b00ac70d167a Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 12:27:56 +0300 Subject: [PATCH 085/219] Add hierarchical AutoMod and combined strike/warning controls --- src/handlers/securityAdvancedHandlers.js | 199 +++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 src/handlers/securityAdvancedHandlers.js diff --git a/src/handlers/securityAdvancedHandlers.js b/src/handlers/securityAdvancedHandlers.js new file mode 100644 index 0000000000..d8872b97b4 --- /dev/null +++ b/src/handlers/securityAdvancedHandlers.js @@ -0,0 +1,199 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + MessageFlags, +} from 'discord.js'; +import { + getSecurityConfig, + updateSecurityConfig, + getStrikes, + clearStrikes, + sendSecurityLog, +} from '../services/security/securityService.js'; +import { WarningService } from '../services/moderation/warningService.js'; + +const RULES = { + spam: { label: '💬 Spam', color: 0xed4245 }, + duplicate: { label: '🔁 Duplicate', color: 0xf47b67 }, + mentions: { label: '📢 Mentions', color: 0xfee75c }, + invites: { label: '🔗 Invites', color: 0x5865f2 }, + links: { label: '🌐 Links', color: 0x5865f2 }, + caps: { label: '🔠 Caps', color: 0x57f287 }, + badWords: { label: '🚫 Bad Words', color: 0xed4245 }, +}; +const ACTIONS = ['delete', 'warn', 'timeout', 'kick', 'ban']; + +function allowed(i) { return i.customId.split(':').at(-1) === i.user.id; } +function reject(i) { return i.reply({ content: 'This security dashboard belongs to another moderator.', flags: MessageFlags.Ephemeral }); } +function btn(id, label, style = ButtonStyle.Secondary) { return new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); } +function next(value, values = ACTIONS) { const index = values.indexOf(value); return values[(index + 1) % values.length]; } +function clamp(value, min, max) { return Math.min(max, Math.max(min, Number(value) || min)); } + +function ruleSummary(config, key) { + const r = config.autoMod[key] || {}; + const common = `**Status:** ${r.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Punishment:** \`${r.punishment || 'delete'}\``; + if (key === 'spam') return `${common}\n**Limit:** ${r.maxMessages ?? 6} messages\n**Window:** ${Math.round((r.windowMs || 5000) / 1000)} seconds`; + if (key === 'duplicate') return `${common}\n**Repeats:** ${r.maxRepeats ?? 3}\n**Window:** ${Math.round((r.windowMs || 10000) / 1000)} seconds`; + if (key === 'mentions') return `${common}\n**Maximum mentions:** ${r.max ?? 6}`; + if (key === 'caps') return `${common}\n**Uppercase ratio:** ${Math.round((r.ratio ?? 0.8) * 100)}%\n**Minimum length:** ${r.minLength ?? 12}`; + if (key === 'badWords') return `${common}\n**Blocked words:** ${r.words?.length || 0}`; + return common; +} + +function ruleEmbed(guild, config, key) { + const meta = RULES[key]; + return new EmbedBuilder() + .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) + .setTitle(`${meta.label} • AutoMod`) + .setDescription(`Configure **${meta.label.replace(/^\S+\s/, '')}** without leaving this dashboard.\n\n${ruleSummary(config, key)}\n\n**Escalation:** violations add a Strike, then the configured Strike escalation can override the rule punishment.`) + .setColor(meta.color) + .setFooter({ text: 'Infinity System • AutoMod • Changes save automatically' }) + .setTimestamp(); +} + +function ruleControls(userId, config, key) { + const r = config.autoMod[key]; + const id = name => `${name}:${key}:${userId}`; + const rows = [ + new ActionRowBuilder().addComponents( + btn(id('security_automod_rule_back'), '← AutoMod'), + btn(id('security_automod_rule_toggle'), r.enabled ? '🟢 Disable' : '🔴 Enable', r.enabled ? ButtonStyle.Success : ButtonStyle.Danger), + btn(id('security_automod_rule_punishment'), `⚖️ ${r.punishment || 'delete'}`, ButtonStyle.Primary), + ), + ]; + + if (key === 'spam') rows.push(new ActionRowBuilder().addComponents( + btn(id('security_automod_rule_down'), '− Messages'), btn(id('security_automod_rule_up'), '+ Messages'), + btn(id('security_automod_rule_window'), `Window ${Math.round((r.windowMs || 5000) / 1000)}s`), + )); + else if (key === 'duplicate') rows.push(new ActionRowBuilder().addComponents( + btn(id('security_automod_rule_down'), '− Repeats'), btn(id('security_automod_rule_up'), '+ Repeats'), + btn(id('security_automod_rule_window'), `Window ${Math.round((r.windowMs || 10000) / 1000)}s`), + )); + else if (key === 'mentions') rows.push(new ActionRowBuilder().addComponents( + btn(id('security_automod_rule_down'), '− Mentions'), btn(id('security_automod_rule_up'), '+ Mentions'), + )); + else if (key === 'caps') rows.push(new ActionRowBuilder().addComponents( + btn(id('security_automod_rule_down'), '− Ratio'), btn(id('security_automod_rule_up'), '+ Ratio'), btn(id('security_automod_rule_min'), `Min length ${r.minLength ?? 12}`), + )); + else if (key === 'badWords') rows.push(new ActionRowBuilder().addComponents( + btn(id('security_automod_rule_words'), '✏️ Manage Words', ButtonStyle.Primary), + )); + + return rows; +} + +function autoModEmbed(guild, config) { + const lines = Object.entries(RULES).map(([key, meta]) => { + const r = config.autoMod[key] || {}; + return `${meta.label} — ${r.enabled ? '🟢' : '🔴'} • punishment: **${r.punishment || 'delete'}**`; + }); + return new EmbedBuilder() + .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) + .setTitle('🤖 AutoMod Rules') + .setDescription(`**AutoMod:** ${config.autoMod.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n\nChoose a rule to configure it. Each rule has its own enable switch, thresholds and punishment.\n\n${lines.join('\n')}`) + .setColor(0x5865f2) + .setFooter({ text: 'Infinity System • AutoMod • Select a rule to edit it' }) + .setTimestamp(); +} + +function autoModControls(userId, config) { + const keys = Object.keys(RULES); + const rows = [new ActionRowBuilder().addComponents( + btn(`security_back:${userId}`, '← Back'), + btn(`security_automod_toggle:${userId}`, config.autoMod.enabled ? '🟢 Disable AutoMod' : '🔴 Enable AutoMod', config.autoMod.enabled ? ButtonStyle.Success : ButtonStyle.Danger), + )]; + for (let i = 0; i < keys.length; i += 3) { + rows.push(new ActionRowBuilder().addComponents(...keys.slice(i, i + 3).map(key => btn(`security_automod_rule:${key}:${userId}`, RULES[key].label, config.autoMod[key]?.enabled ? ButtonStyle.Primary : ButtonStyle.Secondary)))); + } + return rows.slice(0, 5); +} + +async function renderAutoMod(i, client) { + const config = await getSecurityConfig(client, i.guildId); + return i.update({ embeds: [autoModEmbed(i.guild, config)], components: autoModControls(i.user.id, config) }); +} + +async function renderRule(i, client, key) { + const config = await getSecurityConfig(client, i.guildId); + return i.update({ embeds: [ruleEmbed(i.guild, config, key)], components: ruleControls(i.user.id, config, key) }); +} + +async function getCombinedEntries(client, guild) { + const config = await getSecurityConfig(client, guild.id); + const warnings = await WarningService.getGuildWarnings(client, guild.id, { limit: 1000 }).catch(() => []); + const warningMap = new Map(); + for (const warning of warnings) warningMap.set(warning.userId, (warningMap.get(warning.userId) || 0) + 1); + const members = await guild.members.fetch().catch(() => guild.members.cache); + const entries = []; + for (const member of members.values()) { + if (member.user?.bot) continue; + const strike = await getStrikes(client, guild.id, member.id).catch(() => ({ count: 0, updatedAt: 0 })); + const expired = config.strikeDecayMs && strike.updatedAt && Date.now() - strike.updatedAt > Number(config.strikeDecayMs); + const strikes = expired ? 0 : Number(strike.count || 0); + const warningCount = warningMap.get(member.id) || 0; + if (!strikes && !warningCount) continue; + entries.push({ userId: member.id, strikes, warnings: warningCount, updatedAt: strike.updatedAt || 0 }); + } + return entries.sort((a, b) => (b.strikes + b.warnings) - (a.strikes + a.warnings) || b.strikes - a.strikes).slice(0, 5); +} + +function combinedEmbed(guild, entries) { + const text = entries.length ? entries.map((e, i) => `${i + 1}. <@${e.userId}> — **${e.strikes}** Strikes • **${e.warnings}** Warnings`).join('\n') : '✅ No active Strikes or Warnings.'; + return new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }).setTitle('🏆 Strikes & Warnings').setDescription(`Top members by active security history.\n\n${text}\n\nSelect a member below to view their history and manage it.`).setColor(0xfee75c).setFooter({ text: 'Resetting/clearing records does not delete Security Logs.' }).setTimestamp(); +} + +function combinedControls(userId, entries) { + const rows = []; + if (entries.length) rows.push(new ActionRowBuilder().addComponents(...entries.map((e, i) => btn(`security_member_manage:${e.userId}:${userId}`, `Manage #${i + 1}`, ButtonStyle.Primary)))); + rows.push(new ActionRowBuilder().addComponents(btn(`security_back:${userId}`, '← Back'), btn(`security_strikes_refresh:${userId}`, '🔄 Refresh', ButtonStyle.Success))); + return rows; +} + +async function combinedBoard(i, client) { + const entries = await getCombinedEntries(client, i.guild); + return i.update({ embeds: [combinedEmbed(i.guild, entries)], components: combinedControls(i.user.id, entries) }); +} + +async function memberPage(i, client, userId) { + const member = await i.guild.members.fetch(userId).catch(() => null); + const strike = await getStrikes(client, i.guildId, userId).catch(() => ({ count: 0, updatedAt: 0, lastReason: '' })); + const warnings = await WarningService.getWarnings(i.guildId, userId).catch(() => []); + const warningText = warnings.length ? warnings.slice(-5).reverse().map(w => `• ${new Date(w.timestamp || Date.now()).toLocaleString()} — ${String(w.reason || 'No reason').slice(0, 120)}`).join('\n') : 'No active warnings.'; + const embed = new EmbedBuilder().setTitle(`👤 Security History • ${member?.user?.tag || userId}`).setDescription(`**Strikes:** ${strike.count || 0}\n**Warnings:** ${warnings.length}\n**Last Strike Reason:** ${strike.lastReason || '—'}\n\n**Recent Warnings**\n${warningText}`).setColor(0xfee75c).setFooter({ text: 'All changes happen inside this message.' }).setTimestamp(); + return i.update({ embeds: [embed], components: [new ActionRowBuilder().addComponents( + btn(`security_member_reset:${userId}:${i.user.id}`, '🧹 Reset Strikes', ButtonStyle.Danger), + btn(`security_member_clearwarnings:${userId}:${i.user.id}`, '🗑️ Clear Warnings', ButtonStyle.Danger), + btn(`security_member_back:${i.user.id}`, '← Back'), + )] }); +} + +const handlers = []; +handlers.push({ name: 'security_panel_automod', execute: async (i, c) => allowed(i) ? renderAutoMod(i, c) : reject(i) }); +handlers.push({ name: 'security_automod_toggle', execute: async (i, c) => { if (!allowed(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { enabled: !x.autoMod.enabled } }); return renderAutoMod(i, c); } }); +handlers.push({ name: 'security_automod_rule', execute: async (i, c) => { if (!allowed(i)) return reject(i); const parts = i.customId.split(':'); return renderRule(i, c, parts.at(-2)); } }); +handlers.push({ name: 'security_automod_rule_back', execute: async (i, c) => { if (!allowed(i)) return reject(i); return renderAutoMod(i, c); } }); +handlers.push({ name: 'security_automod_rule_toggle', execute: async (i, c) => { if (!allowed(i)) return reject(i); const parts = i.customId.split(':'); const key = parts.at(-2); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { enabled: !x.autoMod[key].enabled } } }); return renderRule(i, c, key); } }); +handlers.push({ name: 'security_automod_rule_punishment', execute: async (i, c) => { if (!allowed(i)) return reject(i); const parts = i.customId.split(':'); const key = parts.at(-2); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { punishment: next(x.autoMod[key].punishment || 'delete') } } }); return renderRule(i, c, key); } }); +handlers.push({ name: 'security_automod_rule_down', execute: async (i, c) => { if (!allowed(i)) return reject(i); const parts = i.customId.split(':'); const key = parts.at(-2); const x = await getSecurityConfig(c, i.guildId); const r = x.autoMod[key]; const field = key === 'spam' ? 'maxMessages' : key === 'duplicate' ? 'maxRepeats' : key === 'mentions' ? 'max' : 'ratio'; const delta = field === 'ratio' ? -0.05 : -1; const min = field === 'ratio' ? 0.5 : key === 'mentions' ? 1 : 2; await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { [field]: clamp((r[field] ?? min) + delta, min, field === 'ratio' ? 1 : 30) } } }); return renderRule(i, c, key); } }); +handlers.push({ name: 'security_automod_rule_up', execute: async (i, c) => { if (!allowed(i)) return reject(i); const parts = i.customId.split(':'); const key = parts.at(-2); const x = await getSecurityConfig(c, i.guildId); const r = x.autoMod[key]; const field = key === 'spam' ? 'maxMessages' : key === 'duplicate' ? 'maxRepeats' : key === 'mentions' ? 'max' : 'ratio'; const delta = field === 'ratio' ? 0.05 : 1; const max = field === 'ratio' ? 1 : 30; await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { [field]: clamp((r[field] ?? 2) + delta, field === 'ratio' ? 0.5 : 1, max) } } }); return renderRule(i, c, key); } }); +handlers.push({ name: 'security_automod_rule_min', execute: async (i, c) => { if (!allowed(i)) return reject(i); const parts = i.customId.split(':'); const key = parts.at(-2); const x = await getSecurityConfig(c, i.guildId); const value = ((x.autoMod[key].minLength ?? 12) + 2); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { minLength: value > 40 ? 8 : value } } }); return renderRule(i, c, key); } }); +handlers.push({ name: 'security_automod_rule_window', execute: async (i, c) => { if (!allowed(i)) return reject(i); const parts = i.customId.split(':'); const key = parts.at(-2); const x = await getSecurityConfig(c, i.guildId); const current = x.autoMod[key].windowMs || 5000; const values = [3000, 5000, 10000, 15000, 30000, 60000]; await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { windowMs: next(current, values) } } }); return renderRule(i, c, key); } }); +handlers.push({ name: 'security_automod_rule_words', execute: async (i, c) => { if (!allowed(i)) return reject(i); const x = await getSecurityConfig(c, i.guildId); const input = new TextInputBuilder().setCustomId('words').setLabel('Blocked words separated by spaces').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue((x.autoMod.badWords.words || []).join(' ').slice(0, 4000)); return i.showModal(new ModalBuilder().setCustomId(`security_automod_rule_words_modal:${i.user.id}`).setTitle('AutoMod • Bad Words').addComponents(new ActionRowBuilder().addComponents(input))); } }); +handlers.push({ name: 'security_panel_strikes', execute: async (i, c) => allowed(i) ? combinedBoard(i, c) : reject(i) }); +handlers.push({ name: 'security_strikes_refresh', execute: async (i, c) => allowed(i) ? combinedBoard(i, c) : reject(i) }); +handlers.push({ name: 'security_member_manage', execute: async (i, c) => { if (!allowed(i)) return reject(i); return memberPage(i, c, i.customId.split(':').at(-2)); } }); +handlers.push({ name: 'security_member_back', execute: async (i, c) => allowed(i) ? combinedBoard(i, c) : reject(i) }); +handlers.push({ name: 'security_member_reset', execute: async (i, c) => { if (!allowed(i)) return reject(i); const userId = i.customId.split(':').at(-2); await clearStrikes(c, i.guildId, userId); await sendSecurityLog(c, i.guild, { title: 'Security Strikes Reset', description: `Strikes reset for <@${userId}> by <@${i.user.id}>.`, color: 0x57f287 }); return memberPage(i, c, userId); } }); +handlers.push({ name: 'security_member_clearwarnings', execute: async (i, c) => { if (!allowed(i)) return reject(i); const userId = i.customId.split(':').at(-2); const result = await WarningService.clearWarnings(i.guildId, userId); await sendSecurityLog(c, i.guild, { title: 'Security Warnings Cleared', description: `Cleared ${result.count} warning(s) for <@${userId}> by <@${i.user.id}>.`, color: 0x57f287 }); return memberPage(i, c, userId); } }); + +export const securityAdvancedButtonHandlers = handlers; + +export const securityAdvancedModalHandlers = [ + { name: 'security_automod_rule_words_modal', execute: async (i, c) => { if (!allowed(i)) return reject(i); const words = String(i.fields.getTextInputValue('words') || '').split(/\s+/).map(x => x.trim()).filter(Boolean).slice(0, 100); await updateSecurityConfig(c, i.guildId, { autoMod: { badWords: { enabled: words.length > 0, words } } }); return renderRule(i, c, 'badWords'); } }, +]; From 91d346acaaa932451f25a5aeae89a22363aed0fc Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 12:28:00 +0300 Subject: [PATCH 086/219] Wire hierarchical security dashboard handlers --- src/interactions/buttons/security/security.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index cf625b44f4..7494dc56c2 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -1,2 +1,6 @@ import { securityButtonHandlers } from '../../../handlers/securityHandlers.js'; -export default securityButtonHandlers; +import { securityAdvancedButtonHandlers } from '../../../handlers/securityAdvancedHandlers.js'; + +// Advanced handlers are last so they intentionally override the older flat +// AutoMod/Strike handlers while preserving every other Security control. +export default [...securityButtonHandlers, ...securityAdvancedButtonHandlers]; From 57297e5f47f1d6dbf53138b2012e10ce69f88eca Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 12:28:05 +0300 Subject: [PATCH 087/219] Wire advanced AutoMod modal handlers --- src/interactions/modals/security/security.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/interactions/modals/security/security.js b/src/interactions/modals/security/security.js index b45cf446db..be1b2ebb48 100644 --- a/src/interactions/modals/security/security.js +++ b/src/interactions/modals/security/security.js @@ -1,2 +1,4 @@ import { securityModalHandlers } from '../../../handlers/securityHandlers.js'; -export default securityModalHandlers; +import { securityAdvancedModalHandlers } from '../../../handlers/securityAdvancedHandlers.js'; + +export default [...securityModalHandlers, ...securityAdvancedModalHandlers]; From 346fe73b3b175a9d13f065fa2cdc2daeef956346 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 12:28:19 +0300 Subject: [PATCH 088/219] Make AutoMod punishments rule-specific with strike escalation --- src/services/security/autoMod.js | 72 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/services/security/autoMod.js b/src/services/security/autoMod.js index c3f666a47d..8481124411 100644 --- a/src/services/security/autoMod.js +++ b/src/services/security/autoMod.js @@ -13,7 +13,6 @@ function getState(map, key) { function detect(message, config) { const text = message.content || ''; - const lower = text.toLowerCase(); const reasons = []; const now = Date.now(); const key = `${message.guild.id}:${message.author.id}`; @@ -22,59 +21,46 @@ function detect(message, config) { const list = getState(userMessages, key).filter(t => now - t <= config.autoMod.spam.windowMs); list.push(now); userMessages.set(key, list); - if (list.length >= config.autoMod.spam.maxMessages) reasons.push('message spam'); + if (list.length >= config.autoMod.spam.maxMessages) reasons.push({ type: 'spam', reason: `message spam (${config.autoMod.spam.maxMessages}/${Math.round(config.autoMod.spam.windowMs / 1000)}s)` }); } - const normalized = text.trim().slice(0, 500); + const normalized = text.trim().slice(0, 500).toLowerCase(); if (config.autoMod.duplicate.enabled && normalized) { const list = getState(duplicates, key).filter(x => now - x.time <= config.autoMod.duplicate.windowMs); list.push({ text: normalized, time: now }); duplicates.set(key, list); - const repeats = list.filter(x => x.text === normalized).length; - if (repeats >= config.autoMod.duplicate.maxRepeats) reasons.push('duplicate messages'); + if (list.filter(x => x.text === normalized).length >= config.autoMod.duplicate.maxRepeats) reasons.push({ type: 'duplicate', reason: `duplicate spam (${config.autoMod.duplicate.maxRepeats})` }); } const mentionCount = message.mentions.users.size + message.mentions.roles.size; - if (config.autoMod.mentions.enabled && mentionCount >= config.autoMod.mentions.max) reasons.push('mention spam'); - if (config.autoMod.mentions.enabled && (message.mentions.everyone || /@(everyone|here)/i.test(text))) reasons.push('everyone/here mention'); - if (config.autoMod.invites.enabled && inviteRegex.test(text)) reasons.push('Discord invite link'); - if (config.autoMod.links.enabled && urlRegex.test(text)) reasons.push('external link'); + if (config.autoMod.mentions.enabled && mentionCount >= config.autoMod.mentions.max) reasons.push({ type: 'mentions', reason: `mention spam (${mentionCount})` }); + if (config.autoMod.mentions.enabled && (message.mentions.everyone || /@(everyone|here)/i.test(text))) reasons.push({ type: 'mentions', reason: 'everyone/here mention' }); + if (config.autoMod.invites.enabled && inviteRegex.test(text)) reasons.push({ type: 'invites', reason: 'Discord invite link' }); + if (config.autoMod.links.enabled && urlRegex.test(text)) reasons.push({ type: 'links', reason: 'external link' }); if (config.autoMod.badWords.enabled && config.autoMod.badWords.words.some(word => { const escaped = String(word).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(`(^|\\s)${escaped}(?=$|\\s|[.!?,])`, 'i').test(text); - })) reasons.push('blocked word'); - if (repeatedCharRegex.test(text)) reasons.push('character spam'); + })) reasons.push({ type: 'badWords', reason: 'blocked word' }); + if (repeatedCharRegex.test(text)) reasons.push({ type: 'spam', reason: 'character spam' }); if (config.autoMod.caps.enabled) { const letters = text.match(/[A-Za-z]/g) || []; const upper = text.match(/[A-Z]/g) || []; - if (letters.length >= config.autoMod.caps.minLength && upper.length / letters.length >= config.autoMod.caps.ratio) { - reasons.push('excessive caps'); - } + if (letters.length >= config.autoMod.caps.minLength && upper.length / letters.length >= config.autoMod.caps.ratio) reasons.push({ type: 'caps', reason: 'excessive caps' }); } - return [...new Set(reasons)]; + const seen = new Set(); + return reasons.filter(item => !seen.has(item.type) && seen.add(item.type)); } -async function executeAction(message, reason, config) { +async function executeAction(message, action, duration, reason, strike) { const deleted = await message.delete().then(() => true).catch(() => false); - const strike = await addStrike(message.client, message.guild.id, message.author.id, reason); - const level = Math.min(strike.count, config.escalation.length); - const escalation = config.escalation[level - 1]; - let action = config.autoMod.action; - - if (escalation?.action && escalation.action !== 'warn') action = escalation.action; - if (escalation?.action === 'warn') action = 'warn'; - const member = await message.guild.members.fetch(message.author.id).catch(() => null); - if (member && !isWhitelisted(member, config)) { - if (action === 'timeout' && member.moderatable) { - await member.timeout(escalation?.durationMs || 60000, `AutoMod: ${reason}`).catch(() => {}); - } else if (action === 'kick' && member.kickable) { - await member.kick(`AutoMod: ${reason}`).catch(() => {}); - } else if (action === 'ban' && member.bannable) { - await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); - } + + if (member && !isWhitelisted(member, await getSecurityConfig(message.client, message.guild.id))) { + if (action === 'timeout' && member.moderatable) await member.timeout(Math.min(Math.max(duration || 60000, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); + else if (action === 'kick' && member.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); + else if (action === 'ban' && member.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); } if (action === 'warn' && !message.channel.isDMBased?.()) { @@ -92,7 +78,8 @@ async function executeAction(message, reason, config) { title: 'AutoMod Action', description: `${message.author} triggered AutoMod.`, fields: [ - { name: 'Reason', value: reason.slice(0, 1024), inline: true }, + { name: 'Rule', value: reason.split(':')[0].slice(0, 100), inline: true }, + { name: 'Reason', value: reason.slice(0, 900), inline: true }, { name: 'Strike', value: String(strike.count), inline: true }, { name: 'Action', value: action, inline: true }, { name: 'Message Deleted', value: deleted ? 'Yes' : 'No', inline: true }, @@ -108,8 +95,21 @@ export async function handleAutoMod(message) { const member = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); if (!member || member.id === message.guild.ownerId || isWhitelisted(member, config)) return false; - const reasons = detect(message, config); - if (!reasons.length) return false; - await executeAction(message, reasons.join(', '), config); + const violations = detect(message, config); + if (!violations.length) return false; + + const primary = violations[0]; + const reason = violations.map(v => `${v.type}: ${v.reason}`).join(', '); + const strike = await addStrike(message.client, message.guild.id, message.author.id, reason); + const rule = config.autoMod[primary.type] || {}; + const baseAction = rule.punishment || config.autoMod.action || 'delete'; + + // The rule's own punishment is used first. Strike escalation takes over from + // the second strike onward, so every AutoMod rule remains independently configurable. + const escalation = strike.count > 1 ? config.escalation?.find(item => item.strike === strike.count) : null; + const action = escalation?.action || baseAction; + const duration = escalation?.durationMs || rule.timeoutMs || 60000; + + await executeAction(message, action, duration, reason, strike); return true; } From 0ed9778c7a50ec07a96b058617171a77b0e96e5b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 12:28:39 +0300 Subject: [PATCH 089/219] Support combined security warning leaderboard queries --- src/services/moderation/warningService.js | 78 ++++++----------------- 1 file changed, 18 insertions(+), 60 deletions(-) diff --git a/src/services/moderation/warningService.js b/src/services/moderation/warningService.js index b0832ecfe3..2c2cd8403f 100644 --- a/src/services/moderation/warningService.js +++ b/src/services/moderation/warningService.js @@ -5,80 +5,37 @@ import { logger } from '../../utils/logger.js'; import { createError, ErrorTypes, wrapServiceClassMethods } from '../../utils/errorHandler.js'; class WarningService { - - static async addWarning({ - guildId, - userId, - moderatorId, - reason, - timestamp = Date.now() - }) { + static async addWarning({ guildId, userId, moderatorId, reason, timestamp = Date.now() }) { const key = getWarningsKey(guildId, userId); const warnings = await getFromDb(key, []); - if (!Array.isArray(warnings)) { logger.warn(`Warnings for ${userId} in ${guildId} corrupted, resetting`); await setInDb(key, []); - throw createError( - 'Corrupted warning data', - ErrorTypes.DATABASE, - 'Warning data was corrupted and has been reset. Please try again.', - { guildId, userId, service: 'warningService', operation: 'addWarning' } - ); + throw createError('Corrupted warning data', ErrorTypes.DATABASE, 'Warning data was corrupted and has been reset. Please try again.', { guildId, userId, service: 'warningService', operation: 'addWarning' }); } - - const warning = { - id: Date.now(), - guildId, - userId, - moderatorId, - reason, - timestamp, - status: 'active' - }; - + const warning = { id: Date.now(), guildId, userId, moderatorId, reason, timestamp, status: 'active' }; warnings.push(warning); await setInDb(key, warnings); - logger.info(`Warning added: ${userId} in ${guildId} by ${moderatorId}`); - - return { - id: warning.id, - totalCount: warnings.length - }; + return { id: warning.id, totalCount: warnings.length }; } static async getWarnings(guildId, userId) { - const key = getWarningsKey(guildId, userId); - const warnings = await getFromDb(key, []); - - return Array.isArray(warnings) - ? warnings.filter(w => w && w.status !== 'deleted') - : []; + const warnings = await getFromDb(getWarningsKey(guildId, userId), []); + return Array.isArray(warnings) ? warnings.filter(w => w && w.status !== 'deleted') : []; } static async getWarningCount(guildId, userId) { - const warnings = await this.getWarnings(guildId, userId); - return warnings.length; + return (await this.getWarnings(guildId, userId)).length; } static async removeWarning(guildId, userId, warningId) { const key = getWarningsKey(guildId, userId); const warnings = await getFromDb(key, []); - const index = warnings.findIndex(w => w.id === warningId); - if (index === -1) { - throw createError( - 'Warning not found', - ErrorTypes.USER_INPUT, - 'That warning could not be found. It may have already been removed.', - { guildId, userId, warningId, service: 'warningService', operation: 'removeWarning' } - ); - } - + if (index === -1) throw createError('Warning not found', ErrorTypes.USER_INPUT, 'That warning could not be found. It may have already been removed.', { guildId, userId, warningId, service: 'warningService', operation: 'removeWarning' }); warnings[index].status = 'deleted'; await setInDb(key, warnings); - logger.info(`Warning removed: ${warningId} for ${userId} in ${guildId}`); return { removed: true }; } @@ -86,25 +43,28 @@ class WarningService { static async clearWarnings(guildId, userId) { const key = getWarningsKey(guildId, userId); const warnings = await getFromDb(key, []); - const count = warnings.length; - + const count = Array.isArray(warnings) ? warnings.length : 0; await setInDb(key, []); - logger.info(`Warnings cleared for ${userId} in ${guildId} (${count} removed)`); return { count }; } - static async getGuildWarnings(guildId, filters = {}) { - const { moderatorId, limit = 100 } = filters; - const prefix = getWarningsPrefix(guildId); + static async getGuildWarnings(guildId, filters = {}, legacyFilters = undefined) { + // Supports both getGuildWarnings(guildId, filters) and the accidental + // getGuildWarnings(client, guildId, filters) call used by older integrations. + if (typeof guildId === 'object' && typeof filters === 'string') { + guildId = filters; + filters = legacyFilters || {}; + } + const { moderatorId, limit = 100 } = filters || {}; + const prefix = getWarningsPrefix(guildId); const keys = await db.list(prefix); const allWarnings = []; for (const key of Array.isArray(keys) ? keys : []) { const warnings = await getFromDb(key, []); if (!Array.isArray(warnings)) continue; - for (const warning of warnings) { if (!warning || warning.status === 'deleted') continue; if (moderatorId && warning.moderatorId !== moderatorId) continue; @@ -113,12 +73,10 @@ class WarningService { } allWarnings.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); - logger.debug(`Fetched guild warnings for ${guildId} with ${allWarnings.length} total`); return allWarnings.slice(0, limit); } } wrapServiceClassMethods(WarningService); - export { WarningService }; From dfd1e5dd8616c968006940964ba0b1a754686b2f Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 14:53:03 +0300 Subject: [PATCH 090/219] Add unified hierarchical Security dashboard controls --- src/handlers/securityDashboardHandlers.js | 149 ++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/handlers/securityDashboardHandlers.js diff --git a/src/handlers/securityDashboardHandlers.js b/src/handlers/securityDashboardHandlers.js new file mode 100644 index 0000000000..ec1527a100 --- /dev/null +++ b/src/handlers/securityDashboardHandlers.js @@ -0,0 +1,149 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; +import { getSecurityConfig, updateSecurityConfig, getStrikes, clearStrikes, sendSecurityLog } from '../services/security/securityService.js'; +import { WarningService } from '../services/moderation/warningService.js'; + +const ACTIONS = ['delete', 'warn', 'timeout', 'kick', 'ban']; +const NUKE_ACTIONS = ['strip', 'kick', 'ban']; +const RAID_ACTIONS = ['timeout', 'kick', 'ban']; +const NUKE_RULES = { + channelDelete: 'Channel Delete', channelCreate: 'Channel Create', roleDelete: 'Role Delete', roleCreate: 'Role Create', + roleUpdate: 'Role Update', webhookUpdate: 'Webhook Update', webhookDelete: 'Webhook Delete', ban: 'Ban', kick: 'Kick', botAdd: 'Bot Add', +}; +const AUTO_RULES = { spam: 'Spam', duplicate: 'Duplicate', mentions: 'Mentions', invites: 'Invites', links: 'Links', caps: 'Caps', badWords: 'Bad Words' }; + +const ok = i => i.customId.split(':').at(-1) === i.user.id; +const deny = i => i.reply({ content: 'This security dashboard belongs to another moderator.', flags: MessageFlags.Ephemeral }); +const B = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); +const cycle = (v, list) => list[(list.indexOf(v) + 1 + list.length) % list.length]; +const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); +const modalField = (id, label, value = '', style = TextInputStyle.Short) => new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false).setValue(String(value).slice(0, 4000))); + +function embed(title, description, color = 0x5865f2, guild) { + return new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }).setTitle(title).setDescription(description).setColor(color).setFooter({ text: 'Infinity System • Changes save automatically' }).setTimestamp(); +} + +async function dashboard(i, c) { + const config = await getSecurityConfig(c, i.guildId); + return i.update({ embeds: [embed('🛡️ Server Protection', `**${i.guild.name}**\n\nChoose a security system to configure.\n\n🛡️ Anti-Nuke — ${config.antiNuke.enabled ? '🟢' : '🔴'}\n🚨 Anti-Raid — ${config.antiRaid.enabled ? '🟢' : '🔴'}\n🤖 AutoMod — ${config.autoMod.enabled ? '🟢' : '🔴'}\n⚖️ Punishments — ${config.escalation.length} escalation levels\n🏆 Strikes & Warnings — management enabled\n👤 Whitelist — ${(config.whitelist.users.length + config.whitelist.roles.length + config.whitelist.bots.length)} entries\n📋 Logs — ${config.logChannelId ? `<#${config.logChannelId}>` : 'not configured'}`, 0x57f287, i.guild)], components: [row(B(`security_panel_nuke2:${i.user.id}`, '🛡️ Anti-Nuke', ButtonStyle.Danger), B(`security_panel_raid2:${i.user.id}`, '🚨 Anti-Raid', ButtonStyle.Primary), B(`security_panel_automod2:${i.user.id}`, '🤖 AutoMod', ButtonStyle.Primary), B(`security_panel_punishments2:${i.user.id}`, '⚖️ Punishments', ButtonStyle.Primary)), row(B(`security_panel_strikes2:${i.user.id}`, '🏆 Strikes', ButtonStyle.Danger), B(`security_panel_whitelist2:${i.user.id}`, '👤 Whitelist'), B(`security_panel_logs2:${i.user.id}`, '📋 Logs'), B(`security_panel_settings2:${i.user.id}`, '⚙️ Settings'), B(`security_refresh:${i.user.id}`, '🔄 Refresh', ButtonStyle.Success)))] }); +} + +async function nuke(i, c) { + const x = await getSecurityConfig(c, i.guildId); + const t = x.antiNuke.thresholds; + const lines = Object.entries(NUKE_RULES).map(([k, n]) => `**${n}** — threshold \`${t[k] ?? 1}\` • punishment **${x.antiNuke.punishments[k] || x.antiNuke.action}**`).join('\n'); + return i.update({ embeds: [embed('🛡️ Anti-Nuke', `**Status:** ${x.antiNuke.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Window:** ${x.antiNuke.windowMs / 1000}s\n**Lockdown:** ${x.antiNuke.lockdown ? '🟢 ON' : '🔴 OFF'}\n\n${lines}`, 0xed4245, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`nuke_toggle2:${i.user.id}`, x.antiNuke.enabled ? 'Disable' : 'Enable', x.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), B(`nuke_lock2:${i.user.id}`, `Lockdown: ${x.antiNuke.lockdown ? 'ON' : 'OFF'}`), B(`nuke_window2:${i.user.id}`, `Window: ${x.antiNuke.windowMs / 1000}s`)), row(B(`nuke_thresholds2:${i.user.id}`, '✏️ Thresholds', ButtonStyle.Primary), B(`nuke_rules2:${i.user.id}`, '⚖️ Rule Punishments', ButtonStyle.Primary))] }); +} + +async function nukeRules(i, c) { + const x = await getSecurityConfig(c, i.guildId); + const keys = Object.keys(NUKE_RULES); + const rows = [row(B(`nuke_rules_back2:${i.user.id}`, '← Anti-Nuke'))]; + for (let n = 0; n < keys.length; n += 4) rows.push(row(...keys.slice(n, n + 4).map(k => B(`nuke_rule:${k}:${i.user.id}`, `${NUKE_RULES[k]}: ${x.antiNuke.punishments[k] || 'strip'}`, ButtonStyle.Primary)))); + return i.update({ embeds: [embed('⚖️ Anti-Nuke Punishments', 'Each Anti-Nuke event has its own punishment. Click an event to cycle its action.', 0xed4245, i.guild)], components: rows.slice(0, 5) }); +} + +async function raid(i, c) { + const x = await getSecurityConfig(c, i.guildId); + return i.update({ embeds: [embed('🚨 Anti-Raid', `**Status:** ${x.antiRaid.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Join burst:** ${x.antiRaid.joins} joins / ${x.antiRaid.windowMs / 1000}s\n**Minimum account age:** ${Math.round(x.antiRaid.minAccountAgeMs / 3600000)}h\n**Punishment:** **${x.antiRaid.punishment}**\n**Lockdown:** ${x.antiRaid.lockdown ? '🟢 ON' : '🔴 OFF'}`, 0xf47b67, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`raid_toggle2:${i.user.id}`, x.antiRaid.enabled ? 'Disable' : 'Enable', x.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), B(`raid_punishment2:${i.user.id}`, `Punishment: ${x.antiRaid.punishment}`, ButtonStyle.Primary)), row(B(`raid_joins2:${i.user.id}`, `Joins: ${x.antiRaid.joins}`), B(`raid_window2:${i.user.id}`, `Window: ${x.antiRaid.windowMs / 1000}s`), B(`raid_age2:${i.user.id}`, `Age: ${Math.round(x.antiRaid.minAccountAgeMs / 3600000)}h`), B(`raid_lock2:${i.user.id}`, `Lockdown: ${x.antiRaid.lockdown ? 'ON' : 'OFF'}`))] }); +} + +async function punishments(i, c) { + const x = await getSecurityConfig(c, i.guildId); + const escalation = (x.escalation || []).map(e => `Strike **${e.strike}** → **${e.action}**${e.durationMs ? ` (${Math.round(e.durationMs / 60000)}m)` : ''}`).join('\n'); + return i.update({ embeds: [embed('⚖️ Punishments & Escalation', `Every security rule has an independent punishment.\n\n**Anti-Raid:** ${x.antiRaid.punishment}\n**Strike decay:** ${Math.round(x.strikeDecayMs / 3600000)}h\n\n**Escalation**\n${escalation}`, 0xfee75c, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`punishment_decay2:${i.user.id}`, '⏱️ Decay', ButtonStyle.Primary), B(`punishment_rules2:${i.user.id}`, '⚖️ Rule Punishments', ButtonStyle.Primary)), row(B(`punishment_escalation2:${i.user.id}`, '📈 Edit Escalation', ButtonStyle.Primary))] }); +} + +async function punishmentRules(i, c) { + const x = await getSecurityConfig(c, i.guildId); + const rows = [row(B(`punishments_back2:${i.user.id}`, '← Punishments'))]; + rows.push(row(B(`pun_raid2:${i.user.id}`, `Raid: ${x.antiRaid.punishment}`, ButtonStyle.Primary), B(`pun_auto2:${i.user.id}`, '🤖 AutoMod Rules', ButtonStyle.Primary))); + const keys = Object.keys(NUKE_RULES); + for (let n = 0; n < keys.length; n += 4) rows.push(row(...keys.slice(n, n + 4).map(k => B(`nuke_rule:${k}:${i.user.id}`, `${NUKE_RULES[k]}: ${x.antiNuke.punishments[k]}`, ButtonStyle.Secondary)))); + return i.update({ embeds: [embed('⚖️ Rule Punishments', 'Choose the protection system, then change the punishment for the exact rule.', 0xfee75c, i.guild)], components: rows.slice(0, 5) }); +} + +async function whitelist(i, c) { + const x = await getSecurityConfig(c, i.guildId); + return i.update({ embeds: [embed('👤 Whitelist', `Trusted entries bypass applicable security enforcement.\n\n**Users:** ${x.whitelist.users.length}\n**Roles:** ${x.whitelist.roles.length}\n**Bots:** ${x.whitelist.bots.length}`, 0x57f287, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`wl_users2:${i.user.id}`, '👤 Users', ButtonStyle.Primary), B(`wl_roles2:${i.user.id}`, '🎭 Roles', ButtonStyle.Primary), B(`wl_bots2:${i.user.id}`, '🤖 Bots', ButtonStyle.Primary))] }); +} + +async function logs(i, c) { + const x = await getSecurityConfig(c, i.guildId); + return i.update({ embeds: [embed('📋 Security Logs', `**Log channel:** ${x.logChannelId ? `<#${x.logChannelId}>` : 'Not configured'}\n**Ignored channels:** ${x.ignoredChannels.length}\n\nAll protection events can be sent to the configured security log channel.`, 0x5865f2, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`logs_channel2:${i.user.id}`, '📋 Set Log Channel', ButtonStyle.Primary), B(`logs_ignored2:${i.user.id}`, '🚫 Ignored Channels', ButtonStyle.Secondary))] }); +} + +async function settings(i, c) { + const x = await getSecurityConfig(c, i.guildId); + return i.update({ embeds: [embed('⚙️ Security Settings', `**Global protection:** ${x.enabled ? '🟢 ON' : '🔴 OFF'}\n**Strike decay:** ${Math.round(x.strikeDecayMs / 3600000)}h\n**Ignored channels:** ${x.ignoredChannels.length}\n\nThese settings affect the whole security system.`, 0x57f287, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`settings_toggle2:${i.user.id}`, x.enabled ? '🔴 Disable Protection' : '🟢 Enable Protection'), B(`settings_decay2:${i.user.id}`, '⏱️ Strike Decay', ButtonStyle.Primary), B(`settings_ignored2:${i.user.id}`, '🚫 Ignored Channels', ButtonStyle.Primary))] }); +} + +async function strikes(i, c) { + const members = await i.guild.members.fetch().catch(() => i.guild.members.cache); + const entries = []; + for (const m of members.values()) { if (m.user.bot) continue; const s = await getStrikes(c, i.guildId, m.id).catch(() => ({ count: 0 })); if (s.count) entries.push({ id: m.id, count: s.count }); } + entries.sort((a, b) => b.count - a.count); + const text = entries.slice(0, 10).map((e, n) => `${n + 1}. <@${e.id}> — **${e.count}** strikes`).join('\n') || 'No active strikes.'; + const rows = [row(B(`security_back2:${i.user.id}`, '← Back'), B(`strikes_refresh2:${i.user.id}`, '🔄 Refresh', ButtonStyle.Success))]; + if (entries.length) rows.push(row(...entries.slice(0, 4).map(e => B(`strike_manage2:${e.id}:${i.user.id}`, `Manage ${e.count}`, ButtonStyle.Primary)))); + return i.update({ embeds: [embed('🏆 Strikes & Warnings', `Top members by active security strikes.\n\n${text}\n\nYou can reset strikes or clear warnings from the member page.`, 0xfee75c, i.guild)], components: rows }); +} + +async function member(i, c, userId) { + const s = await getStrikes(c, i.guildId, userId).catch(() => ({ count: 0, lastReason: '' })); + const w = await WarningService.getWarnings(i.guildId, userId).catch(() => []); + return i.update({ embeds: [embed(`👤 Security History`, `<@${userId}>\n\n**Strikes:** ${s.count || 0}\n**Warnings:** ${w.length}\n**Last strike:** ${s.lastReason || '—'}`, 0xfee75c, i.guild)], components: [row(B(`strike_reset2:${userId}:${i.user.id}`, '🧹 Reset Strikes', ButtonStyle.Danger), B(`warning_clear2:${userId}:${i.user.id}`, '🗑️ Clear Warnings', ButtonStyle.Danger), B(`strikes_back2:${i.user.id}`, '← Back'))] }); +} + +const panelMap = { security_panel_nuke2: nuke, security_panel_raid2: raid, security_panel_punishments2: punishments, security_panel_whitelist2: whitelist, security_panel_logs2: logs, security_panel_settings2: settings, security_panel_strikes2: strikes }; + +export const securityDashboardButtonHandlers = [ + ...Object.entries(panelMap).map(([name, fn]) => ({ name, execute: async (i, c) => ok(i) ? fn(i, c) : deny(i) })), + { name: 'security_back2', execute: async (i, c) => ok(i) ? dashboard(i, c) : deny(i) }, + { name: 'security_refresh', execute: async (i, c) => ok(i) ? dashboard(i, c) : deny(i) }, + { name: 'nuke_toggle2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { enabled: !x.antiNuke.enabled } }); return nuke(i, c); } }, + { name: 'nuke_lock2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { lockdown: !x.antiNuke.lockdown } }); return nuke(i, c); } }, + { name: 'nuke_window2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { windowMs: cycle(x.antiNuke.windowMs, [5000,10000,15000,30000,60000]) } }); return nuke(i, c); } }, + { name: 'nuke_thresholds2', execute: async i => ok(i) ? i.showModal(new ModalBuilder().setCustomId(`nuke_thresholds_modal2:${i.user.id}`).setTitle('Anti-Nuke Thresholds').addComponents(modalField('channelDelete','Channel deletes','3'),modalField('channelCreate','Channel creates','5'),modalField('roleDelete','Role deletes','3'),modalField('roleCreate','Role creates','5'),modalField('botAdd','Bot additions','1'))) : deny(i) }, + { name: 'nuke_rules2', execute: async (i,c) => ok(i) ? nukeRules(i,c) : deny(i) }, + { name: 'nuke_rules_back2', execute: async (i,c) => ok(i) ? nuke(i,c) : deny(i) }, + ...Object.keys(NUKE_RULES).map(k => ({ name: 'nuke_rule', match: k, execute: async (i,c) => { if (!ok(i)) return deny(i); const x=await getSecurityConfig(c,i.guildId); await updateSecurityConfig(c,i.guildId,{antiNuke:{punishments:{[k]:cycle(x.antiNuke.punishments[k]||'strip',NUKE_ACTIONS)}}}); return nukeRules(i,c); } })), + { name: 'security_panel_raid2', execute: async (i,c)=>ok(i)?raid(i,c):deny(i) }, + { name: 'raid_toggle2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{enabled:!x.antiRaid.enabled}});return raid(i,c);} }, + { name: 'raid_punishment2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{punishment:cycle(x.antiRaid.punishment,RAID_ACTIONS)}});return raid(i,c);} }, + { name: 'raid_joins2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{joins:x.antiRaid.joins>=50?2:x.antiRaid.joins+2}});return raid(i,c);} }, + { name: 'raid_window2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{windowMs:cycle(x.antiRaid.windowMs,[5000,10000,15000,30000,60000])}});return raid(i,c);} }, + { name: 'raid_age2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{minAccountAgeMs:cycle(x.antiRaid.minAccountAgeMs,[0,3600000,21600000,86400000,604800000,2592000000])}});return raid(i,c);} }, + { name: 'raid_lock2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{lockdown:!x.antiRaid.lockdown}});return raid(i,c);} }, + { name: 'punishment_rules2', execute: async(i,c)=>ok(i)?punishmentRules(i,c):deny(i) }, + { name: 'punishments_back2', execute: async(i,c)=>ok(i)?punishments(i,c):deny(i) }, + { name: 'pun_raid2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{punishment:cycle(x.antiRaid.punishment,RAID_ACTIONS)}});return punishmentRules(i,c);} }, + { name: 'pun_auto2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);const text=Object.entries(AUTO_RULES).map(([k,n])=>`${n}: **${x.autoMod[k].punishment}**`).join('\n');return i.update({embeds:[embed('🤖 AutoMod Punishments',text,0x5865f2,i.guild)],components:[row(B(`punishments_back2:${i.user.id}`),...Object.keys(AUTO_RULES).slice(0,4).map(k=>B(`auto_pun2:${k}:${i.user.id}`,`${AUTO_RULES[k]}: ${x.autoMod[k].punishment}`,ButtonStyle.Primary)))]});}}, + { name: 'punishment_decay2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{strikeDecayMs:x.strikeDecayMs>=30*86400000?3600000:x.strikeDecayMs+3600000});return punishments(i,c);} }, + { name: 'security_panel_whitelist2', execute: async(i,c)=>ok(i)?whitelist(i,c):deny(i) }, + { name: 'security_panel_logs2', execute: async(i,c)=>ok(i)?logs(i,c):deny(i) }, + { name: 'security_panel_settings2', execute: async(i,c)=>ok(i)?settings(i,c):deny(i) }, + { name: 'security_panel_strikes2', execute: async(i,c)=>ok(i)?strikes(i,c):deny(i) }, + { name: 'strikes_refresh2', execute: async(i,c)=>ok(i)?strikes(i,c):deny(i) }, + { name: 'strike_manage2', execute: async(i,c)=>ok(i)?member(i,c,i.customId.split(':').at(-2)):deny(i) }, + { name: 'strike_reset2', execute: async(i,c)=>{if(!ok(i))return deny(i);const u=i.customId.split(':').at(-2);await clearStrikes(c,i.guildId,u);await sendSecurityLog(c,i.guild,{title:'Strikes Reset',description:`<@${u}> strikes reset by <@${i.user.id}>`,color:0x57f287});return member(i,c,u);} }, + { name: 'warning_clear2', execute: async(i,c)=>{if(!ok(i))return deny(i);const u=i.customId.split(':').at(-2);await WarningService.clearWarnings(i.guildId,u);return member(i,c,u);} }, + { name: 'strikes_back2', execute: async(i,c)=>ok(i)?strikes(i,c):deny(i) }, + { name: 'wl_users2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`wl_users_modal2:${i.user.id}`).setTitle('Whitelist Users').addComponents(modalField('value','User IDs, one per line','',TextInputStyle.Paragraph))):deny(i) }, + { name: 'wl_roles2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`wl_roles_modal2:${i.user.id}`).setTitle('Whitelist Roles').addComponents(modalField('value','Role IDs, one per line','',TextInputStyle.Paragraph))):deny(i) }, + { name: 'wl_bots2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`wl_bots_modal2:${i.user.id}`).setTitle('Whitelist Bots').addComponents(modalField('value','Bot IDs, one per line','',TextInputStyle.Paragraph))):deny(i) }, + { name: 'logs_channel2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`logs_channel_modal2:${i.user.id}`).setTitle('Security Log Channel').addComponents(modalField('value','Channel ID'))):deny(i) }, + { name: 'logs_ignored2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`logs_ignored_modal2:${i.user.id}`).setTitle('Ignored Channels').addComponents(modalField('value','Channel IDs, one per line','',TextInputStyle.Paragraph))):deny(i) }, + { name: 'settings_toggle2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{enabled:!x.enabled});return settings(i,c);} }, + { name: 'settings_decay2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{strikeDecayMs:x.strikeDecayMs>=30*86400000?3600000:x.strikeDecayMs+3600000});return settings(i,c);} }, + { name: 'settings_ignored2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`settings_ignored_modal2:${i.user.id}`).setTitle('Ignored Channels').addComponents(modalField('value','Channel IDs, one per line','',TextInputStyle.Paragraph))):deny(i) }, +]; + +export const securityDashboardModalHandlers = [ + { name:'nuke_thresholds_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);const t={...x.antiNuke.thresholds};for(const k of ['channelDelete','channelCreate','roleDelete','roleCreate','botAdd'])t[k]=Math.max(1,Number(i.fields.getTextInputValue(k))||t[k]);await updateSecurityConfig(c,i.guildId,{antiNuke:{thresholds:t}});return nuke(i,c);} }, + { name:'wl_users_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const users=String(i.fields.getTextInputValue('value')||'').split(/\s+/).filter(Boolean).slice(0,100);await updateSecurityConfig(c,i.guildId,{whitelist:{users}});return whitelist(i,c);} }, + { name:'wl_roles_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const roles=String(i.fields.getTextInputValue('value')||'').split(/\s+/).filter(Boolean).slice(0,100);await updateSecurityConfig(c,i.guildId,{whitelist:{roles}});return whitelist(i,c);} }, + { name:'wl_bots_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const bots=String(i.fields.getTextInputValue('value')||'').split(/\s+/).filter(Boolean).slice(0,100);await updateSecurityConfig(c,i.guildId,{whitelist:{bots}});return whitelist(i,c);} }, + { name:'logs_channel_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const value=String(i.fields.getTextInputValue('value')||'').trim()||null;await updateSecurityConfig(c,i.guildId,{logChannelId:value});return logs(i,c);} }, + { name:'logs_ignored_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const value=String(i.fields.getTextInputValue('value')||'').split(/\s+/).filter(Boolean).slice(0,100);await updateSecurityConfig(c,i.guildId,{ignoredChannels:value});return logs(i,c);} }, + { name:'settings_ignored_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const value=String(i.fields.getTextInputValue('value')||'').split(/\s+/).filter(Boolean).slice(0,100);await updateSecurityConfig(c,i.guildId,{ignoredChannels:value});return settings(i,c);} }, +]; From dd08ae9f324b71c1b6d7f61a5c81719215868a84 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 14:53:09 +0300 Subject: [PATCH 091/219] Enable hierarchical controls for every Security panel --- src/interactions/buttons/security/security.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index 7494dc56c2..bc6c62a0ff 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -1,6 +1,11 @@ import { securityButtonHandlers } from '../../../handlers/securityHandlers.js'; import { securityAdvancedButtonHandlers } from '../../../handlers/securityAdvancedHandlers.js'; +import { securityDashboardButtonHandlers } from '../../../handlers/securityDashboardHandlers.js'; -// Advanced handlers are last so they intentionally override the older flat -// AutoMod/Strike handlers while preserving every other Security control. -export default [...securityButtonHandlers, ...securityAdvancedButtonHandlers]; +// Order matters: the hierarchical dashboard handlers are last so their +// dedicated IDs take precedence over the older flat panel controls. +export default [ + ...securityButtonHandlers, + ...securityAdvancedButtonHandlers, + ...securityDashboardButtonHandlers, +]; From 953fe09b22a0fc4173b9f292f0f3f5a81adf5c08 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 14:53:17 +0300 Subject: [PATCH 092/219] Enable hierarchical Security modal controls --- src/interactions/modals/security/security.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/interactions/modals/security/security.js b/src/interactions/modals/security/security.js index be1b2ebb48..ea0ec8ed46 100644 --- a/src/interactions/modals/security/security.js +++ b/src/interactions/modals/security/security.js @@ -1,4 +1,9 @@ import { securityModalHandlers } from '../../../handlers/securityHandlers.js'; import { securityAdvancedModalHandlers } from '../../../handlers/securityAdvancedHandlers.js'; +import { securityDashboardModalHandlers } from '../../../handlers/securityDashboardHandlers.js'; -export default [...securityModalHandlers, ...securityAdvancedModalHandlers]; +export default [ + ...securityModalHandlers, + ...securityAdvancedModalHandlers, + ...securityDashboardModalHandlers, +]; From ce77588719edc8de95e33ff84018de10c0d5796b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 14:53:29 +0300 Subject: [PATCH 093/219] Route legacy Security panel buttons to hierarchical dashboard --- src/handlers/securityPanelOverrides.js | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/handlers/securityPanelOverrides.js diff --git a/src/handlers/securityPanelOverrides.js b/src/handlers/securityPanelOverrides.js new file mode 100644 index 0000000000..ef666d1e88 --- /dev/null +++ b/src/handlers/securityPanelOverrides.js @@ -0,0 +1,38 @@ +import { securityDashboardButtonHandlers } from './securityDashboardHandlers.js'; + +// Alias the original /security panel IDs to the new hierarchical dashboard. +// This keeps existing command messages/buttons compatible after deployment. +const names = new Set([ + 'security_panel_nuke', + 'security_panel_raid', + 'security_panel_punishments', + 'security_panel_strikes', + 'security_panel_whitelist', + 'security_panel_logs', + 'security_panel_settings', +]); + +const target = { + security_panel_nuke: 'security_panel_nuke2', + security_panel_raid: 'security_panel_raid2', + security_panel_punishments: 'security_panel_punishments2', + security_panel_strikes: 'security_panel_strikes2', + security_panel_whitelist: 'security_panel_whitelist2', + security_panel_logs: 'security_panel_logs2', + security_panel_settings: 'security_panel_settings2', +}; + +export default [...names].map(name => ({ + name, + execute: async (interaction, client) => { + const handler = securityDashboardButtonHandlers.find(h => h.name === target[name]); + if (!handler) return interaction.reply({ content: 'Security panel handler unavailable.', ephemeral: true }); + const original = interaction.customId; + interaction.customId = `${target[name]}:${interaction.user.id}`; + try { + return await handler.execute(interaction, client); + } finally { + interaction.customId = original; + } + }, +})); From 00a5a66a82c1767588a3091824e169bc11b43a70 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 14:53:34 +0300 Subject: [PATCH 094/219] Route all Security panels through hierarchical dashboard --- src/interactions/buttons/security/security.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index bc6c62a0ff..6843a54303 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -1,11 +1,11 @@ import { securityButtonHandlers } from '../../../handlers/securityHandlers.js'; import { securityAdvancedButtonHandlers } from '../../../handlers/securityAdvancedHandlers.js'; import { securityDashboardButtonHandlers } from '../../../handlers/securityDashboardHandlers.js'; +import securityPanelOverrides from '../../../handlers/securityPanelOverrides.js'; -// Order matters: the hierarchical dashboard handlers are last so their -// dedicated IDs take precedence over the older flat panel controls. export default [ ...securityButtonHandlers, ...securityAdvancedButtonHandlers, ...securityDashboardButtonHandlers, + ...securityPanelOverrides, ]; From 4cbdb354fa009351f9ffcd3975011cc7e57b731a Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 14:53:58 +0300 Subject: [PATCH 095/219] Complete per-rule punishment navigation for Security dashboard --- src/handlers/securityDashboardRuleHandlers.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/handlers/securityDashboardRuleHandlers.js diff --git a/src/handlers/securityDashboardRuleHandlers.js b/src/handlers/securityDashboardRuleHandlers.js new file mode 100644 index 0000000000..958c5afe50 --- /dev/null +++ b/src/handlers/securityDashboardRuleHandlers.js @@ -0,0 +1,25 @@ +import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; +import { ButtonStyle, ActionRowBuilder, ButtonBuilder, EmbedBuilder } from 'discord.js'; + +const NUKE = ['strip', 'kick', 'ban']; +const AUTO = ['delete', 'warn', 'timeout', 'kick', 'ban']; +const AUTO_LABELS = { spam: 'Spam', duplicate: 'Duplicate', mentions: 'Mentions', invites: 'Invites', links: 'Links', caps: 'Caps', badWords: 'Bad Words' }; +const ok = i => i.customId.split(':').at(-1) === i.user.id; +const deny = i => i.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); +const B = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); +const row = (...b) => new ActionRowBuilder().addComponents(b); +const cycle = (v, list) => list[(list.indexOf(v) + 1) % list.length]; +const autoKeys = Object.keys(AUTO_LABELS); + +async function autoPage(i, c) { + const x = await getSecurityConfig(c, i.guildId); + const rows = [row(B(`punishments_back2:${i.user.id}`, '← Punishments'))]; + for (let n = 0; n < autoKeys.length; n += 4) rows.push(row(...autoKeys.slice(n, n + 4).map(k => B(`auto_pun2:${k}:${i.user.id}`, `${AUTO_LABELS[k]}: ${x.autoMod[k].punishment}`, ButtonStyle.Primary)))); + return i.update({ embeds: [new EmbedBuilder().setTitle('🤖 AutoMod Punishments').setDescription('Every AutoMod rule has an independent punishment. Click a rule to cycle: Delete → Warn → Timeout → Kick → Ban.').setColor(0x5865f2)], components: rows }); +} + +export default [ + { name: 'nuke_rule', execute: async (i, c) => { if (!ok(i)) return deny(i); const key = i.customId.split(':').at(-2); const x = await getSecurityConfig(c, i.guildId); const current = x.antiNuke.punishments[key] || 'strip'; await updateSecurityConfig(c, i.guildId, { antiNuke: { punishments: { [key]: cycle(current, NUKE) } } }); return i.update({ content: null, embeds: [new EmbedBuilder().setTitle('⚖️ Anti-Nuke Punishment Updated').setDescription(`**${key}** punishment is now **${cycle(current, NUKE)}**.`).setColor(0xed4245)], components: [row(B(`security_back2:${i.user.id}`, '← Back'))] }); } }, + { name: 'pun_auto2', execute: async (i, c) => ok(i) ? autoPage(i, c) : deny(i) }, + { name: 'auto_pun2', execute: async (i, c) => { if (!ok(i)) return deny(i); const key = i.customId.split(':').at(-2); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { punishment: cycle(x.autoMod[key].punishment, AUTO) } } }); return autoPage(i, c); } }, +]; From 1542c71c3176899fb50683c0ec2826e00a938034 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 14:54:03 +0300 Subject: [PATCH 096/219] Finish Security rule navigation routing --- src/interactions/buttons/security/security.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index 6843a54303..c2644faf2a 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -2,10 +2,12 @@ import { securityButtonHandlers } from '../../../handlers/securityHandlers.js'; import { securityAdvancedButtonHandlers } from '../../../handlers/securityAdvancedHandlers.js'; import { securityDashboardButtonHandlers } from '../../../handlers/securityDashboardHandlers.js'; import securityPanelOverrides from '../../../handlers/securityPanelOverrides.js'; +import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRuleHandlers.js'; export default [ ...securityButtonHandlers, ...securityAdvancedButtonHandlers, ...securityDashboardButtonHandlers, + ...securityDashboardRuleHandlers, ...securityPanelOverrides, ]; From 2a38dbbd63ca7d3fdc7576fc648f2987821ad83e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:01:50 +0300 Subject: [PATCH 097/219] Add public temporary voice control panel service --- src/services/temporaryVoicePanelService.js | 217 +++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/services/temporaryVoicePanelService.js diff --git a/src/services/temporaryVoicePanelService.js b/src/services/temporaryVoicePanelService.js new file mode 100644 index 0000000000..d7c5e6007f --- /dev/null +++ b/src/services/temporaryVoicePanelService.js @@ -0,0 +1,217 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + UserSelectMenuBuilder, + PermissionFlagsBits, + ChannelType, +} from 'discord.js'; +import { getJoinToCreateConfig, getTemporaryChannelInfo } from '../utils/database.js'; +import { logger } from '../utils/logger.js'; + +const PANEL_PREFIX = 'tvp'; + +export function panelCustomId(action, channelId) { + return `${PANEL_PREFIX}:${action}:${channelId}`; +} + +export function buildTemporaryVoicePanel(channel, ownerId) { + const locked = !channel.permissionsFor(channel.guild.roles.everyone)?.has(PermissionFlagsBits.Connect); + + const embed = new EmbedBuilder() + .setColor('#5865F2') + .setTitle('🎛️ Temporary Voice Panel') + .setDescription( + `Manage **${channel.name}** from this panel.\n\n` + + `👑 **Owner:** <@${ownerId}>\n` + + `👥 **Users:** ${channel.members.size}${channel.userLimit ? `/${channel.userLimit}` : ''}\n` + + `🔐 **Privacy:** ${locked ? 'Private' : 'Public'}\n\n` + + 'Only the current room owner can use these controls.' + ) + .setFooter({ text: 'The panel is public • Controls affect this voice room only' }) + .setTimestamp(); + + const row1 = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(panelCustomId('name', channel.id)).setLabel('Name').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId(panelCustomId('limit', channel.id)).setLabel('Limit').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId(panelCustomId('privacy', channel.id)).setLabel('Privacy').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId(panelCustomId('trust', channel.id)).setLabel('Trust').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId(panelCustomId('untrust', channel.id)).setLabel('Untrust').setStyle(ButtonStyle.Secondary), + ); + + const row2 = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(panelCustomId('invite', channel.id)).setLabel('Invite').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId(panelCustomId('kick', channel.id)).setLabel('Kick').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId(panelCustomId('block', channel.id)).setLabel('Block').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId(panelCustomId('unblock', channel.id)).setLabel('Unblock').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId(panelCustomId('transfer', channel.id)).setLabel('Transfer').setStyle(ButtonStyle.Secondary), + ); + + const row3 = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(panelCustomId('delete', channel.id)).setLabel('Delete').setStyle(ButtonStyle.Danger), + ); + + return { embeds: [embed], components: [row1, row2, row3] }; +} + +export function buildUserSelect(action, channelId, placeholder) { + const select = new UserSelectMenuBuilder() + .setCustomId(panelCustomId(`select:${action}`, channelId)) + .setPlaceholder(placeholder) + .setMinValues(1) + .setMaxValues(1); + return new ActionRowBuilder().addComponents(select); +} + +export function buildNameModal(channelId, currentName) { + const input = new TextInputBuilder() + .setCustomId('name') + .setLabel('Room name') + .setStyle(TextInputStyle.Short) + .setMinLength(1) + .setMaxLength(100) + .setRequired(true) + .setValue(currentName); + return new ModalBuilder() + .setCustomId(panelCustomId('modal:name', channelId)) + .setTitle('Change Room Name') + .addComponents(new ActionRowBuilder().addComponents(input)); +} + +export function buildLimitModal(channelId, currentLimit) { + const input = new TextInputBuilder() + .setCustomId('limit') + .setLabel('User limit (0 = unlimited)') + .setStyle(TextInputStyle.Short) + .setMinLength(1) + .setMaxLength(2) + .setRequired(true) + .setValue(String(currentLimit || 0)); + return new ModalBuilder() + .setCustomId(panelCustomId('modal:limit', channelId)) + .setTitle('Change User Limit') + .addComponents(new ActionRowBuilder().addComponents(input)); +} + +export async function getTemporaryOwner(client, guildId, channelId) { + const info = await getTemporaryChannelInfo(client, guildId, channelId); + return info?.ownerId || null; +} + +export async function isTemporaryOwner(interaction, client, channelId) { + if (!interaction.guild || !interaction.member) return false; + const ownerId = await getTemporaryOwner(client, interaction.guild.id, channelId); + return ownerId === interaction.user.id; +} + +async function getConfig(client, guildId) { + return getJoinToCreateConfig(client, guildId); +} + +async function saveConfig(client, guildId, config) { + await client.db.set(`guild:${guildId}:jointocreate`, config); +} + +export async function updatePanel(client, channel) { + try { + const info = await getTemporaryChannelInfo(client, channel.guild.id, channel.id); + if (!info) return null; + + const config = await getConfig(client, channel.guild.id); + const stored = config?.temporaryChannels?.[channel.id]; + let message = null; + + if (stored?.panelMessageId) { + message = await channel.messages.fetch(stored.panelMessageId).catch(() => null); + } + + if (message) { + await message.edit(buildTemporaryVoicePanel(channel, info.ownerId)); + return message; + } + + if (!channel.isSendable?.()) return null; + message = await channel.send(buildTemporaryVoicePanel(channel, info.ownerId)); + + if (config?.temporaryChannels?.[channel.id]) { + config.temporaryChannels[channel.id].panelMessageId = message.id; + await saveConfig(client, channel.guild.id, config); + } + + return message; + } catch (error) { + logger.warn(`Failed to sync temporary voice panel for ${channel?.id}: ${error.message}`); + return null; + } +} + +export async function togglePrivacy(client, channel) { + const everyone = channel.guild.roles.everyone; + const currentlyPublic = channel.permissionsFor(everyone)?.has(PermissionFlagsBits.Connect); + await channel.permissionOverwrites.edit(everyone, { Connect: !currentlyPublic }); + return !currentlyPublic; +} + +export async function trustUser(channel, userId) { + await channel.permissionOverwrites.edit(userId, { + Connect: true, + Speak: true, + }); +} + +export async function blockUser(channel, userId) { + await channel.permissionOverwrites.edit(userId, { + Connect: false, + Speak: false, + }); +} + +export async function clearUserOverride(channel, userId) { + await channel.permissionOverwrites.delete(userId).catch(() => null); +} + +export async function transferOwnership(client, channel, newOwnerId) { + const guildId = channel.guild.id; + const config = await getConfig(client, guildId); + const info = config?.temporaryChannels?.[channel.id]; + if (!info) throw new Error('Temporary channel information was not found.'); + + info.ownerId = newOwnerId; + info.panelMessageId = info.panelMessageId || null; + await saveConfig(client, guildId, config); + + const newOwner = await channel.guild.members.fetch(newOwnerId); + const sourceTrigger = info.triggerChannelId ? channel.guild.channels.cache.get(info.triggerChannelId) : null; + const sourceName = sourceTrigger?.name || 'Room'; + const safeName = `${newOwner.displayName || newOwner.user.username}'s Room`.slice(0, 100); + await channel.setName(safeName || sourceName); + + return newOwner; +} + +export async function kickUser(channel, userId) { + const member = await channel.guild.members.fetch(userId).catch(() => null); + if (!member || member.voice.channelId !== channel.id) return false; + await member.voice.disconnect('Removed by temporary room owner'); + return true; +} + +export async function deleteTemporaryRoom(client, channel) { + const guildId = channel.guild.id; + const config = await getConfig(client, guildId); + if (config?.temporaryChannels?.[channel.id]) { + delete config.temporaryChannels[channel.id]; + await saveConfig(client, guildId, config); + } + await channel.delete('Temporary voice room deleted by owner'); +} + +export async function createRoomInvite(channel) { + return channel.createInvite({ maxAge: 0, maxUses: 0, unique: true }); +} + +export { ChannelType }; From 00ce3f0f2cb7ad556b0ed6c46cde43871746f974 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:01:57 +0300 Subject: [PATCH 098/219] Create public control panels for temporary voice rooms --- src/events/temporaryVoicePanel.js | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/events/temporaryVoicePanel.js diff --git a/src/events/temporaryVoicePanel.js b/src/events/temporaryVoicePanel.js new file mode 100644 index 0000000000..1dd6f049bc --- /dev/null +++ b/src/events/temporaryVoicePanel.js @@ -0,0 +1,40 @@ +import { getTemporaryChannelInfo } from '../utils/database.js'; +import { updatePanel } from '../services/temporaryVoicePanelService.js'; +import { logger } from '../utils/logger.js'; + +export default { + name: 'voiceStateUpdate', + once: false, + async execute(oldState, newState, client) { + try { + const guild = newState.guild || oldState.guild; + if (!guild || !client || newState.member?.user?.bot) return; + + // The main voiceStateUpdate handler creates/deletes the room. This listener + // only attaches/synchronizes the public control panel after those changes. + if (newState.channelId) { + const info = await getTemporaryChannelInfo(client, guild.id, newState.channelId); + if (info && newState.channel) { + // Give the main JTC handler a moment to finish registering the room + // before attempting to persist the panel message id. + setTimeout(() => { + updatePanel(client, newState.channel).catch((error) => + logger.debug(`Temporary voice panel sync failed: ${error.message}`) + ); + }, 250); + } + } + + if (oldState.channelId && oldState.channelId !== newState.channelId) { + const oldInfo = await getTemporaryChannelInfo(client, guild.id, oldState.channelId); + if (oldInfo && oldState.channel) { + setTimeout(() => { + updatePanel(client, oldState.channel).catch(() => null); + }, 500); + } + } + } catch (error) { + logger.debug(`Temporary voice panel event error: ${error.message}`); + } + } +}; From 38e6b66f69dad14b9438a87c92bdfd52e18561f7 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:02:10 +0300 Subject: [PATCH 099/219] Add public temporary voice room controls --- src/events/temporaryVoicePanelInteraction.js | 190 +++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/events/temporaryVoicePanelInteraction.js diff --git a/src/events/temporaryVoicePanelInteraction.js b/src/events/temporaryVoicePanelInteraction.js new file mode 100644 index 0000000000..e146d129c8 --- /dev/null +++ b/src/events/temporaryVoicePanelInteraction.js @@ -0,0 +1,190 @@ +import { Events, MessageFlags } from 'discord.js'; +import { getTemporaryChannelInfo } from '../utils/database.js'; +import { + panelCustomId, + buildUserSelect, + buildNameModal, + buildLimitModal, + isTemporaryOwner, + updatePanel, + togglePrivacy, + trustUser, + blockUser, + clearUserOverride, + transferOwnership, + kickUser, + deleteTemporaryRoom, + createRoomInvite, +} from '../services/temporaryVoicePanelService.js'; +import { logger } from '../utils/logger.js'; + +const PREFIX = 'tvp:'; + +export default { + name: Events.InteractionCreate, + once: false, + async execute(interaction, client) { + try { + const id = interaction.customId; + if (!id || !id.startsWith(PREFIX)) return; + + const parts = id.split(':'); + const action = parts[1]; + const channelId = parts[2]; + if (!channelId || !interaction.guild) return; + + const channel = interaction.guild.channels.cache.get(channelId) + || await interaction.guild.channels.fetch(channelId).catch(() => null); + if (!channel || !channel.isVoiceBased?.()) { + await safeReply(interaction, '❌ This temporary room no longer exists.'); + return; + } + + const info = await getTemporaryChannelInfo(client, interaction.guild.id, channelId); + if (!info) { + await safeReply(interaction, '❌ This is no longer an active temporary room.'); + return; + } + + if (!await isTemporaryOwner(interaction, client, channelId)) { + await safeReply(interaction, '❌ Only the current owner of this room can use the control panel.'); + return; + } + + if (interaction.isButton()) { + if (action === 'name') { + await interaction.showModal(buildNameModal(channelId, channel.name)); + return; + } + + if (action === 'limit') { + await interaction.showModal(buildLimitModal(channelId, channel.userLimit || 0)); + return; + } + + if (action === 'privacy') { + const isPrivate = await togglePrivacy(client, channel); + await updatePanel(client, channel); + await safeReply(interaction, isPrivate ? '🔒 Room is now private.' : '🔓 Room is now public.'); + return; + } + + if (action === 'invite') { + const invite = await createRoomInvite(channel); + await safeReply(interaction, `🔗 **Room invite:** ${invite.url}`); + return; + } + + const selectActions = { + trust: 'Select a member to trust.', + untrust: 'Select a member to untrust.', + kick: 'Select a member to kick from the room.', + block: 'Select a member to block.', + unblock: 'Select a member to unblock.', + transfer: 'Select the new room owner.', + }; + + if (selectActions[action]) { + await interaction.reply({ + content: `🎛️ ${selectActions[action]}`, + components: [buildUserSelect(action, channelId, selectActions[action])], + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (action === 'delete') { + await deleteTemporaryRoom(client, channel); + await safeUpdate(interaction, '🗑️ Temporary room deleted.'); + return; + } + } + + if (interaction.isUserSelectMenu() && action.startsWith('select')) { + const selectedId = interaction.values[0]; + const selectedMember = await interaction.guild.members.fetch(selectedId).catch(() => null); + if (!selectedMember) { + await safeReply(interaction, '❌ Member not found.'); + return; + } + + const selectedAction = parts[2]; + + if (selectedAction === 'trust') { + await trustUser(channel, selectedId); + await safeReply(interaction, `✅ ${selectedMember} is now trusted in this room.`); + } else if (selectedAction === 'untrust') { + await clearUserOverride(channel, selectedId); + await safeReply(interaction, `✅ ${selectedMember} is no longer trusted.`); + } else if (selectedAction === 'block') { + await blockUser(channel, selectedId); + if (selectedMember.voice.channelId === channelId) { + await selectedMember.voice.disconnect('Blocked by temporary room owner').catch(() => null); + } + await safeReply(interaction, `🚫 ${selectedMember} is blocked from this room.`); + } else if (selectedAction === 'unblock') { + await clearUserOverride(channel, selectedId); + await safeReply(interaction, `✅ ${selectedMember} is unblocked.`); + } else if (selectedAction === 'kick') { + const removed = await kickUser(channel, selectedId); + await safeReply(interaction, removed ? `👢 ${selectedMember} was removed from the room.` : '❌ That member is not currently in this room.'); + } else if (selectedAction === 'transfer') { + if (selectedId === interaction.user.id) { + await safeReply(interaction, '❌ You already own this room.'); + return; + } + await transferOwnership(client, channel, selectedId); + await updatePanel(client, channel); + await safeReply(interaction, `👑 Ownership transferred to ${selectedMember}.`); + } + return; + } + + if (interaction.isModalSubmit()) { + if (action === 'modal') { + const modalType = parts[2]; + if (modalType === 'name') { + const value = interaction.fields.getTextInputValue('name').trim(); + if (!value || value.length > 100) { + await safeReply(interaction, '❌ The room name must be between 1 and 100 characters.'); + return; + } + await channel.setName(value); + await updatePanel(client, channel); + await safeReply(interaction, '✅ Room name updated.'); + return; + } + + if (modalType === 'limit') { + const raw = interaction.fields.getTextInputValue('limit').trim(); + const limit = Number(raw); + if (!Number.isInteger(limit) || limit < 0 || limit > 99) { + await safeReply(interaction, '❌ Enter a number from **0** to **99**. 0 means unlimited.'); + return; + } + await channel.setUserLimit(limit); + await updatePanel(client, channel); + await safeReply(interaction, `✅ User limit set to **${limit === 0 ? 'Unlimited' : limit}**.`); + } + } + } + } catch (error) { + logger.error(`Temporary voice panel interaction error: ${error.message}`); + await safeReply(interaction, '❌ Something went wrong while updating the room.'); + } + } +}; + +async function safeReply(interaction, content) { + if (interaction.replied || interaction.deferred) { + return interaction.followUp({ content, flags: MessageFlags.Ephemeral }).catch(() => null); + } + return interaction.reply({ content, flags: MessageFlags.Ephemeral }).catch(() => null); +} + +async function safeUpdate(interaction, content) { + if (interaction.isButton() && !interaction.replied && !interaction.deferred) { + return interaction.update({ content, embeds: [], components: [] }).catch(() => null); + } + return safeReply(interaction, content); +} From b0a3ec3c44a3eb24d96d78806ae9fc37fe661a88 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:02:52 +0300 Subject: [PATCH 100/219] Fix temporary voice privacy state handling --- src/services/temporaryVoicePanelService.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/services/temporaryVoicePanelService.js b/src/services/temporaryVoicePanelService.js index d7c5e6007f..79a05cb32a 100644 --- a/src/services/temporaryVoicePanelService.js +++ b/src/services/temporaryVoicePanelService.js @@ -20,7 +20,7 @@ export function panelCustomId(action, channelId) { } export function buildTemporaryVoicePanel(channel, ownerId) { - const locked = !channel.permissionsFor(channel.guild.roles.everyone)?.has(PermissionFlagsBits.Connect); + const isPublic = channel.permissionsFor(channel.guild.roles.everyone)?.has(PermissionFlagsBits.Connect); const embed = new EmbedBuilder() .setColor('#5865F2') @@ -29,7 +29,7 @@ export function buildTemporaryVoicePanel(channel, ownerId) { `Manage **${channel.name}** from this panel.\n\n` + `👑 **Owner:** <@${ownerId}>\n` + `👥 **Users:** ${channel.members.size}${channel.userLimit ? `/${channel.userLimit}` : ''}\n` + - `🔐 **Privacy:** ${locked ? 'Private' : 'Public'}\n\n` + + `🔐 **Privacy:** ${isPublic ? 'Public' : 'Private'}\n\n` + 'Only the current room owner can use these controls.' ) .setFooter({ text: 'The panel is public • Controls affect this voice room only' }) @@ -185,10 +185,8 @@ export async function transferOwnership(client, channel, newOwnerId) { await saveConfig(client, guildId, config); const newOwner = await channel.guild.members.fetch(newOwnerId); - const sourceTrigger = info.triggerChannelId ? channel.guild.channels.cache.get(info.triggerChannelId) : null; - const sourceName = sourceTrigger?.name || 'Room'; const safeName = `${newOwner.displayName || newOwner.user.username}'s Room`.slice(0, 100); - await channel.setName(safeName || sourceName); + await channel.setName(safeName || 'Voice Room'); return newOwner; } From 7d30f0d9bfd0a9d3e4453f0b9c2faf1f2346cd69 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:03:04 +0300 Subject: [PATCH 101/219] Fix temporary voice panel component routing --- src/events/temporaryVoicePanelInteraction.js | 64 +++++++++----------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/src/events/temporaryVoicePanelInteraction.js b/src/events/temporaryVoicePanelInteraction.js index e146d129c8..f34835a171 100644 --- a/src/events/temporaryVoicePanelInteraction.js +++ b/src/events/temporaryVoicePanelInteraction.js @@ -1,7 +1,6 @@ import { Events, MessageFlags } from 'discord.js'; import { getTemporaryChannelInfo } from '../utils/database.js'; import { - panelCustomId, buildUserSelect, buildNameModal, buildLimitModal, @@ -30,7 +29,7 @@ export default { const parts = id.split(':'); const action = parts[1]; - const channelId = parts[2]; + const channelId = action === 'select' || action === 'modal' ? parts[3] : parts[2]; if (!channelId || !interaction.guild) return; const channel = interaction.guild.channels.cache.get(channelId) @@ -63,9 +62,9 @@ export default { } if (action === 'privacy') { - const isPrivate = await togglePrivacy(client, channel); + const isPublic = await togglePrivacy(client, channel); await updatePanel(client, channel); - await safeReply(interaction, isPrivate ? '🔒 Room is now private.' : '🔓 Room is now public.'); + await safeReply(interaction, isPublic ? '🔓 Room is now public.' : '🔒 Room is now private.'); return; } @@ -94,13 +93,14 @@ export default { } if (action === 'delete') { + await interaction.reply({ content: '🗑️ Deleting the temporary room...', flags: MessageFlags.Ephemeral }); await deleteTemporaryRoom(client, channel); - await safeUpdate(interaction, '🗑️ Temporary room deleted.'); return; } } - if (interaction.isUserSelectMenu() && action.startsWith('select')) { + if (interaction.isUserSelectMenu() && action === 'select') { + const selectedAction = parts[2]; const selectedId = interaction.values[0]; const selectedMember = await interaction.guild.members.fetch(selectedId).catch(() => null); if (!selectedMember) { @@ -108,8 +108,6 @@ export default { return; } - const selectedAction = parts[2]; - if (selectedAction === 'trust') { await trustUser(channel, selectedId); await safeReply(interaction, `✅ ${selectedMember} is now trusted in this room.`); @@ -140,32 +138,31 @@ export default { return; } - if (interaction.isModalSubmit()) { - if (action === 'modal') { - const modalType = parts[2]; - if (modalType === 'name') { - const value = interaction.fields.getTextInputValue('name').trim(); - if (!value || value.length > 100) { - await safeReply(interaction, '❌ The room name must be between 1 and 100 characters.'); - return; - } - await channel.setName(value); - await updatePanel(client, channel); - await safeReply(interaction, '✅ Room name updated.'); + if (interaction.isModalSubmit() && action === 'modal') { + const modalType = parts[2]; + + if (modalType === 'name') { + const value = interaction.fields.getTextInputValue('name').trim(); + if (!value || value.length > 100) { + await safeReply(interaction, '❌ The room name must be between 1 and 100 characters.'); return; } + await channel.setName(value); + await updatePanel(client, channel); + await safeReply(interaction, '✅ Room name updated.'); + return; + } - if (modalType === 'limit') { - const raw = interaction.fields.getTextInputValue('limit').trim(); - const limit = Number(raw); - if (!Number.isInteger(limit) || limit < 0 || limit > 99) { - await safeReply(interaction, '❌ Enter a number from **0** to **99**. 0 means unlimited.'); - return; - } - await channel.setUserLimit(limit); - await updatePanel(client, channel); - await safeReply(interaction, `✅ User limit set to **${limit === 0 ? 'Unlimited' : limit}**.`); + if (modalType === 'limit') { + const raw = interaction.fields.getTextInputValue('limit').trim(); + const limit = Number(raw); + if (!Number.isInteger(limit) || limit < 0 || limit > 99) { + await safeReply(interaction, '❌ Enter a number from **0** to **99**. 0 means unlimited.'); + return; } + await channel.setUserLimit(limit); + await updatePanel(client, channel); + await safeReply(interaction, `✅ User limit set to **${limit === 0 ? 'Unlimited' : limit}**.`); } } } catch (error) { @@ -181,10 +178,3 @@ async function safeReply(interaction, content) { } return interaction.reply({ content, flags: MessageFlags.Ephemeral }).catch(() => null); } - -async function safeUpdate(interaction, content) { - if (interaction.isButton() && !interaction.replied && !interaction.deferred) { - return interaction.update({ content, embeds: [], components: [] }).catch(() => null); - } - return safeReply(interaction, content); -} From 4a6201942bf4c73b748ae001028d03729cc4e6f1 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:03:21 +0300 Subject: [PATCH 102/219] Make temporary voice panel creation resilient to event ordering --- src/events/temporaryVoicePanel.js | 41 ++++++++++++++++--------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/events/temporaryVoicePanel.js b/src/events/temporaryVoicePanel.js index 1dd6f049bc..f10439f764 100644 --- a/src/events/temporaryVoicePanel.js +++ b/src/events/temporaryVoicePanel.js @@ -10,28 +10,29 @@ export default { const guild = newState.guild || oldState.guild; if (!guild || !client || newState.member?.user?.bot) return; - // The main voiceStateUpdate handler creates/deletes the room. This listener - // only attaches/synchronizes the public control panel after those changes. - if (newState.channelId) { - const info = await getTemporaryChannelInfo(client, guild.id, newState.channelId); - if (info && newState.channel) { - // Give the main JTC handler a moment to finish registering the room - // before attempting to persist the panel message id. - setTimeout(() => { - updatePanel(client, newState.channel).catch((error) => - logger.debug(`Temporary voice panel sync failed: ${error.message}`) - ); - }, 250); - } + if (newState.channelId && newState.channel) { + // The main JTC listener registers the temporary channel before moving + // the member. We intentionally re-check after a short delay so listener + // ordering/race conditions cannot prevent the public panel from appearing. + setTimeout(async () => { + try { + const info = await getTemporaryChannelInfo(client, guild.id, newState.channelId); + if (info) await updatePanel(client, newState.channel); + } catch (error) { + logger.debug(`Temporary voice panel sync failed: ${error.message}`); + } + }, 400); } - if (oldState.channelId && oldState.channelId !== newState.channelId) { - const oldInfo = await getTemporaryChannelInfo(client, guild.id, oldState.channelId); - if (oldInfo && oldState.channel) { - setTimeout(() => { - updatePanel(client, oldState.channel).catch(() => null); - }, 500); - } + if (oldState.channelId && oldState.channelId !== newState.channelId && oldState.channel) { + setTimeout(async () => { + try { + const info = await getTemporaryChannelInfo(client, guild.id, oldState.channelId); + if (info) await updatePanel(client, oldState.channel); + } catch { + // The main JTC handler may already have deleted the empty room. + } + }, 600); } } catch (error) { logger.debug(`Temporary voice panel event error: ${error.message}`); From 48ff17ecc2edf5fc3aaca6bb30d1f48c7379281e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:10:24 +0300 Subject: [PATCH 103/219] Fix security AutoMod dashboard routing and rule settings --- src/handlers/securityAutoModDashboard.js | 157 +++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/handlers/securityAutoModDashboard.js diff --git a/src/handlers/securityAutoModDashboard.js b/src/handlers/securityAutoModDashboard.js new file mode 100644 index 0000000000..163c82d671 --- /dev/null +++ b/src/handlers/securityAutoModDashboard.js @@ -0,0 +1,157 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; + +const RULES = { + spam: { label: '💬 Spam', values: ['delete', 'warn', 'timeout', 'kick', 'ban'] }, + duplicate: { label: '🔁 Duplicate', values: ['delete', 'warn', 'timeout', 'kick', 'ban'] }, + mentions: { label: '📢 Mentions', values: ['delete', 'warn', 'timeout', 'kick', 'ban'] }, + invites: { label: '🔗 Invites', values: ['delete', 'warn', 'timeout', 'kick', 'ban'] }, + links: { label: '🌐 Links', values: ['delete', 'warn', 'timeout', 'kick', 'ban'] }, + caps: { label: '🔠 Caps', values: ['delete', 'warn', 'timeout', 'kick', 'ban'] }, + badWords: { label: '🚫 Bad Words', values: ['delete', 'warn', 'timeout', 'kick', 'ban'] }, +}; + +const ok = i => i.customId.split(':').at(-1) === i.user.id; +const deny = i => i.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); +const B = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); +const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); +const cycle = (value, values) => { + const index = values.indexOf(value); + return values[(index < 0 ? -1 : index) + 1 >= values.length ? 0 : index + 1]; +}; + +function embed(title, description, guild) { + return new EmbedBuilder() + .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) + .setTitle(title) + .setDescription(description) + .setColor(0x5865f2) + .setFooter({ text: 'Infinity System • Changes save automatically' }) + .setTimestamp(); +} + +async function autoModPage(i, client) { + const config = await getSecurityConfig(client, i.guildId); + const rows = [row(B(`security_back2:${i.user.id}`, '← Back'), B(`automod_global_toggle:${i.user.id}`, config.autoMod.enabled ? '🔴 Disable AutoMod' : '🟢 Enable AutoMod', config.autoMod.enabled ? ButtonStyle.Danger : ButtonStyle.Success))]; + const keys = Object.keys(RULES); + for (let n = 0; n < keys.length; n += 4) { + rows.push(row(...keys.slice(n, n + 4).map(key => { + const rule = config.autoMod[key]; + return B(`automod_rule:${key}:${i.user.id}`, `${RULES[key].label}: ${rule.enabled ? 'ON' : 'OFF'}`, rule.enabled ? ButtonStyle.Success : ButtonStyle.Secondary); + }))); + } + return i.update({ embeds: [embed('🤖 AutoMod', `**Status:** ${config.autoMod.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n\nChoose a rule to configure its detection settings and punishment.`, i.guild)], components: rows }); +} + +async function rulePage(i, client, key) { + const config = await getSecurityConfig(client, i.guildId); + const rule = config.autoMod[key]; + const meta = RULES[key]; + if (!rule || !meta) return autoModPage(i, client); + + const settings = []; + if (key === 'spam') settings.push(`**Limit:** ${rule.maxMessages} messages`, `**Window:** ${Math.round(rule.windowMs / 1000)} seconds`); + if (key === 'duplicate') settings.push(`**Repeats:** ${rule.maxRepeats}`, `**Window:** ${Math.round(rule.windowMs / 1000)} seconds`); + if (key === 'mentions') settings.push(`**Max mentions:** ${rule.max}`); + if (key === 'caps') settings.push(`**Caps ratio:** ${Math.round(rule.ratio * 100)}%`, `**Minimum length:** ${rule.minLength}`); + if (key === 'badWords') settings.push(`**Blocked words:** ${rule.words?.length || 0}`); + if (!settings.length) settings.push('This rule has no extra detection values.'); + + const rows = [ + row(B(`automod_back:${i.user.id}`, '← AutoMod'), B(`automod_toggle:${key}:${i.user.id}`, rule.enabled ? '🔴 Disable' : '🟢 Enable', rule.enabled ? ButtonStyle.Danger : ButtonStyle.Success), B(`automod_punishment:${key}:${i.user.id}`, `⚖️ ${rule.punishment}`, ButtonStyle.Primary)), + ]; + + if (key === 'spam' || key === 'duplicate') rows.push(row(B(`automod_limit:${key}:${i.user.id}`, key === 'spam' ? `Limit: ${rule.maxMessages}` : `Repeats: ${rule.maxRepeats}`, ButtonStyle.Secondary), B(`automod_window:${key}:${i.user.id}`, `Window: ${Math.round(rule.windowMs / 1000)}s`, ButtonStyle.Secondary))); + if (key === 'mentions') rows.push(row(B(`automod_limit:${key}:${i.user.id}`, `Max: ${rule.max}`, ButtonStyle.Secondary))); + if (key === 'caps') rows.push(row(B(`automod_limit:${key}:${i.user.id}`, `Ratio: ${Math.round(rule.ratio * 100)}%`, ButtonStyle.Secondary), B(`automod_min:${key}:${i.user.id}`, `Min: ${rule.minLength}`, ButtonStyle.Secondary))); + if (key === 'badWords') rows.push(row(B(`automod_words:${i.user.id}`, `Blocked words: ${rule.words?.length || 0}`, ButtonStyle.Secondary))); + + return i.update({ embeds: [embed(`${meta.label} Settings`, `**Status:** ${rule.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Punishment:** **${rule.punishment}**\n\n${settings.join('\n')}`, i.guild)], components: rows }); +} + +function parseKey(i) { return i.customId.split(':').at(-2); } + +export default [ + { + name: 'security_panel_automod2', + execute: async (i, client) => ok(i) ? autoModPage(i, client) : deny(i), + }, + { + name: 'automod_global_toggle', + execute: async (i, client) => { + if (!ok(i)) return deny(i); + const x = await getSecurityConfig(client, i.guildId); + await updateSecurityConfig(client, i.guildId, { autoMod: { enabled: !x.autoMod.enabled } }); + return autoModPage(i, client); + }, + }, + { + name: 'automod_rule', + execute: async (i, client) => ok(i) ? rulePage(i, client, parseKey(i)) : deny(i), + }, + { + name: 'automod_back', + execute: async (i, client) => ok(i) ? autoModPage(i, client) : deny(i), + }, + { + name: 'automod_toggle', + execute: async (i, client) => { + if (!ok(i)) return deny(i); + const key = parseKey(i); + const x = await getSecurityConfig(client, i.guildId); + await updateSecurityConfig(client, i.guildId, { autoMod: { [key]: { enabled: !x.autoMod[key].enabled } } }); + return rulePage(i, client, key); + }, + }, + { + name: 'automod_punishment', + execute: async (i, client) => { + if (!ok(i)) return deny(i); + const key = parseKey(i); + const x = await getSecurityConfig(client, i.guildId); + const values = RULES[key].values; + await updateSecurityConfig(client, i.guildId, { autoMod: { [key]: { punishment: cycle(x.autoMod[key].punishment, values) } } }); + return rulePage(i, client, key); + }, + }, + { + name: 'automod_limit', + execute: async (i, client) => { + if (!ok(i)) return deny(i); + const key = parseKey(i); + const x = await getSecurityConfig(client, i.guildId); + const rule = x.autoMod[key]; + const patch = key === 'spam' ? { maxMessages: rule.maxMessages >= 12 ? 3 : rule.maxMessages + 1 } : key === 'duplicate' ? { maxRepeats: rule.maxRepeats >= 8 ? 2 : rule.maxRepeats + 1 } : key === 'mentions' ? { max: rule.max >= 15 ? 3 : rule.max + 1 } : key === 'caps' ? { ratio: rule.ratio >= 0.95 ? 0.5 : Number((rule.ratio + 0.05).toFixed(2)) } : {}; + await updateSecurityConfig(client, i.guildId, { autoMod: { [key]: patch } }); + return rulePage(i, client, key); + }, + }, + { + name: 'automod_window', + execute: async (i, client) => { + if (!ok(i)) return deny(i); + const key = parseKey(i); + const x = await getSecurityConfig(client, i.guildId); + const current = Math.round(x.autoMod[key].windowMs / 1000); + const values = key === 'spam' ? [3, 5, 10, 15, 30] : [5, 10, 15, 30, 60]; + const next = cycle(current, values); + await updateSecurityConfig(client, i.guildId, { autoMod: { [key]: { windowMs: next * 1000 } } }); + return rulePage(i, client, key); + }, + }, + { + name: 'automod_min', + execute: async (i, client) => { + if (!ok(i)) return deny(i); + const key = parseKey(i); + const x = await getSecurityConfig(client, i.guildId); + const next = x.autoMod[key].minLength >= 30 ? 8 : x.autoMod[key].minLength + 2; + await updateSecurityConfig(client, i.guildId, { autoMod: { [key]: { minLength: next } } }); + return rulePage(i, client, key); + }, + }, + { + name: 'automod_words', + execute: async (i, client) => ok(i) ? rulePage(i, client, 'badWords') : deny(i), + }, +]; From df87acd6e52b52c83ca5f5668847fb6fa6529adb Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:10:31 +0300 Subject: [PATCH 104/219] Register AutoMod security dashboard handlers --- src/interactions/buttons/security/security.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index c2644faf2a..598584c2e4 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -3,6 +3,7 @@ import { securityAdvancedButtonHandlers } from '../../../handlers/securityAdvanc import { securityDashboardButtonHandlers } from '../../../handlers/securityDashboardHandlers.js'; import securityPanelOverrides from '../../../handlers/securityPanelOverrides.js'; import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRuleHandlers.js'; +import securityAutoModDashboard from '../../../handlers/securityAutoModDashboard.js'; export default [ ...securityButtonHandlers, @@ -10,4 +11,5 @@ export default [ ...securityDashboardButtonHandlers, ...securityDashboardRuleHandlers, ...securityPanelOverrides, + ...securityAutoModDashboard, ]; From 36ca51c97ff8bca88d3122a0be182b7e862bcc4e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:40:49 +0300 Subject: [PATCH 105/219] Update securityPanelOverrides.js --- src/handlers/securityPanelOverrides.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/handlers/securityPanelOverrides.js b/src/handlers/securityPanelOverrides.js index ef666d1e88..3966aadb03 100644 --- a/src/handlers/securityPanelOverrides.js +++ b/src/handlers/securityPanelOverrides.js @@ -5,6 +5,7 @@ import { securityDashboardButtonHandlers } from './securityDashboardHandlers.js' const names = new Set([ 'security_panel_nuke', 'security_panel_raid', + 'security_panel_automod', 'security_panel_punishments', 'security_panel_strikes', 'security_panel_whitelist', @@ -15,6 +16,7 @@ const names = new Set([ const target = { security_panel_nuke: 'security_panel_nuke2', security_panel_raid: 'security_panel_raid2', + security_panel_automod: 'security_panel_automod2', security_panel_punishments: 'security_panel_punishments2', security_panel_strikes: 'security_panel_strikes2', security_panel_whitelist: 'security_panel_whitelist2', @@ -24,11 +26,23 @@ const target = { export default [...names].map(name => ({ name, + execute: async (interaction, client) => { - const handler = securityDashboardButtonHandlers.find(h => h.name === target[name]); - if (!handler) return interaction.reply({ content: 'Security panel handler unavailable.', ephemeral: true }); + const handler = securityDashboardButtonHandlers.find( + h => h.name === target[name] + ); + + if (!handler) { + return interaction.reply({ + content: 'Security panel handler unavailable.', + ephemeral: true, + }); + } + const original = interaction.customId; + interaction.customId = `${target[name]}:${interaction.user.id}`; + try { return await handler.execute(interaction, client); } finally { From ae6f267c5efa49c7d3d7601a5403997b6f3dab5e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:58:07 +0300 Subject: [PATCH 106/219] Fix security command to use new dashboard handlers --- src/commands/Core/security.js | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/commands/Core/security.js b/src/commands/Core/security.js index 38fa6f6993..3002bde7e6 100644 --- a/src/commands/Core/security.js +++ b/src/commands/Core/security.js @@ -1,6 +1,5 @@ import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; -import { getSecurityConfig } from '../../services/security/securityService.js'; -import { buildSecurityDashboard } from '../../handlers/securityHandlers.js'; +import { buildSecurityDashboard } from '../../handlers/securityDashboardHandlers.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -12,27 +11,18 @@ export default { category: 'Core', async execute(interaction, config, client) { if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - await interaction.reply({ content: 'You need the Manage Server permission to use this panel.', flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: 'You need the Manage Server permission to use this panel.', + flags: MessageFlags.Ephemeral, + }); return; } const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferred) return; - const securityConfig = await getSecurityConfig(client, interaction.guildId); - const view = await buildSecurityDashboard(client, interaction.guild); - view.components = view.components.map((row) => { - const json = row.toJSON(); - json.components = json.components.map((component) => ({ - ...component, - custom_id: component.custom_id.replace(':pending', `:${interaction.user.id}`), - })); - return json; - }); + const view = await buildSecurityDashboard(client, interaction.guild, interaction.user.id); - await InteractionHelper.safeEditReply(interaction, { - embeds: [view.embeds[0]], - components: view.components, - }); + await InteractionHelper.safeEditReply(interaction, view); }, }; From 91f77fc7a98f0168d1fff7ddec01cbe7da123f4f Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:58:35 +0300 Subject: [PATCH 107/219] Export unified security dashboard builder --- src/handlers/securityDashboardHandlers.js | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/handlers/securityDashboardHandlers.js b/src/handlers/securityDashboardHandlers.js index ec1527a100..15b1ac4dfe 100644 --- a/src/handlers/securityDashboardHandlers.js +++ b/src/handlers/securityDashboardHandlers.js @@ -22,9 +22,30 @@ function embed(title, description, color = 0x5865f2, guild) { return new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }).setTitle(title).setDescription(description).setColor(color).setFooter({ text: 'Infinity System • Changes save automatically' }).setTimestamp(); } +export async function buildSecurityDashboard(client, guild, userId) { + const config = await getSecurityConfig(client, guild.id); + return { + embeds: [embed('🛡️ Server Protection', `**${guild.name}**\n\nChoose a security system to configure.\n\n🛡️ Anti-Nuke — ${config.antiNuke.enabled ? '🟢' : '🔴'}\n🚨 Anti-Raid — ${config.antiRaid.enabled ? '🟢' : '🔴'}\n🤖 AutoMod — ${config.autoMod.enabled ? '🟢' : '🔴'}\n⚖️ Punishments — ${(config.escalation || []).length} escalation levels\n🏆 Strikes & Warnings — management enabled\n👤 Whitelist — ${(config.whitelist.users.length + config.whitelist.roles.length + config.whitelist.bots.length)} entries\n📋 Logs — ${config.logChannelId ? `<#${config.logChannelId}>` : 'not configured'}`, 0x57f287, guild)], + components: [ + row( + B(`security_panel_nuke2:${userId}`, '🛡️ Anti-Nuke', ButtonStyle.Danger), + B(`security_panel_raid2:${userId}`, '🚨 Anti-Raid', ButtonStyle.Primary), + B(`security_panel_automod2:${userId}`, '🤖 AutoMod', ButtonStyle.Primary), + B(`security_panel_punishments2:${userId}`, '⚖️ Punishments', ButtonStyle.Primary), + ), + row( + B(`security_panel_strikes2:${userId}`, '🏆 Strikes', ButtonStyle.Danger), + B(`security_panel_whitelist2:${userId}`, '👤 Whitelist'), + B(`security_panel_logs2:${userId}`, '📋 Logs'), + B(`security_panel_settings2:${userId}`, '⚙️ Settings'), + B(`security_refresh:${userId}`, '🔄 Refresh', ButtonStyle.Success), + ), + ], + }; +} + async function dashboard(i, c) { - const config = await getSecurityConfig(c, i.guildId); - return i.update({ embeds: [embed('🛡️ Server Protection', `**${i.guild.name}**\n\nChoose a security system to configure.\n\n🛡️ Anti-Nuke — ${config.antiNuke.enabled ? '🟢' : '🔴'}\n🚨 Anti-Raid — ${config.antiRaid.enabled ? '🟢' : '🔴'}\n🤖 AutoMod — ${config.autoMod.enabled ? '🟢' : '🔴'}\n⚖️ Punishments — ${config.escalation.length} escalation levels\n🏆 Strikes & Warnings — management enabled\n👤 Whitelist — ${(config.whitelist.users.length + config.whitelist.roles.length + config.whitelist.bots.length)} entries\n📋 Logs — ${config.logChannelId ? `<#${config.logChannelId}>` : 'not configured'}`, 0x57f287, i.guild)], components: [row(B(`security_panel_nuke2:${i.user.id}`, '🛡️ Anti-Nuke', ButtonStyle.Danger), B(`security_panel_raid2:${i.user.id}`, '🚨 Anti-Raid', ButtonStyle.Primary), B(`security_panel_automod2:${i.user.id}`, '🤖 AutoMod', ButtonStyle.Primary), B(`security_panel_punishments2:${i.user.id}`, '⚖️ Punishments', ButtonStyle.Primary)), row(B(`security_panel_strikes2:${i.user.id}`, '🏆 Strikes', ButtonStyle.Danger), B(`security_panel_whitelist2:${i.user.id}`, '👤 Whitelist'), B(`security_panel_logs2:${i.user.id}`, '📋 Logs'), B(`security_panel_settings2:${i.user.id}`, '⚙️ Settings'), B(`security_refresh:${i.user.id}`, '🔄 Refresh', ButtonStyle.Success)))] }); + return i.update(await buildSecurityDashboard(c, i.guild, i.user.id)); } async function nuke(i, c) { From ceb4123eca4733efc2e0405e3473de18c1694271 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:58:47 +0300 Subject: [PATCH 108/219] Unify security button handlers --- src/interactions/buttons/security/security.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index 598584c2e4..01a6e66403 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -1,13 +1,9 @@ -import { securityButtonHandlers } from '../../../handlers/securityHandlers.js'; -import { securityAdvancedButtonHandlers } from '../../../handlers/securityAdvancedHandlers.js'; import { securityDashboardButtonHandlers } from '../../../handlers/securityDashboardHandlers.js'; import securityPanelOverrides from '../../../handlers/securityPanelOverrides.js'; import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRuleHandlers.js'; import securityAutoModDashboard from '../../../handlers/securityAutoModDashboard.js'; export default [ - ...securityButtonHandlers, - ...securityAdvancedButtonHandlers, ...securityDashboardButtonHandlers, ...securityDashboardRuleHandlers, ...securityPanelOverrides, From 9015bce8ada7ed457c4d3c8eba3d0ff53124860c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 15:58:51 +0300 Subject: [PATCH 109/219] Fix legacy security panel aliases --- src/handlers/securityPanelOverrides.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/handlers/securityPanelOverrides.js b/src/handlers/securityPanelOverrides.js index 3966aadb03..9f01b93db7 100644 --- a/src/handlers/securityPanelOverrides.js +++ b/src/handlers/securityPanelOverrides.js @@ -1,7 +1,7 @@ import { securityDashboardButtonHandlers } from './securityDashboardHandlers.js'; +import securityAutoModDashboard from './securityAutoModDashboard.js'; -// Alias the original /security panel IDs to the new hierarchical dashboard. -// This keeps existing command messages/buttons compatible after deployment. +// Keep old /security panel button IDs compatible with the new dashboard. const names = new Set([ 'security_panel_nuke', 'security_panel_raid', @@ -26,11 +26,10 @@ const target = { export default [...names].map(name => ({ name, - execute: async (interaction, client) => { - const handler = securityDashboardButtonHandlers.find( - h => h.name === target[name] - ); + const targetName = target[name]; + const handlers = [...securityDashboardButtonHandlers, ...securityAutoModDashboard]; + const handler = handlers.find(h => h.name === targetName); if (!handler) { return interaction.reply({ @@ -40,8 +39,7 @@ export default [...names].map(name => ({ } const original = interaction.customId; - - interaction.customId = `${target[name]}:${interaction.user.id}`; + interaction.customId = `${targetName}:${interaction.user.id}`; try { return await handler.execute(interaction, client); From e3c52e29425bf2b69007fb22b5e41d7b69ecc506 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 16:51:07 +0300 Subject: [PATCH 110/219] Delete src/commands/Core/security.js --- src/commands/Core/security.js | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 src/commands/Core/security.js diff --git a/src/commands/Core/security.js b/src/commands/Core/security.js deleted file mode 100644 index 3002bde7e6..0000000000 --- a/src/commands/Core/security.js +++ /dev/null @@ -1,28 +0,0 @@ -import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; -import { buildSecurityDashboard } from '../../handlers/securityDashboardHandlers.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -export default { - data: new SlashCommandBuilder() - .setName('security') - .setDescription('Open the server security control panel') - .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) - .setDMPermission(false), - category: 'Core', - async execute(interaction, config, client) { - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - await interaction.reply({ - content: 'You need the Manage Server permission to use this panel.', - flags: MessageFlags.Ephemeral, - }); - return; - } - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) return; - - const view = await buildSecurityDashboard(client, interaction.guild, interaction.user.id); - - await InteractionHelper.safeEditReply(interaction, view); - }, -}; From 48e161804b99e5258c77c4ee89d664d234b963b7 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 17:47:10 +0300 Subject: [PATCH 111/219] fix security dashboard interaction handlers --- src/handlers/securityDashboardCore.js | 115 ++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/handlers/securityDashboardCore.js diff --git a/src/handlers/securityDashboardCore.js b/src/handlers/securityDashboardCore.js new file mode 100644 index 0000000000..30c97ca16d --- /dev/null +++ b/src/handlers/securityDashboardCore.js @@ -0,0 +1,115 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { getSecurityConfig, updateSecurityConfig, getStrikes, clearStrikes } from '../services/security/securityService.js'; + +const NUKE_ACTIONS = ['strip', 'kick', 'ban']; +const RAID_ACTIONS = ['timeout', 'kick', 'ban']; + +const ok = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; +const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); +const button = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); +const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); +const cycle = (value, values) => values[(values.indexOf(value) + 1) % values.length]; + +function embed(title, description, guild, color = 0x5865f2) { + return new EmbedBuilder() + .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) + .setTitle(title) + .setDescription(description) + .setColor(color) + .setFooter({ text: 'Infinity System • Changes save automatically' }) + .setTimestamp(); +} + +export async function buildSecurityDashboard(client, guild, userId) { + const config = await getSecurityConfig(client, guild.id); + return { + embeds: [embed('🛡️ Server Protection', `**${guild.name}**\n\nChoose a security system to configure.\n\n🛡️ Anti-Nuke — ${config.antiNuke?.enabled ? '🟢' : '🔴'}\n🚨 Anti-Raid — ${config.antiRaid?.enabled ? '🟢' : '🔴'}\n🤖 AutoMod — ${config.autoMod?.enabled ? '🟢' : '🔴'}\n⚖️ Punishments — ${(config.escalation || []).length} escalation levels\n🏆 Strikes & Warnings — management enabled\n👤 Whitelist — ${(config.whitelist?.users?.length || 0) + (config.whitelist?.roles?.length || 0) + (config.whitelist?.bots?.length || 0)} entries\n📋 Logs — ${config.logChannelId ? `<#${config.logChannelId}>` : 'not configured'}`, guild, 0x57f287)], + components: [ + row(button(`security_panel_nuke2:${userId}`, '🛡️ Anti-Nuke', ButtonStyle.Danger), button(`security_panel_raid2:${userId}`, '🚨 Anti-Raid', ButtonStyle.Primary), button(`security_panel_automod2:${userId}`, '🤖 AutoMod', ButtonStyle.Primary), button(`security_panel_punishments2:${userId}`, '⚖️ Punishments', ButtonStyle.Primary)), + row(button(`security_panel_strikes2:${userId}`, '🏆 Strikes', ButtonStyle.Danger), button(`security_panel_whitelist2:${userId}`, '👤 Whitelist'), button(`security_panel_logs2:${userId}`, '📋 Logs'), button(`security_panel_settings2:${userId}`, '⚙️ Settings'), button(`security_refresh:${userId}`, '🔄 Refresh', ButtonStyle.Success)), + ], + }; +} + +async function dashboard(interaction, client) { + return interaction.update(await buildSecurityDashboard(client, interaction.guild, interaction.user.id)); +} + +async function panel(interaction, client, type) { + const config = await getSecurityConfig(client, interaction.guildId); + let title = '🛡️ Security'; + let description = ''; + let color = 0x5865f2; + let components = [row(button(`security_back2:${interaction.user.id}`, '← Back'))]; + + if (type === 'nuke') { + title = '🛡️ Anti-Nuke'; color = 0xed4245; + description = `**Status:** ${config.antiNuke.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Window:** ${config.antiNuke.windowMs / 1000}s\n**Lockdown:** ${config.antiNuke.lockdown ? '🟢 ON' : '🔴 OFF'}\n**Default action:** ${config.antiNuke.action || 'strip'}`; + components = [row(button(`security_back2:${interaction.user.id}`, '← Back'), button(`nuke_toggle2:${interaction.user.id}`, config.antiNuke.enabled ? 'Disable' : 'Enable', config.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(`nuke_lock2:${interaction.user.id}`, `Lockdown: ${config.antiNuke.lockdown ? 'ON' : 'OFF'}`), button(`nuke_window2:${interaction.user.id}`, `Window: ${config.antiNuke.windowMs / 1000}s`)), row(button(`nuke_rules2:${interaction.user.id}`, '⚖️ Rule Punishments', ButtonStyle.Primary))]; + } else if (type === 'raid') { + title = '🚨 Anti-Raid'; color = 0xf47b67; + description = `**Status:** ${config.antiRaid.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Joins:** ${config.antiRaid.joins}\n**Window:** ${config.antiRaid.windowMs / 1000}s\n**Account age:** ${Math.round(config.antiRaid.minAccountAgeMs / 3600000)}h\n**Punishment:** ${config.antiRaid.punishment}\n**Lockdown:** ${config.antiRaid.lockdown ? 'ON' : 'OFF'}`; + components = [row(button(`security_back2:${interaction.user.id}`, '← Back'), button(`raid_toggle2:${interaction.user.id}`, config.antiRaid.enabled ? 'Disable' : 'Enable', config.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), button(`raid_punishment2:${interaction.user.id}`, `Punishment: ${config.antiRaid.punishment}`, ButtonStyle.Primary)), row(button(`raid_joins2:${interaction.user.id}`, `Joins: ${config.antiRaid.joins}`), button(`raid_window2:${interaction.user.id}`, `Window: ${config.antiRaid.windowMs / 1000}s`), button(`raid_age2:${interaction.user.id}`, `Age: ${Math.round(config.antiRaid.minAccountAgeMs / 3600000)}h`), button(`raid_lock2:${interaction.user.id}`, `Lockdown: ${config.antiRaid.lockdown ? 'ON' : 'OFF'}`))]; + } else if (type === 'punishments') { + title = '⚖️ Punishments'; color = 0xfee75c; + const escalation = (config.escalation || []).map(e => `Strike ${e.strike} → **${e.action}**`).join('\n') || 'No escalation rules configured.'; + description = `**Anti-Raid:** ${config.antiRaid.punishment}\n**Strike decay:** ${Math.round(config.strikeDecayMs / 3600000)}h\n\n**Escalation**\n${escalation}`; + components = [row(button(`security_back2:${interaction.user.id}`, '← Back'), button(`punishment_decay2:${interaction.user.id}`, '⏱️ Decay', ButtonStyle.Primary), button(`punishment_rules2:${interaction.user.id}`, '⚖️ Rule Punishments', ButtonStyle.Primary))]; + } else if (type === 'whitelist') { + title = '👤 Whitelist'; color = 0x57f287; + description = `**Users:** ${config.whitelist?.users?.length || 0}\n**Roles:** ${config.whitelist?.roles?.length || 0}\n**Bots:** ${config.whitelist?.bots?.length || 0}`; + components = [row(button(`security_back2:${interaction.user.id}`, '← Back'), button(`wl_users2:${interaction.user.id}`, '👤 Users', ButtonStyle.Primary), button(`wl_roles2:${interaction.user.id}`, '🎭 Roles', ButtonStyle.Primary), button(`wl_bots2:${interaction.user.id}`, '🤖 Bots', ButtonStyle.Primary))]; + } else if (type === 'logs') { + title = '📋 Security Logs'; + description = `**Log channel:** ${config.logChannelId ? `<#${config.logChannelId}>` : 'Not configured'}\n**Ignored channels:** ${config.ignoredChannels?.length || 0}`; + components = [row(button(`security_back2:${interaction.user.id}`, '← Back'), button(`logs_channel2:${interaction.user.id}`, '📋 Set Log Channel', ButtonStyle.Primary), button(`logs_ignored2:${interaction.user.id}`, '🚫 Ignored Channels'))]; + } else if (type === 'settings') { + title = '⚙️ Security Settings'; color = 0x57f287; + description = `**Global protection:** ${config.enabled ? '🟢 ON' : '🔴 OFF'}\n**Strike decay:** ${Math.round(config.strikeDecayMs / 3600000)}h\n**Ignored channels:** ${config.ignoredChannels?.length || 0}`; + components = [row(button(`security_back2:${interaction.user.id}`, '← Back'), button(`settings_toggle2:${interaction.user.id}`, config.enabled ? 'Disable Protection' : 'Enable Protection'), button(`settings_decay2:${interaction.user.id}`, '⏱️ Strike Decay', ButtonStyle.Primary))]; + } + + return interaction.update({ embeds: [embed(title, description, interaction.guild, color)], components }); +} + +async function strikes(interaction, client) { + const members = await interaction.guild.members.fetch().catch(() => interaction.guild.members.cache); + const entries = []; + for (const member of members.values()) { + if (member.user.bot) continue; + const strike = await getStrikes(client, interaction.guildId, member.id).catch(() => ({ count: 0 })); + if (strike.count) entries.push({ id: member.id, count: strike.count }); + } + entries.sort((a, b) => b.count - a.count); + const text = entries.slice(0, 10).map((entry, index) => `${index + 1}. <@${entry.id}> — **${entry.count}** strikes`).join('\n') || 'No active strikes.'; + return interaction.update({ embeds: [embed('🏆 Strikes & Warnings', text, interaction.guild, 0xfee75c)], components: [row(button(`security_back2:${interaction.user.id}`, '← Back'), button(`strikes_refresh2:${interaction.user.id}`, '🔄 Refresh', ButtonStyle.Success))] }); +} + +const panelHandlers = { + security_panel_nuke2: 'nuke', security_panel_raid2: 'raid', security_panel_punishments2: 'punishments', security_panel_whitelist2: 'whitelist', security_panel_logs2: 'logs', security_panel_settings2: 'settings', + security_panel_nuke: 'nuke', security_panel_raid: 'raid', security_panel_punishments: 'punishments', security_panel_whitelist: 'whitelist', security_panel_logs: 'logs', security_panel_settings: 'settings', +}; + +export const securityDashboardButtonHandlers = [ + ...Object.entries(panelHandlers).map(([name, type]) => ({ name, execute: async (interaction, client) => ok(interaction) ? panel(interaction, client, type) : deny(interaction) })), + { name: 'security_panel_strikes2', execute: async (interaction, client) => ok(interaction) ? strikes(interaction, client) : deny(interaction) }, + { name: 'security_panel_strikes', execute: async (interaction, client) => ok(interaction) ? strikes(interaction, client) : deny(interaction) }, + { name: 'security_back2', execute: async (interaction, client) => ok(interaction) ? dashboard(interaction, client) : deny(interaction) }, + { name: 'security_back', execute: async (interaction, client) => ok(interaction) ? dashboard(interaction, client) : deny(interaction) }, + { name: 'security_refresh', execute: async (interaction, client) => ok(interaction) ? dashboard(interaction, client) : deny(interaction) }, + { name: 'nuke_toggle2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { enabled: !x.antiNuke.enabled } }); return panel(i, c, 'nuke'); } }, + { name: 'nuke_lock2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { lockdown: !x.antiNuke.lockdown } }); return panel(i, c, 'nuke'); } }, + { name: 'nuke_window2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiNuke: { windowMs: cycle(x.antiNuke.windowMs, [5000, 10000, 15000, 30000, 60000]) } }); return panel(i, c, 'nuke'); } }, + { name: 'nuke_rules2', execute: async (i, c) => ok(i) ? i.update({ embeds: [embed('⚖️ Anti-Nuke Rule Punishments', 'Use the rule buttons from the security punishment dashboard.', i.guild, 0xed4245)], components: [row(button(`punishment_rules2:${i.user.id}`, '← Punishments'))] }) : deny(i) }, + { name: 'raid_toggle2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { enabled: !x.antiRaid.enabled } }); return panel(i, c, 'raid'); } }, + { name: 'raid_punishment2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { punishment: cycle(x.antiRaid.punishment, RAID_ACTIONS) } }); return panel(i, c, 'raid'); } }, + { name: 'raid_joins2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { joins: x.antiRaid.joins >= 50 ? 2 : x.antiRaid.joins + 2 } }); return panel(i, c, 'raid'); } }, + { name: 'raid_window2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { windowMs: cycle(x.antiRaid.windowMs, [5000, 10000, 15000, 30000, 60000]) } }); return panel(i, c, 'raid'); } }, + { name: 'raid_age2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { minAccountAgeMs: cycle(x.antiRaid.minAccountAgeMs, [0, 3600000, 21600000, 86400000, 604800000, 2592000000]) } }); return panel(i, c, 'raid'); } }, + { name: 'raid_lock2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { antiRaid: { lockdown: !x.antiRaid.lockdown } }); return panel(i, c, 'raid'); } }, + { name: 'punishment_rules2', execute: async (i, c) => ok(i) ? panel(i, c, 'punishments') : deny(i) }, + { name: 'punishments_back2', execute: async (i, c) => ok(i) ? panel(i, c, 'punishments') : deny(i) }, + { name: 'punishment_decay2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { strikeDecayMs: x.strikeDecayMs >= 30 * 86400000 ? 3600000 : x.strikeDecayMs + 3600000 }); return panel(i, c, 'punishments'); } }, + { name: 'settings_toggle2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { enabled: !x.enabled }); return panel(i, c, 'settings'); } }, + { name: 'settings_decay2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { strikeDecayMs: x.strikeDecayMs >= 30 * 86400000 ? 3600000 : x.strikeDecayMs + 3600000 }); return panel(i, c, 'settings'); } }, +]; From df60ab14372752cfc6e8f7197e856c5318eed9fb Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 17:47:14 +0300 Subject: [PATCH 112/219] fix security button handler loading --- src/interactions/buttons/security/security.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index 01a6e66403..bf8a0c8cf1 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -1,11 +1,9 @@ -import { securityDashboardButtonHandlers } from '../../../handlers/securityDashboardHandlers.js'; -import securityPanelOverrides from '../../../handlers/securityPanelOverrides.js'; +import { securityDashboardButtonHandlers } from '../../../handlers/securityDashboardCore.js'; import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRuleHandlers.js'; import securityAutoModDashboard from '../../../handlers/securityAutoModDashboard.js'; export default [ ...securityDashboardButtonHandlers, ...securityDashboardRuleHandlers, - ...securityPanelOverrides, ...securityAutoModDashboard, ]; From e0ea16c9710a91bc19e8aaacf35a752e36fcd509 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 17:52:48 +0300 Subject: [PATCH 113/219] Fix AutoMod button and preserve exact security dashboard on Back --- src/handlers/securityDashboardOverrides.js | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/handlers/securityDashboardOverrides.js diff --git a/src/handlers/securityDashboardOverrides.js b/src/handlers/securityDashboardOverrides.js new file mode 100644 index 0000000000..a46011fe8d --- /dev/null +++ b/src/handlers/securityDashboardOverrides.js @@ -0,0 +1,57 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; +import { buildSecurityDashboard, buildSecurityControls } from '../commands/Security/security.js'; + +const ok = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; +const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); +const button = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); + +async function main(interaction, client) { + const config = await getSecurityConfig(client, interaction.guildId); + return interaction.update({ + embeds: [buildSecurityDashboard(config, interaction.guild)], + components: buildSecurityControls(interaction.user.id), + }); +} + +async function automod(interaction, client) { + const config = await getSecurityConfig(client, interaction.guildId); + const auto = config.autoMod || {}; + const enabled = !!auto.enabled; + const row1 = new ActionRowBuilder().addComponents( + button(`security_main2:${interaction.user.id}`, '← Back'), + button(`security_automod_toggle3:${interaction.user.id}`, enabled ? '🟢 Disable AutoMod' : '🔴 Enable AutoMod', enabled ? ButtonStyle.Danger : ButtonStyle.Success), + button(`pun_auto2:${interaction.user.id}`, '⚖️ Punishments', ButtonStyle.Primary), + ); + const rows = [row1]; + const labels = { spam: 'Spam', duplicate: 'Duplicate', mentions: 'Mentions', invites: 'Invites', links: 'Links', caps: 'Caps', badWords: 'Bad Words' }; + rows.push(new ActionRowBuilder().addComponents( + ...Object.entries(labels).slice(0, 4).map(([key, label]) => button(`auto_pun2:${key}:${interaction.user.id}`, `${label}: ${auto[key]?.punishment || 'delete'}`, ButtonStyle.Primary)), + )); + rows.push(new ActionRowBuilder().addComponents( + ...Object.entries(labels).slice(4).map(([key, label]) => button(`auto_pun2:${key}:${interaction.user.id}`, `${label}: ${auto[key]?.punishment || 'delete'}`, ButtonStyle.Primary)), + )); + return interaction.update({ + embeds: [new EmbedBuilder() + .setTitle('🤖 AutoMod') + .setDescription(`**Status:** ${enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n\nConfigure AutoMod punishments below. Each rule can use its own punishment.`) + .setColor(0x5865f2) + .setFooter({ text: 'Infinity System • Security Center • Changes save automatically' })], + components: rows, + }); +} + +export default [ + { name: 'security_panel_automod', execute: async (i, c) => ok(i) ? automod(i, c) : deny(i) }, + { name: 'security_panel_automod2', execute: async (i, c) => ok(i) ? automod(i, c) : deny(i) }, + { name: 'security_main2', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, + { name: 'security_back2', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, + { name: 'security_back', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, + { name: 'security_refresh', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, + { name: 'security_automod_toggle3', execute: async (i, c) => { + if (!ok(i)) return deny(i); + const x = await getSecurityConfig(c, i.guildId); + await updateSecurityConfig(c, i.guildId, { autoMod: { enabled: !x.autoMod.enabled } }); + return automod(i, c); + } }, +]; From 49a7c8801a070cbeb460dda2b279454bd14f4a45 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 17:52:55 +0300 Subject: [PATCH 114/219] Prioritize Security dashboard overrides for AutoMod and Back --- src/interactions/buttons/security/security.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index bf8a0c8cf1..520e373b25 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -1,8 +1,10 @@ +import securityDashboardOverrides from '../../../handlers/securityDashboardOverrides.js'; import { securityDashboardButtonHandlers } from '../../../handlers/securityDashboardCore.js'; import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRuleHandlers.js'; import securityAutoModDashboard from '../../../handlers/securityAutoModDashboard.js'; export default [ + ...securityDashboardOverrides, ...securityDashboardButtonHandlers, ...securityDashboardRuleHandlers, ...securityAutoModDashboard, From ac5f5b3a67481b14812ede9ed7d355f362e93620 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 17:56:59 +0300 Subject: [PATCH 115/219] fix security automod legacy button alias --- src/handlers/securityAutoModDashboard.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/handlers/securityAutoModDashboard.js b/src/handlers/securityAutoModDashboard.js index 163c82d671..1483708cef 100644 --- a/src/handlers/securityAutoModDashboard.js +++ b/src/handlers/securityAutoModDashboard.js @@ -72,6 +72,10 @@ async function rulePage(i, client, key) { function parseKey(i) { return i.customId.split(':').at(-2); } export default [ + { + name: 'security_panel_automod', + execute: async (i, client) => ok(i) ? autoModPage(i, client) : deny(i), + }, { name: 'security_panel_automod2', execute: async (i, client) => ok(i) ? autoModPage(i, client) : deny(i), @@ -110,7 +114,7 @@ export default [ const key = parseKey(i); const x = await getSecurityConfig(client, i.guildId); const values = RULES[key].values; - await updateSecurityConfig(client, i.guildId, { autoMod: { [key]: { punishment: cycle(x.autoMod[key].punishment, values) } } }); + await updateSecurityConfig(client, i.guildId, { autoMod: { [key]: { punishment: cycle(x.autoMod[key].punishment, values) } }); return rulePage(i, client, key); }, }, From 723f00ad79d59a8364c2fd7ec6f9dde65895ee42 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 17:57:20 +0300 Subject: [PATCH 116/219] fix automod handler syntax and legacy alias --- src/handlers/securityAutoModDashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/securityAutoModDashboard.js b/src/handlers/securityAutoModDashboard.js index 1483708cef..f20d66309b 100644 --- a/src/handlers/securityAutoModDashboard.js +++ b/src/handlers/securityAutoModDashboard.js @@ -114,7 +114,7 @@ export default [ const key = parseKey(i); const x = await getSecurityConfig(client, i.guildId); const values = RULES[key].values; - await updateSecurityConfig(client, i.guildId, { autoMod: { [key]: { punishment: cycle(x.autoMod[key].punishment, values) } }); + await updateSecurityConfig(client, i.guildId, { autoMod: { [key]: { punishment: cycle(x.autoMod[key].punishment, values) } } }); return rulePage(i, client, key); }, }, From 585619e38dd82d3571c63358dd5335b051c729fd Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 17:57:31 +0300 Subject: [PATCH 117/219] fix security back to preserve original dashboard --- src/handlers/securityDashboardOverrides.js | 39 +--------------------- 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/src/handlers/securityDashboardOverrides.js b/src/handlers/securityDashboardOverrides.js index a46011fe8d..e6d71dcf49 100644 --- a/src/handlers/securityDashboardOverrides.js +++ b/src/handlers/securityDashboardOverrides.js @@ -1,10 +1,8 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; -import { getSecurityConfig, updateSecurityConfig } from '../services/security/securityService.js'; import { buildSecurityDashboard, buildSecurityControls } from '../commands/Security/security.js'; +import { getSecurityConfig } from '../services/security/securityService.js'; const ok = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); -const button = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); async function main(interaction, client) { const config = await getSecurityConfig(client, interaction.guildId); @@ -14,44 +12,9 @@ async function main(interaction, client) { }); } -async function automod(interaction, client) { - const config = await getSecurityConfig(client, interaction.guildId); - const auto = config.autoMod || {}; - const enabled = !!auto.enabled; - const row1 = new ActionRowBuilder().addComponents( - button(`security_main2:${interaction.user.id}`, '← Back'), - button(`security_automod_toggle3:${interaction.user.id}`, enabled ? '🟢 Disable AutoMod' : '🔴 Enable AutoMod', enabled ? ButtonStyle.Danger : ButtonStyle.Success), - button(`pun_auto2:${interaction.user.id}`, '⚖️ Punishments', ButtonStyle.Primary), - ); - const rows = [row1]; - const labels = { spam: 'Spam', duplicate: 'Duplicate', mentions: 'Mentions', invites: 'Invites', links: 'Links', caps: 'Caps', badWords: 'Bad Words' }; - rows.push(new ActionRowBuilder().addComponents( - ...Object.entries(labels).slice(0, 4).map(([key, label]) => button(`auto_pun2:${key}:${interaction.user.id}`, `${label}: ${auto[key]?.punishment || 'delete'}`, ButtonStyle.Primary)), - )); - rows.push(new ActionRowBuilder().addComponents( - ...Object.entries(labels).slice(4).map(([key, label]) => button(`auto_pun2:${key}:${interaction.user.id}`, `${label}: ${auto[key]?.punishment || 'delete'}`, ButtonStyle.Primary)), - )); - return interaction.update({ - embeds: [new EmbedBuilder() - .setTitle('🤖 AutoMod') - .setDescription(`**Status:** ${enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n\nConfigure AutoMod punishments below. Each rule can use its own punishment.`) - .setColor(0x5865f2) - .setFooter({ text: 'Infinity System • Security Center • Changes save automatically' })], - components: rows, - }); -} - export default [ - { name: 'security_panel_automod', execute: async (i, c) => ok(i) ? automod(i, c) : deny(i) }, - { name: 'security_panel_automod2', execute: async (i, c) => ok(i) ? automod(i, c) : deny(i) }, { name: 'security_main2', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, { name: 'security_back2', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, { name: 'security_back', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, { name: 'security_refresh', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, - { name: 'security_automod_toggle3', execute: async (i, c) => { - if (!ok(i)) return deny(i); - const x = await getSecurityConfig(c, i.guildId); - await updateSecurityConfig(c, i.guildId, { autoMod: { enabled: !x.autoMod.enabled } }); - return automod(i, c); - } }, ]; From 32e52ddba1de6abfbc31e0ddb9ff7a8d5a4cf615 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 17:57:36 +0300 Subject: [PATCH 118/219] fix security handler precedence and dashboard back behavior --- src/interactions/buttons/security/security.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index 520e373b25..44090ede47 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -3,9 +3,12 @@ import { securityDashboardButtonHandlers } from '../../../handlers/securityDashb import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRuleHandlers.js'; import securityAutoModDashboard from '../../../handlers/securityAutoModDashboard.js'; +// Load the core/dashboard handlers first, then AutoMod, and finally the +// compatibility overrides so Back/Refresh always return to the exact +// original /security dashboard UI. export default [ - ...securityDashboardOverrides, ...securityDashboardButtonHandlers, ...securityDashboardRuleHandlers, ...securityAutoModDashboard, + ...securityDashboardOverrides, ]; From 8fe870397f2413ae203e33985eebfc471ff3e960 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:03:07 +0300 Subject: [PATCH 119/219] Fix Security AutoMod alias and preserve original dashboard on Back --- src/handlers/securityDashboardOverrides.js | 24 ++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/handlers/securityDashboardOverrides.js b/src/handlers/securityDashboardOverrides.js index e6d71dcf49..fd66b899a5 100644 --- a/src/handlers/securityDashboardOverrides.js +++ b/src/handlers/securityDashboardOverrides.js @@ -1,10 +1,12 @@ import { buildSecurityDashboard, buildSecurityControls } from '../commands/Security/security.js'; import { getSecurityConfig } from '../services/security/securityService.js'; +import securityAutoModDashboard from './securityAutoModDashboard.js'; const ok = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); async function main(interaction, client) { + if (!ok(interaction)) return deny(interaction); const config = await getSecurityConfig(client, interaction.guildId); return interaction.update({ embeds: [buildSecurityDashboard(config, interaction.guild)], @@ -12,9 +14,23 @@ async function main(interaction, client) { }); } +async function automod(interaction, client) { + if (!ok(interaction)) return deny(interaction); + const handler = securityAutoModDashboard.find(h => h.name === 'security_panel_automod2'); + if (!handler) return interaction.reply({ content: 'AutoMod handler unavailable.', ephemeral: true }); + const original = interaction.customId; + interaction.customId = `security_panel_automod2:${interaction.user.id}`; + try { + return await handler.execute(interaction, client); + } finally { + interaction.customId = original; + } +} + export default [ - { name: 'security_main2', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, - { name: 'security_back2', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, - { name: 'security_back', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, - { name: 'security_refresh', execute: async (i, c) => ok(i) ? main(i, c) : deny(i) }, + { name: 'security_panel_automod', execute: automod }, + { name: 'security_main2', execute: main }, + { name: 'security_back2', execute: main }, + { name: 'security_back', execute: main }, + { name: 'security_refresh', execute: main }, ]; From fd557748f69798dd9fb9070527650fab1691fc8a Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:03:24 +0300 Subject: [PATCH 120/219] Prioritize Security compatibility handlers for legacy buttons and Back --- src/interactions/buttons/security/security.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index 44090ede47..e6d85a589e 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -3,12 +3,12 @@ import { securityDashboardButtonHandlers } from '../../../handlers/securityDashb import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRuleHandlers.js'; import securityAutoModDashboard from '../../../handlers/securityAutoModDashboard.js'; -// Load the core/dashboard handlers first, then AutoMod, and finally the -// compatibility overrides so Back/Refresh always return to the exact -// original /security dashboard UI. +// Compatibility handlers must be registered first so legacy message buttons +// resolve to the intended dashboard/AutoMod behavior before duplicate IDs from +// the newer dashboard handlers are encountered. export default [ + ...securityDashboardOverrides, ...securityDashboardButtonHandlers, ...securityDashboardRuleHandlers, ...securityAutoModDashboard, - ...securityDashboardOverrides, ]; From 0fb13af564ee05e310c90e5602b74a40d26e54ca Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:07:31 +0300 Subject: [PATCH 121/219] Fix security back dashboard to use the original /security dashboard --- src/handlers/securityDashboardCore.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/handlers/securityDashboardCore.js b/src/handlers/securityDashboardCore.js index 30c97ca16d..e3d28f60ad 100644 --- a/src/handlers/securityDashboardCore.js +++ b/src/handlers/securityDashboardCore.js @@ -1,5 +1,6 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; import { getSecurityConfig, updateSecurityConfig, getStrikes, clearStrikes } from '../services/security/securityService.js'; +import { buildSecurityDashboard as buildOriginalSecurityDashboard, buildSecurityControls } from '../commands/Security/security.js'; const NUKE_ACTIONS = ['strip', 'kick', 'ban']; const RAID_ACTIONS = ['timeout', 'kick', 'ban']; @@ -20,14 +21,13 @@ function embed(title, description, guild, color = 0x5865f2) { .setTimestamp(); } +// IMPORTANT: this must be the exact same dashboard used by /security. +// Do not maintain a second dashboard layout here. export async function buildSecurityDashboard(client, guild, userId) { const config = await getSecurityConfig(client, guild.id); return { - embeds: [embed('🛡️ Server Protection', `**${guild.name}**\n\nChoose a security system to configure.\n\n🛡️ Anti-Nuke — ${config.antiNuke?.enabled ? '🟢' : '🔴'}\n🚨 Anti-Raid — ${config.antiRaid?.enabled ? '🟢' : '🔴'}\n🤖 AutoMod — ${config.autoMod?.enabled ? '🟢' : '🔴'}\n⚖️ Punishments — ${(config.escalation || []).length} escalation levels\n🏆 Strikes & Warnings — management enabled\n👤 Whitelist — ${(config.whitelist?.users?.length || 0) + (config.whitelist?.roles?.length || 0) + (config.whitelist?.bots?.length || 0)} entries\n📋 Logs — ${config.logChannelId ? `<#${config.logChannelId}>` : 'not configured'}`, guild, 0x57f287)], - components: [ - row(button(`security_panel_nuke2:${userId}`, '🛡️ Anti-Nuke', ButtonStyle.Danger), button(`security_panel_raid2:${userId}`, '🚨 Anti-Raid', ButtonStyle.Primary), button(`security_panel_automod2:${userId}`, '🤖 AutoMod', ButtonStyle.Primary), button(`security_panel_punishments2:${userId}`, '⚖️ Punishments', ButtonStyle.Primary)), - row(button(`security_panel_strikes2:${userId}`, '🏆 Strikes', ButtonStyle.Danger), button(`security_panel_whitelist2:${userId}`, '👤 Whitelist'), button(`security_panel_logs2:${userId}`, '📋 Logs'), button(`security_panel_settings2:${userId}`, '⚙️ Settings'), button(`security_refresh:${userId}`, '🔄 Refresh', ButtonStyle.Success)), - ], + embeds: [buildOriginalSecurityDashboard(config, guild)], + components: buildSecurityControls(userId), }; } From a5055ca27b68f358c281bc0dc4568fd9c3ae4720 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:22:44 +0300 Subject: [PATCH 122/219] Add TempVoice persistence and room service --- src/services/tempVoiceService.js | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/services/tempVoiceService.js diff --git a/src/services/tempVoiceService.js b/src/services/tempVoiceService.js new file mode 100644 index 0000000000..360f180251 --- /dev/null +++ b/src/services/tempVoiceService.js @@ -0,0 +1,57 @@ +import { ChannelType, PermissionFlagsBits } from 'discord.js'; + +const key = guildId => `guild:${guildId}:tempvoice:config`; + +async function read(client, guildId) { + const value = await client.db.get(key(guildId), null); + return value && typeof value === 'object' ? value : null; +} + +async function write(client, guildId, config) { + await client.db.set(key(guildId), config); + return config; +} + +export async function getTempVoiceConfig(client, guildId) { + return await read(client, guildId) || { + categoryId: null, + triggerChannelId: null, + panelChannelId: null, + panelMessageId: null, + rooms: {}, + }; +} + +export async function saveTempVoiceConfig(client, guildId, config) { + return write(client, guildId, config); +} + +export async function createTempRoom(guild, member, settings) { + const name = `${settings.prefix || '🔊・'}${member.displayName}'s Room`.slice(0, 100); + const channel = await guild.channels.create({ + name, + type: ChannelType.GuildVoice, + parent: settings.categoryId || undefined, + userLimit: settings.userLimit || 0, + bitrate: Math.min(settings.bitrate || 64000, guild.maximumBitrate || 64000), + permissionOverwrites: [ + { id: guild.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.Connect, PermissionFlagsBits.Speak] }, + { id: member.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.Connect, PermissionFlagsBits.Speak, PermissionFlagsBits.MoveMembers, PermissionFlagsBits.ManageChannels] }, + ], + }); + return channel; +} + +export async function removeRoomRecord(client, guildId, channelId) { + const config = await getTempVoiceConfig(client, guildId); + if (config.rooms?.[channelId]) { + delete config.rooms[channelId]; + await saveTempVoiceConfig(client, guildId, config); + } +} + +export async function findOwnedRoom(client, guildId, ownerId) { + const config = await getTempVoiceConfig(client, guildId); + const entry = Object.entries(config.rooms || {}).find(([, room]) => room.ownerId === ownerId); + return entry ? { channelId: entry[0], ...entry[1] } : null; +} From 75df143fd2429222486d3fe67b51385d24488279 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:22:54 +0300 Subject: [PATCH 123/219] Add /tempvoice setup and control panel --- src/commands/JoinToCreate/tempvoice.js | 69 ++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/commands/JoinToCreate/tempvoice.js diff --git a/src/commands/JoinToCreate/tempvoice.js b/src/commands/JoinToCreate/tempvoice.js new file mode 100644 index 0000000000..b0edfedf56 --- /dev/null +++ b/src/commands/JoinToCreate/tempvoice.js @@ -0,0 +1,69 @@ +import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; +import { getTempVoiceConfig, saveTempVoiceConfig } from '../../services/tempVoiceService.js'; + +export default { + data: new SlashCommandBuilder() + .setName('tempvoice') + .setDescription('Set up the temporary voice room system.') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .setDMPermission(false) + .addSubcommand(sub => sub.setName('setup').setDescription('Create Join to Create and the control panel.')) + .addSubcommand(sub => sub.setName('reset').setDescription('Remove the TempVoice setup.')), + + async execute(interaction, config, client) { + const guild = interaction.guild; + const current = await getTempVoiceConfig(client, guild.id); + + if (interaction.options.getSubcommand() === 'reset') { + if (current.triggerChannelId) await guild.channels.delete(current.triggerChannelId).catch(() => {}); + if (current.panelChannelId) await guild.channels.delete(current.panelChannelId).catch(() => {}); + if (current.categoryId) await guild.channels.delete(current.categoryId).catch(() => {}); + for (const channelId of Object.keys(current.rooms || {})) await guild.channels.delete(channelId).catch(() => {}); + await saveTempVoiceConfig(client, guild.id, { categoryId: null, triggerChannelId: null, panelChannelId: null, panelMessageId: null, rooms: {} }); + return interaction.reply({ content: '✅ TempVoice setup has been reset.', ephemeral: true }); + } + + if (current.triggerChannelId && guild.channels.cache.has(current.triggerChannelId)) { + return interaction.reply({ content: `⚠️ TempVoice is already set up: <#${current.triggerChannelId}>`, ephemeral: true }); + } + + await interaction.deferReply({ ephemeral: true }); + + const category = await guild.channels.create({ name: 'Temporary Voice', type: ChannelType.GuildCategory }); + const trigger = await guild.channels.create({ name: '➕・Join to Create', type: ChannelType.GuildVoice, parent: category.id }); + const panel = await guild.channels.create({ name: 'tempvoice-panel', type: ChannelType.GuildText, parent: category.id }); + + const embed = new EmbedBuilder() + .setColor(0x5865f2) + .setTitle('🎙️ Temporary Voice Rooms') + .setDescription(`Join <#${trigger.id}> to create your temporary voice room.\n\nUse the panel below while you are inside your room to manage it.`) + .addFields({ name: 'Controls', value: '🔒 Lock • 👁️ Hide • ✏️ Rename • 👥 Limit\n🚫 Kick • 🔇 Mute • 👑 Transfer • 🗑️ Delete' }) + .setFooter({ text: 'Only the room owner can use these controls.' }); + + const rows = [ + new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('tempvoice_lock').setLabel('🔒 Lock').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('tempvoice_hide').setLabel('👁️ Hide').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('tempvoice_rename').setLabel('✏️ Rename').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('tempvoice_limit').setLabel('👥 Limit').setStyle(ButtonStyle.Primary), + ), + new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('tempvoice_kick').setLabel('🚫 Kick').setStyle(ButtonStyle.Danger), + new ButtonBuilder().setCustomId('tempvoice_mute').setLabel('🔇 Mute').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('tempvoice_transfer').setLabel('👑 Transfer').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('tempvoice_delete').setLabel('🗑️ Delete').setStyle(ButtonStyle.Danger), + ), + ]; + + const message = await panel.send({ embeds: [embed], components: rows }); + await saveTempVoiceConfig(client, guild.id, { + categoryId: category.id, + triggerChannelId: trigger.id, + panelChannelId: panel.id, + panelMessageId: message.id, + rooms: {}, + }); + + await interaction.editReply(`✅ TempVoice is ready.\n\n🎙️ Join to Create: ${trigger}\n🎛️ Control Panel: ${panel}`); + }, +}; From 643f44a5b65017e8f4cbbf99ef723aa86b9f4d71 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:23:30 +0300 Subject: [PATCH 124/219] Connect TempVoice setup to existing JoinToCreate engine --- src/commands/JoinToCreate/tempvoice.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/commands/JoinToCreate/tempvoice.js b/src/commands/JoinToCreate/tempvoice.js index b0edfedf56..098e5bb73b 100644 --- a/src/commands/JoinToCreate/tempvoice.js +++ b/src/commands/JoinToCreate/tempvoice.js @@ -1,4 +1,5 @@ import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; +import { initializeJoinToCreate, getConfiguration } from '../../services/joinToCreateService.js'; import { getTempVoiceConfig, saveTempVoiceConfig } from '../../services/tempVoiceService.js'; export default { @@ -12,13 +13,15 @@ export default { async execute(interaction, config, client) { const guild = interaction.guild; + const subcommand = interaction.options.getSubcommand(); const current = await getTempVoiceConfig(client, guild.id); - if (interaction.options.getSubcommand() === 'reset') { + if (subcommand === 'reset') { + const jtc = await getConfiguration(client, guild.id).catch(() => null); + for (const channelId of Object.keys(jtc?.temporaryChannels || {})) await guild.channels.delete(channelId).catch(() => {}); if (current.triggerChannelId) await guild.channels.delete(current.triggerChannelId).catch(() => {}); if (current.panelChannelId) await guild.channels.delete(current.panelChannelId).catch(() => {}); if (current.categoryId) await guild.channels.delete(current.categoryId).catch(() => {}); - for (const channelId of Object.keys(current.rooms || {})) await guild.channels.delete(channelId).catch(() => {}); await saveTempVoiceConfig(client, guild.id, { categoryId: null, triggerChannelId: null, panelChannelId: null, panelMessageId: null, rooms: {} }); return interaction.reply({ content: '✅ TempVoice setup has been reset.', ephemeral: true }); } @@ -33,6 +36,13 @@ export default { const trigger = await guild.channels.create({ name: '➕・Join to Create', type: ChannelType.GuildVoice, parent: category.id }); const panel = await guild.channels.create({ name: 'tempvoice-panel', type: ChannelType.GuildText, parent: category.id }); + await initializeJoinToCreate(client, guild.id, trigger.id, { + nameTemplate: "{username}'s Room", + userLimit: 0, + bitrate: 64000, + categoryId: category.id, + }); + const embed = new EmbedBuilder() .setColor(0x5865f2) .setTitle('🎙️ Temporary Voice Rooms') From eb679cc3a849965ead86e831b5591e832ea568b8 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:23:37 +0300 Subject: [PATCH 125/219] Add TempVoice room control buttons --- src/interactions/buttons/tempvoice.js | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/interactions/buttons/tempvoice.js diff --git a/src/interactions/buttons/tempvoice.js b/src/interactions/buttons/tempvoice.js new file mode 100644 index 0000000000..32e6c8c155 --- /dev/null +++ b/src/interactions/buttons/tempvoice.js @@ -0,0 +1,53 @@ +import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js'; +import { getJoinToCreateConfig, saveJoinToCreateConfig, getTemporaryChannelInfo } from '../../utils/database.js'; +import { getTempVoiceConfig } from '../../services/tempVoiceService.js'; + +async function getOwnedRoom(interaction, client) { + const channel = interaction.member?.voice?.channel; + if (!channel) return { error: '❌ Join your temporary voice room first.' }; + const info = await getTemporaryChannelInfo(client, interaction.guildId, channel.id); + if (!info) return { error: '❌ You are not inside a temporary voice room.' }; + if (info.ownerId !== interaction.user.id) return { error: '❌ Only the room owner can use this panel.' }; + return { channel, info }; +} + +function modal(id, title, label, placeholder) { + return new ModalBuilder().setCustomId(id).setTitle(title).addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder(placeholder).setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(100), + ), + ); +} + +const handlers = { + tempvoice_lock: async (i, c) => { + const room = await getOwnedRoom(i, c); if (room.error) return i.reply({ content: room.error, ephemeral: true }); + const config = await getJoinToCreateConfig(c, i.guildId); const info = config.temporaryChannels[room.channel.id]; + const locked = !info.locked; + info.locked = locked; + await room.channel.permissionOverwrites.edit(i.guildId, { Connect: !locked }); + await saveJoinToCreateConfig(c, i.guildId, config); + return i.reply({ content: locked ? '🔒 Room locked.' : '🔓 Room unlocked.', ephemeral: true }); + }, + tempvoice_hide: async (i, c) => { + const room = await getOwnedRoom(i, c); if (room.error) return i.reply({ content: room.error, ephemeral: true }); + const config = await getJoinToCreateConfig(c, i.guildId); const info = config.temporaryChannels[room.channel.id]; + const hidden = !info.hidden; + info.hidden = hidden; + await room.channel.permissionOverwrites.edit(i.guildId, { ViewChannel: !hidden }); + await saveJoinToCreateConfig(c, i.guildId, config); + return i.reply({ content: hidden ? '👁️ Room hidden.' : '👁️ Room visible.', ephemeral: true }); + }, + tempvoice_rename: async i => i.showModal(modal('tempvoice_rename_modal', 'Rename Room', 'New room name', "Abdallah's Room")), + tempvoice_limit: async i => i.showModal(modal('tempvoice_limit_modal', 'User Limit', 'Maximum users (0 = unlimited)', '0')), + tempvoice_kick: async i => i.showModal(modal('tempvoice_kick_modal', 'Kick User', 'User ID', '123456789012345678')), + tempvoice_mute: async i => i.showModal(modal('tempvoice_mute_modal', 'Mute User', 'User ID', '123456789012345678')), + tempvoice_transfer: async i => i.showModal(modal('tempvoice_transfer_modal', 'Transfer Ownership', 'New owner User ID', '123456789012345678')), + tempvoice_delete: async (i, c) => { + const room = await getOwnedRoom(i, c); if (room.error) return i.reply({ content: room.error, ephemeral: true }); + await room.channel.delete('TempVoice owner deleted room').catch(() => {}); + return i.reply({ content: '🗑️ Room deleted.', ephemeral: true }); + }, +}; + +export default Object.entries(handlers).map(([name, execute]) => ({ name, execute })); From 3e8ea0d259686e2a68f39e19f8e2fb3852726585 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:23:47 +0300 Subject: [PATCH 126/219] Add TempVoice control modal handlers --- src/interactions/modals/tempvoice.js | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/interactions/modals/tempvoice.js diff --git a/src/interactions/modals/tempvoice.js b/src/interactions/modals/tempvoice.js new file mode 100644 index 0000000000..09919a31f5 --- /dev/null +++ b/src/interactions/modals/tempvoice.js @@ -0,0 +1,69 @@ +import { getJoinToCreateConfig, saveJoinToCreateConfig, getTemporaryChannelInfo, unregisterTemporaryChannel } from '../../utils/database.js'; + +async function roomFor(interaction, client) { + const channel = interaction.member?.voice?.channel; + if (!channel) return { error: '❌ Join your temporary voice room first.' }; + const info = await getTemporaryChannelInfo(client, interaction.guildId, channel.id); + if (!info || info.ownerId !== interaction.user.id) return { error: '❌ Only the owner of a temporary room can use this panel.' }; + return { channel, info }; +} + +export default [ + { + name: 'tempvoice_rename_modal', + async execute(i, client) { + const room = await roomFor(i, client); if (room.error) return i.reply({ content: room.error, ephemeral: true }); + const name = i.fields.getTextInputValue('value').trim().replace(/[\r\n\t]/g, ' ').slice(0, 100); + if (!name) return i.reply({ content: '❌ Enter a valid room name.', ephemeral: true }); + await room.channel.setName(name); + return i.reply({ content: `✏️ Room renamed to **${name}**.`, ephemeral: true }); + }, + }, + { + name: 'tempvoice_limit_modal', + async execute(i, client) { + const room = await roomFor(i, client); if (room.error) return i.reply({ content: room.error, ephemeral: true }); + const limit = Number(i.fields.getTextInputValue('value')); + if (!Number.isInteger(limit) || limit < 0 || limit > 99) return i.reply({ content: '❌ Limit must be between 0 and 99.', ephemeral: true }); + await room.channel.setUserLimit(limit); + return i.reply({ content: `👥 User limit set to **${limit === 0 ? 'Unlimited' : limit}**.`, ephemeral: true }); + }, + }, + { + name: 'tempvoice_kick_modal', + async execute(i, client) { + const room = await roomFor(i, client); if (room.error) return i.reply({ content: room.error, ephemeral: true }); + const id = i.fields.getTextInputValue('value').trim(); + const member = await i.guild.members.fetch(id).catch(() => null); + if (!member || member.voice.channelId !== room.channel.id) return i.reply({ content: '❌ That user is not in your room.', ephemeral: true }); + await member.voice.disconnect('TempVoice owner kicked user'); + return i.reply({ content: `🚫 Kicked <@${member.id}> from the room.`, ephemeral: true }); + }, + }, + { + name: 'tempvoice_mute_modal', + async execute(i, client) { + const room = await roomFor(i, client); if (room.error) return i.reply({ content: room.error, ephemeral: true }); + const id = i.fields.getTextInputValue('value').trim(); + const member = await i.guild.members.fetch(id).catch(() => null); + if (!member || member.voice.channelId !== room.channel.id) return i.reply({ content: '❌ That user is not in your room.', ephemeral: true }); + await member.voice.setMute(!member.voice.serverMute, 'TempVoice owner toggled mute'); + return i.reply({ content: member.voice.serverMute ? `🔇 Muted <@${member.id}>.` : `🔊 Unmuted <@${member.id}>.`, ephemeral: true }); + }, + }, + { + name: 'tempvoice_transfer_modal', + async execute(i, client) { + const room = await roomFor(i, client); if (room.error) return i.reply({ content: room.error, ephemeral: true }); + const id = i.fields.getTextInputValue('value').trim(); + const member = await i.guild.members.fetch(id).catch(() => null); + if (!member || member.voice.channelId !== room.channel.id) return i.reply({ content: '❌ The new owner must be inside your room.', ephemeral: true }); + const config = await getJoinToCreateConfig(client, i.guildId); + const info = config.temporaryChannels[room.channel.id]; + info.ownerId = member.id; + await saveJoinToCreateConfig(client, i.guildId, config); + await room.channel.permissionOverwrites.edit(member.id, { Connect: true, Speak: true, MoveMembers: true, ManageChannels: true }).catch(() => {}); + return i.reply({ content: `👑 Ownership transferred to <@${member.id}>.`, ephemeral: true }); + }, + }, +]; From a11dd796d9afda45c3692c0a3d1b77b87ccf638e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:24:05 +0300 Subject: [PATCH 127/219] Fix TempVoice delete persistence --- src/interactions/buttons/tempvoice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interactions/buttons/tempvoice.js b/src/interactions/buttons/tempvoice.js index 32e6c8c155..3a2e6fec72 100644 --- a/src/interactions/buttons/tempvoice.js +++ b/src/interactions/buttons/tempvoice.js @@ -1,6 +1,5 @@ import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js'; -import { getJoinToCreateConfig, saveJoinToCreateConfig, getTemporaryChannelInfo } from '../../utils/database.js'; -import { getTempVoiceConfig } from '../../services/tempVoiceService.js'; +import { getJoinToCreateConfig, saveJoinToCreateConfig, getTemporaryChannelInfo, unregisterTemporaryChannel } from '../../utils/database.js'; async function getOwnedRoom(interaction, client) { const channel = interaction.member?.voice?.channel; @@ -45,6 +44,7 @@ const handlers = { tempvoice_transfer: async i => i.showModal(modal('tempvoice_transfer_modal', 'Transfer Ownership', 'New owner User ID', '123456789012345678')), tempvoice_delete: async (i, c) => { const room = await getOwnedRoom(i, c); if (room.error) return i.reply({ content: room.error, ephemeral: true }); + await unregisterTemporaryChannel(c, i.guildId, room.channel.id); await room.channel.delete('TempVoice owner deleted room').catch(() => {}); return i.reply({ content: '🗑️ Room deleted.', ephemeral: true }); }, From 83bc5e3fa76ba184999f904621edd5cd41350a8b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:24:19 +0300 Subject: [PATCH 128/219] Harden TempVoice setup and reset --- src/commands/JoinToCreate/tempvoice.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commands/JoinToCreate/tempvoice.js b/src/commands/JoinToCreate/tempvoice.js index 098e5bb73b..693bee380b 100644 --- a/src/commands/JoinToCreate/tempvoice.js +++ b/src/commands/JoinToCreate/tempvoice.js @@ -1,5 +1,5 @@ import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; -import { initializeJoinToCreate, getConfiguration } from '../../services/joinToCreateService.js'; +import { initializeJoinToCreate, getConfiguration, removeTriggerChannel } from '../../services/joinToCreateService.js'; import { getTempVoiceConfig, saveTempVoiceConfig } from '../../services/tempVoiceService.js'; export default { @@ -19,6 +19,9 @@ export default { if (subcommand === 'reset') { const jtc = await getConfiguration(client, guild.id).catch(() => null); for (const channelId of Object.keys(jtc?.temporaryChannels || {})) await guild.channels.delete(channelId).catch(() => {}); + if (jtc?.triggerChannels?.length) { + for (const triggerId of jtc.triggerChannels) await removeTriggerChannel(client, guild.id, triggerId).catch(() => {}); + } if (current.triggerChannelId) await guild.channels.delete(current.triggerChannelId).catch(() => {}); if (current.panelChannelId) await guild.channels.delete(current.panelChannelId).catch(() => {}); if (current.categoryId) await guild.channels.delete(current.categoryId).catch(() => {}); @@ -30,6 +33,11 @@ export default { return interaction.reply({ content: `⚠️ TempVoice is already set up: <#${current.triggerChannelId}>`, ephemeral: true }); } + const existingJtc = await getConfiguration(client, guild.id); + if (existingJtc?.triggerChannels?.length) { + return interaction.reply({ content: `⚠️ This server already has a Join to Create system: <#${existingJtc.triggerChannels[0]}>. Reset it first with \`/tempvoice reset\`.`, ephemeral: true }); + } + await interaction.deferReply({ ephemeral: true }); const category = await guild.channels.create({ name: 'Temporary Voice', type: ChannelType.GuildCategory }); From 15a0afeba3644236f20eb779d53fc4f31e1d2d05 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:24:28 +0300 Subject: [PATCH 129/219] Keep TempVoice owner visible when hiding room --- src/interactions/buttons/tempvoice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interactions/buttons/tempvoice.js b/src/interactions/buttons/tempvoice.js index 3a2e6fec72..4f019fc5bb 100644 --- a/src/interactions/buttons/tempvoice.js +++ b/src/interactions/buttons/tempvoice.js @@ -34,6 +34,7 @@ const handlers = { const hidden = !info.hidden; info.hidden = hidden; await room.channel.permissionOverwrites.edit(i.guildId, { ViewChannel: !hidden }); + await room.channel.permissionOverwrites.edit(i.user.id, { ViewChannel: true, Connect: true, Speak: true, MoveMembers: true, ManageChannels: true }); await saveJoinToCreateConfig(c, i.guildId, config); return i.reply({ content: hidden ? '👁️ Room hidden.' : '👁️ Room visible.', ephemeral: true }); }, From 1c14cd15b540f567207c2e6ee5cd5452f37ff902 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 18:24:36 +0300 Subject: [PATCH 130/219] Harden TempVoice ownership transfer and modal controls --- src/interactions/modals/tempvoice.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/interactions/modals/tempvoice.js b/src/interactions/modals/tempvoice.js index 09919a31f5..a0c3e26595 100644 --- a/src/interactions/modals/tempvoice.js +++ b/src/interactions/modals/tempvoice.js @@ -1,4 +1,4 @@ -import { getJoinToCreateConfig, saveJoinToCreateConfig, getTemporaryChannelInfo, unregisterTemporaryChannel } from '../../utils/database.js'; +import { getJoinToCreateConfig, saveJoinToCreateConfig, getTemporaryChannelInfo } from '../../utils/database.js'; async function roomFor(interaction, client) { const channel = interaction.member?.voice?.channel; @@ -47,8 +47,9 @@ export default [ const id = i.fields.getTextInputValue('value').trim(); const member = await i.guild.members.fetch(id).catch(() => null); if (!member || member.voice.channelId !== room.channel.id) return i.reply({ content: '❌ That user is not in your room.', ephemeral: true }); - await member.voice.setMute(!member.voice.serverMute, 'TempVoice owner toggled mute'); - return i.reply({ content: member.voice.serverMute ? `🔇 Muted <@${member.id}>.` : `🔊 Unmuted <@${member.id}>.`, ephemeral: true }); + const nextMuted = !member.voice.serverMute; + await member.voice.setMute(nextMuted, 'TempVoice owner toggled mute'); + return i.reply({ content: nextMuted ? `🔇 Muted <@${member.id}>.` : `🔊 Unmuted <@${member.id}>.`, ephemeral: true }); }, }, { @@ -60,9 +61,13 @@ export default [ if (!member || member.voice.channelId !== room.channel.id) return i.reply({ content: '❌ The new owner must be inside your room.', ephemeral: true }); const config = await getJoinToCreateConfig(client, i.guildId); const info = config.temporaryChannels[room.channel.id]; + const oldOwnerId = info.ownerId; info.ownerId = member.id; await saveJoinToCreateConfig(client, i.guildId, config); await room.channel.permissionOverwrites.edit(member.id, { Connect: true, Speak: true, MoveMembers: true, ManageChannels: true }).catch(() => {}); + if (oldOwnerId !== member.id) { + await room.channel.permissionOverwrites.edit(oldOwnerId, { Connect: true, Speak: true, MoveMembers: false, ManageChannels: false }).catch(() => {}); + } return i.reply({ content: `👑 Ownership transferred to <@${member.id}>.`, ephemeral: true }); }, }, From 657cce49808bbe7bdccce909dca06c527f9f57fe Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:27:13 +0300 Subject: [PATCH 131/219] feat: add suggestions system command --- src/commands/Utility/suggestions.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/commands/Utility/suggestions.js diff --git a/src/commands/Utility/suggestions.js b/src/commands/Utility/suggestions.js new file mode 100644 index 0000000000..118e05df65 --- /dev/null +++ b/src/commands/Utility/suggestions.js @@ -0,0 +1,26 @@ +import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { setupSuggestions } from '../../utils/suggestions.js'; + +export default { + data: new SlashCommandBuilder() + .setName('suggestions') + .setDescription('Manage the server suggestions system') + .addSubcommand(sub => sub + .setName('setup') + .setDescription('Create the suggestions channel and panel')) + .addSubcommand(sub => sub + .setName('dashboard') + .setDescription('Open the suggestions management dashboard')) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild), + + async execute(interaction) { + const subcommand = interaction.options.getSubcommand(); + if (subcommand === 'setup') { + return setupSuggestions(interaction); + } + return interaction.reply({ + content: 'The suggestions dashboard will be available after setup.', + ephemeral: true, + }); + }, +}; From d63a5bb80a0bc43281a1054f71918a0b16f31a64 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:27:32 +0300 Subject: [PATCH 132/219] feat: add suggestions storage and setup --- src/utils/suggestions.js | 75 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/utils/suggestions.js diff --git a/src/utils/suggestions.js b/src/utils/suggestions.js new file mode 100644 index 0000000000..038ae72ae9 --- /dev/null +++ b/src/utils/suggestions.js @@ -0,0 +1,75 @@ +import { ChannelType, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; + +const key = guildId => `suggestions:${guildId}`; + +async function load(client, guildId) { + const data = await client.db.get(key(guildId), {}); + return data && typeof data === 'object' ? data : {}; +} + +async function save(client, guildId, data) { + await client.db.set(key(guildId), data); +} + +export async function setupSuggestions(interaction) { + const guild = interaction.guild; + const existing = await load(interaction.client, guild.id); + let channel = existing.channelId ? guild.channels.cache.get(existing.channelId) : null; + if (!channel) { + channel = await guild.channels.create({ + name: 'suggestions', + type: ChannelType.GuildText, + reason: 'Suggestions system setup', + permissionOverwrites: [ + { id: guild.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }, + ], + }); + } + const embed = new EmbedBuilder() + .setTitle('💡 Suggestions') + .setDescription('Have an idea for the server? Submit it below and let the community vote.') + .addFields({ name: 'How it works', value: 'Click **Submit Suggestion**, write your idea, then the community can vote on it.' }); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('suggestions_submit').setLabel('Submit Suggestion').setEmoji('💡').setStyle(ButtonStyle.Primary), + ); + if (existing.panelMessageId) { + const old = await channel.messages.fetch(existing.panelMessageId).catch(() => null); + if (old) await old.edit({ embeds: [embed], components: [row] }); + else existing.panelMessageId = null; + } + if (!existing.panelMessageId) { + const message = await channel.send({ embeds: [embed], components: [row] }); + existing.panelMessageId = message.id; + } + existing.channelId = channel.id; + existing.counter = Number(existing.counter || 0); + await save(interaction.client, guild.id, existing); + return interaction.reply({ content: `✅ Suggestions system is ready in ${channel}.`, ephemeral: true }); +} + +export async function getSuggestions(client, guildId) { return load(client, guildId); } +export async function saveSuggestions(client, guildId, data) { return save(client, guildId, data); } +export async function nextSuggestionId(client, guildId) { + const data = await load(client, guildId); + data.counter = Number(data.counter || 0) + 1; + await save(client, guildId, data); + return data.counter; +} + +export function suggestionEmbed(s) { + const status = { pending: '🟡 Pending', accepted: '🟢 Accepted', rejected: '🔴 Rejected', considered: '🔵 Considered', closed: '⚫ Closed' }[s.status] || '🟡 Pending'; + return new EmbedBuilder().setTitle(`💡 Suggestion #${s.id}`).setDescription(s.text).addFields( + { name: 'Author', value: `<@${s.authorId}>`, inline: true }, + { name: 'Status', value: status, inline: true }, + { name: 'Votes', value: `👍 ${s.upvotes.length} • 👎 ${s.downvotes.length}`, inline: true }, + ).setTimestamp(new Date(s.createdAt)); +} + +export function suggestionButtons(s) { + return [new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`suggestions_up:${s.id}`).setLabel(`👍 ${s.upvotes.length}`).setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId(`suggestions_down:${s.id}`).setLabel(`👎 ${s.downvotes.length}`).setStyle(ButtonStyle.Danger), + new ButtonBuilder().setCustomId(`suggestions_accept:${s.id}`).setLabel('Accept').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId(`suggestions_reject:${s.id}`).setLabel('Reject').setStyle(ButtonStyle.Danger), + )]; +} From 7bb2638b8f20ddcbd8d2cc96dc7bad52f08c8023 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:27:43 +0300 Subject: [PATCH 133/219] feat: add suggestion panel buttons --- src/interactions/buttons/suggestions.js | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/interactions/buttons/suggestions.js diff --git a/src/interactions/buttons/suggestions.js b/src/interactions/buttons/suggestions.js new file mode 100644 index 0000000000..ec3255c85c --- /dev/null +++ b/src/interactions/buttons/suggestions.js @@ -0,0 +1,52 @@ +import { ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle, PermissionFlagsBits } from 'discord.js'; +import { getSuggestions, saveSuggestions, nextSuggestionId, suggestionEmbed, suggestionButtons } from '../../utils/suggestions.js'; + +const submitModal = new ModalBuilder().setCustomId('suggestions_submit_modal').setTitle('Submit Suggestion').addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder().setCustomId('suggestion').setLabel('Your suggestion').setPlaceholder('Describe your idea...').setStyle(TextInputStyle.Paragraph).setRequired(true).setMaxLength(1500), + ), +); + +async function getSuggestion(interaction, id) { + const data = await getSuggestions(interaction.client, interaction.guildId); + const suggestion = data.items?.find(item => String(item.id) === String(id)); + return { data, suggestion }; +} + +async function refresh(interaction, suggestion) { + const message = await interaction.channel.messages.fetch(suggestion.messageId).catch(() => null); + if (message) await message.edit({ embeds: [suggestionEmbed(suggestion)], components: suggestionButtons(suggestion) }); +} + +export default [ + { name: 'suggestions_submit', async execute(interaction) { await interaction.showModal(submitModal); } }, + { name: 'suggestions_up', async execute(interaction, client, args) { + const { data, suggestion } = await getSuggestion(interaction, args[0]); + if (!suggestion) return interaction.reply({ content: '❌ Suggestion not found.', ephemeral: true }); + if (suggestion.authorId === interaction.user.id) return interaction.reply({ content: '❌ You cannot vote on your own suggestion.', ephemeral: true }); + suggestion.downvotes = suggestion.downvotes.filter(id => id !== interaction.user.id); + if (suggestion.upvotes.includes(interaction.user.id)) suggestion.upvotes = suggestion.upvotes.filter(id => id !== interaction.user.id); + else suggestion.upvotes.push(interaction.user.id); + await saveSuggestions(client, interaction.guildId, data); await refresh(interaction, suggestion); + return interaction.deferUpdate(); + } }, + { name: 'suggestions_down', async execute(interaction, client, args) { + const { data, suggestion } = await getSuggestion(interaction, args[0]); + if (!suggestion) return interaction.reply({ content: '❌ Suggestion not found.', ephemeral: true }); + if (suggestion.authorId === interaction.user.id) return interaction.reply({ content: '❌ You cannot vote on your own suggestion.', ephemeral: true }); + suggestion.upvotes = suggestion.upvotes.filter(id => id !== interaction.user.id); + if (suggestion.downvotes.includes(interaction.user.id)) suggestion.downvotes = suggestion.downvotes.filter(id => id !== interaction.user.id); + else suggestion.downvotes.push(interaction.user.id); + await saveSuggestions(client, interaction.guildId, data); await refresh(interaction, suggestion); + return interaction.deferUpdate(); + } }, + ...['accept', 'reject'].map(action => ({ name: `suggestions_${action}`, async execute(interaction, client, args) { + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return interaction.reply({ content: '❌ You need Manage Server permission.', ephemeral: true }); + const { data, suggestion } = await getSuggestion(interaction, args[0]); + if (!suggestion) return interaction.reply({ content: '❌ Suggestion not found.', ephemeral: true }); + suggestion.status = action === 'accept' ? 'accepted' : 'rejected'; + suggestion.moderatorId = interaction.user.id; + await saveSuggestions(client, interaction.guildId, data); await refresh(interaction, suggestion); + return interaction.deferUpdate(); + } })), +]; From d64256ac39fb4538e273bcce2af2c3a4066e2658 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:27:49 +0300 Subject: [PATCH 134/219] feat: add suggestion submission modal --- src/interactions/modals/suggestions.js | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/interactions/modals/suggestions.js diff --git a/src/interactions/modals/suggestions.js b/src/interactions/modals/suggestions.js new file mode 100644 index 0000000000..54ca0b366b --- /dev/null +++ b/src/interactions/modals/suggestions.js @@ -0,0 +1,33 @@ +import { getSuggestions, saveSuggestions, nextSuggestionId, suggestionEmbed, suggestionButtons } from '../../utils/suggestions.js'; + +export default { + name: 'suggestions_submit_modal', + async execute(interaction, client) { + const text = interaction.fields.getTextInputValue('suggestion').trim(); + if (!text) return interaction.reply({ content: '❌ Suggestion cannot be empty.', ephemeral: true }); + + const data = await getSuggestions(client, interaction.guildId); + const id = await nextSuggestionId(client, interaction.guildId); + const suggestion = { + id, + authorId: interaction.user.id, + text, + status: 'pending', + upvotes: [], + downvotes: [], + createdAt: new Date().toISOString(), + messageId: null, + }; + + const channel = interaction.guild.channels.cache.get(data.channelId); + if (!channel) return interaction.reply({ content: '❌ Suggestions channel is missing. Run `/suggestions setup` again.', ephemeral: true }); + + const message = await channel.send({ embeds: [suggestionEmbed(suggestion)], components: suggestionButtons(suggestion) }); + suggestion.messageId = message.id; + data.items = Array.isArray(data.items) ? data.items : []; + data.items.push(suggestion); + await saveSuggestions(client, interaction.guildId, data); + + return interaction.reply({ content: `✅ Suggestion #${id} submitted.`, ephemeral: true }); + }, +}; From 9bae1ebd21200e2359a79925bb0d729f6c52823d Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:27:58 +0300 Subject: [PATCH 135/219] fix: acknowledge suggestion buttons before updates --- src/interactions/buttons/suggestions.js | 51 +++++++++++++------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/interactions/buttons/suggestions.js b/src/interactions/buttons/suggestions.js index ec3255c85c..57dd9fddf9 100644 --- a/src/interactions/buttons/suggestions.js +++ b/src/interactions/buttons/suggestions.js @@ -1,5 +1,5 @@ import { ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle, PermissionFlagsBits } from 'discord.js'; -import { getSuggestions, saveSuggestions, nextSuggestionId, suggestionEmbed, suggestionButtons } from '../../utils/suggestions.js'; +import { getSuggestions, saveSuggestions, suggestionEmbed, suggestionButtons } from '../../utils/suggestions.js'; const submitModal = new ModalBuilder().setCustomId('suggestions_submit_modal').setTitle('Submit Suggestion').addComponents( new ActionRowBuilder().addComponents( @@ -18,35 +18,36 @@ async function refresh(interaction, suggestion) { if (message) await message.edit({ embeds: [suggestionEmbed(suggestion)], components: suggestionButtons(suggestion) }); } -export default [ - { name: 'suggestions_submit', async execute(interaction) { await interaction.showModal(submitModal); } }, - { name: 'suggestions_up', async execute(interaction, client, args) { - const { data, suggestion } = await getSuggestion(interaction, args[0]); - if (!suggestion) return interaction.reply({ content: '❌ Suggestion not found.', ephemeral: true }); - if (suggestion.authorId === interaction.user.id) return interaction.reply({ content: '❌ You cannot vote on your own suggestion.', ephemeral: true }); +async function vote(interaction, client, args, direction) { + await interaction.deferUpdate(); + const { data, suggestion } = await getSuggestion(interaction, args[0]); + if (!suggestion) return interaction.followUp({ content: '❌ Suggestion not found.', ephemeral: true }); + if (suggestion.authorId === interaction.user.id) return interaction.followUp({ content: '❌ You cannot vote on your own suggestion.', ephemeral: true }); + suggestion.upvotes = Array.isArray(suggestion.upvotes) ? suggestion.upvotes : []; + suggestion.downvotes = Array.isArray(suggestion.downvotes) ? suggestion.downvotes : []; + if (direction === 'up') { suggestion.downvotes = suggestion.downvotes.filter(id => id !== interaction.user.id); - if (suggestion.upvotes.includes(interaction.user.id)) suggestion.upvotes = suggestion.upvotes.filter(id => id !== interaction.user.id); - else suggestion.upvotes.push(interaction.user.id); - await saveSuggestions(client, interaction.guildId, data); await refresh(interaction, suggestion); - return interaction.deferUpdate(); - } }, - { name: 'suggestions_down', async execute(interaction, client, args) { - const { data, suggestion } = await getSuggestion(interaction, args[0]); - if (!suggestion) return interaction.reply({ content: '❌ Suggestion not found.', ephemeral: true }); - if (suggestion.authorId === interaction.user.id) return interaction.reply({ content: '❌ You cannot vote on your own suggestion.', ephemeral: true }); + suggestion.upvotes = suggestion.upvotes.includes(interaction.user.id) ? suggestion.upvotes.filter(id => id !== interaction.user.id) : [...suggestion.upvotes, interaction.user.id]; + } else { suggestion.upvotes = suggestion.upvotes.filter(id => id !== interaction.user.id); - if (suggestion.downvotes.includes(interaction.user.id)) suggestion.downvotes = suggestion.downvotes.filter(id => id !== interaction.user.id); - else suggestion.downvotes.push(interaction.user.id); - await saveSuggestions(client, interaction.guildId, data); await refresh(interaction, suggestion); - return interaction.deferUpdate(); - } }, + suggestion.downvotes = suggestion.downvotes.includes(interaction.user.id) ? suggestion.downvotes.filter(id => id !== interaction.user.id) : [...suggestion.downvotes, interaction.user.id]; + } + await saveSuggestions(client, interaction.guildId, data); + await refresh(interaction, suggestion); +} + +export default [ + { name: 'suggestions_submit', async execute(interaction) { await interaction.showModal(submitModal); } }, + { name: 'suggestions_up', async execute(interaction, client, args) { await vote(interaction, client, args, 'up'); } }, + { name: 'suggestions_down', async execute(interaction, client, args) { await vote(interaction, client, args, 'down'); } }, ...['accept', 'reject'].map(action => ({ name: `suggestions_${action}`, async execute(interaction, client, args) { - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return interaction.reply({ content: '❌ You need Manage Server permission.', ephemeral: true }); + await interaction.deferUpdate(); + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return interaction.followUp({ content: '❌ You need Manage Server permission.', ephemeral: true }); const { data, suggestion } = await getSuggestion(interaction, args[0]); - if (!suggestion) return interaction.reply({ content: '❌ Suggestion not found.', ephemeral: true }); + if (!suggestion) return interaction.followUp({ content: '❌ Suggestion not found.', ephemeral: true }); suggestion.status = action === 'accept' ? 'accepted' : 'rejected'; suggestion.moderatorId = interaction.user.id; - await saveSuggestions(client, interaction.guildId, data); await refresh(interaction, suggestion); - return interaction.deferUpdate(); + await saveSuggestions(client, interaction.guildId, data); + await refresh(interaction, suggestion); } })), ]; From 61541cdf716bfff2d168472629730b6f66c0baa2 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:28:06 +0300 Subject: [PATCH 136/219] fix: persist suggestion counter with submissions --- src/interactions/modals/suggestions.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/interactions/modals/suggestions.js b/src/interactions/modals/suggestions.js index 54ca0b366b..1d826bae38 100644 --- a/src/interactions/modals/suggestions.js +++ b/src/interactions/modals/suggestions.js @@ -8,6 +8,10 @@ export default { const data = await getSuggestions(client, interaction.guildId); const id = await nextSuggestionId(client, interaction.guildId); + data.counter = Math.max(Number(data.counter || 0), id); + const channel = interaction.guild.channels.cache.get(data.channelId); + if (!channel) return interaction.reply({ content: '❌ Suggestions channel is missing. Run `/suggestions setup` again.', ephemeral: true }); + const suggestion = { id, authorId: interaction.user.id, @@ -19,9 +23,6 @@ export default { messageId: null, }; - const channel = interaction.guild.channels.cache.get(data.channelId); - if (!channel) return interaction.reply({ content: '❌ Suggestions channel is missing. Run `/suggestions setup` again.', ephemeral: true }); - const message = await channel.send({ embeds: [suggestionEmbed(suggestion)], components: suggestionButtons(suggestion) }); suggestion.messageId = message.id; data.items = Array.isArray(data.items) ? data.items : []; From 4d870cacfbcf7f65cccb296dea17cf2ea9fddaf0 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:54:54 +0300 Subject: [PATCH 137/219] feat: add partner system command --- src/commands/Utility/partner.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/commands/Utility/partner.js diff --git a/src/commands/Utility/partner.js b/src/commands/Utility/partner.js new file mode 100644 index 0000000000..b7521abc7d --- /dev/null +++ b/src/commands/Utility/partner.js @@ -0,0 +1,24 @@ +import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { setupPartnerPanel } from '../../utils/partner.js'; + +export default { + data: new SlashCommandBuilder() + .setName('partner') + .setDescription('Manage the server partnership system') + .addSubcommand(sub => sub + .setName('setup') + .setDescription('Create the partnership panel')) + .addSubcommand(sub => sub + .setName('dashboard') + .setDescription('Open the partnership management dashboard')) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild), + + async execute(interaction) { + const subcommand = interaction.options.getSubcommand(); + if (subcommand === 'setup') return setupPartnerPanel(interaction); + return interaction.reply({ + content: 'The partnership dashboard will be available after setup.', + ephemeral: true, + }); + }, +}; From e92eb3f7b652fc3d7595f171c3ba9fa8adb87710 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:55:00 +0300 Subject: [PATCH 138/219] feat: add partnership panel foundation --- src/utils/partner.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/utils/partner.js diff --git a/src/utils/partner.js b/src/utils/partner.js new file mode 100644 index 0000000000..2b00ed612b --- /dev/null +++ b/src/utils/partner.js @@ -0,0 +1,35 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; + +export async function setupPartnerPanel(interaction) { + const guild = interaction.guild; + if (!guild) return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); + + await interaction.deferReply({ ephemeral: true }); + + let channel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnerships'); + if (!channel) { + channel = await guild.channels.create({ + name: 'partnerships', + type: ChannelType.GuildText, + reason: 'Partner system setup', + permissionOverwrites: [ + { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.SendMessages] }, + ], + }); + } + + const embed = new EmbedBuilder() + .setColor(0x5865f2) + .setTitle('🤝 Server Partnerships') + .setDescription('Interested in partnering with this server?\n\nReview the requirements and submit your partnership request using the buttons below.') + .addFields({ name: 'Requirements', value: '• Active community\n• Valid invite link\n• No recent serious violations\n• Server must meet the configured requirements' }) + .setFooter({ text: `${guild.name} • Partnership System` }); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('partner_apply').setLabel('Apply for Partnership').setEmoji('🤝').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('partner_requirements').setLabel('Requirements').setEmoji('📋').setStyle(ButtonStyle.Secondary), + ); + + await channel.send({ embeds: [embed], components: [row] }); + return interaction.editReply({ content: `Partnership panel is ready in ${channel}.` }); +} From fc8251ad704e889d449c7c39f17a8f8a11eb6eb4 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:56:42 +0300 Subject: [PATCH 139/219] feat: add partner button interactions --- src/interactions/buttons/partner.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/interactions/buttons/partner.js diff --git a/src/interactions/buttons/partner.js b/src/interactions/buttons/partner.js new file mode 100644 index 0000000000..1005e3c8f9 --- /dev/null +++ b/src/interactions/buttons/partner.js @@ -0,0 +1,27 @@ +import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, EmbedBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; + +const ids = new Set(['partner_apply','partner_requirements','partner_accept','partner_reject']); + +export default { + name: 'partner', + ids, + async execute(interaction) { + const id = interaction.customId; + if (id === 'partner_apply') { + const modal = new ModalBuilder().setCustomId('partner_apply_modal').setTitle('Partnership Request'); + const server = new TextInputBuilder().setCustomId('server_name').setLabel('Server name').setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(100); + const invite = new TextInputBuilder().setCustomId('invite').setLabel('Invite link').setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(200); + const members = new TextInputBuilder().setCustomId('members').setLabel('Member count').setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(10); + const description = new TextInputBuilder().setCustomId('description').setLabel('Server description').setStyle(TextInputStyle.Paragraph).setRequired(true).setMaxLength(1000); + modal.addComponents(...[server, invite, members, description].map(x => new ActionRowBuilder().addComponents(x))); + return interaction.showModal(modal); + } + if (id === 'partner_requirements') return interaction.reply({ content: 'Partnership requirements: active community, valid invite, no recent serious violations, and the server must meet the configured member/activity requirements.', ephemeral: true }); + if (id === 'partner_accept' || id === 'partner_reject') { + if (!interaction.memberPermissions?.has('ManageGuild')) return interaction.reply({ content: 'You do not have permission to manage partnerships.', ephemeral: true }); + const accepted = id === 'partner_accept'; + const embed = EmbedBuilder.from(interaction.message.embeds[0] || new EmbedBuilder()).setColor(accepted ? 0x57f287 : 0xed4245).addFields({ name: 'Status', value: accepted ? '🟢 Accepted' : '🔴 Rejected' }, { name: 'Reviewed by', value: `${interaction.user}` }); + return interaction.update({ embeds: [embed], components: [] }); + } + }, +}; From 018eb19d321af350269b1a9431f4d03fd4abc1a2 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:56:49 +0300 Subject: [PATCH 140/219] feat: add partnership application modal --- src/interactions/modals/partnerApply.js | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/interactions/modals/partnerApply.js diff --git a/src/interactions/modals/partnerApply.js b/src/interactions/modals/partnerApply.js new file mode 100644 index 0000000000..f2252bf0da --- /dev/null +++ b/src/interactions/modals/partnerApply.js @@ -0,0 +1,30 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; + +export default { + name: 'partner_apply_modal', + async execute(interaction) { + const server = interaction.fields.getTextInputValue('server_name'); + const invite = interaction.fields.getTextInputValue('invite'); + const members = interaction.fields.getTextInputValue('members'); + const description = interaction.fields.getTextInputValue('description'); + const embed = new EmbedBuilder() + .setColor(0x5865f2) + .setTitle('🤝 Partnership Request') + .addFields( + { name: 'Server', value: server }, + { name: 'Members', value: members }, + { name: 'Invite', value: invite }, + { name: 'Applicant', value: `${interaction.user}` }, + { name: 'Description', value: description }, + ) + .setFooter({ text: 'Status: Pending' }); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('partner_accept').setLabel('Accept').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId('partner_reject').setLabel('Reject').setStyle(ButtonStyle.Danger), + ); + const target = interaction.guild.channels.cache.find(c => c.isTextBased() && c.name === 'partnership-requests'); + if (!target) return interaction.reply({ content: 'The partnership request channel is not configured. Ask an administrator to run `/partner setup` again.', ephemeral: true }); + await target.send({ embeds: [embed], components: [row] }); + return interaction.reply({ content: 'Your partnership request has been submitted.', ephemeral: true }); + }, +}; From 43dd9c5e90b26ef9e9144fc8c98dbcf1e19624ee Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:57:50 +0300 Subject: [PATCH 141/219] fix: create dedicated partnership request channel --- src/utils/partner.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/utils/partner.js b/src/utils/partner.js index 2b00ed612b..9652837fd3 100644 --- a/src/utils/partner.js +++ b/src/utils/partner.js @@ -18,11 +18,23 @@ export async function setupPartnerPanel(interaction) { }); } + let requestChannel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnership-requests'); + if (!requestChannel) { + requestChannel = await guild.channels.create({ + name: 'partnership-requests', + type: ChannelType.GuildText, + reason: 'Partner system request channel', + permissionOverwrites: [ + { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.SendMessages] }, + ], + }); + } + const embed = new EmbedBuilder() .setColor(0x5865f2) .setTitle('🤝 Server Partnerships') .setDescription('Interested in partnering with this server?\n\nReview the requirements and submit your partnership request using the buttons below.') - .addFields({ name: 'Requirements', value: '• Active community\n• Valid invite link\n• No recent serious violations\n• Server must meet the configured requirements' }) + .addFields({ name: 'Requirements', value: '• Active community\n• Valid invite link\n• No recent serious violations\n• Server must meet the configured member/activity requirements' }) .setFooter({ text: `${guild.name} • Partnership System` }); const row = new ActionRowBuilder().addComponents( @@ -31,5 +43,5 @@ export async function setupPartnerPanel(interaction) { ); await channel.send({ embeds: [embed], components: [row] }); - return interaction.editReply({ content: `Partnership panel is ready in ${channel}.` }); + return interaction.editReply({ content: `Partnership panel is ready in ${channel}.\nRequests: ${requestChannel}` }); } From 1eaac76802883da2b95911316425e7d405420bfa Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:58:53 +0300 Subject: [PATCH 142/219] feat: complete partner command dashboard --- src/commands/Utility/partner.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/commands/Utility/partner.js b/src/commands/Utility/partner.js index b7521abc7d..2adcc2ccd6 100644 --- a/src/commands/Utility/partner.js +++ b/src/commands/Utility/partner.js @@ -1,24 +1,17 @@ import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; -import { setupPartnerPanel } from '../../utils/partner.js'; +import { setupPartnerPanel, partnerDashboard } from '../../utils/partner.js'; export default { data: new SlashCommandBuilder() .setName('partner') .setDescription('Manage the server partnership system') - .addSubcommand(sub => sub - .setName('setup') - .setDescription('Create the partnership panel')) - .addSubcommand(sub => sub - .setName('dashboard') - .setDescription('Open the partnership management dashboard')) + .addSubcommand(sub => sub.setName('setup').setDescription('Create or repair the partnership system')) + .addSubcommand(sub => sub.setName('dashboard').setDescription('Open the partnership management dashboard')) .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild), async execute(interaction) { const subcommand = interaction.options.getSubcommand(); if (subcommand === 'setup') return setupPartnerPanel(interaction); - return interaction.reply({ - content: 'The partnership dashboard will be available after setup.', - ephemeral: true, - }); + return partnerDashboard(interaction); }, }; From 57a54f69f6123ff1e7a98da3d4fca0d09f9c23e7 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:59:04 +0300 Subject: [PATCH 143/219] feat: complete persistent partnership system --- src/utils/partner.js | 121 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 24 deletions(-) diff --git a/src/utils/partner.js b/src/utils/partner.js index 9652837fd3..96c1ab4103 100644 --- a/src/utils/partner.js +++ b/src/utils/partner.js @@ -1,47 +1,120 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; +const key = guildId => `partners:${guildId}`; + +async function load(client, guildId) { + const data = await client.db.get(key(guildId), {}); + return data && typeof data === 'object' ? data : {}; +} + +async function save(client, guildId, data) { + await client.db.set(key(guildId), data); +} + +export async function getPartnerData(client, guildId) { + const data = await load(client, guildId); + data.counter = Number(data.counter || 0); + data.applications = Array.isArray(data.applications) ? data.applications : []; + data.partners = Array.isArray(data.partners) ? data.partners : []; + data.requirements = { + minMembers: Number(data.requirements?.minMembers ?? 500), + minAccountAgeDays: Number(data.requirements?.minAccountAgeDays ?? 0), + requireInvite: data.requirements?.requireInvite !== false, + requireActive: data.requirements?.requireActive !== false, + }; + return data; +} + +export async function savePartnerData(client, guildId, data) { + await save(client, guildId, data); +} + export async function setupPartnerPanel(interaction) { const guild = interaction.guild; if (!guild) return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); - await interaction.deferReply({ ephemeral: true }); + const data = await getPartnerData(interaction.client, guild.id); - let channel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnerships'); - if (!channel) { - channel = await guild.channels.create({ - name: 'partnerships', - type: ChannelType.GuildText, - reason: 'Partner system setup', - permissionOverwrites: [ - { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.SendMessages] }, - ], + let panelChannel = data.panelChannelId ? guild.channels.cache.get(data.panelChannelId) : null; + if (!panelChannel) panelChannel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnerships'); + if (!panelChannel) { + panelChannel = await guild.channels.create({ + name: 'partnerships', type: ChannelType.GuildText, reason: 'Partner system setup', + permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }], }); } - let requestChannel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnership-requests'); + let requestChannel = data.requestChannelId ? guild.channels.cache.get(data.requestChannelId) : null; + if (!requestChannel) requestChannel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnership-requests'); if (!requestChannel) { requestChannel = await guild.channels.create({ - name: 'partnership-requests', - type: ChannelType.GuildText, - reason: 'Partner system request channel', - permissionOverwrites: [ - { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.SendMessages] }, - ], + name: 'partnership-requests', type: ChannelType.GuildText, reason: 'Partner system request channel', + permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }], }); } const embed = new EmbedBuilder() - .setColor(0x5865f2) - .setTitle('🤝 Server Partnerships') - .setDescription('Interested in partnering with this server?\n\nReview the requirements and submit your partnership request using the buttons below.') - .addFields({ name: 'Requirements', value: '• Active community\n• Valid invite link\n• No recent serious violations\n• Server must meet the configured member/activity requirements' }) + .setColor(0x5865f2).setTitle('🤝 Server Partnerships') + .setDescription('Interested in partnering with this server? Review the requirements and submit a request below.') + .addFields({ name: 'Requirements', value: `• ${data.requirements.minMembers}+ members\n• ${data.requirements.requireInvite ? 'Valid invite link' : 'Invite link optional'}\n• ${data.requirements.requireActive ? 'Active community' : 'Activity check disabled'}\n• No recent serious violations` }) .setFooter({ text: `${guild.name} • Partnership System` }); - const row = new ActionRowBuilder().addComponents( new ButtonBuilder().setCustomId('partner_apply').setLabel('Apply for Partnership').setEmoji('🤝').setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId('partner_requirements').setLabel('Requirements').setEmoji('📋').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('partner_list').setLabel('Partners').setEmoji('🤝').setStyle(ButtonStyle.Secondary), + ); + + let panelMessage = data.panelMessageId ? await panelChannel.messages.fetch(data.panelMessageId).catch(() => null) : null; + if (panelMessage) await panelMessage.edit({ embeds: [embed], components: [row] }); + else panelMessage = await panelChannel.send({ embeds: [embed], components: [row] }); + + data.panelChannelId = panelChannel.id; + data.panelMessageId = panelMessage.id; + data.requestChannelId = requestChannel.id; + await savePartnerData(interaction.client, guild.id, data); + return interaction.editReply({ content: `✅ Partner system is ready.\nPanel: ${panelChannel}\nRequests: ${requestChannel}` }); +} + +export async function partnerDashboard(interaction) { + const data = await getPartnerData(interaction.client, interaction.guildId); + const pending = data.applications.filter(a => a.status === 'pending').length; + const active = data.partners.filter(p => p.status === 'active').length; + const embed = new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 Partnership Management') + .setDescription('Manage partnership requests and active partners from this dashboard.') + .addFields( + { name: 'Active Partners', value: String(active), inline: true }, + { name: 'Pending Requests', value: String(pending), inline: true }, + { name: 'Total Applications', value: String(data.applications.length), inline: true }, + { name: 'Requirements', value: `${data.requirements.minMembers}+ members • Invite ${data.requirements.requireInvite ? 'required' : 'optional'}` }, + ); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('partner_pending').setLabel('Pending').setEmoji('🟡').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('partner_active').setLabel('Partners').setEmoji('🤝').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId('partner_stats').setLabel('Statistics').setEmoji('📊').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('partner_settings').setLabel('Settings').setEmoji('⚙️').setStyle(ButtonStyle.Secondary), ); + return interaction.reply({ embeds: [embed], components: [row], ephemeral: true }); +} + +export function applicationEmbed(app, guild) { + const status = app.status === 'accepted' ? '🟢 Accepted' : app.status === 'rejected' ? '🔴 Rejected' : '🟡 Pending'; + return new EmbedBuilder().setColor(app.status === 'accepted' ? 0x57f287 : app.status === 'rejected' ? 0xed4245 : 0x5865f2) + .setTitle(`🤝 Partnership Request #${app.id}`).addFields( + { name: 'Server', value: app.serverName, inline: true }, + { name: 'Members', value: String(app.members), inline: true }, + { name: 'Status', value: status, inline: true }, + { name: 'Invite', value: app.invite, inline: false }, + { name: 'Applicant', value: `<@${app.applicantId}>`, inline: true }, + { name: 'Description', value: app.description || 'No description provided.' }, + { name: 'Submitted', value: `` }, + ...(app.reviewedBy ? [{ name: 'Reviewed By', value: `<@${app.reviewedBy}>`, inline: true }] : []), + ); +} - await channel.send({ embeds: [embed], components: [row] }); - return interaction.editReply({ content: `Partnership panel is ready in ${channel}.\nRequests: ${requestChannel}` }); +export function applicationButtons(app) { + if (app.status !== 'pending') return []; + return [new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`partner_accept:${app.id}`).setLabel('Accept').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId(`partner_reject:${app.id}`).setLabel('Reject').setStyle(ButtonStyle.Danger), + )]; } From c08cbe6210d076352e139200b64a35478312a88c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:59:17 +0300 Subject: [PATCH 144/219] feat: complete partner interactions and management --- src/interactions/buttons/partner.js | 119 ++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 25 deletions(-) diff --git a/src/interactions/buttons/partner.js b/src/interactions/buttons/partner.js index 1005e3c8f9..5eb5dcc562 100644 --- a/src/interactions/buttons/partner.js +++ b/src/interactions/buttons/partner.js @@ -1,27 +1,96 @@ -import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, EmbedBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; +import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, EmbedBuilder, ButtonBuilder, ButtonStyle, PermissionFlagsBits } from 'discord.js'; +import { getPartnerData, savePartnerData, applicationEmbed, applicationButtons } from '../../utils/partner.js'; -const ids = new Set(['partner_apply','partner_requirements','partner_accept','partner_reject']); +async function renderApplications(interaction, filter) { + const data = await getPartnerData(interaction.client, interaction.guildId); + const items = filter === 'active' ? data.partners.filter(p => p.status === 'active') : data.applications.filter(a => a.status === 'pending'); + const title = filter === 'active' ? '🤝 Active Partners' : '🟡 Pending Partnership Requests'; + if (!items.length) return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle(title).setDescription('Nothing to display.')], components: [] }); + const description = items.slice(0, 15).map((x, i) => filter === 'active' + ? `**${i + 1}. ${x.serverName}** • ${x.members} members • ` + : `**#${x.id} — ${x.serverName}** • ${x.members} members • <@${x.applicantId}>`).join('\n'); + const rows = []; + if (filter !== 'active') rows.push(new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('Back').setStyle(ButtonStyle.Secondary))); + else rows.push(new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('Back').setStyle(ButtonStyle.Secondary))); + return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle(title).setDescription(description)], components: rows }); +} -export default { - name: 'partner', - ids, - async execute(interaction) { - const id = interaction.customId; - if (id === 'partner_apply') { - const modal = new ModalBuilder().setCustomId('partner_apply_modal').setTitle('Partnership Request'); - const server = new TextInputBuilder().setCustomId('server_name').setLabel('Server name').setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(100); - const invite = new TextInputBuilder().setCustomId('invite').setLabel('Invite link').setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(200); - const members = new TextInputBuilder().setCustomId('members').setLabel('Member count').setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(10); - const description = new TextInputBuilder().setCustomId('description').setLabel('Server description').setStyle(TextInputStyle.Paragraph).setRequired(true).setMaxLength(1000); - modal.addComponents(...[server, invite, members, description].map(x => new ActionRowBuilder().addComponents(x))); - return interaction.showModal(modal); - } - if (id === 'partner_requirements') return interaction.reply({ content: 'Partnership requirements: active community, valid invite, no recent serious violations, and the server must meet the configured member/activity requirements.', ephemeral: true }); - if (id === 'partner_accept' || id === 'partner_reject') { - if (!interaction.memberPermissions?.has('ManageGuild')) return interaction.reply({ content: 'You do not have permission to manage partnerships.', ephemeral: true }); - const accepted = id === 'partner_accept'; - const embed = EmbedBuilder.from(interaction.message.embeds[0] || new EmbedBuilder()).setColor(accepted ? 0x57f287 : 0xed4245).addFields({ name: 'Status', value: accepted ? '🟢 Accepted' : '🔴 Rejected' }, { name: 'Reviewed by', value: `${interaction.user}` }); - return interaction.update({ embeds: [embed], components: [] }); - } - }, -}; +export default [ + { name: 'partner_apply', async execute(interaction) { + const modal = new ModalBuilder().setCustomId('partner_apply_modal').setTitle('Partnership Request'); + const fields = [ + ['server_name', 'Server name', TextInputStyle.Short, 100], + ['invite', 'Invite link', TextInputStyle.Short, 200], + ['members', 'Member count', TextInputStyle.Short, 10], + ['description', 'Server description', TextInputStyle.Paragraph, 1000], + ]; + modal.addComponents(...fields.map(([id, label, style, max]) => new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(true).setMaxLength(max)))); + return interaction.showModal(modal); + }}, + { name: 'partner_requirements', async execute(interaction) { + const data = await getPartnerData(interaction.client, interaction.guildId); + return interaction.reply({ content: `📋 **Partnership Requirements**\n\n• ${data.requirements.minMembers}+ members\n• ${data.requirements.requireInvite ? 'Valid invite link' : 'Invite optional'}\n• ${data.requirements.requireActive ? 'Active community' : 'Activity check disabled'}\n• No recent serious violations`, ephemeral: true }); + }}, + { name: 'partner_list', async execute(interaction) { + const data = await getPartnerData(interaction.client, interaction.guildId); + const active = data.partners.filter(p => p.status === 'active'); + const description = active.length ? active.slice(0, 25).map((p, i) => `**${i + 1}. ${p.serverName}** • ${p.members} members • ${p.invite}`).join('\n') : 'No active partners yet.'; + return interaction.reply({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 Partners').setDescription(description)], ephemeral: true }); + }}, + { name: 'partner_pending', async execute(interaction) { return renderApplications(interaction, 'pending'); } }, + { name: 'partner_active', async execute(interaction) { return renderApplications(interaction, 'active'); } }, + { name: 'partner_stats', async execute(interaction) { + const data = await getPartnerData(interaction.client, interaction.guildId); + const accepted = data.applications.filter(a => a.status === 'accepted').length; + const rejected = data.applications.filter(a => a.status === 'rejected').length; + const pending = data.applications.filter(a => a.status === 'pending').length; + return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('📊 Partnership Statistics').addFields( + { name: 'Active Partners', value: String(data.partners.filter(p => p.status === 'active').length), inline: true }, + { name: 'Accepted', value: String(accepted), inline: true }, + { name: 'Rejected', value: String(rejected), inline: true }, + { name: 'Pending', value: String(pending), inline: true }, + { name: 'Total Applications', value: String(data.applications.length), inline: true }, + )], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('Back').setStyle(ButtonStyle.Secondary))] }); + }}, + { name: 'partner_settings', async execute(interaction) { + const data = await getPartnerData(interaction.client, interaction.guildId); + const modal = new ModalBuilder().setCustomId('partner_settings_modal').setTitle('Partnership Settings').addComponents( + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('min_members').setLabel('Minimum members').setStyle(TextInputStyle.Short).setValue(String(data.requirements.minMembers)).setRequired(true)), + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('require_invite').setLabel('Require invite? yes/no').setStyle(TextInputStyle.Short).setValue(data.requirements.requireInvite ? 'yes' : 'no').setRequired(true)), + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('require_active').setLabel('Require active community? yes/no').setStyle(TextInputStyle.Short).setValue(data.requirements.requireActive ? 'yes' : 'no').setRequired(true)), + ); + return interaction.showModal(modal); + }}, + { name: 'partner_back_dashboard', async execute(interaction) { + const data = await getPartnerData(interaction.client, interaction.guildId); + const pending = data.applications.filter(a => a.status === 'pending').length; + const active = data.partners.filter(p => p.status === 'active').length; + return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 Partnership Management').setDescription('Manage partnership requests and active partners.').addFields( + { name: 'Active Partners', value: String(active), inline: true }, { name: 'Pending Requests', value: String(pending), inline: true }, { name: 'Total Applications', value: String(data.applications.length), inline: true }, + )], components: [new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('partner_pending').setLabel('Pending').setEmoji('🟡').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('partner_active').setLabel('Partners').setEmoji('🤝').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId('partner_stats').setLabel('Statistics').setEmoji('📊').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('partner_settings').setLabel('Settings').setEmoji('⚙️').setStyle(ButtonStyle.Secondary), + )] }); + }}, + { name: 'partner_accept', async execute(interaction, client, args) { return review(interaction, client, args[0], 'accepted'); } }, + { name: 'partner_reject', async execute(interaction, client, args) { return review(interaction, client, args[0], 'rejected'); } }, +]; + +async function review(interaction, client, id, status) { + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return interaction.reply({ content: '❌ You need Manage Server permission.', ephemeral: true }); + await interaction.deferUpdate(); + const data = await getPartnerData(client, interaction.guildId); + const app = data.applications.find(a => String(a.id) === String(id)); + if (!app || app.status !== 'pending') return interaction.followUp({ content: '❌ Partnership request not found or already reviewed.', ephemeral: true }); + app.status = status; + app.reviewedBy = interaction.user.id; + app.reviewedAt = new Date().toISOString(); + if (status === 'accepted') data.partners.push({ ...app, status: 'active', acceptedAt: app.reviewedAt }); + await savePartnerData(client, interaction.guildId, data); + const channel = interaction.guild.channels.cache.get(data.requestChannelId); + const message = channel ? await channel.messages.fetch(app.messageId).catch(() => null) : null; + if (message) await message.edit({ embeds: [applicationEmbed(app, interaction.guild)], components: applicationButtons(app) }); + return interaction.followUp({ content: `✅ Partnership request #${app.id} ${status}.`, ephemeral: true }); +} From 39dbfa2a88b4a6d208ed218fc9c689e341849c7d Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:59:23 +0300 Subject: [PATCH 145/219] feat: persist partnership applications --- src/interactions/modals/partnerApply.js | 48 ++++++++++++------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/interactions/modals/partnerApply.js b/src/interactions/modals/partnerApply.js index f2252bf0da..d4d992e442 100644 --- a/src/interactions/modals/partnerApply.js +++ b/src/interactions/modals/partnerApply.js @@ -1,30 +1,28 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { getPartnerData, savePartnerData, applicationEmbed, applicationButtons } from '../../utils/partner.js'; export default { name: 'partner_apply_modal', - async execute(interaction) { - const server = interaction.fields.getTextInputValue('server_name'); - const invite = interaction.fields.getTextInputValue('invite'); - const members = interaction.fields.getTextInputValue('members'); - const description = interaction.fields.getTextInputValue('description'); - const embed = new EmbedBuilder() - .setColor(0x5865f2) - .setTitle('🤝 Partnership Request') - .addFields( - { name: 'Server', value: server }, - { name: 'Members', value: members }, - { name: 'Invite', value: invite }, - { name: 'Applicant', value: `${interaction.user}` }, - { name: 'Description', value: description }, - ) - .setFooter({ text: 'Status: Pending' }); - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId('partner_accept').setLabel('Accept').setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId('partner_reject').setLabel('Reject').setStyle(ButtonStyle.Danger), - ); - const target = interaction.guild.channels.cache.find(c => c.isTextBased() && c.name === 'partnership-requests'); - if (!target) return interaction.reply({ content: 'The partnership request channel is not configured. Ask an administrator to run `/partner setup` again.', ephemeral: true }); - await target.send({ embeds: [embed], components: [row] }); - return interaction.reply({ content: 'Your partnership request has been submitted.', ephemeral: true }); + async execute(interaction, client) { + const server = interaction.fields.getTextInputValue('server_name').trim(); + const invite = interaction.fields.getTextInputValue('invite').trim(); + const membersRaw = interaction.fields.getTextInputValue('members').trim(); + const description = interaction.fields.getTextInputValue('description').trim(); + const members = Number(membersRaw.replace(/[^0-9]/g, '')); + if (!server || !description || !Number.isFinite(members)) return interaction.reply({ content: '❌ Please provide valid application details.', ephemeral: true }); + const data = await getPartnerData(client, interaction.guildId); + if (data.requirements.requireInvite && !/^https?:\/\/discord(?:\.gg|\.com\/invite)\//i.test(invite)) return interaction.reply({ content: '❌ Please provide a valid Discord invite link.', ephemeral: true }); + if (members < data.requirements.minMembers) return interaction.reply({ content: `❌ Your server needs at least ${data.requirements.minMembers} members.`, ephemeral: true }); + const duplicate = data.applications.find(a => a.applicantId === interaction.user.id && a.status === 'pending'); + if (duplicate) return interaction.reply({ content: `❌ You already have pending partnership request #${duplicate.id}.`, ephemeral: true }); + + data.counter += 1; + const app = { id: data.counter, serverName: server, invite, members, description, applicantId: interaction.user.id, status: 'pending', createdAt: new Date().toISOString(), messageId: null }; + const target = interaction.guild.channels.cache.get(data.requestChannelId); + if (!target) return interaction.reply({ content: '❌ Partnership request channel is missing. Run `/partner setup` again.', ephemeral: true }); + const message = await target.send({ embeds: [applicationEmbed(app, interaction.guild)], components: applicationButtons(app) }); + app.messageId = message.id; + data.applications.push(app); + await savePartnerData(client, interaction.guildId, data); + return interaction.reply({ content: `✅ Partnership request #${app.id} submitted.`, ephemeral: true }); }, }; From a862a68d1120689a8daba09105820edda1648eb6 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sat, 22 Aug 2026 23:59:31 +0300 Subject: [PATCH 146/219] feat: add partnership settings modal --- src/interactions/modals/partnerSettings.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/interactions/modals/partnerSettings.js diff --git a/src/interactions/modals/partnerSettings.js b/src/interactions/modals/partnerSettings.js new file mode 100644 index 0000000000..652b670a86 --- /dev/null +++ b/src/interactions/modals/partnerSettings.js @@ -0,0 +1,17 @@ +import { getPartnerData, savePartnerData } from '../../utils/partner.js'; + +export default { + name: 'partner_settings_modal', + async execute(interaction, client) { + const minMembers = Number(interaction.fields.getTextInputValue('min_members').replace(/[^0-9]/g, '')); + const requireInvite = interaction.fields.getTextInputValue('require_invite').trim().toLowerCase() === 'yes'; + const requireActive = interaction.fields.getTextInputValue('require_active').trim().toLowerCase() === 'yes'; + if (!Number.isFinite(minMembers) || minMembers < 0) return interaction.reply({ content: '❌ Minimum members must be a valid number.', ephemeral: true }); + const data = await getPartnerData(client, interaction.guildId); + data.requirements.minMembers = minMembers; + data.requirements.requireInvite = requireInvite; + data.requirements.requireActive = requireActive; + await savePartnerData(client, interaction.guildId, data); + return interaction.reply({ content: `✅ Partnership settings updated. Minimum members: ${minMembers}.`, ephemeral: true }); + }, +}; From f09ba564f8f8e7469fd95d8be97a3e666920bfc9 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:08:08 +0300 Subject: [PATCH 147/219] fix: simplify Arabic partnership panel and set minimum members to 20 --- src/utils/partner.js | 96 ++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 53 deletions(-) diff --git a/src/utils/partner.js b/src/utils/partner.js index 96c1ab4103..dee42d3fec 100644 --- a/src/utils/partner.js +++ b/src/utils/partner.js @@ -7,9 +7,7 @@ async function load(client, guildId) { return data && typeof data === 'object' ? data : {}; } -async function save(client, guildId, data) { - await client.db.set(key(guildId), data); -} +async function save(client, guildId, data) { await client.db.set(key(guildId), data); } export async function getPartnerData(client, guildId) { const data = await load(client, guildId); @@ -17,51 +15,43 @@ export async function getPartnerData(client, guildId) { data.applications = Array.isArray(data.applications) ? data.applications : []; data.partners = Array.isArray(data.partners) ? data.partners : []; data.requirements = { - minMembers: Number(data.requirements?.minMembers ?? 500), - minAccountAgeDays: Number(data.requirements?.minAccountAgeDays ?? 0), + minMembers: Number(data.requirements?.minMembers ?? 20), requireInvite: data.requirements?.requireInvite !== false, requireActive: data.requirements?.requireActive !== false, }; + if (data.requirements.minMembers < 20) data.requirements.minMembers = 20; return data; } -export async function savePartnerData(client, guildId, data) { - await save(client, guildId, data); -} +export async function savePartnerData(client, guildId, data) { return save(client, guildId, data); } export async function setupPartnerPanel(interaction) { const guild = interaction.guild; - if (!guild) return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); + if (!guild) return interaction.reply({ content: 'هذا الأمر يعمل داخل السيرفر فقط.', ephemeral: true }); await interaction.deferReply({ ephemeral: true }); const data = await getPartnerData(interaction.client, guild.id); let panelChannel = data.panelChannelId ? guild.channels.cache.get(data.panelChannelId) : null; if (!panelChannel) panelChannel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnerships'); - if (!panelChannel) { - panelChannel = await guild.channels.create({ - name: 'partnerships', type: ChannelType.GuildText, reason: 'Partner system setup', - permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }], - }); - } + if (!panelChannel) panelChannel = await guild.channels.create({ + name: 'partnerships', type: ChannelType.GuildText, reason: 'Partner system setup', + permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }], + }); let requestChannel = data.requestChannelId ? guild.channels.cache.get(data.requestChannelId) : null; if (!requestChannel) requestChannel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnership-requests'); - if (!requestChannel) { - requestChannel = await guild.channels.create({ - name: 'partnership-requests', type: ChannelType.GuildText, reason: 'Partner system request channel', - permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }], - }); - } + if (!requestChannel) requestChannel = await guild.channels.create({ + name: 'partnership-requests', type: ChannelType.GuildText, reason: 'Partner system request channel', + permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }], + }); - const embed = new EmbedBuilder() - .setColor(0x5865f2).setTitle('🤝 Server Partnerships') - .setDescription('Interested in partnering with this server? Review the requirements and submit a request below.') - .addFields({ name: 'Requirements', value: `• ${data.requirements.minMembers}+ members\n• ${data.requirements.requireInvite ? 'Valid invite link' : 'Invite link optional'}\n• ${data.requirements.requireActive ? 'Active community' : 'Activity check disabled'}\n• No recent serious violations` }) - .setFooter({ text: `${guild.name} • Partnership System` }); + const embed = new EmbedBuilder().setColor(0x5865f2) + .setTitle('🤝 شراكات السيرفر') + .setDescription('حاب تسوي شراكة مع سيرفرنا؟ اضغط الزر وأرسل طلب الشراكة.\n\n**شروط الشراكة:**\n• السيرفر يحتوي على 20 عضو أو أكثر\n• رابط دعوة صالح\n• سيرفر نشط ومجتمعه فعّال\n• لا توجد مخالفات خطيرة حديثة') + .setFooter({ text: `${guild.name} • نظام الشراكات` }); const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId('partner_apply').setLabel('Apply for Partnership').setEmoji('🤝').setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId('partner_requirements').setLabel('Requirements').setEmoji('📋').setStyle(ButtonStyle.Secondary), - new ButtonBuilder().setCustomId('partner_list').setLabel('Partners').setEmoji('🤝').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('partner_apply').setLabel('تقديم طلب شراكة').setEmoji('🤝').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('partner_list').setLabel('الشركاء').setEmoji('📋').setStyle(ButtonStyle.Secondary), ); let panelMessage = data.panelMessageId ? await panelChannel.messages.fetch(data.panelMessageId).catch(() => null) : null; @@ -72,49 +62,49 @@ export async function setupPartnerPanel(interaction) { data.panelMessageId = panelMessage.id; data.requestChannelId = requestChannel.id; await savePartnerData(interaction.client, guild.id, data); - return interaction.editReply({ content: `✅ Partner system is ready.\nPanel: ${panelChannel}\nRequests: ${requestChannel}` }); + return interaction.editReply({ content: `✅ تم تجهيز نظام الشراكات.\nلوحة الشراكات: ${panelChannel}\nطلبات الشراكة: ${requestChannel}` }); } export async function partnerDashboard(interaction) { const data = await getPartnerData(interaction.client, interaction.guildId); const pending = data.applications.filter(a => a.status === 'pending').length; const active = data.partners.filter(p => p.status === 'active').length; - const embed = new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 Partnership Management') - .setDescription('Manage partnership requests and active partners from this dashboard.') + const embed = new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 إدارة الشراكات') + .setDescription('إدارة طلبات الشراكة والشركاء الحاليين من هنا.') .addFields( - { name: 'Active Partners', value: String(active), inline: true }, - { name: 'Pending Requests', value: String(pending), inline: true }, - { name: 'Total Applications', value: String(data.applications.length), inline: true }, - { name: 'Requirements', value: `${data.requirements.minMembers}+ members • Invite ${data.requirements.requireInvite ? 'required' : 'optional'}` }, + { name: 'الشراكات الحالية', value: String(active), inline: true }, + { name: 'الطلبات المعلقة', value: String(pending), inline: true }, + { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true }, + { name: 'الحد الأدنى', value: '20 عضوًا على الأقل', inline: true }, ); const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId('partner_pending').setLabel('Pending').setEmoji('🟡').setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId('partner_active').setLabel('Partners').setEmoji('🤝').setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId('partner_stats').setLabel('Statistics').setEmoji('📊').setStyle(ButtonStyle.Secondary), - new ButtonBuilder().setCustomId('partner_settings').setLabel('Settings').setEmoji('⚙️').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('partner_pending').setLabel('الطلبات').setEmoji('🟡').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('partner_active').setLabel('الشركاء').setEmoji('🤝').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId('partner_stats').setLabel('الإحصائيات').setEmoji('📊').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('partner_settings').setLabel('الإعدادات').setEmoji('⚙️').setStyle(ButtonStyle.Secondary), ); return interaction.reply({ embeds: [embed], components: [row], ephemeral: true }); } -export function applicationEmbed(app, guild) { - const status = app.status === 'accepted' ? '🟢 Accepted' : app.status === 'rejected' ? '🔴 Rejected' : '🟡 Pending'; +export function applicationEmbed(app) { + const status = app.status === 'accepted' ? '🟢 مقبول' : app.status === 'rejected' ? '🔴 مرفوض' : '🟡 قيد المراجعة'; return new EmbedBuilder().setColor(app.status === 'accepted' ? 0x57f287 : app.status === 'rejected' ? 0xed4245 : 0x5865f2) - .setTitle(`🤝 Partnership Request #${app.id}`).addFields( - { name: 'Server', value: app.serverName, inline: true }, - { name: 'Members', value: String(app.members), inline: true }, - { name: 'Status', value: status, inline: true }, - { name: 'Invite', value: app.invite, inline: false }, - { name: 'Applicant', value: `<@${app.applicantId}>`, inline: true }, - { name: 'Description', value: app.description || 'No description provided.' }, - { name: 'Submitted', value: `` }, - ...(app.reviewedBy ? [{ name: 'Reviewed By', value: `<@${app.reviewedBy}>`, inline: true }] : []), + .setTitle(`🤝 طلب شراكة #${app.id}`).addFields( + { name: 'السيرفر', value: app.serverName, inline: true }, + { name: 'عدد الأعضاء', value: String(app.members), inline: true }, + { name: 'الحالة', value: status, inline: true }, + { name: 'رابط الدعوة', value: app.invite }, + { name: 'مقدم الطلب', value: `<@${app.applicantId}>`, inline: true }, + { name: 'وصف السيرفر', value: app.description || 'لا يوجد وصف.' }, + { name: 'تاريخ الطلب', value: `` }, + ...(app.reviewedBy ? [{ name: 'تمت المراجعة بواسطة', value: `<@${app.reviewedBy}>`, inline: true }] : []), ); } export function applicationButtons(app) { if (app.status !== 'pending') return []; return [new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId(`partner_accept:${app.id}`).setLabel('Accept').setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId(`partner_reject:${app.id}`).setLabel('Reject').setStyle(ButtonStyle.Danger), + new ButtonBuilder().setCustomId(`partner_accept:${app.id}`).setLabel('قبول').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId(`partner_reject:${app.id}`).setLabel('رفض').setStyle(ButtonStyle.Danger), )]; } From c88079920a221101d00a31a5ccf902084435df26 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:08:20 +0300 Subject: [PATCH 148/219] feat: configure partnership announcement channel from setup --- src/commands/Utility/partner.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/commands/Utility/partner.js b/src/commands/Utility/partner.js index 2adcc2ccd6..91797182cb 100644 --- a/src/commands/Utility/partner.js +++ b/src/commands/Utility/partner.js @@ -1,17 +1,29 @@ -import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { SlashCommandBuilder, PermissionFlagsBits, ChannelType } from 'discord.js'; import { setupPartnerPanel, partnerDashboard } from '../../utils/partner.js'; export default { data: new SlashCommandBuilder() .setName('partner') - .setDescription('Manage the server partnership system') - .addSubcommand(sub => sub.setName('setup').setDescription('Create or repair the partnership system')) - .addSubcommand(sub => sub.setName('dashboard').setDescription('Open the partnership management dashboard')) + .setDescription('إدارة نظام الشراكات') + .addSubcommand(sub => sub + .setName('setup') + .setDescription('إعداد نظام الشراكات') + .addChannelOption(opt => opt + .setName('announcement_channel') + .setDescription('الروم الذي سيتم نشر الشراكة المقبولة فيه') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true))) + .addSubcommand(sub => sub + .setName('dashboard') + .setDescription('فتح لوحة إدارة الشراكات')) .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild), async execute(interaction) { const subcommand = interaction.options.getSubcommand(); - if (subcommand === 'setup') return setupPartnerPanel(interaction); + if (subcommand === 'setup') { + const announcementChannel = interaction.options.getChannel('announcement_channel'); + return setupPartnerPanel(interaction, announcementChannel); + } return partnerDashboard(interaction); }, }; From 76c1337abde7ac353c4b19986d377814681169c4 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:08:31 +0300 Subject: [PATCH 149/219] fix: configure partnership announcement channel and simplify panel --- src/utils/partner.js | 84 ++++++++++---------------------------------- 1 file changed, 19 insertions(+), 65 deletions(-) diff --git a/src/utils/partner.js b/src/utils/partner.js index dee42d3fec..2c58f38df8 100644 --- a/src/utils/partner.js +++ b/src/utils/partner.js @@ -1,12 +1,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; const key = guildId => `partners:${guildId}`; - -async function load(client, guildId) { - const data = await client.db.get(key(guildId), {}); - return data && typeof data === 'object' ? data : {}; -} - +async function load(client, guildId) { const data = await client.db.get(key(guildId), {}); return data && typeof data === 'object' ? data : {}; } async function save(client, guildId, data) { await client.db.set(key(guildId), data); } export async function getPartnerData(client, guildId) { @@ -14,97 +9,56 @@ export async function getPartnerData(client, guildId) { data.counter = Number(data.counter || 0); data.applications = Array.isArray(data.applications) ? data.applications : []; data.partners = Array.isArray(data.partners) ? data.partners : []; - data.requirements = { - minMembers: Number(data.requirements?.minMembers ?? 20), - requireInvite: data.requirements?.requireInvite !== false, - requireActive: data.requirements?.requireActive !== false, - }; - if (data.requirements.minMembers < 20) data.requirements.minMembers = 20; + data.requirements = { minMembers: Math.max(20, Number(data.requirements?.minMembers ?? 20)), requireInvite: data.requirements?.requireInvite !== false, requireActive: data.requirements?.requireActive !== false }; return data; } - export async function savePartnerData(client, guildId, data) { return save(client, guildId, data); } -export async function setupPartnerPanel(interaction) { +export async function setupPartnerPanel(interaction, announcementChannel) { const guild = interaction.guild; if (!guild) return interaction.reply({ content: 'هذا الأمر يعمل داخل السيرفر فقط.', ephemeral: true }); + if (!announcementChannel || announcementChannel.type !== ChannelType.GuildText) return interaction.reply({ content: 'اختر روم نصي صالح للإعلانات.', ephemeral: true }); await interaction.deferReply({ ephemeral: true }); const data = await getPartnerData(interaction.client, guild.id); - let panelChannel = data.panelChannelId ? guild.channels.cache.get(data.panelChannelId) : null; if (!panelChannel) panelChannel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnerships'); - if (!panelChannel) panelChannel = await guild.channels.create({ - name: 'partnerships', type: ChannelType.GuildText, reason: 'Partner system setup', - permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }], - }); - + if (!panelChannel) panelChannel = await guild.channels.create({ name: 'partnerships', type: ChannelType.GuildText, reason: 'Partner system setup', permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }] }); let requestChannel = data.requestChannelId ? guild.channels.cache.get(data.requestChannelId) : null; if (!requestChannel) requestChannel = guild.channels.cache.find(c => c.type === ChannelType.GuildText && c.name === 'partnership-requests'); - if (!requestChannel) requestChannel = await guild.channels.create({ - name: 'partnership-requests', type: ChannelType.GuildText, reason: 'Partner system request channel', - permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }], - }); + if (!requestChannel) requestChannel = await guild.channels.create({ name: 'partnership-requests', type: ChannelType.GuildText, reason: 'Partner system request channel', permissionOverwrites: [{ id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory], deny: [PermissionFlagsBits.SendMessages] }] }); - const embed = new EmbedBuilder().setColor(0x5865f2) - .setTitle('🤝 شراكات السيرفر') - .setDescription('حاب تسوي شراكة مع سيرفرنا؟ اضغط الزر وأرسل طلب الشراكة.\n\n**شروط الشراكة:**\n• السيرفر يحتوي على 20 عضو أو أكثر\n• رابط دعوة صالح\n• سيرفر نشط ومجتمعه فعّال\n• لا توجد مخالفات خطيرة حديثة') + const embed = new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 شراكات السيرفر') + .setDescription('حاب تسوي شراكة مع سيرفرنا؟ اضغط **تقديم طلب شراكة** وأرسل بيانات سيرفرك.\n\n**الشروط:**\n• 20 عضو أو أكثر\n• رابط دعوة صالح\n• سيرفر نشط\n• لا توجد مخالفات خطيرة حديثة') .setFooter({ text: `${guild.name} • نظام الشراكات` }); - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId('partner_apply').setLabel('تقديم طلب شراكة').setEmoji('🤝').setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId('partner_list').setLabel('الشركاء').setEmoji('📋').setStyle(ButtonStyle.Secondary), - ); - + const row = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_apply').setLabel('تقديم طلب شراكة').setEmoji('🤝').setStyle(ButtonStyle.Primary)); let panelMessage = data.panelMessageId ? await panelChannel.messages.fetch(data.panelMessageId).catch(() => null) : null; if (panelMessage) await panelMessage.edit({ embeds: [embed], components: [row] }); else panelMessage = await panelChannel.send({ embeds: [embed], components: [row] }); - data.panelChannelId = panelChannel.id; data.panelMessageId = panelMessage.id; data.requestChannelId = requestChannel.id; + data.announcementChannelId = announcementChannel.id; await savePartnerData(interaction.client, guild.id, data); - return interaction.editReply({ content: `✅ تم تجهيز نظام الشراكات.\nلوحة الشراكات: ${panelChannel}\nطلبات الشراكة: ${requestChannel}` }); + return interaction.editReply({ content: `✅ تم تجهيز نظام الشراكات.\nلوحة الطلبات: ${panelChannel}\nروم الطلبات: ${requestChannel}\nروم إعلانات الشراكات: ${announcementChannel}` }); } export async function partnerDashboard(interaction) { const data = await getPartnerData(interaction.client, interaction.guildId); const pending = data.applications.filter(a => a.status === 'pending').length; const active = data.partners.filter(p => p.status === 'active').length; - const embed = new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 إدارة الشراكات') - .setDescription('إدارة طلبات الشراكة والشركاء الحاليين من هنا.') - .addFields( - { name: 'الشراكات الحالية', value: String(active), inline: true }, - { name: 'الطلبات المعلقة', value: String(pending), inline: true }, - { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true }, - { name: 'الحد الأدنى', value: '20 عضوًا على الأقل', inline: true }, - ); + const embed = new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 إدارة الشراكات').setDescription('إدارة طلبات الشراكة والشركاء من هنا.').addFields( + { name: 'الشراكات الحالية', value: String(active), inline: true }, { name: 'الطلبات المعلقة', value: String(pending), inline: true }, { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true }, { name: 'الحد الأدنى', value: '20 عضوًا', inline: true }, { name: 'روم الإعلانات', value: data.announcementChannelId ? `<#${data.announcementChannelId}>` : 'غير محدد' }, + ); const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId('partner_pending').setLabel('الطلبات').setEmoji('🟡').setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId('partner_active').setLabel('الشركاء').setEmoji('🤝').setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId('partner_stats').setLabel('الإحصائيات').setEmoji('📊').setStyle(ButtonStyle.Secondary), - new ButtonBuilder().setCustomId('partner_settings').setLabel('الإعدادات').setEmoji('⚙️').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('partner_pending').setLabel('الطلبات').setEmoji('🟡').setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId('partner_active').setLabel('الشركاء').setEmoji('🤝').setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId('partner_stats').setLabel('الإحصائيات').setEmoji('📊').setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId('partner_settings').setLabel('الإعدادات').setEmoji('⚙️').setStyle(ButtonStyle.Secondary), ); return interaction.reply({ embeds: [embed], components: [row], ephemeral: true }); } export function applicationEmbed(app) { const status = app.status === 'accepted' ? '🟢 مقبول' : app.status === 'rejected' ? '🔴 مرفوض' : '🟡 قيد المراجعة'; - return new EmbedBuilder().setColor(app.status === 'accepted' ? 0x57f287 : app.status === 'rejected' ? 0xed4245 : 0x5865f2) - .setTitle(`🤝 طلب شراكة #${app.id}`).addFields( - { name: 'السيرفر', value: app.serverName, inline: true }, - { name: 'عدد الأعضاء', value: String(app.members), inline: true }, - { name: 'الحالة', value: status, inline: true }, - { name: 'رابط الدعوة', value: app.invite }, - { name: 'مقدم الطلب', value: `<@${app.applicantId}>`, inline: true }, - { name: 'وصف السيرفر', value: app.description || 'لا يوجد وصف.' }, - { name: 'تاريخ الطلب', value: `` }, - ...(app.reviewedBy ? [{ name: 'تمت المراجعة بواسطة', value: `<@${app.reviewedBy}>`, inline: true }] : []), - ); -} - -export function applicationButtons(app) { - if (app.status !== 'pending') return []; - return [new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId(`partner_accept:${app.id}`).setLabel('قبول').setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId(`partner_reject:${app.id}`).setLabel('رفض').setStyle(ButtonStyle.Danger), - )]; + return new EmbedBuilder().setColor(app.status === 'accepted' ? 0x57f287 : app.status === 'rejected' ? 0xed4245 : 0x5865f2).setTitle(`🤝 طلب شراكة #${app.id}`).addFields( + { name: 'السيرفر', value: app.serverName, inline: true }, { name: 'عدد الأعضاء', value: String(app.members), inline: true }, { name: 'الحالة', value: status, inline: true }, { name: 'رابط الدعوة', value: app.invite }, { name: 'مقدم الطلب', value: `<@${app.applicantId}>`, inline: true }, { name: 'وصف السيرفر', value: app.description || 'لا يوجد وصف.' }, { name: 'تاريخ الطلب', value: `` }, ...(app.reviewedBy ? [{ name: 'تمت المراجعة بواسطة', value: `<@${app.reviewedBy}>`, inline: true }] : []), + ); } +export function applicationButtons(app) { if (app.status !== 'pending') return []; return [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`partner_accept:${app.id}`).setLabel('قبول').setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId(`partner_reject:${app.id}`).setLabel('رفض').setStyle(ButtonStyle.Danger))]; } From 33ce42dea2ec244c3ea3b0c154d287f21bde9669 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:08:43 +0300 Subject: [PATCH 150/219] fix: remove redundant partner panel buttons and localize partner UI --- src/interactions/buttons/partner.js | 91 +++++++++++------------------ 1 file changed, 33 insertions(+), 58 deletions(-) diff --git a/src/interactions/buttons/partner.js b/src/interactions/buttons/partner.js index 5eb5dcc562..83b4c9dc30 100644 --- a/src/interactions/buttons/partner.js +++ b/src/interactions/buttons/partner.js @@ -4,93 +4,68 @@ import { getPartnerData, savePartnerData, applicationEmbed, applicationButtons } async function renderApplications(interaction, filter) { const data = await getPartnerData(interaction.client, interaction.guildId); const items = filter === 'active' ? data.partners.filter(p => p.status === 'active') : data.applications.filter(a => a.status === 'pending'); - const title = filter === 'active' ? '🤝 Active Partners' : '🟡 Pending Partnership Requests'; - if (!items.length) return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle(title).setDescription('Nothing to display.')], components: [] }); - const description = items.slice(0, 15).map((x, i) => filter === 'active' - ? `**${i + 1}. ${x.serverName}** • ${x.members} members • ` - : `**#${x.id} — ${x.serverName}** • ${x.members} members • <@${x.applicantId}>`).join('\n'); - const rows = []; - if (filter !== 'active') rows.push(new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('Back').setStyle(ButtonStyle.Secondary))); - else rows.push(new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('Back').setStyle(ButtonStyle.Secondary))); - return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle(title).setDescription(description)], components: rows }); + const title = filter === 'active' ? '🤝 الشراكات الحالية' : '🟡 طلبات الشراكة المعلقة'; + if (!items.length) return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle(title).setDescription('لا توجد بيانات لعرضها حاليًا.')], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('رجوع').setStyle(ButtonStyle.Secondary))] }); + const description = items.slice(0, 15).map((x, i) => filter === 'active' ? `**${i + 1}. ${x.serverName}** • ${x.members} عضو • ` : `**#${x.id} — ${x.serverName}** • ${x.members} عضو • <@${x.applicantId}>`).join('\n'); + return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle(title).setDescription(description)], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('رجوع').setStyle(ButtonStyle.Secondary))] }); } export default [ { name: 'partner_apply', async execute(interaction) { - const modal = new ModalBuilder().setCustomId('partner_apply_modal').setTitle('Partnership Request'); - const fields = [ - ['server_name', 'Server name', TextInputStyle.Short, 100], - ['invite', 'Invite link', TextInputStyle.Short, 200], - ['members', 'Member count', TextInputStyle.Short, 10], - ['description', 'Server description', TextInputStyle.Paragraph, 1000], - ]; - modal.addComponents(...fields.map(([id, label, style, max]) => new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(true).setMaxLength(max)))); + const modal = new ModalBuilder().setCustomId('partner_apply_modal').setTitle('طلب شراكة').addComponents( + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('server_name').setLabel('اسم السيرفر').setPlaceholder('اكتب اسم سيرفرك').setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(100)), + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('invite').setLabel('رابط الدعوة').setPlaceholder('https://discord.gg/...').setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(200)), + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('members').setLabel('عدد الأعضاء').setPlaceholder('مثال: 150').setStyle(TextInputStyle.Short).setRequired(true).setMaxLength(10)), + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('description').setLabel('وصف السيرفر').setPlaceholder('عرفنا بسيرفرك ومحتواه').setStyle(TextInputStyle.Paragraph).setRequired(true).setMaxLength(1000)), + ); return interaction.showModal(modal); }}, - { name: 'partner_requirements', async execute(interaction) { - const data = await getPartnerData(interaction.client, interaction.guildId); - return interaction.reply({ content: `📋 **Partnership Requirements**\n\n• ${data.requirements.minMembers}+ members\n• ${data.requirements.requireInvite ? 'Valid invite link' : 'Invite optional'}\n• ${data.requirements.requireActive ? 'Active community' : 'Activity check disabled'}\n• No recent serious violations`, ephemeral: true }); - }}, - { name: 'partner_list', async execute(interaction) { - const data = await getPartnerData(interaction.client, interaction.guildId); - const active = data.partners.filter(p => p.status === 'active'); - const description = active.length ? active.slice(0, 25).map((p, i) => `**${i + 1}. ${p.serverName}** • ${p.members} members • ${p.invite}`).join('\n') : 'No active partners yet.'; - return interaction.reply({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 Partners').setDescription(description)], ephemeral: true }); - }}, { name: 'partner_pending', async execute(interaction) { return renderApplications(interaction, 'pending'); } }, { name: 'partner_active', async execute(interaction) { return renderApplications(interaction, 'active'); } }, { name: 'partner_stats', async execute(interaction) { const data = await getPartnerData(interaction.client, interaction.guildId); - const accepted = data.applications.filter(a => a.status === 'accepted').length; - const rejected = data.applications.filter(a => a.status === 'rejected').length; - const pending = data.applications.filter(a => a.status === 'pending').length; - return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('📊 Partnership Statistics').addFields( - { name: 'Active Partners', value: String(data.partners.filter(p => p.status === 'active').length), inline: true }, - { name: 'Accepted', value: String(accepted), inline: true }, - { name: 'Rejected', value: String(rejected), inline: true }, - { name: 'Pending', value: String(pending), inline: true }, - { name: 'Total Applications', value: String(data.applications.length), inline: true }, - )], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('Back').setStyle(ButtonStyle.Secondary))] }); + const accepted = data.applications.filter(a => a.status === 'accepted').length, rejected = data.applications.filter(a => a.status === 'rejected').length, pending = data.applications.filter(a => a.status === 'pending').length; + return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('📊 إحصائيات الشراكات').addFields( + { name: 'الشراكات الحالية', value: String(data.partners.filter(p => p.status === 'active').length), inline: true }, { name: 'المقبولة', value: String(accepted), inline: true }, { name: 'المرفوضة', value: String(rejected), inline: true }, { name: 'المعلقة', value: String(pending), inline: true }, { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true }, + )], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('رجوع').setStyle(ButtonStyle.Secondary))] }); }}, { name: 'partner_settings', async execute(interaction) { const data = await getPartnerData(interaction.client, interaction.guildId); - const modal = new ModalBuilder().setCustomId('partner_settings_modal').setTitle('Partnership Settings').addComponents( - new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('min_members').setLabel('Minimum members').setStyle(TextInputStyle.Short).setValue(String(data.requirements.minMembers)).setRequired(true)), - new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('require_invite').setLabel('Require invite? yes/no').setStyle(TextInputStyle.Short).setValue(data.requirements.requireInvite ? 'yes' : 'no').setRequired(true)), - new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('require_active').setLabel('Require active community? yes/no').setStyle(TextInputStyle.Short).setValue(data.requirements.requireActive ? 'yes' : 'no').setRequired(true)), + const modal = new ModalBuilder().setCustomId('partner_settings_modal').setTitle('إعدادات الشراكات').addComponents( + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('min_members').setLabel('الحد الأدنى للأعضاء').setStyle(TextInputStyle.Short).setValue(String(data.requirements.minMembers)).setRequired(true)), + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('require_invite').setLabel('هل رابط الدعوة مطلوب؟ نعم/لا').setStyle(TextInputStyle.Short).setValue(data.requirements.requireInvite ? 'نعم' : 'لا').setRequired(true)), + new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('require_active').setLabel('هل يشترط سيرفر نشط؟ نعم/لا').setStyle(TextInputStyle.Short).setValue(data.requirements.requireActive ? 'نعم' : 'لا').setRequired(true)), ); return interaction.showModal(modal); }}, { name: 'partner_back_dashboard', async execute(interaction) { const data = await getPartnerData(interaction.client, interaction.guildId); - const pending = data.applications.filter(a => a.status === 'pending').length; - const active = data.partners.filter(p => p.status === 'active').length; - return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 Partnership Management').setDescription('Manage partnership requests and active partners.').addFields( - { name: 'Active Partners', value: String(active), inline: true }, { name: 'Pending Requests', value: String(pending), inline: true }, { name: 'Total Applications', value: String(data.applications.length), inline: true }, - )], components: [new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId('partner_pending').setLabel('Pending').setEmoji('🟡').setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId('partner_active').setLabel('Partners').setEmoji('🤝').setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId('partner_stats').setLabel('Statistics').setEmoji('📊').setStyle(ButtonStyle.Secondary), - new ButtonBuilder().setCustomId('partner_settings').setLabel('Settings').setEmoji('⚙️').setStyle(ButtonStyle.Secondary), - )] }); + const pending = data.applications.filter(a => a.status === 'pending').length, active = data.partners.filter(p => p.status === 'active').length; + return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 إدارة الشراكات').setDescription('إدارة طلبات الشراكة والشراكات الحالية من هنا.').addFields( + { name: 'الشراكات الحالية', value: String(active), inline: true }, { name: 'الطلبات المعلقة', value: String(pending), inline: true }, { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true }, + )], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_pending').setLabel('الطلبات').setEmoji('🟡').setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId('partner_active').setLabel('الشركاء').setEmoji('🤝').setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId('partner_stats').setLabel('الإحصائيات').setEmoji('📊').setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId('partner_settings').setLabel('الإعدادات').setEmoji('⚙️').setStyle(ButtonStyle.Secondary))] }); }}, { name: 'partner_accept', async execute(interaction, client, args) { return review(interaction, client, args[0], 'accepted'); } }, { name: 'partner_reject', async execute(interaction, client, args) { return review(interaction, client, args[0], 'rejected'); } }, ]; async function review(interaction, client, id, status) { - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return interaction.reply({ content: '❌ You need Manage Server permission.', ephemeral: true }); + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return interaction.reply({ content: '❌ تحتاج صلاحية إدارة السيرفر.', ephemeral: true }); await interaction.deferUpdate(); const data = await getPartnerData(client, interaction.guildId); const app = data.applications.find(a => String(a.id) === String(id)); - if (!app || app.status !== 'pending') return interaction.followUp({ content: '❌ Partnership request not found or already reviewed.', ephemeral: true }); - app.status = status; - app.reviewedBy = interaction.user.id; - app.reviewedAt = new Date().toISOString(); + if (!app || app.status !== 'pending') return interaction.followUp({ content: '❌ طلب الشراكة غير موجود أو تمت مراجعته مسبقًا.', ephemeral: true }); + app.status = status; app.reviewedBy = interaction.user.id; app.reviewedAt = new Date().toISOString(); if (status === 'accepted') data.partners.push({ ...app, status: 'active', acceptedAt: app.reviewedAt }); await savePartnerData(client, interaction.guildId, data); const channel = interaction.guild.channels.cache.get(data.requestChannelId); const message = channel ? await channel.messages.fetch(app.messageId).catch(() => null) : null; - if (message) await message.edit({ embeds: [applicationEmbed(app, interaction.guild)], components: applicationButtons(app) }); - return interaction.followUp({ content: `✅ Partnership request #${app.id} ${status}.`, ephemeral: true }); + if (message) await message.edit({ embeds: [applicationEmbed(app)], components: applicationButtons(app) }); + if (status === 'accepted' && data.announcementChannelId) { + const announcementChannel = interaction.guild.channels.cache.get(data.announcementChannelId); + if (announcementChannel?.isTextBased()) { + await announcementChannel.send({ embeds: [new EmbedBuilder().setColor(0x57f287).setTitle('🤝 شراكة جديدة').setDescription(`يسعدنا الإعلان عن شراكتنا الجديدة مع **${app.serverName}**!\n\n🔗 ${app.invite}\n👥 ${app.members} عضو\n\nنتمنى لهم ولنا التوفيق.`)] }); + } + } + return interaction.followUp({ content: status === 'accepted' ? `✅ تم قبول طلب الشراكة #${app.id} ونشر الإعلان.` : `✅ تم رفض طلب الشراكة #${app.id}.`, ephemeral: true }); } From 8221f75e0466b17dc007e89a2b34ee237418faf1 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:08:51 +0300 Subject: [PATCH 151/219] fix: localize partnership application responses --- src/interactions/modals/partnerApply.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/interactions/modals/partnerApply.js b/src/interactions/modals/partnerApply.js index d4d992e442..b52e75f9df 100644 --- a/src/interactions/modals/partnerApply.js +++ b/src/interactions/modals/partnerApply.js @@ -8,21 +8,21 @@ export default { const membersRaw = interaction.fields.getTextInputValue('members').trim(); const description = interaction.fields.getTextInputValue('description').trim(); const members = Number(membersRaw.replace(/[^0-9]/g, '')); - if (!server || !description || !Number.isFinite(members)) return interaction.reply({ content: '❌ Please provide valid application details.', ephemeral: true }); + if (!server || !description || !Number.isFinite(members)) return interaction.reply({ content: '❌ تأكد من تعبئة جميع بيانات الطلب بشكل صحيح.', ephemeral: true }); const data = await getPartnerData(client, interaction.guildId); - if (data.requirements.requireInvite && !/^https?:\/\/discord(?:\.gg|\.com\/invite)\//i.test(invite)) return interaction.reply({ content: '❌ Please provide a valid Discord invite link.', ephemeral: true }); - if (members < data.requirements.minMembers) return interaction.reply({ content: `❌ Your server needs at least ${data.requirements.minMembers} members.`, ephemeral: true }); + if (data.requirements.requireInvite && !/^https?:\/\/discord(?:\.gg|\.com\/invite)\//i.test(invite)) return interaction.reply({ content: '❌ أرسل رابط دعوة صالح لسيرفرك.', ephemeral: true }); + if (members < data.requirements.minMembers) return interaction.reply({ content: `❌ يجب أن يحتوي سيرفرك على ${data.requirements.minMembers} عضوًا على الأقل.`, ephemeral: true }); const duplicate = data.applications.find(a => a.applicantId === interaction.user.id && a.status === 'pending'); - if (duplicate) return interaction.reply({ content: `❌ You already have pending partnership request #${duplicate.id}.`, ephemeral: true }); + if (duplicate) return interaction.reply({ content: `❌ لديك طلب شراكة معلق بالفعل (#${duplicate.id}).`, ephemeral: true }); data.counter += 1; const app = { id: data.counter, serverName: server, invite, members, description, applicantId: interaction.user.id, status: 'pending', createdAt: new Date().toISOString(), messageId: null }; const target = interaction.guild.channels.cache.get(data.requestChannelId); - if (!target) return interaction.reply({ content: '❌ Partnership request channel is missing. Run `/partner setup` again.', ephemeral: true }); - const message = await target.send({ embeds: [applicationEmbed(app, interaction.guild)], components: applicationButtons(app) }); + if (!target) return interaction.reply({ content: '❌ روم طلبات الشراكة غير موجود. شغّل `/partner setup` مرة أخرى.', ephemeral: true }); + const message = await target.send({ embeds: [applicationEmbed(app)], components: applicationButtons(app) }); app.messageId = message.id; data.applications.push(app); await savePartnerData(client, interaction.guildId, data); - return interaction.reply({ content: `✅ Partnership request #${app.id} submitted.`, ephemeral: true }); + return interaction.reply({ content: `✅ تم إرسال طلب الشراكة #${app.id} بنجاح.`, ephemeral: true }); }, }; From 34dce34870f0bad3ddfcb0752eb700e1ee5def8e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:12:52 +0300 Subject: [PATCH 152/219] fix: migrate legacy 500 member partnership requirement to 20 --- src/utils/partner.js | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/utils/partner.js b/src/utils/partner.js index 2c58f38df8..b10f59c256 100644 --- a/src/utils/partner.js +++ b/src/utils/partner.js @@ -9,7 +9,11 @@ export async function getPartnerData(client, guildId) { data.counter = Number(data.counter || 0); data.applications = Array.isArray(data.applications) ? data.applications : []; data.partners = Array.isArray(data.partners) ? data.partners : []; - data.requirements = { minMembers: Math.max(20, Number(data.requirements?.minMembers ?? 20)), requireInvite: data.requirements?.requireInvite !== false, requireActive: data.requirements?.requireActive !== false }; + const storedMin = Number(data.requirements?.minMembers); + // 500 was the old hard-coded default. Migrate that legacy value to the new default of 20. + const minMembers = !Number.isFinite(storedMin) || storedMin === 500 ? 20 : Math.max(20, storedMin); + data.requirements = { minMembers, requireInvite: data.requirements?.requireInvite !== false, requireActive: data.requirements?.requireActive !== false }; + if (storedMin === 500) await save(client, guildId, data); return data; } export async function savePartnerData(client, guildId, data) { return save(client, guildId, data); } @@ -46,11 +50,17 @@ export async function partnerDashboard(interaction) { const data = await getPartnerData(interaction.client, interaction.guildId); const pending = data.applications.filter(a => a.status === 'pending').length; const active = data.partners.filter(p => p.status === 'active').length; - const embed = new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 إدارة الشراكات').setDescription('إدارة طلبات الشراكة والشركاء من هنا.').addFields( - { name: 'الشراكات الحالية', value: String(active), inline: true }, { name: 'الطلبات المعلقة', value: String(pending), inline: true }, { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true }, { name: 'الحد الأدنى', value: '20 عضوًا', inline: true }, { name: 'روم الإعلانات', value: data.announcementChannelId ? `<#${data.announcementChannelId}>` : 'غير محدد' }, + const embed = new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 إدارة الشراكات').setDescription('إدارة طلبات الشراكة والشراكات الحالية من هنا.').addFields( + { name: 'الشراكات الحالية', value: String(active), inline: true }, + { name: 'الطلبات المعلقة', value: String(pending), inline: true }, + { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true }, + { name: 'الحد الأدنى', value: `${data.requirements.minMembers} عضوًا`, inline: true }, ); const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId('partner_pending').setLabel('الطلبات').setEmoji('🟡').setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId('partner_active').setLabel('الشركاء').setEmoji('🤝').setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId('partner_stats').setLabel('الإحصائيات').setEmoji('📊').setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId('partner_settings').setLabel('الإعدادات').setEmoji('⚙️').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('partner_pending').setLabel('الطلبات').setEmoji('🟡').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('partner_active').setLabel('الشركاء').setEmoji('🤝').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId('partner_stats').setLabel('الإحصائيات').setEmoji('📊').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('partner_settings').setLabel('الإعدادات').setEmoji('⚙️').setStyle(ButtonStyle.Secondary), ); return interaction.reply({ embeds: [embed], components: [row], ephemeral: true }); } @@ -58,7 +68,21 @@ export async function partnerDashboard(interaction) { export function applicationEmbed(app) { const status = app.status === 'accepted' ? '🟢 مقبول' : app.status === 'rejected' ? '🔴 مرفوض' : '🟡 قيد المراجعة'; return new EmbedBuilder().setColor(app.status === 'accepted' ? 0x57f287 : app.status === 'rejected' ? 0xed4245 : 0x5865f2).setTitle(`🤝 طلب شراكة #${app.id}`).addFields( - { name: 'السيرفر', value: app.serverName, inline: true }, { name: 'عدد الأعضاء', value: String(app.members), inline: true }, { name: 'الحالة', value: status, inline: true }, { name: 'رابط الدعوة', value: app.invite }, { name: 'مقدم الطلب', value: `<@${app.applicantId}>`, inline: true }, { name: 'وصف السيرفر', value: app.description || 'لا يوجد وصف.' }, { name: 'تاريخ الطلب', value: `` }, ...(app.reviewedBy ? [{ name: 'تمت المراجعة بواسطة', value: `<@${app.reviewedBy}>`, inline: true }] : []), + { name: 'السيرفر', value: app.serverName, inline: true }, + { name: 'عدد الأعضاء', value: String(app.members), inline: true }, + { name: 'الحالة', value: status, inline: true }, + { name: 'رابط الدعوة', value: app.invite }, + { name: 'مقدم الطلب', value: `<@${app.applicantId}>`, inline: true }, + { name: 'وصف السيرفر', value: app.description || 'لا يوجد وصف.' }, + { name: 'تاريخ الطلب', value: `` }, + ...(app.reviewedBy ? [{ name: 'تمت المراجعة بواسطة', value: `<@${app.reviewedBy}>`, inline: true }] : []), ); } -export function applicationButtons(app) { if (app.status !== 'pending') return []; return [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`partner_accept:${app.id}`).setLabel('قبول').setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId(`partner_reject:${app.id}`).setLabel('رفض').setStyle(ButtonStyle.Danger))]; } + +export function applicationButtons(app) { + if (app.status !== 'pending') return []; + return [new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`partner_accept:${app.id}`).setLabel('قبول').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId(`partner_reject:${app.id}`).setLabel('رفض').setStyle(ButtonStyle.Danger), + )]; +} From 424e66133611baf69cb672a93f1641149483b042 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:21:07 +0300 Subject: [PATCH 153/219] fix: post accepted partnerships as plain mentions --- src/interactions/buttons/partner.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/interactions/buttons/partner.js b/src/interactions/buttons/partner.js index 83b4c9dc30..e227068ec1 100644 --- a/src/interactions/buttons/partner.js +++ b/src/interactions/buttons/partner.js @@ -25,9 +25,7 @@ export default [ { name: 'partner_stats', async execute(interaction) { const data = await getPartnerData(interaction.client, interaction.guildId); const accepted = data.applications.filter(a => a.status === 'accepted').length, rejected = data.applications.filter(a => a.status === 'rejected').length, pending = data.applications.filter(a => a.status === 'pending').length; - return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('📊 إحصائيات الشراكات').addFields( - { name: 'الشراكات الحالية', value: String(data.partners.filter(p => p.status === 'active').length), inline: true }, { name: 'المقبولة', value: String(accepted), inline: true }, { name: 'المرفوضة', value: String(rejected), inline: true }, { name: 'المعلقة', value: String(pending), inline: true }, { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true }, - )], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('رجوع').setStyle(ButtonStyle.Secondary))] }); + return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('📊 إحصائيات الشراكات').addFields({ name: 'الشراكات الحالية', value: String(data.partners.filter(p => p.status === 'active').length), inline: true }, { name: 'المقبولة', value: String(accepted), inline: true }, { name: 'المرفوضة', value: String(rejected), inline: true }, { name: 'المعلقة', value: String(pending), inline: true }, { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true })], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_back_dashboard').setLabel('رجوع').setStyle(ButtonStyle.Secondary))] }); }}, { name: 'partner_settings', async execute(interaction) { const data = await getPartnerData(interaction.client, interaction.guildId); @@ -41,9 +39,7 @@ export default [ { name: 'partner_back_dashboard', async execute(interaction) { const data = await getPartnerData(interaction.client, interaction.guildId); const pending = data.applications.filter(a => a.status === 'pending').length, active = data.partners.filter(p => p.status === 'active').length; - return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 إدارة الشراكات').setDescription('إدارة طلبات الشراكة والشراكات الحالية من هنا.').addFields( - { name: 'الشراكات الحالية', value: String(active), inline: true }, { name: 'الطلبات المعلقة', value: String(pending), inline: true }, { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true }, - )], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_pending').setLabel('الطلبات').setEmoji('🟡').setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId('partner_active').setLabel('الشركاء').setEmoji('🤝').setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId('partner_stats').setLabel('الإحصائيات').setEmoji('📊').setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId('partner_settings').setLabel('الإعدادات').setEmoji('⚙️').setStyle(ButtonStyle.Secondary))] }); + return interaction.update({ embeds: [new EmbedBuilder().setColor(0x5865f2).setTitle('🤝 إدارة الشراكات').setDescription('إدارة طلبات الشراكة والشراكات الحالية من هنا.').addFields({ name: 'الشراكات الحالية', value: String(active), inline: true }, { name: 'الطلبات المعلقة', value: String(pending), inline: true }, { name: 'إجمالي الطلبات', value: String(data.applications.length), inline: true })], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('partner_pending').setLabel('الطلبات').setEmoji('🟡').setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId('partner_active').setLabel('الشركاء').setEmoji('🤝').setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId('partner_stats').setLabel('الإحصائيات').setEmoji('📊').setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId('partner_settings').setLabel('الإعدادات').setEmoji('⚙️').setStyle(ButtonStyle.Secondary))] }); }}, { name: 'partner_accept', async execute(interaction, client, args) { return review(interaction, client, args[0], 'accepted'); } }, { name: 'partner_reject', async execute(interaction, client, args) { return review(interaction, client, args[0], 'rejected'); } }, @@ -64,7 +60,8 @@ async function review(interaction, client, id, status) { if (status === 'accepted' && data.announcementChannelId) { const announcementChannel = interaction.guild.channels.cache.get(data.announcementChannelId); if (announcementChannel?.isTextBased()) { - await announcementChannel.send({ embeds: [new EmbedBuilder().setColor(0x57f287).setTitle('🤝 شراكة جديدة').setDescription(`يسعدنا الإعلان عن شراكتنا الجديدة مع **${app.serverName}**!\n\n🔗 ${app.invite}\n👥 ${app.members} عضو\n\nنتمنى لهم ولنا التوفيق.`)] }); + const text = `@everyone @here\n\n🤝 **شراكة جديدة!**\n\nيسرنا الإعلان عن شراكتنا مع **${app.serverName}**!\n\n📝 **تعريف السيرفر:**\n${app.description}\n\n🔗 **رابط السيرفر:** ${app.invite}\n\nنتمنى لهم ولنا التوفيق!`; + await announcementChannel.send({ content: text, allowedMentions: { parse: ['everyone'] } }); } } return interaction.followUp({ content: status === 'accepted' ? `✅ تم قبول طلب الشراكة #${app.id} ونشر الإعلان.` : `✅ تم رفض طلب الشراكة #${app.id}.`, ephemeral: true }); From c3478cb8b556ac250cc92032f41fa9ecd1f65875 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:38:23 +0300 Subject: [PATCH 154/219] fix: simplify partnership announcement format --- src/interactions/buttons/partner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interactions/buttons/partner.js b/src/interactions/buttons/partner.js index e227068ec1..71ce30e2eb 100644 --- a/src/interactions/buttons/partner.js +++ b/src/interactions/buttons/partner.js @@ -60,7 +60,7 @@ async function review(interaction, client, id, status) { if (status === 'accepted' && data.announcementChannelId) { const announcementChannel = interaction.guild.channels.cache.get(data.announcementChannelId); if (announcementChannel?.isTextBased()) { - const text = `@everyone @here\n\n🤝 **شراكة جديدة!**\n\nيسرنا الإعلان عن شراكتنا مع **${app.serverName}**!\n\n📝 **تعريف السيرفر:**\n${app.description}\n\n🔗 **رابط السيرفر:** ${app.invite}\n\nنتمنى لهم ولنا التوفيق!`; + const text = `🤝 **${app.serverName}**\n\n${app.description}\n\n${app.invite}\n\n@everyone @here`; await announcementChannel.send({ content: text, allowedMentions: { parse: ['everyone'] } }); } } From 3b4dbd45c40d510c2ddf82f5fdb9df73932b2440 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:39:43 +0300 Subject: [PATCH 155/219] fix: restore partnership announcement title --- src/interactions/buttons/partner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interactions/buttons/partner.js b/src/interactions/buttons/partner.js index 71ce30e2eb..7c59d61d57 100644 --- a/src/interactions/buttons/partner.js +++ b/src/interactions/buttons/partner.js @@ -60,7 +60,7 @@ async function review(interaction, client, id, status) { if (status === 'accepted' && data.announcementChannelId) { const announcementChannel = interaction.guild.channels.cache.get(data.announcementChannelId); if (announcementChannel?.isTextBased()) { - const text = `🤝 **${app.serverName}**\n\n${app.description}\n\n${app.invite}\n\n@everyone @here`; + const text = `🤝 **شراكة جديدة**\n\n**${app.serverName}**\n\n${app.description}\n\n${app.invite}\n\n@everyone @here`; await announcementChannel.send({ content: text, allowedMentions: { parse: ['everyone'] } }); } } From 3eb97b0e71cf8f9b403208c108707e317ecc564e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:45:13 +0300 Subject: [PATCH 156/219] fix: enforce strike-only escalation and whitelist bypass --- src/services/security/securityService.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js index 8da11ea188..c0de5ccf88 100644 --- a/src/services/security/securityService.js +++ b/src/services/security/securityService.js @@ -27,17 +27,17 @@ export const SECURITY_DEFAULTS = { }, autoMod: { enabled: true, - spam: { enabled: true, maxMessages: 6, windowMs: 5000, punishment: 'timeout' }, - duplicate: { enabled: true, maxRepeats: 3, windowMs: 10000, punishment: 'timeout' }, - mentions: { enabled: true, max: 6, punishment: 'timeout' }, - caps: { enabled: false, ratio: 0.8, minLength: 12, punishment: 'warn' }, + spam: { enabled: true, maxMessages: 6, windowMs: 5000, punishment: 'delete' }, + duplicate: { enabled: true, maxRepeats: 3, windowMs: 10000, punishment: 'delete' }, + mentions: { enabled: true, max: 6, punishment: 'delete' }, + caps: { enabled: false, ratio: 0.8, minLength: 12, punishment: 'delete' }, invites: { enabled: true, punishment: 'delete' }, links: { enabled: false, punishment: 'delete' }, - badWords: { enabled: false, words: [], punishment: 'timeout' }, + badWords: { enabled: false, words: [], punishment: 'delete' }, action: 'delete', }, escalation: [ - { strike: 1, action: 'warn', durationMs: 0 }, + { strike: 1, action: 'delete', durationMs: 0 }, { strike: 2, action: 'timeout', durationMs: 60 * 1000 }, { strike: 3, action: 'timeout', durationMs: 10 * 60 * 1000 }, { strike: 4, action: 'timeout', durationMs: 60 * 60 * 1000 }, @@ -68,6 +68,7 @@ export async function getSecurityConfig(client, guildId) { const config = deepMerge(clone(SECURITY_DEFAULTS), stored || {}); config.antiNuke.punishments = { ...SECURITY_DEFAULTS.antiNuke.punishments, ...(config.antiNuke.punishments || {}) }; for (const type of AutoModTypes) config.autoMod[type].punishment ||= SECURITY_DEFAULTS.autoMod[type].punishment; + config.escalation = (config.escalation || []).map(level => ({ ...level, action: level.action === 'warn' ? 'delete' : level.action })); return config; } catch (error) { logger.error('Failed to load security config', { guildId, error: error.message }); @@ -77,6 +78,7 @@ export async function getSecurityConfig(client, guildId) { export async function updateSecurityConfig(client, guildId, patch) { const current = await getSecurityConfig(client, guildId); const updated = deepMerge(current, patch); + updated.escalation = (updated.escalation || []).map(level => ({ ...level, action: level.action === 'warn' ? 'delete' : level.action })); await setInDb(configKey(guildId), updated); return updated; } @@ -139,10 +141,6 @@ async function executeAutoModAction(message, action, duration, reason) { if (action === 'timeout' && member?.moderatable) await member.timeout(Math.min(Math.max(duration || 60000, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); else if (action === 'kick' && member?.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); else if (action === 'ban' && member?.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); - else if (action === 'warn') { - const warning = await message.channel.send(`⚠️ <@${message.author.id}> your message was removed: **${reason}**.`).catch(() => null); - if (warning) setTimeout(() => warning.delete().catch(() => {}), 5000); - } } export async function processAutoMod(message, client) { @@ -174,7 +172,7 @@ export async function processAutoMod(message, client) { const strike = await addStrike(client, message.guild.id, message.author.id, reason); const escalation = config.escalation?.find(item => item.strike === strike.count); const primary = violations[0]; - const action = escalation?.action || a[primary.type]?.punishment || a.action || 'delete'; + const action = (escalation?.action || a[primary.type]?.punishment || a.action || 'delete') === 'warn' ? 'delete' : (escalation?.action || a[primary.type]?.punishment || a.action || 'delete'); const duration = escalation?.durationMs || 60000; await message.delete().catch(() => {}); From 5d5cd8dbabef59edc6cbf46c5424660ef93dba21 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:45:25 +0300 Subject: [PATCH 157/219] fix: restore whitelist and strike controls --- src/handlers/securityDashboardFixes.js | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/handlers/securityDashboardFixes.js diff --git a/src/handlers/securityDashboardFixes.js b/src/handlers/securityDashboardFixes.js new file mode 100644 index 0000000000..1fa4439738 --- /dev/null +++ b/src/handlers/securityDashboardFixes.js @@ -0,0 +1,57 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; +import { getSecurityConfig, updateSecurityConfig, getStrikes, clearStrikes, sendSecurityLog } from '../services/security/securityService.js'; + +const ok = i => i.customId.split(':').at(-1) === i.user.id; +const deny = i => i.reply({ content: 'This security dashboard belongs to another moderator.', flags: MessageFlags.Ephemeral }); +const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); +const button = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); +const modalField = (id, label, value = '', style = TextInputStyle.Paragraph) => new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false).setValue(String(value).slice(0, 4000))); + +async function whitelist(i, c) { + const x = await getSecurityConfig(c, i.guildId); + return i.update({ + embeds: [new EmbedBuilder().setColor(0x57f287).setTitle('👤 Whitelist').setDescription(`الحسابات المضافة هنا تتجاوز إجراءات الحماية.\n\n**الأشخاص:** ${x.whitelist.users.length}\n**الرتب:** ${x.whitelist.roles.length}\n**البوتات:** ${x.whitelist.bots.length}`)], + components: [row(button(`security_back2:${i.user.id}`, '← Back'), button(`wl_users2:${i.user.id}`, '👤 الأشخاص', ButtonStyle.Primary), button(`wl_roles2:${i.user.id}`, '🎭 الرتب', ButtonStyle.Primary), button(`wl_bots2:${i.user.id}`, '🤖 البوتات', ButtonStyle.Primary))], + }); +} + +function showWhitelistModal(i, type, title, label, current) { + return i.showModal(new ModalBuilder().setCustomId(`security_wl_${type}_modal:${i.user.id}`).setTitle(title).addComponents(modalField('value', label, current))); +} + +async function strikes(i, c) { + const members = await i.guild.members.fetch().catch(() => i.guild.members.cache); + const entries = []; + for (const m of members.values()) { + if (m.user.bot) continue; + const s = await getStrikes(c, i.guildId, m.id).catch(() => ({ count: 0 })); + if (s.count) entries.push({ id: m.id, count: Number(s.count) }); + } + entries.sort((a, b) => b.count - a.count); + const text = entries.slice(0, 10).map((e, n) => `${n + 1}. <@${e.id}> — **${e.count}** strikes`).join('\n') || 'لا توجد Strikes فعالة'; + const rows = [row(button(`security_back2:${i.user.id}`, '← Back'), button(`security_strikes_refresh2:${i.user.id}`, '🔄 Refresh', ButtonStyle.Success))]; + if (entries.length) rows.push(row(...entries.slice(0, 4).map(e => button(`security_strike_manage:${e.id}:${i.user.id}`, `Manage ${e.count}`, ButtonStyle.Primary)))); + return i.update({ embeds: [new EmbedBuilder().setColor(0xfee75c).setTitle('🏆 Strikes').setDescription(`**${i.guild.name}**\n\n${text}\n\nيمكنك تصفير الـStrike من صفحة العضو.`)], components: rows }); +} + +async function strikeMember(i, c, userId) { + const s = await getStrikes(c, i.guildId, userId).catch(() => ({ count: 0, lastReason: '' })); + return i.update({ + embeds: [new EmbedBuilder().setColor(0xfee75c).setTitle('🏆 إدارة Strikes').setDescription(`<@${userId}>\n\n**Strikes:** ${s.count || 0}\n**آخر سبب:** ${s.lastReason || '—'}`)], + components: [row(button(`security_strike_reset:${userId}:${i.user.id}`, '🧹 Reset Strikes', ButtonStyle.Danger), button(`security_strikes_refresh2:${i.user.id}`, '← Back'))], + }); +} + +export default [ + { name: 'security_panel_whitelist2', execute: async (i, c) => ok(i) ? whitelist(i, c) : deny(i) }, + { name: 'wl_users2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return showWhitelistModal(i, 'users', 'Whitelist Users', 'User IDs, one per line', x.whitelist.users.join('\n')); } }, + { name: 'wl_roles2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return showWhitelistModal(i, 'roles', 'Whitelist Roles', 'Role IDs, one per line', x.whitelist.roles.join('\n')); } }, + { name: 'wl_bots2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return showWhitelistModal(i, 'bots', 'Whitelist Bots', 'Bot IDs, one per line', x.whitelist.bots.join('\n')); } }, + { name: 'security_panel_strikes2', execute: async (i, c) => ok(i) ? strikes(i, c) : deny(i) }, + { name: 'security_strikes_refresh2', execute: async (i, c) => ok(i) ? strikes(i, c) : deny(i) }, + { name: 'security_strike_manage', execute: async (i, c, args) => ok(i) ? strikeMember(i, c, args[0]) : deny(i) }, + { name: 'security_strike_reset', execute: async (i, c, args) => { if (!ok(i)) return deny(i); const userId = args[0]; await clearStrikes(c, i.guildId, userId); await sendSecurityLog(c, i.guild, { title: 'Strikes Reset', description: `<@${userId}> strikes reset by <@${i.user.id}>`, color: 0x57f287 }); return strikeMember(i, c, userId); } }, + { name: 'security_wl_users_modal', execute: async (i, c) => { if (!ok(i)) return deny(i); const users = String(i.fields.getTextInputValue('value') || '').split(/\s+/).filter(Boolean).slice(0, 100); await updateSecurityConfig(c, i.guildId, { whitelist: { users } }); return whitelist(i, c); } }, + { name: 'security_wl_roles_modal', execute: async (i, c) => { if (!ok(i)) return deny(i); const roles = String(i.fields.getTextInputValue('value') || '').split(/\s+/).filter(Boolean).slice(0, 100); await updateSecurityConfig(c, i.guildId, { whitelist: { roles } }); return whitelist(i, c); } }, + { name: 'security_wl_bots_modal', execute: async (i, c) => { if (!ok(i)) return deny(i); const bots = String(i.fields.getTextInputValue('value') || '').split(/\s+/).filter(Boolean).slice(0, 100); await updateSecurityConfig(c, i.guildId, { whitelist: { bots } }); return whitelist(i, c); } }, +]; From 6a904611ab26b7ae4a2e4c27033bb0f2f8a29daf Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:45:32 +0300 Subject: [PATCH 158/219] fix: register security whitelist and strike fixes --- src/interactions/buttons/security/security.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index e6d85a589e..122a5105e5 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -2,13 +2,14 @@ import securityDashboardOverrides from '../../../handlers/securityDashboardOverr import { securityDashboardButtonHandlers } from '../../../handlers/securityDashboardCore.js'; import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRuleHandlers.js'; import securityAutoModDashboard from '../../../handlers/securityAutoModDashboard.js'; +import securityDashboardFixes from '../../../handlers/securityDashboardFixes.js'; -// Compatibility handlers must be registered first so legacy message buttons -// resolve to the intended dashboard/AutoMod behavior before duplicate IDs from -// the newer dashboard handlers are encountered. +// Compatibility handlers first, then the core dashboard, then targeted fixes. +// The final array order intentionally lets the fixes replace broken/legacy IDs. export default [ ...securityDashboardOverrides, ...securityDashboardButtonHandlers, ...securityDashboardRuleHandlers, ...securityAutoModDashboard, + ...securityDashboardFixes, ]; From 856e069a5beb73adae7a66addafa89502d4ea68a Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:45:46 +0300 Subject: [PATCH 159/219] fix: register security whitelist modal fixes --- src/interactions/modals/security/security.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/interactions/modals/security/security.js b/src/interactions/modals/security/security.js index ea0ec8ed46..65fabbf9f3 100644 --- a/src/interactions/modals/security/security.js +++ b/src/interactions/modals/security/security.js @@ -1,9 +1,14 @@ import { securityModalHandlers } from '../../../handlers/securityHandlers.js'; import { securityAdvancedModalHandlers } from '../../../handlers/securityAdvancedHandlers.js'; import { securityDashboardModalHandlers } from '../../../handlers/securityDashboardHandlers.js'; +import securityDashboardFixes from '../../../handlers/securityDashboardFixes.js'; + +const ok = i => i.customId.split(':').at(-1) === i.user.id; +const deny = i => i.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); export default [ ...securityModalHandlers, ...securityAdvancedModalHandlers, ...securityDashboardModalHandlers, + ...securityDashboardFixes.filter(h => h.name.endsWith('_modal')), ]; From 48b6509b6724ba6f9eadf527626927b8c79ffe55 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:45:58 +0300 Subject: [PATCH 160/219] fix: remove warning punishment option --- src/handlers/securityDashboardRuleHandlers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/handlers/securityDashboardRuleHandlers.js b/src/handlers/securityDashboardRuleHandlers.js index 958c5afe50..b2bb1e8e6f 100644 --- a/src/handlers/securityDashboardRuleHandlers.js +++ b/src/handlers/securityDashboardRuleHandlers.js @@ -2,7 +2,7 @@ import { getSecurityConfig, updateSecurityConfig } from '../services/security/se import { ButtonStyle, ActionRowBuilder, ButtonBuilder, EmbedBuilder } from 'discord.js'; const NUKE = ['strip', 'kick', 'ban']; -const AUTO = ['delete', 'warn', 'timeout', 'kick', 'ban']; +const AUTO = ['delete', 'timeout', 'kick', 'ban']; const AUTO_LABELS = { spam: 'Spam', duplicate: 'Duplicate', mentions: 'Mentions', invites: 'Invites', links: 'Links', caps: 'Caps', badWords: 'Bad Words' }; const ok = i => i.customId.split(':').at(-1) === i.user.id; const deny = i => i.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); @@ -15,11 +15,11 @@ async function autoPage(i, c) { const x = await getSecurityConfig(c, i.guildId); const rows = [row(B(`punishments_back2:${i.user.id}`, '← Punishments'))]; for (let n = 0; n < autoKeys.length; n += 4) rows.push(row(...autoKeys.slice(n, n + 4).map(k => B(`auto_pun2:${k}:${i.user.id}`, `${AUTO_LABELS[k]}: ${x.autoMod[k].punishment}`, ButtonStyle.Primary)))); - return i.update({ embeds: [new EmbedBuilder().setTitle('🤖 AutoMod Punishments').setDescription('Every AutoMod rule has an independent punishment. Click a rule to cycle: Delete → Warn → Timeout → Kick → Ban.').setColor(0x5865f2)], components: rows }); + return i.update({ embeds: [new EmbedBuilder().setTitle('🤖 AutoMod Punishments').setDescription('Every AutoMod rule has an independent punishment. Click a rule to cycle: Delete → Timeout → Kick → Ban.').setColor(0x5865f2)], components: rows }); } export default [ - { name: 'nuke_rule', execute: async (i, c) => { if (!ok(i)) return deny(i); const key = i.customId.split(':').at(-2); const x = await getSecurityConfig(c, i.guildId); const current = x.antiNuke.punishments[key] || 'strip'; await updateSecurityConfig(c, i.guildId, { antiNuke: { punishments: { [key]: cycle(current, NUKE) } } }); return i.update({ content: null, embeds: [new EmbedBuilder().setTitle('⚖️ Anti-Nuke Punishment Updated').setDescription(`**${key}** punishment is now **${cycle(current, NUKE)}**.`).setColor(0xed4245)], components: [row(B(`security_back2:${i.user.id}`, '← Back'))] }); } }, + { name: 'nuke_rule', execute: async (i, c) => { if (!ok(i)) return deny(i); const key = i.customId.split(':').at(-2); const x = await getSecurityConfig(c, i.guildId); const current = x.antiNuke.punishments[key] || 'strip'; const next = cycle(current, NUKE); await updateSecurityConfig(c, i.guildId, { antiNuke: { punishments: { [key]: next } } }); return i.update({ content: null, embeds: [new EmbedBuilder().setTitle('⚖️ Anti-Nuke Punishment Updated').setDescription(`**${key}** punishment is now **${next}**.`).setColor(0xed4245)], components: [row(B(`security_back2:${i.user.id}`, '← Back'))] }); } }, { name: 'pun_auto2', execute: async (i, c) => ok(i) ? autoPage(i, c) : deny(i) }, - { name: 'auto_pun2', execute: async (i, c) => { if (!ok(i)) return deny(i); const key = i.customId.split(':').at(-2); const x = await getSecurityConfig(c, i.guildId); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { punishment: cycle(x.autoMod[key].punishment, AUTO) } } }); return autoPage(i, c); } }, + { name: 'auto_pun2', execute: async (i, c) => { if (!ok(i)) return deny(i); const key = i.customId.split(':').at(-2); const x = await getSecurityConfig(c, i.guildId); const next = cycle(x.autoMod[key].punishment, AUTO); await updateSecurityConfig(c, i.guildId, { autoMod: { [key]: { punishment: next } } }); return autoPage(i, c); } }, ]; From 9ac54e06b3bd34ea3bc0e1635fc10922ef53e1e7 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:49:49 +0300 Subject: [PATCH 161/219] fix: support legacy strike management buttons --- .../security/securityStrikeCompatibility.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/interactions/buttons/security/securityStrikeCompatibility.js diff --git a/src/interactions/buttons/security/securityStrikeCompatibility.js b/src/interactions/buttons/security/securityStrikeCompatibility.js new file mode 100644 index 0000000000..654e5eb7e3 --- /dev/null +++ b/src/interactions/buttons/security/securityStrikeCompatibility.js @@ -0,0 +1,33 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { getStrikes, clearStrikes, sendSecurityLog } from '../../../services/security/securityService.js'; + +const ok = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; +const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); +const button = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); + +async function manage(interaction, client, userId) { + const strike = await getStrikes(client, interaction.guildId, userId).catch(() => ({ count: 0, lastReason: '' })); + return interaction.update({ + embeds: [new EmbedBuilder().setColor(0xfee75c).setTitle('🏆 إدارة Strikes').setDescription(`<@${userId}>\n\n**Strikes:** ${strike.count || 0}\n**آخر سبب:** ${strike.lastReason || '—'}`)], + components: [new ActionRowBuilder().addComponents( + button(`strike_reset2:${userId}:${interaction.user.id}`, '🧹 Reset Strikes', ButtonStyle.Danger), + button(`strikes_back2:${interaction.user.id}`, '← Back') + )] + }); +} + +async function reset(interaction, client, userId) { + await clearStrikes(client, interaction.guildId, userId); + await sendSecurityLog(client, interaction.guild, { + title: 'Strikes Reset', + description: `<@${userId}> strikes reset by <@${interaction.user.id}>`, + color: 0x57f287, + }); + return manage(interaction, client, userId); +} + +export default [ + { name: 'strike_manage2', execute: async (i, c, args) => ok(i) ? manage(i, c, args[0]) : deny(i) }, + { name: 'strike_reset2', execute: async (i, c, args) => ok(i) ? reset(i, c, args[0]) : deny(i) }, + { name: 'strikes_back2', execute: async (i, c) => ok(i) ? i.update({ components: [new ActionRowBuilder().addComponents(button(`security_back2:${i.user.id}`, '← Back'))] }) : deny(i) }, +]; \ No newline at end of file From 05db4bcb95df5261c75f3def0bf1e272851f4d84 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:52:00 +0300 Subject: [PATCH 162/219] fix: enforce security whitelist before all automod actions --- src/services/security/securityService.js | 43 +++++++++++++++++++----- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js index c0de5ccf88..0f07b5b28b 100644 --- a/src/services/security/securityService.js +++ b/src/services/security/securityService.js @@ -62,10 +62,21 @@ function deepMerge(base, patch) { function configKey(guildId) { return `security:config:${guildId}`; } function strikeKey(guildId, userId) { return `security:strikes:${guildId}:${userId}`; } +function normalizeIdList(value) { + if (Array.isArray(value)) return value.map(String).filter(Boolean); + if (value instanceof Set) return [...value].map(String).filter(Boolean); + if (value && typeof value === 'object') return Object.keys(value).map(String).filter(Boolean); + return []; +} + export async function getSecurityConfig(client, guildId) { try { const stored = await getFromDb(configKey(guildId), null); const config = deepMerge(clone(SECURITY_DEFAULTS), stored || {}); + config.whitelist = config.whitelist || {}; + config.whitelist.users = normalizeIdList(config.whitelist.users); + config.whitelist.roles = normalizeIdList(config.whitelist.roles); + config.whitelist.bots = normalizeIdList(config.whitelist.bots); config.antiNuke.punishments = { ...SECURITY_DEFAULTS.antiNuke.punishments, ...(config.antiNuke.punishments || {}) }; for (const type of AutoModTypes) config.autoMod[type].punishment ||= SECURITY_DEFAULTS.autoMod[type].punishment; config.escalation = (config.escalation || []).map(level => ({ ...level, action: level.action === 'warn' ? 'delete' : level.action })); @@ -78,6 +89,10 @@ export async function getSecurityConfig(client, guildId) { export async function updateSecurityConfig(client, guildId, patch) { const current = await getSecurityConfig(client, guildId); const updated = deepMerge(current, patch); + updated.whitelist = updated.whitelist || {}; + updated.whitelist.users = normalizeIdList(updated.whitelist.users); + updated.whitelist.roles = normalizeIdList(updated.whitelist.roles); + updated.whitelist.bots = normalizeIdList(updated.whitelist.bots); updated.escalation = (updated.escalation || []).map(level => ({ ...level, action: level.action === 'warn' ? 'delete' : level.action })); await setInDb(configKey(guildId), updated); return updated; @@ -97,13 +112,19 @@ export async function addStrike(client, guildId, userId, reason = 'AutoMod viola return next; } export async function clearStrikes(client, guildId, userId) { await setInDb(strikeKey(guildId, userId), { count: 0, updatedAt: Date.now() }); } + export function isWhitelisted(member, config) { if (!member) return false; - if (config.whitelist?.users?.includes(member.id)) return true; - if (member.roles?.cache?.some(role => config.whitelist?.roles?.includes(role.id))) return true; - if (member.user?.bot && config.whitelist?.bots?.includes(member.id)) return true; + const whitelist = config?.whitelist || {}; + const users = normalizeIdList(whitelist.users); + const roles = normalizeIdList(whitelist.roles); + const bots = normalizeIdList(whitelist.bots); + if (users.includes(String(member.id))) return true; + if (member.roles?.cache?.some(role => roles.includes(String(role.id)))) return true; + if (member.user?.bot && bots.includes(String(member.id))) return true; return false; } + export async function sendSecurityLog(client, guild, payload) { try { const config = await getSecurityConfig(client, guild.id); @@ -135,19 +156,23 @@ function capRatio(content) { return letters.length ? upper.length / letters.length : 0; } -async function executeAutoModAction(message, action, duration, reason) { +async function executeAutoModAction(message, action, duration, reason, config) { const member = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); + if (!member || isWhitelisted(member, config)) return; if (action === 'delete') return; - if (action === 'timeout' && member?.moderatable) await member.timeout(Math.min(Math.max(duration || 60000, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); - else if (action === 'kick' && member?.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); - else if (action === 'ban' && member?.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); + if (action === 'timeout' && member.moderatable) await member.timeout(Math.min(Math.max(duration || 60000, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); + else if (action === 'kick' && member.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); + else if (action === 'ban' && member.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); } export async function processAutoMod(message, client) { if (!message?.guild || message.author?.bot) return false; const config = await getSecurityConfig(client, message.guild.id); const a = config.autoMod; - if (!config.enabled || !a?.enabled || config.ignoredChannels?.includes(message.channel.id) || isWhitelisted(message.member, config)) return false; + if (!config.enabled || !a?.enabled || config.ignoredChannels?.includes(message.channel.id)) return false; + + const member = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); + if (!member || isWhitelisted(member, config)) return false; const now = Date.now(); const data = autoModData(message.guild.id, message.author.id); @@ -176,7 +201,7 @@ export async function processAutoMod(message, client) { const duration = escalation?.durationMs || 60000; await message.delete().catch(() => {}); - await executeAutoModAction(message, action, duration, reason); + await executeAutoModAction(message, action, duration, reason, config); await sendSecurityLog(client, message.guild, { title: 'AutoMod Triggered', description: `AutoMod acted on **${message.author.tag}**.`, fields: [ { name: 'Rule', value: primary.type, inline: true }, { name: 'Reason', value: reason.slice(0, 1024), inline: true }, From fe754c7c7acb4e5d6e5659d02788904ad7966ebb Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:59:14 +0300 Subject: [PATCH 163/219] fix: clean security strikes and restore dashboard handlers --- src/handlers/securityDashboardHandlers.js | 83 ++++++++--------------- 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/src/handlers/securityDashboardHandlers.js b/src/handlers/securityDashboardHandlers.js index 15b1ac4dfe..dc6a36ffbe 100644 --- a/src/handlers/securityDashboardHandlers.js +++ b/src/handlers/securityDashboardHandlers.js @@ -1,10 +1,9 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; import { getSecurityConfig, updateSecurityConfig, getStrikes, clearStrikes, sendSecurityLog } from '../services/security/securityService.js'; -import { WarningService } from '../services/moderation/warningService.js'; -const ACTIONS = ['delete', 'warn', 'timeout', 'kick', 'ban']; const NUKE_ACTIONS = ['strip', 'kick', 'ban']; const RAID_ACTIONS = ['timeout', 'kick', 'ban']; +const AUTO_ACTIONS = ['delete', 'timeout', 'kick', 'ban']; const NUKE_RULES = { channelDelete: 'Channel Delete', channelCreate: 'Channel Create', roleDelete: 'Role Delete', roleCreate: 'Role Create', roleUpdate: 'Role Update', webhookUpdate: 'Webhook Update', webhookDelete: 'Webhook Delete', ban: 'Ban', kick: 'Kick', botAdd: 'Bot Add', @@ -25,7 +24,7 @@ function embed(title, description, color = 0x5865f2, guild) { export async function buildSecurityDashboard(client, guild, userId) { const config = await getSecurityConfig(client, guild.id); return { - embeds: [embed('🛡️ Server Protection', `**${guild.name}**\n\nChoose a security system to configure.\n\n🛡️ Anti-Nuke — ${config.antiNuke.enabled ? '🟢' : '🔴'}\n🚨 Anti-Raid — ${config.antiRaid.enabled ? '🟢' : '🔴'}\n🤖 AutoMod — ${config.autoMod.enabled ? '🟢' : '🔴'}\n⚖️ Punishments — ${(config.escalation || []).length} escalation levels\n🏆 Strikes & Warnings — management enabled\n👤 Whitelist — ${(config.whitelist.users.length + config.whitelist.roles.length + config.whitelist.bots.length)} entries\n📋 Logs — ${config.logChannelId ? `<#${config.logChannelId}>` : 'not configured'}`, 0x57f287, guild)], + embeds: [embed('🛡️ Server Protection', `**${guild.name}**\n\nChoose a security system to configure.\n\n🛡️ Anti-Nuke — ${config.antiNuke.enabled ? '🟢' : '🔴'}\n🚨 Anti-Raid — ${config.antiRaid.enabled ? '🟢' : '🔴'}\n🤖 AutoMod — ${config.autoMod.enabled ? '🟢' : '🔴'}\n⚖️ Punishments — ${(config.escalation || []).length} escalation levels\n🏆 Strikes — management enabled\n👤 Whitelist — ${(config.whitelist.users.length + config.whitelist.roles.length + config.whitelist.bots.length)} entries\n📋 Logs — ${config.logChannelId ? `<#${config.logChannelId}>` : 'not configured'}`, 0x57f287, guild)], components: [ row( B(`security_panel_nuke2:${userId}`, '🛡️ Anti-Nuke', ButtonStyle.Danger), @@ -44,78 +43,58 @@ export async function buildSecurityDashboard(client, guild, userId) { }; } -async function dashboard(i, c) { - return i.update(await buildSecurityDashboard(c, i.guild, i.user.id)); -} +async function dashboard(i, c) { return i.update(await buildSecurityDashboard(c, i.guild, i.user.id)); } async function nuke(i, c) { - const x = await getSecurityConfig(c, i.guildId); - const t = x.antiNuke.thresholds; + const x = await getSecurityConfig(c, i.guildId), t = x.antiNuke.thresholds; const lines = Object.entries(NUKE_RULES).map(([k, n]) => `**${n}** — threshold \`${t[k] ?? 1}\` • punishment **${x.antiNuke.punishments[k] || x.antiNuke.action}**`).join('\n'); return i.update({ embeds: [embed('🛡️ Anti-Nuke', `**Status:** ${x.antiNuke.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Window:** ${x.antiNuke.windowMs / 1000}s\n**Lockdown:** ${x.antiNuke.lockdown ? '🟢 ON' : '🔴 OFF'}\n\n${lines}`, 0xed4245, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`nuke_toggle2:${i.user.id}`, x.antiNuke.enabled ? 'Disable' : 'Enable', x.antiNuke.enabled ? ButtonStyle.Success : ButtonStyle.Danger), B(`nuke_lock2:${i.user.id}`, `Lockdown: ${x.antiNuke.lockdown ? 'ON' : 'OFF'}`), B(`nuke_window2:${i.user.id}`, `Window: ${x.antiNuke.windowMs / 1000}s`)), row(B(`nuke_thresholds2:${i.user.id}`, '✏️ Thresholds', ButtonStyle.Primary), B(`nuke_rules2:${i.user.id}`, '⚖️ Rule Punishments', ButtonStyle.Primary))] }); } - async function nukeRules(i, c) { - const x = await getSecurityConfig(c, i.guildId); - const keys = Object.keys(NUKE_RULES); + const x = await getSecurityConfig(c, i.guildId), keys = Object.keys(NUKE_RULES); const rows = [row(B(`nuke_rules_back2:${i.user.id}`, '← Anti-Nuke'))]; for (let n = 0; n < keys.length; n += 4) rows.push(row(...keys.slice(n, n + 4).map(k => B(`nuke_rule:${k}:${i.user.id}`, `${NUKE_RULES[k]}: ${x.antiNuke.punishments[k] || 'strip'}`, ButtonStyle.Primary)))); - return i.update({ embeds: [embed('⚖️ Anti-Nuke Punishments', 'Each Anti-Nuke event has its own punishment. Click an event to cycle its action.', 0xed4245, i.guild)], components: rows.slice(0, 5) }); + return i.update({ embeds: [embed('⚖️ Anti-Nuke Punishments', 'Each Anti-Nuke event has its own punishment.', 0xed4245, i.guild)], components: rows.slice(0, 5) }); } - async function raid(i, c) { const x = await getSecurityConfig(c, i.guildId); return i.update({ embeds: [embed('🚨 Anti-Raid', `**Status:** ${x.antiRaid.enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Join burst:** ${x.antiRaid.joins} joins / ${x.antiRaid.windowMs / 1000}s\n**Minimum account age:** ${Math.round(x.antiRaid.minAccountAgeMs / 3600000)}h\n**Punishment:** **${x.antiRaid.punishment}**\n**Lockdown:** ${x.antiRaid.lockdown ? '🟢 ON' : '🔴 OFF'}`, 0xf47b67, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`raid_toggle2:${i.user.id}`, x.antiRaid.enabled ? 'Disable' : 'Enable', x.antiRaid.enabled ? ButtonStyle.Success : ButtonStyle.Danger), B(`raid_punishment2:${i.user.id}`, `Punishment: ${x.antiRaid.punishment}`, ButtonStyle.Primary)), row(B(`raid_joins2:${i.user.id}`, `Joins: ${x.antiRaid.joins}`), B(`raid_window2:${i.user.id}`, `Window: ${x.antiRaid.windowMs / 1000}s`), B(`raid_age2:${i.user.id}`, `Age: ${Math.round(x.antiRaid.minAccountAgeMs / 3600000)}h`), B(`raid_lock2:${i.user.id}`, `Lockdown: ${x.antiRaid.lockdown ? 'ON' : 'OFF'}`))] }); } - -async function punishments(i, c) { +async function automod(i, c) { const x = await getSecurityConfig(c, i.guildId); - const escalation = (x.escalation || []).map(e => `Strike **${e.strike}** → **${e.action}**${e.durationMs ? ` (${Math.round(e.durationMs / 60000)}m)` : ''}`).join('\n'); - return i.update({ embeds: [embed('⚖️ Punishments & Escalation', `Every security rule has an independent punishment.\n\n**Anti-Raid:** ${x.antiRaid.punishment}\n**Strike decay:** ${Math.round(x.strikeDecayMs / 3600000)}h\n\n**Escalation**\n${escalation}`, 0xfee75c, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`punishment_decay2:${i.user.id}`, '⏱️ Decay', ButtonStyle.Primary), B(`punishment_rules2:${i.user.id}`, '⚖️ Rule Punishments', ButtonStyle.Primary)), row(B(`punishment_escalation2:${i.user.id}`, '📈 Edit Escalation', ButtonStyle.Primary))] }); + const text = Object.entries(AUTO_RULES).map(([k, n]) => `${n}: **${x.autoMod[k]?.punishment || 'delete'}**`).join('\n'); + const keys = Object.keys(AUTO_RULES); + const rows = [row(B(`security_back2:${i.user.id}`, '← Back'), B(`automod_toggle2:${i.user.id}`, x.autoMod.enabled ? 'Disable' : 'Enable', x.autoMod.enabled ? ButtonStyle.Success : ButtonStyle.Danger))]; + for (let n = 0; n < keys.length; n += 4) rows.push(row(...keys.slice(n, n + 4).map(k => B(`auto_pun2:${k}:${i.user.id}`, `${AUTO_RULES[k]}: ${x.autoMod[k]?.punishment || 'delete'}`, ButtonStyle.Primary)))); + return i.update({ embeds: [embed('🤖 AutoMod', text, 0x5865f2, i.guild)], components: rows.slice(0, 3) }); } - -async function punishmentRules(i, c) { +async function punishments(i, c) { const x = await getSecurityConfig(c, i.guildId); - const rows = [row(B(`punishments_back2:${i.user.id}`, '← Punishments'))]; - rows.push(row(B(`pun_raid2:${i.user.id}`, `Raid: ${x.antiRaid.punishment}`, ButtonStyle.Primary), B(`pun_auto2:${i.user.id}`, '🤖 AutoMod Rules', ButtonStyle.Primary))); - const keys = Object.keys(NUKE_RULES); - for (let n = 0; n < keys.length; n += 4) rows.push(row(...keys.slice(n, n + 4).map(k => B(`nuke_rule:${k}:${i.user.id}`, `${NUKE_RULES[k]}: ${x.antiNuke.punishments[k]}`, ButtonStyle.Secondary)))); - return i.update({ embeds: [embed('⚖️ Rule Punishments', 'Choose the protection system, then change the punishment for the exact rule.', 0xfee75c, i.guild)], components: rows.slice(0, 5) }); + const escalation = (x.escalation || []).map(e => `Strike **${e.strike}** → **${e.action}**${e.durationMs ? ` (${Math.round(e.durationMs / 60000)}m)` : ''}`).join('\n') || 'No escalation rules.'; + return i.update({ embeds: [embed('⚖️ Punishments & Escalation', `Every security rule has an independent punishment.\n\n**Anti-Raid:** ${x.antiRaid.punishment}\n**Strike decay:** ${Math.round(x.strikeDecayMs / 3600000)}h\n\n**Escalation**\n${escalation}`, 0xfee75c, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`punishment_decay2:${i.user.id}`, '⏱️ Decay', ButtonStyle.Primary))] }); } - +async function punishmentRules(i, c) { return punishments(i, c); } async function whitelist(i, c) { const x = await getSecurityConfig(c, i.guildId); return i.update({ embeds: [embed('👤 Whitelist', `Trusted entries bypass applicable security enforcement.\n\n**Users:** ${x.whitelist.users.length}\n**Roles:** ${x.whitelist.roles.length}\n**Bots:** ${x.whitelist.bots.length}`, 0x57f287, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`wl_users2:${i.user.id}`, '👤 Users', ButtonStyle.Primary), B(`wl_roles2:${i.user.id}`, '🎭 Roles', ButtonStyle.Primary), B(`wl_bots2:${i.user.id}`, '🤖 Bots', ButtonStyle.Primary))] }); } - -async function logs(i, c) { - const x = await getSecurityConfig(c, i.guildId); - return i.update({ embeds: [embed('📋 Security Logs', `**Log channel:** ${x.logChannelId ? `<#${x.logChannelId}>` : 'Not configured'}\n**Ignored channels:** ${x.ignoredChannels.length}\n\nAll protection events can be sent to the configured security log channel.`, 0x5865f2, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`logs_channel2:${i.user.id}`, '📋 Set Log Channel', ButtonStyle.Primary), B(`logs_ignored2:${i.user.id}`, '🚫 Ignored Channels', ButtonStyle.Secondary))] }); -} - -async function settings(i, c) { - const x = await getSecurityConfig(c, i.guildId); - return i.update({ embeds: [embed('⚙️ Security Settings', `**Global protection:** ${x.enabled ? '🟢 ON' : '🔴 OFF'}\n**Strike decay:** ${Math.round(x.strikeDecayMs / 3600000)}h\n**Ignored channels:** ${x.ignoredChannels.length}\n\nThese settings affect the whole security system.`, 0x57f287, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`settings_toggle2:${i.user.id}`, x.enabled ? '🔴 Disable Protection' : '🟢 Enable Protection'), B(`settings_decay2:${i.user.id}`, '⏱️ Strike Decay', ButtonStyle.Primary), B(`settings_ignored2:${i.user.id}`, '🚫 Ignored Channels', ButtonStyle.Primary))] }); -} - +async function logs(i, c) { const x = await getSecurityConfig(c, i.guildId); return i.update({ embeds: [embed('📋 Security Logs', `**Log channel:** ${x.logChannelId ? `<#${x.logChannelId}>` : 'Not configured'}\n**Ignored channels:** ${x.ignoredChannels.length}`, 0x5865f2, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`logs_channel2:${i.user.id}`, '📋 Set Log Channel', ButtonStyle.Primary), B(`logs_ignored2:${i.user.id}`, '🚫 Ignored Channels'))] }); } +async function settings(i, c) { const x = await getSecurityConfig(c, i.guildId); return i.update({ embeds: [embed('⚙️ Security Settings', `**Global protection:** ${x.enabled ? '🟢 ON' : '🔴 OFF'}\n**Strike decay:** ${Math.round(x.strikeDecayMs / 3600000)}h\n**Ignored channels:** ${x.ignoredChannels.length}`, 0x57f287, i.guild)], components: [row(B(`security_back2:${i.user.id}`, '← Back'), B(`settings_toggle2:${i.user.id}`, x.enabled ? '🔴 Disable Protection' : '🟢 Enable Protection'), B(`settings_decay2:${i.user.id}`, '⏱️ Strike Decay', ButtonStyle.Primary), B(`settings_ignored2:${i.user.id}`, '🚫 Ignored Channels', ButtonStyle.Primary))] }); } async function strikes(i, c) { - const members = await i.guild.members.fetch().catch(() => i.guild.members.cache); - const entries = []; - for (const m of members.values()) { if (m.user.bot) continue; const s = await getStrikes(c, i.guildId, m.id).catch(() => ({ count: 0 })); if (s.count) entries.push({ id: m.id, count: s.count }); } + const members = await i.guild.members.fetch().catch(() => i.guild.members.cache), entries = []; + for (const m of members.values()) { if (m.user.bot) continue; const s = await getStrikes(c, i.guildId, m.id).catch(() => ({ count: 0 })); if (Number(s.count) > 0) entries.push({ id: m.id, count: Number(s.count) }); } entries.sort((a, b) => b.count - a.count); const text = entries.slice(0, 10).map((e, n) => `${n + 1}. <@${e.id}> — **${e.count}** strikes`).join('\n') || 'No active strikes.'; const rows = [row(B(`security_back2:${i.user.id}`, '← Back'), B(`strikes_refresh2:${i.user.id}`, '🔄 Refresh', ButtonStyle.Success))]; if (entries.length) rows.push(row(...entries.slice(0, 4).map(e => B(`strike_manage2:${e.id}:${i.user.id}`, `Manage ${e.count}`, ButtonStyle.Primary)))); - return i.update({ embeds: [embed('🏆 Strikes & Warnings', `Top members by active security strikes.\n\n${text}\n\nYou can reset strikes or clear warnings from the member page.`, 0xfee75c, i.guild)], components: rows }); + return i.update({ embeds: [embed('🏆 Strikes', `Members with active security strikes.\n\n${text}`, 0xfee75c, i.guild)], components: rows }); } - async function member(i, c, userId) { const s = await getStrikes(c, i.guildId, userId).catch(() => ({ count: 0, lastReason: '' })); - const w = await WarningService.getWarnings(i.guildId, userId).catch(() => []); - return i.update({ embeds: [embed(`👤 Security History`, `<@${userId}>\n\n**Strikes:** ${s.count || 0}\n**Warnings:** ${w.length}\n**Last strike:** ${s.lastReason || '—'}`, 0xfee75c, i.guild)], components: [row(B(`strike_reset2:${userId}:${i.user.id}`, '🧹 Reset Strikes', ButtonStyle.Danger), B(`warning_clear2:${userId}:${i.user.id}`, '🗑️ Clear Warnings', ButtonStyle.Danger), B(`strikes_back2:${i.user.id}`, '← Back'))] }); + return i.update({ embeds: [embed('👤 Security Strikes', `<@${userId}>\n\n**Strikes:** ${Number(s.count) || 0}\n**Last strike:** ${s.lastReason || '—'}`, 0xfee75c, i.guild)], components: [row(B(`strike_reset2:${userId}:${i.user.id}`, '🧹 Reset Strikes', ButtonStyle.Danger), B(`strikes_back2:${i.user.id}`, '← Back'))] }); } -const panelMap = { security_panel_nuke2: nuke, security_panel_raid2: raid, security_panel_punishments2: punishments, security_panel_whitelist2: whitelist, security_panel_logs2: logs, security_panel_settings2: settings, security_panel_strikes2: strikes }; +const panelMap = { security_panel_nuke2: nuke, security_panel_raid2: raid, security_panel_automod2: automod, security_panel_punishments2: punishments, security_panel_whitelist2: whitelist, security_panel_logs2: logs, security_panel_settings2: settings, security_panel_strikes2: strikes }; export const securityDashboardButtonHandlers = [ ...Object.entries(panelMap).map(([name, fn]) => ({ name, execute: async (i, c) => ok(i) ? fn(i, c) : deny(i) })), @@ -128,7 +107,6 @@ export const securityDashboardButtonHandlers = [ { name: 'nuke_rules2', execute: async (i,c) => ok(i) ? nukeRules(i,c) : deny(i) }, { name: 'nuke_rules_back2', execute: async (i,c) => ok(i) ? nuke(i,c) : deny(i) }, ...Object.keys(NUKE_RULES).map(k => ({ name: 'nuke_rule', match: k, execute: async (i,c) => { if (!ok(i)) return deny(i); const x=await getSecurityConfig(c,i.guildId); await updateSecurityConfig(c,i.guildId,{antiNuke:{punishments:{[k]:cycle(x.antiNuke.punishments[k]||'strip',NUKE_ACTIONS)}}}); return nukeRules(i,c); } })), - { name: 'security_panel_raid2', execute: async (i,c)=>ok(i)?raid(i,c):deny(i) }, { name: 'raid_toggle2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{enabled:!x.antiRaid.enabled}});return raid(i,c);} }, { name: 'raid_punishment2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{punishment:cycle(x.antiRaid.punishment,RAID_ACTIONS)}});return raid(i,c);} }, { name: 'raid_joins2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{joins:x.antiRaid.joins>=50?2:x.antiRaid.joins+2}});return raid(i,c);} }, @@ -138,16 +116,13 @@ export const securityDashboardButtonHandlers = [ { name: 'punishment_rules2', execute: async(i,c)=>ok(i)?punishmentRules(i,c):deny(i) }, { name: 'punishments_back2', execute: async(i,c)=>ok(i)?punishments(i,c):deny(i) }, { name: 'pun_raid2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{antiRaid:{punishment:cycle(x.antiRaid.punishment,RAID_ACTIONS)}});return punishmentRules(i,c);} }, - { name: 'pun_auto2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);const text=Object.entries(AUTO_RULES).map(([k,n])=>`${n}: **${x.autoMod[k].punishment}**`).join('\n');return i.update({embeds:[embed('🤖 AutoMod Punishments',text,0x5865f2,i.guild)],components:[row(B(`punishments_back2:${i.user.id}`),...Object.keys(AUTO_RULES).slice(0,4).map(k=>B(`auto_pun2:${k}:${i.user.id}`,`${AUTO_RULES[k]}: ${x.autoMod[k].punishment}`,ButtonStyle.Primary)))]});}}, + { name: 'pun_auto2', execute: async(i,c)=>ok(i)?automod(i,c):deny(i) }, + { name: 'automod_toggle2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{enabled:!x.autoMod.enabled}});return automod(i,c);} }, + ...Object.keys(AUTO_RULES).map(k => ({ name: 'auto_pun2', match: k, execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{autoMod:{[k]:{punishment:cycle(x.autoMod[k]?.punishment||'delete',AUTO_ACTIONS)}}});return automod(i,c);} })), { name: 'punishment_decay2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{strikeDecayMs:x.strikeDecayMs>=30*86400000?3600000:x.strikeDecayMs+3600000});return punishments(i,c);} }, - { name: 'security_panel_whitelist2', execute: async(i,c)=>ok(i)?whitelist(i,c):deny(i) }, - { name: 'security_panel_logs2', execute: async(i,c)=>ok(i)?logs(i,c):deny(i) }, - { name: 'security_panel_settings2', execute: async(i,c)=>ok(i)?settings(i,c):deny(i) }, - { name: 'security_panel_strikes2', execute: async(i,c)=>ok(i)?strikes(i,c):deny(i) }, { name: 'strikes_refresh2', execute: async(i,c)=>ok(i)?strikes(i,c):deny(i) }, { name: 'strike_manage2', execute: async(i,c)=>ok(i)?member(i,c,i.customId.split(':').at(-2)):deny(i) }, - { name: 'strike_reset2', execute: async(i,c)=>{if(!ok(i))return deny(i);const u=i.customId.split(':').at(-2);await clearStrikes(c,i.guildId,u);await sendSecurityLog(c,i.guild,{title:'Strikes Reset',description:`<@${u}> strikes reset by <@${i.user.id}>`,color:0x57f287});return member(i,c,u);} }, - { name: 'warning_clear2', execute: async(i,c)=>{if(!ok(i))return deny(i);const u=i.customId.split(':').at(-2);await WarningService.clearWarnings(i.guildId,u);return member(i,c,u);} }, + { name: 'strike_reset2', execute: async(i,c)=>{if(!ok(i))return deny(i);const u=i.customId.split(':').at(-2);await clearStrikes(c,i.guildId,u);await sendSecurityLog(c,i.guild,{title:'Strikes Reset',description:`<@${u}> strikes reset by <@${i.user.id}>`,color:0x57f287});return strikes(i,c);} }, { name: 'strikes_back2', execute: async(i,c)=>ok(i)?strikes(i,c):deny(i) }, { name: 'wl_users2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`wl_users_modal2:${i.user.id}`).setTitle('Whitelist Users').addComponents(modalField('value','User IDs, one per line','',TextInputStyle.Paragraph))):deny(i) }, { name: 'wl_roles2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`wl_roles_modal2:${i.user.id}`).setTitle('Whitelist Roles').addComponents(modalField('value','Role IDs, one per line','',TextInputStyle.Paragraph))):deny(i) }, @@ -157,6 +132,8 @@ export const securityDashboardButtonHandlers = [ { name: 'settings_toggle2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{enabled:!x.enabled});return settings(i,c);} }, { name: 'settings_decay2', execute: async(i,c)=>{if(!ok(i))return deny(i);const x=await getSecurityConfig(c,i.guildId);await updateSecurityConfig(c,i.guildId,{strikeDecayMs:x.strikeDecayMs>=30*86400000?3600000:x.strikeDecayMs+3600000});return settings(i,c);} }, { name: 'settings_ignored2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`settings_ignored_modal2:${i.user.id}`).setTitle('Ignored Channels').addComponents(modalField('value','Channel IDs, one per line','',TextInputStyle.Paragraph))):deny(i) }, + { name: 'logs_ignored2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`logs_ignored_modal2:${i.user.id}`).setTitle('Ignored Channels').addComponents(modalField('value','Channel IDs, one per line','',TextInputStyle.Paragraph))):deny(i) }, + { name: 'logs_channel2', execute: async i=>ok(i)?i.showModal(new ModalBuilder().setCustomId(`logs_channel_modal2:${i.user.id}`).setTitle('Security Log Channel').addComponents(modalField('value','Channel ID'))):deny(i) }, ]; export const securityDashboardModalHandlers = [ @@ -167,4 +144,4 @@ export const securityDashboardModalHandlers = [ { name:'logs_channel_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const value=String(i.fields.getTextInputValue('value')||'').trim()||null;await updateSecurityConfig(c,i.guildId,{logChannelId:value});return logs(i,c);} }, { name:'logs_ignored_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const value=String(i.fields.getTextInputValue('value')||'').split(/\s+/).filter(Boolean).slice(0,100);await updateSecurityConfig(c,i.guildId,{ignoredChannels:value});return logs(i,c);} }, { name:'settings_ignored_modal2', execute:async(i,c)=>{if(!ok(i))return deny(i);const value=String(i.fields.getTextInputValue('value')||'').split(/\s+/).filter(Boolean).slice(0,100);await updateSecurityConfig(c,i.guildId,{ignoredChannels:value});return settings(i,c);} }, -]; +]; \ No newline at end of file From 04027dd183569cdcae4bbc475ce3bc40507cec9c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 00:59:39 +0300 Subject: [PATCH 164/219] fix: enforce security whitelist before strikes and actions --- src/services/security/securityService.js | 95 +++++++----------------- 1 file changed, 28 insertions(+), 67 deletions(-) diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js index 0f07b5b28b..9a7648fc48 100644 --- a/src/services/security/securityService.js +++ b/src/services/security/securityService.js @@ -6,48 +6,16 @@ const AutoModTypes = ['spam','duplicate','mentions','invites','links','caps','ba export const SECURITY_DEFAULTS = { enabled: true, - antiNuke: { - enabled: true, - windowMs: 10000, - thresholds: { channelDelete: 3, channelCreate: 5, roleDelete: 3, roleCreate: 5, roleUpdate: 1, webhookUpdate: 3, webhookDelete: 2, ban: 5, kick: 5, botAdd: 1 }, - action: 'strip', - punishments: Object.fromEntries(NukeTypes.map(type => [type, ['ban','kick','botAdd'].includes(type) ? 'ban' : 'strip'])), - lockdown: true, - }, - antiRaid: { - enabled: true, - joins: 8, - windowMs: 10000, - minAccountAgeMs: 24 * 60 * 60 * 1000, - action: 'timeout', - punishment: 'timeout', - timeoutMs: 10 * 60 * 1000, - lockdown: true, - lockdownMs: 10 * 60 * 1000, - }, - autoMod: { - enabled: true, - spam: { enabled: true, maxMessages: 6, windowMs: 5000, punishment: 'delete' }, - duplicate: { enabled: true, maxRepeats: 3, windowMs: 10000, punishment: 'delete' }, - mentions: { enabled: true, max: 6, punishment: 'delete' }, - caps: { enabled: false, ratio: 0.8, minLength: 12, punishment: 'delete' }, - invites: { enabled: true, punishment: 'delete' }, - links: { enabled: false, punishment: 'delete' }, - badWords: { enabled: false, words: [], punishment: 'delete' }, - action: 'delete', - }, + antiNuke: { enabled: true, windowMs: 10000, thresholds: { channelDelete: 3, channelCreate: 5, roleDelete: 3, roleCreate: 5, roleUpdate: 1, webhookUpdate: 3, webhookDelete: 2, ban: 5, kick: 5, botAdd: 1 }, action: 'strip', punishments: Object.fromEntries(NukeTypes.map(type => [type, ['ban','kick','botAdd'].includes(type) ? 'ban' : 'strip'])), lockdown: true }, + antiRaid: { enabled: true, joins: 8, windowMs: 10000, minAccountAgeMs: 24 * 60 * 60 * 1000, action: 'timeout', punishment: 'timeout', timeoutMs: 10 * 60 * 1000, lockdown: true, lockdownMs: 10 * 60 * 1000 }, + autoMod: { enabled: true, spam: { enabled: true, maxMessages: 6, windowMs: 5000, punishment: 'delete' }, duplicate: { enabled: true, maxRepeats: 3, windowMs: 10000, punishment: 'delete' }, mentions: { enabled: true, max: 6, punishment: 'delete' }, caps: { enabled: false, ratio: 0.8, minLength: 12, punishment: 'delete' }, invites: { enabled: true, punishment: 'delete' }, links: { enabled: false, punishment: 'delete' }, badWords: { enabled: false, words: [], punishment: 'delete' }, action: 'delete' }, escalation: [ - { strike: 1, action: 'delete', durationMs: 0 }, - { strike: 2, action: 'timeout', durationMs: 60 * 1000 }, - { strike: 3, action: 'timeout', durationMs: 10 * 60 * 1000 }, - { strike: 4, action: 'timeout', durationMs: 60 * 60 * 1000 }, - { strike: 5, action: 'kick', durationMs: 0 }, - { strike: 6, action: 'ban', durationMs: 0 }, + { strike: 1, action: 'delete', durationMs: 0 }, { strike: 2, action: 'timeout', durationMs: 60 * 1000 }, { strike: 3, action: 'timeout', durationMs: 10 * 60 * 1000 }, + { strike: 4, action: 'timeout', durationMs: 60 * 60 * 1000 }, { strike: 5, action: 'kick', durationMs: 0 }, { strike: 6, action: 'ban', durationMs: 0 }, ], strikeDecayMs: 24 * 60 * 60 * 1000, whitelist: { users: [], roles: [], bots: [] }, - ignoredChannels: [], - logChannelId: null, + ignoredChannels: [], logChannelId: null, }; function clone(value) { return JSON.parse(JSON.stringify(value)); } @@ -61,7 +29,6 @@ function deepMerge(base, patch) { } function configKey(guildId) { return `security:config:${guildId}`; } function strikeKey(guildId, userId) { return `security:strikes:${guildId}:${userId}`; } - function normalizeIdList(value) { if (Array.isArray(value)) return value.map(String).filter(Boolean); if (value instanceof Set) return [...value].map(String).filter(Boolean); @@ -111,7 +78,9 @@ export async function addStrike(client, guildId, userId, reason = 'AutoMod viola await setInDb(strikeKey(guildId, userId), next); return next; } -export async function clearStrikes(client, guildId, userId) { await setInDb(strikeKey(guildId, userId), { count: 0, updatedAt: Date.now() }); } +export async function clearStrikes(client, guildId, userId) { + await setInDb(strikeKey(guildId, userId), { count: 0, updatedAt: Date.now(), lastReason: '' }); +} export function isWhitelisted(member, config) { if (!member) return false; @@ -119,9 +88,13 @@ export function isWhitelisted(member, config) { const users = normalizeIdList(whitelist.users); const roles = normalizeIdList(whitelist.roles); const bots = normalizeIdList(whitelist.bots); - if (users.includes(String(member.id))) return true; - if (member.roles?.cache?.some(role => roles.includes(String(role.id)))) return true; - if (member.user?.bot && bots.includes(String(member.id))) return true; + const memberId = String(member.id); + if (users.includes(memberId)) return true; + const roleIds = new Set(); + if (member.roles?.cache) for (const role of member.roles.cache.values()) roleIds.add(String(role.id)); + if (Array.isArray(member._roles)) for (const roleId of member._roles) roleIds.add(String(roleId)); + if ([...roleIds].some(roleId => roles.includes(roleId))) return true; + if (member.user?.bot && bots.includes(memberId)) return true; return false; } @@ -134,35 +107,21 @@ export async function sendSecurityLog(client, guild, payload) { await channel.send({ embeds: [{ title: payload.title || 'Security Event', description: payload.description || 'Security event detected.', color: payload.color || 0xED4245, fields: payload.fields || [], timestamp: new Date().toISOString() }] }); } catch (error) { logger.warn('Failed to send security log', { guildId: guild.id, error: error.message }); } } -export function getRecentCount(store, key, now, windowMs) { - const list = store.get(key) || []; - const recent = list.filter(timestamp => now - timestamp <= windowMs); - store.set(key, recent); - return recent; -} const autoModState = new Map(); function autoModKey(guildId, userId) { return `${guildId}:${userId}`; } -function autoModData(guildId, userId) { - const key = autoModKey(guildId, userId); - let data = autoModState.get(key); - if (!data) { data = { messages: [], repeats: [] }; autoModState.set(key, data); } - return data; -} +function autoModData(guildId, userId) { const key = autoModKey(guildId, userId); let data = autoModState.get(key); if (!data) { data = { messages: [], repeats: [] }; autoModState.set(key, data); } return data; } function normalizeMessage(content) { return String(content || '').trim().toLowerCase().replace(/\s+/g, ' '); } -function capRatio(content) { - const letters = String(content || '').match(/[A-Za-z]/g) || []; - const upper = String(content || '').match(/[A-Z]/g) || []; - return letters.length ? upper.length / letters.length : 0; -} +function capRatio(content) { const letters = String(content || '').match(/[A-Za-z]/g) || []; const upper = String(content || '').match(/[A-Z]/g) || []; return letters.length ? upper.length / letters.length : 0; } async function executeAutoModAction(message, action, duration, reason, config) { const member = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); - if (!member || isWhitelisted(member, config)) return; - if (action === 'delete') return; + if (!member || isWhitelisted(member, config)) return false; + if (action === 'delete') return true; if (action === 'timeout' && member.moderatable) await member.timeout(Math.min(Math.max(duration || 60000, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); else if (action === 'kick' && member.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); else if (action === 'ban' && member.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); + return true; } export async function processAutoMod(message, client) { @@ -200,13 +159,15 @@ export async function processAutoMod(message, client) { const action = (escalation?.action || a[primary.type]?.punishment || a.action || 'delete') === 'warn' ? 'delete' : (escalation?.action || a[primary.type]?.punishment || a.action || 'delete'); const duration = escalation?.durationMs || 60000; + // Re-check immediately before any destructive action so a whitelist change cannot race with enforcement. + const latestConfig = await getSecurityConfig(client, message.guild.id); + const latestMember = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); + if (!latestMember || isWhitelisted(latestMember, latestConfig)) return false; + await message.delete().catch(() => {}); - await executeAutoModAction(message, action, duration, reason, config); + await executeAutoModAction(message, action, duration, reason, latestConfig); await sendSecurityLog(client, message.guild, { title: 'AutoMod Triggered', description: `AutoMod acted on **${message.author.tag}**.`, fields: [ - { name: 'Rule', value: primary.type, inline: true }, - { name: 'Reason', value: reason.slice(0, 1024), inline: true }, - { name: 'Strike', value: String(strike.count), inline: true }, - { name: 'Action', value: action, inline: true }, + { name: 'Rule', value: primary.type, inline: true }, { name: 'Reason', value: reason.slice(0, 1024), inline: true }, { name: 'Strike', value: String(strike.count), inline: true }, { name: 'Action', value: action, inline: true }, ] }); return true; } From baa5fa5d6ca634f50e0664bb5499701cc813bc09 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:01:07 +0300 Subject: [PATCH 165/219] fix: remove warning punishment from all security configurations --- src/services/security/securityService.js | 47 ++++++++++-------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js index 9a7648fc48..361bc97356 100644 --- a/src/services/security/securityService.js +++ b/src/services/security/securityService.js @@ -3,6 +3,7 @@ import { logger } from '../../utils/logger.js'; const NukeTypes = ['channelDelete','channelCreate','roleDelete','roleCreate','roleUpdate','webhookUpdate','webhookDelete','ban','kick','botAdd']; const AutoModTypes = ['spam','duplicate','mentions','invites','links','caps','badWords']; +const AUTO_ACTIONS = new Set(['delete', 'timeout', 'kick', 'ban']); export const SECURITY_DEFAULTS = { enabled: true, @@ -35,32 +36,26 @@ function normalizeIdList(value) { if (value && typeof value === 'object') return Object.keys(value).map(String).filter(Boolean); return []; } +function sanitizeConfig(config) { + config.whitelist = config.whitelist || {}; + config.whitelist.users = normalizeIdList(config.whitelist.users); + config.whitelist.roles = normalizeIdList(config.whitelist.roles); + config.whitelist.bots = normalizeIdList(config.whitelist.bots); + config.antiNuke.punishments = { ...SECURITY_DEFAULTS.antiNuke.punishments, ...(config.antiNuke.punishments || {}) }; + for (const type of AutoModTypes) { + config.autoMod[type] ||= clone(SECURITY_DEFAULTS.autoMod[type]); + if (!AUTO_ACTIONS.has(config.autoMod[type].punishment)) config.autoMod[type].punishment = 'delete'; + } + config.escalation = (config.escalation || []).map(level => ({ ...level, action: AUTO_ACTIONS.has(level.action) ? level.action : 'delete' })); + return config; +} export async function getSecurityConfig(client, guildId) { - try { - const stored = await getFromDb(configKey(guildId), null); - const config = deepMerge(clone(SECURITY_DEFAULTS), stored || {}); - config.whitelist = config.whitelist || {}; - config.whitelist.users = normalizeIdList(config.whitelist.users); - config.whitelist.roles = normalizeIdList(config.whitelist.roles); - config.whitelist.bots = normalizeIdList(config.whitelist.bots); - config.antiNuke.punishments = { ...SECURITY_DEFAULTS.antiNuke.punishments, ...(config.antiNuke.punishments || {}) }; - for (const type of AutoModTypes) config.autoMod[type].punishment ||= SECURITY_DEFAULTS.autoMod[type].punishment; - config.escalation = (config.escalation || []).map(level => ({ ...level, action: level.action === 'warn' ? 'delete' : level.action })); - return config; - } catch (error) { - logger.error('Failed to load security config', { guildId, error: error.message }); - return clone(SECURITY_DEFAULTS); - } + try { return sanitizeConfig(deepMerge(clone(SECURITY_DEFAULTS), await getFromDb(configKey(guildId), null) || {})); } + catch (error) { logger.error('Failed to load security config', { guildId, error: error.message }); return clone(SECURITY_DEFAULTS); } } export async function updateSecurityConfig(client, guildId, patch) { - const current = await getSecurityConfig(client, guildId); - const updated = deepMerge(current, patch); - updated.whitelist = updated.whitelist || {}; - updated.whitelist.users = normalizeIdList(updated.whitelist.users); - updated.whitelist.roles = normalizeIdList(updated.whitelist.roles); - updated.whitelist.bots = normalizeIdList(updated.whitelist.bots); - updated.escalation = (updated.escalation || []).map(level => ({ ...level, action: level.action === 'warn' ? 'delete' : level.action })); + const updated = sanitizeConfig(deepMerge(await getSecurityConfig(client, guildId), patch)); await setInDb(configKey(guildId), updated); return updated; } @@ -78,9 +73,7 @@ export async function addStrike(client, guildId, userId, reason = 'AutoMod viola await setInDb(strikeKey(guildId, userId), next); return next; } -export async function clearStrikes(client, guildId, userId) { - await setInDb(strikeKey(guildId, userId), { count: 0, updatedAt: Date.now(), lastReason: '' }); -} +export async function clearStrikes(client, guildId, userId) { await setInDb(strikeKey(guildId, userId), { count: 0, updatedAt: Date.now(), lastReason: '' }); } export function isWhitelisted(member, config) { if (!member) return false; @@ -129,7 +122,6 @@ export async function processAutoMod(message, client) { const config = await getSecurityConfig(client, message.guild.id); const a = config.autoMod; if (!config.enabled || !a?.enabled || config.ignoredChannels?.includes(message.channel.id)) return false; - const member = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); if (!member || isWhitelisted(member, config)) return false; @@ -156,10 +148,9 @@ export async function processAutoMod(message, client) { const strike = await addStrike(client, message.guild.id, message.author.id, reason); const escalation = config.escalation?.find(item => item.strike === strike.count); const primary = violations[0]; - const action = (escalation?.action || a[primary.type]?.punishment || a.action || 'delete') === 'warn' ? 'delete' : (escalation?.action || a[primary.type]?.punishment || a.action || 'delete'); + const action = escalation?.action || a[primary.type]?.punishment || 'delete'; const duration = escalation?.durationMs || 60000; - // Re-check immediately before any destructive action so a whitelist change cannot race with enforcement. const latestConfig = await getSecurityConfig(client, message.guild.id); const latestMember = message.member || await message.guild.members.fetch(message.author.id).catch(() => null); if (!latestMember || isWhitelisted(latestMember, latestConfig)) return false; From 4ed28e3ac46f9f88a6ae14731dabffc1007c83cb Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:08:28 +0300 Subject: [PATCH 166/219] fix: finalize security strikes and whitelist controls --- .../security/securityFinalOverrides.js | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/interactions/buttons/security/securityFinalOverrides.js diff --git a/src/interactions/buttons/security/securityFinalOverrides.js b/src/interactions/buttons/security/securityFinalOverrides.js new file mode 100644 index 0000000000..89cf01e4e8 --- /dev/null +++ b/src/interactions/buttons/security/securityFinalOverrides.js @@ -0,0 +1,127 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { getSecurityConfig, updateSecurityConfig, getStrikes, clearStrikes } from '../../../services/security/securityService.js'; + +const ok = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; +const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); +const button = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); +const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); + +function embed(title, description, guild, color = 0xfee75c) { + return new EmbedBuilder() + .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) + .setTitle(title) + .setDescription(description) + .setColor(color) + .setFooter({ text: 'Infinity System • Changes save automatically' }) + .setTimestamp(); +} + +async function renderStrikes(interaction, client) { + const members = await interaction.guild.members.fetch().catch(() => interaction.guild.members.cache); + const entries = []; + for (const member of members.values()) { + if (member.user.bot) continue; + const strike = await getStrikes(client, interaction.guildId, member.id).catch(() => ({ count: 0 })); + const count = Number(strike?.count || 0); + if (count > 0) entries.push({ id: member.id, count }); + } + entries.sort((a, b) => b.count - a.count); + + const text = entries.slice(0, 10) + .map((entry, index) => `${index + 1}. <@${entry.id}> — **${entry.count}** strikes`) + .join('\n') || 'لا توجد Strikes فعالة'; + + const components = [row( + button(`security_back2:${interaction.user.id}`, '← Back'), + button(`strikes_refresh2:${interaction.user.id}`, '🔄 Refresh', ButtonStyle.Success), + )]; + + for (let i = 0; i < Math.min(entries.length, 8); i += 4) { + components.push(row(...entries.slice(i, i + 4).map(entry => + button(`strike_manage2:${entry.id}:${interaction.user.id}`, `إدارة ${entry.count}`, ButtonStyle.Primary) + ))); + } + + return interaction.update({ + embeds: [embed('🏆 Strikes', `**${interaction.guild.name}**\n\n${text}\n\nاختر عضوًا لإلغاء الـStrike أو إدارة Strikes الخاصة به.`, interaction.guild)], + components, + }); +} + +async function manageStrike(interaction, client, userId) { + const strike = await getStrikes(client, interaction.guildId, userId).catch(() => ({ count: 0, lastReason: '' })); + return interaction.update({ + embeds: [embed('🏆 إدارة Strikes', `<@${userId}>\n\n**Strikes:** ${Number(strike?.count || 0)}\n**آخر سبب:** ${strike?.lastReason || '—'}`, interaction.guild)], + components: [row( + button(`strike_reset2:${userId}:${interaction.user.id}`, '🧹 إلغاء الـStrikes', ButtonStyle.Danger), + button(`strikes_back2:${interaction.user.id}`, '← رجوع'), + )], + }); +} + +async function resetStrike(interaction, client, userId) { + await clearStrikes(client, interaction.guildId, userId); + return renderStrikes(interaction, client); +} + +function parseIds(value) { + return [...new Set( + String(value || '') + .split(/[\s,\n]+/) + .map(value => value.match(/\d{15,25}/)?.[0]) + .filter(Boolean) + )].slice(0, 100); +} + +async function whitelistPage(interaction, client) { + const config = await getSecurityConfig(client, interaction.guildId); + return interaction.update({ + embeds: [embed('👤 Whitelist', `الحسابات الموجودة هنا تتجاوز إجراءات الحماية.\n\n**الأشخاص:** ${config.whitelist.users.length}\n**الرتب:** ${config.whitelist.roles.length}\n**البوتات:** ${config.whitelist.bots.length}`, interaction.guild, 0x57f287)], + components: [row( + button(`security_back2:${interaction.user.id}`, '← Back'), + button(`wl_users2:${interaction.user.id}`, '👤 الأشخاص', ButtonStyle.Primary), + button(`wl_roles2:${interaction.user.id}`, '🎭 الرتب', ButtonStyle.Primary), + button(`wl_bots2:${interaction.user.id}`, '🤖 البوتات', ButtonStyle.Primary), + )], + }); +} + +function whitelistModal(interaction, type, title, label, current) { + return interaction.showModal(new ModalBuilder() + .setCustomId(`security_final_wl_${type}:${interaction.user.id}`) + .setTitle(title) + .addComponents(new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('value') + .setLabel(label) + .setPlaceholder('ID أو منشن، ويمكن إضافة أكثر من واحد') + .setStyle(TextInputStyle.Paragraph) + .setRequired(false) + .setValue(current.join('\n').slice(0, 4000)) + ))); +} + +async function saveWhitelist(interaction, client, type) { + const ids = parseIds(interaction.fields.getTextInputValue('value')); + await updateSecurityConfig(client, interaction.guildId, { whitelist: { [type]: ids } }); + return whitelistPage(interaction, client); +} + +export default [ + { name: 'security_panel_strikes2', execute: async (i, c) => ok(i) ? renderStrikes(i, c) : deny(i) }, + { name: 'security_panel_strikes', execute: async (i, c) => ok(i) ? renderStrikes(i, c) : deny(i) }, + { name: 'strikes_refresh2', execute: async (i, c) => ok(i) ? renderStrikes(i, c) : deny(i) }, + { name: 'strike_manage2', execute: async (i, c, args) => ok(i) ? manageStrike(i, c, args[0]) : deny(i) }, + { name: 'strike_reset2', execute: async (i, c, args) => ok(i) ? resetStrike(i, c, args[0]) : deny(i) }, + { name: 'strikes_back2', execute: async (i, c) => ok(i) ? renderStrikes(i, c) : deny(i) }, + + { name: 'security_panel_whitelist2', execute: async (i, c) => ok(i) ? whitelistPage(i, c) : deny(i) }, + { name: 'security_panel_whitelist', execute: async (i, c) => ok(i) ? whitelistPage(i, c) : deny(i) }, + { name: 'wl_users2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return whitelistModal(i, 'users', 'Whitelist الأشخاص', 'User IDs', x.whitelist.users); } }, + { name: 'wl_roles2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return whitelistModal(i, 'roles', 'Whitelist الرتب', 'Role IDs', x.whitelist.roles); } }, + { name: 'wl_bots2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return whitelistModal(i, 'bots', 'Whitelist البوتات', 'Bot IDs', x.whitelist.bots); } }, + + { name: 'security_final_wl_users', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'users') : deny(i) }, + { name: 'security_final_wl_roles', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'roles') : deny(i) }, + { name: 'security_final_wl_bots', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'bots') : deny(i) }, +]; From df1e305cc809524e0015f6b7dcb3791db47605da Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:08:34 +0300 Subject: [PATCH 167/219] fix: load final security dashboard overrides last --- src/interactions/buttons/security/security.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index 122a5105e5..f12f0eebc9 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -3,13 +3,13 @@ import { securityDashboardButtonHandlers } from '../../../handlers/securityDashb import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRuleHandlers.js'; import securityAutoModDashboard from '../../../handlers/securityAutoModDashboard.js'; import securityDashboardFixes from '../../../handlers/securityDashboardFixes.js'; +import securityFinalOverrides from './securityFinalOverrides.js'; -// Compatibility handlers first, then the core dashboard, then targeted fixes. -// The final array order intentionally lets the fixes replace broken/legacy IDs. export default [ ...securityDashboardOverrides, ...securityDashboardButtonHandlers, ...securityDashboardRuleHandlers, ...securityAutoModDashboard, ...securityDashboardFixes, + ...securityFinalOverrides, ]; From 2bdb06b9f2bab5eb82786cf7ef9f51a6b5e92b3b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:08:49 +0300 Subject: [PATCH 168/219] fix: enforce whitelist before any automod action and remove warnings --- src/services/security/autoMod.js | 39 ++++++++++++++------------------ 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/services/security/autoMod.js b/src/services/security/autoMod.js index 8481124411..ab600e0f4a 100644 --- a/src/services/security/autoMod.js +++ b/src/services/security/autoMod.js @@ -54,25 +54,17 @@ function detect(message, config) { } async function executeAction(message, action, duration, reason, strike) { - const deleted = await message.delete().then(() => true).catch(() => false); const member = await message.guild.members.fetch(message.author.id).catch(() => null); + const config = await getSecurityConfig(message.client, message.guild.id); - if (member && !isWhitelisted(member, await getSecurityConfig(message.client, message.guild.id))) { - if (action === 'timeout' && member.moderatable) await member.timeout(Math.min(Math.max(duration || 60000, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); - else if (action === 'kick' && member.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); - else if (action === 'ban' && member.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); - } + // Whitelist is an absolute bypass: no deletion, timeout, kick, ban, or warning. + if (!member || isWhitelisted(member, config)) return false; - if (action === 'warn' && !message.channel.isDMBased?.()) { - const warning = await message.channel.send({ - content: `${message.author}, warning: **${reason}**. Strike **${strike.count}**.`, - allowedMentions: { users: [message.author.id] }, - }).catch(() => null); - if (warning) { - const timer = setTimeout(() => warning.delete().catch(() => {}), 5000); - timer.unref?.(); - } - } + const deleted = await message.delete().then(() => true).catch(() => false); + + if (action === 'timeout' && member.moderatable) await member.timeout(Math.min(Math.max(duration || 60000, 1000), 2419200000), `AutoMod: ${reason}`).catch(() => {}); + else if (action === 'kick' && member.kickable) await member.kick(`AutoMod: ${reason}`).catch(() => {}); + else if (action === 'ban' && member.bannable) await member.ban({ reason: `AutoMod: ${reason}` }).catch(() => {}); await sendSecurityLog(message.client, message.guild, { title: 'AutoMod Action', @@ -85,6 +77,7 @@ async function executeAction(message, action, duration, reason, strike) { { name: 'Message Deleted', value: deleted ? 'Yes' : 'No', inline: true }, ], }); + return true; } export async function handleAutoMod(message) { @@ -98,15 +91,17 @@ export async function handleAutoMod(message) { const violations = detect(message, config); if (!violations.length) return false; + // Re-check immediately before changing strike state in case the whitelist was changed during detection. + const latestConfig = await getSecurityConfig(message.client, message.guild.id); + const latestMember = await message.guild.members.fetch(message.author.id).catch(() => member); + if (!latestMember || latestMember.id === message.guild.ownerId || isWhitelisted(latestMember, latestConfig)) return false; + const primary = violations[0]; const reason = violations.map(v => `${v.type}: ${v.reason}`).join(', '); const strike = await addStrike(message.client, message.guild.id, message.author.id, reason); - const rule = config.autoMod[primary.type] || {}; - const baseAction = rule.punishment || config.autoMod.action || 'delete'; - - // The rule's own punishment is used first. Strike escalation takes over from - // the second strike onward, so every AutoMod rule remains independently configurable. - const escalation = strike.count > 1 ? config.escalation?.find(item => item.strike === strike.count) : null; + const rule = latestConfig.autoMod[primary.type] || {}; + const baseAction = rule.punishment || latestConfig.autoMod.action || 'delete'; + const escalation = strike.count > 1 ? latestConfig.escalation?.find(item => item.strike === strike.count) : null; const action = escalation?.action || baseAction; const duration = escalation?.durationMs || rule.timeoutMs || 60000; From 3508c08b62c3820522f660bd8213cb9c956d295f Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:09:14 +0300 Subject: [PATCH 169/219] fix: remove warning punishment from automod controls --- .../security/securityFinalOverrides.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/interactions/buttons/security/securityFinalOverrides.js b/src/interactions/buttons/security/securityFinalOverrides.js index 89cf01e4e8..87516244cd 100644 --- a/src/interactions/buttons/security/securityFinalOverrides.js +++ b/src/interactions/buttons/security/securityFinalOverrides.js @@ -5,6 +5,7 @@ const ok = interaction => interaction.customId.split(':').at(-1) === interaction const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); const button = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); +const AUTOMOD_ACTIONS = ['delete', 'timeout', 'kick', 'ban']; function embed(title, description, guild, color = 0xfee75c) { return new EmbedBuilder() @@ -107,6 +108,29 @@ async function saveWhitelist(interaction, client, type) { return whitelistPage(interaction, client); } +function parseAutoModKey(interaction) { + return interaction.customId.split(':').at(-2); +} + +async function autoModPunishment(interaction, client) { + if (!ok(interaction)) return deny(interaction); + const key = parseAutoModKey(interaction); + const config = await getSecurityConfig(client, interaction.guildId); + if (!config.autoMod[key]) return interaction.reply({ content: 'AutoMod rule unavailable.', ephemeral: true }); + const current = config.autoMod[key].punishment; + const index = AUTOMOD_ACTIONS.indexOf(current); + const next = AUTOMOD_ACTIONS[(index < 0 ? 0 : index + 1) % AUTOMOD_ACTIONS.length]; + await updateSecurityConfig(client, interaction.guildId, { autoMod: { [key]: { punishment: next } } }); + return interaction.update({ + embeds: [embed(`🤖 ${key} Settings`, `**Status:** ${config.autoMod[key].enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Punishment:** **${next}**`, interaction.guild)], + components: [row( + button(`automod_back:${interaction.user.id}`, '← AutoMod'), + button(`automod_toggle:${key}:${interaction.user.id}`, config.autoMod[key].enabled ? '🔴 Disable' : '🟢 Enable', config.autoMod[key].enabled ? ButtonStyle.Danger : ButtonStyle.Success), + button(`automod_punishment:${key}:${interaction.user.id}`, `⚖️ ${next}`, ButtonStyle.Primary), + )], + }); +} + export default [ { name: 'security_panel_strikes2', execute: async (i, c) => ok(i) ? renderStrikes(i, c) : deny(i) }, { name: 'security_panel_strikes', execute: async (i, c) => ok(i) ? renderStrikes(i, c) : deny(i) }, @@ -124,4 +148,5 @@ export default [ { name: 'security_final_wl_users', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'users') : deny(i) }, { name: 'security_final_wl_roles', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'roles') : deny(i) }, { name: 'security_final_wl_bots', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'bots') : deny(i) }, + { name: 'automod_punishment', execute: autoModPunishment }, ]; From fe270e3937ea2e6d78ecc84aa98eb2277687bc15 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:24:29 +0300 Subject: [PATCH 170/219] ui: simplify security strike member buttons --- .../security/securityFinalOverrides.js | 93 ++++--------------- 1 file changed, 16 insertions(+), 77 deletions(-) diff --git a/src/interactions/buttons/security/securityFinalOverrides.js b/src/interactions/buttons/security/securityFinalOverrides.js index 87516244cd..fc2f7b042d 100644 --- a/src/interactions/buttons/security/securityFinalOverrides.js +++ b/src/interactions/buttons/security/securityFinalOverrides.js @@ -8,13 +8,7 @@ const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); const AUTOMOD_ACTIONS = ['delete', 'timeout', 'kick', 'ban']; function embed(title, description, guild, color = 0xfee75c) { - return new EmbedBuilder() - .setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }) - .setTitle(title) - .setDescription(description) - .setColor(color) - .setFooter({ text: 'Infinity System • Changes save automatically' }) - .setTimestamp(); + return new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: guild.iconURL({ size: 128 }) || undefined }).setTitle(title).setDescription(description).setColor(color).setFooter({ text: 'Infinity System • Changes save automatically' }).setTimestamp(); } async function renderStrikes(interaction, client) { @@ -24,40 +18,20 @@ async function renderStrikes(interaction, client) { if (member.user.bot) continue; const strike = await getStrikes(client, interaction.guildId, member.id).catch(() => ({ count: 0 })); const count = Number(strike?.count || 0); - if (count > 0) entries.push({ id: member.id, count }); + if (count > 0) entries.push({ id: member.id, name: member.displayName || member.user.username, count }); } entries.sort((a, b) => b.count - a.count); - - const text = entries.slice(0, 10) - .map((entry, index) => `${index + 1}. <@${entry.id}> — **${entry.count}** strikes`) - .join('\n') || 'لا توجد Strikes فعالة'; - - const components = [row( - button(`security_back2:${interaction.user.id}`, '← Back'), - button(`strikes_refresh2:${interaction.user.id}`, '🔄 Refresh', ButtonStyle.Success), - )]; - + const text = entries.slice(0, 10).map((entry, index) => `${index + 1}. <@${entry.id}> — **${entry.count}** strikes`).join('\n') || 'No active strikes.'; + const components = [row(button(`security_back2:${interaction.user.id}`, '← Back'), button(`strikes_refresh2:${interaction.user.id}`, '🔄 Refresh', ButtonStyle.Success))]; for (let i = 0; i < Math.min(entries.length, 8); i += 4) { - components.push(row(...entries.slice(i, i + 4).map(entry => - button(`strike_manage2:${entry.id}:${interaction.user.id}`, `إدارة ${entry.count}`, ButtonStyle.Primary) - ))); + components.push(row(...entries.slice(i, i + 4).map(entry => button(`strike_manage2:${entry.id}:${interaction.user.id}`, entry.name.slice(0, 80), ButtonStyle.Primary)))); } - - return interaction.update({ - embeds: [embed('🏆 Strikes', `**${interaction.guild.name}**\n\n${text}\n\nاختر عضوًا لإلغاء الـStrike أو إدارة Strikes الخاصة به.`, interaction.guild)], - components, - }); + return interaction.update({ embeds: [embed('🏆 Strikes', `**${interaction.guild.name}**\n\n${text}\n\nSelect a member to manage their strikes.`, interaction.guild)], components }); } async function manageStrike(interaction, client, userId) { const strike = await getStrikes(client, interaction.guildId, userId).catch(() => ({ count: 0, lastReason: '' })); - return interaction.update({ - embeds: [embed('🏆 إدارة Strikes', `<@${userId}>\n\n**Strikes:** ${Number(strike?.count || 0)}\n**آخر سبب:** ${strike?.lastReason || '—'}`, interaction.guild)], - components: [row( - button(`strike_reset2:${userId}:${interaction.user.id}`, '🧹 إلغاء الـStrikes', ButtonStyle.Danger), - button(`strikes_back2:${interaction.user.id}`, '← رجوع'), - )], - }); + return interaction.update({ embeds: [embed('🏆 Strike Management', `<@${userId}>\n\n**Strikes:** ${Number(strike?.count || 0)}\n**Last Reason:** ${strike?.lastReason || '—'}`, interaction.guild)], components: [row(button(`strike_reset2:${userId}:${interaction.user.id}`, '🧹 Remove Strikes', ButtonStyle.Danger), button(`strikes_back2:${interaction.user.id}`, '← Back'))] }); } async function resetStrike(interaction, client, userId) { @@ -66,40 +40,16 @@ async function resetStrike(interaction, client, userId) { } function parseIds(value) { - return [...new Set( - String(value || '') - .split(/[\s,\n]+/) - .map(value => value.match(/\d{15,25}/)?.[0]) - .filter(Boolean) - )].slice(0, 100); + return [...new Set(String(value || '').split(/[\s,\n]+/).map(value => value.match(/\d{15,25}/)?.[0]).filter(Boolean))].slice(0, 100); } async function whitelistPage(interaction, client) { const config = await getSecurityConfig(client, interaction.guildId); - return interaction.update({ - embeds: [embed('👤 Whitelist', `الحسابات الموجودة هنا تتجاوز إجراءات الحماية.\n\n**الأشخاص:** ${config.whitelist.users.length}\n**الرتب:** ${config.whitelist.roles.length}\n**البوتات:** ${config.whitelist.bots.length}`, interaction.guild, 0x57f287)], - components: [row( - button(`security_back2:${interaction.user.id}`, '← Back'), - button(`wl_users2:${interaction.user.id}`, '👤 الأشخاص', ButtonStyle.Primary), - button(`wl_roles2:${interaction.user.id}`, '🎭 الرتب', ButtonStyle.Primary), - button(`wl_bots2:${interaction.user.id}`, '🤖 البوتات', ButtonStyle.Primary), - )], - }); + return interaction.update({ embeds: [embed('👤 Whitelist', `Accounts listed here bypass security actions.\n\n**Users:** ${config.whitelist.users.length}\n**Roles:** ${config.whitelist.roles.length}\n**Bots:** ${config.whitelist.bots.length}`, interaction.guild, 0x57f287)], components: [row(button(`security_back2:${interaction.user.id}`, '← Back'), button(`wl_users2:${interaction.user.id}`, '👤 Users', ButtonStyle.Primary), button(`wl_roles2:${interaction.user.id}`, '🎭 Roles', ButtonStyle.Primary), button(`wl_bots2:${interaction.user.id}`, '🤖 Bots', ButtonStyle.Primary))] }); } function whitelistModal(interaction, type, title, label, current) { - return interaction.showModal(new ModalBuilder() - .setCustomId(`security_final_wl_${type}:${interaction.user.id}`) - .setTitle(title) - .addComponents(new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId('value') - .setLabel(label) - .setPlaceholder('ID أو منشن، ويمكن إضافة أكثر من واحد') - .setStyle(TextInputStyle.Paragraph) - .setRequired(false) - .setValue(current.join('\n').slice(0, 4000)) - ))); + return interaction.showModal(new ModalBuilder().setCustomId(`security_final_wl_${type}:${interaction.user.id}`).setTitle(title).addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder('ID or mention, multiple values supported').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue(current.join('\n').slice(0, 4000)))); } async function saveWhitelist(interaction, client, type) { @@ -108,9 +58,7 @@ async function saveWhitelist(interaction, client, type) { return whitelistPage(interaction, client); } -function parseAutoModKey(interaction) { - return interaction.customId.split(':').at(-2); -} +function parseAutoModKey(interaction) { return interaction.customId.split(':').at(-2); } async function autoModPunishment(interaction, client) { if (!ok(interaction)) return deny(interaction); @@ -121,14 +69,7 @@ async function autoModPunishment(interaction, client) { const index = AUTOMOD_ACTIONS.indexOf(current); const next = AUTOMOD_ACTIONS[(index < 0 ? 0 : index + 1) % AUTOMOD_ACTIONS.length]; await updateSecurityConfig(client, interaction.guildId, { autoMod: { [key]: { punishment: next } } }); - return interaction.update({ - embeds: [embed(`🤖 ${key} Settings`, `**Status:** ${config.autoMod[key].enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Punishment:** **${next}**`, interaction.guild)], - components: [row( - button(`automod_back:${interaction.user.id}`, '← AutoMod'), - button(`automod_toggle:${key}:${interaction.user.id}`, config.autoMod[key].enabled ? '🔴 Disable' : '🟢 Enable', config.autoMod[key].enabled ? ButtonStyle.Danger : ButtonStyle.Success), - button(`automod_punishment:${key}:${interaction.user.id}`, `⚖️ ${next}`, ButtonStyle.Primary), - )], - }); + return interaction.update({ embeds: [embed(`🤖 ${key} Settings`, `**Status:** ${config.autoMod[key].enabled ? '🟢 ACTIVE' : '🔴 OFF'}\n**Punishment:** **${next}**`, interaction.guild)], components: [row(button(`automod_back:${interaction.user.id}`, '← AutoMod'), button(`automod_toggle:${key}:${interaction.user.id}`, config.autoMod[key].enabled ? '🔴 Disable' : '🟢 Enable', config.autoMod[key].enabled ? ButtonStyle.Danger : ButtonStyle.Success), button(`automod_punishment:${key}:${interaction.user.id}`, `⚖️ ${next}`, ButtonStyle.Primary))] }); } export default [ @@ -138,15 +79,13 @@ export default [ { name: 'strike_manage2', execute: async (i, c, args) => ok(i) ? manageStrike(i, c, args[0]) : deny(i) }, { name: 'strike_reset2', execute: async (i, c, args) => ok(i) ? resetStrike(i, c, args[0]) : deny(i) }, { name: 'strikes_back2', execute: async (i, c) => ok(i) ? renderStrikes(i, c) : deny(i) }, - { name: 'security_panel_whitelist2', execute: async (i, c) => ok(i) ? whitelistPage(i, c) : deny(i) }, { name: 'security_panel_whitelist', execute: async (i, c) => ok(i) ? whitelistPage(i, c) : deny(i) }, - { name: 'wl_users2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return whitelistModal(i, 'users', 'Whitelist الأشخاص', 'User IDs', x.whitelist.users); } }, - { name: 'wl_roles2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return whitelistModal(i, 'roles', 'Whitelist الرتب', 'Role IDs', x.whitelist.roles); } }, - { name: 'wl_bots2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return whitelistModal(i, 'bots', 'Whitelist البوتات', 'Bot IDs', x.whitelist.bots); } }, - + { name: 'wl_users2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return whitelistModal(i, 'users', 'Whitelist Users', 'User IDs', x.whitelist.users); } }, + { name: 'wl_roles2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return whitelistModal(i, 'roles', 'Whitelist Roles', 'Role IDs', x.whitelist.roles); } }, + { name: 'wl_bots2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return whitelistModal(i, 'bots', 'Whitelist Bots', 'Bot IDs', x.whitelist.bots); } }, { name: 'security_final_wl_users', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'users') : deny(i) }, { name: 'security_final_wl_roles', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'roles') : deny(i) }, { name: 'security_final_wl_bots', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'bots') : deny(i) }, { name: 'automod_punishment', execute: autoModPunishment }, -]; +]; \ No newline at end of file From 61b1e76890e994e962d8fe5927d3a311a98e85d0 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:26:19 +0300 Subject: [PATCH 171/219] fix: register security whitelist modal handlers --- src/interactions/modals/security/security.js | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/interactions/modals/security/security.js b/src/interactions/modals/security/security.js index 65fabbf9f3..754ee214ac 100644 --- a/src/interactions/modals/security/security.js +++ b/src/interactions/modals/security/security.js @@ -2,13 +2,44 @@ import { securityModalHandlers } from '../../../handlers/securityHandlers.js'; import { securityAdvancedModalHandlers } from '../../../handlers/securityAdvancedHandlers.js'; import { securityDashboardModalHandlers } from '../../../handlers/securityDashboardHandlers.js'; import securityDashboardFixes from '../../../handlers/securityDashboardFixes.js'; +import { getSecurityConfig, updateSecurityConfig } from '../../../services/security/securityService.js'; const ok = i => i.customId.split(':').at(-1) === i.user.id; const deny = i => i.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); +function parseIds(value) { + return [...new Set(String(value || '') + .split(/[\s,\n]+/) + .map(part => part.match(/\d{15,25}/)?.[0]) + .filter(Boolean))].slice(0, 100); +} + +async function saveWhitelist(interaction, client, type) { + if (!ok(interaction)) return deny(interaction); + + const ids = parseIds(interaction.fields.getTextInputValue('value')); + const config = await getSecurityConfig(client, interaction.guildId); + const whitelist = { + users: Array.isArray(config.whitelist?.users) ? config.whitelist.users.map(String) : [], + roles: Array.isArray(config.whitelist?.roles) ? config.whitelist.roles.map(String) : [], + bots: Array.isArray(config.whitelist?.bots) ? config.whitelist.bots.map(String) : [], + }; + + whitelist[type] = ids; + await updateSecurityConfig(client, interaction.guildId, { whitelist }); + + return interaction.reply({ + content: `Whitelist updated successfully. **${ids.length}** ${type} entr${ids.length === 1 ? 'y' : 'ies'} saved.`, + ephemeral: true, + }); +} + export default [ ...securityModalHandlers, ...securityAdvancedModalHandlers, ...securityDashboardModalHandlers, ...securityDashboardFixes.filter(h => h.name.endsWith('_modal')), + { name: 'security_final_wl_users', execute: (i, c) => saveWhitelist(i, c, 'users') }, + { name: 'security_final_wl_roles', execute: (i, c) => saveWhitelist(i, c, 'roles') }, + { name: 'security_final_wl_bots', execute: (i, c) => saveWhitelist(i, c, 'bots') }, ]; From 157de5a10ce6318332e70e88a1998557849ea0f9 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:28:47 +0300 Subject: [PATCH 172/219] fix: make security panel handlers load last --- src/interactions/buttons/security/security.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/interactions/buttons/security/security.js b/src/interactions/buttons/security/security.js index f12f0eebc9..50798cc5bd 100644 --- a/src/interactions/buttons/security/security.js +++ b/src/interactions/buttons/security/security.js @@ -4,7 +4,11 @@ import securityDashboardRuleHandlers from '../../../handlers/securityDashboardRu import securityAutoModDashboard from '../../../handlers/securityAutoModDashboard.js'; import securityDashboardFixes from '../../../handlers/securityDashboardFixes.js'; import securityFinalOverrides from './securityFinalOverrides.js'; +import securityStrikeCompatibility from './securityStrikeCompatibility.js'; +// Keep the Security handlers in one deterministic registration point. +// The final override/compatibility handlers intentionally come last so legacy +// dashboard buttons cannot replace the current handlers. export default [ ...securityDashboardOverrides, ...securityDashboardButtonHandlers, @@ -12,4 +16,5 @@ export default [ ...securityAutoModDashboard, ...securityDashboardFixes, ...securityFinalOverrides, + ...securityStrikeCompatibility, ]; From d7f5194dc2059575e4eac89c10c8396ac483ffe0 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:31:14 +0300 Subject: [PATCH 173/219] fix: add standalone security panel button aliases --- .../buttons/security/securityBaseAliases.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/interactions/buttons/security/securityBaseAliases.js diff --git a/src/interactions/buttons/security/securityBaseAliases.js b/src/interactions/buttons/security/securityBaseAliases.js new file mode 100644 index 0000000000..1be75b54cd --- /dev/null +++ b/src/interactions/buttons/security/securityBaseAliases.js @@ -0,0 +1,48 @@ +import { buildSecurityPanel, buildSecurityPanelControls, buildSecurityDashboard, buildSecurityControls } from '../../../commands/Security/security.js'; +import { getSecurityConfig, getStrikes, clearStrikes } from '../../../services/security/securityService.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; + +const owned = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; +const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); +const btn = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); +const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); + +async function dashboard(i, c) { + const config = await getSecurityConfig(c, i.guildId); + return i.update({ embeds: [buildSecurityDashboard(config, i.guild)], components: buildSecurityControls(i.user.id) }); +} + +async function panel(i, c, type) { + const config = await getSecurityConfig(c, i.guildId); + return i.update({ embeds: [buildSecurityPanel(config, i.guild, type)], components: buildSecurityPanelControls(i.user.id, type, config) }); +} + +async function strikes(i, c) { + const members = await i.guild.members.fetch().catch(() => i.guild.members.cache); + const entries = []; + for (const member of members.values()) { + if (member.user.bot) continue; + const strike = await getStrikes(c, i.guildId, member.id).catch(() => ({ count: 0 })); + if (Number(strike?.count || 0) > 0) entries.push({ id: member.id, count: Number(strike.count) }); + } + entries.sort((a, b) => b.count - a.count); + const description = entries.slice(0, 10).map((x, n) => `${n + 1}. <@${x.id}> — **${x.count}** strikes`).join('\n') || 'No active strikes.'; + const rows = [row(btn(`security_back:${i.user.id}`, '← Back'), btn(`security_strikes_refresh:${i.user.id}`, '🔄 Refresh', ButtonStyle.Success))]; + for (let n = 0; n < Math.min(entries.length, 8); n += 4) rows.push(row(...entries.slice(n, n + 4).map(x => btn(`strike_manage2:${x.id}:${i.user.id}`, `<@${x.id}>`, ButtonStyle.Primary)))); + return i.update({ embeds: [new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: i.guild.iconURL({ size: 128 }) || undefined }).setTitle('🏆 Strikes').setDescription(`**${i.guild.name}**\n\n${description}`).setColor(0xfee75c)], components: rows }); +} + +const panels = ['nuke', 'raid', 'automod', 'punishments', 'strikes', 'whitelist', 'logs', 'settings']; + +export default [ + ...panels.flatMap(type => [ + { name: `security_panel_${type}`, execute: async (i, c) => { if (!owned(i)) return deny(i); return type === 'strikes' ? strikes(i, c) : panel(i, c, type); } }, + { name: `security_panel_${type}2`, execute: async (i, c) => { if (!owned(i)) return deny(i); return type === 'strikes' ? strikes(i, c) : panel(i, c, type); } }, + ]), + { name: 'security_back', execute: async (i, c) => owned(i) ? dashboard(i, c) : deny(i) }, + { name: 'security_back2', execute: async (i, c) => owned(i) ? dashboard(i, c) : deny(i) }, + { name: 'security_refresh', execute: async (i, c) => owned(i) ? dashboard(i, c) : deny(i) }, + { name: 'security_strikes_refresh', execute: async (i, c) => owned(i) ? strikes(i, c) : deny(i) }, + { name: 'strikes_refresh2', execute: async (i, c) => owned(i) ? strikes(i, c) : deny(i) }, + { name: 'strike_reset2', execute: async (i, c, args) => { if (!owned(i)) return deny(i); await clearStrikes(c, i.guildId, args[0]); return strikes(i, c); } }, +]; \ No newline at end of file From fd29f6a84c7e62af4976dd10c722f0cf3e000de8 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:36:49 +0300 Subject: [PATCH 174/219] fix: unify legacy security button ids --- .../buttons/security/securityBaseAliases.js | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/interactions/buttons/security/securityBaseAliases.js b/src/interactions/buttons/security/securityBaseAliases.js index 1be75b54cd..18ff166d32 100644 --- a/src/interactions/buttons/security/securityBaseAliases.js +++ b/src/interactions/buttons/security/securityBaseAliases.js @@ -1,6 +1,6 @@ import { buildSecurityPanel, buildSecurityPanelControls, buildSecurityDashboard, buildSecurityControls } from '../../../commands/Security/security.js'; -import { getSecurityConfig, getStrikes, clearStrikes } from '../../../services/security/securityService.js'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { getSecurityConfig, getStrikes, clearStrikes, updateSecurityConfig } from '../../../services/security/securityService.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; const owned = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); @@ -23,26 +23,47 @@ async function strikes(i, c) { for (const member of members.values()) { if (member.user.bot) continue; const strike = await getStrikes(c, i.guildId, member.id).catch(() => ({ count: 0 })); - if (Number(strike?.count || 0) > 0) entries.push({ id: member.id, count: Number(strike.count) }); + const count = Number(strike?.count || 0); + if (count > 0) entries.push({ id: member.id, count }); } entries.sort((a, b) => b.count - a.count); const description = entries.slice(0, 10).map((x, n) => `${n + 1}. <@${x.id}> — **${x.count}** strikes`).join('\n') || 'No active strikes.'; const rows = [row(btn(`security_back:${i.user.id}`, '← Back'), btn(`security_strikes_refresh:${i.user.id}`, '🔄 Refresh', ButtonStyle.Success))]; - for (let n = 0; n < Math.min(entries.length, 8); n += 4) rows.push(row(...entries.slice(n, n + 4).map(x => btn(`strike_manage2:${x.id}:${i.user.id}`, `<@${x.id}>`, ButtonStyle.Primary)))); + for (let n = 0; n < Math.min(entries.length, 8); n += 4) { + rows.push(row(...entries.slice(n, n + 4).map(x => btn(`strike_manage2:${x.id}:${i.user.id}`, `<@${x.id}>`, ButtonStyle.Primary)))); + } return i.update({ embeds: [new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: i.guild.iconURL({ size: 128 }) || undefined }).setTitle('🏆 Strikes').setDescription(`**${i.guild.name}**\n\n${description}`).setColor(0xfee75c)], components: rows }); } +async function whitelist(i, c) { + const config = await getSecurityConfig(c, i.guildId); + return i.update({ embeds: [new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: i.guild.iconURL({ size: 128 }) || undefined }).setTitle('👤 Whitelist').setDescription(`Trusted users, roles and bots bypass security actions.\n\n**Users:** ${config.whitelist?.users?.length || 0}\n**Roles:** ${config.whitelist?.roles?.length || 0}\n**Bots:** ${config.whitelist?.bots?.length || 0}`).setColor(0x57f287)], components: [row(btn(`security_back:${i.user.id}`, '← Back'), btn(`security_whitelist_users:${i.user.id}`, '👤 Users', ButtonStyle.Primary), btn(`security_whitelist_roles:${i.user.id}`, '🎭 Roles', ButtonStyle.Primary), btn(`security_whitelist_bots:${i.user.id}`, '🤖 Bots', ButtonStyle.Primary))] }); +} + +async function whitelistModal(i, c, type, title, label) { + const config = await getSecurityConfig(c, i.guildId); + const current = Array.isArray(config.whitelist?.[type]) ? config.whitelist[type] : []; + return i.showModal(new ModalBuilder().setCustomId(`security_whitelist_${type}_modal:${i.user.id}`).setTitle(title).addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder('ID or mention, multiple values supported').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue(current.join('\n').slice(0, 4000)))); +} + const panels = ['nuke', 'raid', 'automod', 'punishments', 'strikes', 'whitelist', 'logs', 'settings']; +const panelHandlers = panels.flatMap(type => [ + { name: `security_panel_${type}`, execute: async (i, c) => { if (!owned(i)) return deny(i); return type === 'strikes' ? strikes(i, c) : type === 'whitelist' ? whitelist(i, c) : panel(i, c, type); } }, + { name: `security_panel_${type}2`, execute: async (i, c) => { if (!owned(i)) return deny(i); return type === 'strikes' ? strikes(i, c) : type === 'whitelist' ? whitelist(i, c) : panel(i, c, type); } }, +]); export default [ - ...panels.flatMap(type => [ - { name: `security_panel_${type}`, execute: async (i, c) => { if (!owned(i)) return deny(i); return type === 'strikes' ? strikes(i, c) : panel(i, c, type); } }, - { name: `security_panel_${type}2`, execute: async (i, c) => { if (!owned(i)) return deny(i); return type === 'strikes' ? strikes(i, c) : panel(i, c, type); } }, - ]), + ...panelHandlers, { name: 'security_back', execute: async (i, c) => owned(i) ? dashboard(i, c) : deny(i) }, { name: 'security_back2', execute: async (i, c) => owned(i) ? dashboard(i, c) : deny(i) }, { name: 'security_refresh', execute: async (i, c) => owned(i) ? dashboard(i, c) : deny(i) }, { name: 'security_strikes_refresh', execute: async (i, c) => owned(i) ? strikes(i, c) : deny(i) }, { name: 'strikes_refresh2', execute: async (i, c) => owned(i) ? strikes(i, c) : deny(i) }, { name: 'strike_reset2', execute: async (i, c, args) => { if (!owned(i)) return deny(i); await clearStrikes(c, i.guildId, args[0]); return strikes(i, c); } }, + { name: 'security_whitelist_users', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'users', 'Whitelist Users', 'User IDs') : deny(i) }, + { name: 'security_whitelist_roles', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'roles', 'Whitelist Roles', 'Role IDs') : deny(i) }, + { name: 'security_whitelist_bots', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'bots', 'Whitelist Bots', 'Bot IDs') : deny(i) }, + { name: 'security_whitelist_user', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'users', 'Whitelist Users', 'User IDs') : deny(i) }, + { name: 'security_whitelist_role', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'roles', 'Whitelist Roles', 'Role IDs') : deny(i) }, + { name: 'security_whitelist_bot', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'bots', 'Whitelist Bots', 'Bot IDs') : deny(i) }, ]; \ No newline at end of file From 66cdc3bb7824933f52e3f53e2036bc4ee5f2ef7c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:36:56 +0300 Subject: [PATCH 175/219] fix: support legacy security whitelist modals --- src/interactions/modals/security/security.js | 21 +++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/interactions/modals/security/security.js b/src/interactions/modals/security/security.js index 754ee214ac..e18b8a0c80 100644 --- a/src/interactions/modals/security/security.js +++ b/src/interactions/modals/security/security.js @@ -8,15 +8,11 @@ const ok = i => i.customId.split(':').at(-1) === i.user.id; const deny = i => i.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); function parseIds(value) { - return [...new Set(String(value || '') - .split(/[\s,\n]+/) - .map(part => part.match(/\d{15,25}/)?.[0]) - .filter(Boolean))].slice(0, 100); + return [...new Set(String(value || '').split(/[\s,\n]+/).map(part => part.match(/\d{15,25}/)?.[0]).filter(Boolean))].slice(0, 100); } async function saveWhitelist(interaction, client, type) { if (!ok(interaction)) return deny(interaction); - const ids = parseIds(interaction.fields.getTextInputValue('value')); const config = await getSecurityConfig(client, interaction.guildId); const whitelist = { @@ -24,14 +20,9 @@ async function saveWhitelist(interaction, client, type) { roles: Array.isArray(config.whitelist?.roles) ? config.whitelist.roles.map(String) : [], bots: Array.isArray(config.whitelist?.bots) ? config.whitelist.bots.map(String) : [], }; - whitelist[type] = ids; await updateSecurityConfig(client, interaction.guildId, { whitelist }); - - return interaction.reply({ - content: `Whitelist updated successfully. **${ids.length}** ${type} entr${ids.length === 1 ? 'y' : 'ies'} saved.`, - ephemeral: true, - }); + return interaction.reply({ content: `Whitelist updated successfully. **${ids.length}** ${type} entr${ids.length === 1 ? 'y' : 'ies'} saved.`, ephemeral: true }); } export default [ @@ -42,4 +33,10 @@ export default [ { name: 'security_final_wl_users', execute: (i, c) => saveWhitelist(i, c, 'users') }, { name: 'security_final_wl_roles', execute: (i, c) => saveWhitelist(i, c, 'roles') }, { name: 'security_final_wl_bots', execute: (i, c) => saveWhitelist(i, c, 'bots') }, -]; + { name: 'security_whitelist_users_modal', execute: (i, c) => saveWhitelist(i, c, 'users') }, + { name: 'security_whitelist_roles_modal', execute: (i, c) => saveWhitelist(i, c, 'roles') }, + { name: 'security_whitelist_bots_modal', execute: (i, c) => saveWhitelist(i, c, 'bots') }, + { name: 'security_whitelist_user_modal', execute: (i, c) => saveWhitelist(i, c, 'users') }, + { name: 'security_whitelist_role_modal', execute: (i, c) => saveWhitelist(i, c, 'roles') }, + { name: 'security_whitelist_bot_modal', execute: (i, c) => saveWhitelist(i, c, 'bots') }, +]; \ No newline at end of file From 746ac9cdb45819cfc8b2331803ebd2686ba3c4a6 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:39:46 +0300 Subject: [PATCH 176/219] Update securityService.js --- src/services/security/securityService.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/security/securityService.js b/src/services/security/securityService.js index 361bc97356..3b29856ea1 100644 --- a/src/services/security/securityService.js +++ b/src/services/security/securityService.js @@ -162,3 +162,8 @@ export async function processAutoMod(message, client) { ] }); return true; } + +export function getRecentCount(store, mapKey, now, windowMs) { + const existing = store.get(mapKey) || []; + return existing.filter(timestamp => now - timestamp <= windowMs); +} From 02d90de30f372736845f31b8ec269ea176dc4344 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:43:01 +0300 Subject: [PATCH 177/219] fix: remove anti-nuke dependency on getRecentCount export --- src/services/security/antiNuke.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/services/security/antiNuke.js b/src/services/security/antiNuke.js index 21813b5c67..8fce835c99 100644 --- a/src/services/security/antiNuke.js +++ b/src/services/security/antiNuke.js @@ -1,5 +1,5 @@ import { AuditLogEvent, PermissionFlagsBits } from 'discord.js'; -import { getSecurityConfig, isWhitelisted, sendSecurityLog, getRecentCount } from './securityService.js'; +import { getSecurityConfig, isWhitelisted, sendSecurityLog } from './securityService.js'; import { logger } from '../../utils/logger.js'; const counters = new Map(); @@ -16,6 +16,10 @@ const EVENT_MAP = { botAdd: AuditLogEvent.BotAdd, }; function key(guildId, executorId, type) { return `${guildId}:${executorId}:${type}`; } +function getRecentCount(store, mapKey, now, windowMs) { + const existing = store.get(mapKey) || []; + return existing.filter(timestamp => now - timestamp <= windowMs); +} async function findExecutor(guild, auditType, targetId = null) { const types = Array.isArray(auditType) ? auditType : [auditType]; @@ -64,12 +68,6 @@ async function punishExecutor(guild, executor, config, type, reason, targetId) { else if (action === 'strip') { const stripped = await stripDangerousRoles(member, reason); actionTaken = stripped ? 'strip' : 'none'; - } else if (action === 'warn') { - const channel = guild.systemChannel; - if (channel?.isTextBased()) { - const warning = await channel.send(`⚠️ <@${executor.id}> Anti-Nuke triggered: **${reason}**.`).catch(() => null); - if (warning) setTimeout(() => warning.delete().catch(() => {}), 5000); - } } if (type === 'botAdd' && targetId) { From 1cfebe5234365d96a968953847f3e48a67630f3a Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:45:15 +0300 Subject: [PATCH 178/219] fix(security): retire duplicate legacy button aliases --- .../buttons/security/securityBaseAliases.js | 72 +------------------ 1 file changed, 3 insertions(+), 69 deletions(-) diff --git a/src/interactions/buttons/security/securityBaseAliases.js b/src/interactions/buttons/security/securityBaseAliases.js index 18ff166d32..86bd804603 100644 --- a/src/interactions/buttons/security/securityBaseAliases.js +++ b/src/interactions/buttons/security/securityBaseAliases.js @@ -1,69 +1,3 @@ -import { buildSecurityPanel, buildSecurityPanelControls, buildSecurityDashboard, buildSecurityControls } from '../../../commands/Security/security.js'; -import { getSecurityConfig, getStrikes, clearStrikes, updateSecurityConfig } from '../../../services/security/securityService.js'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; - -const owned = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; -const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); -const btn = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); -const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); - -async function dashboard(i, c) { - const config = await getSecurityConfig(c, i.guildId); - return i.update({ embeds: [buildSecurityDashboard(config, i.guild)], components: buildSecurityControls(i.user.id) }); -} - -async function panel(i, c, type) { - const config = await getSecurityConfig(c, i.guildId); - return i.update({ embeds: [buildSecurityPanel(config, i.guild, type)], components: buildSecurityPanelControls(i.user.id, type, config) }); -} - -async function strikes(i, c) { - const members = await i.guild.members.fetch().catch(() => i.guild.members.cache); - const entries = []; - for (const member of members.values()) { - if (member.user.bot) continue; - const strike = await getStrikes(c, i.guildId, member.id).catch(() => ({ count: 0 })); - const count = Number(strike?.count || 0); - if (count > 0) entries.push({ id: member.id, count }); - } - entries.sort((a, b) => b.count - a.count); - const description = entries.slice(0, 10).map((x, n) => `${n + 1}. <@${x.id}> — **${x.count}** strikes`).join('\n') || 'No active strikes.'; - const rows = [row(btn(`security_back:${i.user.id}`, '← Back'), btn(`security_strikes_refresh:${i.user.id}`, '🔄 Refresh', ButtonStyle.Success))]; - for (let n = 0; n < Math.min(entries.length, 8); n += 4) { - rows.push(row(...entries.slice(n, n + 4).map(x => btn(`strike_manage2:${x.id}:${i.user.id}`, `<@${x.id}>`, ButtonStyle.Primary)))); - } - return i.update({ embeds: [new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: i.guild.iconURL({ size: 128 }) || undefined }).setTitle('🏆 Strikes').setDescription(`**${i.guild.name}**\n\n${description}`).setColor(0xfee75c)], components: rows }); -} - -async function whitelist(i, c) { - const config = await getSecurityConfig(c, i.guildId); - return i.update({ embeds: [new EmbedBuilder().setAuthor({ name: 'Infinity Security Center', iconURL: i.guild.iconURL({ size: 128 }) || undefined }).setTitle('👤 Whitelist').setDescription(`Trusted users, roles and bots bypass security actions.\n\n**Users:** ${config.whitelist?.users?.length || 0}\n**Roles:** ${config.whitelist?.roles?.length || 0}\n**Bots:** ${config.whitelist?.bots?.length || 0}`).setColor(0x57f287)], components: [row(btn(`security_back:${i.user.id}`, '← Back'), btn(`security_whitelist_users:${i.user.id}`, '👤 Users', ButtonStyle.Primary), btn(`security_whitelist_roles:${i.user.id}`, '🎭 Roles', ButtonStyle.Primary), btn(`security_whitelist_bots:${i.user.id}`, '🤖 Bots', ButtonStyle.Primary))] }); -} - -async function whitelistModal(i, c, type, title, label) { - const config = await getSecurityConfig(c, i.guildId); - const current = Array.isArray(config.whitelist?.[type]) ? config.whitelist[type] : []; - return i.showModal(new ModalBuilder().setCustomId(`security_whitelist_${type}_modal:${i.user.id}`).setTitle(title).addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder('ID or mention, multiple values supported').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue(current.join('\n').slice(0, 4000)))); -} - -const panels = ['nuke', 'raid', 'automod', 'punishments', 'strikes', 'whitelist', 'logs', 'settings']; -const panelHandlers = panels.flatMap(type => [ - { name: `security_panel_${type}`, execute: async (i, c) => { if (!owned(i)) return deny(i); return type === 'strikes' ? strikes(i, c) : type === 'whitelist' ? whitelist(i, c) : panel(i, c, type); } }, - { name: `security_panel_${type}2`, execute: async (i, c) => { if (!owned(i)) return deny(i); return type === 'strikes' ? strikes(i, c) : type === 'whitelist' ? whitelist(i, c) : panel(i, c, type); } }, -]); - -export default [ - ...panelHandlers, - { name: 'security_back', execute: async (i, c) => owned(i) ? dashboard(i, c) : deny(i) }, - { name: 'security_back2', execute: async (i, c) => owned(i) ? dashboard(i, c) : deny(i) }, - { name: 'security_refresh', execute: async (i, c) => owned(i) ? dashboard(i, c) : deny(i) }, - { name: 'security_strikes_refresh', execute: async (i, c) => owned(i) ? strikes(i, c) : deny(i) }, - { name: 'strikes_refresh2', execute: async (i, c) => owned(i) ? strikes(i, c) : deny(i) }, - { name: 'strike_reset2', execute: async (i, c, args) => { if (!owned(i)) return deny(i); await clearStrikes(c, i.guildId, args[0]); return strikes(i, c); } }, - { name: 'security_whitelist_users', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'users', 'Whitelist Users', 'User IDs') : deny(i) }, - { name: 'security_whitelist_roles', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'roles', 'Whitelist Roles', 'Role IDs') : deny(i) }, - { name: 'security_whitelist_bots', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'bots', 'Whitelist Bots', 'Bot IDs') : deny(i) }, - { name: 'security_whitelist_user', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'users', 'Whitelist Users', 'User IDs') : deny(i) }, - { name: 'security_whitelist_role', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'roles', 'Whitelist Roles', 'Role IDs') : deny(i) }, - { name: 'security_whitelist_bot', execute: async (i, c) => owned(i) ? whitelistModal(i, c, 'bots', 'Whitelist Bots', 'Bot IDs') : deny(i) }, -]; \ No newline at end of file +// Legacy Security aliases were retired. +// The canonical handlers now live in securityDashboardCore.js and securityFinalOverrides.js. +export default []; From 67decd10490216ecbd0237f081c78b8eb22cd87d Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:45:19 +0300 Subject: [PATCH 179/219] fix(security): retire duplicate dashboard fix handlers --- src/handlers/securityDashboardFixes.js | 60 ++------------------------ 1 file changed, 3 insertions(+), 57 deletions(-) diff --git a/src/handlers/securityDashboardFixes.js b/src/handlers/securityDashboardFixes.js index 1fa4439738..501a78d6ef 100644 --- a/src/handlers/securityDashboardFixes.js +++ b/src/handlers/securityDashboardFixes.js @@ -1,57 +1,3 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from 'discord.js'; -import { getSecurityConfig, updateSecurityConfig, getStrikes, clearStrikes, sendSecurityLog } from '../services/security/securityService.js'; - -const ok = i => i.customId.split(':').at(-1) === i.user.id; -const deny = i => i.reply({ content: 'This security dashboard belongs to another moderator.', flags: MessageFlags.Ephemeral }); -const row = (...buttons) => new ActionRowBuilder().addComponents(buttons); -const button = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); -const modalField = (id, label, value = '', style = TextInputStyle.Paragraph) => new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId(id).setLabel(label).setStyle(style).setRequired(false).setValue(String(value).slice(0, 4000))); - -async function whitelist(i, c) { - const x = await getSecurityConfig(c, i.guildId); - return i.update({ - embeds: [new EmbedBuilder().setColor(0x57f287).setTitle('👤 Whitelist').setDescription(`الحسابات المضافة هنا تتجاوز إجراءات الحماية.\n\n**الأشخاص:** ${x.whitelist.users.length}\n**الرتب:** ${x.whitelist.roles.length}\n**البوتات:** ${x.whitelist.bots.length}`)], - components: [row(button(`security_back2:${i.user.id}`, '← Back'), button(`wl_users2:${i.user.id}`, '👤 الأشخاص', ButtonStyle.Primary), button(`wl_roles2:${i.user.id}`, '🎭 الرتب', ButtonStyle.Primary), button(`wl_bots2:${i.user.id}`, '🤖 البوتات', ButtonStyle.Primary))], - }); -} - -function showWhitelistModal(i, type, title, label, current) { - return i.showModal(new ModalBuilder().setCustomId(`security_wl_${type}_modal:${i.user.id}`).setTitle(title).addComponents(modalField('value', label, current))); -} - -async function strikes(i, c) { - const members = await i.guild.members.fetch().catch(() => i.guild.members.cache); - const entries = []; - for (const m of members.values()) { - if (m.user.bot) continue; - const s = await getStrikes(c, i.guildId, m.id).catch(() => ({ count: 0 })); - if (s.count) entries.push({ id: m.id, count: Number(s.count) }); - } - entries.sort((a, b) => b.count - a.count); - const text = entries.slice(0, 10).map((e, n) => `${n + 1}. <@${e.id}> — **${e.count}** strikes`).join('\n') || 'لا توجد Strikes فعالة'; - const rows = [row(button(`security_back2:${i.user.id}`, '← Back'), button(`security_strikes_refresh2:${i.user.id}`, '🔄 Refresh', ButtonStyle.Success))]; - if (entries.length) rows.push(row(...entries.slice(0, 4).map(e => button(`security_strike_manage:${e.id}:${i.user.id}`, `Manage ${e.count}`, ButtonStyle.Primary)))); - return i.update({ embeds: [new EmbedBuilder().setColor(0xfee75c).setTitle('🏆 Strikes').setDescription(`**${i.guild.name}**\n\n${text}\n\nيمكنك تصفير الـStrike من صفحة العضو.`)], components: rows }); -} - -async function strikeMember(i, c, userId) { - const s = await getStrikes(c, i.guildId, userId).catch(() => ({ count: 0, lastReason: '' })); - return i.update({ - embeds: [new EmbedBuilder().setColor(0xfee75c).setTitle('🏆 إدارة Strikes').setDescription(`<@${userId}>\n\n**Strikes:** ${s.count || 0}\n**آخر سبب:** ${s.lastReason || '—'}`)], - components: [row(button(`security_strike_reset:${userId}:${i.user.id}`, '🧹 Reset Strikes', ButtonStyle.Danger), button(`security_strikes_refresh2:${i.user.id}`, '← Back'))], - }); -} - -export default [ - { name: 'security_panel_whitelist2', execute: async (i, c) => ok(i) ? whitelist(i, c) : deny(i) }, - { name: 'wl_users2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return showWhitelistModal(i, 'users', 'Whitelist Users', 'User IDs, one per line', x.whitelist.users.join('\n')); } }, - { name: 'wl_roles2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return showWhitelistModal(i, 'roles', 'Whitelist Roles', 'Role IDs, one per line', x.whitelist.roles.join('\n')); } }, - { name: 'wl_bots2', execute: async (i, c) => { if (!ok(i)) return deny(i); const x = await getSecurityConfig(c, i.guildId); return showWhitelistModal(i, 'bots', 'Whitelist Bots', 'Bot IDs, one per line', x.whitelist.bots.join('\n')); } }, - { name: 'security_panel_strikes2', execute: async (i, c) => ok(i) ? strikes(i, c) : deny(i) }, - { name: 'security_strikes_refresh2', execute: async (i, c) => ok(i) ? strikes(i, c) : deny(i) }, - { name: 'security_strike_manage', execute: async (i, c, args) => ok(i) ? strikeMember(i, c, args[0]) : deny(i) }, - { name: 'security_strike_reset', execute: async (i, c, args) => { if (!ok(i)) return deny(i); const userId = args[0]; await clearStrikes(c, i.guildId, userId); await sendSecurityLog(c, i.guild, { title: 'Strikes Reset', description: `<@${userId}> strikes reset by <@${i.user.id}>`, color: 0x57f287 }); return strikeMember(i, c, userId); } }, - { name: 'security_wl_users_modal', execute: async (i, c) => { if (!ok(i)) return deny(i); const users = String(i.fields.getTextInputValue('value') || '').split(/\s+/).filter(Boolean).slice(0, 100); await updateSecurityConfig(c, i.guildId, { whitelist: { users } }); return whitelist(i, c); } }, - { name: 'security_wl_roles_modal', execute: async (i, c) => { if (!ok(i)) return deny(i); const roles = String(i.fields.getTextInputValue('value') || '').split(/\s+/).filter(Boolean).slice(0, 100); await updateSecurityConfig(c, i.guildId, { whitelist: { roles } }); return whitelist(i, c); } }, - { name: 'security_wl_bots_modal', execute: async (i, c) => { if (!ok(i)) return deny(i); const bots = String(i.fields.getTextInputValue('value') || '').split(/\s+/).filter(Boolean).slice(0, 100); await updateSecurityConfig(c, i.guildId, { whitelist: { bots } }); return whitelist(i, c); } }, -]; +// Retired duplicate Security dashboard handlers. +// Canonical handlers are registered by securityDashboardCore.js and securityFinalOverrides.js. +export default []; From 87d10ea00ec2d3d1bf599ad91f0abf0c637939e9 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:45:24 +0300 Subject: [PATCH 180/219] fix(security): retire duplicate strike compatibility handlers --- .../security/securityStrikeCompatibility.js | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/src/interactions/buttons/security/securityStrikeCompatibility.js b/src/interactions/buttons/security/securityStrikeCompatibility.js index 654e5eb7e3..2b472eeb80 100644 --- a/src/interactions/buttons/security/securityStrikeCompatibility.js +++ b/src/interactions/buttons/security/securityStrikeCompatibility.js @@ -1,33 +1,3 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; -import { getStrikes, clearStrikes, sendSecurityLog } from '../../../services/security/securityService.js'; - -const ok = interaction => interaction.customId.split(':').at(-1) === interaction.user.id; -const deny = interaction => interaction.reply({ content: 'This security dashboard belongs to another moderator.', ephemeral: true }); -const button = (id, label, style = ButtonStyle.Secondary) => new ButtonBuilder().setCustomId(id).setLabel(label).setStyle(style); - -async function manage(interaction, client, userId) { - const strike = await getStrikes(client, interaction.guildId, userId).catch(() => ({ count: 0, lastReason: '' })); - return interaction.update({ - embeds: [new EmbedBuilder().setColor(0xfee75c).setTitle('🏆 إدارة Strikes').setDescription(`<@${userId}>\n\n**Strikes:** ${strike.count || 0}\n**آخر سبب:** ${strike.lastReason || '—'}`)], - components: [new ActionRowBuilder().addComponents( - button(`strike_reset2:${userId}:${interaction.user.id}`, '🧹 Reset Strikes', ButtonStyle.Danger), - button(`strikes_back2:${interaction.user.id}`, '← Back') - )] - }); -} - -async function reset(interaction, client, userId) { - await clearStrikes(client, interaction.guildId, userId); - await sendSecurityLog(client, interaction.guild, { - title: 'Strikes Reset', - description: `<@${userId}> strikes reset by <@${interaction.user.id}>`, - color: 0x57f287, - }); - return manage(interaction, client, userId); -} - -export default [ - { name: 'strike_manage2', execute: async (i, c, args) => ok(i) ? manage(i, c, args[0]) : deny(i) }, - { name: 'strike_reset2', execute: async (i, c, args) => ok(i) ? reset(i, c, args[0]) : deny(i) }, - { name: 'strikes_back2', execute: async (i, c) => ok(i) ? i.update({ components: [new ActionRowBuilder().addComponents(button(`security_back2:${i.user.id}`, '← Back'))] }) : deny(i) }, -]; \ No newline at end of file +// Legacy strike compatibility handlers retired. +// Strikes are handled by securityFinalOverrides.js. +export default []; From ce427a3810322237c6a5877e60080a25800b8cf5 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 01:45:49 +0300 Subject: [PATCH 181/219] ci: add JavaScript syntax validation --- .github/workflows/javascript-syntax.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/javascript-syntax.yml diff --git a/.github/workflows/javascript-syntax.yml b/.github/workflows/javascript-syntax.yml new file mode 100644 index 0000000000..90f189c45b --- /dev/null +++ b/.github/workflows/javascript-syntax.yml @@ -0,0 +1,23 @@ +name: JavaScript Syntax Check + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + syntax: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Check JavaScript syntax + shell: bash + run: | + set -euo pipefail + find src scripts -type f -name '*.js' -print0 | while IFS= read -r -d '' file; do + node --check "$file" + done From 52d30266187cc1c3c67ab709b624e6482b2328b4 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 02:57:42 +0300 Subject: [PATCH 182/219] Update securityFinalOverrides.js --- src/interactions/buttons/security/securityFinalOverrides.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interactions/buttons/security/securityFinalOverrides.js b/src/interactions/buttons/security/securityFinalOverrides.js index fc2f7b042d..2ce09aed0a 100644 --- a/src/interactions/buttons/security/securityFinalOverrides.js +++ b/src/interactions/buttons/security/securityFinalOverrides.js @@ -49,7 +49,7 @@ async function whitelistPage(interaction, client) { } function whitelistModal(interaction, type, title, label, current) { - return interaction.showModal(new ModalBuilder().setCustomId(`security_final_wl_${type}:${interaction.user.id}`).setTitle(title).addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder('ID or mention, multiple values supported').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue(current.join('\n').slice(0, 4000)))); + return interaction.showModal(new ModalBuilder().setCustomId(`security_final_wl_${type}:${interaction.user.id}`).setTitle(title).addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder('ID or mention, multiple values supported').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue(current.join('\n').slice(0, 4000))))); } async function saveWhitelist(interaction, client, type) { @@ -88,4 +88,4 @@ export default [ { name: 'security_final_wl_roles', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'roles') : deny(i) }, { name: 'security_final_wl_bots', execute: async (i, c) => ok(i) ? saveWhitelist(i, c, 'bots') : deny(i) }, { name: 'automod_punishment', execute: autoModPunishment }, -]; \ No newline at end of file +]; From e9f6509efb9be6135d9f6f56fa8ce95d737543cb Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 03:02:54 +0300 Subject: [PATCH 183/219] Fix formatting in securityFinalOverrides.js --- src/interactions/buttons/security/securityFinalOverrides.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interactions/buttons/security/securityFinalOverrides.js b/src/interactions/buttons/security/securityFinalOverrides.js index 2ce09aed0a..0824a75c4b 100644 --- a/src/interactions/buttons/security/securityFinalOverrides.js +++ b/src/interactions/buttons/security/securityFinalOverrides.js @@ -49,7 +49,7 @@ async function whitelistPage(interaction, client) { } function whitelistModal(interaction, type, title, label, current) { - return interaction.showModal(new ModalBuilder().setCustomId(`security_final_wl_${type}:${interaction.user.id}`).setTitle(title).addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder('ID or mention, multiple values supported').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue(current.join('\n').slice(0, 4000))))); + return interaction.showModal(new ModalBuilder().setCustomId(`security_final_wl_${type}:${interaction.user.id}`).setTitle(title).addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder('ID or mention, multiple values supported').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue(current.join('\n').slice(0, 4000)))); } async function saveWhitelist(interaction, client, type) { From e28f1c386e09c498633af058379f95cac9d2a348 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 03:03:51 +0300 Subject: [PATCH 184/219] Fix formatting in securityFinalOverrides.js --- src/interactions/buttons/security/securityFinalOverrides.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interactions/buttons/security/securityFinalOverrides.js b/src/interactions/buttons/security/securityFinalOverrides.js index 0824a75c4b..2ce09aed0a 100644 --- a/src/interactions/buttons/security/securityFinalOverrides.js +++ b/src/interactions/buttons/security/securityFinalOverrides.js @@ -49,7 +49,7 @@ async function whitelistPage(interaction, client) { } function whitelistModal(interaction, type, title, label, current) { - return interaction.showModal(new ModalBuilder().setCustomId(`security_final_wl_${type}:${interaction.user.id}`).setTitle(title).addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder('ID or mention, multiple values supported').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue(current.join('\n').slice(0, 4000)))); + return interaction.showModal(new ModalBuilder().setCustomId(`security_final_wl_${type}:${interaction.user.id}`).setTitle(title).addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId('value').setLabel(label).setPlaceholder('ID or mention, multiple values supported').setStyle(TextInputStyle.Paragraph).setRequired(false).setValue(current.join('\n').slice(0, 4000))))); } async function saveWhitelist(interaction, client, type) { From 763e654669ddf4771f613c1b05bb1b5234bd7fbd Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 03:50:23 +0300 Subject: [PATCH 185/219] feat: add staff management system * feat(staff): add persistent staff management service * feat(staff): add staff management dashboard and commands * feat(staff): add staff dashboard button interactions --- src/commands/Community/staff.js | 179 ++++++++++++++++++++++++++++++ src/interactions/buttons/staff.js | 112 +++++++++++++++++++ src/services/staffService.js | 152 +++++++++++++++++++++++++ 3 files changed, 443 insertions(+) create mode 100644 src/commands/Community/staff.js create mode 100644 src/interactions/buttons/staff.js create mode 100644 src/services/staffService.js diff --git a/src/commands/Community/staff.js b/src/commands/Community/staff.js new file mode 100644 index 0000000000..c34b1b32d8 --- /dev/null +++ b/src/commands/Community/staff.js @@ -0,0 +1,179 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ChannelType, + EmbedBuilder, + PermissionFlagsBits, + SlashCommandBuilder, +} from 'discord.js'; +import { + calculateActivityScore, + countWarnings, + getStaffData, + getStaffProfile, + updateStaffConfig, + addStaffWarning, + addPromotion, + addDemotion, + addStaffNote, +} from '../../services/staffService.js'; +import { withErrorHandling } from '../../utils/errorHandler.js'; + +const dashboardButtons = () => [ + new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('staff_my_profile').setLabel('My Profile').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('staff_activity').setLabel('Activity').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('staff_list').setLabel('Staff List').setStyle(ButtonStyle.Secondary), + ), + new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('staff_warnings').setLabel('Warnings').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('staff_promotions').setLabel('Promotions').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('staff_demotions').setLabel('Demotions').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('staff_notes').setLabel('Notes').setStyle(ButtonStyle.Secondary), + ), +]; + +function dashboardEmbed(guild, data) { + const count = Object.keys(data.members).length; + const warned = Object.values(data.members).filter((member) => countWarnings(member) > 0).length; + return new EmbedBuilder() + .setTitle('Staff Management') + .setDescription(`**${guild.name}**\nCentralized staff management, activity and history.`) + .addFields( + { name: 'Staff', value: `**${count}**`, inline: true }, + { name: 'With Warnings', value: `**${warned}**`, inline: true }, + { name: 'Review Threshold', value: `**${data.config.warningsBeforeReview}** warnings`, inline: true }, + ) + .setFooter({ text: 'Use /staff profile, /staff warn, /staff promote or /staff demote to manage staff.' }); +} + +export default { + data: new SlashCommandBuilder() + .setName('staff') + .setDescription('Manage server staff') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand((sub) => sub.setName('dashboard').setDescription('Open the staff dashboard')) + .addSubcommand((sub) => sub + .setName('profile') + .setDescription('View a staff profile') + .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(true))) + .addSubcommand((sub) => sub + .setName('warn') + .setDescription('Issue a staff warning') + .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(true)) + .addStringOption((opt) => opt.setName('reason').setDescription('Reason').setRequired(true).setMaxLength(500))) + .addSubcommand((sub) => sub + .setName('promote') + .setDescription('Promote a staff member') + .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(true)) + .addRoleOption((opt) => opt.setName('new_role').setDescription('Role to give').setRequired(true)) + .addRoleOption((opt) => opt.setName('remove_role').setDescription('Role to remove').setRequired(true)) + .addStringOption((opt) => opt.setName('reason').setDescription('Reason').setRequired(true).setMaxLength(500))) + .addSubcommand((sub) => sub + .setName('demote') + .setDescription('Demote a staff member') + .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(true)) + .addRoleOption((opt) => opt.setName('new_role').setDescription('Role to give').setRequired(true)) + .addRoleOption((opt) => opt.setName('remove_role').setDescription('Role to remove').setRequired(true)) + .addStringOption((opt) => opt.setName('reason').setDescription('Reason').setRequired(true).setMaxLength(500))) + .addSubcommand((sub) => sub + .setName('note') + .setDescription('Add a private staff note') + .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(true)) + .addStringOption((opt) => opt.setName('note').setDescription('Internal note').setRequired(true).setMaxLength(1000))) + .addSubcommand((sub) => sub + .setName('setup') + .setDescription('Configure staff channels and permissions') + .addChannelOption((opt) => opt.setName('promotion_channel').setDescription('Promotion announcement channel').addChannelTypes(ChannelType.GuildText)) + .addChannelOption((opt) => opt.setName('demotion_channel').setDescription('Demotion announcement channel').addChannelTypes(ChannelType.GuildText)) + .addChannelOption((opt) => opt.setName('warning_channel').setDescription('Staff warning channel').addChannelTypes(ChannelType.GuildText)) + .addRoleOption((opt) => opt.setName('manager_role').setDescription('Role allowed to manage staff')) + .addIntegerOption((opt) => opt.setName('warnings_before_review').setDescription('Warnings before review').setMinValue(1).setMaxValue(20))), + category: 'Community', + execute: withErrorHandling(async (interaction) => { + if (!interaction.inGuild()) return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); + const sub = interaction.options.getSubcommand(); + const data = await getStaffData(interaction.guildId); + + if (sub === 'dashboard') { + return interaction.reply({ embeds: [dashboardEmbed(interaction.guild, data)], components: dashboardButtons() }); + } + + if (sub === 'setup') { + const patch = {}; + const promotionChannel = interaction.options.getChannel('promotion_channel'); + const demotionChannel = interaction.options.getChannel('demotion_channel'); + const warningChannel = interaction.options.getChannel('warning_channel'); + const managerRole = interaction.options.getRole('manager_role'); + const threshold = interaction.options.getInteger('warnings_before_review'); + if (promotionChannel) patch.promotionChannelId = promotionChannel.id; + if (demotionChannel) patch.demotionChannelId = demotionChannel.id; + if (warningChannel) patch.warningChannelId = warningChannel.id; + if (managerRole) patch.managerRoleId = managerRole.id; + if (threshold) patch.warningsBeforeReview = threshold; + if (!Object.keys(patch).length) return interaction.reply({ content: 'No settings were supplied.', ephemeral: true }); + const updated = await updateStaffConfig(interaction.guildId, patch); + return interaction.reply({ content: `Staff settings saved.\nPromotion: ${updated.config.promotionChannelId ? `<#${updated.config.promotionChannelId}>` : 'Not set'}\nDemotion: ${updated.config.demotionChannelId ? `<#${updated.config.demotionChannelId}>` : 'Not set'}\nWarnings: ${updated.config.warningChannelId ? `<#${updated.config.warningChannelId}>` : 'Not set'}\nManager role: ${updated.config.managerRoleId ? `<@&${updated.config.managerRoleId}>` : 'Not set'}`, ephemeral: true }); + } + + const user = interaction.options.getUser('user'); + if (!user) return interaction.reply({ content: 'A staff member is required.', ephemeral: true }); + + if (sub === 'profile') { + const member = await interaction.guild.members.fetch(user.id).catch(() => null); + const profile = await getStaffProfile(interaction.guildId, user.id, { joinedAt: member?.joinedAt?.toISOString() }); + return interaction.reply({ embeds: [new EmbedBuilder() + .setTitle('Staff Profile') + .setDescription(`**${user}**\n${member?.roles?.highest ? `Current Role: **${member.roles.highest.name}**` : ''}`) + .addFields( + { name: 'Activity', value: `**${calculateActivityScore(profile)}%**`, inline: true }, + { name: 'Warnings', value: `**${countWarnings(profile)}**`, inline: true }, + { name: 'Moderation Actions', value: `**${profile.activity?.moderationActions || 0}**`, inline: true }, + { name: 'Tickets Handled', value: `**${profile.activity?.ticketsHandled || 0}**`, inline: true }, + { name: 'Promotions', value: `**${profile.promotions.length}**`, inline: true }, + { name: 'Demotions', value: `**${profile.demotions.length}**`, inline: true }, + ) + .setThumbnail(user.displayAvatarURL())] }); + } + + if (sub === 'warn') { + const reason = interaction.options.getString('reason', true); + const warning = await addStaffWarning(interaction.guildId, user.id, interaction.user.id, reason); + const profile = await getStaffProfile(interaction.guildId, user.id); + const channelId = data.config.warningChannelId; + if (channelId) { + const channel = await interaction.guild.channels.fetch(channelId).catch(() => null); + if (channel?.isTextBased()) await channel.send(`⚠️ **Staff Warning**\n${user}\nReason: ${reason}\nIssued by: ${interaction.user}\nWarnings: **${profile.warnings.length}/${data.config.warningsBeforeReview}**`); + } + return interaction.reply({ content: `Staff warning issued to ${user}. Total warnings: **${profile.warnings.length}**.`, ephemeral: true }); + } + + const newRole = interaction.options.getRole('new_role'); + const removeRole = interaction.options.getRole('remove_role'); + const reason = interaction.options.getString('reason', true); + if (!newRole || !removeRole) return interaction.reply({ content: 'Both roles are required.', ephemeral: true }); + const target = await interaction.guild.members.fetch(user.id); + if (newRole.position >= interaction.guild.members.me.roles.highest.position || removeRole.position >= interaction.guild.members.me.roles.highest.position) return interaction.reply({ content: 'I cannot manage one of these roles because it is above my highest role.', ephemeral: true }); + + if (sub === 'promote' || sub === 'demote') { + await target.roles.remove(removeRole).catch(() => null); + await target.roles.add(newRole); + const record = { fromRoleId: removeRole.id, fromRoleName: removeRole.name, toRoleId: newRole.id, toRoleName: newRole.name, reason, issuerId: interaction.user.id }; + if (sub === 'promote') await addPromotion(interaction.guildId, user.id, record); + else await addDemotion(interaction.guildId, user.id, record); + const channelId = sub === 'promote' ? data.config.promotionChannelId : data.config.demotionChannelId; + if (channelId) { + const channel = await interaction.guild.channels.fetch(channelId).catch(() => null); + if (channel?.isTextBased()) await channel.send(`${sub === 'promote' ? '📈' : '📉'} **Staff ${sub === 'promote' ? 'Promotion' : 'Demotion'}**\n${user}\n**${removeRole.name}** → **${newRole.name}**\nReason: ${reason}\nBy: ${interaction.user}`); + } + return interaction.reply({ content: `${user} has been ${sub === 'promote' ? 'promoted' : 'demoted'}: **${removeRole.name}** → **${newRole.name}**.`, ephemeral: true }); + } + + if (sub === 'note') { + const note = interaction.options.getString('note', true); + await addStaffNote(interaction.guildId, user.id, interaction.user.id, note); + return interaction.reply({ content: `Private staff note added for ${user}.`, ephemeral: true }); + } + }), +}; diff --git a/src/interactions/buttons/staff.js b/src/interactions/buttons/staff.js new file mode 100644 index 0000000000..e6900139cd --- /dev/null +++ b/src/interactions/buttons/staff.js @@ -0,0 +1,112 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { calculateActivityScore, countWarnings, getStaffData, getStaffProfile } from '../../services/staffService.js'; + +function nav() { + return [ + new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('staff_my_profile').setLabel('My Profile').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('staff_activity').setLabel('Activity').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('staff_list').setLabel('Staff List').setStyle(ButtonStyle.Secondary), + ), + new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('staff_warnings').setLabel('Warnings').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('staff_promotions').setLabel('Promotions').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('staff_demotions').setLabel('Demotions').setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId('staff_notes').setLabel('Notes').setStyle(ButtonStyle.Secondary), + ), + ]; +} + +function back() { + return [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId('staff_home').setLabel('Back').setStyle(ButtonStyle.Secondary))]; +} + +async function render(interaction, embed) { + return interaction.update({ embeds: [embed], components: back() }); +} + +export default [ + { + name: 'staff_home', + async execute(interaction) { + const data = await getStaffData(interaction.guildId); + const warned = Object.values(data.members).filter((m) => countWarnings(m) > 0).length; + await interaction.update({ + embeds: [new EmbedBuilder().setTitle('Staff Management').setDescription(`**${interaction.guild.name}**\nCentralized staff management, activity and history.`).addFields( + { name: 'Staff', value: `**${Object.keys(data.members).length}**`, inline: true }, + { name: 'With Warnings', value: `**${warned}**`, inline: true }, + { name: 'Review Threshold', value: `**${data.config.warningsBeforeReview}** warnings`, inline: true }, + )], + components: nav(), + }); + }, + }, + { + name: 'staff_my_profile', + async execute(interaction) { + const profile = await getStaffProfile(interaction.guildId, interaction.user.id); + await render(interaction, new EmbedBuilder().setTitle('Staff Profile').setDescription(`${interaction.user}`).addFields( + { name: 'Activity', value: `**${calculateActivityScore(profile)}%**`, inline: true }, + { name: 'Warnings', value: `**${countWarnings(profile)}**`, inline: true }, + { name: 'Moderation Actions', value: `**${profile.activity?.moderationActions || 0}**`, inline: true }, + { name: 'Tickets Handled', value: `**${profile.activity?.ticketsHandled || 0}**`, inline: true }, + { name: 'Promotions', value: `**${profile.promotions.length}**`, inline: true }, + { name: 'Demotions', value: `**${profile.demotions.length}**`, inline: true }, + ).setThumbnail(interaction.user.displayAvatarURL())); + }, + }, + { + name: 'staff_activity', + async execute(interaction) { + const data = await getStaffData(interaction.guildId); + const rows = Object.entries(data.members).map(([id, profile]) => ({ id, score: calculateActivityScore(profile) })).sort((a, b) => b.score - a.score).slice(0, 10); + const description = rows.length ? rows.map((row, i) => `${i + 1}. <@${row.id}> — **${row.score}%**`).join('\n') : 'No staff activity has been recorded yet.'; + await render(interaction, new EmbedBuilder().setTitle('Staff Activity').setDescription(description)); + }, + }, + { + name: 'staff_list', + async execute(interaction) { + const data = await getStaffData(interaction.guildId); + const entries = Object.entries(data.members).slice(0, 20); + const description = entries.length ? entries.map(([id, profile]) => `<@${id}> — **${calculateActivityScore(profile)}%** activity • **${countWarnings(profile)}** warnings`).join('\n') : 'No staff profiles have been created yet.'; + await render(interaction, new EmbedBuilder().setTitle('Staff List').setDescription(description)); + }, + }, + { + name: 'staff_warnings', + async execute(interaction) { + const data = await getStaffData(interaction.guildId); + const entries = Object.entries(data.members).filter(([, p]) => countWarnings(p) > 0).slice(0, 10); + const description = entries.length ? entries.map(([id, p]) => `<@${id}> — **${countWarnings(p)}** warnings`).join('\n') : 'No staff warnings.'; + await render(interaction, new EmbedBuilder().setTitle('Staff Warnings').setDescription(description)); + }, + }, + { + name: 'staff_promotions', + async execute(interaction) { + const data = await getStaffData(interaction.guildId); + const records = Object.entries(data.members).flatMap(([id, p]) => (p.promotions || []).map((record) => ({ id, ...record }))).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))).slice(0, 10); + const description = records.length ? records.map((r) => `<@${r.id}> — **${r.fromRoleName}** → **${r.toRoleName}**\n${r.reason}`).join('\n\n') : 'No promotions recorded.'; + await render(interaction, new EmbedBuilder().setTitle('Staff Promotions').setDescription(description)); + }, + }, + { + name: 'staff_demotions', + async execute(interaction) { + const data = await getStaffData(interaction.guildId); + const records = Object.entries(data.members).flatMap(([id, p]) => (p.demotions || []).map((record) => ({ id, ...record }))).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))).slice(0, 10); + const description = records.length ? records.map((r) => `<@${r.id}> — **${r.fromRoleName}** → **${r.toRoleName}**\n${r.reason}`).join('\n\n') : 'No demotions recorded.'; + await render(interaction, new EmbedBuilder().setTitle('Staff Demotions').setDescription(description)); + }, + }, + { + name: 'staff_notes', + async execute(interaction) { + const data = await getStaffData(interaction.guildId); + const records = Object.entries(data.members).flatMap(([id, p]) => (p.notes || []).map((record) => ({ id, ...record }))).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))).slice(0, 10); + const description = records.length ? records.map((r) => `<@${r.id}> — ${r.note}`).join('\n') : 'No staff notes.'; + await render(interaction, new EmbedBuilder().setTitle('Staff Notes').setDescription(description)); + }, + }, +]; diff --git a/src/services/staffService.js b/src/services/staffService.js new file mode 100644 index 0000000000..f5a82df930 --- /dev/null +++ b/src/services/staffService.js @@ -0,0 +1,152 @@ +import { getFromDb, setInDb } from '../utils/database.js'; + +const key = (guildId) => `guild:${guildId}:staff`; + +const DEFAULTS = { + config: { + promotionChannelId: null, + demotionChannelId: null, + warningChannelId: null, + notesChannelId: null, + managerRoleId: null, + warningsBeforeReview: 3, + }, + members: {}, +}; + +function normalize(data) { + const value = data && typeof data === 'object' ? data : {}; + return { + config: { ...DEFAULTS.config, ...(value.config || {}) }, + members: value.members && typeof value.members === 'object' ? value.members : {}, + }; +} + +export async function getStaffData(guildId) { + return normalize(await getFromDb(key(guildId), DEFAULTS)); +} + +export async function saveStaffData(guildId, data) { + const normalized = normalize(data); + await setInDb(key(guildId), normalized); + return normalized; +} + +export async function getStaffProfile(guildId, userId, defaults = {}) { + const data = await getStaffData(guildId); + const existing = data.members[userId] || {}; + return { + userId, + joinedAt: existing.joinedAt || defaults.joinedAt || new Date().toISOString(), + warnings: Array.isArray(existing.warnings) ? existing.warnings : [], + promotions: Array.isArray(existing.promotions) ? existing.promotions : [], + demotions: Array.isArray(existing.demotions) ? existing.demotions : [], + notes: Array.isArray(existing.notes) ? existing.notes : [], + activity: existing.activity && typeof existing.activity === 'object' ? existing.activity : {}, + ...existing, + }; +} + +export async function ensureStaffMember(guildId, userId, defaults = {}) { + const data = await getStaffData(guildId); + const profile = await getStaffProfile(guildId, userId, defaults); + data.members[userId] = profile; + await saveStaffData(guildId, data); + return profile; +} + +export async function updateStaffConfig(guildId, patch) { + const data = await getStaffData(guildId); + data.config = { ...data.config, ...patch }; + return saveStaffData(guildId, data); +} + +export async function addStaffWarning(guildId, userId, issuerId, reason) { + const data = await getStaffData(guildId); + const profile = await getStaffProfile(guildId, userId); + const warning = { + id: `sw_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + reason: String(reason).trim(), + issuerId, + createdAt: new Date().toISOString(), + }; + profile.warnings.push(warning); + data.members[userId] = profile; + await saveStaffData(guildId, data); + return warning; +} + +export async function removeStaffWarning(guildId, userId, warningId) { + const data = await getStaffData(guildId); + const profile = await getStaffProfile(guildId, userId); + const before = profile.warnings.length; + profile.warnings = profile.warnings.filter((warning) => warning.id !== warningId); + data.members[userId] = profile; + await saveStaffData(guildId, data); + return before !== profile.warnings.length; +} + +export async function addPromotion(guildId, userId, record) { + const data = await getStaffData(guildId); + const profile = await getStaffProfile(guildId, userId); + profile.promotions.push({ + id: `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + ...record, + createdAt: new Date().toISOString(), + }); + data.members[userId] = profile; + await saveStaffData(guildId, data); + return profile.promotions.at(-1); +} + +export async function addDemotion(guildId, userId, record) { + const data = await getStaffData(guildId); + const profile = await getStaffProfile(guildId, userId); + profile.demotions.push({ + id: `sd_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + ...record, + createdAt: new Date().toISOString(), + }); + data.members[userId] = profile; + await saveStaffData(guildId, data); + return profile.demotions.at(-1); +} + +export async function addStaffNote(guildId, userId, authorId, note) { + const data = await getStaffData(guildId); + const profile = await getStaffProfile(guildId, userId); + profile.notes.push({ + id: `sn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + note: String(note).trim(), + authorId, + createdAt: new Date().toISOString(), + }); + data.members[userId] = profile; + await saveStaffData(guildId, data); + return profile.notes.at(-1); +} + +export async function incrementStaffActivity(guildId, userId, type, amount = 1) { + const data = await getStaffData(guildId); + const profile = await getStaffProfile(guildId, userId); + profile.activity = profile.activity || {}; + profile.activity[type] = Math.max(0, Number(profile.activity[type] || 0) + Number(amount || 0)); + profile.activity.lastActiveAt = new Date().toISOString(); + data.members[userId] = profile; + await saveStaffData(guildId, data); + return profile; +} + +export function calculateActivityScore(profile) { + const activity = profile?.activity || {}; + const moderation = Number(activity.moderationActions || 0); + const tickets = Number(activity.ticketsHandled || 0); + const events = Number(activity.eventsManaged || 0); + const commands = Number(activity.commands || 0); + const score = Math.min(100, moderation * 0.4 + tickets * 0.35 + events * 5 + commands * 0.05); + return Math.round(score); +} + +export function countWarnings(profile) { + return Array.isArray(profile?.warnings) ? profile.warnings.length : 0; +} From fe5bc0f271d83d2308750d752da344cc033b0b48 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 03:53:23 +0300 Subject: [PATCH 186/219] feat(staff): add staff leaderboard service --- src/services/staffService.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/services/staffService.js b/src/services/staffService.js index f5a82df930..44cf0a3e88 100644 --- a/src/services/staffService.js +++ b/src/services/staffService.js @@ -150,3 +150,10 @@ export function calculateActivityScore(profile) { export function countWarnings(profile) { return Array.isArray(profile?.warnings) ? profile.warnings.length : 0; } + +export function getStaffLeaderboard(data, limit = 10) { + return Object.entries(data?.members || {}) + .map(([userId, profile]) => ({ userId, profile, score: calculateActivityScore(profile) })) + .sort((a, b) => b.score - a.score) + .slice(0, Math.max(1, Math.min(25, Number(limit) || 10))); +} From d201f1bba49ae78fc5743f1cb9594b5acb9342f4 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 03:53:39 +0300 Subject: [PATCH 187/219] feat(staff): track staff message activity --- src/events/staffActivity.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/events/staffActivity.js diff --git a/src/events/staffActivity.js b/src/events/staffActivity.js new file mode 100644 index 0000000000..56f986d1cb --- /dev/null +++ b/src/events/staffActivity.js @@ -0,0 +1,20 @@ +import { Events, PermissionFlagsBits } from 'discord.js'; +import { getStaffData, incrementStaffActivity } from '../services/staffService.js'; + +export default { + name: Events.MessageCreate, + async execute(message) { + if (!message.guild || message.author.bot) return; + + const member = message.member; + if (!member) return; + + const data = await getStaffData(message.guild.id); + const isManager = data.config.managerRoleId && member.roles.cache.has(data.config.managerRoleId); + const isGuildManager = member.permissions.has(PermissionFlagsBits.ManageGuild); + + if (!isManager && !isGuildManager) return; + + await incrementStaffActivity(message.guild.id, message.author.id, 'messages', 1); + }, +}; From ce088f2012c1959f500f39a1e7c24c019902e28c Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 03:53:52 +0300 Subject: [PATCH 188/219] feat(staff): include message activity in staff score --- src/services/staffService.js | 146 +++++------------------------------ 1 file changed, 19 insertions(+), 127 deletions(-) diff --git a/src/services/staffService.js b/src/services/staffService.js index 44cf0a3e88..4564c80743 100644 --- a/src/services/staffService.js +++ b/src/services/staffService.js @@ -3,139 +3,39 @@ import { getFromDb, setInDb } from '../utils/database.js'; const key = (guildId) => `guild:${guildId}:staff`; const DEFAULTS = { - config: { - promotionChannelId: null, - demotionChannelId: null, - warningChannelId: null, - notesChannelId: null, - managerRoleId: null, - warningsBeforeReview: 3, - }, + config: { promotionChannelId: null, demotionChannelId: null, warningChannelId: null, notesChannelId: null, managerRoleId: null, warningsBeforeReview: 3 }, members: {}, }; function normalize(data) { const value = data && typeof data === 'object' ? data : {}; - return { - config: { ...DEFAULTS.config, ...(value.config || {}) }, - members: value.members && typeof value.members === 'object' ? value.members : {}, - }; + return { config: { ...DEFAULTS.config, ...(value.config || {}) }, members: value.members && typeof value.members === 'object' ? value.members : {} }; } -export async function getStaffData(guildId) { - return normalize(await getFromDb(key(guildId), DEFAULTS)); -} - -export async function saveStaffData(guildId, data) { - const normalized = normalize(data); - await setInDb(key(guildId), normalized); - return normalized; -} +export async function getStaffData(guildId) { return normalize(await getFromDb(key(guildId), DEFAULTS)); } +export async function saveStaffData(guildId, data) { const normalized = normalize(data); await setInDb(key(guildId), normalized); return normalized; } export async function getStaffProfile(guildId, userId, defaults = {}) { const data = await getStaffData(guildId); const existing = data.members[userId] || {}; - return { - userId, - joinedAt: existing.joinedAt || defaults.joinedAt || new Date().toISOString(), - warnings: Array.isArray(existing.warnings) ? existing.warnings : [], - promotions: Array.isArray(existing.promotions) ? existing.promotions : [], - demotions: Array.isArray(existing.demotions) ? existing.demotions : [], - notes: Array.isArray(existing.notes) ? existing.notes : [], - activity: existing.activity && typeof existing.activity === 'object' ? existing.activity : {}, - ...existing, - }; + return { userId, joinedAt: existing.joinedAt || defaults.joinedAt || new Date().toISOString(), warnings: Array.isArray(existing.warnings) ? existing.warnings : [], promotions: Array.isArray(existing.promotions) ? existing.promotions : [], demotions: Array.isArray(existing.demotions) ? existing.demotions : [], notes: Array.isArray(existing.notes) ? existing.notes : [], activity: existing.activity && typeof existing.activity === 'object' ? existing.activity : {}, ...existing }; } -export async function ensureStaffMember(guildId, userId, defaults = {}) { - const data = await getStaffData(guildId); - const profile = await getStaffProfile(guildId, userId, defaults); - data.members[userId] = profile; - await saveStaffData(guildId, data); - return profile; -} - -export async function updateStaffConfig(guildId, patch) { - const data = await getStaffData(guildId); - data.config = { ...data.config, ...patch }; - return saveStaffData(guildId, data); -} +export async function ensureStaffMember(guildId, userId, defaults = {}) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId, defaults); data.members[userId] = profile; await saveStaffData(guildId, data); return profile; } +export async function updateStaffConfig(guildId, patch) { const data = await getStaffData(guildId); data.config = { ...data.config, ...patch }; return saveStaffData(guildId, data); } export async function addStaffWarning(guildId, userId, issuerId, reason) { - const data = await getStaffData(guildId); - const profile = await getStaffProfile(guildId, userId); - const warning = { - id: `sw_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - reason: String(reason).trim(), - issuerId, - createdAt: new Date().toISOString(), - }; - profile.warnings.push(warning); - data.members[userId] = profile; - await saveStaffData(guildId, data); - return warning; + const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); + const warning = { id: `sw_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, reason: String(reason).trim(), issuerId, createdAt: new Date().toISOString() }; + profile.warnings.push(warning); data.members[userId] = profile; await saveStaffData(guildId, data); return warning; } +export async function removeStaffWarning(guildId, userId, warningId) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); const before = profile.warnings.length; profile.warnings = profile.warnings.filter((warning) => warning.id !== warningId); data.members[userId] = profile; await saveStaffData(guildId, data); return before !== profile.warnings.length; } -export async function removeStaffWarning(guildId, userId, warningId) { - const data = await getStaffData(guildId); - const profile = await getStaffProfile(guildId, userId); - const before = profile.warnings.length; - profile.warnings = profile.warnings.filter((warning) => warning.id !== warningId); - data.members[userId] = profile; - await saveStaffData(guildId, data); - return before !== profile.warnings.length; -} +export async function addPromotion(guildId, userId, record) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); profile.promotions.push({ id: `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, ...record, createdAt: new Date().toISOString() }); data.members[userId] = profile; await saveStaffData(guildId, data); return profile.promotions.at(-1); } +export async function addDemotion(guildId, userId, record) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); profile.demotions.push({ id: `sd_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, ...record, createdAt: new Date().toISOString() }); data.members[userId] = profile; await saveStaffData(guildId, data); return profile.demotions.at(-1); } +export async function addStaffNote(guildId, userId, authorId, note) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); profile.notes.push({ id: `sn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, note: String(note).trim(), authorId, createdAt: new Date().toISOString() }); data.members[userId] = profile; await saveStaffData(guildId, data); return profile.notes.at(-1); } -export async function addPromotion(guildId, userId, record) { - const data = await getStaffData(guildId); - const profile = await getStaffProfile(guildId, userId); - profile.promotions.push({ - id: `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - ...record, - createdAt: new Date().toISOString(), - }); - data.members[userId] = profile; - await saveStaffData(guildId, data); - return profile.promotions.at(-1); -} - -export async function addDemotion(guildId, userId, record) { - const data = await getStaffData(guildId); - const profile = await getStaffProfile(guildId, userId); - profile.demotions.push({ - id: `sd_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - ...record, - createdAt: new Date().toISOString(), - }); - data.members[userId] = profile; - await saveStaffData(guildId, data); - return profile.demotions.at(-1); -} - -export async function addStaffNote(guildId, userId, authorId, note) { - const data = await getStaffData(guildId); - const profile = await getStaffProfile(guildId, userId); - profile.notes.push({ - id: `sn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - note: String(note).trim(), - authorId, - createdAt: new Date().toISOString(), - }); - data.members[userId] = profile; - await saveStaffData(guildId, data); - return profile.notes.at(-1); -} - -export async function incrementStaffActivity(guildId, userId, type, amount = 1) { - const data = await getStaffData(guildId); - const profile = await getStaffProfile(guildId, userId); - profile.activity = profile.activity || {}; - profile.activity[type] = Math.max(0, Number(profile.activity[type] || 0) + Number(amount || 0)); - profile.activity.lastActiveAt = new Date().toISOString(); - data.members[userId] = profile; - await saveStaffData(guildId, data); - return profile; -} +export async function incrementStaffActivity(guildId, userId, type, amount = 1) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); profile.activity = profile.activity || {}; profile.activity[type] = Math.max(0, Number(profile.activity[type] || 0) + Number(amount || 0)); profile.activity.lastActiveAt = new Date().toISOString(); data.members[userId] = profile; await saveStaffData(guildId, data); return profile; } export function calculateActivityScore(profile) { const activity = profile?.activity || {}; @@ -143,17 +43,9 @@ export function calculateActivityScore(profile) { const tickets = Number(activity.ticketsHandled || 0); const events = Number(activity.eventsManaged || 0); const commands = Number(activity.commands || 0); - const score = Math.min(100, moderation * 0.4 + tickets * 0.35 + events * 5 + commands * 0.05); - return Math.round(score); + const messages = Number(activity.messages || 0); + return Math.round(Math.min(100, moderation * 0.4 + tickets * 0.35 + events * 5 + commands * 0.05 + messages * 0.01)); } -export function countWarnings(profile) { - return Array.isArray(profile?.warnings) ? profile.warnings.length : 0; -} - -export function getStaffLeaderboard(data, limit = 10) { - return Object.entries(data?.members || {}) - .map(([userId, profile]) => ({ userId, profile, score: calculateActivityScore(profile) })) - .sort((a, b) => b.score - a.score) - .slice(0, Math.max(1, Math.min(25, Number(limit) || 10))); -} +export function countWarnings(profile) { return Array.isArray(profile?.warnings) ? profile.warnings.length : 0; } +export function getStaffLeaderboard(data, limit = 10) { return Object.entries(data?.members || {}).map(([userId, profile]) => ({ userId, profile, score: calculateActivityScore(profile) })).sort((a, b) => b.score - a.score).slice(0, Math.max(1, Math.min(25, Number(limit) || 10))); } From bf1f7678d4c63ccb770af1d6a778435ae2b613f3 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:10:18 +0300 Subject: [PATCH 189/219] Add ticket log integration to staff tracking --- src/services/staffService.js | 37 +++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/services/staffService.js b/src/services/staffService.js index 4564c80743..ccfe67e014 100644 --- a/src/services/staffService.js +++ b/src/services/staffService.js @@ -5,11 +5,16 @@ const key = (guildId) => `guild:${guildId}:staff`; const DEFAULTS = { config: { promotionChannelId: null, demotionChannelId: null, warningChannelId: null, notesChannelId: null, managerRoleId: null, warningsBeforeReview: 3 }, members: {}, + ticketLogMessages: {}, }; function normalize(data) { const value = data && typeof data === 'object' ? data : {}; - return { config: { ...DEFAULTS.config, ...(value.config || {}) }, members: value.members && typeof value.members === 'object' ? value.members : {} }; + return { + config: { ...DEFAULTS.config, ...(value.config || {}) }, + members: value.members && typeof value.members === 'object' ? value.members : {}, + ticketLogMessages: value.ticketLogMessages && typeof value.ticketLogMessages === 'object' ? value.ticketLogMessages : {}, + }; } export async function getStaffData(guildId) { return normalize(await getFromDb(key(guildId), DEFAULTS)); } @@ -37,6 +42,36 @@ export async function addStaffNote(guildId, userId, authorId, note) { const data export async function incrementStaffActivity(guildId, userId, type, amount = 1) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); profile.activity = profile.activity || {}; profile.activity[type] = Math.max(0, Number(profile.activity[type] || 0) + Number(amount || 0)); profile.activity.lastActiveAt = new Date().toISOString(); data.members[userId] = profile; await saveStaffData(guildId, data); return profile; } +export async function recordTicketLog(guildId, { messageId, staffId, ticketId, ticketType = null, closedBy = null, closedAt = null }) { + if (!guildId || !messageId || !staffId || !ticketId) return { recorded: false, reason: 'missing_data' }; + const data = await getStaffData(guildId); + if (data.ticketLogMessages[messageId]) return { recorded: false, reason: 'duplicate' }; + + const profile = await getStaffProfile(guildId, staffId); + profile.activity = profile.activity || {}; + profile.activity.ticketsHandled = Math.max(0, Number(profile.activity.ticketsHandled || 0) + 1); + profile.activity.lastActiveAt = new Date().toISOString(); + + data.members[staffId] = profile; + data.ticketLogMessages[messageId] = { + ticketId: String(ticketId), + staffId: String(staffId), + ticketType, + closedBy, + closedAt: closedAt || new Date().toISOString(), + recordedAt: new Date().toISOString(), + }; + + // Keep the deduplication map bounded so it cannot grow forever. + const entries = Object.entries(data.ticketLogMessages); + if (entries.length > 1000) { + for (const [oldMessageId] of entries.slice(0, entries.length - 1000)) delete data.ticketLogMessages[oldMessageId]; + } + + await saveStaffData(guildId, data); + return { recorded: true, profile }; +} + export function calculateActivityScore(profile) { const activity = profile?.activity || {}; const moderation = Number(activity.moderationActions || 0); From a76075745c3e82f2e03dabb6ec3c87625b1b839f Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:10:32 +0300 Subject: [PATCH 190/219] Read ticket bot closed logs for staff ticket counts --- src/events/messageCreate.js | 69 ++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js index 97c6c061f3..9a63f32a9f 100644 --- a/src/events/messageCreate.js +++ b/src/events/messageCreate.js @@ -13,6 +13,7 @@ import { enforceAbuseProtection, formatCooldownDuration } from '../utils/abusePr import { createEmbed } from '../utils/embeds.js'; import { isCommandEnabled } from '../services/commandAccessService.js'; import { processAutoMod } from '../services/security/securityService.js'; +import { recordTicketLog } from '../services/staffService.js'; import { getCountingGameConfig, saveCountingGameConfig, @@ -27,7 +28,15 @@ export default { name: Events.MessageCreate, async execute(message, client) { try { - if (message.author.bot || !message.guild) return; + if (!message.guild) return; + + // Ticket bot integration: read only the ticket bot's closed-ticket embed. + // This runs before the normal bot-message early return so messages from the + // separate ticket bot can update Staff > Tickets Handled. + if (message.author.bot) { + await handleTicketClosedLog(message); + return; + } const autoModTriggered = await processAutoMod(message, client); if (autoModTriggered) return; @@ -45,6 +54,64 @@ export default { } }; +async function handleTicketClosedLog(message) { + try { + if (!message.embeds?.length) return; + + const embed = message.embeds.find((item) => item.title?.trim() === 'تم إغلاق تذكرة'); + if (!embed) return; + + const fields = Array.isArray(embed.fields) ? embed.fields : []; + const getField = (...names) => { + const field = fields.find((item) => names.includes(String(item.name || '').trim())); + return field?.value?.trim() || null; + }; + + const channelName = getField('اسم القناة'); + const claimedBy = getField('مستلم التذكرة'); + const closedBy = getField('تم الإغلاق بواسطة'); + if (!channelName || !claimedBy) return; + + // "لم يتم استلامها" means there is no staff member to credit. + if (/لم\s*يتم\s*استلامها/i.test(claimedBy)) return; + + // Discord mentions are the safest source of the staff ID. Support both + // user mentions and nick mentions, with a conservative fallback for IDs. + const mentionMatch = claimedBy.match(/<@!?([0-9]{15,25})>/); + const idMatch = claimedBy.match(/\b([0-9]{15,25})\b/); + const staffId = mentionMatch?.[1] || idMatch?.[1]; + if (!staffId) { + logger.warn(`Ticket log found but claimed staff ID could not be parsed: ${claimedBy}`, { + guildId: message.guild.id, + messageId: message.id, + }); + return; + } + + const ticketType = channelName.split('-')[0] || null; + const result = await recordTicketLog(message.guild.id, { + messageId: message.id, + staffId, + ticketId: channelName, + ticketType, + closedBy, + closedAt: embed.timestamp || message.createdAt?.toISOString() || null, + }); + + if (result.recorded) { + logger.info(`Ticket handled recorded for staff ${staffId}: ${channelName}`, { + event: 'staff.ticket_handled', + guildId: message.guild.id, + staffId, + ticketId: channelName, + sourceMessageId: message.id, + }); + } + } catch (error) { + logger.error('Error processing ticket closed log:', error); + } +} + async function handlePrefixCommand(message, client) { try { const guildConfig = await getGuildConfig(client, message.guild.id); From babb7cd60306684b255f0e96a9f4331d39a8a6d0 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:28:07 +0300 Subject: [PATCH 191/219] Add optional staff profile reset to promote/demote --- src/services/staffService.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/services/staffService.js b/src/services/staffService.js index ccfe67e014..317f575adf 100644 --- a/src/services/staffService.js +++ b/src/services/staffService.js @@ -40,6 +40,21 @@ export async function addPromotion(guildId, userId, record) { const data = await export async function addDemotion(guildId, userId, record) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); profile.demotions.push({ id: `sd_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, ...record, createdAt: new Date().toISOString() }); data.members[userId] = profile; await saveStaffData(guildId, data); return profile.demotions.at(-1); } export async function addStaffNote(guildId, userId, authorId, note) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); profile.notes.push({ id: `sn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, note: String(note).trim(), authorId, createdAt: new Date().toISOString() }); data.members[userId] = profile; await saveStaffData(guildId, data); return profile.notes.at(-1); } +export async function resetStaffProfile(guildId, userId) { + const data = await getStaffData(guildId); + const profile = await getStaffProfile(guildId, userId); + + // Reset current profile statistics/data only. + // Promotions and demotions are permanent history and must never be removed by this reset. + profile.warnings = []; + profile.notes = []; + profile.activity = {}; + + data.members[userId] = profile; + await saveStaffData(guildId, data); + return profile; +} + export async function incrementStaffActivity(guildId, userId, type, amount = 1) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); profile.activity = profile.activity || {}; profile.activity[type] = Math.max(0, Number(profile.activity[type] || 0) + Number(amount || 0)); profile.activity.lastActiveAt = new Date().toISOString(); data.members[userId] = profile; await saveStaffData(guildId, data); return profile; } export async function recordTicketLog(guildId, { messageId, staffId, ticketId, ticketType = null, closedBy = null, closedAt = null }) { From 0e34f7154b03ed3732543d3470ce3cfc177955fe Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:28:26 +0300 Subject: [PATCH 192/219] Add reset_profile option to staff promote and demote --- src/commands/Community/staff.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/commands/Community/staff.js b/src/commands/Community/staff.js index c34b1b32d8..e78d0aa4c1 100644 --- a/src/commands/Community/staff.js +++ b/src/commands/Community/staff.js @@ -17,6 +17,7 @@ import { addPromotion, addDemotion, addStaffNote, + resetStaffProfile, } from '../../services/staffService.js'; import { withErrorHandling } from '../../utils/errorHandler.js'; @@ -69,14 +70,16 @@ export default { .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(true)) .addRoleOption((opt) => opt.setName('new_role').setDescription('Role to give').setRequired(true)) .addRoleOption((opt) => opt.setName('remove_role').setDescription('Role to remove').setRequired(true)) - .addStringOption((opt) => opt.setName('reason').setDescription('Reason').setRequired(true).setMaxLength(500))) + .addStringOption((opt) => opt.setName('reason').setDescription('Reason').setRequired(true).setMaxLength(500)) + .addBooleanOption((opt) => opt.setName('reset_profile').setDescription('Reset current profile data after the promotion').setRequired(true))) .addSubcommand((sub) => sub .setName('demote') .setDescription('Demote a staff member') .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(true)) .addRoleOption((opt) => opt.setName('new_role').setDescription('Role to give').setRequired(true)) .addRoleOption((opt) => opt.setName('remove_role').setDescription('Role to remove').setRequired(true)) - .addStringOption((opt) => opt.setName('reason').setDescription('Reason').setRequired(true).setMaxLength(500))) + .addStringOption((opt) => opt.setName('reason').setDescription('Reason').setRequired(true).setMaxLength(500)) + .addBooleanOption((opt) => opt.setName('reset_profile').setDescription('Reset current profile data after the demotion').setRequired(true))) .addSubcommand((sub) => sub .setName('note') .setDescription('Add a private staff note') @@ -157,17 +160,19 @@ export default { if (newRole.position >= interaction.guild.members.me.roles.highest.position || removeRole.position >= interaction.guild.members.me.roles.highest.position) return interaction.reply({ content: 'I cannot manage one of these roles because it is above my highest role.', ephemeral: true }); if (sub === 'promote' || sub === 'demote') { + const resetProfile = interaction.options.getBoolean('reset_profile', true); await target.roles.remove(removeRole).catch(() => null); await target.roles.add(newRole); const record = { fromRoleId: removeRole.id, fromRoleName: removeRole.name, toRoleId: newRole.id, toRoleName: newRole.name, reason, issuerId: interaction.user.id }; if (sub === 'promote') await addPromotion(interaction.guildId, user.id, record); else await addDemotion(interaction.guildId, user.id, record); + if (resetProfile) await resetStaffProfile(interaction.guildId, user.id); const channelId = sub === 'promote' ? data.config.promotionChannelId : data.config.demotionChannelId; if (channelId) { const channel = await interaction.guild.channels.fetch(channelId).catch(() => null); if (channel?.isTextBased()) await channel.send(`${sub === 'promote' ? '📈' : '📉'} **Staff ${sub === 'promote' ? 'Promotion' : 'Demotion'}**\n${user}\n**${removeRole.name}** → **${newRole.name}**\nReason: ${reason}\nBy: ${interaction.user}`); } - return interaction.reply({ content: `${user} has been ${sub === 'promote' ? 'promoted' : 'demoted'}: **${removeRole.name}** → **${newRole.name}**.`, ephemeral: true }); + return interaction.reply({ content: `${user} has been ${sub === 'promote' ? 'promoted' : 'demoted'}: **${removeRole.name}** → **${newRole.name}**.${resetProfile ? ' Profile data has been reset; promotion/demotion history was preserved.' : ''}`, ephemeral: true }); } if (sub === 'note') { From ea9f8a1f78843c1c3b8e137378b38b0e807c9da8 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:31:27 +0300 Subject: [PATCH 193/219] Show staff message count in profile --- src/commands/Community/staff.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/Community/staff.js b/src/commands/Community/staff.js index e78d0aa4c1..dbd2a927b1 100644 --- a/src/commands/Community/staff.js +++ b/src/commands/Community/staff.js @@ -8,7 +8,6 @@ import { SlashCommandBuilder, } from 'discord.js'; import { - calculateActivityScore, countWarnings, getStaffData, getStaffProfile, @@ -130,7 +129,7 @@ export default { .setTitle('Staff Profile') .setDescription(`**${user}**\n${member?.roles?.highest ? `Current Role: **${member.roles.highest.name}**` : ''}`) .addFields( - { name: 'Activity', value: `**${calculateActivityScore(profile)}%**`, inline: true }, + { name: 'Activity', value: `**${Number(profile.activity?.messages || 0).toLocaleString()} messages**`, inline: true }, { name: 'Warnings', value: `**${countWarnings(profile)}**`, inline: true }, { name: 'Moderation Actions', value: `**${profile.activity?.moderationActions || 0}**`, inline: true }, { name: 'Tickets Handled', value: `**${profile.activity?.ticketsHandled || 0}**`, inline: true }, From 8e47f053608e668dc26241ba93a8fedc4c1a7678 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:31:44 +0300 Subject: [PATCH 194/219] Track staff messages for staff profiles --- src/events/messageCreate.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js index 9a63f32a9f..3b90910ab0 100644 --- a/src/events/messageCreate.js +++ b/src/events/messageCreate.js @@ -13,7 +13,7 @@ import { enforceAbuseProtection, formatCooldownDuration } from '../utils/abusePr import { createEmbed } from '../utils/embeds.js'; import { isCommandEnabled } from '../services/commandAccessService.js'; import { processAutoMod } from '../services/security/securityService.js'; -import { recordTicketLog } from '../services/staffService.js'; +import { getStaffData, incrementStaffActivity, recordTicketLog } from '../services/staffService.js'; import { getCountingGameConfig, saveCountingGameConfig, @@ -23,6 +23,8 @@ import { const MESSAGE_XP_RATE_LIMIT_ATTEMPTS = 12; const MESSAGE_XP_RATE_LIMIT_WINDOW_MS = 10000; +const STAFF_CACHE_TTL_MS = 60 * 1000; +const staffCache = new Map(); export default { name: Events.MessageCreate, @@ -38,6 +40,10 @@ export default { return; } + // Count every message sent by a tracked staff member. Bot messages are + // excluded above, and the 60-second cache avoids a database read per message. + await trackStaffMessage(message); + const autoModTriggered = await processAutoMod(message, client); if (autoModTriggered) return; @@ -54,6 +60,27 @@ export default { } }; +async function trackStaffMessage(message) { + try { + const now = Date.now(); + let cached = staffCache.get(message.guild.id); + + if (!cached || now - cached.updatedAt >= STAFF_CACHE_TTL_MS) { + const data = await getStaffData(message.guild.id); + cached = { + updatedAt: now, + staffIds: new Set(Object.keys(data.members || {})), + }; + staffCache.set(message.guild.id, cached); + } + + if (!cached.staffIds.has(message.author.id)) return; + await incrementStaffActivity(message.guild.id, message.author.id, 'messages'); + } catch (error) { + logger.error('Error tracking staff message activity:', error); + } +} + async function handleTicketClosedLog(message) { try { if (!message.embeds?.length) return; From fd0c484e691d3863a1c0023ca87c4c56a13cc7c9 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:37:33 +0300 Subject: [PATCH 195/219] feat: add role add/remove commands --- src/commands/Community/role.js | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/commands/Community/role.js diff --git a/src/commands/Community/role.js b/src/commands/Community/role.js new file mode 100644 index 0000000000..4637d3b94c --- /dev/null +++ b/src/commands/Community/role.js @@ -0,0 +1,83 @@ +import { + PermissionFlagsBits, + SlashCommandBuilder, +} from 'discord.js'; +import { withErrorHandling } from '../../utils/errorHandler.js'; + +function canManageRole(interaction, role) { + const me = interaction.guild.members.me; + if (!me) return false; + return role.editable && role.position < me.roles.highest.position; +} + +export default { + data: new SlashCommandBuilder() + .setName('role') + .setDescription('Manage member roles') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles) + .addSubcommand((sub) => sub + .setName('add') + .setDescription('Give a role to a member') + .addUserOption((opt) => opt + .setName('user') + .setDescription('Member') + .setRequired(true)) + .addRoleOption((opt) => opt + .setName('role') + .setDescription('Role to give') + .setRequired(true))) + .addSubcommand((sub) => sub + .setName('remove') + .setDescription('Remove a role from a member') + .addUserOption((opt) => opt + .setName('user') + .setDescription('Member') + .setRequired(true)) + .addRoleOption((opt) => opt + .setName('role') + .setDescription('Role to remove') + .setRequired(true))), + category: 'Moderation', + execute: withErrorHandling(async (interaction) => { + if (!interaction.inGuild()) { + return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); + } + + const user = interaction.options.getUser('user', true); + const role = interaction.options.getRole('role', true); + const sub = interaction.options.getSubcommand(); + const member = await interaction.guild.members.fetch(user.id).catch(() => null); + + if (!member) { + return interaction.reply({ content: 'Member not found.', ephemeral: true }); + } + + if (role.managed) { + return interaction.reply({ content: 'This role is managed by Discord and cannot be manually assigned.', ephemeral: true }); + } + + if (!canManageRole(interaction, role)) { + return interaction.reply({ content: 'I cannot manage this role because it is above or equal to my highest role.', ephemeral: true }); + } + + if (sub === 'add') { + if (member.roles.cache.has(role.id)) { + return interaction.reply({ content: `${user} already has **${role.name}**.`, ephemeral: true }); + } + + await member.roles.add(role, `Role added by ${interaction.user.tag}`); + return interaction.reply({ + content: `Added **${role.name}** to ${user}.\nBy: ${interaction.user}`, + }); + } + + if (!member.roles.cache.has(role.id)) { + return interaction.reply({ content: `${user} does not have **${role.name}**.`, ephemeral: true }); + } + + await member.roles.remove(role, `Role removed by ${interaction.user.tag}`); + return interaction.reply({ + content: `Removed **${role.name}** from ${user}.\nBy: ${interaction.user}`, + }); + }), +}; From 93060b06ae61558e0c35bafae3c4baa1b3d288e8 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:42:45 +0300 Subject: [PATCH 196/219] feat: add exact-match auto reply command --- src/commands/Community/autoreply.js | 102 ++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/commands/Community/autoreply.js diff --git a/src/commands/Community/autoreply.js b/src/commands/Community/autoreply.js new file mode 100644 index 0000000000..d1970dd907 --- /dev/null +++ b/src/commands/Community/autoreply.js @@ -0,0 +1,102 @@ +import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; +import { getGuildConfig, updateGuildConfig } from '../../services/config/guildConfig.js'; +import { withErrorHandling } from '../../utils/errorHandler.js'; + +const MAX_RULES = 100; +const MAX_TRIGGER_LENGTH = 500; +const MAX_RESPONSE_LENGTH = 2000; + +function normalizeText(value) { + return String(value ?? '').trim(); +} + +export default { + data: new SlashCommandBuilder() + .setName('autoreply') + .setDescription('Manage exact-match automatic replies') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand((sub) => sub + .setName('add') + .setDescription('Add an automatic reply') + .addStringOption((opt) => opt + .setName('message') + .setDescription('The exact message that should trigger the reply') + .setRequired(true) + .setMaxLength(MAX_TRIGGER_LENGTH)) + .addStringOption((opt) => opt + .setName('reply') + .setDescription('What the bot should reply with') + .setRequired(true) + .setMaxLength(MAX_RESPONSE_LENGTH))) + .addSubcommand((sub) => sub + .setName('remove') + .setDescription('Remove an automatic reply') + .addStringOption((opt) => opt + .setName('message') + .setDescription('The exact message to remove') + .setRequired(true) + .setMaxLength(MAX_TRIGGER_LENGTH))) + .addSubcommand((sub) => sub + .setName('list') + .setDescription('List automatic replies')), + category: 'Community', + execute: withErrorHandling(async (interaction, guildConfig) => { + if (!interaction.inGuild()) { + return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); + } + + const config = guildConfig || await getGuildConfig(interaction.client, interaction.guildId); + const rules = Array.isArray(config.autoReplies) ? config.autoReplies : []; + const sub = interaction.options.getSubcommand(); + + if (sub === 'list') { + if (!rules.length) { + return interaction.reply({ content: 'No automatic replies are configured.', ephemeral: true }); + } + + const lines = rules.map((rule, index) => + `**${index + 1}.** \`${rule.trigger.replace(/`/g, '\\`')}\` → ${rule.response}` + ); + return interaction.reply({ content: `**Automatic Replies (${rules.length})**\n${lines.join('\n')}`, ephemeral: true }); + } + + const trigger = normalizeText(interaction.options.getString('message', true)); + if (!trigger) return interaction.reply({ content: 'The trigger message cannot be empty.', ephemeral: true }); + + if (sub === 'remove') { + const index = rules.findIndex((rule) => rule.trigger === trigger); + if (index === -1) { + return interaction.reply({ content: 'No automatic reply was found for that exact message.', ephemeral: true }); + } + + const updatedRules = rules.filter((_, ruleIndex) => ruleIndex !== index); + await updateGuildConfig(interaction.client, interaction.guildId, { autoReplies: updatedRules }); + return interaction.reply({ content: `Removed the automatic reply for: \`${trigger}\``, ephemeral: true }); + } + + const response = normalizeText(interaction.options.getString('reply', true)); + if (!response) return interaction.reply({ content: 'The reply cannot be empty.', ephemeral: true }); + + const existingIndex = rules.findIndex((rule) => rule.trigger === trigger); + const nextRule = { trigger, response }; + let updatedRules; + + if (existingIndex >= 0) { + updatedRules = [...rules]; + updatedRules[existingIndex] = nextRule; + } else { + if (rules.length >= MAX_RULES) { + return interaction.reply({ content: `You can have a maximum of ${MAX_RULES} automatic replies.`, ephemeral: true }); + } + updatedRules = [...rules, nextRule]; + } + + await updateGuildConfig(interaction.client, interaction.guildId, { autoReplies: updatedRules }); + return interaction.reply({ + content: existingIndex >= 0 + ? `Updated the automatic reply for: \`${trigger}\`` + : `Added an automatic reply for: \`${trigger}\``, + ephemeral: true, + }); + }), +}; From f5e49c15e0d9372e7e864e6079498d03264fc8c9 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:42:51 +0300 Subject: [PATCH 197/219] feat: add auto reply guild config defaults --- src/utils/constants.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utils/constants.js b/src/utils/constants.js index 18382d6225..e869832b1c 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -30,6 +30,7 @@ export const DEFAULT_GUILD_CONFIG = { adminRole: null, welcomeChannel: null, autoRole: null, + autoReplies: [], logging: { enabled: false, channels: { audit: null, applications: null, reports: null }, @@ -42,9 +43,9 @@ export const DEFAULT_GUILD_CONFIG = { }; export const INTERACTION_TIMEOUTS = { - EXPIRE: 15 * 60 * 1000, - DEFER_TIMEOUT: 3000, - REPLY_TIMEOUT: 3000 + EXPIRE: 15 * 60 * 1000, + DEFER_TIMEOUT: 3000, + REPLY_TIMEOUT: 3000 }; export const STORAGE_LIMITS = { @@ -94,4 +95,4 @@ export default { DEFAULTS, ERROR_DEFAULTS, TIME -}; \ No newline at end of file +}; From e445826ca16131b31f27ad6bfbeac9290c68419b Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:43:10 +0300 Subject: [PATCH 198/219] feat: implement exact-match auto replies --- src/events/messageCreate.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js index 3b90910ab0..7203628e61 100644 --- a/src/events/messageCreate.js +++ b/src/events/messageCreate.js @@ -44,6 +44,11 @@ export default { // excluded above, and the 60-second cache avoids a database read per message. await trackStaffMessage(message); + // Auto replies use exact, case-sensitive matching across every guild channel. + // Bot messages are already excluded above, so the bot can never trigger itself. + const autoReplied = await handleAutoReply(message, client); + if (autoReplied) return; + const autoModTriggered = await processAutoMod(message, client); if (autoModTriggered) return; @@ -60,6 +65,26 @@ export default { } }; +async function handleAutoReply(message, client) { + try { + const content = String(message.content ?? ''); + if (!content) return false; + + const config = await getGuildConfig(client, message.guild.id); + const rules = Array.isArray(config?.autoReplies) ? config.autoReplies : []; + if (!rules.length) return false; + + const rule = rules.find((item) => String(item?.trigger ?? '') === content); + if (!rule || !String(rule.response ?? '').trim()) return false; + + await message.channel.send({ content: String(rule.response).slice(0, 2000) }); + return true; + } catch (error) { + logger.error('Error handling auto reply:', error); + return false; + } +} + async function trackStaffMessage(message) { try { const now = Date.now(); @@ -99,11 +124,8 @@ async function handleTicketClosedLog(message) { const closedBy = getField('تم الإغلاق بواسطة'); if (!channelName || !claimedBy) return; - // "لم يتم استلامها" means there is no staff member to credit. if (/لم\s*يتم\s*استلامها/i.test(claimedBy)) return; - // Discord mentions are the safest source of the staff ID. Support both - // user mentions and nick mentions, with a conservative fallback for IDs. const mentionMatch = claimedBy.match(/<@!?([0-9]{15,25})>/); const idMatch = claimedBy.match(/\b([0-9]{15,25})\b/); const staffId = mentionMatch?.[1] || idMatch?.[1]; From dd0295d7c4f1a0bfad086337a836d01f025c9e40 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:47:30 +0300 Subject: [PATCH 199/219] Add auto reaction command --- src/commands/Community/autoreaction.js | 82 ++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/commands/Community/autoreaction.js diff --git a/src/commands/Community/autoreaction.js b/src/commands/Community/autoreaction.js new file mode 100644 index 0000000000..779e3ddece --- /dev/null +++ b/src/commands/Community/autoreaction.js @@ -0,0 +1,82 @@ +import { ChannelType, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; +import { getGuildConfig, updateGuildConfig } from '../../services/config/guildConfig.js'; +import { withErrorHandling } from '../../utils/errorHandler.js'; + +function normalizeEmoji(value) { + return String(value ?? '').trim(); +} + +export default { + data: new SlashCommandBuilder() + .setName('autoreaction') + .setDescription('Manage automatic reactions for a channel') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand((sub) => sub + .setName('setup') + .setDescription('Set the channel and reaction') + .addChannelOption((opt) => opt + .setName('channel') + .setDescription('Channel where every new message gets the reaction') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true)) + .addStringOption((opt) => opt + .setName('reaction') + .setDescription('Emoji or custom Discord emoji') + .setRequired(true) + .setMaxLength(100))) + .addSubcommand((sub) => sub + .setName('disable') + .setDescription('Disable automatic reactions')) + .addSubcommand((sub) => sub + .setName('status') + .setDescription('Show the current automatic reaction settings')), + category: 'Community', + execute: withErrorHandling(async (interaction, guildConfig) => { + if (!interaction.inGuild()) { + return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); + } + + const config = guildConfig || await getGuildConfig(interaction.client, interaction.guildId); + const current = config.autoReaction || { enabled: false, channelId: null, reaction: null }; + const sub = interaction.options.getSubcommand(); + + if (sub === 'disable') { + await updateGuildConfig(interaction.client, interaction.guildId, { + autoReaction: { ...current, enabled: false }, + }); + return interaction.reply({ content: 'Automatic reactions have been disabled.', ephemeral: true }); + } + + if (sub === 'status') { + if (!current.enabled || !current.channelId || !current.reaction) { + return interaction.reply({ content: 'Automatic reactions are currently disabled.', ephemeral: true }); + } + return interaction.reply({ + content: `**Auto Reaction**\nChannel: <#${current.channelId}>\nReaction: ${current.reaction}\nStatus: **Enabled**`, + ephemeral: true, + }); + } + + const channel = interaction.options.getChannel('channel', true); + const reaction = normalizeEmoji(interaction.options.getString('reaction', true)); + if (!reaction) return interaction.reply({ content: 'The reaction cannot be empty.', ephemeral: true }); + + const me = interaction.guild.members.me; + if (!me?.permissionsIn(channel).has('AddReactions')) { + return interaction.reply({ content: 'I need the Add Reactions permission in that channel.', ephemeral: true }); + } + + await updateGuildConfig(interaction.client, interaction.guildId, { + autoReaction: { + enabled: true, + channelId: channel.id, + reaction, + }, + }); + + return interaction.reply({ + content: `Auto Reaction enabled.\nChannel: <#${channel.id}>\nReaction: ${reaction}`, + ephemeral: true, + }); + }), +}; From 02d4e14d210953108a2e364d4933c900c0258b94 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:47:37 +0300 Subject: [PATCH 200/219] Add auto reaction guild config defaults --- src/utils/constants.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/constants.js b/src/utils/constants.js index e869832b1c..b0da0c1d45 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -31,6 +31,11 @@ export const DEFAULT_GUILD_CONFIG = { welcomeChannel: null, autoRole: null, autoReplies: [], + autoReaction: { + enabled: false, + channelId: null, + reaction: null, + }, logging: { enabled: false, channels: { audit: null, applications: null, reports: null }, From 6d59d1b11400f6ad8263beb0ec693e26e962d069 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:47:53 +0300 Subject: [PATCH 201/219] Add auto reaction message handler --- src/events/messageCreate.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js index 7203628e61..7a3fea57b5 100644 --- a/src/events/messageCreate.js +++ b/src/events/messageCreate.js @@ -44,11 +44,14 @@ export default { // excluded above, and the 60-second cache avoids a database read per message. await trackStaffMessage(message); - // Auto replies use exact, case-sensitive matching across every guild channel. - // Bot messages are already excluded above, so the bot can never trigger itself. + // Exact-match auto replies run across every guild channel. const autoReplied = await handleAutoReply(message, client); if (autoReplied) return; + // Auto reaction: only the configured channel is monitored. Bot messages + // are excluded above, so the bot can never react to its own reaction flow. + await handleAutoReaction(message, client); + const autoModTriggered = await processAutoMod(message, client); if (autoModTriggered) return; @@ -85,6 +88,26 @@ async function handleAutoReply(message, client) { } } +async function handleAutoReaction(message, client) { + try { + const config = await getGuildConfig(client, message.guild.id); + const autoReaction = config?.autoReaction; + if (!autoReaction?.enabled || !autoReaction.channelId || !autoReaction.reaction) return false; + if (message.channel.id !== autoReaction.channelId) return false; + + await message.react(autoReaction.reaction); + return true; + } catch (error) { + logger.error('Error handling auto reaction:', { + error: error.message, + guildId: message.guild?.id, + channelId: message.channel?.id, + messageId: message.id, + }); + return false; + } +} + async function trackStaffMessage(message) { try { const now = Date.now(); From 75051d1a675aeb8cc994dea60190b47dec14b8f7 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 04:48:06 +0300 Subject: [PATCH 202/219] Add auto reaction config defaults --- src/config/guild/guildConfigDefaults.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config/guild/guildConfigDefaults.js b/src/config/guild/guildConfigDefaults.js index ef60a90be6..82e0bb278f 100644 --- a/src/config/guild/guildConfigDefaults.js +++ b/src/config/guild/guildConfigDefaults.js @@ -12,4 +12,9 @@ export const GUILD_CONFIG_DEFAULTS = { dmOnClose: true, disabledCommands: {}, disabledCategories: {}, + autoReaction: { + enabled: false, + channelId: null, + reaction: null, + }, }; From a1b06bbec548482104e278d866b1b39e853b9400 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:24:01 +0300 Subject: [PATCH 203/219] Remove birthday system --- src/commands/Birthday/birthday.js | 97 ------------------------------- 1 file changed, 97 deletions(-) delete mode 100644 src/commands/Birthday/birthday.js diff --git a/src/commands/Birthday/birthday.js b/src/commands/Birthday/birthday.js deleted file mode 100644 index c1f8672d00..0000000000 --- a/src/commands/Birthday/birthday.js +++ /dev/null @@ -1,97 +0,0 @@ -import { SlashCommandBuilder, MessageFlags, ChannelType } from 'discord.js'; -import { createEmbed, successEmbed } from '../../utils/embeds.js'; -import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; - -import birthdaySet from './modules/birthday_set.js'; -import birthdayInfo from './modules/birthday_info.js'; -import birthdayList from './modules/birthday_list.js'; -import birthdayRemove from './modules/birthday_remove.js'; -import nextBirthdays from './modules/next_birthdays.js'; -import birthdaySetchannel from './modules/birthday_setchannel.js'; - -import { InteractionHelper } from '../../utils/interactionHelper.js'; -export default { - data: new SlashCommandBuilder() - .setName('birthday') - .setDescription('Birthday system commands') - .addSubcommand(subcommand => - subcommand - .setName('set') - .setDescription('Set your birthday') - .addIntegerOption(option => - option - .setName('month') - .setDescription('Birth month (1-12)') - .setRequired(true) - .setMinValue(1) - .setMaxValue(12) - ) - .addIntegerOption(option => - option - .setName('day') - .setDescription('Birth day (1-31)') - .setRequired(true) - .setMinValue(1) - .setMaxValue(31) - ) - ) - .addSubcommand(subcommand => - subcommand - .setName('info') - .setDescription('View birthday information') - .addUserOption(option => - option - .setName('user') - .setDescription('User to check birthday for') - .setRequired(false) - ) - ) - .addSubcommand(subcommand => - subcommand - .setName('list') - .setDescription('List all birthdays in the server') - ) - .addSubcommand(subcommand => - subcommand - .setName('remove') - .setDescription('Remove your birthday') - ) - .addSubcommand(subcommand => - subcommand - .setName('next') - .setDescription('Show upcoming birthdays') - ) - .addSubcommand(subcommand => - subcommand - .setName('setchannel') - .setDescription('Set or disable the channel for birthday announcements. (Manage Server required)') - .addChannelOption(option => - option - .setName('channel') - .setDescription('The text channel for announcements. Leave empty to disable.') - .addChannelTypes(ChannelType.GuildText) - .setRequired(false) - ) - ), - - async execute(interaction, config, client) { - const subcommand = interaction.options.getSubcommand(); - - switch (subcommand) { - case 'set': - return await birthdaySet.execute(interaction, config, client); - case 'info': - return await birthdayInfo.execute(interaction, config, client); - case 'list': - return await birthdayList.execute(interaction, config, client); - case 'remove': - return await birthdayRemove.execute(interaction, config, client); - case 'next': - return await nextBirthdays.execute(interaction, config, client); - case 'setchannel': - return await birthdaySetchannel.execute(interaction, config, client); - default: - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unknown subcommand' }); - } - } -}; \ No newline at end of file From e86a7df3a8c2c61cc9a8b4bc165e9b6a9f0df305 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:24:05 +0300 Subject: [PATCH 204/219] Remove birthday system --- .../Birthday/modules/birthday_info.js | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/commands/Birthday/modules/birthday_info.js diff --git a/src/commands/Birthday/modules/birthday_info.js b/src/commands/Birthday/modules/birthday_info.js deleted file mode 100644 index 07b8c20020..0000000000 --- a/src/commands/Birthday/modules/birthday_info.js +++ /dev/null @@ -1,44 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { getUserBirthday } from '../../../services/birthdayService.js'; -import { logger } from '../../../utils/logger.js'; - -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -export default { - async execute(interaction, config, client) { - await InteractionHelper.safeDefer(interaction); - - const targetUser = interaction.options.getUser("user") || interaction.user; - const userId = targetUser.id; - const guildId = interaction.guildId; - - const birthdayData = await getUserBirthday(client, guildId, userId); - - if (!birthdayData) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthday Found') - .setDescription(targetUser.id === interaction.user.id - ? "You haven't set your birthday yet. Use `/birthday set` to add it!" - : `${targetUser.username} hasn't set their birthday yet.`); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Birthday Information') - .setDescription(`**Date:** ${birthdayData.monthName} ${birthdayData.day}\n**User:** ${targetUser.toString()}`); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - - logger.info('Birthday info retrieved successfully', { - userId: interaction.user.id, - targetUserId: targetUser.id, - guildId, - commandName: 'birthday_info' - }); - } -}; \ No newline at end of file From a88c520cc79947ca4efeac34c45f565de46c0f98 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:24:08 +0300 Subject: [PATCH 205/219] Remove birthday system --- .../Birthday/modules/birthday_list.js | 76 ------------------- 1 file changed, 76 deletions(-) delete mode 100644 src/commands/Birthday/modules/birthday_list.js diff --git a/src/commands/Birthday/modules/birthday_list.js b/src/commands/Birthday/modules/birthday_list.js deleted file mode 100644 index ce7ee064da..0000000000 --- a/src/commands/Birthday/modules/birthday_list.js +++ /dev/null @@ -1,76 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { getAllBirthdays } from '../../../services/birthdayService.js'; -import { deleteBirthday } from '../../../utils/database.js'; -import { logger } from '../../../utils/logger.js'; - -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -export default { - async execute(interaction, config, client) { - await InteractionHelper.safeDefer(interaction); - - const guildId = interaction.guildId; - - const sortedBirthdays = await getAllBirthdays(client, guildId); - - if (sortedBirthdays.length === 0) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthdays') - .setDescription('No birthdays have been set in this server yet.'); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - const userIds = sortedBirthdays.map(b => b.userId); - const fetchedMembers = await interaction.guild.members.fetch({ user: userIds }).catch(() => null); - - let birthdayList = ''; - let displayIndex = 0; - const staleUserIds = []; - - for (const birthday of sortedBirthdays) { - if (fetchedMembers && !fetchedMembers.has(birthday.userId)) { - staleUserIds.push(birthday.userId); - continue; - } - displayIndex++; - birthdayList += `${displayIndex}. <@${birthday.userId}> - ${birthday.monthName} ${birthday.day}\n`; - } - - if (fetchedMembers && staleUserIds.length > 0) { - for (const userId of staleUserIds) { - deleteBirthday(client, guildId, userId).catch(() => null); - } - } - - if (displayIndex === 0) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthdays') - .setDescription('No birthdays have been set by current server members.'); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - birthdayList = `**${displayIndex} birthday${displayIndex !== 1 ? 's' : ''} in ${interaction.guild.name}**\n\n` + birthdayList; - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Server Birthdays') - .setDescription(`${birthdayList}\n\nTotal: ${displayIndex} birthday${displayIndex !== 1 ? 's' : ''}`); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - - logger.info('Birthday list retrieved successfully', { - userId: interaction.user.id, - guildId, - birthdayCount: displayIndex, - staleRemoved: staleUserIds.length, - commandName: 'birthday_list' - }); - } -}; \ No newline at end of file From 7ecaa230e1abc8dba620ea04d941e446da0146e4 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:24:13 +0300 Subject: [PATCH 206/219] Remove birthday system --- .../Birthday/modules/birthday_remove.js | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 src/commands/Birthday/modules/birthday_remove.js diff --git a/src/commands/Birthday/modules/birthday_remove.js b/src/commands/Birthday/modules/birthday_remove.js deleted file mode 100644 index 2494c42768..0000000000 --- a/src/commands/Birthday/modules/birthday_remove.js +++ /dev/null @@ -1,33 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { deleteBirthday } from '../../../services/birthdayService.js'; - -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -export default { - async execute(interaction, config, client) { - await InteractionHelper.safeDefer(interaction); - - const userId = interaction.user.id; - const guildId = interaction.guildId; - - const result = await deleteBirthday(client, guildId, userId); - - if (result.status === 'not_found') { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthday Found') - .setDescription('You don\'t have a birthday set to remove.'); - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - return; - } - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Birthday Removed') - .setDescription('Your birthday has been successfully removed from the server.'); - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } -}; \ No newline at end of file From cdca4e527c33af36c7a1f79d01b903f71a74ed02 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:24:18 +0300 Subject: [PATCH 207/219] Remove birthday system --- src/commands/Birthday/modules/birthday_set.js | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/commands/Birthday/modules/birthday_set.js diff --git a/src/commands/Birthday/modules/birthday_set.js b/src/commands/Birthday/modules/birthday_set.js deleted file mode 100644 index bf968b065d..0000000000 --- a/src/commands/Birthday/modules/birthday_set.js +++ /dev/null @@ -1,25 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { setBirthday } from '../../../services/birthdayService.js'; - -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -export default { - async execute(interaction, config, client) { - await InteractionHelper.safeDefer(interaction); - - const month = interaction.options.getInteger("month"); - const day = interaction.options.getInteger("day"); - const userId = interaction.user.id; - const guildId = interaction.guildId; - - const result = await setBirthday(client, guildId, userId, month, day); - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Birthday Set!') - .setDescription(`Your birthday has been set to **${result.data.monthName} ${result.data.day}**!`); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } -}; \ No newline at end of file From 020d5f75c94d8d578a20e66bf3c828e49ae20405 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:24:21 +0300 Subject: [PATCH 208/219] Remove birthday system --- .../Birthday/modules/birthday_setchannel.js | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 src/commands/Birthday/modules/birthday_setchannel.js diff --git a/src/commands/Birthday/modules/birthday_setchannel.js b/src/commands/Birthday/modules/birthday_setchannel.js deleted file mode 100644 index 09083bfa9f..0000000000 --- a/src/commands/Birthday/modules/birthday_setchannel.js +++ /dev/null @@ -1,59 +0,0 @@ -import { PermissionsBitField, EmbedBuilder, MessageFlags } from 'discord.js'; -import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js'; -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -import { logger } from '../../../utils/logger.js'; - -export default { - async execute(interaction, config, client) { - if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('Permission Denied') - .setDescription('You need **Manage Server** permissions to configure the birthday channel.'); - return InteractionHelper.safeReply(interaction, { - embeds: [embed], - flags: MessageFlags.Ephemeral, - }); - } - - try { - const channel = interaction.options.getChannel('channel'); - const guildId = interaction.guildId; - const guildConfig = await getGuildConfig(client, guildId); - - if (channel) { - guildConfig.birthdayChannelId = channel.id; - await setGuildConfig(client, guildId, guildConfig); - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Birthday Announcements Enabled') - .setDescription(`Birthday announcements will now be posted in ${channel}.`); - return InteractionHelper.safeReply(interaction, { - embeds: [embed], - flags: MessageFlags.Ephemeral, - }); - } else { - guildConfig.birthdayChannelId = null; - await setGuildConfig(client, guildId, guildConfig); - const embed = new EmbedBuilder() - .setColor(0xFFFF00) - .setTitle('Birthday Announcements Disabled') - .setDescription('No channel provided — birthday announcements have been disabled.'); - return InteractionHelper.safeReply(interaction, { - embeds: [embed], - flags: MessageFlags.Ephemeral, - }); - } - } catch (error) { - logger.error('birthday_setchannel error:', error); - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('⚠️ Configuration Error') - .setDescription('Could not save the birthday channel configuration.'); - return InteractionHelper.safeReply(interaction, { - embeds: [embed], - flags: MessageFlags.Ephemeral, - }); - } - }, -}; \ No newline at end of file From 8f072a2e655ed72b760da5093091e6c14c0d5bfd Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:24:26 +0300 Subject: [PATCH 209/219] Remove birthday system --- .../Birthday/modules/next_birthdays.js | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 src/commands/Birthday/modules/next_birthdays.js diff --git a/src/commands/Birthday/modules/next_birthdays.js b/src/commands/Birthday/modules/next_birthdays.js deleted file mode 100644 index 0695347c16..0000000000 --- a/src/commands/Birthday/modules/next_birthdays.js +++ /dev/null @@ -1,91 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { getUpcomingBirthdays } from '../../../services/birthdayService.js'; -import { deleteBirthday } from '../../../utils/database.js'; -import { logger } from '../../../utils/logger.js'; - -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -export default { - async execute(interaction, config, client) { - await InteractionHelper.safeDefer(interaction); - - const next5 = await getUpcomingBirthdays(client, interaction.guildId, 5); - - if (next5.length === 0) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthdays Found') - .setDescription('No birthdays have been set up in this server yet. Use `/birthday set` to add birthdays!'); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - let displayIndex = 0; - for (const birthday of next5) { - const member = await interaction.guild.members.fetch(birthday.userId).catch(() => null); - if (!member) { - deleteBirthday(client, interaction.guildId, birthday.userId).catch(() => null); - continue; - } - displayIndex++; - - let timeUntil = ''; - if (birthday.daysUntil === 0) { - timeUntil = '🎉 **Today!**'; - } else if (birthday.daysUntil === 1) { - timeUntil = '📅 **Tomorrow!**'; - } else { - timeUntil = `In ${birthday.daysUntil} day${birthday.daysUntil > 1 ? 's' : ''}`; - } - } - - if (displayIndex === 0) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Upcoming Birthdays') - .setDescription('No upcoming birthdays found for current server members.'); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - let birthdayList = `🎂 **Next 5 Upcoming Birthdays**\n\nHere are the next 5 birthdays in ${interaction.guild.name}:\n\n`; - displayIndex = 0; - for (const birthday of next5) { - const member = await interaction.guild.members.fetch(birthday.userId).catch(() => null); - if (!member) { - continue; - } - displayIndex++; - - let timeUntil = ''; - if (birthday.daysUntil === 0) { - timeUntil = '🎉 **Today!**'; - } else if (birthday.daysUntil === 1) { - timeUntil = '📅 **Tomorrow!**'; - } else { - timeUntil = `In ${birthday.daysUntil} day${birthday.daysUntil > 1 ? 's' : ''}`; - } - - birthdayList += `${displayIndex}. **${member.displayName}**\n<@${birthday.userId}>\n📅 **Date:** ${birthday.monthName} ${birthday.day}\n⏰ **Time:** ${timeUntil}\n\n`; - } - - birthdayList += `Use /birthday set to add your birthday!`; - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Next 5 Upcoming Birthdays') - .setDescription(birthdayList); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - - logger.info('Next birthdays retrieved successfully', { - userId: interaction.user.id, - guildId: interaction.guildId, - upcomingCount: displayIndex, - commandName: 'next_birthdays' - }); - } -}; \ No newline at end of file From ac35b956ab9f34d44530868c2df149f3d9fcae92 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:29:18 +0300 Subject: [PATCH 210/219] Delete src/services/birthdayService.js --- src/services/birthdayService.js | 359 -------------------------------- 1 file changed, 359 deletions(-) delete mode 100644 src/services/birthdayService.js diff --git a/src/services/birthdayService.js b/src/services/birthdayService.js deleted file mode 100644 index 002665d4eb..0000000000 --- a/src/services/birthdayService.js +++ /dev/null @@ -1,359 +0,0 @@ -// birthdayService.js - -import { getGuildConfig } from './config/guildConfig.js'; -import { getGuildBirthdays, setBirthday as dbSetBirthday, deleteBirthday as dbDeleteBirthday, getMonthName, getBirthdayTrackingKey } from '../utils/database.js'; -import { logger } from '../utils/logger.js'; -import { TitanBotError, ErrorTypes } from '../utils/errorHandler.js'; - -export function validateBirthday(month, day) { - - if (typeof month !== 'number' || typeof day !== 'number') { - return { - isValid: false, - error: 'Month and day must be numbers' - }; - } - - if (month < 1 || month > 12) { - return { - isValid: false, - error: 'Month must be between 1 and 12' - }; - } - - if (day < 1 || day > 31) { - return { - isValid: false, - error: 'Day must be between 1 and 31' - }; - } - - const currentYear = new Date().getFullYear(); - const date = new Date(currentYear, month - 1, day); - - if (isNaN(date.getTime()) || date.getMonth() !== month - 1 || date.getDate() !== day) { - return { - isValid: false, - error: 'Invalid date. Please check the month and day combination (e.g., February 29th only exists in leap years)' - }; - } - - return { isValid: true }; -} - -export async function setBirthday(client, guildId, userId, month, day) { - try { - - const validation = validateBirthday(month, day); - if (!validation.isValid) { - logger.warn('Birthday validation failed', { - userId, - guildId, - month, - day, - error: validation.error - }); - - throw new TitanBotError( - validation.error, - ErrorTypes.VALIDATION, - validation.error, - { month, day, userId, guildId } - ); - } - - const success = await dbSetBirthday(client, guildId, userId, month, day); - - if (!success) { - throw new TitanBotError( - 'Failed to save birthday to database', - ErrorTypes.DATABASE, - 'Failed to set your birthday. Please try again later.', - { userId, guildId, month, day } - ); - } - - logger.info('Birthday set successfully', { - userId, - guildId, - month, - day, - monthName: getMonthName(month) - }); - - return { - data: { - month, - day, - monthName: getMonthName(month) - } - }; - } catch (error) { - logger.error('Error in setBirthday service', { - error: error.message, - stack: error.stack, - userId, - guildId, - month, - day - }); - - throw error; - } -} - -export async function getUserBirthday(client, guildId, userId) { - try { - const birthdays = await getGuildBirthdays(client, guildId); - const birthdayData = birthdays[userId]; - - if (!birthdayData) { - return null; - } - - return { - month: birthdayData.month, - day: birthdayData.day, - monthName: getMonthName(birthdayData.month) - }; - } catch (error) { - logger.error('Error in getUserBirthday service', { - error: error.message, - userId, - guildId - }); - throw error; - } -} - -export async function getAllBirthdays(client, guildId) { - try { - const birthdays = await getGuildBirthdays(client, guildId); - - if (!birthdays || Object.keys(birthdays).length === 0) { - return []; - } - - const sortedBirthdays = Object.entries(birthdays) - .map(([userId, data]) => ({ - userId, - month: data.month, - day: data.day, - monthName: getMonthName(data.month) - })) - .sort((a, b) => { - if (a.month !== b.month) return a.month - b.month; - return a.day - b.day; - }); - - return sortedBirthdays; - } catch (error) { - logger.error('Error in getAllBirthdays service', { - error: error.message, - guildId - }); - throw error; - } -} - -export async function deleteBirthday(client, guildId, userId) { - try { - - const birthday = await getUserBirthday(client, guildId, userId); - - if (!birthday) { - return { - status: 'not_found', - }; - } - - const success = await dbDeleteBirthday(client, guildId, userId); - - if (!success) { - throw new TitanBotError( - 'Failed to delete birthday from database', - ErrorTypes.DATABASE, - 'Failed to remove your birthday. Please try again.', - { userId, guildId } - ); - } - - logger.info('Birthday removed successfully', { - userId, - guildId - }); - - return { - status: 'removed', - }; - } catch (error) { - logger.error('Error in deleteBirthday service', { - error: error.message, - userId, - guildId - }); - throw error; - } -} - -export async function getUpcomingBirthdays(client, guildId, limit = 5) { - try { - const birthdays = await getGuildBirthdays(client, guildId); - - if (!birthdays || Object.keys(birthdays).length === 0) { - return []; - } - - const today = new Date(); - const currentYear = today.getFullYear(); - - const upcomingBirthdays = []; - - for (const [userId, userData] of Object.entries(birthdays)) { - let nextBirthday = new Date(currentYear, userData.month - 1, userData.day); - - if (nextBirthday < today) { - nextBirthday = new Date(currentYear + 1, userData.month - 1, userData.day); - } - - const daysUntil = Math.ceil((nextBirthday - today) / (1000 * 60 * 60 * 24)); - - upcomingBirthdays.push({ - userId, - month: userData.month, - day: userData.day, - monthName: getMonthName(userData.month), - date: nextBirthday, - daysUntil - }); - } - - upcomingBirthdays.sort((a, b) => a.daysUntil - b.daysUntil); - - return upcomingBirthdays.slice(0, limit); - } catch (error) { - logger.error('Error in getUpcomingBirthdays service', { - error: error.message, - guildId, - limit - }); - throw error; - } -} - -export async function getTodaysBirthdays(client, guildId) { - try { - const birthdays = await getGuildBirthdays(client, guildId); - const today = new Date(); - const currentMonth = today.getUTCMonth() + 1; - const currentDay = today.getUTCDate(); - - const todaysBirthdays = []; - - for (const [userId, userData] of Object.entries(birthdays)) { - if (userData.month === currentMonth && userData.day === currentDay) { - todaysBirthdays.push({ - userId, - month: userData.month, - day: userData.day, - monthName: getMonthName(userData.month) - }); - } - } - - return todaysBirthdays; - } catch (error) { - logger.error('Error in getTodaysBirthdays service', { - error: error.message, - guildId - }); - throw error; - } -} - -export async function checkBirthdays(client) { - const today = new Date(); - const currentMonth = today.getUTCMonth() + 1; - const currentDay = today.getUTCDate(); - - if (process.env.NODE_ENV !== 'production') { - logger.debug(`🎂 Running daily birthday check for UTC: ${currentMonth}/${currentDay}.`); - } - - for (const [guildId, guild] of client.guilds.cache) { - try { - const config = await getGuildConfig(client, guildId); - const { birthdayChannelId, birthdayRoleId } = config; - - // A channel is required for announcements; the birthday role is optional. - if (!birthdayChannelId) { - if (process.env.NODE_ENV !== 'production') { - logger.debug(`Skipping birthday check for ${guild.name}: Missing channel config.`); - } - continue; - } - - const channel = await guild.channels.fetch(birthdayChannelId).catch(() => null); - if (!channel) continue; - - const trackingKey = getBirthdayTrackingKey(guildId); - const trackingData = (await client.db.get(trackingKey)) || {}; - const updatedTrackingData = { ...trackingData }; - - for (const userId of Object.keys(trackingData)) { - try { - if (birthdayRoleId) { - const member = await guild.members.fetch(userId).catch(() => null); - if (member && member.roles.cache.has(birthdayRoleId)) { - await member.roles.remove(birthdayRoleId, "Birthday role expired"); - } - } - delete updatedTrackingData[userId]; - } catch (error) { - logger.error(`Error removing birthday role from ${userId}:`, error); - } - } - - if (Object.keys(updatedTrackingData).length !== Object.keys(trackingData).length) { - await client.db.set(trackingKey, updatedTrackingData); - } - - // Use the canonical birthday storage (guild::birthdays) that set/remove commands write to. - const birthdays = (await getGuildBirthdays(client, guildId)) || {}; - const birthdayMembers = []; - for (const [userId, userData] of Object.entries(birthdays)) { - if (userData.month === currentMonth && userData.day === currentDay) { - const member = await guild.members.fetch(userId).catch(() => null); - if (member) { - birthdayMembers.push(member); - if (birthdayRoleId) { - try { - await member.roles.add(birthdayRoleId, "Happy Birthday! 🎉"); - updatedTrackingData[userId] = true; - } catch (error) { - logger.error(`Error adding birthday role to ${member.user.tag}:`, error); - } - } - } - } - } - - if (birthdayMembers.length > 0) { - await client.db.set(trackingKey, updatedTrackingData); - const mentionList = birthdayMembers.map(m => m.toString()).join(', '); - - await channel.send({ - embeds: [{ - title: '🎉 Happy Birthday! 🎂', - description: `A very happy birthday to ${mentionList}! Wishing you an amazing day! 🎈`, - color: 0xff69b4, - footer: { text: 'Birthday Bot' }, - timestamp: new Date() - }] - }); - } - } catch (error) { - logger.error(`Error processing birthdays for guild ${guildId}:`, error); - } - } -} \ No newline at end of file From 8f4c0753a096850b9e1accabaf4162ddf5fdd47f Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:37:21 +0300 Subject: [PATCH 211/219] Update app.js --- src/app.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app.js b/src/app.js index 693cd228ea..227a997a54 100644 --- a/src/app.js +++ b/src/app.js @@ -9,7 +9,6 @@ import { initializeDatabase } from './utils/database.js'; import { getGuildConfig } from './services/config/guildConfig.js'; import { getServerCounters, saveServerCounters, updateCounter } from './services/serverstatsService.js'; import { logger, startupLog, shutdownLog } from './utils/logger.js'; -import { checkBirthdays } from './services/birthdayService.js'; import { checkGiveaways } from './services/giveawayService.js'; import { loadCommands, registerCommands as registerSlashCommands } from './handlers/loaders/commandLoader.js'; import { runSafeTask, handleTaskError, ErrorCodes } from './utils/errorHandler.js'; @@ -248,7 +247,6 @@ class TitanBot extends Client { } setupCronJobs() { - cron.schedule('0 6 * * *', runSafeTask('birthday_check', () => checkBirthdays(this))); cron.schedule('* * * * *', runSafeTask('giveaway_check', () => checkGiveaways(this))); cron.schedule('*/15 * * * *', runSafeTask('counter_update', () => this.updateAllCounters())); } @@ -428,4 +426,4 @@ try { process.exit(1); } -export default TitanBot; \ No newline at end of file +export default TitanBot; From 776b90bcefacbe6705c51a577d71d0b37ff0747d Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:47:59 +0300 Subject: [PATCH 212/219] Add persistent staff shift service --- src/services/staffShiftService.js | 121 ++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/services/staffShiftService.js diff --git a/src/services/staffShiftService.js b/src/services/staffShiftService.js new file mode 100644 index 0000000000..6cb6d51fe6 --- /dev/null +++ b/src/services/staffShiftService.js @@ -0,0 +1,121 @@ +import { getFromDb, setInDb } from '../utils/database.js'; + +const key = (guildId) => `guild:${guildId}:staff-shifts`; + +const DEFAULTS = { + config: { minimumHours: 0 }, + active: {}, + history: {}, +}; + +function normalize(data) { + const value = data && typeof data === 'object' ? data : {}; + return { + config: { ...DEFAULTS.config, ...(value.config || {}) }, + active: value.active && typeof value.active === 'object' ? value.active : {}, + history: value.history && typeof value.history === 'object' ? value.history : {}, + }; +} + +async function getData(guildId) { + return normalize(await getFromDb(key(guildId), DEFAULTS)); +} + +async function saveData(guildId, data) { + const normalized = normalize(data); + await setInDb(key(guildId), normalized); + return normalized; +} + +export async function getShiftData(guildId) { + return getData(guildId); +} + +export async function updateShiftConfig(guildId, patch) { + const data = await getData(guildId); + data.config = { ...data.config, ...patch }; + return saveData(guildId, data); +} + +export async function getActiveShift(guildId, userId) { + const data = await getData(guildId); + return data.active[userId] || null; +} + +export async function startShift(guildId, userId) { + const data = await getData(guildId); + if (data.active[userId]) return { started: false, reason: 'already_active', shift: data.active[userId] }; + + const shift = { + id: `shift_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + userId: String(userId), + startedAt: new Date().toISOString(), + }; + data.active[userId] = shift; + await saveData(guildId, data); + return { started: true, shift }; +} + +export async function stopShift(guildId, userId) { + const data = await getData(guildId); + const active = data.active[userId]; + if (!active) return { stopped: false, reason: 'not_active' }; + + const endedAt = new Date(); + const startedAt = new Date(active.startedAt); + const durationMs = Math.max(0, endedAt.getTime() - startedAt.getTime()); + const durationHours = durationMs / 3600000; + const record = { + ...active, + endedAt: endedAt.toISOString(), + durationMs, + durationHours: Number(durationHours.toFixed(4)), + }; + + delete data.active[userId]; + data.history[userId] = Array.isArray(data.history[userId]) ? data.history[userId] : []; + data.history[userId].push(record); + if (data.history[userId].length > 500) data.history[userId] = data.history[userId].slice(-500); + await saveData(guildId, data); + return { stopped: true, shift: record }; +} + +export async function getShiftHistory(guildId, userId, limit = 10) { + const data = await getData(guildId); + const history = Array.isArray(data.history[userId]) ? data.history[userId] : []; + return history.slice(-Math.max(1, Math.min(50, Number(limit) || 10))).reverse(); +} + +export function getShiftStats(data, userId) { + const history = Array.isArray(data?.history?.[userId]) ? data.history[userId] : []; + const active = data?.active?.[userId] || null; + const completedMs = history.reduce((sum, shift) => sum + Number(shift.durationMs || 0), 0); + const activeMs = active ? Math.max(0, Date.now() - new Date(active.startedAt).getTime()) : 0; + const totalMs = completedMs + activeMs; + return { + shiftCount: history.length, + completedHours: completedMs / 3600000, + activeHours: activeMs / 3600000, + totalHours: totalMs / 3600000, + active, + lastShift: history.at(-1) || null, + }; +} + +export function getShiftLeaderboard(data, limit = 10) { + const userIds = new Set([ + ...Object.keys(data?.history || {}), + ...Object.keys(data?.active || {}), + ]); + return [...userIds] + .map((userId) => ({ userId, stats: getShiftStats(data, userId) })) + .sort((a, b) => b.stats.totalHours - a.stats.totalHours) + .slice(0, Math.max(1, Math.min(25, Number(limit) || 10))); +} + +export function formatDuration(ms) { + const totalMinutes = Math.max(0, Math.floor(Number(ms || 0) / 60000)); + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + return `${hours}h ${minutes}m`; +} From 3b56444bfa5d766632f108276439864b5094e524 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:48:12 +0300 Subject: [PATCH 213/219] Add staff shift commands --- src/commands/Community/shift.js | 128 ++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/commands/Community/shift.js diff --git a/src/commands/Community/shift.js b/src/commands/Community/shift.js new file mode 100644 index 0000000000..482be90ea3 --- /dev/null +++ b/src/commands/Community/shift.js @@ -0,0 +1,128 @@ +import { + EmbedBuilder, + PermissionFlagsBits, + SlashCommandBuilder, +} from 'discord.js'; +import { getStaffData } from '../../services/staffService.js'; +import { + formatDuration, + getShiftData, + getShiftHistory, + getShiftLeaderboard, + getShiftStats, + startShift, + stopShift, + updateShiftConfig, +} from '../../services/staffShiftService.js'; +import { withErrorHandling } from '../../utils/errorHandler.js'; + +function hasManagerAccess(interaction, staffData) { + return interaction.member.permissions.has(PermissionFlagsBits.ManageGuild) + || Boolean(staffData.config.managerRoleId && interaction.member.roles.cache.has(staffData.config.managerRoleId)); +} + +function formatHours(hours) { + return `${Number(hours || 0).toFixed(2)}h`; +} + +export default { + data: new SlashCommandBuilder() + .setName('shift') + .setDescription('Manage staff shifts') + .addSubcommand((sub) => sub + .setName('start') + .setDescription('Start your staff shift')) + .addSubcommand((sub) => sub + .setName('stop') + .setDescription('Stop your current staff shift')) + .addSubcommand((sub) => sub + .setName('status') + .setDescription('View a staff shift status') + .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(false))) + .addSubcommand((sub) => sub + .setName('history') + .setDescription('View staff shift history') + .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(false)) + .addIntegerOption((opt) => opt.setName('limit').setDescription('Number of shifts to show').setMinValue(1).setMaxValue(20))) + .addSubcommand((sub) => sub + .setName('leaderboard') + .setDescription('View the staff shift leaderboard')) + .addSubcommand((sub) => sub + .setName('setup') + .setDescription('Configure staff shift requirements') + .addNumberOption((opt) => opt.setName('minimum_hours').setDescription('Minimum required shift hours').setMinValue(0).setMaxValue(1000).setRequired(true))), + category: 'Community', + execute: withErrorHandling(async (interaction) => { + if (!interaction.inGuild()) return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); + + const sub = interaction.options.getSubcommand(); + const staffData = await getStaffData(interaction.guildId); + const shiftData = await getShiftData(interaction.guildId); + + if (sub === 'setup') { + if (!hasManagerAccess(interaction, staffData)) { + return interaction.reply({ content: 'You do not have permission to configure staff shifts.', ephemeral: true }); + } + const minimumHours = interaction.options.getNumber('minimum_hours', true); + const updated = await updateShiftConfig(interaction.guildId, { minimumHours }); + return interaction.reply({ content: `Staff shift settings saved. Minimum hours: **${Number(updated.config.minimumHours).toFixed(2)}h**.`, ephemeral: true }); + } + + const requestedUser = interaction.options.getUser('user'); + const targetUser = requestedUser || interaction.user; + const isSelf = targetUser.id === interaction.user.id; + const managerAccess = hasManagerAccess(interaction, staffData); + + if (!isSelf && !managerAccess) { + return interaction.reply({ content: 'You can only view your own shift information.', ephemeral: true }); + } + + if (sub === 'start') { + if (!staffData.members[targetUser.id]) { + return interaction.reply({ content: 'You are not registered in the staff system yet. Open your staff profile first.', ephemeral: true }); + } + const result = await startShift(interaction.guildId, targetUser.id); + if (!result.started) return interaction.reply({ content: `You already have an active shift since .`, ephemeral: true }); + return interaction.reply({ content: `Shift started at .`, ephemeral: true }); + } + + if (sub === 'stop') { + const result = await stopShift(interaction.guildId, targetUser.id); + if (!result.stopped) return interaction.reply({ content: 'You do not have an active shift.', ephemeral: true }); + const minimum = Number(shiftData.config.minimumHours || 0); + const duration = formatDuration(result.shift.durationMs); + const requirement = minimum > 0 ? `\nMinimum: **${minimum.toFixed(2)}h** — ${result.shift.durationHours >= minimum ? 'Met' : 'Not met'}` : ''; + return interaction.reply({ content: `Shift stopped. Duration: **${duration}**.${requirement}`, ephemeral: true }); + } + + if (sub === 'status') { + const stats = getShiftStats(shiftData, targetUser.id); + const embed = new EmbedBuilder() + .setTitle(`Shift Status — ${targetUser.username}`) + .setThumbnail(targetUser.displayAvatarURL()) + .addFields( + { name: 'Status', value: stats.active ? `🟢 Active\nStarted ` : '⚪ Offline', inline: true }, + { name: 'Total Hours', value: `**${formatHours(stats.totalHours)}**`, inline: true }, + { name: 'Completed Shifts', value: `**${stats.shiftCount}**`, inline: true }, + { name: 'Minimum Hours', value: `**${Number(shiftData.config.minimumHours || 0).toFixed(2)}h**`, inline: true }, + ); + return interaction.reply({ embeds: [embed] }); + } + + if (sub === 'history') { + const limit = interaction.options.getInteger('limit') || 10; + const history = await getShiftHistory(interaction.guildId, targetUser.id, limit); + if (!history.length) return interaction.reply({ content: `No completed shifts found for ${targetUser}.`, ephemeral: true }); + const lines = history.map((shift, index) => `${index + 1}. — **${formatDuration(shift.durationMs)}** — to `); + return interaction.reply({ embeds: [new EmbedBuilder().setTitle(`Shift History — ${targetUser.username}`).setDescription(lines.join('\n')).setFooter({ text: `Showing ${history.length} shift(s)` })] }); + } + + if (sub === 'leaderboard') { + if (!managerAccess) return interaction.reply({ content: 'You do not have permission to view the staff shift leaderboard.', ephemeral: true }); + const leaderboard = getShiftLeaderboard(shiftData, 10); + if (!leaderboard.length) return interaction.reply({ content: 'No staff shift data exists yet.', ephemeral: true }); + const lines = leaderboard.map((entry, index) => `${index + 1}. <@${entry.userId}> — **${formatHours(entry.stats.totalHours)}** — ${entry.stats.shiftCount} completed shift(s)${entry.stats.active ? ' 🟢' : ''}`); + return interaction.reply({ embeds: [new EmbedBuilder().setTitle('Staff Shift Leaderboard').setDescription(lines.join('\n')).addFields({ name: 'Minimum Hours', value: `**${Number(shiftData.config.minimumHours || 0).toFixed(2)}h**` })] }); + } + }), +}; From 0e52858b01ef6dd9c6fe4fd810a385371d4f84ad Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:48:45 +0300 Subject: [PATCH 214/219] Include shift hours in staff performance --- src/services/staffService.js | 38 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/services/staffService.js b/src/services/staffService.js index 317f575adf..0adbb8fdc3 100644 --- a/src/services/staffService.js +++ b/src/services/staffService.js @@ -43,13 +43,9 @@ export async function addStaffNote(guildId, userId, authorId, note) { const data export async function resetStaffProfile(guildId, userId) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); - - // Reset current profile statistics/data only. - // Promotions and demotions are permanent history and must never be removed by this reset. profile.warnings = []; profile.notes = []; profile.activity = {}; - data.members[userId] = profile; await saveStaffData(guildId, data); return profile; @@ -57,32 +53,31 @@ export async function resetStaffProfile(guildId, userId) { export async function incrementStaffActivity(guildId, userId, type, amount = 1) { const data = await getStaffData(guildId); const profile = await getStaffProfile(guildId, userId); profile.activity = profile.activity || {}; profile.activity[type] = Math.max(0, Number(profile.activity[type] || 0) + Number(amount || 0)); profile.activity.lastActiveAt = new Date().toISOString(); data.members[userId] = profile; await saveStaffData(guildId, data); return profile; } +export async function recordStaffShift(guildId, userId, durationHours) { + const data = await getStaffData(guildId); + const profile = await getStaffProfile(guildId, userId); + profile.activity = profile.activity || {}; + profile.activity.shiftHours = Math.max(0, Number(profile.activity.shiftHours || 0) + Number(durationHours || 0)); + profile.activity.shiftCount = Math.max(0, Number(profile.activity.shiftCount || 0) + 1); + profile.activity.lastShiftHours = Math.max(0, Number(durationHours || 0)); + profile.activity.lastActiveAt = new Date().toISOString(); + data.members[userId] = profile; + await saveStaffData(guildId, data); + return profile; +} + export async function recordTicketLog(guildId, { messageId, staffId, ticketId, ticketType = null, closedBy = null, closedAt = null }) { if (!guildId || !messageId || !staffId || !ticketId) return { recorded: false, reason: 'missing_data' }; const data = await getStaffData(guildId); if (data.ticketLogMessages[messageId]) return { recorded: false, reason: 'duplicate' }; - const profile = await getStaffProfile(guildId, staffId); profile.activity = profile.activity || {}; profile.activity.ticketsHandled = Math.max(0, Number(profile.activity.ticketsHandled || 0) + 1); profile.activity.lastActiveAt = new Date().toISOString(); - data.members[staffId] = profile; - data.ticketLogMessages[messageId] = { - ticketId: String(ticketId), - staffId: String(staffId), - ticketType, - closedBy, - closedAt: closedAt || new Date().toISOString(), - recordedAt: new Date().toISOString(), - }; - - // Keep the deduplication map bounded so it cannot grow forever. + data.ticketLogMessages[messageId] = { ticketId: String(ticketId), staffId: String(staffId), ticketType, closedBy, closedAt: closedAt || new Date().toISOString(), recordedAt: new Date().toISOString() }; const entries = Object.entries(data.ticketLogMessages); - if (entries.length > 1000) { - for (const [oldMessageId] of entries.slice(0, entries.length - 1000)) delete data.ticketLogMessages[oldMessageId]; - } - + if (entries.length > 1000) for (const [oldMessageId] of entries.slice(0, entries.length - 1000)) delete data.ticketLogMessages[oldMessageId]; await saveStaffData(guildId, data); return { recorded: true, profile }; } @@ -94,7 +89,8 @@ export function calculateActivityScore(profile) { const events = Number(activity.eventsManaged || 0); const commands = Number(activity.commands || 0); const messages = Number(activity.messages || 0); - return Math.round(Math.min(100, moderation * 0.4 + tickets * 0.35 + events * 5 + commands * 0.05 + messages * 0.01)); + const shiftHours = Number(activity.shiftHours || 0); + return Math.round(Math.min(100, moderation * 0.4 + tickets * 0.35 + events * 5 + commands * 0.05 + messages * 0.01 + shiftHours * 1.5)); } export function countWarnings(profile) { return Array.isArray(profile?.warnings) ? profile.warnings.length : 0; } From da8e0a8d3a2b8e7d9b6208fa0043dbe173b6a921 Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:49:02 +0300 Subject: [PATCH 215/219] Record completed shift stats in staff performance --- src/commands/Community/shift.js | 43 +++++++++------------------------ 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/src/commands/Community/shift.js b/src/commands/Community/shift.js index 482be90ea3..9344dc9d40 100644 --- a/src/commands/Community/shift.js +++ b/src/commands/Community/shift.js @@ -3,7 +3,7 @@ import { PermissionFlagsBits, SlashCommandBuilder, } from 'discord.js'; -import { getStaffData } from '../../services/staffService.js'; +import { getStaffData, recordStaffShift } from '../../services/staffService.js'; import { formatDuration, getShiftData, @@ -29,28 +29,12 @@ export default { data: new SlashCommandBuilder() .setName('shift') .setDescription('Manage staff shifts') - .addSubcommand((sub) => sub - .setName('start') - .setDescription('Start your staff shift')) - .addSubcommand((sub) => sub - .setName('stop') - .setDescription('Stop your current staff shift')) - .addSubcommand((sub) => sub - .setName('status') - .setDescription('View a staff shift status') - .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(false))) - .addSubcommand((sub) => sub - .setName('history') - .setDescription('View staff shift history') - .addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(false)) - .addIntegerOption((opt) => opt.setName('limit').setDescription('Number of shifts to show').setMinValue(1).setMaxValue(20))) - .addSubcommand((sub) => sub - .setName('leaderboard') - .setDescription('View the staff shift leaderboard')) - .addSubcommand((sub) => sub - .setName('setup') - .setDescription('Configure staff shift requirements') - .addNumberOption((opt) => opt.setName('minimum_hours').setDescription('Minimum required shift hours').setMinValue(0).setMaxValue(1000).setRequired(true))), + .addSubcommand((sub) => sub.setName('start').setDescription('Start your staff shift')) + .addSubcommand((sub) => sub.setName('stop').setDescription('Stop your current staff shift')) + .addSubcommand((sub) => sub.setName('status').setDescription('View a staff shift status').addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(false))) + .addSubcommand((sub) => sub.setName('history').setDescription('View staff shift history').addUserOption((opt) => opt.setName('user').setDescription('Staff member').setRequired(false)).addIntegerOption((opt) => opt.setName('limit').setDescription('Number of shifts to show').setMinValue(1).setMaxValue(20))) + .addSubcommand((sub) => sub.setName('leaderboard').setDescription('View the staff shift leaderboard')) + .addSubcommand((sub) => sub.setName('setup').setDescription('Configure staff shift requirements').addNumberOption((opt) => opt.setName('minimum_hours').setDescription('Minimum required shift hours').setMinValue(0).setMaxValue(1000).setRequired(true))), category: 'Community', execute: withErrorHandling(async (interaction) => { if (!interaction.inGuild()) return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); @@ -60,9 +44,7 @@ export default { const shiftData = await getShiftData(interaction.guildId); if (sub === 'setup') { - if (!hasManagerAccess(interaction, staffData)) { - return interaction.reply({ content: 'You do not have permission to configure staff shifts.', ephemeral: true }); - } + if (!hasManagerAccess(interaction, staffData)) return interaction.reply({ content: 'You do not have permission to configure staff shifts.', ephemeral: true }); const minimumHours = interaction.options.getNumber('minimum_hours', true); const updated = await updateShiftConfig(interaction.guildId, { minimumHours }); return interaction.reply({ content: `Staff shift settings saved. Minimum hours: **${Number(updated.config.minimumHours).toFixed(2)}h**.`, ephemeral: true }); @@ -73,14 +55,10 @@ export default { const isSelf = targetUser.id === interaction.user.id; const managerAccess = hasManagerAccess(interaction, staffData); - if (!isSelf && !managerAccess) { - return interaction.reply({ content: 'You can only view your own shift information.', ephemeral: true }); - } + if (!isSelf && !managerAccess) return interaction.reply({ content: 'You can only view your own shift information.', ephemeral: true }); if (sub === 'start') { - if (!staffData.members[targetUser.id]) { - return interaction.reply({ content: 'You are not registered in the staff system yet. Open your staff profile first.', ephemeral: true }); - } + if (!staffData.members[targetUser.id]) return interaction.reply({ content: 'You are not registered in the staff system yet. Open your staff profile first.', ephemeral: true }); const result = await startShift(interaction.guildId, targetUser.id); if (!result.started) return interaction.reply({ content: `You already have an active shift since .`, ephemeral: true }); return interaction.reply({ content: `Shift started at .`, ephemeral: true }); @@ -89,6 +67,7 @@ export default { if (sub === 'stop') { const result = await stopShift(interaction.guildId, targetUser.id); if (!result.stopped) return interaction.reply({ content: 'You do not have an active shift.', ephemeral: true }); + await recordStaffShift(interaction.guildId, targetUser.id, result.shift.durationHours); const minimum = Number(shiftData.config.minimumHours || 0); const duration = formatDuration(result.shift.durationMs); const requirement = minimum > 0 ? `\nMinimum: **${minimum.toFixed(2)}h** — ${result.shift.durationHours >= minimum ? 'Met' : 'Not met'}` : ''; From e12487c7d8c4bceb3ad1ce9b0bcff493d068293f Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 05:49:24 +0300 Subject: [PATCH 216/219] Add staff performance leaderboard and shift stats to profile --- src/commands/Community/staff.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/commands/Community/staff.js b/src/commands/Community/staff.js index dbd2a927b1..07c61bbb2a 100644 --- a/src/commands/Community/staff.js +++ b/src/commands/Community/staff.js @@ -8,8 +8,10 @@ import { SlashCommandBuilder, } from 'discord.js'; import { + calculateActivityScore, countWarnings, getStaffData, + getStaffLeaderboard, getStaffProfile, updateStaffConfig, addStaffWarning, @@ -54,6 +56,7 @@ export default { .setDescription('Manage server staff') .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) .addSubcommand((sub) => sub.setName('dashboard').setDescription('Open the staff dashboard')) + .addSubcommand((sub) => sub.setName('leaderboard').setDescription('View staff performance leaderboard')) .addSubcommand((sub) => sub .setName('profile') .setDescription('View a staff profile') @@ -98,8 +101,16 @@ export default { const sub = interaction.options.getSubcommand(); const data = await getStaffData(interaction.guildId); - if (sub === 'dashboard') { - return interaction.reply({ embeds: [dashboardEmbed(interaction.guild, data)], components: dashboardButtons() }); + if (sub === 'dashboard') return interaction.reply({ embeds: [dashboardEmbed(interaction.guild, data)], components: dashboardButtons() }); + + if (sub === 'leaderboard') { + const leaderboard = getStaffLeaderboard(data, 10); + if (!leaderboard.length) return interaction.reply({ content: 'No staff performance data exists yet.', ephemeral: true }); + const lines = leaderboard.map((entry, index) => { + const activity = entry.profile?.activity || {}; + return `${index + 1}. <@${entry.userId}> — **${entry.score}/100**\n 💬 ${Number(activity.messages || 0).toLocaleString()} messages • 🎫 ${Number(activity.ticketsHandled || 0)} tickets • ⏱️ ${Number(activity.shiftHours || 0).toFixed(2)}h shifts • ⚠️ ${countWarnings(entry.profile)} warnings`; + }); + return interaction.reply({ embeds: [new EmbedBuilder().setTitle('Staff Performance Leaderboard').setDescription(lines.join('\n\n')).setFooter({ text: 'Performance score combines staff activity and completed shift hours.' })] }); } if (sub === 'setup') { @@ -129,10 +140,13 @@ export default { .setTitle('Staff Profile') .setDescription(`**${user}**\n${member?.roles?.highest ? `Current Role: **${member.roles.highest.name}**` : ''}`) .addFields( + { name: 'Performance', value: `**${calculateActivityScore(profile)}/100**`, inline: true }, { name: 'Activity', value: `**${Number(profile.activity?.messages || 0).toLocaleString()} messages**`, inline: true }, { name: 'Warnings', value: `**${countWarnings(profile)}**`, inline: true }, { name: 'Moderation Actions', value: `**${profile.activity?.moderationActions || 0}**`, inline: true }, { name: 'Tickets Handled', value: `**${profile.activity?.ticketsHandled || 0}**`, inline: true }, + { name: 'Shift Hours', value: `**${Number(profile.activity?.shiftHours || 0).toFixed(2)}h**`, inline: true }, + { name: 'Shifts', value: `**${profile.activity?.shiftCount || 0}**`, inline: true }, { name: 'Promotions', value: `**${profile.promotions.length}**`, inline: true }, { name: 'Demotions', value: `**${profile.demotions.length}**`, inline: true }, ) @@ -141,7 +155,7 @@ export default { if (sub === 'warn') { const reason = interaction.options.getString('reason', true); - const warning = await addStaffWarning(interaction.guildId, user.id, interaction.user.id, reason); + await addStaffWarning(interaction.guildId, user.id, interaction.user.id, reason); const profile = await getStaffProfile(interaction.guildId, user.id); const channelId = data.config.warningChannelId; if (channelId) { From ab07396183ddd2b00fceaac6344f66bd9fc500cb Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 07:42:13 +0300 Subject: [PATCH 217/219] Add clan persistence service --- src/services/clanService.js | 108 ++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/services/clanService.js diff --git a/src/services/clanService.js b/src/services/clanService.js new file mode 100644 index 0000000000..d9d56d7f51 --- /dev/null +++ b/src/services/clanService.js @@ -0,0 +1,108 @@ +import { logger } from '../utils/logger.js'; + +const CLAN_KEY_PREFIX = 'clans:'; + +function key(guildId) { + return `${CLAN_KEY_PREFIX}${guildId}`; +} + +function normalizeClan(clan) { + return { + id: String(clan.id), + name: String(clan.name), + ownerId: String(clan.ownerId), + roleId: String(clan.roleId), + categoryId: String(clan.categoryId), + textChannelId: String(clan.textChannelId), + voiceChannelId: String(clan.voiceChannelId), + memberIds: Array.from(new Set((clan.memberIds || []).map(String))), + createdAt: clan.createdAt || new Date().toISOString(), + }; +} + +export async function getClans(client, guildId) { + const data = await client.db.get(key(guildId), { clans: [] }); + return Array.isArray(data?.clans) ? data.clans.map(normalizeClan) : []; +} + +async function saveClans(client, guildId, clans) { + await client.db.set(key(guildId), { clans }); + return clans; +} + +export async function getClan(client, guildId, clanId) { + const clans = await getClans(client, guildId); + return clans.find((clan) => clan.id === clanId) || null; +} + +export async function getClanForUser(client, guildId, userId) { + const clans = await getClans(client, guildId); + return clans.find((clan) => clan.ownerId === userId || clan.memberIds.includes(userId)) || null; +} + +export async function createClan(client, guildId, clan) { + const clans = await getClans(client, guildId); + if (clans.some((item) => item.name.toLowerCase() === clan.name.toLowerCase())) { + throw new Error('A clan with this name already exists.'); + } + if (clans.some((item) => item.ownerId === clan.ownerId)) { + throw new Error('This user already owns a clan.'); + } + + const normalized = normalizeClan(clan); + clans.push(normalized); + await saveClans(client, guildId, clans); + return normalized; +} + +export async function updateClan(client, guildId, clanId, updates) { + const clans = await getClans(client, guildId); + const index = clans.findIndex((clan) => clan.id === clanId); + if (index === -1) throw new Error('Clan not found.'); + + const updated = normalizeClan({ ...clans[index], ...updates }); + clans[index] = updated; + await saveClans(client, guildId, clans); + return updated; +} + +export async function deleteClan(client, guildId, clanId) { + const clans = await getClans(client, guildId); + const filtered = clans.filter((clan) => clan.id !== clanId); + if (filtered.length === clans.length) throw new Error('Clan not found.'); + await saveClans(client, guildId, filtered); + return true; +} + +export async function addClanMember(client, guildId, clanId, userId) { + const clan = await getClan(client, guildId, clanId); + if (!clan) throw new Error('Clan not found.'); + if (!clan.memberIds.includes(userId)) clan.memberIds.push(userId); + return updateClan(client, guildId, clanId, { memberIds: clan.memberIds }); +} + +export async function removeClanMember(client, guildId, clanId, userId) { + const clan = await getClan(client, guildId, clanId); + if (!clan) throw new Error('Clan not found.'); + return updateClan(client, guildId, clanId, { + memberIds: clan.memberIds.filter((id) => id !== userId), + }); +} + +export function makeClanId() { + return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; +} + +export function sanitizeClanName(name) { + return String(name || '') + .normalize('NFKC') + .replace(/[\x00-\x1F\x7F]/g, '') + .replace(/[@#:`]/g, '') + .trim() + .replace(/\s+/g, ' ') + .slice(0, 50); +} + +export function clanLoggerError(context, error) { + logger.error(`Clan system error (${context}):`, error); +} From 682455828ebc91ea11edc760a3bb15ba6a35a81e Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 07:42:32 +0300 Subject: [PATCH 218/219] Add clan management slash command --- src/commands/Community/clan.js | 296 +++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 src/commands/Community/clan.js diff --git a/src/commands/Community/clan.js b/src/commands/Community/clan.js new file mode 100644 index 0000000000..97c8c3d11d --- /dev/null +++ b/src/commands/Community/clan.js @@ -0,0 +1,296 @@ +import { + ChannelType, + EmbedBuilder, + PermissionFlagsBits, + SlashCommandBuilder, +} from 'discord.js'; +import { + addClanMember, + createClan, + deleteClan, + getClan, + getClanForUser, + getClans, + makeClanId, + removeClanMember, + sanitizeClanName, + updateClan, +} from '../../services/clanService.js'; + +const clanCommand = new SlashCommandBuilder() + .setName('clan') + .setDescription('Manage clans and your clan') + .setDMPermission(false) + .addSubcommandGroup((group) => group + .setName('admin') + .setDescription('Administrator clan controls') + .addSubcommand((sub) => sub + .setName('create') + .setDescription('Create a clan for a user') + .addUserOption((opt) => opt.setName('owner').setDescription('Clan owner').setRequired(true)) + .addStringOption((opt) => opt.setName('name').setDescription('Clan name').setRequired(true).setMaxLength(50))) + .addSubcommand((sub) => sub + .setName('delete') + .setDescription('Delete a clan and its channels') + .addStringOption((opt) => opt.setName('clan_id').setDescription('Clan ID').setRequired(true))) + .addSubcommand((sub) => sub + .setName('list') + .setDescription('List all clans')) + .addSubcommand((sub) => sub + .setName('info') + .setDescription('View a clan') + .addStringOption((opt) => opt.setName('clan_id').setDescription('Clan ID').setRequired(true)))) + .addSubcommand((sub) => sub + .setName('info') + .setDescription('View your clan')) + .addSubcommand((sub) => sub + .setName('members') + .setDescription('View your clan members')) + .addSubcommand((sub) => sub + .setName('add') + .setDescription('Add a member to your clan') + .addUserOption((opt) => opt.setName('user').setDescription('Member to add').setRequired(true))) + .addSubcommand((sub) => sub + .setName('remove') + .setDescription('Remove a member from your clan') + .addUserOption((opt) => opt.setName('user').setDescription('Member to remove').setRequired(true))) + .addSubcommand((sub) => sub + .setName('rename') + .setDescription('Rename your clan') + .addStringOption((opt) => opt.setName('name').setDescription('New clan name').setRequired(true).setMaxLength(50))); + +function isAdmin(interaction) { + return interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild); +} + +function clanEmbed(clan) { + return new EmbedBuilder() + .setTitle(`Clan • ${clan.name}`) + .addFields( + { name: 'Clan ID', value: `\`${clan.id}\``, inline: true }, + { name: 'Owner', value: `<@${clan.ownerId}>`, inline: true }, + { name: 'Members', value: `**${clan.memberIds.length + 1}**`, inline: true }, + { name: 'Role', value: `<@&${clan.roleId}>`, inline: true }, + { name: 'Text', value: `<#${clan.textChannelId}>`, inline: true }, + { name: 'Voice', value: `<#${clan.voiceChannelId}>`, inline: true }, + ) + .setTimestamp(); +} + +async function fetchClanOwnerMember(interaction, clan) { + return interaction.guild.members.fetch(clan.ownerId).catch(() => null); +} + +async function createClanResources(interaction, owner, name) { + const guild = interaction.guild; + const role = await guild.roles.create({ + name: name.slice(0, 100), + reason: `Clan created by ${interaction.user.tag}`, + }); + + let category; + let textChannel; + let voiceChannel; + + try { + const overwrites = [ + { id: guild.roles.everyone.id, deny: [PermissionFlagsBits.ViewChannel] }, + { + id: role.id, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.ReadMessageHistory, + PermissionFlagsBits.Connect, + PermissionFlagsBits.Speak, + ], + }, + { + id: guild.members.me.id, + allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels, PermissionFlagsBits.ManageMessages, PermissionFlagsBits.Connect, PermissionFlagsBits.Speak], + }, + ]; + + category = await guild.channels.create({ + name: `Clan • ${name}`.slice(0, 100), + type: ChannelType.GuildCategory, + permissionOverwrites: overwrites, + reason: `Clan category for ${name}`, + }); + + textChannel = await guild.channels.create({ + name: 'chat', + type: ChannelType.GuildText, + parent: category.id, + permissionOverwrites: overwrites, + reason: `Clan text channel for ${name}`, + }); + + voiceChannel = await guild.channels.create({ + name: 'Voice', + type: ChannelType.GuildVoice, + parent: category.id, + permissionOverwrites: overwrites, + reason: `Clan voice channel for ${name}`, + }); + + await owner.roles.add(role, `Clan owner: ${name}`); + + return { role, category, textChannel, voiceChannel }; + } catch (error) { + await voiceChannel?.delete().catch(() => {}); + await textChannel?.delete().catch(() => {}); + await category?.delete().catch(() => {}); + await role.delete().catch(() => {}); + throw error; + } +} + +export default { + data: clanCommand, + category: 'Community', + async execute(interaction, config, client) { + if (!interaction.inGuild()) { + return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); + } + + try { + const group = interaction.options.getSubcommandGroup(false); + const sub = interaction.options.getSubcommand(); + + if (group === 'admin') { + if (!isAdmin(interaction)) { + return interaction.reply({ content: 'You need **Manage Server** permission to use clan administration.', ephemeral: true }); + } + + if (sub === 'create') { + const owner = interaction.options.getMember('owner'); + const name = sanitizeClanName(interaction.options.getString('name', true)); + if (!owner || !name) return interaction.reply({ content: 'Invalid clan owner or name.', ephemeral: true }); + + const existing = await getClanForUser(client, interaction.guildId, owner.id); + if (existing) return interaction.reply({ content: `That user already belongs to **${existing.name}**.`, ephemeral: true }); + + const clans = await getClans(client, interaction.guildId); + if (clans.some((clan) => clan.name.toLowerCase() === name.toLowerCase())) { + return interaction.reply({ content: 'A clan with that name already exists.', ephemeral: true }); + } + + const resources = await createClanResources(interaction, owner, name); + const clan = await createClan(client, interaction.guildId, { + id: makeClanId(), + name, + ownerId: owner.id, + roleId: resources.role.id, + categoryId: resources.category.id, + textChannelId: resources.textChannel.id, + voiceChannelId: resources.voiceChannel.id, + memberIds: [], + }); + + await resources.textChannel.send(`Welcome to **${name}**!\nOwner: <@${owner.id}>\nUse \`/clan members\` and \`/clan add\` to manage your clan.`).catch(() => {}); + return interaction.reply({ embeds: [new EmbedBuilder().setTitle('Clan Created').setDescription(`**${name}** has been created for ${owner}.\n\n${resources.textChannel}\n${resources.voiceChannel}\nRole: ${resources.role}\nClan ID: \`${clan.id}\``).setTimestamp()] }); + } + + const clanId = interaction.options.getString('clan_id', true); + const clan = await getClan(client, interaction.guildId, clanId); + if (!clan) return interaction.reply({ content: 'Clan not found.', ephemeral: true }); + + if (sub === 'list') { + const clans = await getClans(client, interaction.guildId); + if (!clans.length) return interaction.reply({ content: 'No clans exist in this server.', ephemeral: true }); + return interaction.reply({ embeds: [new EmbedBuilder().setTitle('Server Clans').setDescription(clans.map((item, index) => `${index + 1}. **${item.name}** — <@${item.ownerId}> — \`${item.id}\``).join('\n')).setTimestamp()] }); + } + + if (sub === 'info') { + return interaction.reply({ embeds: [clanEmbed(clan)] }); + } + + if (sub === 'delete') { + const channelIds = [clan.voiceChannelId, clan.textChannelId, clan.categoryId]; + for (const channelId of channelIds) { + const channel = await guildChannel(interaction, channelId); + await channel?.delete(`Deleting clan ${clan.name}`).catch(() => {}); + } + const role = await interaction.guild.roles.fetch(clan.roleId).catch(() => null); + await role?.delete(`Deleting clan ${clan.name}`).catch(() => {}); + await deleteClan(client, interaction.guildId, clan.id); + return interaction.reply({ content: `Deleted clan **${clan.name}** and its role/channels.`, ephemeral: true }); + } + } + + const clan = await getClan(client, interaction.guildId, await resolveUserClanId(client, interaction.guildId, interaction.user.id)); + if (!clan) { + return interaction.reply({ content: 'You are not a member of a clan.', ephemeral: true }); + } + + if (sub === 'info') return interaction.reply({ embeds: [clanEmbed(clan)] }); + + if (sub === 'members') { + const ids = [clan.ownerId, ...clan.memberIds]; + const members = await Promise.all(ids.map(async (id) => { + const member = await interaction.guild.members.fetch(id).catch(() => null); + return member ? `${member} — ${id === clan.ownerId ? 'Owner' : 'Member'}` : `<@${id}>`; + })); + return interaction.reply({ embeds: [new EmbedBuilder().setTitle(`${clan.name} Members`).setDescription(members.join('\n') || 'No members.').setTimestamp()] }); + } + + if (clan.ownerId !== interaction.user.id) { + return interaction.reply({ content: 'Only the clan owner can manage clan members or rename the clan.', ephemeral: true }); + } + + if (sub === 'add') { + const user = interaction.options.getUser('user', true); + if (user.bot) return interaction.reply({ content: 'Bots cannot be added to clans.', ephemeral: true }); + const currentClan = await getClanForUser(client, interaction.guildId, user.id); + if (currentClan) return interaction.reply({ content: `That user already belongs to **${currentClan.name}**.`, ephemeral: true }); + await addClanMember(client, interaction.guildId, clan.id, user.id); + const role = await interaction.guild.roles.fetch(clan.roleId).catch(() => null); + if (role) await interaction.guild.members.addRole({ user: user.id, role: role.id, reason: `Added to clan ${clan.name}` }).catch(async () => { + const member = await interaction.guild.members.fetch(user.id).catch(() => null); + await member?.roles.add(role).catch(() => {}); + }); + return interaction.reply({ content: `${user} was added to **${clan.name}**.` }); + } + + if (sub === 'remove') { + const user = interaction.options.getUser('user', true); + if (user.id === clan.ownerId) return interaction.reply({ content: 'The clan owner cannot be removed.', ephemeral: true }); + if (!clan.memberIds.includes(user.id)) return interaction.reply({ content: 'That user is not in your clan.', ephemeral: true }); + await removeClanMember(client, interaction.guildId, clan.id, user.id); + const role = await interaction.guild.roles.fetch(clan.roleId).catch(() => null); + const member = await interaction.guild.members.fetch(user.id).catch(() => null); + await member?.roles.remove(role).catch(() => {}); + return interaction.reply({ content: `${user} was removed from **${clan.name}**.` }); + } + + if (sub === 'rename') { + const name = sanitizeClanName(interaction.options.getString('name', true)); + if (!name) return interaction.reply({ content: 'Invalid clan name.', ephemeral: true }); + const clans = await getClans(client, interaction.guildId); + if (clans.some((item) => item.id !== clan.id && item.name.toLowerCase() === name.toLowerCase())) { + return interaction.reply({ content: 'A clan with that name already exists.', ephemeral: true }); + } + await updateClan(client, interaction.guildId, clan.id, { name }); + const role = await interaction.guild.roles.fetch(clan.roleId).catch(() => null); + await role?.setName(name).catch(() => {}); + const category = await interaction.guild.channels.fetch(clan.categoryId).catch(() => null); + await category?.setName(`Clan • ${name}`.slice(0, 100)).catch(() => {}); + return interaction.reply({ content: `Your clan has been renamed to **${name}**.` }); + } + + return interaction.reply({ content: 'Unknown clan action.', ephemeral: true }); + } catch (error) { + return interaction.reply({ content: `Clan operation failed: ${error.message || 'Unknown error'}`, ephemeral: true }).catch(() => {}); + } + }, +}; + +async function resolveUserClanId(client, guildId, userId) { + const clan = await getClanForUser(client, guildId, userId); + return clan?.id || ''; +} + +async function guildChannel(interaction, channelId) { + return interaction.guild.channels.fetch(channelId).catch(() => null); +} From 367423783bb6f78c45f44fda71a380a16f3e6ace Mon Sep 17 00:00:00 2001 From: abdallahsaleed2001-afk Date: Sun, 23 Aug 2026 07:42:52 +0300 Subject: [PATCH 219/219] Fix clan command admin list and member role assignment --- src/commands/Community/clan.js | 224 ++++++++------------------------- 1 file changed, 53 insertions(+), 171 deletions(-) diff --git a/src/commands/Community/clan.js b/src/commands/Community/clan.js index 97c8c3d11d..57abb7e5b8 100644 --- a/src/commands/Community/clan.js +++ b/src/commands/Community/clan.js @@ -1,71 +1,30 @@ -import { - ChannelType, - EmbedBuilder, - PermissionFlagsBits, - SlashCommandBuilder, -} from 'discord.js'; -import { - addClanMember, - createClan, - deleteClan, - getClan, - getClanForUser, - getClans, - makeClanId, - removeClanMember, - sanitizeClanName, - updateClan, -} from '../../services/clanService.js'; +import { ChannelType, EmbedBuilder, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; +import { addClanMember, createClan, deleteClan, getClan, getClanForUser, getClans, makeClanId, removeClanMember, sanitizeClanName, updateClan } from '../../services/clanService.js'; const clanCommand = new SlashCommandBuilder() - .setName('clan') - .setDescription('Manage clans and your clan') - .setDMPermission(false) - .addSubcommandGroup((group) => group - .setName('admin') - .setDescription('Administrator clan controls') - .addSubcommand((sub) => sub - .setName('create') - .setDescription('Create a clan for a user') + .setName('clan').setDescription('Manage clans and your clan').setDMPermission(false) + .addSubcommandGroup((group) => group.setName('admin').setDescription('Administrator clan controls') + .addSubcommand((sub) => sub.setName('create').setDescription('Create a clan for a user') .addUserOption((opt) => opt.setName('owner').setDescription('Clan owner').setRequired(true)) .addStringOption((opt) => opt.setName('name').setDescription('Clan name').setRequired(true).setMaxLength(50))) - .addSubcommand((sub) => sub - .setName('delete') - .setDescription('Delete a clan and its channels') + .addSubcommand((sub) => sub.setName('delete').setDescription('Delete a clan and its channels') .addStringOption((opt) => opt.setName('clan_id').setDescription('Clan ID').setRequired(true))) - .addSubcommand((sub) => sub - .setName('list') - .setDescription('List all clans')) - .addSubcommand((sub) => sub - .setName('info') - .setDescription('View a clan') + .addSubcommand((sub) => sub.setName('list').setDescription('List all clans')) + .addSubcommand((sub) => sub.setName('info').setDescription('View a clan') .addStringOption((opt) => opt.setName('clan_id').setDescription('Clan ID').setRequired(true)))) - .addSubcommand((sub) => sub - .setName('info') - .setDescription('View your clan')) - .addSubcommand((sub) => sub - .setName('members') - .setDescription('View your clan members')) - .addSubcommand((sub) => sub - .setName('add') - .setDescription('Add a member to your clan') + .addSubcommand((sub) => sub.setName('info').setDescription('View your clan')) + .addSubcommand((sub) => sub.setName('members').setDescription('View your clan members')) + .addSubcommand((sub) => sub.setName('add').setDescription('Add a member to your clan') .addUserOption((opt) => opt.setName('user').setDescription('Member to add').setRequired(true))) - .addSubcommand((sub) => sub - .setName('remove') - .setDescription('Remove a member from your clan') + .addSubcommand((sub) => sub.setName('remove').setDescription('Remove a member from your clan') .addUserOption((opt) => opt.setName('user').setDescription('Member to remove').setRequired(true))) - .addSubcommand((sub) => sub - .setName('rename') - .setDescription('Rename your clan') + .addSubcommand((sub) => sub.setName('rename').setDescription('Rename your clan') .addStringOption((opt) => opt.setName('name').setDescription('New clan name').setRequired(true).setMaxLength(50))); -function isAdmin(interaction) { - return interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild); -} +const adminAllowed = (interaction) => interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild); function clanEmbed(clan) { - return new EmbedBuilder() - .setTitle(`Clan • ${clan.name}`) + return new EmbedBuilder().setTitle(`Clan • ${clan.name}`) .addFields( { name: 'Clan ID', value: `\`${clan.id}\``, inline: true }, { name: 'Owner', value: `<@${clan.ownerId}>`, inline: true }, @@ -73,69 +32,25 @@ function clanEmbed(clan) { { name: 'Role', value: `<@&${clan.roleId}>`, inline: true }, { name: 'Text', value: `<#${clan.textChannelId}>`, inline: true }, { name: 'Voice', value: `<#${clan.voiceChannelId}>`, inline: true }, - ) - .setTimestamp(); -} - -async function fetchClanOwnerMember(interaction, clan) { - return interaction.guild.members.fetch(clan.ownerId).catch(() => null); + ).setTimestamp(); } async function createClanResources(interaction, owner, name) { const guild = interaction.guild; - const role = await guild.roles.create({ - name: name.slice(0, 100), - reason: `Clan created by ${interaction.user.tag}`, - }); - + const role = await guild.roles.create({ name: name.slice(0, 100), reason: `Clan created by ${interaction.user.tag}` }); let category; let textChannel; let voiceChannel; - try { const overwrites = [ { id: guild.roles.everyone.id, deny: [PermissionFlagsBits.ViewChannel] }, - { - id: role.id, - allow: [ - PermissionFlagsBits.ViewChannel, - PermissionFlagsBits.SendMessages, - PermissionFlagsBits.ReadMessageHistory, - PermissionFlagsBits.Connect, - PermissionFlagsBits.Speak, - ], - }, - { - id: guild.members.me.id, - allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels, PermissionFlagsBits.ManageMessages, PermissionFlagsBits.Connect, PermissionFlagsBits.Speak], - }, + { id: role.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory, PermissionFlagsBits.Connect, PermissionFlagsBits.Speak] }, + { id: guild.members.me.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels, PermissionFlagsBits.ManageMessages, PermissionFlagsBits.Connect, PermissionFlagsBits.Speak] }, ]; - - category = await guild.channels.create({ - name: `Clan • ${name}`.slice(0, 100), - type: ChannelType.GuildCategory, - permissionOverwrites: overwrites, - reason: `Clan category for ${name}`, - }); - - textChannel = await guild.channels.create({ - name: 'chat', - type: ChannelType.GuildText, - parent: category.id, - permissionOverwrites: overwrites, - reason: `Clan text channel for ${name}`, - }); - - voiceChannel = await guild.channels.create({ - name: 'Voice', - type: ChannelType.GuildVoice, - parent: category.id, - permissionOverwrites: overwrites, - reason: `Clan voice channel for ${name}`, - }); - + category = await guild.channels.create({ name: `Clan • ${name}`.slice(0, 100), type: ChannelType.GuildCategory, permissionOverwrites: overwrites, reason: `Clan category for ${name}` }); + textChannel = await guild.channels.create({ name: 'chat', type: ChannelType.GuildText, parent: category.id, permissionOverwrites: overwrites, reason: `Clan text channel for ${name}` }); + voiceChannel = await guild.channels.create({ name: 'Voice', type: ChannelType.GuildVoice, parent: category.id, permissionOverwrites: overwrites, reason: `Clan voice channel for ${name}` }); await owner.roles.add(role, `Clan owner: ${name}`); - return { role, category, textChannel, voiceChannel }; } catch (error) { await voiceChannel?.delete().catch(() => {}); @@ -150,66 +65,50 @@ export default { data: clanCommand, category: 'Community', async execute(interaction, config, client) { - if (!interaction.inGuild()) { - return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); - } - + if (!interaction.inGuild()) return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }); try { const group = interaction.options.getSubcommandGroup(false); const sub = interaction.options.getSubcommand(); if (group === 'admin') { - if (!isAdmin(interaction)) { - return interaction.reply({ content: 'You need **Manage Server** permission to use clan administration.', ephemeral: true }); + if (!adminAllowed(interaction)) return interaction.reply({ content: 'You need **Manage Server** permission to use clan administration.', ephemeral: true }); + + if (sub === 'list') { + const clans = await getClans(client, interaction.guildId); + if (!clans.length) return interaction.reply({ content: 'No clans exist in this server.', ephemeral: true }); + return interaction.reply({ embeds: [new EmbedBuilder().setTitle('Server Clans').setDescription(clans.map((item, index) => `${index + 1}. **${item.name}** — <@${item.ownerId}> — \`${item.id}\``).join('\n')).setTimestamp()] }); } if (sub === 'create') { const owner = interaction.options.getMember('owner'); const name = sanitizeClanName(interaction.options.getString('name', true)); if (!owner || !name) return interaction.reply({ content: 'Invalid clan owner or name.', ephemeral: true }); - const existing = await getClanForUser(client, interaction.guildId, owner.id); if (existing) return interaction.reply({ content: `That user already belongs to **${existing.name}**.`, ephemeral: true }); - const clans = await getClans(client, interaction.guildId); - if (clans.some((clan) => clan.name.toLowerCase() === name.toLowerCase())) { - return interaction.reply({ content: 'A clan with that name already exists.', ephemeral: true }); - } + if (clans.some((clan) => clan.name.toLowerCase() === name.toLowerCase())) return interaction.reply({ content: 'A clan with that name already exists.', ephemeral: true }); const resources = await createClanResources(interaction, owner, name); - const clan = await createClan(client, interaction.guildId, { - id: makeClanId(), - name, - ownerId: owner.id, - roleId: resources.role.id, - categoryId: resources.category.id, - textChannelId: resources.textChannel.id, - voiceChannelId: resources.voiceChannel.id, - memberIds: [], - }); - - await resources.textChannel.send(`Welcome to **${name}**!\nOwner: <@${owner.id}>\nUse \`/clan members\` and \`/clan add\` to manage your clan.`).catch(() => {}); - return interaction.reply({ embeds: [new EmbedBuilder().setTitle('Clan Created').setDescription(`**${name}** has been created for ${owner}.\n\n${resources.textChannel}\n${resources.voiceChannel}\nRole: ${resources.role}\nClan ID: \`${clan.id}\``).setTimestamp()] }); + try { + const clan = await createClan(client, interaction.guildId, { id: makeClanId(), name, ownerId: owner.id, roleId: resources.role.id, categoryId: resources.category.id, textChannelId: resources.textChannel.id, voiceChannelId: resources.voiceChannel.id, memberIds: [] }); + await resources.textChannel.send(`Welcome to **${name}**!\nOwner: <@${owner.id}>\nUse \`/clan members\` and \`/clan add\` to manage your clan.`).catch(() => {}); + return interaction.reply({ embeds: [new EmbedBuilder().setTitle('Clan Created').setDescription(`**${name}** has been created for ${owner}.\n\n${resources.textChannel}\n${resources.voiceChannel}\nRole: ${resources.role}\nClan ID: \`${clan.id}\``).setTimestamp()] }); + } catch (error) { + await resources.voiceChannel.delete().catch(() => {}); + await resources.textChannel.delete().catch(() => {}); + await resources.category.delete().catch(() => {}); + await resources.role.delete().catch(() => {}); + throw error; + } } const clanId = interaction.options.getString('clan_id', true); const clan = await getClan(client, interaction.guildId, clanId); if (!clan) return interaction.reply({ content: 'Clan not found.', ephemeral: true }); - - if (sub === 'list') { - const clans = await getClans(client, interaction.guildId); - if (!clans.length) return interaction.reply({ content: 'No clans exist in this server.', ephemeral: true }); - return interaction.reply({ embeds: [new EmbedBuilder().setTitle('Server Clans').setDescription(clans.map((item, index) => `${index + 1}. **${item.name}** — <@${item.ownerId}> — \`${item.id}\``).join('\n')).setTimestamp()] }); - } - - if (sub === 'info') { - return interaction.reply({ embeds: [clanEmbed(clan)] }); - } - + if (sub === 'info') return interaction.reply({ embeds: [clanEmbed(clan)] }); if (sub === 'delete') { - const channelIds = [clan.voiceChannelId, clan.textChannelId, clan.categoryId]; - for (const channelId of channelIds) { - const channel = await guildChannel(interaction, channelId); + for (const channelId of [clan.voiceChannelId, clan.textChannelId, clan.categoryId]) { + const channel = await interaction.guild.channels.fetch(channelId).catch(() => null); await channel?.delete(`Deleting clan ${clan.name}`).catch(() => {}); } const role = await interaction.guild.roles.fetch(clan.roleId).catch(() => null); @@ -219,11 +118,8 @@ export default { } } - const clan = await getClan(client, interaction.guildId, await resolveUserClanId(client, interaction.guildId, interaction.user.id)); - if (!clan) { - return interaction.reply({ content: 'You are not a member of a clan.', ephemeral: true }); - } - + const clan = await getClanForUser(client, interaction.guildId, interaction.user.id); + if (!clan) return interaction.reply({ content: 'You are not a member of a clan.', ephemeral: true }); if (sub === 'info') return interaction.reply({ embeds: [clanEmbed(clan)] }); if (sub === 'members') { @@ -235,21 +131,18 @@ export default { return interaction.reply({ embeds: [new EmbedBuilder().setTitle(`${clan.name} Members`).setDescription(members.join('\n') || 'No members.').setTimestamp()] }); } - if (clan.ownerId !== interaction.user.id) { - return interaction.reply({ content: 'Only the clan owner can manage clan members or rename the clan.', ephemeral: true }); - } + if (clan.ownerId !== interaction.user.id) return interaction.reply({ content: 'Only the clan owner can manage members or rename the clan.', ephemeral: true }); if (sub === 'add') { const user = interaction.options.getUser('user', true); if (user.bot) return interaction.reply({ content: 'Bots cannot be added to clans.', ephemeral: true }); const currentClan = await getClanForUser(client, interaction.guildId, user.id); if (currentClan) return interaction.reply({ content: `That user already belongs to **${currentClan.name}**.`, ephemeral: true }); - await addClanMember(client, interaction.guildId, clan.id, user.id); + const member = await interaction.guild.members.fetch(user.id).catch(() => null); const role = await interaction.guild.roles.fetch(clan.roleId).catch(() => null); - if (role) await interaction.guild.members.addRole({ user: user.id, role: role.id, reason: `Added to clan ${clan.name}` }).catch(async () => { - const member = await interaction.guild.members.fetch(user.id).catch(() => null); - await member?.roles.add(role).catch(() => {}); - }); + if (!member || !role) return interaction.reply({ content: 'Could not find the member or clan role.', ephemeral: true }); + await member.roles.add(role, `Added to clan ${clan.name}`); + await addClanMember(client, interaction.guildId, clan.id, user.id); return interaction.reply({ content: `${user} was added to **${clan.name}**.` }); } @@ -258,8 +151,8 @@ export default { if (user.id === clan.ownerId) return interaction.reply({ content: 'The clan owner cannot be removed.', ephemeral: true }); if (!clan.memberIds.includes(user.id)) return interaction.reply({ content: 'That user is not in your clan.', ephemeral: true }); await removeClanMember(client, interaction.guildId, clan.id, user.id); - const role = await interaction.guild.roles.fetch(clan.roleId).catch(() => null); const member = await interaction.guild.members.fetch(user.id).catch(() => null); + const role = await interaction.guild.roles.fetch(clan.roleId).catch(() => null); await member?.roles.remove(role).catch(() => {}); return interaction.reply({ content: `${user} was removed from **${clan.name}**.` }); } @@ -268,9 +161,7 @@ export default { const name = sanitizeClanName(interaction.options.getString('name', true)); if (!name) return interaction.reply({ content: 'Invalid clan name.', ephemeral: true }); const clans = await getClans(client, interaction.guildId); - if (clans.some((item) => item.id !== clan.id && item.name.toLowerCase() === name.toLowerCase())) { - return interaction.reply({ content: 'A clan with that name already exists.', ephemeral: true }); - } + if (clans.some((item) => item.id !== clan.id && item.name.toLowerCase() === name.toLowerCase())) return interaction.reply({ content: 'A clan with that name already exists.', ephemeral: true }); await updateClan(client, interaction.guildId, clan.id, { name }); const role = await interaction.guild.roles.fetch(clan.roleId).catch(() => null); await role?.setName(name).catch(() => {}); @@ -285,12 +176,3 @@ export default { } }, }; - -async function resolveUserClanId(client, guildId, userId) { - const clan = await getClanForUser(client, guildId, userId); - return clan?.id || ''; -} - -async function guildChannel(interaction, channelId) { - return interaction.guild.channels.fetch(channelId).catch(() => null); -}