blob: 3c2dd2cb9e325d590b370a205d6861632072ab23 [file] [log] [blame]
PineaFan538d3752023-01-12 21:48:23 +00001import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, ButtonInteraction } from "discord.js";
pineafan813bdf42022-07-24 10:39:10 +01002import { tickets, toHexArray } from "../../utils/calculate.js";
3import client from "../../utils/client.js";
4import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
5import getEmojiByName from "../../utils/getEmojiByName.js";
PineaFan0d06edc2023-01-17 22:10:31 +00006import { getCommandMentionByName } from "../../utils/getCommandMentionByName.js";
pineafan813bdf42022-07-24 10:39:10 +01007
8function capitalize(s: string) {
pineafan63fc5e22022-08-04 22:04:10 +01009 s = s.replace(/([A-Z])/g, " $1");
PineaFan0d06edc2023-01-17 22:10:31 +000010 return s.length < 3 ? s.toUpperCase() : s[0]!.toUpperCase() + s.slice(1).toLowerCase();
pineafan813bdf42022-07-24 10:39:10 +010011}
12
PineaFan538d3752023-01-12 21:48:23 +000013export default async function (interaction: CommandInteraction | ButtonInteraction) {
PineaFana00db1b2023-01-02 15:32:54 +000014 if (!interaction.guild) return;
Skyler Grey11236ba2022-08-08 21:13:33 +010015 const { log, NucleusColors, entry, renderUser, renderChannel, renderDelta } = client.logger;
pineafan63fc5e22022-08-04 22:04:10 +010016 const config = await client.database.guilds.read(interaction.guild.id);
pineafan813bdf42022-07-24 10:39:10 +010017 if (!config.tickets.enabled || !config.tickets.category) {
Skyler Grey75ea9172022-08-06 10:22:23 +010018 return await interaction.reply({
19 embeds: [
20 new EmojiEmbed()
21 .setTitle("Tickets are disabled")
Skyler Grey11236ba2022-08-08 21:13:33 +010022 .setDescription("Please enable tickets in the configuration to use this command.")
Skyler Grey75ea9172022-08-06 10:22:23 +010023 .setFooter({
PineaFan0d06edc2023-01-17 22:10:31 +000024 text: (interaction.member!.permissions as Discord.PermissionsBitField).has("ManageGuild")
Skyler Grey75ea9172022-08-06 10:22:23 +010025 ? "You can enable it by running /settings tickets"
26 : ""
27 })
28 .setStatus("Danger")
29 .setEmoji("CONTROL.BLOCKCROSS")
30 ],
31 ephemeral: true
32 });
pineafan813bdf42022-07-24 10:39:10 +010033 }
pineafan813bdf42022-07-24 10:39:10 +010034 let count = 0;
PineaFan0d06edc2023-01-17 22:10:31 +000035 const targetChannel: Discord.CategoryChannel | Discord.TextChannel = (await interaction.guild.channels.fetch(config.tickets.category))! as Discord.CategoryChannel | Discord.TextChannel;
36 if (targetChannel.type === Discord.ChannelType.GuildCategory) {
37 // For channels, the topic is the user ID, then the word Active
38 const category = targetChannel as Discord.CategoryChannel;
39 category.children.cache.forEach((element) => {
40 if (!(element.type === Discord.ChannelType.GuildText)) return;
41 if (((element as Discord.TextChannel).topic ?? "").includes(`${interaction.member!.user.id}`)) {
42 if (((element as Discord.TextChannel).topic ?? "").endsWith("Active")) { count++; }
pineafan813bdf42022-07-24 10:39:10 +010043 }
PineaFan0d06edc2023-01-17 22:10:31 +000044 });
45 } else {
46 // For threads, the name is the users name, id, then the word Active
47 const channel = targetChannel as Discord.TextChannel;
48 channel.threads.cache.forEach((element: Discord.ThreadChannel) => {
49 if (element.name.includes(`${interaction.member!.user.id}`)) {
50 if (element.name.endsWith("Active")) { count++; }
51 }
52 });
53 }
pineafan813bdf42022-07-24 10:39:10 +010054 if (count >= config.tickets.maxTickets) {
Skyler Grey75ea9172022-08-06 10:22:23 +010055 return await interaction.reply({
56 embeds: [
57 new EmojiEmbed()
58 .setTitle("Create Ticket")
59 .setDescription(
60 `You have reached the maximum amount of tickets (${config.tickets.maxTickets}). Please close one of your active tickets before creating a new one.`
61 )
62 .setStatus("Danger")
63 .setEmoji("CONTROL.BLOCKCROSS")
64 ],
65 ephemeral: true
66 });
pineafan813bdf42022-07-24 10:39:10 +010067 }
PineaFan0d06edc2023-01-17 22:10:31 +000068 let ticketTypes: string[];
pineafan63fc5e22022-08-04 22:04:10 +010069 let custom = false;
Skyler Grey75ea9172022-08-06 10:22:23 +010070 if (config.tickets.customTypes && config.tickets.useCustom) {
71 ticketTypes = config.tickets.customTypes;
72 custom = true;
Skyler Grey11236ba2022-08-08 21:13:33 +010073 } else if (config.tickets.types) ticketTypes = toHexArray(config.tickets.types, tickets);
pineafan813bdf42022-07-24 10:39:10 +010074 else ticketTypes = [];
PineaFan0d06edc2023-01-17 22:10:31 +000075 let chosenType: string | null;
76 let splitFormattedTicketTypes: ActionRowBuilder<ButtonBuilder>[] = [];
pineafan813bdf42022-07-24 10:39:10 +010077 if (ticketTypes.length > 0) {
78 let formattedTicketTypes = [];
Skyler Grey75ea9172022-08-06 10:22:23 +010079 formattedTicketTypes = ticketTypes.map((type) => {
pineafan813bdf42022-07-24 10:39:10 +010080 if (custom) {
TheCodedProf21c08592022-09-13 14:14:43 -040081 return new ButtonBuilder().setLabel(type).setStyle(ButtonStyle.Primary).setCustomId(type);
pineafan813bdf42022-07-24 10:39:10 +010082 } else {
TheCodedProf21c08592022-09-13 14:14:43 -040083 return new ButtonBuilder()
pineafan813bdf42022-07-24 10:39:10 +010084 .setLabel(capitalize(type))
TheCodedProf21c08592022-09-13 14:14:43 -040085 .setStyle(ButtonStyle.Primary)
pineafan813bdf42022-07-24 10:39:10 +010086 .setCustomId(type)
Skyler Grey11236ba2022-08-08 21:13:33 +010087 .setEmoji(getEmojiByName("TICKETS." + type.toString().toUpperCase(), "id"));
pineafan813bdf42022-07-24 10:39:10 +010088 }
89 });
90 for (let i = 0; i < formattedTicketTypes.length; i += 5) {
PineaFan0d06edc2023-01-17 22:10:31 +000091 splitFormattedTicketTypes.push(new ActionRowBuilder<ButtonBuilder>().addComponents(formattedTicketTypes.slice(i, i + 5)));
pineafan813bdf42022-07-24 10:39:10 +010092 }
Skyler Grey75ea9172022-08-06 10:22:23 +010093 const m = await interaction.reply({
94 embeds: [
95 new EmojiEmbed()
96 .setTitle("Create Ticket")
97 .setDescription("Select a ticket type")
98 .setStatus("Success")
99 .setEmoji("GUILD.TICKET.OPEN")
100 ],
101 ephemeral: true,
102 fetchReply: true,
103 components: splitFormattedTicketTypes
104 });
pineafan813bdf42022-07-24 10:39:10 +0100105 let component;
106 try {
PineaFan0d06edc2023-01-17 22:10:31 +0000107 component = await m.awaitMessageComponent({
108 time: 300000,
109 filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
110 });
pineafan813bdf42022-07-24 10:39:10 +0100111 } catch (e) {
112 return;
113 }
114 chosenType = component.customId;
115 splitFormattedTicketTypes = [];
116 formattedTicketTypes = [];
Skyler Grey75ea9172022-08-06 10:22:23 +0100117 formattedTicketTypes = ticketTypes.map((type) => {
pineafan813bdf42022-07-24 10:39:10 +0100118 if (custom) {
TheCodedProf21c08592022-09-13 14:14:43 -0400119 return new ButtonBuilder()
pineafan813bdf42022-07-24 10:39:10 +0100120 .setLabel(type)
TheCodedProf21c08592022-09-13 14:14:43 -0400121 .setStyle(chosenType === type ? ButtonStyle.Success : ButtonStyle.Secondary)
pineafan813bdf42022-07-24 10:39:10 +0100122 .setCustomId(type)
pineafan63fc5e22022-08-04 22:04:10 +0100123 .setDisabled(true);
124 } else {
TheCodedProf21c08592022-09-13 14:14:43 -0400125 return new ButtonBuilder()
pineafan813bdf42022-07-24 10:39:10 +0100126 .setLabel(capitalize(type))
TheCodedProf21c08592022-09-13 14:14:43 -0400127 .setStyle(chosenType === type ? ButtonStyle.Success : ButtonStyle.Secondary)
pineafan813bdf42022-07-24 10:39:10 +0100128 .setCustomId(type)
Skyler Grey11236ba2022-08-08 21:13:33 +0100129 .setEmoji(getEmojiByName("TICKETS." + type.toString().toUpperCase(), "id"))
pineafan63fc5e22022-08-04 22:04:10 +0100130 .setDisabled(true);
pineafan813bdf42022-07-24 10:39:10 +0100131 }
132 });
133 for (let i = 0; i < formattedTicketTypes.length; i += 5) {
PineaFan0d06edc2023-01-17 22:10:31 +0000134 splitFormattedTicketTypes.push(new ActionRowBuilder<ButtonBuilder>().addComponents(formattedTicketTypes.slice(i, i + 5)));
pineafan813bdf42022-07-24 10:39:10 +0100135 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100136 component.update({
137 embeds: [
138 new EmojiEmbed()
139 .setTitle("Create Ticket")
140 .setDescription("Select a ticket type")
141 .setStatus("Success")
142 .setEmoji("GUILD.TICKET.OPEN")
143 ],
144 components: splitFormattedTicketTypes
145 });
pineafan813bdf42022-07-24 10:39:10 +0100146 } else {
pineafan63fc5e22022-08-04 22:04:10 +0100147 chosenType = null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100148 await interaction.reply({
Skyler Grey11236ba2022-08-08 21:13:33 +0100149 embeds: [new EmojiEmbed().setTitle("Create Ticket").setEmoji("GUILD.TICKET.OPEN")],
Skyler Grey75ea9172022-08-06 10:22:23 +0100150 ephemeral: true,
151 components: splitFormattedTicketTypes
152 });
pineafan813bdf42022-07-24 10:39:10 +0100153 }
PineaFan0d06edc2023-01-17 22:10:31 +0000154 let c: Discord.TextChannel | Discord.PrivateThreadChannel;
155 if (targetChannel.type === Discord.ChannelType.GuildCategory) {
156 const overwrites = [
157 {
158 id: interaction.member,
159 allow: ["ViewChannel", "SendMessages", "AttachFiles", "AddReactions", "ReadMessageHistory"],
160 type: Discord.OverwriteType.Member
161 }
162 ] as Discord.OverwriteResolvable[];
pineafan813bdf42022-07-24 10:39:10 +0100163 overwrites.push({
PineaFan0d06edc2023-01-17 22:10:31 +0000164 id: interaction.guild.roles.everyone,
165 deny: ["ViewChannel"],
166 type: Discord.OverwriteType.Role
pineafan63fc5e22022-08-04 22:04:10 +0100167 });
PineaFan0d06edc2023-01-17 22:10:31 +0000168 if (config.tickets.supportRole !== null) {
169 overwrites.push({
170 id: interaction.guild.roles.cache.get(config.tickets.supportRole)!,
171 allow: ["ViewChannel", "SendMessages", "AttachFiles", "AddReactions", "ReadMessageHistory"],
172 type: Discord.OverwriteType.Role
173 });
174 }
pineafan813bdf42022-07-24 10:39:10 +0100175
PineaFan0d06edc2023-01-17 22:10:31 +0000176 try {
177 c = await interaction.guild.channels.create({
178 name: `${interaction.member!.user.username.toLowerCase()}`,
179 type: Discord.ChannelType.GuildText,
180 topic: `${interaction.member!.user.id} Active`,
181 parent: config.tickets.category,
182 nsfw: false,
183 permissionOverwrites: overwrites as Discord.OverwriteResolvable[],
184 reason: "Creating ticket"
185 });
186 } catch (e) {
187 return await interaction.editReply({
188 embeds: [
189 new EmojiEmbed()
190 .setTitle("Create Ticket")
191 .setDescription("Failed to create ticket")
192 .setStatus("Danger")
193 .setEmoji("CONTROL.BLOCKCROSS")
194 ]
195 });
196 }
197 try {
198 await c.send({
199 content:
200 `<@${interaction.member!.user.id}>` +
201 (config.tickets.supportRole !== null ? ` • <@&${config.tickets.supportRole}>` : ""),
202 allowedMentions: {
203 users: [(interaction.member as Discord.GuildMember).id],
204 roles: config.tickets.supportRole !== null ? [config.tickets.supportRole] : []
205 }
206 });
207 let content: string | null = null;
208 if (interaction.isCommand()) {
209 content = interaction.options.get("message")?.value as string;
Skyler Grey75ea9172022-08-06 10:22:23 +0100210 }
PineaFan0d06edc2023-01-17 22:10:31 +0000211 if (content) content = `**Message:**\n> ${content}\n`;
212 let emoji;
213 if (chosenType) {
214 emoji = custom ? "" : getEmojiByName("TICKETS." + chosenType.toUpperCase());
215 } else {
216 emoji = "";
pineafan813bdf42022-07-24 10:39:10 +0100217 }
PineaFan0d06edc2023-01-17 22:10:31 +0000218 await c.send({
219 embeds: [
220 new EmojiEmbed()
221 .setTitle("New Ticket")
222 .setDescription(
223 `Ticket created by <@${interaction.member!.user.id}>\n` +
224 `**Support type:** ${
225 chosenType !== null ? emoji + " " + capitalize(chosenType) : "General"
226 }\n` +
227 `**Ticket ID:** \`${c.id}\`\n${content ?? ""}\n` +
228 `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.`
229 )
230 .setStatus("Success")
231 .setEmoji("GUILD.TICKET.OPEN")
232 ],
233 components: [
234 new ActionRowBuilder<ButtonBuilder>().addComponents([
235 new ButtonBuilder()
236 .setLabel("Close")
237 .setStyle(ButtonStyle.Danger)
238 .setCustomId("closeticket")
239 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
240 ])
241 ]
242 });
243 } catch (e) {
244 return await interaction.editReply({
245 embeds: [
246 new EmojiEmbed()
247 .setTitle("Create Ticket")
248 .setDescription("Failed to create ticket")
249 .setStatus("Danger")
250 .setEmoji("GUILD.TICKET.CLOSE")
251 ]
252 });
253 }
254 } else {
255 c = await targetChannel.threads.create({name: `${interaction.member!.user.username} - ${interaction.member!.user.id} - Active`,
256 autoArchiveDuration: 60 * 24 * 7,
257 type: Discord.ChannelType.PrivateThread,
258 reason: "Creating ticket"
259 }) as Discord.PrivateThreadChannel;
260 c.members.add(interaction.member!.user.id); // TODO: When a thread is used, and a support role is added, automatically set channel permissions
261 try {
262 await c.send({
263 content:
264 `<@${interaction.member!.user.id}>` +
265 (config.tickets.supportRole !== null ? ` • <@&${config.tickets.supportRole}>` : ""),
266 allowedMentions: {
267 users: [(interaction.member as Discord.GuildMember).id],
268 roles: config.tickets.supportRole !== null ? [config.tickets.supportRole] : []
269 }
270 });
271 let content: string | null = null;
272 if (interaction.isCommand()) {
273 content = interaction.options.get("message")?.value as string;
274 }
275 if (content) content = `**Message:**\n> ${content}\n`;
276 let emoji;
277 if (chosenType) {
278 emoji = custom ? "" : getEmojiByName("TICKETS." + chosenType.toUpperCase());
279 } else {
280 emoji = "";
281 }
282 await c.send({
283 embeds: [
284 new EmojiEmbed()
285 .setTitle("New Ticket")
286 .setDescription(
287 `Ticket created by <@${interaction.member!.user.id}>\n` +
288 `**Support type:** ${
289 chosenType !== null ? emoji + " " + capitalize(chosenType) : "General"
290 }\n` +
291 `**Ticket ID:** \`${c.id}\`\n${content ?? ""}\n` +
292 `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.`
293 )
294 .setStatus("Success")
295 .setEmoji("GUILD.TICKET.OPEN")
296 ],
297 components: [
298 new ActionRowBuilder<ButtonBuilder>().addComponents([
299 new ButtonBuilder()
300 .setLabel("Close")
301 .setStyle(ButtonStyle.Danger)
302 .setCustomId("closeticket")
303 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
304 ])
305 ]
306 });
307 } catch (e) {
308 return await interaction.editReply({
309 embeds: [
310 new EmojiEmbed()
311 .setTitle("Create Ticket")
312 .setDescription("Failed to create ticket")
313 .setStatus("Danger")
314 .setEmoji("GUILD.TICKET.CLOSE")
315 ]
316 });
317 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100318 }
PineaFan0d06edc2023-01-17 22:10:31 +0000319 const data = {
320 meta: {
321 type: "ticketCreate",
322 displayName: "Ticket Created",
323 calculateType: "ticketUpdate",
324 color: NucleusColors.green,
325 emoji: "GUILD.TICKET.OPEN",
326 timestamp: new Date().getTime()
327 },
328 list: {
329 ticketFor: entry(interaction.member!.user.id, renderUser(interaction.member!.user! as Discord.User)),
330 created: entry(new Date().getTime(), renderDelta(new Date().getTime())),
331 ticketChannel: entry(c.id, renderChannel(c))
332 },
333 hidden: {
334 guild: interaction.guild.id
335 }
336 };
337 log(data);
Skyler Grey75ea9172022-08-06 10:22:23 +0100338 await interaction.editReply({
339 embeds: [
340 new EmojiEmbed()
341 .setTitle("Create Ticket")
Skyler Grey11236ba2022-08-08 21:13:33 +0100342 .setDescription(`Ticket created. You can view it here: <#${c.id}>`)
Skyler Grey75ea9172022-08-06 10:22:23 +0100343 .setStatus("Success")
344 .setEmoji("GUILD.TICKET.OPEN")
345 ],
346 components: splitFormattedTicketTypes
347 });
348}