diff --git a/commands/moderation/role.js b/commands/moderation/role.js new file mode 100644 index 0000000..f1fbbf2 --- /dev/null +++ b/commands/moderation/role.js @@ -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}.`, + }); + }, +}; diff --git a/commands/moderation/warn.js b/commands/moderation/warn.js new file mode 100644 index 0000000..6ed3617 --- /dev/null +++ b/commands/moderation/warn.js @@ -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}**.`, + }); + }, +}; diff --git a/commands/moderation/warnings.js b/commands/moderation/warnings.js new file mode 100644 index 0000000..654d7dc --- /dev/null +++ b/commands/moderation/warnings.js @@ -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}`, + }); + }, +}; diff --git a/commands/unique/guess.js b/commands/unique/guess.js new file mode 100644 index 0000000..b772e4c --- /dev/null +++ b/commands/unique/guess.js @@ -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!`, + }); + }, +}; diff --git a/services/warnings.js b/services/warnings.js new file mode 100644 index 0000000..a40904a --- /dev/null +++ b/services/warnings.js @@ -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, +};