blob: e664bd0bbaeaa6fdcd481ed56ce4ee64ede73580 [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))
pineafan6702cef2022-06-13 17:52:37 +010019 .addNumberOption(option => option.setName("maxticketsperuser").setDescription("The maximum amount of tickets a user can create | Default 5").setRequired(false).setMinValue(1))
20 .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)
112 if (confirmation.success) {
113 let toUpdate = {}
114 if (options.enabled !== null) toUpdate["tickets.enabled"] = options.enabled
115 if (options.category) toUpdate["tickets.category"] = options.category.id
116 if (options.maxtickets) toUpdate["tickets.maxTickets"] = options.maxtickets
117 if (options.supportping) toUpdate["tickets.supportRole"] = options.supportping.id
118 try {
pineafan4edb7762022-06-26 19:21:04 +0100119 await client.database.guilds.write(interaction.guild.id, toUpdate)
pineafan6702cef2022-06-13 17:52:37 +0100120 } catch (e) {
121 return interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +0100122 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100123 .setTitle("Tickets")
124 .setDescription(`Something went wrong and the staff notifications channel could not be set`)
125 .setStatus("Danger")
126 .setEmoji("GUILD.TICKET.DELETE")
127 ], components: []
128 });
129 }
130 } else {
131 return interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +0100132 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100133 .setTitle("Tickets")
134 .setDescription(`No changes were made`)
135 .setStatus("Success")
136 .setEmoji("GUILD.TICKET.OPEN")
137 ], components: []
138 });
139 }
140 }
pineafan4edb7762022-06-26 19:21:04 +0100141 let data = await client.database.guilds.read(interaction.guild.id);
pineafan6702cef2022-06-13 17:52:37 +0100142 data.tickets.customTypes = data.tickets.customTypes.filter((v, i, a) => a.indexOf(v) === i)
143 let lastClicked = "";
144 let embed;
145 data = {
146 enabled: data.tickets.enabled,
147 category: data.tickets.category,
148 maxTickets: data.tickets.maxTickets,
149 supportRole: data.tickets.supportRole,
150 useCustom: data.tickets.useCustom,
151 types: data.tickets.types,
152 customTypes: data.tickets.customTypes
153 }
154 while (true) {
pineafan4edb7762022-06-26 19:21:04 +0100155 embed = new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100156 .setTitle("Tickets")
157 .setDescription(
158 `${data.enabled ? "" : getEmojiByName("TICKETS.REPORT")} **Enabled:** ${data.enabled ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`}\n` +
159 `${data.category ? "" : getEmojiByName("TICKETS.REPORT")} **Category:** ${data.category ? `<#${data.category}>` : "*None set*"}\n` +
160 `**Max Tickets:** ${data.maxTickets ? data.maxTickets : "*No limit*"}\n` +
161 `**Support Ping:** ${data.supportRole ? `<@&${data.supportRole}>` : "*None set*"}\n\n` +
162 ((data.useCustom && data.customTypes === null) ? `${getEmojiByName("TICKETS.REPORT")} ` : "") +
163 `${data.useCustom ? "Custom" : "Default"} types in use` + "\n\n" +
164 `${getEmojiByName("TICKETS.REPORT")} *Indicates a setting stopping tickets from being used*`
165 )
166 .setStatus("Success")
167 .setEmoji("GUILD.TICKET.OPEN")
168 m = await interaction.editReply({
169 embeds: [embed], components: [new MessageActionRow().addComponents([
170 new MessageButton()
171 .setLabel("Tickets " + (data.enabled ? "enabled" : "disabled"))
172 .setEmoji(getEmojiByName("CONTROL." + (data.enabled ? "TICK" : "CROSS"), "id"))
173 .setStyle(data.enabled ? "SUCCESS" : "DANGER")
174 .setCustomId("enabled"),
175 new MessageButton()
176 .setLabel(lastClicked == "cat" ? "Click again to confirm" : "Clear category")
177 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
178 .setStyle("DANGER")
179 .setCustomId("clearCategory")
180 .setDisabled(data.category == null),
181 new MessageButton()
182 .setLabel(lastClicked == "max" ? "Click again to confirm" : "Reset max tickets")
183 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
184 .setStyle("DANGER")
185 .setCustomId("clearMaxTickets")
186 .setDisabled(data.maxTickets == 5),
187 new MessageButton()
188 .setLabel(lastClicked == "sup" ? "Click again to confirm" : "Clear support ping")
189 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
190 .setStyle("DANGER")
191 .setCustomId("clearSupportPing")
192 .setDisabled(data.supportRole == null),
193 ]), new MessageActionRow().addComponents([
194 new MessageButton()
195 .setLabel("Manage types")
196 .setEmoji(getEmojiByName("TICKETS.OTHER", "id"))
197 .setStyle("SECONDARY")
198 .setCustomId("manageTypes"),
199 ])]
200 });
201 let i;
202 try {
pineafanc6158ab2022-06-17 16:34:07 +0100203 i = await m.awaitMessageComponent({ time: 300000 });
pineafan6702cef2022-06-13 17:52:37 +0100204 } catch (e) { break }
205 i.deferUpdate()
206 if (i.component.customId == "clearCategory") {
207 if (lastClicked == "cat") {
208 lastClicked = "";
pineafan4edb7762022-06-26 19:21:04 +0100209 await client.database.guilds.write(interaction.guild.id, {}, ["tickets.category"])
pineafan6702cef2022-06-13 17:52:37 +0100210 data.category = undefined;
211 } else lastClicked = "cat";
212 } else if (i.component.customId == "clearMaxTickets") {
213 if (lastClicked == "max") {
214 lastClicked = "";
pineafan4edb7762022-06-26 19:21:04 +0100215 await client.database.guilds.write(interaction.guild.id, {}, ["tickets.maxTickets"])
pineafan6702cef2022-06-13 17:52:37 +0100216 data.maxTickets = 5;
217 } else lastClicked = "max";
218 } else if (i.component.customId == "clearSupportPing") {
219 if (lastClicked == "sup") {
220 lastClicked = "";
pineafan4edb7762022-06-26 19:21:04 +0100221 await client.database.guilds.write(interaction.guild.id, {}, ["tickets.supportRole"])
pineafan6702cef2022-06-13 17:52:37 +0100222 data.supportRole = undefined;
223 } else lastClicked = "sup";
224 } else if (i.component.customId == "enabled") {
pineafan4edb7762022-06-26 19:21:04 +0100225 await client.database.guilds.write(interaction.guild.id, { "tickets.enabled": !data.enabled })
pineafan6702cef2022-06-13 17:52:37 +0100226 data.enabled = !data.enabled;
227 } else if (i.component.customId == "manageTypes") {
228 data = await manageTypes(interaction, data, m);
229 } else {
230 break
231 }
232 }
233 await interaction.editReply({ embeds: [embed.setFooter({ text: "Message closed" })], components: [] });
pineafan4f164f32022-02-26 22:07:12 +0000234}
235
pineafan6702cef2022-06-13 17:52:37 +0100236async function manageTypes(interaction, data, m) {
237 while (true) {
238 if (data.useCustom) {
239 let customTypes = data.customTypes;
pineafanc6158ab2022-06-17 16:34:07 +0100240 await interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +0100241 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100242 .setTitle("Tickets > Types")
243 .setDescription(
244 "**Custom types enabled**\n\n" +
245 "**Types in use:**\n" + ((customTypes !== null) ?
246 (customTypes.map((t) => `> ${t}`).join("\n")) :
247 "*None set*"
248 ) + "\n\n" + (customTypes === null ?
249 `${getEmojiByName("TICKETS.REPORT")} Having no types will disable tickets. Please add at least 1 type or use default types` : ""
250 )
251 )
252 .setStatus("Success")
253 .setEmoji("GUILD.TICKET.OPEN")
254 ], components: (customTypes ? [
255 new MessageActionRow().addComponents([new Discord.MessageSelectMenu()
256 .setCustomId("removeTypes")
257 .setPlaceholder("Select types to remove")
258 .setMaxValues(customTypes.length)
259 .setMinValues(1)
260 .addOptions(customTypes.map((t) => new SelectMenuOption().setLabel(t).setValue(t)))
261 ])
262 ] : []).concat([
263 new MessageActionRow().addComponents([
264 new MessageButton()
265 .setLabel("Back")
266 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
267 .setStyle("PRIMARY")
268 .setCustomId("back"),
269 new MessageButton()
270 .setLabel("Add new type")
271 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
272 .setStyle("PRIMARY")
273 .setCustomId("addType")
274 .setDisabled(customTypes !== null && customTypes.length >= 25),
275 new MessageButton()
276 .setLabel("Switch to default types")
277 .setStyle("SECONDARY")
278 .setCustomId("switchToDefault"),
279 ])
280 ])
281 });
282 } else {
283 let inUse = toHexArray(data.types, ticketTypes)
284 let options = [];
285 ticketTypes.forEach(type => {
286 options.push(new SelectMenuOption({
287 label: capitalize(type),
288 value: type,
PineappleFanb3dd83c2022-06-17 10:53:48 +0100289 emoji: client.emojis.cache.get(getEmojiByName(`TICKETS.${type.toUpperCase()}`, "id")),
pineafan6702cef2022-06-13 17:52:37 +0100290 default: inUse.includes(type)
291 }))
292 })
293 let selectPane = new MessageActionRow().addComponents([
294 new Discord.MessageSelectMenu()
295 .addOptions(options)
296 .setCustomId("types")
297 .setMaxValues(ticketTypes.length)
298 .setMinValues(1)
299 .setPlaceholder("Select types to use")
300 ])
pineafanc6158ab2022-06-17 16:34:07 +0100301 await interaction.editReply({
pineafan4edb7762022-06-26 19:21:04 +0100302 embeds: [new EmojiEmbed()
pineafan6702cef2022-06-13 17:52:37 +0100303 .setTitle("Tickets > Types")
304 .setDescription(
305 "**Default types enabled**\n\n" +
306 "**Types in use:**\n" +
307 (inUse.map((t) => `> ${getEmojiByName("TICKETS." + t.toUpperCase())} ${capitalize(t)}`).join("\n"))
308 )
309 .setStatus("Success")
310 .setEmoji("GUILD.TICKET.OPEN")
311 ], components: [
312 selectPane,
313 new MessageActionRow().addComponents([
314 new MessageButton()
315 .setLabel("Back")
316 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
317 .setStyle("PRIMARY")
318 .setCustomId("back"),
319 new MessageButton()
320 .setLabel("Switch to custom types")
321 .setStyle("SECONDARY")
322 .setCustomId("switchToCustom"),
323 ])
324 ]
325 });
326 }
327 let i;
328 try {
pineafanc6158ab2022-06-17 16:34:07 +0100329 i = await m.awaitMessageComponent({ time: 300000 });
pineafan6702cef2022-06-13 17:52:37 +0100330 } catch (e) { break }
331 if (i.component.customId == "types") {
332 i.deferUpdate()
333 let types = toHexInteger(i.values, ticketTypes);
pineafan4edb7762022-06-26 19:21:04 +0100334 await client.database.guilds.write(interaction.guild.id, { "tickets.types": types })
pineafan6702cef2022-06-13 17:52:37 +0100335 data.types = types;
336 } else if (i.component.customId == "removeTypes") {
337 i.deferUpdate()
338 let types = i.values
339 let customTypes = data.customTypes;
340 if (customTypes) {
341 customTypes = customTypes.filter((t) => !types.includes(t));
342 customTypes = customTypes.length > 0 ? customTypes : null;
pineafan4edb7762022-06-26 19:21:04 +0100343 await client.database.guilds.write(interaction.guild.id, { "tickets.customTypes": customTypes })
pineafan6702cef2022-06-13 17:52:37 +0100344 data.customTypes = customTypes;
345 }
346 } else if (i.component.customId == "addType") {
347 await i.showModal(new Discord.Modal().setCustomId("modal").setTitle("Enter a name for the new type").addComponents(
348 // @ts-ignore
349 new MessageActionRow().addComponents(new TextInputComponent()
350 .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 };