From a2c3c9bff6b54b5bdd6a703c74e90249d1a539b3 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 6 Feb 2024 00:54:34 +0800 Subject: [PATCH] Add support for assigning and unassigning tickets via slash command --- .../Support Ticket/assignSupportTicket.ts | 1 + .../Support Ticket/moveSupportTicket.ts | 1 + .../Support Ticket/unassignSupportTicket.ts | 1 + .../ticket/subcommands/ticket-assign.ts | 75 +++++++++++++ .../ticket/subcommands/ticket-assigned.ts | 104 ++++++++++++++++++ .../General/ticket/subcommands/ticket-list.ts | 8 ++ .../ticket/subcommands/ticket-unassign.ts | 75 +++++++++++++ .../commands/General/ticket/ticket.ts | 69 ++++++++++++ .../General/ticket/TicketLocalization.ts | 6 + .../translations/TicketENTranslation.ts | 9 ++ 10 files changed, 349 insertions(+) create mode 100644 src/interactions/commands/General/ticket/subcommands/ticket-assign.ts create mode 100644 src/interactions/commands/General/ticket/subcommands/ticket-assigned.ts create mode 100644 src/interactions/commands/General/ticket/subcommands/ticket-unassign.ts diff --git a/src/interactions/buttons/Support Ticket/assignSupportTicket.ts b/src/interactions/buttons/Support Ticket/assignSupportTicket.ts index 0f963fb04..37ae31a0f 100644 --- a/src/interactions/buttons/Support Ticket/assignSupportTicket.ts +++ b/src/interactions/buttons/Support Ticket/assignSupportTicket.ts @@ -44,5 +44,6 @@ export const run: ButtonCommand["run"] = async (_, interaction) => { }; export const config: ButtonCommand["config"] = { + cooldown: 5, replyEphemeral: true, }; diff --git a/src/interactions/buttons/Support Ticket/moveSupportTicket.ts b/src/interactions/buttons/Support Ticket/moveSupportTicket.ts index cf01eb637..608e5d151 100644 --- a/src/interactions/buttons/Support Ticket/moveSupportTicket.ts +++ b/src/interactions/buttons/Support Ticket/moveSupportTicket.ts @@ -100,5 +100,6 @@ export const run: ButtonCommand["run"] = async (_, interaction) => { }; export const config: ButtonCommand["config"] = { + cooldown: 30, replyEphemeral: true, }; diff --git a/src/interactions/buttons/Support Ticket/unassignSupportTicket.ts b/src/interactions/buttons/Support Ticket/unassignSupportTicket.ts index e0f723d20..6f8f7bcb6 100644 --- a/src/interactions/buttons/Support Ticket/unassignSupportTicket.ts +++ b/src/interactions/buttons/Support Ticket/unassignSupportTicket.ts @@ -44,5 +44,6 @@ export const run: ButtonCommand["run"] = async (_, interaction) => { }; export const config: ButtonCommand["config"] = { + cooldown: 5, replyEphemeral: true, }; diff --git a/src/interactions/commands/General/ticket/subcommands/ticket-assign.ts b/src/interactions/commands/General/ticket/subcommands/ticket-assign.ts new file mode 100644 index 000000000..ee10937a7 --- /dev/null +++ b/src/interactions/commands/General/ticket/subcommands/ticket-assign.ts @@ -0,0 +1,75 @@ +import { Config } from "@alice-core/Config"; +import { Constants } from "@alice-core/Constants"; +import { DatabaseManager } from "@alice-database/DatabaseManager"; +import { SupportTicket } from "@alice-database/utils/aliceDb/SupportTicket"; +import { ConstantsLocalization } from "@alice-localization/core/constants/ConstantsLocalization"; +import { TicketLocalization } from "@alice-localization/interactions/commands/General/ticket/TicketLocalization"; +import { SlashSubcommand } from "@alice-structures/core/SlashSubcommand"; +import { MessageCreator } from "@alice-utils/creators/MessageCreator"; +import { CommandHelper } from "@alice-utils/helpers/CommandHelper"; +import { InteractionHelper } from "@alice-utils/helpers/InteractionHelper"; + +export const run: SlashSubcommand["run"] = async (_, interaction) => { + if (!interaction.inCachedGuild()) { + return; + } + + const language = await CommandHelper.getLocale(interaction); + + if (!interaction.member.roles.cache.hasAny(...Config.verifyPerm)) { + return InteractionHelper.reply(interaction, { + content: MessageCreator.createReject( + new ConstantsLocalization(language).getTranslation( + Constants.noPermissionReject, + ), + ), + }); + } + + await InteractionHelper.deferReply(interaction); + + const localization = new TicketLocalization(language); + const dbManager = DatabaseManager.aliceDb.collections.supportTicket; + const author = interaction.options.getUser("author"); + const ticketId = interaction.options.getInteger("id"); + + let ticket: SupportTicket | null; + + await InteractionHelper.deferReply(interaction); + + if (author !== null && ticketId !== null) { + ticket = await dbManager.getFromUser(author.id, ticketId); + } else { + ticket = await dbManager.getFromChannel(interaction.channelId); + } + + if (!ticket) { + return InteractionHelper.reply(interaction, { + content: MessageCreator.createReject( + localization.getTranslation("ticketNotFound"), + ), + }); + } + + const result = await ticket.assign(interaction.user.id); + + if (result.failed()) { + return InteractionHelper.reply(interaction, { + content: MessageCreator.createReject( + localization.getTranslation("assignTicketFailed"), + result.reason, + ), + }); + } + + InteractionHelper.reply(interaction, { + content: MessageCreator.createAccept( + localization.getTranslation("assignTicketSuccess"), + ), + }); +}; + +export const config: SlashSubcommand["config"] = { + permissions: [], + cooldown: 5, +}; diff --git a/src/interactions/commands/General/ticket/subcommands/ticket-assigned.ts b/src/interactions/commands/General/ticket/subcommands/ticket-assigned.ts new file mode 100644 index 000000000..91d281f2a --- /dev/null +++ b/src/interactions/commands/General/ticket/subcommands/ticket-assigned.ts @@ -0,0 +1,104 @@ +import { Config } from "@alice-core/Config"; +import { Constants } from "@alice-core/Constants"; +import { DatabaseManager } from "@alice-database/DatabaseManager"; +import { SupportTicketStatus } from "@alice-enums/ticket/SupportTicketStatus"; +import { ConstantsLocalization } from "@alice-localization/core/constants/ConstantsLocalization"; +import { TicketLocalization } from "@alice-localization/interactions/commands/General/ticket/TicketLocalization"; +import { SlashSubcommand } from "@alice-structures/core/SlashSubcommand"; +import { OnButtonPageChange } from "@alice-structures/utils/OnButtonPageChange"; +import { EmbedCreator } from "@alice-utils/creators/EmbedCreator"; +import { MessageButtonCreator } from "@alice-utils/creators/MessageButtonCreator"; +import { MessageCreator } from "@alice-utils/creators/MessageCreator"; +import { CommandHelper } from "@alice-utils/helpers/CommandHelper"; +import { InteractionHelper } from "@alice-utils/helpers/InteractionHelper"; +import { StringHelper } from "@alice-utils/helpers/StringHelper"; +import { hyperlink } from "discord.js"; + +export const run: SlashSubcommand["run"] = async (_, interaction) => { + if (!interaction.inCachedGuild()) { + return; + } + + const language = await CommandHelper.getLocale(interaction); + + if (!interaction.member.roles.cache.hasAny(...Config.verifyPerm)) { + return InteractionHelper.reply(interaction, { + content: MessageCreator.createReject( + new ConstantsLocalization(language).getTranslation( + Constants.noPermissionReject, + ), + ), + }); + } + + await InteractionHelper.deferReply(interaction); + + const localization = new TicketLocalization(language); + const author = interaction.options.getUser("author"); + const status = ( + interaction.options.getInteger("status") + ); + + const tickets = await DatabaseManager.aliceDb.collections.supportTicket.get( + "id", + { + assigneeIds: { + $in: [interaction.user.id], + }, + authorId: author?.id, + status: status ?? undefined, + }, + ); + + if (tickets.size === 0) { + return InteractionHelper.reply(interaction, { + content: MessageCreator.createReject( + localization.getTranslation("noTicketsAssigned"), + ), + }); + } + + const ticketArray = [...tickets.values()]; + const ticketsPerPage = 5; + const embed = EmbedCreator.createNormalEmbed({ + author: interaction.user, + color: interaction.member.displayColor, + }); + + embed.setTitle( + StringHelper.formatString( + localization.getTranslation("assignedTicketListEmbedTitle"), + interaction.user.username, + ), + ); + + const onPageChange: OnButtonPageChange = async (_, page) => { + const tickets = ticketArray.slice( + ticketsPerPage * page, + ticketsPerPage * (page + 1), + ); + + for (let i = 0; i < tickets.length; ++i) { + const ticket = tickets[i]; + + embed.addFields({ + name: `${ticketsPerPage * page + i + 1}. ${ticket.title}`, + value: `${localization.getTranslation("ticketStatus")}: ${ticket.statusToString()} | ${hyperlink(localization.getTranslation("ticketGoToChannel"), ticket.threadChannelURL)}`, + }); + } + }; + + MessageButtonCreator.createLimitedButtonBasedPaging( + interaction, + { embeds: [embed] }, + [interaction.user.id], + 1, + Math.ceil(tickets.size / ticketsPerPage), + 120, + onPageChange, + ); +}; + +export const config: SlashSubcommand["config"] = { + permissions: ["Special"], +}; diff --git a/src/interactions/commands/General/ticket/subcommands/ticket-list.ts b/src/interactions/commands/General/ticket/subcommands/ticket-list.ts index 64a47c8e9..123521228 100644 --- a/src/interactions/commands/General/ticket/subcommands/ticket-list.ts +++ b/src/interactions/commands/General/ticket/subcommands/ticket-list.ts @@ -45,6 +45,14 @@ export const run: SlashSubcommand["run"] = async (_, interaction) => { authorId: author.id, status: status ?? undefined, }, + { + projection: { + id: 1, + title: 1, + status: 1, + threadChannelId: 1, + }, + }, ); if (tickets.size === 0) { diff --git a/src/interactions/commands/General/ticket/subcommands/ticket-unassign.ts b/src/interactions/commands/General/ticket/subcommands/ticket-unassign.ts new file mode 100644 index 000000000..30248244f --- /dev/null +++ b/src/interactions/commands/General/ticket/subcommands/ticket-unassign.ts @@ -0,0 +1,75 @@ +import { Config } from "@alice-core/Config"; +import { Constants } from "@alice-core/Constants"; +import { DatabaseManager } from "@alice-database/DatabaseManager"; +import { SupportTicket } from "@alice-database/utils/aliceDb/SupportTicket"; +import { ConstantsLocalization } from "@alice-localization/core/constants/ConstantsLocalization"; +import { TicketLocalization } from "@alice-localization/interactions/commands/General/ticket/TicketLocalization"; +import { SlashSubcommand } from "@alice-structures/core/SlashSubcommand"; +import { MessageCreator } from "@alice-utils/creators/MessageCreator"; +import { CommandHelper } from "@alice-utils/helpers/CommandHelper"; +import { InteractionHelper } from "@alice-utils/helpers/InteractionHelper"; + +export const run: SlashSubcommand["run"] = async (_, interaction) => { + if (!interaction.inCachedGuild()) { + return; + } + + const language = await CommandHelper.getLocale(interaction); + + if (!interaction.member.roles.cache.hasAny(...Config.verifyPerm)) { + return InteractionHelper.reply(interaction, { + content: MessageCreator.createReject( + new ConstantsLocalization(language).getTranslation( + Constants.noPermissionReject, + ), + ), + }); + } + + await InteractionHelper.deferReply(interaction); + + const localization = new TicketLocalization(language); + const dbManager = DatabaseManager.aliceDb.collections.supportTicket; + const author = interaction.options.getUser("author"); + const ticketId = interaction.options.getInteger("id"); + + let ticket: SupportTicket | null; + + await InteractionHelper.deferReply(interaction); + + if (author !== null && ticketId !== null) { + ticket = await dbManager.getFromUser(author.id, ticketId); + } else { + ticket = await dbManager.getFromChannel(interaction.channelId); + } + + if (!ticket) { + return InteractionHelper.reply(interaction, { + content: MessageCreator.createReject( + localization.getTranslation("ticketNotFound"), + ), + }); + } + + const result = await ticket.unassign(interaction.user.id); + + if (result.failed()) { + return InteractionHelper.reply(interaction, { + content: MessageCreator.createReject( + localization.getTranslation("unassignTicketFailed"), + result.reason, + ), + }); + } + + InteractionHelper.reply(interaction, { + content: MessageCreator.createAccept( + localization.getTranslation("unassignTicketSuccess"), + ), + }); +}; + +export const config: SlashSubcommand["config"] = { + permissions: [], + cooldown: 5, +}; diff --git a/src/interactions/commands/General/ticket/ticket.ts b/src/interactions/commands/General/ticket/ticket.ts index 2e0608ab3..2b3224a58 100644 --- a/src/interactions/commands/General/ticket/ticket.ts +++ b/src/interactions/commands/General/ticket/ticket.ts @@ -17,6 +17,55 @@ export const config: SlashCommand["config"] = { name: "ticket", description: "Primary interface of the ticket system.", options: [ + { + name: "assign", + type: ApplicationCommandOptionType.Subcommand, + description: "Assigns yourself to a ticket.", + options: [ + { + name: "author", + type: ApplicationCommandOptionType.User, + description: + "The user who opened the ticket. If unspecified, will default to the ticket in the channel.", + }, + { + name: "id", + type: ApplicationCommandOptionType.Integer, + minValue: 1, + description: + "The ID of the ticket. If unspecified, will default to the ticket in the channel.", + }, + ], + }, + { + name: "assigned", + type: ApplicationCommandOptionType.Subcommand, + description: "Lists all tickets that you are assigned to.", + options: [ + { + name: "author", + type: ApplicationCommandOptionType.User, + description: + "The ticket author to list for. If unspecified, all ticket authors will be listed.", + }, + { + name: "status", + type: ApplicationCommandOptionType.Integer, + description: + "The ticket status to filter for. If unspecified, no status filter is applied.", + choices: [ + { + name: "Open", + value: SupportTicketStatus.open, + }, + { + name: "Closed", + value: SupportTicketStatus.closed, + }, + ], + }, + ], + }, { name: "close", type: ApplicationCommandOptionType.Subcommand, @@ -149,6 +198,26 @@ export const config: SlashCommand["config"] = { }, ], }, + { + name: "unassign", + type: ApplicationCommandOptionType.Subcommand, + description: "Unassigns a yourself from a ticket.", + options: [ + { + name: "author", + type: ApplicationCommandOptionType.User, + description: + "The user who opened the ticket. If unspecified, will default to the ticket in the channel.", + }, + { + name: "id", + type: ApplicationCommandOptionType.Integer, + minValue: 1, + description: + "The ID of the ticket. If unspecified, will default to the ticket in the channel.", + }, + ], + }, { name: "view", type: ApplicationCommandOptionType.Subcommand, diff --git a/src/localization/interactions/commands/General/ticket/TicketLocalization.ts b/src/localization/interactions/commands/General/ticket/TicketLocalization.ts index d6d20c2bf..cd70787cf 100644 --- a/src/localization/interactions/commands/General/ticket/TicketLocalization.ts +++ b/src/localization/interactions/commands/General/ticket/TicketLocalization.ts @@ -6,6 +6,7 @@ export interface TicketStrings { readonly ticketNotFound: string; readonly presetNotFound: string; readonly noTicketsFound: string; + readonly noTicketsAssigned: string; readonly ticketEditModalTitle: string; readonly ticketCreateModalTitle: string; readonly ticketModalTitleLabel: string; @@ -20,7 +21,12 @@ export interface TicketStrings { readonly moveTicketConfirm: string; readonly moveTicketFailed: string; readonly moveTicketSuccess: string; + readonly assignTicketFailed: string; + readonly assignTicketSuccess: string; + readonly unassignTicketFailed: string; + readonly unassignTicketSuccess: string; readonly ticketListEmbedTitle: string; + readonly assignedTicketListEmbedTitle: string; readonly ticketStatus: string; readonly ticketGoToChannel: string; } diff --git a/src/localization/interactions/commands/General/ticket/translations/TicketENTranslation.ts b/src/localization/interactions/commands/General/ticket/translations/TicketENTranslation.ts index 5039276e8..aa20f124d 100644 --- a/src/localization/interactions/commands/General/ticket/translations/TicketENTranslation.ts +++ b/src/localization/interactions/commands/General/ticket/translations/TicketENTranslation.ts @@ -9,6 +9,8 @@ export class TicketENTranslation extends Translation { ticketNotFound: "I'm sorry, I could not find the ticket!", presetNotFound: "I'm sorry, I could not find the preset!", noTicketsFound: "I'm sorry, I could not find any tickets!", + noTicketsAssigned: + "I'm sorry, you do not have any assigned tickets that match your filter!", ticketEditModalTitle: "Edit Ticket", ticketCreateModalTitle: "Create Ticket", ticketModalTitleLabel: "Title", @@ -24,7 +26,14 @@ export class TicketENTranslation extends Translation { moveTicketConfirm: "Are you sure you want to move this ticket to %s?", moveTicketFailed: "I'm sorry, I could not move the ticket: %s.", moveTicketSuccess: "Successfully moved the ticket to %s.", + assignTicketFailed: + "I'm sorry, I could not assign you to the ticket: %s.", + unassignTicketFailed: + "I'm sorry, I could not unassign you from the ticket: %s.", + unassignTicketSuccess: "Successfully unassigned you from the ticket.", + assignTicketSuccess: "Successfully assigned you to the ticket.", ticketListEmbedTitle: "Tickets from %s", + assignedTicketListEmbedTitle: "Assigned Tickets", ticketStatus: "Status", ticketGoToChannel: "Go to Ticket Channel", };