Add giveaway, ticket, self-role, starboard, and suggestion features to the bot
- Introduced new models in the Prisma schema for giveaways, tickets, self-role panels, starboard configurations, and suggestions, enhancing the bot's functionality. - Implemented job handling for self-role expiration and ticket management, improving user experience. - Updated command localization to support new features in both German and English. - Enhanced the job processing system to include dedicated workers for managing tickets and self-role expirations. - Documented the new features and their setup processes in PHASE-TRACKING.md for clarity on implementation steps.
This commit is contained in:
190
apps/bot/src/modules/tickets/commands.ts
Normal file
190
apps/bot/src/modules/tickets/commands.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
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 { ticketCommandData } from './command-definitions.js';
|
||||
import {
|
||||
addUserToTicket,
|
||||
assertTicketStaff,
|
||||
buildPanelComponents,
|
||||
buildPanelEmbed,
|
||||
claimTicket,
|
||||
closeTicket,
|
||||
createCategoryWithRole,
|
||||
findTicketByChannel,
|
||||
getGuildCategories,
|
||||
removeUserFromTicket,
|
||||
renameTicket,
|
||||
setTicketPriority,
|
||||
TicketError,
|
||||
upsertCategoryByName
|
||||
} from './service.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);
|
||||
}
|
||||
|
||||
const ticketCommand: SlashCommand = {
|
||||
data: ticketCommandData,
|
||||
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 group = interaction.options.getSubcommandGroup(false);
|
||||
const sub = interaction.options.getSubcommand(false);
|
||||
|
||||
if (group === 'panel' && sub === 'create') {
|
||||
if (!(await ensureManageGuild(interaction, locale))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = interaction.options.getString('name', true);
|
||||
const description = interaction.options.getString('description');
|
||||
await upsertCategoryByName(context, interaction.guildId!, name, description);
|
||||
|
||||
const categories = await getGuildCategories(context, interaction.guildId!);
|
||||
if (categories.length === 0) {
|
||||
await interaction.reply({ content: t(locale, 'ticket.error.no_categories'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = interaction.channel;
|
||||
if (!channel?.isTextBased() || channel.isDMBased()) {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'ticket.error.invalid_channel'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const embed = buildPanelEmbed(locale, name, description);
|
||||
const components = buildPanelComponents(categories);
|
||||
await channel.send({ embeds: [embed], components });
|
||||
await interaction.reply({ content: t(locale, 'ticket.panel.created'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (group === 'category' && sub === 'create') {
|
||||
if (!(await ensureManageGuild(interaction, locale))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = interaction.options.getString('name', true);
|
||||
const role = interaction.options.getRole('support_role', true);
|
||||
const category = await createCategoryWithRole(
|
||||
context,
|
||||
interaction.guildId!,
|
||||
name,
|
||||
role.id
|
||||
);
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'ticket.category.created', { name: category.name }),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ticket = await findTicketByChannel(context.prisma, interaction.channelId!);
|
||||
if (!ticket) {
|
||||
await interaction.reply({ content: t(locale, 'ticket.error.not_in_ticket'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||
|
||||
try {
|
||||
if (sub === 'close') {
|
||||
const isStaff = await (async () => {
|
||||
try {
|
||||
await assertTicketStaff(member, ticket);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
const isOpener = ticket.openerId === interaction.user.id;
|
||||
|
||||
if (!isStaff && !isOpener) {
|
||||
await interaction.reply({ content: t(locale, 'ticket.error.not_staff'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const reason = interaction.options.getString('reason') ?? undefined;
|
||||
await closeTicket(context, ticket, interaction.user.id, locale, reason);
|
||||
await interaction.reply({ content: t(locale, 'ticket.close.success'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'claim') {
|
||||
await claimTicket(context, ticket, member, locale);
|
||||
await interaction.reply({ content: t(locale, 'ticket.claim.success'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'add') {
|
||||
const user = interaction.options.getUser('user', true);
|
||||
const target = await interaction.guild!.members.fetch(user.id);
|
||||
await addUserToTicket(context, ticket, member, target, locale);
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'ticket.add.success', { user: `<@${user.id}>` }),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'remove') {
|
||||
const user = interaction.options.getUser('user', true);
|
||||
const target = await interaction.guild!.members.fetch(user.id);
|
||||
await removeUserFromTicket(context, ticket, member, target, locale);
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'ticket.remove.success', { user: `<@${user.id}>` }),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'rename') {
|
||||
const name = interaction.options.getString('name', true);
|
||||
await renameTicket(context, ticket, member, name, locale);
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'ticket.rename.success', { name }),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'priority') {
|
||||
const level = interaction.options.getString('level', true);
|
||||
await setTicketPriority(context, ticket, member, level, locale);
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'ticket.priority.success', {
|
||||
priority: t(locale, `ticket.priority.${level}`)
|
||||
}),
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof TicketError) {
|
||||
await interaction.reply({
|
||||
content: t(locale, `ticket.error.${error.code}`),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ticketsCommands = [ticketCommand];
|
||||
Reference in New Issue
Block a user