Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-01-27 09:59:24 +01:00
11 changed files with 372 additions and 1 deletions
+20
View File
@@ -70,3 +70,23 @@ Im going to make a To-do List for the features i want to implement for my Bot.
2. **Monitoring**
- [ ] Add logging to track errors and usage statistics.
- [ ] Implement uptime monitoring to ensure the bot stays online.
---
### **Server-Specific Permission Configuration**
Moderation commands support per-server permission overrides stored in MongoDB. Each guild can define which users or roles are allowed to run specific commands, plus optional admin overrides that apply to all moderation commands. This is intended for future web-based configuration.
Example document stored in the `discord.guild_permissions` collection:
```json
{
"guildId": "1234567890",
"adminUserIds": ["1111111111"],
"adminRoleIds": ["2222222222"],
"commandPermissions": {
"kick": {
"allowedUserIds": ["3333333333"],
"allowedRoleIds": ["4444444444"]
}
}
}
```
+1
View File
@@ -1,6 +1,7 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
module.exports = {
permissionGroup: 'moderation',
data: new SlashCommandBuilder()
.setName("kick")
.setDescription("Kick misbehaving people")
+1
View File
@@ -1,6 +1,7 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
module.exports = {
permissionGroup: 'moderation',
data: new SlashCommandBuilder()
.setName("mute")
.setDescription("Temporarily mute a member with a timeout.")
+90
View File
@@ -0,0 +1,90 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const ensureRoleEditable = (role) => {
if (role.managed) {
return 'That role is managed by an integration and cannot be modified.';
}
if (!role.editable) {
return 'I cannot manage that role. Check my role permissions and hierarchy.';
}
return null;
};
module.exports = {
permissionGroup: 'moderation',
data: new SlashCommandBuilder()
.setName('role')
.setDescription('Assign or remove roles from members.')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
.addSubcommand(subcommand =>
subcommand
.setName('add')
.setDescription('Add a role to a member.')
.addUserOption(option =>
option
.setName('target')
.setDescription('The member to update')
.setRequired(true))
.addRoleOption(option =>
option
.setName('role')
.setDescription('The role to add')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('remove')
.setDescription('Remove a role from a member.')
.addUserOption(option =>
option
.setName('target')
.setDescription('The member to update')
.setRequired(true))
.addRoleOption(option =>
option
.setName('role')
.setDescription('The role to remove')
.setRequired(true))),
category: 'Moderation',
async execute(interaction) {
const target = interaction.options.getUser('target');
const role = interaction.options.getRole('role');
const action = interaction.options.getSubcommand();
const roleError = ensureRoleEditable(role);
if (roleError) {
return interaction.reply({ content: roleError, ephemeral: true });
}
const member = await interaction.guild.members.fetch(target.id);
if (action === 'add') {
if (member.roles.cache.has(role.id)) {
return interaction.reply({
content: `${target.tag} already has ${role.name}.`,
ephemeral: true,
});
}
await member.roles.add(role);
return interaction.reply({
content: `Added ${role.name} to ${target.tag}.`,
});
}
if (!member.roles.cache.has(role.id)) {
return interaction.reply({
content: `${target.tag} does not have ${role.name}.`,
ephemeral: true,
});
}
await member.roles.remove(role);
return interaction.reply({
content: `Removed ${role.name} from ${target.tag}.`,
});
},
};
+1
View File
@@ -3,6 +3,7 @@ const { connectToDatabase } = require('../../db.js');
module.exports = {
mod: true,
permissionGroup: 'moderation',
data: new SlashCommandBuilder()
.setName("temp-ban")
.setDescription("Temporarily ban a member.")
+43
View File
@@ -0,0 +1,43 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const { addWarning } = require('../../services/warnings');
module.exports = {
permissionGroup: 'moderation',
data: new SlashCommandBuilder()
.setName('warn')
.setDescription('Warn a member and track strikes.')
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
.addUserOption(option =>
option
.setName('target')
.setDescription('The member to warn')
.setRequired(true))
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason for the warning')
.setRequired(true)),
category: 'Moderation',
async execute(interaction) {
const target = interaction.options.getUser('target');
const reason = interaction.options.getString('reason');
if (!interaction.guildId) {
return interaction.reply({
content: 'Warnings can only be issued inside a server.',
ephemeral: true,
});
}
const { warnings } = await addWarning({
guildId: interaction.guildId,
userId: target.id,
moderatorId: interaction.user.id,
reason,
});
return interaction.reply({
content: `${target.tag} has been warned. Total strikes: **${warnings.length}**.`,
});
},
};
+52
View File
@@ -0,0 +1,52 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const { getWarnings } = require('../../services/warnings');
const formatWarning = (warning, index) => {
const date = new Date(warning.createdAt);
const timestamp = Number.isNaN(date.getTime())
? 'Unknown date'
: date.toLocaleString();
return `${index + 1}. ${warning.reason} (by <@${warning.moderatorId}> on ${timestamp})`;
};
module.exports = {
permissionGroup: 'moderation',
data: new SlashCommandBuilder()
.setName('warnings')
.setDescription('View a member warning history.')
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
.addUserOption(option =>
option
.setName('target')
.setDescription('The member to check')
.setRequired(true)),
category: 'Moderation',
async execute(interaction) {
const target = interaction.options.getUser('target');
if (!interaction.guildId) {
return interaction.reply({
content: 'Warnings can only be checked inside a server.',
ephemeral: true,
});
}
const warnings = await getWarnings({
guildId: interaction.guildId,
userId: target.id,
});
if (warnings.length === 0) {
return interaction.reply({
content: `${target.tag} has no warnings.`,
});
}
const recentWarnings = warnings.slice(-5).map(formatWarning).join('\n');
return interaction.reply({
content: `Warnings for ${target.tag} (total ${warnings.length}):\n${recentWarnings}`,
});
},
};
+29
View File
@@ -0,0 +1,29 @@
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('guess')
.setDescription('Try to guess a number between 1 and 10.')
.addIntegerOption(option =>
option
.setName('number')
.setDescription('Your guess (1-10)')
.setMinValue(1)
.setMaxValue(10)
.setRequired(true)),
category: 'Unique',
async execute(interaction) {
const guess = interaction.options.getInteger('number');
const answer = Math.floor(Math.random() * 10) + 1;
if (guess === answer) {
return interaction.reply({
content: `🎉 Correct! The number was **${answer}**.`,
});
}
return interaction.reply({
content: `Not quite! The number was **${answer}**. Try again!`,
});
},
};
+19
View File
@@ -1,4 +1,5 @@
const { Events, MessageFlags } = require('discord.js');
const { isCommandAllowed } = require('../services/permissions');
module.exports = {
name: Events.InteractionCreate,
@@ -13,6 +14,24 @@ 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,
});
if (!allowed) {
await interaction.reply({
content: 'You do not have permission to use this command.',
flags: MessageFlags.Ephemeral,
});
return;
}
}
await command.execute(interaction);
} catch (error) {
console.error(error);
+73
View File
@@ -0,0 +1,73 @@
const { connectToDatabase } = require('../db');
const DEFAULT_GUILD_PERMISSIONS = {
adminRoleIds: [],
adminUserIds: [],
commandPermissions: {},
};
const normalizePermissionConfig = (config = {}) => ({
...DEFAULT_GUILD_PERMISSIONS,
...config,
adminRoleIds: Array.isArray(config.adminRoleIds) ? config.adminRoleIds : [],
adminUserIds: Array.isArray(config.adminUserIds) ? config.adminUserIds : [],
commandPermissions: config.commandPermissions ?? {},
});
const memberHasRole = (member, roleIds) =>
roleIds.some((roleId) => member.roles?.cache?.has(roleId));
const getGuildPermissions = async (guildId) => {
const dbClient = await connectToDatabase();
const db = dbClient.db('discord');
const collection = db.collection('guild_permissions');
const config = await collection.findOne({ guildId });
return normalizePermissionConfig(config);
};
const isCommandAllowed = async ({
guildId,
userId,
member,
commandName,
permissionGroup,
}) => {
if (!guildId) return true;
const permissions = await getGuildPermissions(guildId);
const isAdmin =
permissions.adminUserIds.includes(userId) ||
(member && memberHasRole(member, permissions.adminRoleIds));
if (permissionGroup && isAdmin) {
return true;
}
const commandPermissions = permissions.commandPermissions?.[commandName];
if (!commandPermissions) {
return true;
}
const allowedUserIds = Array.isArray(commandPermissions.allowedUserIds)
? commandPermissions.allowedUserIds
: [];
const allowedRoleIds = Array.isArray(commandPermissions.allowedRoleIds)
? commandPermissions.allowedRoleIds
: [];
if (allowedUserIds.length === 0 && allowedRoleIds.length === 0) {
return true;
}
if (allowedUserIds.includes(userId)) {
return true;
}
return member ? memberHasRole(member, allowedRoleIds) : false;
};
module.exports = {
getGuildPermissions,
isCommandAllowed,
};
+42
View File
@@ -0,0 +1,42 @@
const { connectToDatabase } = require('../db');
const WARNING_COLLECTION = 'warnings';
const getWarningsCollection = async () => {
const client = await connectToDatabase();
return client.db('discord').collection(WARNING_COLLECTION);
};
const addWarning = async ({ guildId, userId, moderatorId, reason }) => {
const collection = await getWarningsCollection();
const warningEntry = {
reason,
moderatorId,
createdAt: new Date(),
};
const result = await collection.findOneAndUpdate(
{ guildId, userId },
{
$push: { warnings: warningEntry },
$setOnInsert: { guildId, userId },
},
{ upsert: true, returnDocument: 'after' },
);
const warnings = result.value?.warnings ?? [];
return { warnings, warningEntry };
};
const getWarnings = async ({ guildId, userId }) => {
const collection = await getWarningsCollection();
const record = await collection.findOne({ guildId, userId });
return record?.warnings ?? [];
};
module.exports = {
addWarning,
getWarnings,
};