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
401 lines
13 KiB
JavaScript
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;
|
|
}
|