feat: /rename command (#583)

* feat(rename): add /rename command

- implement the /rename command to allow staff to change the name of a topic
- validate that the new name is between 1 and 100 characters
- return appropriate success or error messages based on the validation

* refactor: ??

* fix(i18n): lowercase command name

* fix: consistency

* style: sort & format `en-GB.yml`

* feat: log rename

---------

Co-authored-by: Isaac <git@eartharoid.me>
This commit is contained in:
Ole 2025-02-11 18:28:04 +01:00 committed by GitHub
parent dcf1c83228
commit aae41ffee3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 193 additions and 5 deletions

View File

@ -0,0 +1,166 @@
const { SlashCommand } = require('@eartharoid/dbf');
const ExtendedEmbedBuilder = require('../../lib/embed');
const {
MessageFlags,
ApplicationCommandOptionType,
} = require('discord.js');
const { isStaff } = require('../../lib/users');
const ms = require('ms');
const { logTicketEvent } = require('../../lib/logging');
module.exports = class RenameSlashCommand extends SlashCommand {
constructor(client, options) {
const name = 'rename';
super(client, {
...options,
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
descriptionLocalisations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
dmPermission: false,
name,
nameLocalisations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
options: [
{
name: 'name',
required: true,
type: ApplicationCommandOptionType.String,
},
].map(option => {
option.descriptionLocalisations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
option.description = option.descriptionLocalisations['en-GB'];
option.nameLocalisations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
return option;
}),
});
}
/**
* Handle the 'rename' command
* @param {import("discord.js").ChatInputCommandInteraction} interaction
*/
async run(interaction) {
/** @type {import("client")} */
const client = this.client;
// Defer the reply while processing the request
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
// Fetch the necessary ticket data for the channel
const ticket = await client.prisma.ticket.findUnique({
include: { guild: true },
where: { id: interaction.channel.id },
});
// If no ticket found for the channel, return an error
if (!ticket) {
// Fetch guild settings
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
const getMessage = client.i18n.getLocale(settings.locale);
return await interaction.editReply({
embeds: [
new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(),
text: settings.footer,
})
.setColor(settings.errorColour)
.setTitle(getMessage('misc.not_ticket.title'))
.setDescription(getMessage('misc.not_ticket.description')),
],
});
}
const getMessage = client.i18n.getLocale(ticket.guild.locale);
// Check if the user has permission to rename the channel
if (
ticket.id !== interaction.channel.id &&
ticket.createdById !== interaction.member.id &&
!(await isStaff(interaction.guild, interaction.member.id))
) {
return await interaction.editReply({
embeds: [
new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(),
text: ticket.guild.footer,
})
.setColor(ticket.guild.errorColour)
.setTitle(getMessage('commands.slash.rename.not_staff.title'))
.setDescription(getMessage('commands.slash.rename.not_staff.description')),
],
});
}
const { name: originalName } = interaction.channel;
const name = interaction.options.getString('name'); // Get the new name from the user's input
// Validate the new name length (must be between 1 and 100 characters)
if (name.length < 1 || name.length > 100) {
return await interaction.editReply({
embeds: [
new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(),
text: ticket.guild.footer,
})
.setColor(ticket.guild.errorColour)
.setTitle(getMessage('commands.slash.rename.invalid.title'))
.setDescription(getMessage('commands.slash.rename.invalid.description')),
],
});
}
// Check for rate limit for renaming the channel (allowing 2 renames every 10 minutes)
const rateLimitKey = `rate-limits/channel-rename:${interaction.channel.id}`;
let renameTimestamps = await this.client.keyv.get(rateLimitKey) ?? [];
// Remove any timestamps older than 10 minutes
renameTimestamps = renameTimestamps.filter(timestamp => Date.now() - timestamp < ms('10m'));
if (renameTimestamps.length >= 2) {
// If two renames have already occurred in the last 10 minutes, return rate limited
return await interaction.editReply({
embeds: [
new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(),
text: ticket.guild.footer,
})
.setColor(ticket.guild.errorColour)
.setTitle(getMessage('commands.slash.rename.ratelimited.title'))
.setDescription(getMessage('commands.slash.rename.ratelimited.description')),
],
ephemeral: true,
});
}
// Add the current timestamp to the array
renameTimestamps.push(Date.now());
await this.client.keyv.set(rateLimitKey, renameTimestamps, ms('10m'));
// Proceed with renaming the channel
await interaction.channel.edit({ name });
// Respond with a success message
await interaction.editReply({
embeds: [
new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(),
text: ticket.guild.footer,
})
.setColor(ticket.guild.successColour)
.setTitle(getMessage('commands.slash.rename.success.title'))
.setDescription(getMessage('commands.slash.rename.success.description', { name })),
],
});
logTicketEvent(this.client, {
action: 'update',
diff: {
original: { name: originalName },
updated: { name },
},
target: {
id: ticket.id,
name: `<#${ticket.id}>`,
},
userId: interaction.user.id,
});
}
};

View File

@ -23,12 +23,12 @@ buttons:
reject_close_request: reject_close_request:
emoji: ✖️ emoji: ✖️
text: Reject text: Reject
unclaim:
emoji: ♻️
text: Release
transcript: transcript:
emoji: 📄 emoji: 📄
text: Transcript text: Transcript
unclaim:
emoji: ♻️
text: Release
commands: commands:
message: message:
create: create:
@ -190,6 +190,28 @@ commands:
success: success:
description: "{member} has been removed from {ticket}." description: "{member} has been removed from {ticket}."
title: ✅ Removed title: ✅ Removed
rename:
description: Rename a ticket channel
error: There was an error while renaming the channel.
invalid:
description: The name must be between 1 and 100 characters in length.
title: Invalid name
name: rename
not_staff:
description: Only staff members can rename tickets.
title: ❌ Error
options:
name:
description: The new name for the ticket channel.
name: name
ratelimited:
description:
You have already renamed this channel twice within the last 10
minutes. Please wait before trying again.
title: Rate Limit Reached
success:
description: The ticket channel has been renamed to `{name}`.
title: Channel renamed
tag: tag:
description: Use a tag description: Use a tag
name: tag name: tag
@ -383,8 +405,8 @@ misc:
for_admins: for_admins:
name: For server administrators name: For server administrators
value: > value: >
An invalid user or role was supplied, which usually means a staff role has been deleted. An invalid user or role was supplied, which usually means a staff role
[Click here]({url}) for resolution instructions. has been deleted. [Click here]({url}) for resolution instructions.
title: ⚠️ Something went wrong title: ⚠️ Something went wrong
unknown_category: unknown_category:
description: Please try a different category. description: Please try a different category.