Moderation Update
This commit is contained in:
383
backend/index.js
383
backend/index.js
@@ -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();
|
||||
|
||||
@@ -41,6 +41,22 @@ async function ensureSchema() {
|
||||
data JSONB DEFAULT '{}'
|
||||
);
|
||||
`);
|
||||
|
||||
await p.query(`
|
||||
CREATE TABLE IF NOT EXISTS admin_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
guild_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL, -- 'kick', 'ban', 'timeout'
|
||||
target_user_id TEXT NOT NULL,
|
||||
target_username TEXT NOT NULL,
|
||||
moderator_user_id TEXT NOT NULL,
|
||||
moderator_username TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
duration TEXT, -- for timeout/ban (e.g., '1d', '30m', 'permanent')
|
||||
end_date TIMESTAMP WITH TIME ZONE, -- calculated end date for timeout/ban
|
||||
timestamp TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
// Servers
|
||||
@@ -76,6 +92,46 @@ async function deleteInvite(guildId, code) {
|
||||
await p.query('DELETE FROM invites WHERE guild_id = $1 AND code = $2', [guildId, code]);
|
||||
}
|
||||
|
||||
// Admin Logs
|
||||
async function addAdminLog(logData) {
|
||||
const p = initPool();
|
||||
const q = `INSERT INTO admin_logs(guild_id, action, target_user_id, target_username, moderator_user_id, moderator_username, reason, duration, end_date)
|
||||
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)`;
|
||||
await p.query(q, [
|
||||
logData.guildId,
|
||||
logData.action,
|
||||
logData.targetUserId,
|
||||
logData.targetUsername,
|
||||
logData.moderatorUserId,
|
||||
logData.moderatorUsername,
|
||||
logData.reason,
|
||||
logData.duration || null,
|
||||
logData.endDate || null
|
||||
]);
|
||||
}
|
||||
|
||||
async function getAdminLogs(guildId, limit = 50) {
|
||||
const p = initPool();
|
||||
const res = await p.query('SELECT * FROM admin_logs WHERE guild_id = $1 ORDER BY timestamp DESC LIMIT $2', [guildId, limit]);
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
async function getAdminLogsByAction(guildId, action, limit = 50) {
|
||||
const p = initPool();
|
||||
const res = await p.query('SELECT * FROM admin_logs WHERE guild_id = $1 AND action = $2 ORDER BY timestamp DESC LIMIT $3', [guildId, action, limit]);
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
async function deleteAdminLog(guildId, logId) {
|
||||
const p = initPool();
|
||||
await p.query('DELETE FROM admin_logs WHERE guild_id = $1 AND id = $2', [guildId, logId]);
|
||||
}
|
||||
|
||||
async function deleteAllAdminLogs(guildId) {
|
||||
const p = initPool();
|
||||
await p.query('DELETE FROM admin_logs WHERE guild_id = $1', [guildId]);
|
||||
}
|
||||
|
||||
// Users
|
||||
async function getUserData(discordId) {
|
||||
const p = initPool();
|
||||
@@ -89,4 +145,4 @@ async function upsertUserData(discordId, data) {
|
||||
await p.query(`INSERT INTO users(discord_id, data) VALUES($1, $2) ON CONFLICT (discord_id) DO UPDATE SET data = $2`, [discordId, data]);
|
||||
}
|
||||
|
||||
module.exports = { initPool, ensureSchema, getServerSettings, upsertServerSettings, listInvites, addInvite, deleteInvite, getUserData, upsertUserData };
|
||||
module.exports = { initPool, ensureSchema, getServerSettings, upsertServerSettings, listInvites, addInvite, deleteInvite, getUserData, upsertUserData, addAdminLog, getAdminLogs, getAdminLogsByAction, deleteAdminLog, deleteAllAdminLogs };
|
||||
|
||||
Reference in New Issue
Block a user