Moderation Update

This commit is contained in:
2025-10-09 06:13:48 -04:00
parent 2ae7202445
commit ff10bb3183
20 changed files with 2056 additions and 381 deletions

View File

@@ -636,6 +636,8 @@ app.post('/api/servers/:guildId/leave', async (req, res) => {
const guild = await bot.client.guilds.fetch(guildId);
if (guild) {
await guild.leave();
// Publish event for bot status change
publishEvent('*', 'botStatusUpdate', { guildId, isBotInServer: false });
res.json({ success: true });
} else {
res.status(404).json({ success: false, message: 'Bot is not in the specified server' });
@@ -654,7 +656,7 @@ app.get('/api/servers/:guildId/channels', async (req, res) => {
}
try {
const channels = await guild.channels.fetch();
const textChannels = channels.filter(channel => channel.type === 0).map(channel => ({ id: channel.id, name: channel.name }));
const textChannels = channels.filter(channel => channel.type === 0).map(channel => ({ id: channel.id, name: channel.name, type: channel.type }));
res.json(textChannels);
} catch (error) {
console.error('Error fetching channels:', error);
@@ -662,6 +664,40 @@ app.get('/api/servers/:guildId/channels', async (req, res) => {
}
});
app.get('/api/servers/:guildId/members', async (req, res) => {
const { guildId } = req.params;
const guild = bot.client.guilds.cache.get(guildId);
if (!guild) {
return res.json([]);
}
try {
// Get the requesting user from the session/token
// For now, we'll assume the frontend sends the user ID in a header or we get it from OAuth
// This is a simplified version - in production you'd want proper authentication
const members = await guild.members.fetch();
// Filter to members the bot can interact with and format for frontend
const bannableMembers = members
.filter(member => !member.user.bot) // Exclude bots
.map(member => ({
id: member.user.id,
username: member.user.username,
globalName: member.user.globalName,
displayName: member.displayName,
avatar: member.user.avatar,
joinedAt: member.joinedAt,
roles: member.roles.cache.map(role => ({ id: role.id, name: role.name, position: role.position }))
}))
.sort((a, b) => a.username.localeCompare(b.username));
res.json(bannableMembers);
} catch (error) {
console.error('Error fetching members:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.get('/api/servers/:guildId/welcome-leave-settings', async (req, res) => {
const { guildId } = req.params;
try {
@@ -1098,6 +1134,351 @@ app.delete('/api/servers/:guildId/invites/:code', async (req, res) => {
}
});
// ADMIN LOGS: configuration and retrieval
app.get('/api/servers/:guildId/admin-logs-settings', async (req, res) => {
try {
const { guildId } = req.params;
const settings = (await pgClient.getServerSettings(guildId)) || {};
const adminLogsSettings = settings.adminLogs || { enabled: false, channelId: '', commands: { kick: true, ban: true, timeout: true } };
res.json(adminLogsSettings);
} catch (error) {
console.error('Error fetching admin logs settings:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.post('/api/servers/:guildId/admin-logs-settings', async (req, res) => {
try {
const { guildId } = req.params;
const newSettings = req.body || {};
const existing = (await pgClient.getServerSettings(guildId)) || {};
const merged = { ...existing };
merged.adminLogs = {
enabled: newSettings.enabled || false,
channelId: newSettings.channelId || '',
commands: newSettings.commands || { kick: true, ban: true, timeout: true }
};
await pgClient.upsertServerSettings(guildId, merged);
// Notify bot of settings change
if (bot && bot.setGuildSettings) {
bot.setGuildSettings(guildId, merged);
}
// If a remote bot push URL is configured, notify it with the new settings
if (process.env.BOT_PUSH_URL) {
try {
const headers = {};
if (process.env.INTERNAL_API_KEY) {
headers['x-api-key'] = process.env.INTERNAL_API_KEY;
}
await axios.post(`${process.env.BOT_PUSH_URL.replace(/\/$/, '')}/internal/set-settings`, { guildId, settings: merged }, { headers });
} catch (e) {
console.error('Failed to push admin logs settings to bot:', e.message);
}
}
res.json({ success: true, settings: merged.adminLogs });
} catch (error) {
console.error('Error saving admin logs settings:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.get('/api/servers/:guildId/admin-logs', async (req, res) => {
try {
const { guildId } = req.params;
const { action, limit } = req.query;
const limitNum = limit ? parseInt(limit) : 50;
let logs;
if (action) {
logs = await pgClient.getAdminLogsByAction(guildId, action, limitNum);
} else {
logs = await pgClient.getAdminLogs(guildId, limitNum);
}
// Transform snake_case to camelCase for frontend compatibility
logs = logs.map(log => ({
id: log.id,
guildId: log.guild_id,
action: log.action,
targetUserId: log.target_user_id,
targetUsername: log.target_username,
moderatorUserId: log.moderator_user_id,
moderatorUsername: log.moderator_username,
reason: log.reason,
duration: log.duration,
endDate: log.end_date,
timestamp: log.timestamp
}));
res.json(logs);
} catch (error) {
console.error('Error fetching admin logs:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.delete('/api/servers/:guildId/admin-logs/:logId', async (req, res) => {
try {
const { guildId, logId } = req.params;
await pgClient.deleteAdminLog(guildId, parseInt(logId));
// Publish SSE event for live updates
publishEvent(guildId, 'adminLogDeleted', { logId: parseInt(logId) });
res.json({ success: true });
} catch (error) {
console.error('Error deleting admin log:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.delete('/api/servers/:guildId/admin-logs', async (req, res) => {
try {
const { guildId } = req.params;
await pgClient.deleteAllAdminLogs(guildId);
// Publish SSE event for live updates
publishEvent(guildId, 'adminLogsCleared', {});
res.json({ success: true });
} catch (error) {
console.error('Error deleting all admin logs:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
// Internal endpoint for logging moderation actions
app.post('/internal/log-moderation', express.json(), async (req, res) => {
try {
const { guildId, action, targetUserId, targetUsername, moderatorUserId, moderatorUsername, reason, duration, endDate } = req.body;
if (!guildId || !action || !targetUserId || !moderatorUserId || !reason) {
return res.status(400).json({ success: false, message: 'Missing required fields' });
}
// Save to database
await pgClient.addAdminLog({
guildId,
action,
targetUserId,
targetUsername,
moderatorUserId,
moderatorUsername,
reason,
duration,
endDate
});
// Check if logging is enabled for this action and send to Discord channel
const settings = (await pgClient.getServerSettings(guildId)) || {};
const adminLogs = settings.adminLogs || { enabled: false, channelId: '', commands: { kick: true, ban: true, timeout: true } };
if (adminLogs.enabled && adminLogs.channelId && adminLogs.commands[action]) {
const guild = bot.client.guilds.cache.get(guildId);
if (guild) {
const channel = guild.channels.cache.get(adminLogs.channelId);
if (channel && channel.type === 0) { // GUILD_TEXT
const embed = {
color: action === 'kick' ? 0xffa500 : action === 'ban' ? 0xff0000 : 0x0000ff,
title: `🚨 ${action.charAt(0).toUpperCase() + action.slice(1)} Action`,
fields: [
{
name: '👤 Target',
value: `${targetUsername} (${targetUserId})`,
inline: true
},
{
name: '👮 Moderator',
value: `${moderatorUsername} (${moderatorUserId})`,
inline: true
},
{
name: '📝 Reason',
value: reason,
inline: false
}
],
timestamp: new Date().toISOString(),
footer: {
text: 'ECS Admin Logs'
}
};
if (duration) {
embed.fields.push({
name: '⏱️ Duration',
value: duration,
inline: true
});
}
if (endDate) {
embed.fields.push({
name: '📅 End Date',
value: new Date(endDate).toLocaleString(),
inline: true
});
}
try {
await channel.send({ embeds: [embed] });
} catch (error) {
console.error('Failed to send admin log to Discord:', error);
}
}
}
}
// Publish SSE event for live updates
publishEvent(guildId, 'adminLogAdded', {
log: {
guildId,
action,
targetUserId,
targetUsername,
moderatorUserId,
moderatorUsername,
reason,
duration,
endDate,
timestamp: new Date().toISOString()
}
});
res.json({ success: true });
} catch (error) {
console.error('Error logging moderation action:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
// MODERATION: frontend moderation actions
app.post('/api/servers/:guildId/moderate', express.json(), async (req, res) => {
try {
const { guildId } = req.params;
const { action, target, reason, duration, moderator } = req.body;
if (!action || !target || !reason) {
return res.status(400).json({ success: false, message: 'Missing required fields: action, target, reason' });
}
// Validate reason has at least 3 words
const reasonWords = reason.trim().split(/\s+/);
if (reasonWords.length < 3) {
return res.status(400).json({ success: false, message: 'Reason must be at least 3 words long' });
}
const guild = bot.client.guilds.cache.get(guildId);
if (!guild) {
return res.status(404).json({ success: false, message: 'Guild not found' });
}
// Find the target user
let targetUser = null;
let targetMember = null;
// Try to find by ID first
try {
targetUser = await bot.client.users.fetch(target);
targetMember = guild.members.cache.get(target);
} catch (e) {
// Try to find by username/mention
const members = await guild.members.fetch();
targetMember = members.find(m =>
m.user.username.toLowerCase().includes(target.toLowerCase()) ||
m.user.tag.toLowerCase().includes(target.toLowerCase()) ||
(target.startsWith('<@') && target.includes(m.user.id))
);
if (targetMember) {
targetUser = targetMember.user;
}
}
if (!targetUser) {
return res.status(404).json({ success: false, message: 'User not found in this server' });
}
// Perform the moderation action
let result = null;
let durationString = null;
let endDate = null;
switch (action) {
case 'kick':
if (!targetMember) {
return res.status(400).json({ success: false, message: 'User is not in this server' });
}
result = await targetMember.kick(reason);
break;
case 'ban':
result = await guild.members.ban(targetUser, { reason });
break;
case 'timeout':
if (!targetMember) {
return res.status(400).json({ success: false, message: 'User is not in this server' });
}
if (!duration || duration < 1 || duration > 40320) {
return res.status(400).json({ success: false, message: 'Invalid timeout duration (1-40320 minutes)' });
}
const timeoutMs = duration * 60 * 1000;
endDate = new Date(Date.now() + timeoutMs);
result = await targetMember.timeout(timeoutMs, reason);
// Format duration string
if (duration >= 1440) {
durationString = `${Math.floor(duration / 1440)}d ${Math.floor((duration % 1440) / 60)}h ${duration % 60}m`;
} else if (duration >= 60) {
durationString = `${Math.floor(duration / 60)}h ${duration % 60}m`;
} else {
durationString = `${duration}m`;
}
break;
default:
return res.status(400).json({ success: false, message: 'Invalid action' });
}
// Log the moderation action
const moderatorUsername = moderator ? (moderator.global_name || moderator.username || 'Unknown User') : 'Web Interface';
try {
const logData = {
guildId,
action,
targetUserId: targetUser.id,
targetUsername: targetUser.global_name || targetUser.username || 'Unknown User',
moderatorUserId: moderator?.id || 'web-interface',
moderatorUsername,
reason,
duration: durationString,
endDate
};
await fetch(`${BACKEND_BASE}/internal/log-moderation`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(logData)
});
} catch (logError) {
console.error('Failed to log moderation action:', logError);
}
res.json({ success: true, message: `${action} action completed successfully` });
} catch (error) {
console.error('Error performing moderation action:', error);
res.status(500).json({ success: false, message: error.message || 'Internal server error' });
}
});
const bot = require('../discord-bot');
bot.login();