Skip to content

Commit 679d93c

Browse files
authored
feat[Ticket] : create Robtic Ticket system #2
Full Robtic Ticket System for support
2 parents 3543a2f + 7728796 commit 679d93c

8 files changed

Lines changed: 817 additions & 2 deletions

File tree

package-lock.json

Lines changed: 364 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"dependencies": {
3030
"chalk": "^5.4.1",
3131
"connect-mongo": "^6.0.0",
32-
"discord.js": "latest",
32+
"discord.js": "^14.25.1",
3333
"dotenv": "latest",
3434
"express": "^5.2.1",
3535
"express-session": "^1.19.0",

src/bot/ticket/commands/ticket.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { BotClient } from "@core/BotClient";
2+
import {
3+
ChatInputCommandInteraction,
4+
LabelBuilder,
5+
ModalBuilder,
6+
SlashCommandBuilder,
7+
StringSelectMenuBuilder,
8+
StringSelectMenuOptionBuilder,
9+
TextDisplayBuilder,
10+
TextInputBuilder,
11+
TextInputStyle,
12+
} from "discord.js";
13+
import { ticketCategories } from "../config/categories";
14+
15+
16+
export const ticketModal = (() => {
17+
const modalBuilder = new ModalBuilder()
18+
.setCustomId("ticket_open")
19+
.setTitle("Support Ticket");
20+
21+
const categorySelectorBuilder = new StringSelectMenuBuilder()
22+
.setCustomId("ticket_category")
23+
.setPlaceholder("Choose a ticket category")
24+
.setOptions(
25+
ticketCategories.map((c) =>
26+
new StringSelectMenuOptionBuilder()
27+
.setLabel(c.displayName)
28+
.setValue(c.id)
29+
.setDescription(c.description),
30+
),
31+
)
32+
.setRequired(true);
33+
34+
const categorySelectorLabel = new LabelBuilder()
35+
.setLabel("Choose type of support:")
36+
.setStringSelectMenuComponent(categorySelectorBuilder);
37+
38+
const descriptionBuilder = new TextInputBuilder()
39+
.setCustomId("ticket_description")
40+
.setStyle(TextInputStyle.Short)
41+
.setPlaceholder("Short description..")
42+
.setMinLength(15)
43+
.setMaxLength(80)
44+
.setRequired(true);
45+
46+
const descriptionLabel = new LabelBuilder()
47+
.setLabel("Enter the subject:")
48+
.setTextInputComponent(descriptionBuilder);
49+
50+
const footerText = new TextDisplayBuilder().setContent(
51+
`Once you press 'Submit' a channel will be created -if you were applicable for a ticket- and staff members will be with you as soon as possible. `,
52+
);
53+
54+
return modalBuilder
55+
.addLabelComponents([categorySelectorLabel,descriptionLabel])
56+
.addTextDisplayComponents(footerText);
57+
})();
58+
59+
export default {
60+
data: new SlashCommandBuilder()
61+
.setName("ticket")
62+
.setDescription("Description")
63+
.addSubcommand((sub) => sub.setName("create").setDescription("Create a new support ticket.")),
64+
// .addSubcommand((sub) => sub.setName("close").setDescription("Close your current ticket.")),
65+
66+
async run(interaction: ChatInputCommandInteraction, client: BotClient) {
67+
if (!interaction.isChatInputCommand()) return;
68+
69+
const sub = interaction.options.getSubcommand();
70+
if (sub === "create"){
71+
await interaction.showModal(ticketModal);
72+
}
73+
},
74+
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { BotClient } from "@core/BotClient";
2+
import { TicketRepository } from "@database/repositories";
3+
import {
4+
ContainerBuilder,
5+
MessageFlags,
6+
SeparatorBuilder,
7+
TextDisplayBuilder,
8+
type ButtonInteraction,
9+
type TextChannel,
10+
} from "discord.js";
11+
import {
12+
SUPPORT_REPORT_CHANNEL_ID,
13+
TICKET_CLOSED_COLOR,
14+
} from "../config/misc";
15+
import { ticketCard } from "./openTicket";
16+
17+
export async function runCloseTicket(
18+
ticketId: string,
19+
interaction: ButtonInteraction,
20+
) {
21+
const ticket = await TicketRepository.close(ticketId, interaction.user.id);
22+
// First, fetch the ticket without mutating it
23+
const existingTicket = await TicketRepository.findById(ticketId);
24+
25+
if (!existingTicket) {
26+
await interaction.reply({
27+
content: `This ticket no longer exists.`,
28+
flags: [MessageFlags.Ephemeral],
29+
});
30+
return;
31+
}
32+
33+
// Validate that the interaction was used in the correct channel
34+
if (interaction.channelId !== existingTicket.channelId) {
35+
await interaction.reply({
36+
content: `You can only close this ticket from its own channel.`,
37+
flags: [MessageFlags.Ephemeral],
38+
});
39+
return;
40+
}
41+
42+
// Acknowledge the interaction before mutating any state
43+
if (interaction.deferUpdate) {
44+
await interaction.deferUpdate();
45+
} else {
46+
await interaction.reply({
47+
content: `Your Ticket was closed !`,
48+
flags: [MessageFlags.Ephemeral],
49+
});
50+
}
51+
52+
// Now safely perform the close operation and subsequent side effects
53+
const ticket = await TicketRepository.close(ticketId, interaction.user.id);
54+
55+
const closedAtStr = ticket.closedAt
56+
? `${Math.floor(ticket.closedAt?.getTime() / 1000) ?? ""}`
57+
: null;
58+
const ticketCardString = ticketCard(
59+
ticket.userId,
60+
ticket.category,
61+
ticket.subject,
62+
);
63+
const ticketClosedString = `${
64+
ticket.closedAt
65+
? `Closed at: <t:${closedAtStr}> (<t:${closedAtStr}:R>)`
66+
: ``
67+
}
68+
Closed by: <@${ticket.closedBy}>`;
69+
70+
const reportChannel = (await interaction.guild!.channels.fetch(
71+
SUPPORT_REPORT_CHANNEL_ID,
72+
)) as TextChannel;
73+
74+
const report = new ContainerBuilder()
75+
.setAccentColor(TICKET_CLOSED_COLOR)
76+
.addTextDisplayComponents(
77+
new TextDisplayBuilder().setContent(`### Support request was closed`),
78+
)
79+
.addSeparatorComponents(new SeparatorBuilder())
80+
.addTextDisplayComponents(
81+
new TextDisplayBuilder().setContent(ticketCardString),
82+
)
83+
.addSeparatorComponents(new SeparatorBuilder())
84+
.addTextDisplayComponents(
85+
new TextDisplayBuilder().setContent(ticketClosedString),
86+
);
87+
88+
await reportChannel.send({
89+
components: [report],
90+
flags: [MessageFlags.IsComponentsV2, MessageFlags.SuppressNotifications],
91+
});
92+
}
93+
94+
export default {
95+
customId: /^ticket_close_/,
96+
97+
async run(interaction: ButtonInteraction, client: BotClient) {
98+
const [_0, _1, ticketId] = interaction.customId.split("_");
99+
100+
await runCloseTicket(ticketId, interaction);
101+
},
102+
};

0 commit comments

Comments
 (0)