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; }