blob: 16d5f3bf00dda6245feb2db41b37e1b2975af614 [file] [log] [blame]
pineafan6702cef2022-06-13 17:52:37 +01001import getEmojiByName from "../../utils/getEmojiByName.js";
pineafan4edb7762022-06-26 19:21:04 +01002import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafan6702cef2022-06-13 17:52:37 +01003import confirmationMessage from "../../utils/confirmationMessage.js";
4import Discord, { CommandInteraction, MessageActionRow, MessageButton, MessageSelectMenu, TextInputComponent } from "discord.js";
5import { SelectMenuOption, SlashCommandSubcommandBuilder } from "@discordjs/builders";
pineafan4f164f32022-02-26 22:07:12 +00006import { WrappedCheck } from "jshaiku";
pineafan1dc15722022-03-14 21:27:34 +00007import { ChannelType } from 'discord-api-types';
pineafan6702cef2022-06-13 17:52:37 +01008import client from "../../utils/client.js";
9import { toHexInteger, toHexArray, tickets as ticketTypes } from "../../utils/calculate.js";
10import { capitalize } from '../../utils/generateKeyValueList.js';
11import { modalInteractionCollector } from "../../utils/dualCollector.js";
pineafan4f164f32022-02-26 22:07:12 +000012
pineafan6702cef2022-06-13 17:52:37 +010013const command = (builder: SlashCommandSubcommandBuilder) => builder
pineafan4f164f32022-02-26 22:07:12 +000014 .setName("tickets")
pineafan6702cef2022-06-13 17:52:37 +010015 .setDescription("Shows settings for tickets | Use no arguments to manage custom types")
16 .addStringOption(option => option.setName("enabled").setDescription("If users should be able to create tickets").setRequired(false)
pineafan1dc15722022-03-14 21:27:34 +000017 .addChoices([["Yes", "yes"], ["No", "no"]]))
18 .addChannelOption(option => option.setName("category").setDescription("The category where tickets are created").addChannelType(ChannelType.GuildCategory).setRequired(false))
pineafan73a7c4a2022-07-24 10:38:04 +010019 .addNumberOption(option => option.setName("maxticketsperuser").setDescription("The maximum amount of tickets a user can create | Default: 5").setRequired(false).setMinValue(1))
pineafan6702cef2022-06-13 17:52:37 +010020 .addRoleOption(option => option.setName("supportrole").setDescription("This role will have view access to all tickets and will be pinged when a ticket is created").setRequired(false))
pineafan4f164f32022-02-26 22:07:12 +000021
pineafan6702cef2022-06-13 17:52:37 +010022const callback = async (interaction: CommandInteraction): Promise<any> => {
23 let m;
24 m = await interaction.reply({
pineafan4edb7762022-06-26 19:21:04 +010025 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +010026 .setTitle("Loading")
27 .setStatus("Danger")
28 .setEmoji("NUCLEUS.LOADING")
29 ], ephemeral: true, fetchReply: true
30 });
31 let options = {
32 enabled: interaction.options.getString("enabled") as string | boolean,
33 category: interaction.options.getChannel("category"),
34 maxtickets: interaction.options.getNumber("maxticketsperuser"),
35 supportping: interaction.options.getRole("supportrole")
36 }
37 if (options.enabled !== null || options.category || options.maxtickets || options.supportping) {
38 options.enabled = options.enabled === "yes" ? true : false;
39 if (options.category) {
40 let channel
41 try {
42 channel = interaction.guild.channels.cache.get(options.category.id)
43 } catch {
44 return await interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +010045 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +010046 .setEmoji("CHANNEL.TEXT.DELETE")
47 .setTitle("Tickets > Category")
48 .setDescription("The channel you provided is not a valid category")
49 .setStatus("Danger")
50 ]
51 })
52 }
53 channel = channel as Discord.CategoryChannel
54 if (channel.guild.id != interaction.guild.id) return interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +010055 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +010056 .setTitle("Tickets > Category")
57 .setDescription(`You must choose a category in this server`)
58 .setStatus("Danger")
59 .setEmoji("CHANNEL.TEXT.DELETE")
60 ]
61 });
62 }
63 if (options.maxtickets) {
64 if (options.maxtickets < 1) return interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +010065 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +010066 .setTitle("Tickets > Max Tickets")
67 .setDescription(`You must choose a number greater than 0`)
68 .setStatus("Danger")
69 .setEmoji("CHANNEL.TEXT.DELETE")
70 ]
71 });
72 }
73 let role
74 if (options.supportping) {
75 try {
76 role = interaction.guild.roles.cache.get(options.supportping.id)
77 } catch {
78 return await interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +010079 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +010080 .setEmoji("GUILD.ROLE.DELETE")
81 .setTitle("Tickets > Support Ping")
82 .setDescription("The role you provided is not a valid role")
83 .setStatus("Danger")
84 ]
85 })
86 }
87 role = role as Discord.Role
88 if (role.guild.id != interaction.guild.id) return interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +010089 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +010090 .setTitle("Tickets > Support Ping")
91 .setDescription(`You must choose a role in this server`)
92 .setStatus("Danger")
93 .setEmoji("GUILD.ROLE.DELETE")
94 ]
95 });
96 }
97
98 let confirmation = await new confirmationMessage(interaction)
99 .setEmoji("GUILD.TICKET.ARCHIVED")
100 .setTitle("Tickets")
101 .setDescription(
102 (options.category ? `**Category:** ${options.category.name}\n` : "") +
103 (options.maxtickets ? `**Max Tickets:** ${options.maxtickets}\n` : "") +
104 (options.supportping ? `**Support Ping:** ${options.supportping.name}\n` : "") +
105 (options.enabled !== null ? `**Enabled:** ${options.enabled ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`
106 }\n` : "") +
107 `\nAre you sure you want to apply these settings?`
108 )
109 .setColor("Warning")
110 .setInverted(true)
111 .send(true)
pineafan02ba0232022-07-24 22:16:15 +0100112 if (confirmation.cancelled) return
pineafan6702cef2022-06-13 17:52:37 +0100113 if (confirmation.success) {
114 let toUpdate = {}
115 if (options.enabled !== null) toUpdate["tickets.enabled"] = options.enabled
116 if (options.category) toUpdate["tickets.category"] = options.category.id
117 if (options.maxtickets) toUpdate["tickets.maxTickets"] = options.maxtickets
118 if (options.supportping) toUpdate["tickets.supportRole"] = options.supportping.id
119 try {
pineafan4edb7762022-06-26 19:21:04 +0100120 await client.database.guilds.write(interaction.guild.id, toUpdate)
pineafan6702cef2022-06-13 17:52:37 +0100121 } catch (e) {
122 return interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +0100123 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100124 .setTitle("Tickets")
125 .setDescription(`Something went wrong and the staff notifications channel could not be set`)
126 .setStatus("Danger")
127 .setEmoji("GUILD.TICKET.DELETE")
128 ], components: []
129 });
130 }
131 } else {
132 return interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +0100133 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100134 .setTitle("Tickets")
135 .setDescription(`No changes were made`)
136 .setStatus("Success")
137 .setEmoji("GUILD.TICKET.OPEN")
138 ], components: []
139 });
140 }
141 }
pineafan4edb7762022-06-26 19:21:04 +0100142 let data = await client.database.guilds.read(interaction.guild.id);
pineafan73a7c4a2022-07-24 10:38:04 +0100143 data.tickets.customTypes = (data.tickets.customTypes || []).filter((v, i, a) => a.indexOf(v) === i)
pineafan6702cef2022-06-13 17:52:37 +0100144 let lastClicked = "";
145 let embed;
146 data = {
147 enabled: data.tickets.enabled,
148 category: data.tickets.category,
149 maxTickets: data.tickets.maxTickets,
150 supportRole: data.tickets.supportRole,
151 useCustom: data.tickets.useCustom,
152 types: data.tickets.types,
153 customTypes: data.tickets.customTypes
154 }
155 while (true) {
pineafan4edb7762022-06-26 19:21:04 +0100156 embed = new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100157 .setTitle("Tickets")
158 .setDescription(
159 `${data.enabled ? "" : getEmojiByName("TICKETS.REPORT")} **Enabled:** ${data.enabled ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`}\n` +
160 `${data.category ? "" : getEmojiByName("TICKETS.REPORT")} **Category:** ${data.category ? `<#${data.category}>` : "*None set*"}\n` +
161 `**Max Tickets:** ${data.maxTickets ? data.maxTickets : "*No limit*"}\n` +
162 `**Support Ping:** ${data.supportRole ? `<@&${data.supportRole}>` : "*None set*"}\n\n` +
163 ((data.useCustom && data.customTypes === null) ? `${getEmojiByName("TICKETS.REPORT")} ` : "") +
164 `${data.useCustom ? "Custom" : "Default"} types in use` + "\n\n" +
165 `${getEmojiByName("TICKETS.REPORT")} *Indicates a setting stopping tickets from being used*`
166 )
167 .setStatus("Success")
168 .setEmoji("GUILD.TICKET.OPEN")
169 m = await interaction.editReply({
170 embeds: [embed], components: [new MessageActionRow().addComponents([
171 new MessageButton()
172 .setLabel("Tickets " + (data.enabled ? "enabled" : "disabled"))
173 .setEmoji(getEmojiByName("CONTROL." + (data.enabled ? "TICK" : "CROSS"), "id"))
174 .setStyle(data.enabled ? "SUCCESS" : "DANGER")
175 .setCustomId("enabled"),
176 new MessageButton()
177 .setLabel(lastClicked == "cat" ? "Click again to confirm" : "Clear category")
178 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
179 .setStyle("DANGER")
180 .setCustomId("clearCategory")
181 .setDisabled(data.category == null),
182 new MessageButton()
183 .setLabel(lastClicked == "max" ? "Click again to confirm" : "Reset max tickets")
184 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
185 .setStyle("DANGER")
186 .setCustomId("clearMaxTickets")
187 .setDisabled(data.maxTickets == 5),
188 new MessageButton()
189 .setLabel(lastClicked == "sup" ? "Click again to confirm" : "Clear support ping")
190 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
191 .setStyle("DANGER")
192 .setCustomId("clearSupportPing")
193 .setDisabled(data.supportRole == null),
194 ]), new MessageActionRow().addComponents([
195 new MessageButton()
196 .setLabel("Manage types")
197 .setEmoji(getEmojiByName("TICKETS.OTHER", "id"))
198 .setStyle("SECONDARY")
199 .setCustomId("manageTypes"),
200 ])]
201 });
202 let i;
203 try {
pineafanc6158ab2022-06-17 16:34:07 +0100204 i = await m.awaitMessageComponent({ time: 300000 });
pineafan6702cef2022-06-13 17:52:37 +0100205 } catch (e) { break }
206 i.deferUpdate()
207 if (i.component.customId == "clearCategory") {
208 if (lastClicked == "cat") {
209 lastClicked = "";
pineafan4edb7762022-06-26 19:21:04 +0100210 await client.database.guilds.write(interaction.guild.id, {}, ["tickets.category"])
pineafan6702cef2022-06-13 17:52:37 +0100211 data.category = undefined;
212 } else lastClicked = "cat";
213 } else if (i.component.customId == "clearMaxTickets") {
214 if (lastClicked == "max") {
215 lastClicked = "";
pineafan4edb7762022-06-26 19:21:04 +0100216 await client.database.guilds.write(interaction.guild.id, {}, ["tickets.maxTickets"])
pineafan6702cef2022-06-13 17:52:37 +0100217 data.maxTickets = 5;
218 } else lastClicked = "max";
219 } else if (i.component.customId == "clearSupportPing") {
220 if (lastClicked == "sup") {
221 lastClicked = "";
pineafan4edb7762022-06-26 19:21:04 +0100222 await client.database.guilds.write(interaction.guild.id, {}, ["tickets.supportRole"])
pineafan6702cef2022-06-13 17:52:37 +0100223 data.supportRole = undefined;
224 } else lastClicked = "sup";
225 } else if (i.component.customId == "enabled") {
pineafan4edb7762022-06-26 19:21:04 +0100226 await client.database.guilds.write(interaction.guild.id, { "tickets.enabled": !data.enabled })
pineafan6702cef2022-06-13 17:52:37 +0100227 data.enabled = !data.enabled;
228 } else if (i.component.customId == "manageTypes") {
229 data = await manageTypes(interaction, data, m);
230 } else {
231 break
232 }
233 }
234 await interaction.editReply({ embeds: [embed.setFooter({ text: "Message closed" })], components: [] });
pineafan4f164f32022-02-26 22:07:12 +0000235}
236
pineafan6702cef2022-06-13 17:52:37 +0100237async function manageTypes(interaction, data, m) {
238 while (true) {
239 if (data.useCustom) {
240 let customTypes = data.customTypes;
pineafanc6158ab2022-06-17 16:34:07 +0100241 await interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +0100242 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100243 .setTitle("Tickets > Types")
244 .setDescription(
245 "**Custom types enabled**\n\n" +
246 "**Types in use:**\n" + ((customTypes !== null) ?
247 (customTypes.map((t) => `> ${t}`).join("\n")) :
248 "*None set*"
249 ) + "\n\n" + (customTypes === null ?
250 `${getEmojiByName("TICKETS.REPORT")} Having no types will disable tickets. Please add at least 1 type or use default types` : ""
251 )
252 )
253 .setStatus("Success")
254 .setEmoji("GUILD.TICKET.OPEN")
255 ], components: (customTypes ? [
256 new MessageActionRow().addComponents([new Discord.MessageSelectMenu()
257 .setCustomId("removeTypes")
258 .setPlaceholder("Select types to remove")
259 .setMaxValues(customTypes.length)
260 .setMinValues(1)
261 .addOptions(customTypes.map((t) => new SelectMenuOption().setLabel(t).setValue(t)))
262 ])
263 ] : []).concat([
264 new MessageActionRow().addComponents([
265 new MessageButton()
266 .setLabel("Back")
267 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
268 .setStyle("PRIMARY")
269 .setCustomId("back"),
270 new MessageButton()
271 .setLabel("Add new type")
272 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
273 .setStyle("PRIMARY")
274 .setCustomId("addType")
275 .setDisabled(customTypes !== null && customTypes.length >= 25),
276 new MessageButton()
277 .setLabel("Switch to default types")
278 .setStyle("SECONDARY")
279 .setCustomId("switchToDefault"),
280 ])
281 ])
282 });
283 } else {
284 let inUse = toHexArray(data.types, ticketTypes)
285 let options = [];
286 ticketTypes.forEach(type => {
287 options.push(new SelectMenuOption({
288 label: capitalize(type),
289 value: type,
PineappleFanb3dd83c2022-06-17 10:53:48 +0100290 emoji: client.emojis.cache.get(getEmojiByName(`TICKETS.${type.toUpperCase()}`, "id")),
pineafan6702cef2022-06-13 17:52:37 +0100291 default: inUse.includes(type)
292 }))
293 })
294 let selectPane = new MessageActionRow().addComponents([
295 new Discord.MessageSelectMenu()
296 .addOptions(options)
297 .setCustomId("types")
298 .setMaxValues(ticketTypes.length)
299 .setMinValues(1)
300 .setPlaceholder("Select types to use")
301 ])
pineafanc6158ab2022-06-17 16:34:07 +0100302 await interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +0100303 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100304 .setTitle("Tickets > Types")
305 .setDescription(
306 "**Default types enabled**\n\n" +
307 "**Types in use:**\n" +
308 (inUse.map((t) => `> ${getEmojiByName("TICKETS." + t.toUpperCase())} ${capitalize(t)}`).join("\n"))
309 )
310 .setStatus("Success")
311 .setEmoji("GUILD.TICKET.OPEN")
312 ], components: [
313 selectPane,
314 new MessageActionRow().addComponents([
315 new MessageButton()
316 .setLabel("Back")
317 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
318 .setStyle("PRIMARY")
319 .setCustomId("back"),
320 new MessageButton()
321 .setLabel("Switch to custom types")
322 .setStyle("SECONDARY")
323 .setCustomId("switchToCustom"),
324 ])
325 ]
326 });
327 }
328 let i;
329 try {
pineafanc6158ab2022-06-17 16:34:07 +0100330 i = await m.awaitMessageComponent({ time: 300000 });
pineafan6702cef2022-06-13 17:52:37 +0100331 } catch (e) { break }
332 if (i.component.customId == "types") {
333 i.deferUpdate()
334 let types = toHexInteger(i.values, ticketTypes);
pineafan4edb7762022-06-26 19:21:04 +0100335 await client.database.guilds.write(interaction.guild.id, { "tickets.types": types })
pineafan6702cef2022-06-13 17:52:37 +0100336 data.types = types;
337 } else if (i.component.customId == "removeTypes") {
338 i.deferUpdate()
339 let types = i.values
340 let customTypes = data.customTypes;
341 if (customTypes) {
342 customTypes = customTypes.filter((t) => !types.includes(t));
343 customTypes = customTypes.length > 0 ? customTypes : null;
pineafan4edb7762022-06-26 19:21:04 +0100344 await client.database.guilds.write(interaction.guild.id, { "tickets.customTypes": customTypes })
pineafan6702cef2022-06-13 17:52:37 +0100345 data.customTypes = customTypes;
346 }
347 } else if (i.component.customId == "addType") {
348 await i.showModal(new Discord.Modal().setCustomId("modal").setTitle("Enter a name for the new type").addComponents(
pineafan02ba0232022-07-24 22:16:15 +0100349 new MessageActionRow<TextInputComponent>().addComponents(new TextInputComponent()
pineafan6702cef2022-06-13 17:52:37 +0100350 .setCustomId("type")
351 .setLabel("Name")
352 .setMaxLength(100)
353 .setMinLength(1)
354 .setPlaceholder("E.g. \"Server Idea\"")
355 .setRequired(true)
356 .setStyle("SHORT")
357 )
358 ))
359 await interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +0100360 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100361 .setTitle("Tickets > Types")
362 .setDescription("Modal opened. If you can't see it, click back and try again.")
363 .setStatus("Success")
364 .setEmoji("GUILD.TICKET.OPEN")
365 ], components: [new MessageActionRow().addComponents([new MessageButton()
366 .setLabel("Back")
367 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
368 .setStyle("PRIMARY")
369 .setCustomId("back")
370 ])]
371 });
pineafan4edb7762022-06-26 19:21:04 +0100372 let out;
pineafan6702cef2022-06-13 17:52:37 +0100373 try {
374 out = await modalInteractionCollector(m, (m) => m.channel.id == interaction.channel.id, (m) => m.customId == "addType")
375 } catch (e) { continue }
376 if (out.fields) {
377 let toAdd = out.fields.getTextInputValue("type");
378 if (!toAdd) { continue }
pineafan4edb7762022-06-26 19:21:04 +0100379 toAdd = toAdd.substring(0, 80)
pineafan6702cef2022-06-13 17:52:37 +0100380 try {
pineafan4edb7762022-06-26 19:21:04 +0100381 await client.database.guilds.append(interaction.guild.id, "tickets.customTypes", toAdd)
pineafan6702cef2022-06-13 17:52:37 +0100382 } catch { continue }
383 data.customTypes = data.customTypes || [];
384 if (!data.customTypes.includes(toAdd)) {
385 data.customTypes.push(toAdd);
386 }
387 } else { continue }
388 } else if (i.component.customId == "switchToDefault") {
389 i.deferUpdate()
pineafan4edb7762022-06-26 19:21:04 +0100390 await client.database.guilds.write(interaction.guild.id, { "tickets.useCustom": false }, [])
pineafan6702cef2022-06-13 17:52:37 +0100391 data.useCustom = false;
392 } else if (i.component.customId == "switchToCustom") {
393 i.deferUpdate()
pineafan4edb7762022-06-26 19:21:04 +0100394 await client.database.guilds.write(interaction.guild.id, { "tickets.useCustom": true }, [])
pineafan6702cef2022-06-13 17:52:37 +0100395 data.useCustom = true;
396 } else {
397 i.deferUpdate()
398 break
399 }
400 }
401 return data
402}
403
404
pineafan4f164f32022-02-26 22:07:12 +0000405const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan6702cef2022-06-13 17:52:37 +0100406 let member = (interaction.member as Discord.GuildMember)
pineafanda6e5342022-07-03 10:03:16 +0100407 if (!member.permissions.has("MANAGE_GUILD")) throw "You must have the Manage Server permission to use this command"
pineafan6702cef2022-06-13 17:52:37 +0100408 return true;
pineafan4f164f32022-02-26 22:07:12 +0000409}
410
411export { command };
412export { callback };
413export { check };