Merge pull request #9 from Fretuks/implement-configurable-command-settings
Add per-guild moderation settings and persistent moderation records
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
|
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
|
||||||
|
const { recordModerationAction } = require('../../services/moderationRecords');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
permissionGroup: 'moderation',
|
permissionGroup: 'moderation',
|
||||||
@@ -31,6 +32,14 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await member.kick(reason);
|
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}`);
|
await interaction.reply(`Kicked ${user.tag} for: ${reason}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
|
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
|
||||||
|
const { recordModerationAction } = require('../../services/moderationRecords');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
permissionGroup: 'moderation',
|
permissionGroup: 'moderation',
|
||||||
@@ -39,6 +40,15 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await member.timeout(durationMs, reason);
|
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({
|
return interaction.reply({
|
||||||
content: `Muted ${target.tag} for **${duration} minutes**. Reason: ${reason}`,
|
content: `Muted ${target.tag} for **${duration} minutes**. Reason: ${reason}`,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require("discord.js");
|
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require("discord.js");
|
||||||
const { connectToDatabase } = require('../../db.js');
|
const { connectToDatabase } = require('../../db.js');
|
||||||
|
const { recordModerationAction } = require('../../services/moderationRecords');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mod: true,
|
mod: true,
|
||||||
@@ -73,11 +74,24 @@ module.exports = {
|
|||||||
const tempBanCollection = db.collection("tempban");
|
const tempBanCollection = db.collection("tempban");
|
||||||
|
|
||||||
// 3. Insert the ban record
|
// 3. Insert the ban record
|
||||||
await tempBanCollection.insertOne({
|
const banRecord = {
|
||||||
guildId: interaction.guild.id,
|
guildId: interaction.guild.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
banTime: banUntil,
|
banTime: banUntil,
|
||||||
reason: reason,
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
+101
-1
@@ -1,5 +1,8 @@
|
|||||||
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
|
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
|
||||||
const { addWarning } = require('../../services/warnings');
|
const { addWarning } = require('../../services/warnings');
|
||||||
|
const { getGuildSettings } = require('../../services/guildSettings');
|
||||||
|
const { recordModerationAction } = require('../../services/moderationRecords');
|
||||||
|
const { connectToDatabase } = require('../../db');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
permissionGroup: 'moderation',
|
permissionGroup: 'moderation',
|
||||||
@@ -29,13 +32,110 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { warnings } = await addWarning({
|
const { warnings, strikeCount } = await addWarning({
|
||||||
guildId: interaction.guildId,
|
guildId: interaction.guildId,
|
||||||
userId: target.id,
|
userId: target.id,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
reason,
|
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({
|
return interaction.reply({
|
||||||
content: `${target.tag} has been warned. Total strikes: **${warnings.length}**.`,
|
content: `${target.tag} has been warned. Total strikes: **${warnings.length}**.`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (command.permissionGroup) {
|
|
||||||
const allowed = await isCommandAllowed({
|
const allowed = await isCommandAllowed({
|
||||||
guildId: interaction.guildId,
|
guildId: interaction.guildId,
|
||||||
userId: interaction.user.id,
|
userId: interaction.user.id,
|
||||||
@@ -30,7 +29,6 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await command.execute(interaction);
|
await command.execute(interaction);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { Events } = require('discord.js');
|
const { Events } = require('discord.js');
|
||||||
const { connectToDatabase } = require('../db.js');
|
const { connectToDatabase } = require('../db.js');
|
||||||
|
const { clearBanStatus } = require('../services/moderationRecords');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: Events.ClientReady,
|
name: Events.ClientReady,
|
||||||
@@ -30,6 +31,10 @@ module.exports = {
|
|||||||
await guild.bans.remove(banData.userId);
|
await guild.bans.remove(banData.userId);
|
||||||
|
|
||||||
await tempBanCollection.deleteOne({ _id: banData._id });
|
await tempBanCollection.deleteOne({ _id: banData._id });
|
||||||
|
await clearBanStatus({
|
||||||
|
guildId: banData.guildId,
|
||||||
|
userId: banData.userId,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error unbanning member:', err);
|
console.error('Error unbanning member:', err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -40,7 +40,7 @@ const isCommandAllowed = async ({
|
|||||||
permissions.adminUserIds.includes(userId) ||
|
permissions.adminUserIds.includes(userId) ||
|
||||||
(member && memberHasRole(member, permissions.adminRoleIds));
|
(member && memberHasRole(member, permissions.adminRoleIds));
|
||||||
|
|
||||||
if (permissionGroup && isAdmin) {
|
if (isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const { connectToDatabase } = require('../db');
|
const { connectToDatabase } = require('../db');
|
||||||
|
const { updateStrikes } = require('./moderationRecords');
|
||||||
|
|
||||||
const WARNING_COLLECTION = 'warnings';
|
const WARNING_COLLECTION = 'warnings';
|
||||||
|
|
||||||
@@ -25,8 +26,11 @@ const addWarning = async ({ guildId, userId, moderatorId, reason }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const warnings = result.value?.warnings ?? [];
|
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 }) => {
|
const getWarnings = async ({ guildId, userId }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user