diff --git a/commands/moderation/kick.js b/commands/moderation/kick.js index ce811c2..e10c0c4 100644 --- a/commands/moderation/kick.js +++ b/commands/moderation/kick.js @@ -1,4 +1,5 @@ const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js"); +const { recordModerationAction } = require('../../services/moderationRecords'); module.exports = { permissionGroup: 'moderation', @@ -31,6 +32,14 @@ module.exports = { } await member.kick(reason); + await recordModerationAction({ + guildId: interaction.guildId, + userId: user.id, + action: 'kick', + moderatorId: interaction.user.id, + reason, + metadata: {}, + }); await interaction.reply(`Kicked ${user.tag} for: ${reason}`); } catch (error) { diff --git a/commands/moderation/mute.js b/commands/moderation/mute.js index 6551591..85ac8db 100644 --- a/commands/moderation/mute.js +++ b/commands/moderation/mute.js @@ -1,4 +1,5 @@ const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js"); +const { recordModerationAction } = require('../../services/moderationRecords'); module.exports = { permissionGroup: 'moderation', @@ -39,6 +40,15 @@ module.exports = { } await member.timeout(durationMs, reason); + await recordModerationAction({ + guildId: interaction.guildId, + userId: target.id, + action: 'mute', + moderatorId: interaction.user.id, + reason, + metadata: { durationMinutes: duration }, + stateUpdates: { mutedUntil: new Date(Date.now() + durationMs) }, + }); return interaction.reply({ content: `Muted ${target.tag} for **${duration} minutes**. Reason: ${reason}`, diff --git a/commands/moderation/temp_ban.js b/commands/moderation/temp_ban.js index 7cd2d3f..fe97802 100644 --- a/commands/moderation/temp_ban.js +++ b/commands/moderation/temp_ban.js @@ -1,5 +1,6 @@ const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require("discord.js"); const { connectToDatabase } = require('../../db.js'); +const { recordModerationAction } = require('../../services/moderationRecords'); module.exports = { mod: true, @@ -73,11 +74,24 @@ module.exports = { const tempBanCollection = db.collection("tempban"); // 3. Insert the ban record - await tempBanCollection.insertOne({ + const banRecord = { guildId: interaction.guild.id, userId: user.id, banTime: banUntil, reason: reason, + }; + await tempBanCollection.insertOne(banRecord); + await recordModerationAction({ + guildId: interaction.guild.id, + userId: user.id, + action: 'temp-ban', + moderatorId: interaction.user.id, + reason, + metadata: { durationMs: durationms }, + stateUpdates: { + isBanned: true, + bannedUntil: new Date(banUntil), + }, }); } catch (err) { console.error(err); diff --git a/commands/moderation/warn.js b/commands/moderation/warn.js index 6ed3617..79163b8 100644 --- a/commands/moderation/warn.js +++ b/commands/moderation/warn.js @@ -1,5 +1,8 @@ const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); const { addWarning } = require('../../services/warnings'); +const { getGuildSettings } = require('../../services/guildSettings'); +const { recordModerationAction } = require('../../services/moderationRecords'); +const { connectToDatabase } = require('../../db'); module.exports = { permissionGroup: 'moderation', @@ -29,13 +32,110 @@ module.exports = { }); } - const { warnings } = await addWarning({ + const { warnings, strikeCount } = await addWarning({ guildId: interaction.guildId, userId: target.id, moderatorId: interaction.user.id, reason, }); + const settings = await getGuildSettings(interaction.guildId); + const thresholds = settings.moderation?.strikeThresholds ?? {}; + const muteThreshold = Number.isInteger(thresholds.mute) ? thresholds.mute : null; + const kickThreshold = Number.isInteger(thresholds.kick) ? thresholds.kick : null; + const banThreshold = Number.isInteger(thresholds.ban) ? thresholds.ban : null; + const muteDurationMinutes = settings.moderation?.muteDurationMinutes ?? 0; + const banDurationMs = settings.moderation?.banDurationMs ?? 0; + + let actionTaken = null; + + if (banThreshold && strikeCount === banThreshold) { + actionTaken = 'ban'; + } else if (kickThreshold && strikeCount === kickThreshold) { + actionTaken = 'kick'; + } else if (muteThreshold && strikeCount === muteThreshold) { + actionTaken = 'mute'; + } + + if (actionTaken === 'mute') { + try { + const member = await interaction.guild.members.fetch(target.id); + if (member.moderatable && muteDurationMinutes > 0) { + const durationMs = muteDurationMinutes * 60 * 1000; + await member.timeout(durationMs, 'Strike threshold reached'); + await recordModerationAction({ + guildId: interaction.guildId, + userId: target.id, + action: 'mute', + moderatorId: interaction.user.id, + reason: 'Strike threshold reached', + metadata: { strikeCount, durationMinutes: muteDurationMinutes }, + stateUpdates: { mutedUntil: new Date(Date.now() + durationMs) }, + }); + } + } catch (error) { + console.error('Failed to auto-mute member:', error); + } + } + + if (actionTaken === 'kick') { + try { + const member = await interaction.guild.members.fetch(target.id); + if (member.kickable) { + await member.kick('Strike threshold reached'); + await recordModerationAction({ + guildId: interaction.guildId, + userId: target.id, + action: 'kick', + moderatorId: interaction.user.id, + reason: 'Strike threshold reached', + metadata: { strikeCount }, + }); + } + } catch (error) { + console.error('Failed to auto-kick member:', error); + } + } + + if (actionTaken === 'ban') { + try { + const banReason = 'Strike threshold reached'; + const shouldTempBan = Number.isInteger(banDurationMs) && banDurationMs > 0; + + await interaction.guild.bans.create(target.id, { + reason: banReason, + }); + + if (shouldTempBan) { + const dbClient = await connectToDatabase(); + const db = dbClient.db('discord'); + const tempBanCollection = db.collection('tempban'); + const banUntil = Date.now() + banDurationMs; + await tempBanCollection.insertOne({ + guildId: interaction.guildId, + userId: target.id, + banTime: banUntil, + reason: banReason, + }); + } + + await recordModerationAction({ + guildId: interaction.guildId, + userId: target.id, + action: 'ban', + moderatorId: interaction.user.id, + reason: banReason, + metadata: { strikeCount, banDurationMs }, + stateUpdates: { + isBanned: true, + bannedUntil: shouldTempBan ? new Date(Date.now() + banDurationMs) : null, + }, + }); + } catch (error) { + console.error('Failed to auto-ban member:', error); + } + } + return interaction.reply({ content: `${target.tag} has been warned. Total strikes: **${warnings.length}**.`, }); diff --git a/events/interactionsCreate.js b/events/interactionsCreate.js index 3f3c407..252f394 100644 --- a/events/interactionsCreate.js +++ b/events/interactionsCreate.js @@ -14,22 +14,20 @@ module.exports = { } try { - if (command.permissionGroup) { - const allowed = await isCommandAllowed({ - guildId: interaction.guildId, - userId: interaction.user.id, - member: interaction.member, - commandName: interaction.commandName, - permissionGroup: command.permissionGroup, - }); + const allowed = await isCommandAllowed({ + guildId: interaction.guildId, + userId: interaction.user.id, + member: interaction.member, + commandName: interaction.commandName, + permissionGroup: command.permissionGroup, + }); - if (!allowed) { - await interaction.reply({ - content: 'You do not have permission to use this command.', - flags: MessageFlags.Ephemeral, - }); - return; - } + if (!allowed) { + await interaction.reply({ + content: 'You do not have permission to use this command.', + flags: MessageFlags.Ephemeral, + }); + return; } await command.execute(interaction); diff --git a/events/tempban.js b/events/tempban.js index cb7d538..941412e 100644 --- a/events/tempban.js +++ b/events/tempban.js @@ -1,5 +1,6 @@ const { Events } = require('discord.js'); const { connectToDatabase } = require('../db.js'); +const { clearBanStatus } = require('../services/moderationRecords'); module.exports = { name: Events.ClientReady, @@ -30,6 +31,10 @@ module.exports = { await guild.bans.remove(banData.userId); await tempBanCollection.deleteOne({ _id: banData._id }); + await clearBanStatus({ + guildId: banData.guildId, + userId: banData.userId, + }); } catch (err) { console.error('Error unbanning member:', err); } finally { diff --git a/services/guildSettings.js b/services/guildSettings.js new file mode 100644 index 0000000..1d6cd3d --- /dev/null +++ b/services/guildSettings.js @@ -0,0 +1,48 @@ +const { connectToDatabase } = require('../db'); + +const DEFAULT_GUILD_SETTINGS = { + moderation: { + strikeThresholds: { + mute: 2, + kick: 3, + ban: 5, + }, + muteDurationMinutes: 30, + banDurationMs: 0, + }, +}; + +const normalizeGuildSettings = (settings = {}) => { + const moderation = settings.moderation ?? {}; + const strikeThresholds = moderation.strikeThresholds ?? {}; + + return { + ...DEFAULT_GUILD_SETTINGS, + ...settings, + moderation: { + ...DEFAULT_GUILD_SETTINGS.moderation, + ...moderation, + strikeThresholds: { + ...DEFAULT_GUILD_SETTINGS.moderation.strikeThresholds, + ...strikeThresholds, + }, + }, + }; +}; + +const getGuildSettings = async (guildId) => { + if (!guildId) { + return normalizeGuildSettings(); + } + + const dbClient = await connectToDatabase(); + const db = dbClient.db('discord'); + const collection = db.collection('guild_settings'); + const settings = await collection.findOne({ guildId }); + + return normalizeGuildSettings(settings); +}; + +module.exports = { + getGuildSettings, +}; diff --git a/services/moderationRecords.js b/services/moderationRecords.js new file mode 100644 index 0000000..7bf3b8b --- /dev/null +++ b/services/moderationRecords.js @@ -0,0 +1,91 @@ +const { connectToDatabase } = require('../db'); + +const MODERATION_COLLECTION = 'moderation_records'; + +const getModerationCollection = async () => { + const client = await connectToDatabase(); + return client.db('discord').collection(MODERATION_COLLECTION); +}; + +const updateStrikes = async ({ guildId, userId, strikes }) => { + const collection = await getModerationCollection(); + const now = new Date(); + + return collection.findOneAndUpdate( + { guildId, userId }, + { + $set: { + strikes, + lastWarnedAt: now, + updatedAt: now, + }, + $setOnInsert: { + guildId, + userId, + actions: [], + createdAt: now, + }, + }, + { upsert: true, returnDocument: 'after' }, + ); +}; + +const recordModerationAction = async ({ + guildId, + userId, + action, + moderatorId, + reason, + metadata = {}, + stateUpdates = {}, +}) => { + const collection = await getModerationCollection(); + const now = new Date(); + const actionEntry = { + action, + moderatorId, + reason, + metadata, + createdAt: now, + }; + + return collection.findOneAndUpdate( + { guildId, userId }, + { + $push: { actions: actionEntry }, + $set: { + lastAction: action, + lastActionAt: now, + updatedAt: now, + ...stateUpdates, + }, + $setOnInsert: { + guildId, + userId, + strikes: 0, + createdAt: now, + }, + }, + { upsert: true, returnDocument: 'after' }, + ); +}; + +const clearBanStatus = async ({ guildId, userId }) => + recordModerationAction({ + guildId, + userId, + action: 'unban', + moderatorId: null, + reason: 'Temporary ban expired', + metadata: {}, + stateUpdates: { + isBanned: false, + bannedUntil: null, + }, + }); + +module.exports = { + updateStrikes, + recordModerationAction, + clearBanStatus, +}; diff --git a/services/permissions.js b/services/permissions.js index 62109df..a443548 100644 --- a/services/permissions.js +++ b/services/permissions.js @@ -40,7 +40,7 @@ const isCommandAllowed = async ({ permissions.adminUserIds.includes(userId) || (member && memberHasRole(member, permissions.adminRoleIds)); - if (permissionGroup && isAdmin) { + if (isAdmin) { return true; } diff --git a/services/warnings.js b/services/warnings.js index a40904a..2c032d6 100644 --- a/services/warnings.js +++ b/services/warnings.js @@ -1,4 +1,5 @@ const { connectToDatabase } = require('../db'); +const { updateStrikes } = require('./moderationRecords'); const WARNING_COLLECTION = 'warnings'; @@ -25,8 +26,11 @@ const addWarning = async ({ guildId, userId, moderatorId, reason }) => { ); const warnings = result.value?.warnings ?? []; + const strikeCount = warnings.length; - return { warnings, warningEntry }; + await updateStrikes({ guildId, userId, strikes: strikeCount }); + + return { warnings, warningEntry, strikeCount }; }; const getWarnings = async ({ guildId, userId }) => {