AeThex-Bot-Master/aethex-bot/commands/bulkmod.js
sirpiglr 299db195c6 Add bulk moderation actions for server administrators
Adds a new `/bulkmod` slash command and a corresponding API endpoint to `webServer.js` to handle bulk moderation actions (ban, kick, timeout, warn, remove_timeout) for up to 25 users at once. Also adds a new "Bulk Actions" tab to the dashboard.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 9c56d07f-e250-4dd9-8108-5c170cbdb44f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/INbSSam
Replit-Helium-Checkpoint-Created: true
2025-12-12 23:58:15 +00:00

401 lines
13 KiB
JavaScript

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