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

172
discord-bot/commands/ban.js Normal file
View File

@@ -0,0 +1,172 @@
const { SlashCommandBuilder, PermissionsBitField } = require('discord.js');
// Helper function to parse user from mention or ID
function parseUser(input, guild) {
// Check if it's a mention <@123456> or <@!123456>
const mentionMatch = input.match(/^<@!?(\d+)>$/);
if (mentionMatch) {
return guild.members.cache.get(mentionMatch[1])?.user;
}
// Check if it's a user ID
if (/^\d{15,20}$/.test(input)) {
return guild.members.cache.get(input)?.user;
}
// Try to find by username or global name
const member = guild.members.cache.find(m =>
(m.user.global_name && m.user.global_name.toLowerCase().includes(input.toLowerCase())) ||
m.user.username.toLowerCase().includes(input.toLowerCase()) ||
(m.user.global_name && m.user.global_name.toLowerCase() === input.toLowerCase()) ||
m.user.username.toLowerCase() === input.toLowerCase()
);
return member?.user;
}
// Helper function to log moderation actions
async function logModerationAction(guildId, action, targetUserId, targetUsername, moderatorUserId, moderatorUsername, reason, duration = null, endDate = null) {
try {
const logData = {
guildId,
action,
targetUserId,
targetUsername,
moderatorUserId,
moderatorUsername,
reason,
duration,
endDate
};
const response = await fetch(`${process.env.BACKEND_BASE || 'http://localhost:3001'}/internal/log-moderation`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(logData)
});
if (!response.ok) {
console.error('Failed to log moderation action:', response.statusText);
}
} catch (error) {
console.error('Error logging moderation action:', error);
}
}
module.exports = {
name: 'ban',
description: 'Ban a user from the server',
enabled: true,
builder: new SlashCommandBuilder()
.setName('ban')
.setDescription('Ban a user from the server')
.addStringOption(option =>
option.setName('user')
.setDescription('The user to ban (mention or user ID)')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Reason for the ban (minimum 3 words)')
.setRequired(true))
.addIntegerOption(option =>
option.setName('days')
.setDescription('Number of days of messages to delete (0-7)')
.setRequired(false)
.setMinValue(0)
.setMaxValue(7)),
async execute(interaction) {
// Check if user has ban permissions
if (!interaction.member.permissions.has(PermissionsBitField.Flags.BanMembers)) {
return await interaction.reply({
content: 'You do not have permission to ban members.',
flags: 64
});
}
// Check if bot has ban permissions
if (!interaction.guild.members.me.permissions.has(PermissionsBitField.Flags.BanMembers)) {
return await interaction.reply({
content: 'I do not have permission to ban members.',
flags: 64
});
}
const userInput = interaction.options.getString('user');
const reason = interaction.options.getString('reason');
const days = interaction.options.getInteger('days') || 0;
// Parse the user from the input
const user = parseUser(userInput, interaction.guild);
if (!user) {
return await interaction.reply({
content: 'Could not find that user. Please provide a valid user mention or user ID.',
flags: 64
});
}
// Validate reason has at least 3 words
const reasonWords = reason.trim().split(/\s+/);
if (reasonWords.length < 3) {
return await interaction.reply({
content: 'Reason must be at least 3 words long.',
flags: 64
});
}
// Cannot ban yourself
if (user.id === interaction.user.id) {
return await interaction.reply({
content: 'You cannot ban yourself.',
flags: 64
});
}
// Cannot ban the bot
if (user.id === interaction.guild.members.me.id) {
return await interaction.reply({
content: 'I cannot ban myself.',
flags: 64
});
}
// Check if user is in the server
const member = interaction.guild.members.cache.get(user.id);
if (member) {
// Check role hierarchy
if (member.roles.highest.position >= interaction.member.roles.highest.position && interaction.user.id !== interaction.guild.ownerId) {
return await interaction.reply({
content: 'You cannot ban a member with a higher or equal role.',
flags: 64
});
}
if (member.roles.highest.position >= interaction.guild.members.me.roles.highest.position) {
return await interaction.reply({
content: 'I cannot ban a member with a higher or equal role.',
flags: 64
});
}
}
try {
await interaction.guild.members.ban(user, {
reason: reason,
deleteMessageDays: days
});
await interaction.reply({
content: `Successfully banned ${user.global_name || user.username} for: ${reason}${days > 0 ? ` (deleted ${days} days of messages)` : ''}`,
flags: 64
});
// Log the action
await logModerationAction(interaction.guildId, 'ban', user.id, user.global_name || user.username, interaction.user.id, interaction.user.global_name || interaction.user.username, reason, 'permanent');
} catch (error) {
console.error('Error banning user:', error);
await interaction.reply({
content: 'Failed to ban the user. Please try again.',
flags: 64
});
}
}
};

View File

@@ -0,0 +1,167 @@
const { SlashCommandBuilder, PermissionsBitField } = require('discord.js');
// Helper function to parse user from mention or ID
function parseUser(input, guild) {
// Check if it's a mention <@123456> or <@!123456>
const mentionMatch = input.match(/^<@!?(\d+)>$/);
if (mentionMatch) {
return guild.members.cache.get(mentionMatch[1])?.user;
}
// Check if it's a user ID
if (/^\d{15,20}$/.test(input)) {
return guild.members.cache.get(input)?.user;
}
// Try to find by username or global name
const member = guild.members.cache.find(m =>
(m.user.global_name && m.user.global_name.toLowerCase().includes(input.toLowerCase())) ||
m.user.username.toLowerCase().includes(input.toLowerCase()) ||
(m.user.global_name && m.user.global_name.toLowerCase() === input.toLowerCase()) ||
m.user.username.toLowerCase() === input.toLowerCase()
);
return member?.user;
}
// Helper function to log moderation actions
async function logModerationAction(guildId, action, targetUserId, targetUsername, moderatorUserId, moderatorUsername, reason, duration = null, endDate = null) {
try {
const logData = {
guildId,
action,
targetUserId,
targetUsername,
moderatorUserId,
moderatorUsername,
reason,
duration,
endDate
};
const response = await fetch(`${process.env.BACKEND_BASE || 'http://localhost:3001'}/internal/log-moderation`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(logData)
});
if (!response.ok) {
console.error('Failed to log moderation action:', response.statusText);
}
} catch (error) {
console.error('Error logging moderation action:', error);
}
}
module.exports = {
name: 'kick',
description: 'Kick a user from the server',
enabled: true,
builder: new SlashCommandBuilder()
.setName('kick')
.setDescription('Kick a user from the server')
.addStringOption(option =>
option.setName('user')
.setDescription('The user to kick (mention or user ID)')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Reason for the kick (minimum 3 words)')
.setRequired(true)),
async execute(interaction) {
// Check if user has kick permissions
if (!interaction.member.permissions.has(PermissionsBitField.Flags.KickMembers)) {
return await interaction.reply({
content: 'You do not have permission to kick members.',
flags: 64
});
}
// Check if bot has kick permissions
if (!interaction.guild.members.me.permissions.has(PermissionsBitField.Flags.KickMembers)) {
return await interaction.reply({
content: 'I do not have permission to kick members.',
flags: 64
});
}
const userInput = interaction.options.getString('user');
const reason = interaction.options.getString('reason');
// Parse the user from the input
const user = parseUser(userInput, interaction.guild);
if (!user) {
return await interaction.reply({
content: 'Could not find that user. Please provide a valid user mention or user ID.',
flags: 64
});
}
// Validate reason has at least 3 words
const reasonWords = reason.trim().split(/\s+/);
if (reasonWords.length < 3) {
return await interaction.reply({
content: 'Reason must be at least 3 words long.',
flags: 64
});
}
// Cannot kick yourself
if (user.id === interaction.user.id) {
return await interaction.reply({
content: 'You cannot kick yourself.',
flags: 64
});
}
// Cannot kick the bot
if (user.id === interaction.guild.members.me.id) {
return await interaction.reply({
content: 'I cannot kick myself.',
flags: 64
});
}
// Check if user is in the server
const member = interaction.guild.members.cache.get(user.id);
if (!member) {
return await interaction.reply({
content: 'That user is not in this server.',
flags: 64
});
}
// Check role hierarchy
if (member.roles.highest.position >= interaction.member.roles.highest.position && interaction.user.id !== interaction.guild.ownerId) {
return await interaction.reply({
content: 'You cannot kick a member with a higher or equal role.',
flags: 64
});
}
if (member.roles.highest.position >= interaction.guild.members.me.roles.highest.position) {
return await interaction.reply({
content: 'I cannot kick a member with a higher or equal role.',
flags: 64
});
}
try {
await member.kick(reason);
await interaction.reply({
content: `Successfully kicked ${user.global_name || user.username} for: ${reason}`,
flags: 64
});
// Log the action
await logModerationAction(interaction.guildId, 'kick', user.id, user.global_name || user.username, interaction.user.id, interaction.user.global_name || interaction.user.username, reason);
} catch (error) {
console.error('Error kicking user:', error);
await interaction.reply({
content: 'Failed to kick the user. Please try again.',
flags: 64
});
}
}
};

View File

@@ -0,0 +1,123 @@
const { SlashCommandBuilder, PermissionsBitField } = require('discord.js');
module.exports = {
name: 'setup-adminlogs',
description: 'Configure admin moderation logging settings',
enabled: true,
builder: new SlashCommandBuilder()
.setName('setup-adminlogs')
.setDescription('Configure admin moderation logging settings')
.addChannelOption(option =>
option.setName('channel')
.setDescription('Channel to send admin logs to')
.setRequired(false))
.addBooleanOption(option =>
option.setName('enabled')
.setDescription('Enable or disable admin logging')
.setRequired(false))
.addBooleanOption(option =>
option.setName('kick_logs')
.setDescription('Log kick actions')
.setRequired(false))
.addBooleanOption(option =>
option.setName('ban_logs')
.setDescription('Log ban actions')
.setRequired(false))
.addBooleanOption(option =>
option.setName('timeout_logs')
.setDescription('Log timeout actions')
.setRequired(false)),
async execute(interaction) {
// Check if user has administrator permissions
if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
return interaction.reply({
content: 'You need Administrator permissions to configure admin logs.',
flags: 64
});
}
const channel = interaction.options.getChannel('channel');
const enabled = interaction.options.getBoolean('enabled');
const kickLogs = interaction.options.getBoolean('kick_logs');
const banLogs = interaction.options.getBoolean('ban_logs');
const timeoutLogs = interaction.options.getBoolean('timeout_logs');
try {
// Get current settings
const response = await fetch(`${process.env.BACKEND_BASE || 'http://localhost:3001'}/api/servers/${interaction.guildId}/admin-logs-settings`);
if (!response.ok) {
throw new Error('Failed to fetch current settings');
}
const currentSettings = await response.json();
// Update settings
const updatedSettings = { ...currentSettings };
if (enabled !== null) {
updatedSettings.enabled = enabled;
}
if (channel) {
// Check if it's a text channel
if (channel.type !== 0) { // 0 = GUILD_TEXT
return interaction.reply({
content: 'Please select a text channel for admin logs.',
flags: 64
});
}
updatedSettings.channelId = channel.id;
}
// Update command-specific settings
updatedSettings.commands = { ...updatedSettings.commands };
if (kickLogs !== null) {
updatedSettings.commands.kick = kickLogs;
}
if (banLogs !== null) {
updatedSettings.commands.ban = banLogs;
}
if (timeoutLogs !== null) {
updatedSettings.commands.timeout = timeoutLogs;
}
// Save settings
const saveResponse = await fetch(`${process.env.BACKEND_BASE || 'http://localhost:3001'}/api/servers/${interaction.guildId}/admin-logs-settings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedSettings)
});
if (!saveResponse.ok) {
throw new Error('Failed to save settings');
}
const result = await saveResponse.json();
// Create response message
let responseMessage = 'Admin logs settings updated!\n\n';
responseMessage += `**Enabled:** ${result.settings.enabled ? 'Yes' : 'No'}\n`;
if (result.settings.channelId) {
responseMessage += `**Channel:** <#${result.settings.channelId}>\n`;
} else {
responseMessage += `**Channel:** Not set\n`;
}
responseMessage += `**Kick Logs:** ${result.settings.commands.kick ? 'Enabled' : 'Disabled'}\n`;
responseMessage += `**Ban Logs:** ${result.settings.commands.ban ? 'Enabled' : 'Disabled'}\n`;
responseMessage += `**Timeout Logs:** ${result.settings.commands.timeout ? 'Enabled' : 'Disabled'}`;
await interaction.reply({
content: responseMessage,
flags: 64
});
} catch (error) {
console.error('Error configuring admin logs:', error);
await interaction.reply({
content: 'An error occurred while configuring admin logs. Please try again later.',
flags: 64
});
}
}
};

View File

@@ -0,0 +1,187 @@
const { SlashCommandBuilder, PermissionsBitField } = require('discord.js');
// Helper function to parse user from mention or ID
function parseUser(input, guild) {
// Check if it's a mention <@123456> or <@!123456>
const mentionMatch = input.match(/^<@!?(\d+)>$/);
if (mentionMatch) {
return guild.members.cache.get(mentionMatch[1])?.user;
}
// Check if it's a user ID
if (/^\d{15,20}$/.test(input)) {
return guild.members.cache.get(input)?.user;
}
// Try to find by username or global name
const member = guild.members.cache.find(m =>
(m.user.global_name && m.user.global_name.toLowerCase().includes(input.toLowerCase())) ||
m.user.username.toLowerCase().includes(input.toLowerCase()) ||
(m.user.global_name && m.user.global_name.toLowerCase() === input.toLowerCase()) ||
m.user.username.toLowerCase() === input.toLowerCase()
);
return member?.user;
}
// Helper function to log moderation actions
async function logModerationAction(guildId, action, targetUserId, targetUsername, moderatorUserId, moderatorUsername, reason, duration = null, endDate = null) {
try {
const logData = {
guildId,
action,
targetUserId,
targetUsername,
moderatorUserId,
moderatorUsername,
reason,
duration,
endDate
};
const response = await fetch(`${process.env.BACKEND_BASE || 'http://localhost:3001'}/internal/log-moderation`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(logData)
});
if (!response.ok) {
console.error('Failed to log moderation action:', response.statusText);
}
} catch (error) {
console.error('Error logging moderation action:', error);
}
}
module.exports = {
name: 'timeout',
description: 'Timeout a user in the server',
enabled: true,
builder: new SlashCommandBuilder()
.setName('timeout')
.setDescription('Timeout a user in the server')
.addStringOption(option =>
option.setName('user')
.setDescription('The user to timeout (mention or user ID)')
.setRequired(true))
.addIntegerOption(option =>
option.setName('duration')
.setDescription('Duration in minutes (max 40320 minutes = 28 days)')
.setRequired(true)
.setMinValue(1)
.setMaxValue(40320))
.addStringOption(option =>
option.setName('reason')
.setDescription('Reason for the timeout (minimum 3 words)')
.setRequired(true)),
async execute(interaction) {
// Check if user has moderate members permissions (required for timeout)
if (!interaction.member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) {
return await interaction.reply({
content: 'You do not have permission to timeout members.',
flags: 64
});
}
// Check if bot has moderate members permissions
if (!interaction.guild.members.me.permissions.has(PermissionsBitField.Flags.ModerateMembers)) {
return await interaction.reply({
content: 'I do not have permission to timeout members.',
flags: 64
});
}
const userInput = interaction.options.getString('user');
const duration = interaction.options.getInteger('duration');
const reason = interaction.options.getString('reason');
// Parse the user from the input
const user = parseUser(userInput, interaction.guild);
if (!user) {
return await interaction.reply({
content: 'Could not find that user. Please provide a valid user mention or user ID.',
flags: 64
});
}
// Validate reason has at least 3 words
const reasonWords = reason.trim().split(/\s+/);
if (reasonWords.length < 3) {
return await interaction.reply({
content: 'Reason must be at least 3 words long.',
flags: 64
});
}
// Cannot timeout yourself
if (user.id === interaction.user.id) {
return await interaction.reply({
content: 'You cannot timeout yourself.',
flags: 64
});
}
// Cannot timeout the bot
if (user.id === interaction.guild.members.me.id) {
return await interaction.reply({
content: 'I cannot timeout myself.',
flags: 64
});
}
// Check if user is in the server
const member = interaction.guild.members.cache.get(user.id);
if (!member) {
return await interaction.reply({
content: 'That user is not in this server.',
flags: 64
});
}
// Check role hierarchy
if (member.roles.highest.position >= interaction.member.roles.highest.position && interaction.user.id !== interaction.guild.ownerId) {
return await interaction.reply({
content: 'You cannot timeout a member with a higher or equal role.',
flags: 64
});
}
if (member.roles.highest.position >= interaction.guild.members.me.roles.highest.position) {
return await interaction.reply({
content: 'I cannot timeout a member with a higher or equal role.',
flags: 64
});
}
// Check if user is already timed out
if (member.communicationDisabledUntil) {
return await interaction.reply({
content: 'This user is already timed out.',
flags: 64
});
}
try {
const timeoutDuration = duration * 60 * 1000; // Convert minutes to milliseconds
const timeoutUntil = new Date(Date.now() + timeoutDuration);
await member.timeout(timeoutDuration, reason);
await interaction.reply({
content: `Successfully timed out ${user.global_name || user.username} for ${duration} minutes. Reason: ${reason}`,
flags: 64
});
// Log the action
const durationString = duration >= 1440 ? `${Math.floor(duration / 1440)}d ${Math.floor((duration % 1440) / 60)}h ${duration % 60}m` :
duration >= 60 ? `${Math.floor(duration / 60)}h ${duration % 60}m` : `${duration}m`;
await logModerationAction(interaction.guildId, 'timeout', user.id, user.global_name || user.username, interaction.user.id, interaction.user.global_name || interaction.user.username, reason, durationString, timeoutUntil);
} catch (error) {
console.error('Error timing out user:', error);
await interaction.reply({
content: 'Failed to timeout the user. Please try again.',
flags: 64
});
}
}
};

View File

@@ -37,4 +37,24 @@ const deployCommands = async (guildId) => {
}
};
// Standalone execution
if (require.main === module) {
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once('ready', async () => {
console.log(`Logged in as ${client.user.tag}`);
console.log(`Deploying commands to ${client.guilds.cache.size} guilds...`);
for (const [guildId, guild] of client.guilds.cache) {
await deployCommands(guildId);
}
console.log('All commands deployed!');
client.destroy();
});
client.login(process.env.DISCORD_BOT_TOKEN);
}
module.exports = deployCommands;

View File

@@ -0,0 +1,15 @@
const api = require('../api');
module.exports = {
name: 'guildCreate',
execute: async (guild, client) => {
console.log(`Bot joined guild: ${guild.name} (${guild.id})`);
try {
// Publish SSE event for bot status change
await api.publishEvent('*', 'botStatusUpdate', { guildId: guild.id, isBotInServer: true });
} catch (error) {
console.error('Error publishing bot join event:', error);
}
},
};

View File

@@ -0,0 +1,15 @@
const api = require('../api');
module.exports = {
name: 'guildDelete',
execute: async (guild, client) => {
console.log(`Bot left guild: ${guild.name} (${guild.id})`);
try {
// Publish SSE event for bot status change
await api.publishEvent('*', 'botStatusUpdate', { guildId: guild.id, isBotInServer: false });
} catch (error) {
console.error('Error publishing bot leave event:', error);
}
},
};