- Introduced `applyCommandCooldown` function to manage per-command cooldowns after execution, improving command rate limiting. - Updated command execution flow to include cooldown application, ensuring users are informed of cooldown status. - Enhanced error handling in interaction replies, allowing for more graceful error management and user feedback. - Improved the handling of interaction responses across various commands, ensuring consistent user experience. - Added permission checks for new commands, ensuring only authorized users can execute specific actions.
210 lines
6.2 KiB
TypeScript
210 lines
6.2 KiB
TypeScript
import { PermissionFlagsBits } from 'discord.js';
|
|
import { t, tf } from '@nexumi/shared';
|
|
import type { SlashCommand } from '../../types.js';
|
|
import { getGuildLocale } from '../../i18n.js';
|
|
import { requirePermission } from '../../permissions.js';
|
|
import { backupCommandData } from './command-definitions.js';
|
|
import {
|
|
createGuildBackup,
|
|
ensureBotCanRestore,
|
|
getGuildBackup,
|
|
GuildBackupError,
|
|
listGuildBackups,
|
|
parseBackupPayload
|
|
} from './service.js';
|
|
import { promptDeleteConfirmation, promptRestoreConfirmation } from './confirmations.js';
|
|
|
|
async function ensureManageGuild(
|
|
interaction: Parameters<SlashCommand['execute']>[0],
|
|
locale: 'de' | 'en'
|
|
): Promise<boolean> {
|
|
if (!interaction.inGuild()) {
|
|
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
|
return false;
|
|
}
|
|
return requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale);
|
|
}
|
|
|
|
function defaultBackupName(): string {
|
|
return `backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`;
|
|
}
|
|
|
|
const backupCommand: SlashCommand = {
|
|
data: backupCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!interaction.inGuild()) {
|
|
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
|
return;
|
|
}
|
|
|
|
const sub = interaction.options.getSubcommand();
|
|
const guild = interaction.guild!;
|
|
|
|
if (sub === 'create') {
|
|
if (!(await ensureManageGuild(interaction, locale))) {
|
|
return;
|
|
}
|
|
|
|
const name = interaction.options.getString('name')?.trim() || defaultBackupName();
|
|
if (name.length < 1 || name.length > 100) {
|
|
await interaction.reply({
|
|
content: t(locale, 'guildbackup.error.invalid_name'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
await interaction.deferReply({ ephemeral: true });
|
|
|
|
try {
|
|
const backup = await createGuildBackup(context, guild, name, interaction.user.id);
|
|
await interaction.editReply({
|
|
content: tf(locale, 'guildbackup.created', { id: backup.id, name: backup.name })
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof GuildBackupError) {
|
|
await interaction.editReply({
|
|
content: t(locale, `guildbackup.error.${error.code}`)
|
|
});
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (sub === 'list') {
|
|
if (!(await ensureManageGuild(interaction, locale))) {
|
|
return;
|
|
}
|
|
const backups = await listGuildBackups(context, guild.id);
|
|
if (backups.length === 0) {
|
|
await interaction.reply({
|
|
content: t(locale, 'guildbackup.list.empty'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
const lines = backups.map((backup) =>
|
|
tf(locale, 'guildbackup.list.line', {
|
|
id: backup.id,
|
|
name: backup.name,
|
|
date: backup.createdAt.toISOString().slice(0, 10)
|
|
})
|
|
);
|
|
|
|
await interaction.reply({
|
|
content: `${t(locale, 'guildbackup.list.header')}\n${lines.join('\n')}`,
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (sub === 'info') {
|
|
if (!(await ensureManageGuild(interaction, locale))) {
|
|
return;
|
|
}
|
|
const backupId = interaction.options.getString('id', true);
|
|
const backup = await getGuildBackup(context, guild.id, backupId);
|
|
if (!backup) {
|
|
await interaction.reply({
|
|
content: t(locale, 'guildbackup.error.not_found'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
let roleCount = 0;
|
|
let channelCount = 0;
|
|
try {
|
|
const payload = parseBackupPayload(backup.payload);
|
|
roleCount = payload.roles.length;
|
|
channelCount = payload.channels.length;
|
|
} catch {
|
|
await interaction.reply({
|
|
content: t(locale, 'guildbackup.error.invalid_payload'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
await interaction.reply({
|
|
content: [
|
|
tf(locale, 'guildbackup.info.id', { id: backup.id }),
|
|
tf(locale, 'guildbackup.info.name', { name: backup.name }),
|
|
tf(locale, 'guildbackup.info.created', { date: backup.createdAt.toISOString() }),
|
|
tf(locale, 'guildbackup.info.createdBy', { userId: backup.createdById }),
|
|
tf(locale, 'guildbackup.info.roles', { count: roleCount }),
|
|
tf(locale, 'guildbackup.info.channels', { count: channelCount }),
|
|
t(locale, 'guildbackup.info.limitations')
|
|
].join('\n'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (sub === 'delete') {
|
|
if (!(await ensureManageGuild(interaction, locale))) {
|
|
return;
|
|
}
|
|
|
|
const backupId = interaction.options.getString('id', true);
|
|
const backup = await getGuildBackup(context, guild.id, backupId);
|
|
if (!backup) {
|
|
await interaction.reply({
|
|
content: t(locale, 'guildbackup.error.not_found'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
await promptDeleteConfirmation(interaction, context, locale, backupId);
|
|
return;
|
|
}
|
|
|
|
if (sub === 'restore') {
|
|
if (guild.ownerId !== interaction.user.id) {
|
|
await interaction.reply({
|
|
content: t(locale, 'guildbackup.error.owner_only'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!ensureBotCanRestore(guild)) {
|
|
await interaction.reply({
|
|
content: t(locale, 'guildbackup.error.bot_missing_permissions'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
const backupId = interaction.options.getString('id', true);
|
|
const backup = await getGuildBackup(context, guild.id, backupId);
|
|
if (!backup) {
|
|
await interaction.reply({
|
|
content: t(locale, 'guildbackup.error.not_found'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
parseBackupPayload(backup.payload);
|
|
} catch {
|
|
await interaction.reply({
|
|
content: t(locale, 'guildbackup.error.invalid_payload'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
await promptRestoreConfirmation(interaction, context, locale, backupId);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const guildBackupCommands = [backupCommand];
|