diff --git a/aethex-bot/commands/bulkmod.js b/aethex-bot/commands/bulkmod.js new file mode 100644 index 0000000..8bb108c --- /dev/null +++ b/aethex-bot/commands/bulkmod.js @@ -0,0 +1,401 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('bulkmod') + .setDescription('Perform moderation actions on multiple users at once') + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .addSubcommand(sub => + sub.setName('ban') + .setDescription('Ban multiple users at once') + .addStringOption(opt => + opt.setName('users') + .setDescription('User IDs or mentions separated by spaces') + .setRequired(true)) + .addStringOption(opt => + opt.setName('reason') + .setDescription('Reason for ban') + .setRequired(false) + .setMaxLength(500)) + .addIntegerOption(opt => + opt.setName('delete_days') + .setDescription('Days of messages to delete (0-7)') + .setMinValue(0) + .setMaxValue(7))) + .addSubcommand(sub => + sub.setName('kick') + .setDescription('Kick multiple users at once') + .addStringOption(opt => + opt.setName('users') + .setDescription('User IDs or mentions separated by spaces') + .setRequired(true)) + .addStringOption(opt => + opt.setName('reason') + .setDescription('Reason for kick') + .setRequired(false) + .setMaxLength(500))) + .addSubcommand(sub => + sub.setName('timeout') + .setDescription('Timeout multiple users at once') + .addStringOption(opt => + opt.setName('users') + .setDescription('User IDs or mentions separated by spaces') + .setRequired(true)) + .addStringOption(opt => + opt.setName('duration') + .setDescription('Timeout duration') + .setRequired(true) + .addChoices( + { name: '5 minutes', value: '5m' }, + { name: '10 minutes', value: '10m' }, + { name: '30 minutes', value: '30m' }, + { name: '1 hour', value: '1h' }, + { name: '6 hours', value: '6h' }, + { name: '12 hours', value: '12h' }, + { name: '1 day', value: '1d' }, + { name: '3 days', value: '3d' }, + { name: '1 week', value: '1w' } + )) + .addStringOption(opt => + opt.setName('reason') + .setDescription('Reason for timeout') + .setRequired(false) + .setMaxLength(500))) + .addSubcommand(sub => + sub.setName('warn') + .setDescription('Warn multiple users at once') + .addStringOption(opt => + opt.setName('users') + .setDescription('User IDs or mentions separated by spaces') + .setRequired(true)) + .addStringOption(opt => + opt.setName('reason') + .setDescription('Reason for warning') + .setRequired(true) + .setMaxLength(500))) + .addSubcommand(sub => + sub.setName('remove_timeout') + .setDescription('Remove timeout from multiple users') + .addStringOption(opt => + opt.setName('users') + .setDescription('User IDs or mentions separated by spaces') + .setRequired(true))), + + async execute(interaction, supabase, client) { + await interaction.deferReply(); + + const subcommand = interaction.options.getSubcommand(); + const usersInput = interaction.options.getString('users'); + const reason = interaction.options.getString('reason') || 'No reason provided'; + const moderator = interaction.user; + + const userIds = parseUserIds(usersInput); + + if (userIds.length === 0) { + return interaction.editReply({ content: '❌ No valid user IDs provided.' }); + } + + if (userIds.length > 25) { + return interaction.editReply({ content: '❌ Maximum 25 users per bulk action.' }); + } + + const results = { success: [], failed: [] }; + + switch (subcommand) { + case 'ban': + await handleBulkBan(interaction, userIds, reason, results, supabase, moderator); + break; + case 'kick': + await handleBulkKick(interaction, userIds, reason, results, supabase, moderator); + break; + case 'timeout': + const duration = interaction.options.getString('duration'); + await handleBulkTimeout(interaction, userIds, duration, reason, results, supabase, moderator); + break; + case 'warn': + await handleBulkWarn(interaction, userIds, reason, results, supabase, moderator); + break; + case 'remove_timeout': + await handleBulkRemoveTimeout(interaction, userIds, results, supabase, moderator); + break; + } + + const embed = createResultEmbed(subcommand, results, moderator); + await interaction.editReply({ embeds: [embed] }); + }, +}; + +function parseUserIds(input) { + const mentionRegex = /<@!?(\d+)>/g; + const idRegex = /\b(\d{17,20})\b/g; + + const ids = new Set(); + + let match; + while ((match = mentionRegex.exec(input)) !== null) { + ids.add(match[1]); + } + while ((match = idRegex.exec(input)) !== null) { + ids.add(match[1]); + } + + return Array.from(ids); +} + +function parseDuration(duration) { + const units = { m: 60, h: 3600, d: 86400, w: 604800 }; + const match = duration.match(/^(\d+)([mhdw])$/); + if (!match) return null; + return parseInt(match[1]) * units[match[2]] * 1000; +} + +async function handleBulkBan(interaction, userIds, reason, results, supabase, moderator) { + const deleteDays = interaction.options.getInteger('delete_days') || 0; + + for (const userId of userIds) { + try { + if (userId === moderator.id) { + results.failed.push({ id: userId, reason: 'Cannot ban yourself' }); + continue; + } + + const member = await interaction.guild.members.fetch(userId).catch(() => null); + if (member && !member.bannable) { + results.failed.push({ id: userId, reason: 'Not bannable' }); + continue; + } + + await interaction.guild.members.ban(userId, { + reason: `[Bulk] ${reason} | By ${moderator.tag}`, + deleteMessageSeconds: deleteDays * 24 * 60 * 60 + }); + + if (supabase) { + const user = await interaction.client.users.fetch(userId).catch(() => null); + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'ban', + user_id: userId, + user_tag: user?.tag || 'Unknown', + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: `[Bulk] ${reason}`, + }).catch(() => {}); + } + + results.success.push(userId); + } catch (error) { + results.failed.push({ id: userId, reason: error.message.substring(0, 50) }); + } + } +} + +async function handleBulkKick(interaction, userIds, reason, results, supabase, moderator) { + for (const userId of userIds) { + try { + if (userId === moderator.id) { + results.failed.push({ id: userId, reason: 'Cannot kick yourself' }); + continue; + } + + const member = await interaction.guild.members.fetch(userId).catch(() => null); + if (!member) { + results.failed.push({ id: userId, reason: 'Not in server' }); + continue; + } + if (!member.kickable) { + results.failed.push({ id: userId, reason: 'Not kickable' }); + continue; + } + + await member.kick(`[Bulk] ${reason} | By ${moderator.tag}`); + + if (supabase) { + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'kick', + user_id: userId, + user_tag: member.user.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: `[Bulk] ${reason}`, + }).catch(() => {}); + } + + results.success.push(userId); + } catch (error) { + results.failed.push({ id: userId, reason: error.message.substring(0, 50) }); + } + } +} + +async function handleBulkTimeout(interaction, userIds, duration, reason, results, supabase, moderator) { + const durationMs = parseDuration(duration); + + for (const userId of userIds) { + try { + if (userId === moderator.id) { + results.failed.push({ id: userId, reason: 'Cannot timeout yourself' }); + continue; + } + + const member = await interaction.guild.members.fetch(userId).catch(() => null); + if (!member) { + results.failed.push({ id: userId, reason: 'Not in server' }); + continue; + } + if (!member.moderatable) { + results.failed.push({ id: userId, reason: 'Not moderatable' }); + continue; + } + + await member.timeout(durationMs, `[Bulk] ${reason} | By ${moderator.tag}`); + + if (supabase) { + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'timeout', + user_id: userId, + user_tag: member.user.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: `[Bulk] ${reason}`, + duration: duration + }).catch(() => {}); + } + + results.success.push(userId); + } catch (error) { + results.failed.push({ id: userId, reason: error.message.substring(0, 50) }); + } + } +} + +async function handleBulkWarn(interaction, userIds, reason, results, supabase, moderator) { + for (const userId of userIds) { + try { + const member = await interaction.guild.members.fetch(userId).catch(() => null); + if (!member) { + results.failed.push({ id: userId, reason: 'Not in server' }); + continue; + } + + if (supabase) { + await supabase.from('warnings').insert({ + guild_id: interaction.guildId, + user_id: userId, + user_tag: member.user.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: `[Bulk] ${reason}`, + }); + + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'warn', + user_id: userId, + user_tag: member.user.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: `[Bulk] ${reason}`, + }).catch(() => {}); + } + + try { + await member.user.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xffa500) + .setTitle(`Warning from ${interaction.guild.name}`) + .setDescription(reason) + .setTimestamp() + ] + }); + } catch {} + + results.success.push(userId); + } catch (error) { + results.failed.push({ id: userId, reason: error.message.substring(0, 50) }); + } + } +} + +async function handleBulkRemoveTimeout(interaction, userIds, results, supabase, moderator) { + for (const userId of userIds) { + try { + const member = await interaction.guild.members.fetch(userId).catch(() => null); + if (!member) { + results.failed.push({ id: userId, reason: 'Not in server' }); + continue; + } + if (!member.isCommunicationDisabled()) { + results.failed.push({ id: userId, reason: 'Not timed out' }); + continue; + } + + await member.timeout(null, `Timeout removed by ${moderator.tag}`); + + if (supabase) { + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'untimeout', + user_id: userId, + user_tag: member.user.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: 'Bulk timeout removal', + }).catch(() => {}); + } + + results.success.push(userId); + } catch (error) { + results.failed.push({ id: userId, reason: error.message.substring(0, 50) }); + } + } +} + +function createResultEmbed(action, results, moderator) { + const actionNames = { + ban: 'Bulk Ban', + kick: 'Bulk Kick', + timeout: 'Bulk Timeout', + warn: 'Bulk Warning', + remove_timeout: 'Bulk Timeout Removal' + }; + + const colors = { + ban: 0xff0000, + kick: 0xff6b6b, + timeout: 0xffa500, + warn: 0xffcc00, + remove_timeout: 0x00ff00 + }; + + const embed = new EmbedBuilder() + .setTitle(`${actionNames[action]} Results`) + .setColor(colors[action]) + .setTimestamp() + .setFooter({ text: `Action by ${moderator.tag}` }); + + if (results.success.length > 0) { + const successList = results.success.map(id => `<@${id}>`).join(', '); + embed.addFields({ + name: `✅ Successful (${results.success.length})`, + value: successList.length > 1024 ? `${results.success.length} users affected` : successList + }); + } + + if (results.failed.length > 0) { + const failedList = results.failed.map(f => `<@${f.id}>: ${f.reason}`).join('\n'); + embed.addFields({ + name: `❌ Failed (${results.failed.length})`, + value: failedList.length > 1024 ? `${results.failed.length} users failed` : failedList + }); + } + + if (results.success.length === 0 && results.failed.length === 0) { + embed.setDescription('No users were processed.'); + } + + return embed; +} diff --git a/aethex-bot/public/dashboard.html b/aethex-bot/public/dashboard.html index 5ed8710..3f898cd 100644 --- a/aethex-bot/public/dashboard.html +++ b/aethex-bot/public/dashboard.html @@ -1870,6 +1870,7 @@