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
This commit is contained in:
sirpiglr 2025-12-12 23:58:15 +00:00
parent 467fd8467f
commit 299db195c6
3 changed files with 799 additions and 0 deletions

View file

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

View file

@ -1870,6 +1870,7 @@
<div class="tab active" data-mod-tab="mod-warnings">Warnings</div>
<div class="tab" data-mod-tab="mod-bans">Bans</div>
<div class="tab" data-mod-tab="mod-activity">Activity Feed</div>
<div class="tab" data-mod-tab="mod-bulk">Bulk Actions</div>
</div>
<div id="mod-warnings" class="mod-section">
@ -1929,6 +1930,80 @@
</div>
</div>
</div>
<div id="mod-bulk" class="mod-section hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">Bulk Moderation Actions</h3>
</div>
<div class="card-body">
<p style="color:var(--text-secondary);margin-bottom:1.5rem">
Perform moderation actions on multiple users at once. Enter user IDs separated by spaces, commas, or newlines.
</p>
<div class="form-group">
<label for="bulkAction">Action Type</label>
<select id="bulkAction" class="form-input" onchange="updateBulkActionForm()">
<option value="ban">Ban Users</option>
<option value="kick">Kick Users</option>
<option value="timeout">Timeout Users</option>
<option value="warn">Warn Users</option>
<option value="remove_timeout">Remove Timeout</option>
</select>
</div>
<div class="form-group">
<label for="bulkUserIds">User IDs (max 25)</label>
<textarea id="bulkUserIds" class="form-input" rows="4" placeholder="123456789012345678&#10;234567890123456789&#10;345678901234567890"></textarea>
<small style="color:var(--text-secondary)">Enter one user ID per line, or separate with spaces/commas</small>
</div>
<div id="bulkTimeoutDuration" class="form-group" style="display:none">
<label for="bulkDuration">Timeout Duration</label>
<select id="bulkDuration" class="form-input">
<option value="5m">5 minutes</option>
<option value="10m">10 minutes</option>
<option value="30m">30 minutes</option>
<option value="1h" selected>1 hour</option>
<option value="6h">6 hours</option>
<option value="12h">12 hours</option>
<option value="1d">1 day</option>
<option value="3d">3 days</option>
<option value="1w">1 week</option>
</select>
</div>
<div id="bulkReasonGroup" class="form-group">
<label for="bulkReason">Reason</label>
<input type="text" id="bulkReason" class="form-input" placeholder="Reason for action..." maxlength="500">
</div>
<div id="bulkDeleteDays" class="form-group" style="display:none">
<label for="bulkBanDeleteDays">Delete Message History</label>
<select id="bulkBanDeleteDays" class="form-input">
<option value="0">Don't delete messages</option>
<option value="1">1 day</option>
<option value="3">3 days</option>
<option value="7">7 days</option>
</select>
</div>
<div style="display:flex;gap:1rem;align-items:center;margin-top:1.5rem">
<button class="btn btn-danger" onclick="executeBulkAction()" id="bulkActionBtn">Execute Bulk Ban</button>
<span id="bulkActionStatus"></span>
</div>
</div>
</div>
<div class="card" style="margin-top:1.5rem">
<div class="card-header">
<h3 class="card-title">Results</h3>
</div>
<div class="card-body" id="bulkActionResults">
<div class="empty-state">No bulk actions executed yet</div>
</div>
</div>
</div>
</div>
<div id="page-analytics" class="page hidden">
@ -3437,6 +3512,136 @@
}
}
function updateBulkActionForm() {
const action = document.getElementById('bulkAction').value;
const btn = document.getElementById('bulkActionBtn');
const timeoutDiv = document.getElementById('bulkTimeoutDuration');
const deleteDaysDiv = document.getElementById('bulkDeleteDays');
const reasonGroup = document.getElementById('bulkReasonGroup');
const actionNames = {
ban: 'Execute Bulk Ban',
kick: 'Execute Bulk Kick',
timeout: 'Execute Bulk Timeout',
warn: 'Execute Bulk Warning',
remove_timeout: 'Remove Timeouts'
};
btn.textContent = actionNames[action] || 'Execute Action';
btn.className = action === 'remove_timeout' ? 'btn btn-primary' : 'btn btn-danger';
timeoutDiv.style.display = action === 'timeout' ? 'block' : 'none';
deleteDaysDiv.style.display = action === 'ban' ? 'block' : 'none';
reasonGroup.style.display = action === 'remove_timeout' ? 'none' : 'block';
}
function parseUserIdsFromInput(input) {
const ids = input.match(/\d{17,20}/g) || [];
return [...new Set(ids)];
}
async function executeBulkAction() {
if (!currentGuild) return;
const action = document.getElementById('bulkAction').value;
const userIdsInput = document.getElementById('bulkUserIds').value;
const reason = document.getElementById('bulkReason').value || 'No reason provided';
const duration = document.getElementById('bulkDuration').value;
const deleteDays = document.getElementById('bulkBanDeleteDays').value;
const userIds = parseUserIdsFromInput(userIdsInput);
if (userIds.length === 0) {
alert('Please enter at least one valid user ID');
return;
}
if (userIds.length > 25) {
alert('Maximum 25 users per bulk action');
return;
}
const actionNames = { ban: 'ban', kick: 'kick', timeout: 'timeout', warn: 'warn', remove_timeout: 'remove timeout from' };
if (!confirm(`Are you sure you want to ${actionNames[action]} ${userIds.length} user(s)? This action cannot be undone.`)) {
return;
}
const btn = document.getElementById('bulkActionBtn');
const status = document.getElementById('bulkActionStatus');
btn.disabled = true;
status.textContent = 'Processing...';
status.style.color = 'var(--text-secondary)';
try {
const res = await fetch('/api/guild/' + currentGuild + '/moderation/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action,
userIds,
reason,
duration,
deleteDays: parseInt(deleteDays, 10)
})
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Bulk action failed');
}
status.textContent = 'Complete!';
status.style.color = 'var(--success)';
displayBulkResults(action, data);
document.getElementById('bulkUserIds').value = '';
document.getElementById('bulkReason').value = '';
loadModerationStats();
loadActivityFeed();
} catch (e) {
status.textContent = 'Error: ' + e.message;
status.style.color = 'var(--danger)';
} finally {
btn.disabled = false;
}
}
function displayBulkResults(action, data) {
const container = document.getElementById('bulkActionResults');
const actionNames = { ban: 'Banned', kick: 'Kicked', timeout: 'Timed out', warn: 'Warned', remove_timeout: 'Timeout removed' };
let html = '';
if (data.success && data.success.length > 0) {
html += '<div style="margin-bottom:1rem">';
html += '<div style="color:var(--success);font-weight:600;margin-bottom:0.5rem">Successful (' + data.success.length + ')</div>';
html += '<div style="display:flex;flex-wrap:wrap;gap:0.5rem">';
data.success.forEach(id => {
html += '<span style="background:rgba(34,197,94,0.2);color:var(--success);padding:0.25rem 0.5rem;border-radius:4px;font-size:0.85rem">' + id + '</span>';
});
html += '</div></div>';
}
if (data.failed && data.failed.length > 0) {
html += '<div>';
html += '<div style="color:var(--danger);font-weight:600;margin-bottom:0.5rem">Failed (' + data.failed.length + ')</div>';
html += '<div style="display:flex;flex-direction:column;gap:0.25rem">';
data.failed.forEach(f => {
html += '<div style="font-size:0.85rem;color:var(--text-secondary)">' + f.id + ': <span style="color:var(--danger)">' + escapeHtml(f.reason) + '</span></div>';
});
html += '</div></div>';
}
if (!html) {
html = '<div class="empty-state">No users processed</div>';
}
container.innerHTML = html;
}
// Analytics Functions
function loadAnalyticsData() {
loadAnalyticsStats();

View file

@ -1596,6 +1596,199 @@ function createWebServer(discordClient, supabase, options = {}) {
}
});
app.post('/api/guild/:guildId/moderation/bulk', async (req, res) => {
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
const { action, userIds, reason, duration, deleteDays } = req.body;
if (!action || !userIds || !Array.isArray(userIds)) {
return res.status(400).json({ error: 'Invalid request' });
}
if (userIds.length > 25) {
return res.status(400).json({ error: 'Maximum 25 users per bulk action' });
}
const validActions = ['ban', 'kick', 'timeout', 'warn', 'remove_timeout'];
if (!validActions.includes(action)) {
return res.status(400).json({ error: 'Invalid action' });
}
const guild = discordClient.guilds.cache.get(guildId);
if (!guild) {
return res.status(404).json({ error: 'Guild not found' });
}
const moderator = req.session.user;
const results = { success: [], failed: [] };
const durationMap = {
'5m': 5 * 60 * 1000,
'10m': 10 * 60 * 1000,
'30m': 30 * 60 * 1000,
'1h': 60 * 60 * 1000,
'6h': 6 * 60 * 60 * 1000,
'12h': 12 * 60 * 60 * 1000,
'1d': 24 * 60 * 60 * 1000,
'3d': 3 * 24 * 60 * 60 * 1000,
'1w': 7 * 24 * 60 * 60 * 1000
};
for (const targetId of userIds) {
try {
switch (action) {
case 'ban': {
await guild.members.ban(targetId, {
reason: `[Dashboard Bulk] ${reason || 'No reason'} | By ${moderator.username}`,
deleteMessageSeconds: (deleteDays || 0) * 24 * 60 * 60
});
if (supabase) {
const user = await discordClient.users.fetch(targetId).catch(() => null);
await supabase.from('mod_actions').insert({
guild_id: guildId,
action: 'ban',
user_id: targetId,
user_tag: user?.tag || 'Unknown',
moderator_id: moderator.id,
moderator_tag: moderator.username,
reason: `[Dashboard Bulk] ${reason || 'No reason'}`
}).catch(() => {});
}
results.success.push(targetId);
break;
}
case 'kick': {
const member = await guild.members.fetch(targetId).catch(() => null);
if (!member) {
results.failed.push({ id: targetId, reason: 'Not in server' });
continue;
}
if (!member.kickable) {
results.failed.push({ id: targetId, reason: 'Not kickable' });
continue;
}
await member.kick(`[Dashboard Bulk] ${reason || 'No reason'} | By ${moderator.username}`);
if (supabase) {
await supabase.from('mod_actions').insert({
guild_id: guildId,
action: 'kick',
user_id: targetId,
user_tag: member.user.tag,
moderator_id: moderator.id,
moderator_tag: moderator.username,
reason: `[Dashboard Bulk] ${reason || 'No reason'}`
}).catch(() => {});
}
results.success.push(targetId);
break;
}
case 'timeout': {
const member = await guild.members.fetch(targetId).catch(() => null);
if (!member) {
results.failed.push({ id: targetId, reason: 'Not in server' });
continue;
}
if (!member.moderatable) {
results.failed.push({ id: targetId, reason: 'Not moderatable' });
continue;
}
const timeoutMs = durationMap[duration] || durationMap['1h'];
await member.timeout(timeoutMs, `[Dashboard Bulk] ${reason || 'No reason'} | By ${moderator.username}`);
if (supabase) {
await supabase.from('mod_actions').insert({
guild_id: guildId,
action: 'timeout',
user_id: targetId,
user_tag: member.user.tag,
moderator_id: moderator.id,
moderator_tag: moderator.username,
reason: `[Dashboard Bulk] ${reason || 'No reason'}`,
duration: duration
}).catch(() => {});
}
results.success.push(targetId);
break;
}
case 'warn': {
const member = await guild.members.fetch(targetId).catch(() => null);
if (!member) {
results.failed.push({ id: targetId, reason: 'Not in server' });
continue;
}
if (supabase) {
await supabase.from('warnings').insert({
guild_id: guildId,
user_id: targetId,
username: member.user.tag,
moderator_id: moderator.id,
moderator_tag: moderator.username,
reason: `[Dashboard Bulk] ${reason || 'No reason'}`
});
await supabase.from('mod_actions').insert({
guild_id: guildId,
action: 'warn',
user_id: targetId,
user_tag: member.user.tag,
moderator_id: moderator.id,
moderator_tag: moderator.username,
reason: `[Dashboard Bulk] ${reason || 'No reason'}`
}).catch(() => {});
}
results.success.push(targetId);
break;
}
case 'remove_timeout': {
const member = await guild.members.fetch(targetId).catch(() => null);
if (!member) {
results.failed.push({ id: targetId, reason: 'Not in server' });
continue;
}
if (!member.isCommunicationDisabled()) {
results.failed.push({ id: targetId, reason: 'Not timed out' });
continue;
}
await member.timeout(null, `Timeout removed via Dashboard by ${moderator.username}`);
if (supabase) {
await supabase.from('mod_actions').insert({
guild_id: guildId,
action: 'untimeout',
user_id: targetId,
user_tag: member.user.tag,
moderator_id: moderator.id,
moderator_tag: moderator.username,
reason: 'Dashboard bulk timeout removal'
}).catch(() => {});
}
results.success.push(targetId);
break;
}
}
} catch (error) {
results.failed.push({ id: targetId, reason: error.message?.substring(0, 50) || 'Unknown error' });
}
}
res.json(results);
});
// Analytics API Endpoints
app.get('/api/guild/:guildId/analytics/stats', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });