@elara-services/tickets 1.6.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +64 -43
  2. package/index.d.ts +39 -9
  3. package/index.js +331 -171
  4. package/package.json +2 -1
package/README.md CHANGED
@@ -6,52 +6,73 @@ This is a customizable ticket system that uses interactions and discord.js
6
6
  # Getting Started
7
7
  ```js
8
8
  const { Client } = require("discord.js"),
9
- Tickets = require(`@elara-services/tickets`),
10
- client = new Client({ intents: [ "GUILDS" ] }),
11
- tickets = new Tickets({
12
- client,
13
- prefix: "support", // This is what is used for interactions (buttons) and the start of the channel name
14
- ticketCategory: "", // When tickets get created they get created in this category, (OPTIONAL) - Default is the category ID for the starter message's channel.
15
- webhookId: "WEBHOOK ID HERE",
16
- webhookToken: "WEBHOOK TOKEN HERE",
17
- encryptToken: "ASB!@#$%^&*(B", // This is to encrypt/decrypt the user IDs in the channel topic, to avoid non-staff from seeing who's ticket it is
18
- supportRoleIds: [
19
- "12345678", // Add the support role ids here
20
- ],
21
- supportUserIds: [
22
- `12345678`, // Add the support user ids here
23
- ],
24
- webhookUsername: "WEBHOOK USERNAME HERE",
25
- webhookAvatar: "WEBHOOK AVATAR URL HERE",
26
- ticketOpen: {
27
- content: "", // The content of the ticket message once it gets created, use "%user%" or "%server%" for the user mention or server name
28
- embeds: [], // View https://discord.com/developers/docs/resources/channel#embed-object
9
+ Tickets = require(`@elara-services/tickets`),
10
+ client = new Client({ intents: ["GUILDS"] }),
11
+ tickets = new Tickets({
12
+ client,
13
+ prefix: "support", // This is what is used for interactions (buttons) and the start of the channel name
14
+ ticketCategory: "", // When tickets get created they get created in this category, (OPTIONAL) - Default is the category ID for the starter message's channel.
15
+ encryptToken: "ASB!@#$%^&*(B", // This is to encrypt/decrypt the user IDs in the channel topic, to avoid non-staff from seeing who's ticket it is
16
+ debug: true, // Only use if you want errors logged.
17
+ ticketOpen: {
18
+ content: "", // The content of the ticket message once it gets created, use "%user%" or "%server%" for the user mention or server name
19
+ embeds: [], // View https://discord.com/developers/docs/resources/channel#embed-object
20
+ },
21
+ appeals: { // OPTIONAL
22
+ enabled: true, // If the appeals server checks should be enabled.
23
+ mainserver: {
24
+ id: "", // The main server's id
25
+ checkIfBanned: true // Check if the user is banned in the main server, if not they can't open a ticket.
29
26
  },
30
- appeals: { // OPTIONAL
31
- enabled: true, // If the appeals server checks should be enabled.
32
- mainserver: {
33
- id: "", // The main server's id
34
- checkIfBanned: true // Check if the user is banned in the main server, if not they can't open a ticket.
35
- },
36
- embeds: {
37
- not_banned: {} // The embeds, content and components for the not banned message.
38
- }
27
+ embeds: {
28
+ not_banned: {} // The embeds, content and components for the not banned message.
39
29
  }
40
- })
30
+ },
41
31
 
42
- client.on("interactionCreate", (int) => tickets.run(int))
32
+ modal: {
33
+ enabled: true, // If this form should be enabled / shown
34
+ title: "", // The top title of the modal / form
35
+ questions: [
36
+ {
37
+ label: "[NAME]", // The name of this question
38
+ style: 2, // 1 (SHORT ANSWER) | 2 (LONG ANSWER)
39
+ placeholder: "", // The placeholder of the question
40
+ value: "", // The default value for the question
41
+ required: true, // If the question should be required
42
+ min_length: 20, // The min length for the question response
43
+ max_length: 4000, // The max length for the question response
44
+ }
45
+ ]
46
+ },
47
+ webhook: {
48
+ id: "", // 'webhookId' support will be removed in the next major version
49
+ token: "", // 'webhookToken' support will be removed in the next major version
50
+ username: "Webhook Username Here", // 'webhookUsername' support will be removed in the next major version
51
+ avatar: "", // 'webhookAvatar' support will be removed in the next major version
52
+ },
53
+ support: {
54
+ roles: [ // 'supportRoleIds' support will be removed in the next major version
55
+ "123456789"
56
+ ],
57
+ users: [ // 'supportUserIds' support will be removed in the next major version
58
+ "123456789"
59
+ ]
60
+ },
61
+ })
43
62
 
44
- client.on("ready", () => {
45
- console.log(`Client is ready`);
46
- // Use it as "node bot.js --starter" or just create a command in your bot to manage the starter message
47
- if (process.argv.find(c => c === "--starter")) {
48
- return tickets.starterMessage(`HELP OR SUPPORT CHANNEL ID HERE`, {
49
- embeds: [
50
- { title: "Support Tickets", description: `Click the button below to create a support ticket!`, color: 0xFF000 }
51
- ]
52
- })
53
- }
54
- });
63
+ client.on("interactionCreate", (int) => tickets.run(int))
64
+
65
+ client.on("ready", () => {
66
+ console.log(`Client is ready`);
67
+ // Use it as "node bot.js --starter" or just create a command in your bot to manage the starter message
68
+ if (process.argv.find(c => c === "--starter")) {
69
+ return tickets.starterMessage(`HELP OR SUPPORT CHANNEL ID HERE`, {
70
+ embeds: [
71
+ { title: "Support Tickets", description: `Click the button below to create a support ticket!`, color: 0xFF000 }
72
+ ]
73
+ })
74
+ }
75
+ });
55
76
 
56
- client.login("BOT TOKEN HERE")
77
+ client.login("BOT TOKEN HERE")
57
78
  ```
package/index.d.ts CHANGED
@@ -1,20 +1,14 @@
1
1
  declare module "@elara-services/tickets" {
2
2
 
3
- import { Client, MessageOptions, GuildMember, Guild, User, TextBasedChannel, Message, Interaction } from "discord.js";
3
+ import { Client, MessageOptions, GuildMember, Guild, User, TextBasedChannel, Message, Interaction, ModalOptions } from "discord.js";
4
4
  import Webhook from "discord-hook";
5
5
 
6
6
  export interface TicketOptions {
7
7
  client: Client;
8
8
  prefix: string;
9
+ debug?: boolean;
9
10
  encryptToken: string;
10
11
  ticketCategory?: string;
11
-
12
- webhookId?: string;
13
- webhookToken?: string;
14
- webhookUsername?: string;
15
- webhookAvatar?: string;
16
- supportRoleIds?: string[];
17
- supportUserIds?: string[];
18
12
  ticketOpen?: Pick<MessageOptions, "content" | "embeds">
19
13
  appeals?: {
20
14
  enabled: boolean;
@@ -25,7 +19,43 @@ declare module "@elara-services/tickets" {
25
19
  embeds?: {
26
20
  not_banned: Pick<MessageOptions, "content" | "embeds" | "components">
27
21
  }
28
- }
22
+ };
23
+ modal?: {
24
+ enabled: boolean;
25
+ title?: string;
26
+ questions?: {
27
+ label: string;
28
+ style: 1 | 2;
29
+ placeholder?: string;
30
+ value?: string;
31
+ required?: boolean;
32
+ min_length?: number;
33
+ max_length?: number;
34
+ }[]
35
+ };
36
+ webhook?: {
37
+ id?: string;
38
+ token?: string;
39
+ username?: string;
40
+ avatar?: string
41
+ };
42
+ support?: {
43
+ roles?: string[];
44
+ users?: string[];
45
+ };
46
+
47
+ /** @deprecated Use 'webhook.id' */
48
+ webhookId?: string;
49
+ /** @deprecated Use 'webhook.token' */
50
+ webhookToken?: string;
51
+ /** @deprecated Use 'webhook.username' */
52
+ webhookUsername?: string;
53
+ /** @deprecated Use 'webhook.avatar' */
54
+ webhookAvatar?: string;
55
+ /** @deprecated Use 'support.roles' */
56
+ supportRoleIds?: string[];
57
+ /** @deprecated Use 'support.users' */
58
+ supportUserIds?: string[];
29
59
  }
30
60
 
31
61
  class Tickets {
package/index.js CHANGED
@@ -1,25 +1,263 @@
1
- const { Collection, WebhookClient } = require("discord.js"),
2
- { encrypt, decrypt } = require("aes256"),
3
- { generate } = require("shortid"),
4
- Webhook = require("discord-hook");
1
+ const { Collection, WebhookClient, MessageEmbed } = require("discord.js"),
2
+ { encrypt, decrypt } = require("aes256"),
3
+ { generate } = require("shortid"),
4
+ { Interactions: { button, modal } } = require("@elara-services/packages"),
5
+ Webhook = require("discord-hook"),
6
+ de = {
7
+ user: "<:Members:860931214232125450>",
8
+ channel: "<:Channel:841654412509839390>",
9
+ transcript: "<:Log:792290922749624320>"
10
+ }
5
11
 
6
12
  module.exports = class Tickets {
7
13
  constructor(options) {
8
14
  this.options = options;
9
15
  };
10
16
  get prefix() { return `system:ticket:${this.options.prefix}`; };
17
+
18
+ /** @private */
19
+ get webhookOptions() {
20
+ return {
21
+ id: this.options.webhook?.id || this.options.webhookId,
22
+ token: this.options.webhook?.token || this.options.webhookToken,
23
+ username: this.options.webhook?.username || this.options.webhookUsername || "Tickets",
24
+ avatar: this.options.webhook?.avatar || this.options.webhookAvatar || "https://cdn.discordapp.com/emojis/818757771310792704.png?v=1"
25
+ }
26
+ }
11
27
  webhook() {
12
- return new Webhook(`https://discord.com/api/webhooks/${this.options.webhookId}/${this.options.webhookToken}`, {
13
- username: this.options.webhookUsername || "Tickets",
14
- avatar_url: this.options.webhookAvatar || "https://cdn.discordapp.com/emojis/818757771310792704.png?v=1",
15
- });
28
+ const { id, token, username, avatar } = this.webhookOptions;
29
+ return new Webhook(`https://discord.com/api/webhooks/${id}/${token}`, { username, avatar_url: avatar });
30
+ };
31
+
32
+ /**
33
+ * @param {import("discord.js").Interaction} int
34
+ */
35
+ async run(int) {
36
+ if (int?.isButton?.() || int?.isModalSubmit()) {
37
+ let { guild, channel, member, customId } = int,
38
+ category = guild?.channels?.resolve?.(this.options.ticketCategory || channel?.parentId);
39
+
40
+ if (!guild || !guild.available || !channel || !member || !category) return;
41
+
42
+ /**
43
+ * @param {import("discord.js").InteractionDeferReplyOptions|import("discord.js").InteractionReplyOptions} options
44
+ * @param {boolean} edit
45
+ * @param {boolean} defer
46
+ */
47
+ const send = async (options = {}, defer = false) => {
48
+ if (defer) return int.deferReply(options).catch(this._debug);
49
+ if (int.replied || int.deferred) return int.editReply(options).catch(this._debug);
50
+ return int.reply(options).catch(this._debug);
51
+ };
52
+ switch (customId) {
53
+ case this.prefix: {
54
+ if (this.options.modal?.enabled) {
55
+ return int.showModal(this.modal({
56
+ title: this.options.modal.title,
57
+ components: this.options.modal.questions?.length >= 1 ?
58
+ this.options.modal.questions.slice(0, 5).map(c => ({ type: 1, components: [{ min_length: c.min_length || 10, max_length: c.max_length || 4000, type: 4, style: c.style || 2, label: c.label, value: c.value, placeholder: c.placeholder, required: c.required, custom_id: c.label || `random_${Math.floor(Math.random() * 10000)}` }] })) :
59
+ []
60
+ })).catch(this._debug);
61
+ }
62
+ return this.handleCreate({ guild, member, category, send })
63
+ };
64
+
65
+ case `${this.prefix}:close`: return send({ ephemeral: true, content: `🤔 Are you sure you want to close this ticket?`, components: [{ type: 1, components: [{ type: 2, custom_id: `${this.prefix}:close:confirm:${this.code(channel.topic?.split?.("ID: ")?.[1])}`, label: "Yes close the ticket", style: 4, emoji: { id: "807031399563264030" } }] }] })
66
+
67
+ case `${this.prefix}:modal_submit`: {
68
+ let [embed, fields, split] = [new MessageEmbed().setColor("ORANGE"), [], false];
69
+ for (const c of int.fields.components) {
70
+ for (const cc of c.components) {
71
+ if (cc.value && cc.customId) {
72
+ fields.push({ name: cc.customId, value: cc.value });
73
+ if (cc.value.length <= 1024) embed.addField(cc.customId, cc.value);
74
+ else split = true
75
+ }
76
+ }
77
+ }
78
+ if (embed.length >= 6000 || split) {
79
+ return this.handleCreate({ guild, member, category, send, embeds: fields.map((v, i) => ({
80
+ title: `Form Response: ${v.name}`,
81
+ color: embed.color,
82
+ description: v.value,
83
+ author: i === 0 ? { name: member.user.username, iconURL: member.user.displayAvatarURL({ dynamic: true }) } : undefined,
84
+ timestamp: fields.length - 1 === i ? new Date() : undefined,
85
+ footer: fields.length - 1 === i ? { text: `ID: ${member.id}` } : undefined
86
+ })) })
87
+ };
88
+
89
+ return this.handleCreate({ guild, member, category, send, embeds: [
90
+ embed
91
+ .setTitle(`Form Responses`)
92
+ .setTimestamp()
93
+ .setAuthor({ name: member.user.username, iconURL: member.user.displayAvatarURL({ dynamic: true }) })
94
+ .setFooter({ text: `ID: ${member.id}` })
95
+ ]})
96
+ }
97
+ };
98
+ if (customId.startsWith(`${this.prefix}:close:confirm`)) {
99
+ let user = this.options.client.users.resolve(customId.split("close:confirm:")[1]) ?? await this.options.client.users.fetch(customId.split("close:confirm:")[1]).catch(() => null);
100
+ if (!user) return send({ content: `❌ I was unable to fetch the user that opened the ticket.`, ephemeral: true })
101
+ let messages = await this.fetchMessages(channel, 5000);
102
+ if (!messages || !messages.length) return send({ ephemeral: true, content: `❌ I was unable to close the ticket, I couldn't fetch the messages in this channel.` })
103
+ let closed = await channel.delete(`${member.user.tag} (${member.id}) closed the ticket.`).catch(this._debug);
104
+ if (!closed) return send({ ephemeral: true, content: `${emojis.x} I was unable to delete the channel & close the ticket.` })
105
+ return this.closeTicket({ channel, guild, user, member, messages });
106
+ }
107
+ };
108
+ };
109
+
110
+ /** @private */
111
+ async handleCreate({ guild, member, category, send, embeds = [] } = {}) {
112
+ let [support, supportUsers, supportIds] = [[], [], this.getSupportIds()];
113
+ if (supportIds.roles.length) for (const sup of supportIds.roles) {
114
+ let role = guild.roles.resolve(sup);
115
+ if (role) support.push(sup);
116
+ };
117
+
118
+ if (supportIds.users.length) for (const uId of supportIds.users) {
119
+ let member = guild.members.resolve(uId) || await guild.members.fetch(uId).catch((e) => {
120
+ if (e?.stack?.includes?.("Unknown Member")) this.options.support.users = this.options.support.users.filter(c => c !== uId);
121
+ return this._debug(e);
122
+ });
123
+ if (member) supportUsers.push(uId);
124
+ }
125
+ await send({ ephemeral: true }, true);
126
+ if (this.options.appeals?.enabled) {
127
+ let appeals = this.options.appeals;
128
+ if (appeals.mainserver?.id && appeals.mainserver.checkIfBanned) {
129
+ let server = this.options.client.guilds.resolve(appeals.mainserver.id);
130
+ if (server?.available) {
131
+ let isBanned = await server.bans.fetch({ user: member.id, force: true }).catch(() => null);
132
+ if (!isBanned) return send(
133
+ typeof appeals.embeds?.not_banned === "object" ?
134
+ appeals.embeds.not_banned :
135
+ {
136
+ embeds: [
137
+ {
138
+ author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
139
+ title: "INFO",
140
+ description: `❌ You can't open this ticket due to you not being banned in the main server!`,
141
+ color: 0xFF0000,
142
+ timestamp: new Date()
143
+ }
144
+ ]
145
+ }
146
+ )
147
+ }
148
+ }
149
+ }
150
+ let [permissions, allow] = [
151
+ [],
152
+ ["ADD_REACTIONS", "ATTACH_FILES", "CREATE_INSTANT_INVITE", "EMBED_LINKS", "READ_MESSAGE_HISTORY", "VIEW_CHANNEL", "USE_EXTERNAL_EMOJIS", "SEND_MESSAGES"]
153
+ ];
154
+ if (support.length) for (const sup of support) permissions.push({ type: "role", id: sup, allow });
155
+ if (supportUsers.length) for (const user of supportUsers) permissions.push({ type: "member", id: user, allow });
156
+
157
+ /** @type {import("discord.js").TextChannel} */
158
+ let channel = await guild.channels.create(`${this.options.prefix}-${generate().slice(0, 5).replace(/-|_/g, "")}`, {
159
+ type: "GUILD_TEXT", parent: category, reason: `Ticket created by: @${member.user.tag} (${member.id})`,
160
+ topic: `ID: ${this.code(member.id, "e")}`,
161
+ permissionOverwrites: [
162
+ { type: "member", id: this.options.client.user.id, allow: ["ADD_REACTIONS", "ATTACH_FILES", "SEND_MESSAGES", "READ_MESSAGE_HISTORY", "EMBED_LINKS", "USE_EXTERNAL_EMOJIS", "VIEW_CHANNEL", "MENTION_EVERYONE"] },
163
+ { type: "member", id: member.id, allow: ["ADD_REACTIONS", "ATTACH_FILES", "SEND_MESSAGES", "READ_MESSAGE_HISTORY", "EMBED_LINKS", "USE_EXTERNAL_EMOJIS", "VIEW_CHANNEL"], deny: ["MENTION_EVERYONE"] },
164
+ { type: "role", id: guild.id, deny: ["VIEW_CHANNEL"] },
165
+ ...permissions
166
+ ]
167
+ }).catch(this._debug);
168
+ if (!channel) return send({ content: `${emojis.x} I was unable to create the ticket channel, if this keeps happening contact one of the staff members via their DMs!` });
169
+ let msg = await channel.send({
170
+ content: this.options.ticketOpen?.content?.replace?.(/%user%/gi, member.user.toString())?.replace?.(/%server%/gi, guild.name) || `${member.user.toString()} 👋 Hello, please explain what you need help with.`,
171
+ embeds: this.options.ticketOpen?.embeds || [{
172
+ author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
173
+ title: `Support will be with you shortly`,
174
+ color: 0xF50DE3,
175
+ timestamp: new Date(),
176
+ footer: { text: `To close this ticket press the button below.` }
177
+ }],
178
+ components: [{ type: 1, components: [{ type: 2, custom_id: `${this.prefix}:close`, label: "Close Ticket", style: 4, emoji: { name: "🔒" } }] }]
179
+ }).catch(this._debug);
180
+ if (!msg) return null;
181
+ if (embeds?.length <= 10) for await (const embed of embeds) await channel.send({ embeds: [ embed ] }).catch(this._debug);
182
+ if (this.webhookOptions.id && this.webhookOptions.token) this.webhook()
183
+ .embed({
184
+ author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
185
+ title: "Ticket: Opened",
186
+ description: `${de.user} User: ${member.user.toString()} \`@${member.user.tag}\` (${member.id})\n${de.channel} Channel: \`#${channel.name}\` (${channel.id})`,
187
+ color: 0xFF000,
188
+ timestamp: new Date(),
189
+ footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` },
190
+ }).send().catch(this._debug);
191
+ return send({
192
+ embeds: [
193
+ {
194
+ author: { name: `Ticket Created!`, icon_url: `https://cdn.discordapp.com/emojis/476629550797684736.gif` },
195
+ description: channel.toString(),
196
+ color: 0xFF000
197
+ }
198
+ ],
199
+ components: [ { type: 1, components: [ button({ title: "Go to ticket", url: msg.url }) ] } ]
200
+ })
201
+ }
202
+
203
+ button(options = { style: 3, label: "Create Ticket", emoji: { name: "📩" } }) {
204
+ return button({
205
+ id: options?.id || this.prefix,
206
+ style: options.style || 3,
207
+ title: options.label,
208
+ emoji: options.emoji
209
+ });
210
+ };
211
+
212
+ /**
213
+ * @param {object} options
214
+ * @param {string} [options.title] The title of the modal submit form
215
+ * @param {import("@elara-services/packages").Modal['components']} [options.components]
216
+ */
217
+ modal(options = { title: "", components: [] }) {
218
+ return modal({
219
+ id: `${this.prefix}:modal_submit`,
220
+ title: options?.title || "Create Ticket",
221
+ components: options?.components?.length >= 1 ? options.components : [
222
+ {
223
+ type: 1, components: [
224
+ { type: 4, min_length: 10, max_length: 4000, custom_id: "message", label: "Content", style: 2, placeholder: "What's the ticket about?", required: true }
225
+ ]
226
+ }
227
+ ]
228
+ })
229
+ }
230
+
231
+ async starterMessage(channelId, options) {
232
+ let channel = this.options.client.channels.resolve(channelId);
233
+ if (!channel) return Promise.reject(`No channel found for: ${channelId}`);
234
+ if (!channel.isText()) return Promise.reject(`The channel ID provided isn't a text-based-channel`);
235
+ if (!channel.permissionsFor?.(this.options.client.user.id)?.has?.(["VIEW_CHANNEL", "SEND_MESSAGES", "EMBED_LINKS", "ATTACH_FILES", "READ_MESSAGE_HISTORY"])) return Promise.reject(`I'm missing permissions in ${channel.name} (${channelId})`);
236
+ return channel.send({
237
+ content: options?.content,
238
+ files: options?.attachments,
239
+ embeds: options?.embeds,
240
+ components: options?.components || [{ type: 1, components: [this.button()] }]
241
+ })
242
+ .then(() => console.log(`Sent the starter message in ${channel.name} (${channel.id})`))
243
+ };
244
+
245
+ code(id, type = "d") {
246
+ try {
247
+ switch (type) {
248
+ case "e": return encrypt(this.options.encryptToken, id);
249
+ case "d": return decrypt(this.options.encryptToken, id);
250
+ }
251
+ } catch {
252
+ return id;
253
+ }
16
254
  };
17
255
 
18
256
  async fetchMessages(channel, limit = 50, before, after, around) {
19
257
  if (limit && limit > 100) {
20
258
  let logs = [];
21
259
  const get = async (_before, _after) => {
22
- const messages = [ ...(await channel.messages.fetch({ limit: 100, before: _before || undefined, after: _after || undefined }).catch(() => new Collection())).values() ];
260
+ const messages = [...(await channel.messages.fetch({ limit: 100, before: _before || undefined, after: _after || undefined }).catch(() => new Collection())).values()];
23
261
  if (limit <= messages.length) {
24
262
  return (_after ? messages.slice(messages.length - limit, messages.length).map((message) => message).concat(logs) : logs.concat(messages.slice(0, limit).map((message) => message)));
25
263
  }
@@ -30,19 +268,26 @@ module.exports = class Tickets {
30
268
  };
31
269
  return get(before, after);
32
270
  }
33
- return [ ...(await channel.messages.fetch({ limit, before, after, around }).catch(() => new Collection())).values() ];
271
+ return [...(await channel.messages.fetch({ limit, before, after, around }).catch(() => new Collection())).values()];
34
272
  };
35
273
 
274
+ /**
275
+ * @param {import("discord.js").TextBasedChannel} channel
276
+ * @param {import("discord.js").Message[]} messages
277
+ * @param {string} ticketID
278
+ * @param {string} type
279
+ * @returns {string}
280
+ */
36
281
  displayMessages(channel, messages = [], ticketID, type) {
37
282
  let users = [];
38
283
  for (const i of messages.values()) {
39
284
  let f = users.find(c => c.user.id === i.author.id);
40
285
  if (f) f.count++; else users.push({ user: i.author, count: 1 });
41
286
  };
42
- return [`<discord-messages>`, `<discord-message author="${type} Ticket: ${ticketID}" bot="true" verified avatar="https://cdn.discordapp.com/emojis/847397714677334066.png?v=1" role-color="#1da1f2">Total Messages: ${users.map(c => c.count).reduce((a, b) => a + b, 0).toLocaleString()}<br>${users.map(c => `<discord-mention type="role" color="${channel.guild?.members?.cache?.get(c.user.id)?.displayColor ? channel.guild?.members?.cache?.get(c.user.id)?.displayHexColor : `#ffffff`}">${c.user.tag}</discord-mention> (${c.user.id})`).join("<br>")}</discord-message></discord-messages><discord-messages>`,
287
+ return [`<discord-messages>`, `<discord-message author="${type} Ticket: ${ticketID}" bot="true" verified avatar="https://cdn.discordapp.com/emojis/847397714677334066.png?v=1" role-color="#1da1f2">Total Messages: ${users.map(c => c.count).reduce((a, b) => a + b, 0).toLocaleString()}<br>${users.map(c => `<discord-mention type="role" color="${channel.guild?.members?.resolve?.(c.user.id)?.displayColor ? channel.guild?.members?.resolve?.(c.user.id)?.displayHexColor : `#ffffff`}">${c.user.tag}</discord-mention> (${c.user.id})`).join("<br>")}</discord-message></discord-messages><discord-messages>`,
43
288
  ...messages.map(message => {
44
289
  let str = [
45
- `<discord-message${message.author.bot ? ` bot="true"` : ""} timestamp="${message.createdAt.toLocaleString("en-US", { timeZone: "America/Los_Angeles" })} (PST/PDT) | ${message.createdAt.toLocaleString("en-GB", { timeZone: "Europe/London" })} (BST/British Time)" author="${message.author.username}" avatar=${message.author.displayAvatarURL({ dynamic: true, format: "png" })} role-color="${channel.guild?.members?.cache?.get(message.author.id)?.displayColor ? channel.guild?.members?.cache?.get(message.author.id)?.displayHexColor : `#ffffff`}">`
290
+ `<discord-message${message.author.bot ? ` bot="true" ${message.author.flags?.has?.("VERIFIED_BOT") ? `verified="true"` : ""}` : ""} new_timestamp="${message.createdAt.toISOString()}" author="${message.author.username}" avatar=${message.author.displayAvatarURL({ dynamic: true, format: "png" })} role-color="${channel.guild?.members?.cache?.get(message.author.id)?.displayColor ? channel.guild?.members?.cache?.get(message.author.id)?.displayHexColor : `#ffffff`}">`
46
291
  ];
47
292
  if (message.content) {
48
293
  let content = message.content;
@@ -51,40 +296,76 @@ module.exports = class Tickets {
51
296
  if (message.mentions.roles.size) for (const role of message.mentions.roles.values()) content = content.replace(new RegExp(role.toString(), "g"), `<discord-mention type="role" color="${role.hexColor}">${role.name}</discord-mention>`)
52
297
  str.push(content)
53
298
  };
299
+ if (message.embeds?.length) {
300
+ let arr = [ `<discord-embeds slot="embeds">` ];
301
+ for (const embed of message.embeds) {
302
+ let emb = [
303
+ `<discord-embed slot="embed" ${embed.thumbnail?.url ? `thumbnail="${embed.thumbnail.url}"` : ""} ${embed.image?.url ? `image="${embed.image.url}"` : ""} ${embed.author ? `${embed.author.name ? `author-name="${embed.author.name}"` : ""} ${embed.author.iconURL ? `author-image="${embed.author.iconURL}"` : ""} ${embed.author.url ? `author-url="${embed.author.url}"` : ""}` : ""} ${embed.title ? `embed-title="${embed.title}"` : ""}${embed.color ? `color="${embed.hexColor}"` : ""}>`
304
+ ];
305
+ if (embed.description) emb.push(`<discord-embed-description slot="description">${embed.description}</discord-embed-description>`);
306
+ if (embed.fields?.length) {
307
+ emb.push(`<discord-embed-fields slot="fields">`)
308
+ for (const field of embed.fields) emb.push(`<discord-embed-field field-title="${field.name}" ${field.inline ? `inline` : ""}>${field.value}</discord-embed-field>`)
309
+ }
310
+ arr.push(...emb, `</discord-embed>`)
311
+ }
312
+ str.push(...arr, "</discord-embeds>")
313
+ }
314
+ if (message.interaction?.user) str.push(`<discord-command slot="reply" command="${message.interaction.type === "APPLICATION_COMMAND" ? "/" : ""}${message.interaction.commandName}" profile="${message.interaction.user.id}" role-color="${channel.guild?.members?.resolve?.(message.interaction.user?.id)?.displayHexColor || "#fffff"}" author="${message.interaction.user.tag}" avatar="${message.interaction.user.displayAvatarURL({ dynamic: true })}"></discord-command>`)
315
+ if (message.components?.length) {
316
+ let row = [
317
+ `<discord-attachments slot="components">`
318
+ ],
319
+ styles = {
320
+ "PRIMARY": "primary",
321
+ "SECONDARY": "secondary",
322
+ "SUCCESS": "success",
323
+ "DANGER": "destructive",
324
+ "LINK": "secondary"
325
+ }
326
+ for (const components of message.components) {
327
+ row.push(`<discord-action-row>`)
328
+ for (const c of components.components) {
329
+ if (c.type === "BUTTON") row.push(`<discord-button ${c.disabled ? `disabled=true` : ""} type="${styles[c.style]}" ${c.emoji?.name ? `emoji-name="${c.emoji.name}"` : ""} ${c.emoji?.id ? `emoji="https://cdn.discordapp.com/emojis/${c.emoji.id}.${c.emoji.animated ? "gif" : "png"}"` : ""} ${c.url ? `url="${c.url}"` : ""}>${c.label || ""}</discord-button>`)
330
+ }
331
+ row.push(`</discord-action-row>`)
332
+ }
333
+ if (row.length >= 2) str.push(...row, `</discord-attachments>`)
334
+ }
54
335
  if (message.attachments.size) str.push(message.attachments.map((c) => `<a href="${c.proxyURL}">${c.name}</a>`).join("<br>"))
55
- str.push(`<br><br><code style="background-color: #36393e; color: white;">ID: ${message.id}</code>`)
336
+ str.push(`${message.content?.length ? `<br><br>` : "" }<code style="background-color: #36393e; color: white;">ID: ${message.id}</code>`)
56
337
  return [...str, `</discord-message>`].join(" ");
57
338
  }),
58
339
  "</discord-messages>"].join(" ");
59
340
  };
60
341
 
61
342
  async closeTicket({ member, messages, channel, guild, user } = {}) {
62
- if (!this.options.webhookId || !this.options.webhookToken) return;
343
+ const { id, token, username, avatar: avatarURL } = this.webhookOptions;
344
+ if (!id || !token) return;
63
345
  let embeds = [
64
346
  {
65
347
  author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
66
348
  title: "Ticket: Closed",
67
- description: `▫️User: ${user.toString()} \`@${user.tag}\` (${user.id})\n▫️Closed By: ${member.toString()} (${member.id})\n▫️Channel: \`#${channel.name}\` (${channel.id})`,
349
+ description: `${de.user}User: ${user.toString()} \`@${user.tag}\` (${user.id})\n${de.user}Closed By: ${member.toString()} (${member.id})\n${de.channel}Channel: \`#${channel.name}\` (${channel.id})`,
68
350
  color: 0xFF0000,
69
351
  timestamp: new Date(),
70
352
  footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` },
71
353
  }
72
354
  ];
73
- new WebhookClient({ id: this.options.webhookId, token: this.options.webhookToken })
74
- .send({
75
- username: this.options.webhookUsername || "Tickets",
76
- avatarURL: this.options.webhookAvatar || "https://cdn.discordapp.com/emojis/818757771310792704.png?v=1",
77
- embeds,
78
- files: [ { name: "transcript.txt", attachment: Buffer.from(this.displayMessages(channel, messages.reverse(), channel.name.split("-")[1], this.options.prefix)) } ]
355
+ new WebhookClient({ id, token })
356
+ .send({
357
+ username, avatarURL,
358
+ embeds,
359
+ files: [{ name: "transcript.txt", attachment: Buffer.from(this.displayMessages(channel, messages.reverse(), channel.name.split("-")[1], this.options.prefix)) }]
79
360
  })
80
361
  .then(m => {
81
- let components = [{ type: 2, style: 5, label: "Transcript", emoji: { name: "📝" }, url: `https://my.elara.services/tickets?url=${Array.isArray(m.attachments) ? m.attachments?.[0]?.url : m.attachments instanceof Collection ? m.attachments?.first?.()?.url : "URL_NOT_FOUND" ?? "URL_NOT_FOUND"}` }];
82
-
362
+ let components = [{ type: 2, style: 5, label: "Transcript", emoji: { id: "792290922749624320" }, url: `https://my.elara.services/tickets?url=${Array.isArray(m.attachments) ? m.attachments?.[0]?.url : m.attachments instanceof Collection ? m.attachments?.first?.()?.url : "URL_NOT_FOUND" ?? "URL_NOT_FOUND"}` }];
363
+ embeds[0].description += `\n${de.transcript} Transcript: [View here](${components[0].url})`
83
364
  this.webhook()
84
- .embeds(embeds)
85
- .button({ type: 1, components })
86
- .edit(m.id)
87
- .catch(() => null);
365
+ .embeds(embeds)
366
+ .button({ type: 1, components })
367
+ .edit(m.id)
368
+ .catch(this._debug);
88
369
  if (user) user.send({
89
370
  embeds: [{
90
371
  author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
@@ -94,151 +375,30 @@ module.exports = class Tickets {
94
375
  footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` }
95
376
  }],
96
377
  components: [{ type: 1, components }]
97
- }).catch(() => null)
98
- })
99
- .catch((e) => console.log(e));
100
- };
101
-
102
- async run(int) {
103
- if (int?.isButton?.()) {
104
- let { guild, channel, member, customId } = int,
105
- category = guild?.channels?.resolve?.(this.options.ticketCategory || channel?.parentId),
106
- [ support, supportUsers ] = [ [], [] ];
107
- if (!guild || !guild.available || !channel || !member || !category) return;
108
- if (this.options?.supportRoleIds?.length) for (const sup of this.options.supportRoleIds) {
109
- let role = guild.roles.resolve(sup);
110
- if (role) support.push(sup);
111
- };
112
-
113
- if (this.options?.supportUserIds?.length) for (const uId of this.options.supportUserIds) {
114
- let member = guild.members.resolve(uId) || await guild.members.fetch(uId).catch((e) => {
115
- if (e?.stack?.includes?.("Unknown Member")) this.options.supportUserIds = this.options.supportUserIds.filter(c => c !== uId);
116
- return null;
117
- });
118
- if (member) supportUsers.push(uId);
119
- }
120
- /**
121
- * @param {import("discord.js").InteractionDeferReplyOptions|import("discord.js").InteractionReplyOptions} options
122
- * @param {boolean} edit
123
- * @param {boolean} defer
124
- */
125
- const send = async (options = {}, defer = false) => {
126
- if (defer) return int.deferReply(options).catch(() => null);
127
- if (int.replied || int.deferred) return int.editReply(options).catch(() => null);
128
- return int.reply(options).catch(() => null);
129
- };
130
- switch (customId) {
131
- case this.prefix: {
132
- await send({ ephemeral: true }, true);
133
- if (this.options.appeals?.enabled) {
134
- let appeals = this.options.appeals;
135
- if (appeals.mainserver?.id && appeals.mainserver.checkIfBanned) {
136
- let server = this.options.client.guilds.resolve(appeals.mainserver.id);
137
- if (server?.available) {
138
- let isBanned = await server.bans.fetch({ user: member.id, force: true }).catch(() => null);
139
- if (!isBanned) return send(
140
- typeof appeals.embeds?.not_banned === "object" ?
141
- appeals.embeds.not_banned :
142
- { embeds: [
143
- {
144
- author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
145
- title: "INFO",
146
- description: `❌ You can't open this ticket due to you not being banned in the main server!`,
147
- color: 0xFF0000,
148
- timestamp: new Date()
149
- }
150
- ]}
151
- )
152
- }
153
- }
154
- }
155
- let [ permissions, allow ] = [
156
- [],
157
- [ "ADD_REACTIONS", "ATTACH_FILES", "CREATE_INSTANT_INVITE", "EMBED_LINKS", "READ_MESSAGE_HISTORY", "VIEW_CHANNEL", "USE_EXTERNAL_EMOJIS", "SEND_MESSAGES" ]
158
- ];
159
- if (support.length) for (const sup of support) permissions.push({ type: "role", id: sup, allow });
160
- if (supportUsers.length) for (const user of supportUsers) permissions.push({ type: "member", id: user, allow });
161
-
162
- /** @type {import("discord.js").TextChannel} */
163
- let channel = await guild.channels.create(`${this.options.prefix}-${generate().slice(0, 5).replace(/-|_/g, "")}`, {
164
- type: "GUILD_TEXT", parent: category, reason: `Ticket created by: @${member.user.tag} (${member.id})`,
165
- topic: `ID: ${this.code(member.id, "e")}`,
166
- permissionOverwrites: [
167
- { type: "member", id: this.options.client.user.id, allow: ["ADD_REACTIONS", "ATTACH_FILES", "SEND_MESSAGES", "READ_MESSAGE_HISTORY", "EMBED_LINKS", "USE_EXTERNAL_EMOJIS", "VIEW_CHANNEL", "MENTION_EVERYONE"] },
168
- { type: "member", id: member.id, allow: ["ADD_REACTIONS", "ATTACH_FILES", "SEND_MESSAGES", "READ_MESSAGE_HISTORY", "EMBED_LINKS", "USE_EXTERNAL_EMOJIS", "VIEW_CHANNEL"], deny: ["MENTION_EVERYONE"] },
169
- { type: "role", id: guild.id, deny: ["VIEW_CHANNEL"] },
170
- ...permissions
171
- ]
172
- }).catch((err) => { console.log(err); return null; });
173
- if (!channel) return send({ content: `${emojis.x} I was unable to create the ticket channel, if this keeps happening contact one of the staff members via their DMs!` });
174
- let msg = await channel.send({
175
- content: this.options.ticketOpen?.content?.replace?.(/%user%/gi, member.user.toString())?.replace?.(/%server%/gi, guild.name) || `${member.user.toString()} 👋 Hello, please explain what you need help with.`,
176
- embeds: this.options.ticketOpen?.embeds || [{
177
- author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
178
- title: `Support will be with you shortly`,
179
- color: 0xF50DE3,
180
- timestamp: new Date(),
181
- footer: { text: `To close this ticket press the button below.` }
182
- }],
183
- components: [{ type: 1, components: [{ type: 2, custom_id: `${this.prefix}:close`, label: "Close Ticket", style: 4, emoji: { name: "🔒" } }] }]
184
- }).catch(() => null);
185
- if (!msg) return null
186
- if (this.options.webhookId && this.options.webhookToken) this.webhook()
187
- .embed({
188
- author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
189
- title: "Ticket: Opened",
190
- description: `▫️User: ${member.user.toString()} \`@${member.user.tag}\` (${member.id})\n▫️Channel: \`#${channel.name}\` (${channel.id})`,
191
- color: 0xFF000,
192
- timestamp: new Date(),
193
- footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` },
194
- }).send().catch((e) => console.log(e));
195
- return send({ content: `✅ Ticket created: ${channel.toString()}`, components: [
196
- { type: 1, components: [
197
- { type: 2, style: 5, url: msg.url, label: "Go to ticket" }
198
- ] }
199
- ] })
200
- };
201
-
202
- case `${this.prefix}:close`: return send({ ephemeral: true, content: `🤔 Are you sure you want to close this ticket?`, components: [{ type: 1, components: [{ type: 2, custom_id: `${this.prefix}:close:confirm:${this.code(channel.topic?.split?.("ID: ")?.[1])}`, label: "Yes close the ticket", style: 4, emoji: { id: "807031399563264030" } }] }] })
203
- };
204
- if (customId.startsWith(`${this.prefix}:close:confirm`)) {
205
- let user = this.options.client.users.resolve(customId.split("close:confirm:")[1]) ?? await this.options.client.users.fetch(customId.split("close:confirm:")[1]).catch(() => null);
206
- if (!user) return send({ content: `❌ I was unable to fetch the user that opened the ticket.`, ephemeral: true })
207
- let messages = await this.fetchMessages(channel, 5000);
208
- if (!messages || !messages.length) return send({ ephemeral: true, content: `❌ I was unable to close the ticket, I couldn't fetch the messages in this channel.` })
209
- let closed = await channel.delete(`${member.user.tag} (${member.id}) closed the ticket.`).catch(() => null);
210
- if (!closed) return send({ ephemeral: true, content: `${emojis.x} I was unable to delete the channel & close the ticket.` })
211
- return this.closeTicket({ channel, guild, user, member, messages });
212
- }
213
- };
214
- };
215
-
216
- async starterMessage(channelId, options) {
217
- let channel = this.options.client.channels.resolve(channelId);
218
- if (!channel) return Promise.reject(`No channel found for: ${channelId}`);
219
- if (!channel.isText()) return Promise.reject(`The channel ID provided isn't a text-based-channel`);
220
- if (!channel.permissionsFor?.(this.options.client.user.id)?.has?.([ "VIEW_CHANNEL", "SEND_MESSAGES", "EMBED_LINKS", "ATTACH_FILES", "READ_MESSAGE_HISTORY" ])) return Promise.reject(`I'm missing permissions in ${channel.name} (${channelId})`);
221
- return channel.send({
222
- content: options?.content,
223
- files: options?.attachments,
224
- embeds: options?.embeds,
225
- components: options?.components || [ { type: 1, components: [ this.button() ] } ]
226
- })
227
- .then(() => console.log(`Sent the starter message in ${channel.name} (${channel.id})`))
378
+ }).catch(this._debug);
379
+ }).catch(this._debug);
228
380
  };
381
+ /**
382
+ * @typedef {Object} getSupportResponse
383
+ * @property {string[]} [roles]
384
+ * @property {string[]} [users]
385
+ *
386
+ *
387
+ * @private
388
+ * @returns {getSupportResponse}
389
+ */
390
+ getSupportIds() {
391
+ return {
392
+ roles: this.options.support?.roles || this.options.supportRoleIds || [],
393
+ users: this.options.support?.users || this.options.supportUserIds || []
394
+ }
395
+ }
229
396
 
230
- button(options = { style: 3, label: "Create Ticket", emoji: { name: "📩" } }) {
231
- return { type: 2, custom_id: options?.id || this.prefix, style: options.style || 3, label: options.label, emoji: options.emoji };
232
- };
397
+ /** @private */
398
+ _debug(...args) {
399
+ if (!this.options?.debug) return null;
400
+ console.log(...args);
401
+ return null;
402
+ }
233
403
 
234
- code(id, type = "d") {
235
- try {
236
- switch (type) {
237
- case "e": return encrypt(this.options.encryptToken, id);
238
- case "d": return decrypt(this.options.encryptToken, id);
239
- }
240
- } catch {
241
- return id;
242
- }
243
- };
244
404
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elara-services/tickets",
3
- "version": "1.6.0",
3
+ "version": "2.0.2",
4
4
  "description": "Helper for tickets",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -11,6 +11,7 @@
11
11
  "discord.js": "13.8.0"
12
12
  },
13
13
  "dependencies": {
14
+ "@elara-services/packages": "5.0.0",
14
15
  "aes256": "1.1.0",
15
16
  "discord-hook": "2.0.0",
16
17
  "shortid": "2.2.16"