@elara-services/tickets 2.3.0 → 3.0.1

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.
package/.eslintrc.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "env": {
3
+ "node": true,
4
+ "commonjs": true,
5
+ "es2021": true
6
+ },
7
+ "extends": "eslint:recommended",
8
+ "parserOptions": {
9
+ "ecmaVersion": 12
10
+ },
11
+ "rules": {
12
+ "no-extra-semi": 0,
13
+ "no-async-promise-executor": 0,
14
+ "no-warning-comments": "warn",
15
+ "no-empty": 0
16
+ },
17
+ "ignorePatterns": [
18
+ "*/*.json"
19
+ ]
20
+ }
package/README.md CHANGED
@@ -6,59 +6,63 @@ 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
- 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.
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
+ 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
15
+ debug: true, // Only use if you want errors logged.
16
+ appeals: { // OPTIONAL
17
+ enabled: true, // If the appeals server checks should be enabled.
18
+ mainserver: {
19
+ id: "", // The main server's id
20
+ checkIfBanned: true // Check if the user is banned in the main server, if not they can't open a ticket.
21
+ },
22
+ embeds: {
23
+ not_banned: {} // The embeds, content and components for the not banned message.
24
+ }
26
25
  },
27
- embeds: {
28
- not_banned: {} // The embeds, content and components for the not banned message.
29
- }
30
- },
31
26
 
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
27
+ modal: {
28
+ enabled: true, // If this form should be enabled / shown
29
+ title: "", // The top title of the modal / form
30
+ questions: [
31
+ {
32
+ label: "[NAME]", // The name of this question
33
+ style: 2, // 1 (SHORT ANSWER) | 2 (LONG ANSWER)
34
+ placeholder: "", // The placeholder of the question
35
+ value: "", // The default value for the question
36
+ required: true, // If the question should be required
37
+ min_length: 20, // The min length for the question response
38
+ max_length: 4000, // The max length for the question response
39
+ }
40
+ ]
41
+ },
42
+ webhook: {
43
+ id: "", // 'webhookId' support will be removed in the next major version
44
+ token: "", // 'webhookToken' support will be removed in the next major version
45
+ username: "Webhook Username Here", // 'webhookUsername' support will be removed in the next major version
46
+ avatar: "", // 'webhookAvatar' support will be removed in the next major version
47
+ },
48
+ support: {
49
+ canOnlyCloseTickets: true, // If 'true' only roles and users listed below can close tickets. (OR people with 'Manage Server' permission)
50
+ roles: [ // 'supportRoleIds' support will be removed in the next major version
51
+ "123456789"
52
+ ],
53
+ users: [ // 'supportUserIds' support will be removed in the next major version
54
+ "123456789"
55
+ ]
56
+ },
57
+ ticket: {
58
+ closeReason: true, // This will enforce users or support staff to provide a reason for closing the ticket
59
+ limitOnePerUser: true, // This will block users from creating more than one ticket.
60
+ category: "", // When tickets get created they get created in this category, (OPTIONAL) - Default is the category ID for the starter message's channel.
61
+ open: {
62
+ content: "", // The content of the ticket message once it gets created, use "%user%" or "%server%" for the user mention or server name
63
+ embeds: [], // View https://discord.com/developers/docs/resources/channel#embed-object
44
64
  }
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
- canOnlyCloseTickets: true, // If 'true' only roles and users listed below can close tickets. (OR people with 'Manage Server' permission)
55
- roles: [ // 'supportRoleIds' support will be removed in the next major version
56
- "123456789"
57
- ],
58
- users: [ // 'supportUserIds' support will be removed in the next major version
59
- "123456789"
60
- ]
61
- },
65
+ }
62
66
  })
63
67
 
64
68
  client.on("interactionCreate", (int) => tickets.run(int))
package/index.d.ts CHANGED
@@ -8,8 +8,6 @@ declare module "@elara-services/tickets" {
8
8
  prefix: string;
9
9
  debug?: boolean;
10
10
  encryptToken: string;
11
- ticketCategory?: string;
12
- ticketOpen?: Pick<MessageOptions, "content" | "embeds">
13
11
  appeals?: {
14
12
  enabled: boolean;
15
13
  sendBanResults?: boolean;
@@ -44,20 +42,19 @@ declare module "@elara-services/tickets" {
44
42
  roles?: string[];
45
43
  users?: string[];
46
44
  canOnlyCloseTickets?: boolean;
45
+ ignore?: string[]
47
46
  };
47
+ ticket?: {
48
+ category?: string;
49
+ closeReason?: boolean;
50
+ limitOnePerUser?: boolean;
51
+ open?: Pick<MessageOptions, "content" | "embeds">
52
+ }
48
53
 
49
- /** @deprecated Use 'webhook.id' */
50
- webhookId?: string;
51
- /** @deprecated Use 'webhook.token' */
52
- webhookToken?: string;
53
- /** @deprecated Use 'webhook.username' */
54
- webhookUsername?: string;
55
- /** @deprecated Use 'webhook.avatar' */
56
- webhookAvatar?: string;
57
- /** @deprecated Use 'support.roles' */
58
- supportRoleIds?: string[];
59
- /** @deprecated Use 'support.users' */
60
- supportUserIds?: string[];
54
+ /** @deprecated Use 'ticket.category', 'ticketCategory' will be removed in the next major version */
55
+ ticketCategory?: string;
56
+ /** @deprecated Use 'ticket.open', 'ticketOpen' will be removed in the next major version */
57
+ ticketOpen?: Pick<MessageOptions, "content" | "embeds">
61
58
  }
62
59
 
63
60
  class Tickets {
package/index.js CHANGED
@@ -1,415 +1,11 @@
1
- const { Collection, WebhookClient, MessageEmbed } = require("discord.js"),
2
- { generate } = require("shortid"),
3
- { Interactions: { button, modal }, AES } = require("@elara-services/packages"),
4
- Webhook = require("discord-hook"),
5
- de = {
6
- user: "<:Members:860931214232125450>",
7
- channel: "<:Channel:841654412509839390>",
8
- transcript: "<:Log:792290922749624320>"
9
- };
10
-
11
- module.exports = class Tickets {
12
- constructor(options = {}) {
13
- if (typeof options !== "object") throw new Error(`You didn't provide any data in the constructor, fill it out!`);
14
- if (!("client" in options) || !("prefix" in options) || !("encryptToken" in options)) throw new Error(`You forgot to fill out either 'client', 'prefix' or 'encryptToken'`)
15
- this.options = options;
16
- };
17
-
18
- get prefix() { return `system:ticket:${this.options.prefix}`; };
19
-
20
- /** @private */
21
- get webhookOptions() {
22
- return {
23
- id: this.options.webhook?.id || this.options.webhookId,
24
- token: this.options.webhook?.token || this.options.webhookToken,
25
- username: this.options.webhook?.username || this.options.webhookUsername || "Tickets",
26
- avatar: this.options.webhook?.avatar || this.options.webhookAvatar || "https://cdn.discordapp.com/emojis/818757771310792704.png?v=1"
27
- }
28
- }
29
-
30
- webhook() {
31
- const { id, token, username, avatar } = this.webhookOptions;
32
- return new Webhook(`https://discord.com/api/webhooks/${id}/${token}`, { username, avatar_url: avatar });
33
- };
34
-
35
- /**
36
- * @param {import("discord.js").Interaction} int
37
- */
38
- async run(int) {
39
- if (int?.isButton?.() || int?.isModalSubmit()) {
40
- let { guild, channel, member, customId } = int,
41
- category = guild?.channels?.resolve?.(this.options.ticketCategory || channel?.parentId);
42
- if (!guild || !guild.available || !channel || !member || !category) return;
43
-
44
- /**
45
- * @param {import("discord.js").InteractionDeferReplyOptions|import("discord.js").InteractionReplyOptions} options
46
- * @param {boolean} edit
47
- * @param {boolean} defer
48
- */
49
- const send = async (options = {}, defer = false) => {
50
- if (defer) return int.deferReply(options).catch(e => this._debug(e));
51
- if (int.replied || int.deferred) return int.editReply(options).catch(e => this._debug(e));
52
- return int.reply(options).catch(e => this._debug(e));
53
- };
54
- switch (customId) {
55
- case this.prefix: {
56
- if (this.options.modal?.enabled) return int.showModal(this.modal({ title: this.options.modal.title, components: this.options.modal.questions?.length >= 1 ? 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)}` }] })) : [] })).catch(e => this._debug(e));
57
- return this.handleCreate({ guild, member, category, send })
58
- };
59
-
60
- case `${this.prefix}:close`: {
61
- if (this.options.support?.canOnlyCloseTickets && !member.permissions.has("MANAGE_GUILD")) {
62
- let [support, staffOnly] = [this.getSupportIds(), () => send({ ephemeral: true, embeds: [{ author: { name: `Only support staff can close tickets`, iconURL: "https://cdn.discordapp.com/emojis/781955502035697745.gif" }, color: 0xFF0000 }] })];
63
- if (!support.users?.includes?.(member.id)) {
64
- if (!support.roles?.some?.(c => member.roles.cache.has(c))) return staffOnly()
65
- }
66
- }
67
- 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" } }] }] })
68
- }
69
-
70
- case `${this.prefix}:modal_submit`: {
71
- let [embed, fields, split] = [new MessageEmbed().setColor("ORANGE").setTimestamp().setTitle(`For Responses`).setFooter({ text: `ID: ${member.id}` }).setAuthor({ name: member.user.username, iconURL: member.user.displayAvatarURL({ dynamic: true }) }), [], false];
72
- for (const c of int.fields.components) {
73
- for (const cc of c.components) {
74
- if (cc.value && cc.customId) {
75
- fields.push({ name: cc.customId, value: cc.value });
76
- if (cc.value.length <= 1024) embed.addField(cc.customId, cc.value);
77
- else split = true
78
- }
79
- }
80
- }
81
- if (embed.length >= 6000 || split) {
82
- return this.handleCreate({
83
- guild, member, category, send, embeds: fields.map((v, i) => ({
84
- title: `Form Response: ${v.name}`, color: embed.color, description: v.value,
85
- author: i === 0 ? { name: member.user.username, iconURL: member.user.displayAvatarURL({ dynamic: true }) } : undefined,
86
- timestamp: fields.length - 1 === i ? new Date() : undefined,
87
- footer: fields.length - 1 === i ? { text: `ID: ${member.id}` } : undefined
88
- }))
89
- })
90
- };
91
- return this.handleCreate({ guild, member, category, send, embeds: [embed] })
92
- }
93
- };
94
- if (customId.startsWith(`${this.prefix}:close:confirm`)) {
95
- await send({ ephemeral: true }, true);
96
- let user = await this.options.client.users.fetch(customId.split("close:confirm:")[1]).catch(e => this._debug(e));
97
- if (!user) return send({ content: `❌ I was unable to fetch the user that opened the ticket.` })
98
- let messages = await this.fetchMessages(channel, 5000);
99
- if (!messages?.length) return send({ content: `❌ I was unable to close the ticket, I couldn't fetch the messages in this channel.` })
100
- let closed = await channel.delete(`${member.user.tag} (${member.id}) closed the ticket.`).catch(e => this._debug(e));
101
- if (!closed) return send({ content: `❌ I was unable to delete the channel & close the ticket.` })
102
- return this.closeTicket({ channel, guild, user, member, messages });
103
- };
104
-
105
- if (customId.startsWith("unban:")) {
106
- if (!int.memberPermissions?.has?.([ "BAN_MEMBERS" ])) return send({ content: `❌ You need (Ban Members) in this server to complete this action!`, ephemeral: true });
107
- let server = this.getAppealServer();
108
- if (!server) return send({ content: `❌ I was unable to find the appeal server!`, ephemeral: true });
109
- let mod = server.members.resolve(member.id) || await server.members.fetch(member.id).catch(e => this._debug(e));
110
- if (!mod) return send({ content: `❌ I was unable to find you in ${server.name}!`, ephemeral: true });
111
- if (!mod.permissions?.has?.([ "BAN_MEMBERS" ])) return send({ content: `❌ You need (Ban Members) in ${server.name} to complete this action!`, ephemeral: true });
112
- return int.showModal(modal({
113
- id: `unban_modal:${customId.split(":")[1]}`,
114
- title: `Unban From ${server.name}`,
115
- components: [ { type: 1, components: [ { type: 4, label: "Reason", custom_id: "reason", style: 2, min_length: 1, max_length: 512, required: true, value: `No Reason Provided | By: ${member.user.tag} (${member.id})` } ] } ]
116
- })).catch(e => this._debug(e));
117
- }
118
-
119
- if (customId.startsWith("unban_modal:")) {
120
- await send({ ephemeral: true }, true);
121
- const [ , id ] = customId.split(":");
122
- if (!int.memberPermissions?.has?.([ "BAN_MEMBERS" ])) return send({ content: `❌ You need (Ban Members) in this server to complete this action!` });
123
- let server = this.getAppealServer();
124
- if (!server) return send({ content: `❌ I was unable to find the appeal server!` });
125
- let mod = server.members.resolve(member.id) || await server.members.fetch(member.id).catch(e => this._debug(e));
126
- if (!mod) return send({ content: `❌ I was unable to find you in ${server.name}!` });
127
- if (!mod.permissions?.has?.([ "BAN_MEMBERS" ])) return send({ content: `❌ You need (Ban Members) in ${server.name} to complete this action!` });
128
- let isBanned = await server.bans.fetch({ user: id, force: true }).catch(e => this._debug(e));
129
- if (!isBanned) return send({ content: `❌ User (<@${id}>) isn't banned in the main server!` });
130
- return server.bans.remove(id, int.fields.getTextInputValue("reason") ?? `No Reason Provided | By: ${member.user.tag} (${member.id})`)
131
- .then(() => {
132
- int.message.edit({ components: [ { type: 1, components: [ button({ title: "Unbanned!", style: 3, id: "_ _", disabled: true, emoji: { id: `476629550797684736` } }) ] } ] }).catch(e => this._debug(e));
133
- send({ content: `✅ Successfully unbanned <@${id}> from ${server.name}!` })
134
- })
135
- .catch(e => send({ content: `❌ Unable to unban <@${id} from ${server.name}!`, embeds: [ { title: "ERROR", description: `\`\`\`js\n${e.message ?? e.stack}\`\`\``, color: 0xFF0000 } ] }))
136
- }
137
- };
138
- };
139
- /**
140
- * @returns {import("discord.js").Guild|null}
141
- */
142
- getAppealServer() {
143
- const { appeals, client } = this.options;
144
- if (!appeals?.enabled || !appeals?.mainserver?.checkIfBanned) return null;
145
- let server = client.guilds.resolve(appeals.mainserver?.id);
146
- if (!server?.available) return null;
147
- return server;
148
- }
149
-
150
- /** @private */
151
- async handleCreate({ guild, member, category, send, embeds = [] } = {}) {
152
- await send({ ephemeral: true }, true);
153
- let [supportIds, permissions, allow, { appeals }, sendBanReason] = [
154
- this.getSupportIds(), [],
155
- ["ADD_REACTIONS", "ATTACH_FILES", "CREATE_INSTANT_INVITE", "EMBED_LINKS", "READ_MESSAGE_HISTORY", "VIEW_CHANNEL", "USE_EXTERNAL_EMOJIS", "SEND_MESSAGES"],
156
- this.options ?? {},
157
- null
158
- ];
159
- if (supportIds.roles.length) for (const sup of supportIds.roles) {
160
- let role = guild.roles.resolve(sup);
161
- if (role) permissions.push({ type: "role", id: sup, allow });
162
- };
163
-
164
- if (supportIds.users.length) for (const uId of supportIds.users) {
165
- let member = guild.members.resolve(uId) || await guild.members.fetch(uId).catch((e) => {
166
- if (e?.stack?.includes?.("Unknown Member")) this.options.support.users = this.options.support.users.filter(c => c !== uId);
167
- return this._debug(e);
168
- });
169
- if (member) permissions.push({ type: "member", id: uId, allow });
170
- }
171
- if (appeals?.enabled) {
172
- let server = this.getAppealServer();
173
- if (server) {
174
- let ban = await server.bans.fetch({ user: member.id, force: true }).catch(e => this._debug(e));
175
- if (!ban) return send(
176
- typeof appeals.embeds?.not_banned === "object" ?
177
- appeals.embeds.not_banned :
178
- {
179
- embeds: [
180
- {
181
- author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
182
- title: "INFO",
183
- description: `❌ You can't open this ticket due to you not being banned in the main server!`,
184
- color: 0xFF0000,
185
- timestamp: new Date()
186
- }
187
- ]
188
- }
189
- );
190
- sendBanReason = {
191
- embeds: [
192
- {
193
- author: { name: server.name, iconURL: server.iconURL({ dynamic: true }) },
194
- title: "Ban Reason",
195
- description: ban.reason ?? "No Reason Provided?",
196
- color: 0xFF0000,
197
- timestamp: new Date()
198
- }
199
- ],
200
- components: [
201
- {
202
- type: 1, components: [
203
- button({ id: `unban:${member.id}`, style: 4, title: "Unban", emoji: { name: "🔒" } })
204
- ]
205
- }
206
- ]
207
- }
208
- }
209
- }
210
- let channel = await guild.channels.create(`${this.options.prefix}-${generate().slice(0, 5).replace(/-|_/g, "")}`, {
211
- type: "GUILD_TEXT", parent: category, reason: `Ticket created by: @${member.user.tag} (${member.id})`,
212
- topic: `ID: ${this.code(member.id, "e")}`,
213
- permissionOverwrites: [
214
- { 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"] },
215
- { 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"] },
216
- { type: "role", id: guild.id, deny: ["VIEW_CHANNEL"] },
217
- ...permissions
218
- ]
219
- }).catch(e => this._debug(e));
220
- if (!channel) return send({ content: `❌ I was unable to create the ticket channel, if this keeps happening contact one of the staff members via their DMs!` });
221
- let msg = await channel.send({
222
- 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.`,
223
- embeds: this.options.ticketOpen?.embeds || [{
224
- author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
225
- title: `Support will be with you shortly`,
226
- color: 0xF50DE3,
227
- timestamp: new Date(),
228
- footer: { text: `To close this ticket press the button below.` }
229
- }],
230
- components: [{ type: 1, components: [{ type: 2, custom_id: `${this.prefix}:close`, label: "Close Ticket", style: 4, emoji: { name: "🔒" } }] }]
231
- }).catch(e => this._debug(e));
232
- if (!msg) return null;
233
- if (sendBanReason) await channel.send(sendBanReason).catch(e => this._debug(e));
234
- if (embeds?.length <= 10) for await (const embed of embeds) await channel.send({ embeds: [embed] }).catch(e => this._debug(e));
235
- if (this.webhookOptions.id && this.webhookOptions.token) this.webhook()
236
- .embed({
237
- author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
238
- title: "Ticket: Opened",
239
- description: `${de.user} User: ${member.user.toString()} \`@${member.user.tag}\` (${member.id})\n${de.channel} Channel: \`#${channel.name}\` (${channel.id})`,
240
- color: 0xFF000,
241
- timestamp: new Date(),
242
- footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` },
243
- }).send().catch(e => this._debug(e));
244
- return send({
245
- embeds: [{ author: { name: `Ticket Created!`, icon_url: `https://cdn.discordapp.com/emojis/476629550797684736.gif` }, description: channel.toString(), color: 0xFF000 }],
246
- components: [{ type: 1, components: [button({ title: "Go to ticket", url: msg.url })] }]
247
- })
248
- }
249
-
250
- button(options = { style: 3, label: "Create Ticket", emoji: { name: "📩" } }) {
251
- return button({ id: options?.id || this.prefix, style: options.style || 3, title: options.label, emoji: options.emoji });
252
- };
253
-
254
- /**
255
- * @param {object} options
256
- * @param {string} [options.title] The title of the modal submit form
257
- * @param {import("@elara-services/packages").Modal['components']} [options.components]
258
- */
259
- modal(options = { title: "", components: [] }) {
260
- return modal({ id: `${this.prefix}:modal_submit`, title: options?.title || "Create Ticket", components: options?.components?.length >= 1 ? options.components : [{ type: 1, components: [{ type: 4, min_length: 10, max_length: 4000, custom_id: "message", label: "Content", style: 2, placeholder: "What's the ticket about?", required: true }] }] })
261
- }
262
-
263
- async starterMessage(channelId, options) {
264
- let channel = this.options.client.channels.resolve(channelId);
265
- if (!channel) return Promise.reject(`No channel found for: ${channelId}`);
266
- if (!channel.isText()) return Promise.reject(`The channel ID provided isn't a text-based-channel`);
267
- 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})`);
268
- return channel.send({ content: options?.content, files: options?.attachments, embeds: options?.embeds, components: options?.components || [{ type: 1, components: [this.button()] }] })
269
- .then(() => console.log(`Sent the starter message in ${channel.name} (${channel.id})`))
270
- };
271
-
272
- code(id, type = "d") {
273
- try {
274
- const aes = new AES(this.options.encryptToken);
275
- switch (type) {
276
- case "e": return aes.encrypt(id);
277
- case "d": return aes.decrypt(id);
278
- }
279
- } catch {
280
- return id;
281
- }
282
- };
283
-
284
- async fetchMessages(channel, limit = 50, before, after, around) {
285
- if (limit && limit > 100) {
286
- let logs = [];
287
- const get = async (_before, _after) => {
288
- const messages = [...(await channel.messages.fetch({ limit: 100, before: _before || undefined, after: _after || undefined }).catch(() => new Collection())).values()];
289
- if (limit <= messages.length) {
290
- return (_after ? messages.slice(messages.length - limit, messages.length).map((message) => message).concat(logs) : logs.concat(messages.slice(0, limit).map((message) => message)));
291
- }
292
- limit -= messages.length;
293
- logs = (_after ? messages.map((message) => message).concat(logs) : logs.concat(messages.map((message) => message)));
294
- if (messages.length < 100) return logs;
295
- return get((_before || !_after) && messages[messages.length - 1].id, _after && messages[0].id);
296
- };
297
- return get(before, after);
298
- }
299
- return [...(await channel.messages.fetch({ limit, before, after, around }).catch(() => new Collection())).values()];
300
- };
301
-
302
- displayMessages(channel, messages = [], ticketID, type) {
303
- let users = [];
304
- for (const i of messages.values()) {
305
- let f = users.find(c => c.user.id === i.author.id);
306
- if (f) f.count++; else users.push({ user: i.author, count: 1 });
307
- };
308
- 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>`,
309
- ...messages.map(message => {
310
- let str = [
311
- `<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`}">`
312
- ];
313
- if (message.content) {
314
- let content = message.content;
315
- if (message.mentions.users.size) for (const user of message.mentions.users.values()) content = content.replace(new RegExp(`<@!?${user.id}>`, "g"), `<discord-mention type="role" color="${message.guild?.members?.cache?.get?.(user?.id)?.displayHexColor ?? "#ffffff"}">${user.tag}</discord-mention>`);
316
- if (message.mentions.channels.size) for (const channel of message.mentions.channels.values()) content = content.replace(new RegExp(channel.toString(), "g"), `<discord-mention type="channel">${channel.name}</discord-mention>`);
317
- 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>`)
318
- str.push(content)
319
- };
320
- if (message.embeds?.length) {
321
- let arr = [`<discord-embeds slot="embeds">`];
322
- for (const embed of message.embeds) {
323
- let emb = [
324
- `<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}"` : ""}>`
325
- ];
326
- if (embed.description) emb.push(`<discord-embed-description slot="description">${embed.description}</discord-embed-description>`);
327
- if (embed.fields?.length) {
328
- emb.push(`<discord-embed-fields slot="fields">`)
329
- for (const field of embed.fields) emb.push(`<discord-embed-field field-title="${field.name}" ${field.inline ? `inline` : ""}>${field.value}</discord-embed-field>`)
330
- }
331
- arr.push(...emb, `</discord-embed>`)
332
- }
333
- str.push(...arr, "</discord-embeds>")
334
- }
335
- 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>`)
336
- if (message.components?.length) {
337
- let row = [
338
- `<discord-attachments slot="components">`
339
- ],
340
- styles = {
341
- "PRIMARY": "primary",
342
- "SECONDARY": "secondary",
343
- "SUCCESS": "success",
344
- "DANGER": "destructive",
345
- "LINK": "secondary"
346
- }
347
- for (const components of message.components) {
348
- row.push(`<discord-action-row>`)
349
- for (const c of components.components) {
350
- 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>`)
351
- }
352
- row.push(`</discord-action-row>`)
353
- }
354
- if (row.length >= 2) str.push(...row, `</discord-attachments>`)
355
- }
356
- if (message.attachments.size) str.push(message.attachments.map((c) => `<a href="${c.proxyURL}">${c.name}</a>`).join("<br>"))
357
- str.push(`${message.content?.length ? `<br><br>` : ""}<code style="background-color: #36393e; color: white;">ID: ${message.id}</code>`)
358
- return [...str, `</discord-message>`].join(" ");
359
- }),
360
- "</discord-messages>"].join(" ");
361
- };
362
-
363
- async closeTicket({ member, messages, channel, guild, user } = {}) {
364
- const { id, token, username, avatar: avatarURL } = this.webhookOptions;
365
- if (!id || !token) return;
366
- let embeds = [
367
- {
368
- author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
369
- title: "Ticket: Closed",
370
- 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})`,
371
- color: 0xFF0000,
372
- timestamp: new Date(),
373
- footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` },
374
- }
375
- ];
376
- new WebhookClient({ id, token })
377
- .send({
378
- username, avatarURL,
379
- embeds,
380
- files: [{ name: "transcript.txt", attachment: Buffer.from(this.displayMessages(channel, messages.reverse(), channel.name.split("-")[1], this.options.prefix)) }]
381
- })
382
- .then(m => {
383
- 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"}` }];
384
- embeds[0].description += `\n${de.transcript} Transcript: [View here](${components[0].url})`
385
- this.webhook()
386
- .embeds(embeds)
387
- .button({ type: 1, components })
388
- .edit(m.id)
389
- .catch(e => this._debug(e));
390
- if (user) user.send({
391
- embeds: [{
392
- author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
393
- title: `Ticket: Closed`,
394
- color: 0xFF0000,
395
- timestamp: new Date(),
396
- footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` }
397
- }],
398
- components: [{ type: 1, components }]
399
- }).catch(e => this._debug(e));
400
- }).catch(e => this._debug(e));
401
- };
402
-
403
- getSupportIds() {
404
- return {
405
- roles: this.options.support?.roles || this.options.supportRoleIds || [],
406
- users: this.options.support?.users || this.options.supportUserIds || []
407
- }
408
- }
409
-
410
- /** @private */
411
- _debug(...args) {
412
- if (this.options?.debug) console.log(...args);
413
- return null;
414
- }
415
- };
1
+ const v13 = require("./lib/v13");
2
+ const pack = require("./package.json");
3
+ module.exports = (() => {
4
+ let version = null;
5
+ try { version = require("discord.js").version; } catch {}
6
+ if (!version) throw new Error(`[${pack.name}, v${pack.version}]: I was unable to find the discord.js version you're using.`);
7
+ let [ major ] = version.split(".");
8
+ if (major === "13") return v13;
9
+ if ([ "12", "11" ].includes(major)) throw new Error(`[${pack.name}, v${pack.version}]: The discord.js version you're using is outdated and unsupported, please update to v13+`)
10
+ throw new Error(`[${pack.name}, v${pack.version}]: The discord.js version you're using isn't supported by this package. (currently supported: v13)`);
11
+ })();
package/lib/base.js ADDED
@@ -0,0 +1,80 @@
1
+ const { getWebhookInfo, displayMessages, de, webhook } = require("./util"),
2
+ { Interactions: { button, modal } } = require("@elara-services/packages"),
3
+ { WebhookClient } = require("discord.js");
4
+
5
+ module.exports = class Tickets {
6
+ constructor(options) {
7
+ if (typeof options !== "object") throw new Error(`You didn't provide any data in the constructor, fill it out!`);
8
+ if (!("client" in options) || !("prefix" in options) || !("encryptToken" in options)) throw new Error(`You forgot to fill out either 'client', 'prefix' or 'encryptToken'`)
9
+ this.options = options;
10
+ }
11
+ get prefix() { return `system:ticket:${this.options.prefix}`; };
12
+ get webhookOptions() { return getWebhookInfo(this.options); };
13
+ get getSupportIds() {
14
+ return {
15
+ roles: this.options.support?.roles || [],
16
+ users: this.options.support?.users || []
17
+ }
18
+ }
19
+
20
+ /** @private */
21
+ _debug(...args) {
22
+ if (this.options?.debug) console.log(...args);
23
+ return null;
24
+ }
25
+
26
+ button(options = { style: 3, label: "Create Ticket", emoji: { name: "📩" } }) {
27
+ return button({ id: options?.id || this.prefix, style: options.style || 3, title: options.label, emoji: options.emoji });
28
+ };
29
+
30
+ /**
31
+ * @param {object} options
32
+ * @param {string} [options.title] The title of the modal submit form
33
+ * @param {import("@elara-services/packages").Modal['components']} [options.components]
34
+ */
35
+ modal(options = { title: "", components: [] }) {
36
+ return modal({ id: `${this.prefix}:modal_submit`, title: options?.title || "Create Ticket", components: options?.components?.length >= 1 ? options.components : [{ type: 1, components: [{ type: 4, min_length: 10, max_length: 4000, custom_id: "message", label: "Content", style: 2, placeholder: "What's the ticket about?", required: true }] }] })
37
+ };
38
+
39
+ async closeTicket({ member, messages, channel, guild, user, reason } = {}) {
40
+ const { id, token, username, avatar: avatarURL } = this.webhookOptions;
41
+ if (!id || !token) return;
42
+ let embeds = [
43
+ {
44
+ author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
45
+ title: "Ticket: Closed",
46
+ 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})`,
47
+ color: 0xFF0000,
48
+ fields: reason ? [ { name: "Close Reason", value: reason?.slice?.(0, 1024) ?? "No Reason Provided" } ] : undefined,
49
+ timestamp: new Date(),
50
+ footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` },
51
+ }
52
+ ];
53
+ new WebhookClient({ id, token })
54
+ .send({
55
+ username, avatarURL,
56
+ embeds,
57
+ files: [{ name: "transcript.txt", attachment: Buffer.from(displayMessages(channel, messages.reverse(), channel.name.split("-")[1], this.options.prefix)) }]
58
+ })
59
+ .then(m => {
60
+ 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?.first?.()?.url ?? "URL_NOT_FOUND"}` }];
61
+ embeds[0].description += `\n${de.transcript} Transcript: [View here](${components[0].url})`
62
+ webhook(this.webhookOptions)
63
+ .embeds(embeds)
64
+ .button({ type: 1, components })
65
+ .edit(m.id)
66
+ .catch(e => this._debug(e));
67
+ if (user) user.send({
68
+ embeds: [{
69
+ author: { name: guild.name, icon_url: guild.iconURL({ dynamic: true }) },
70
+ title: `Ticket: Closed`,
71
+ color: 0xFF0000,
72
+ fields: reason ? [ { name: "Close Reason", value: reason?.slice?.(0, 1024) ?? "No Reason Provided" } ] : undefined,
73
+ timestamp: new Date(),
74
+ footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` }
75
+ }],
76
+ components: [{ type: 1, components }]
77
+ }).catch(e => this._debug(e));
78
+ }).catch(e => this._debug(e));
79
+ };
80
+ };
package/lib/util.js ADDED
@@ -0,0 +1,143 @@
1
+ const { AES } = require("@elara-services/packages"),
2
+ { Collection } = require("discord.js"),
3
+ Webhook = require("discord-hook");
4
+
5
+ exports.code = (message, type = "d", token) => {
6
+ if (!message || !type) return "";
7
+ if (!token) return message;
8
+ try {
9
+ const aes = new AES(token);
10
+ switch (type) {
11
+ case "e": return aes.encrypt(message);
12
+ case "d": return aes.decrypt(message);
13
+ }
14
+ } catch {
15
+ return message;
16
+ }
17
+ }
18
+
19
+ exports.de = {
20
+ user: "<:Members:860931214232125450>",
21
+ channel: "<:Channel:841654412509839390>",
22
+ transcript: "<:Log:792290922749624320>"
23
+ }
24
+
25
+ exports.fetchMessages = async (channel, limit = 50, before, after, around) => {
26
+ if (limit && limit > 100) {
27
+ let logs = [];
28
+ const get = async (_before, _after) => {
29
+ const messages = [...(await channel.messages.fetch({ limit: 100, before: _before || undefined, after: _after || undefined }).catch(() => new Collection())).values()];
30
+ if (limit <= messages.length) {
31
+ return (_after ? messages.slice(messages.length - limit, messages.length).map((message) => message).concat(logs) : logs.concat(messages.slice(0, limit).map((message) => message)));
32
+ }
33
+ limit -= messages.length;
34
+ logs = (_after ? messages.map((message) => message).concat(logs) : logs.concat(messages.map((message) => message)));
35
+ if (messages.length < 100) return logs;
36
+ return get((_before || !_after) && messages[messages.length - 1].id, _after && messages[0].id);
37
+ };
38
+ return get(before, after);
39
+ }
40
+ return [...(await channel.messages.fetch({ limit, before, after, around }).catch(() => new Collection())).values()];
41
+ };
42
+
43
+ exports.displayMessages = (channel, messages = [], ticketID, type) => {
44
+ let users = [];
45
+ for (const i of messages.values()) {
46
+ let f = users.find(c => c.user.id === i.author.id);
47
+ if (f) f.count++; else users.push({ user: i.author, count: 1 });
48
+ };
49
+ 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>`,
50
+ ...messages.map(message => {
51
+ let str = [
52
+ `<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?.resolve?.(message.author.id)?.displayColor ? channel.guild?.members?.resolve?.(message.author.id)?.displayHexColor : `#ffffff`}">`
53
+ ];
54
+ if (message.content) {
55
+ let content = message.content;
56
+ if (message.mentions.users.size) for (const user of message.mentions.users.values()) content = content.replace(new RegExp(`<@!?${user.id}>`, "g"), `<discord-mention type="role" color="${message.guild?.members?.resolve?.(user?.id)?.displayHexColor ?? "#ffffff"}">${user.tag}</discord-mention>`);
57
+ if (message.mentions.channels.size) for (const channel of message.mentions.channels.values()) content = content.replace(new RegExp(channel.toString(), "g"), `<discord-mention type="channel">${channel.name}</discord-mention>`);
58
+ 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>`)
59
+ str.push(content)
60
+ };
61
+ if (message.embeds?.length) {
62
+ let arr = [`<discord-embeds slot="embeds">`];
63
+ for (const embed of message.embeds) {
64
+ let emb = [
65
+ `<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}"` : ""}>`
66
+ ];
67
+ if (embed.description) emb.push(`<discord-embed-description slot="description">${embed.description}</discord-embed-description>`);
68
+ if (embed.fields?.length) {
69
+ emb.push(`<discord-embed-fields slot="fields">`)
70
+ for (const field of embed.fields) emb.push(`<discord-embed-field field-title="${field.name}" ${field.inline ? `inline` : ""}>${field.value}</discord-embed-field>`)
71
+ }
72
+ arr.push(...emb, `</discord-embed>`)
73
+ }
74
+ str.push(...arr, "</discord-embeds>")
75
+ }
76
+ 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>`)
77
+ if (message.components?.length) {
78
+ let row = [
79
+ `<discord-attachments slot="components">`
80
+ ],
81
+ styles = {
82
+ "PRIMARY": "primary",
83
+ "SECONDARY": "secondary",
84
+ "SUCCESS": "success",
85
+ "DANGER": "destructive",
86
+ "LINK": "secondary"
87
+ }
88
+ for (const components of message.components) {
89
+ row.push(`<discord-action-row>`)
90
+ for (const c of components.components) {
91
+ 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>`)
92
+ }
93
+ row.push(`</discord-action-row>`)
94
+ }
95
+ if (row.length >= 2) str.push(...row, `</discord-attachments>`)
96
+ }
97
+ if (message.attachments.size) str.push(message.attachments.map((c) => `<a href="${c.proxyURL}">${c.name}</a>`).join("<br>"))
98
+ str.push(`${message.content?.length ? `<br><br>` : ""}<code style="background-color: #36393e; color: white;">ID: ${message.id}</code>`)
99
+ return [...str, `</discord-message>`].join(" ");
100
+ }),
101
+ "</discord-messages>"].join(" ");
102
+ };
103
+ /**
104
+ * @param {object} options
105
+ * @param {string} [options.userId]
106
+ * @param {string} [options.token]
107
+ * @param {import("discord.js").Guild} [options.guild]
108
+ * @returns {boolean}
109
+ */
110
+ exports.hasTicket = ({ userId, guild, token, prefix } = {}) => {
111
+ if (!userId || !guild || !token || !prefix || !guild?.channels?.cache?.size) return false;
112
+ let channels = guild.channels.cache.filter(c => c.isText() && c.topic && exports.code(c.topic.split("ID: ")[1], "d", token) === userId && c.name?.match?.(new RegExp(prefix, "gi")));
113
+ if (!channels.size) return false;
114
+ return true;
115
+ };
116
+
117
+ exports.embed = (content, { color, title, guild, footer, author } = {}) => ({
118
+ title: title || "INFO", description: content,
119
+ color: color || 0xFF0000, timestamp: new Date(),
120
+ footer,
121
+ author: author || { name: guild?.name ?? "Tickets", icon_url: guild?.iconURL?.({ dynamic: true }) ?? "https://cdn.discordapp.com/emojis/818757771310792704.png?v=1" }
122
+ });
123
+
124
+ exports.webhook = (options) => {
125
+ const { id, token, username, avatar } = options;
126
+ return new Webhook(`https://discord.com/api/webhooks/${id}/${token}`, { username, avatar_url: avatar });
127
+ };
128
+
129
+ exports.getWebhookInfo = (options) => {
130
+ return {
131
+ id: options.webhook?.id,
132
+ token: options.webhook?.token,
133
+ username: options.webhook?.username || "Tickets",
134
+ avatar: options.webhook?.avatar || "https://cdn.discordapp.com/emojis/818757771310792704.png?v=1"
135
+ }
136
+ };
137
+
138
+ exports.getAppealServer = ({ appeals, client } = {}) => {
139
+ if (!appeals?.enabled || !appeals?.mainserver?.checkIfBanned) return null;
140
+ let server = client.guilds.resolve(appeals.mainserver?.id);
141
+ if (!server?.available) return null;
142
+ return server;
143
+ }
package/lib/v13.js ADDED
@@ -0,0 +1,218 @@
1
+ const { MessageEmbed } = require("discord.js"),
2
+ base = require("./base"),
3
+ { de, code, fetchMessages, hasTicket, embed, webhook, getAppealServer } = require("./util"),
4
+ { generate } = require("shortid"),
5
+ { Interactions: { button, modal } } = require("@elara-services/packages");
6
+
7
+ module.exports = class Tickets extends base {
8
+ constructor(options = {}) {
9
+ super(options);
10
+ };
11
+ /**
12
+ * @param {import("discord.js").Interaction} int
13
+ */
14
+ async run(int) {
15
+ if (int?.isButton?.() || int?.isModalSubmit()) {
16
+ let { guild, channel, member, customId } = int,
17
+ category = guild?.channels?.resolve?.(this.options.ticket?.category || this.options.ticketCategory || channel?.parentId);
18
+ if (!guild || !guild.available || !channel || !member || !category) return;
19
+
20
+ /**
21
+ * @param {import("discord.js").InteractionDeferReplyOptions|import("discord.js").InteractionReplyOptions} options
22
+ * @param {boolean} edit
23
+ * @param {boolean} defer
24
+ */
25
+ const send = async (options = {}, defer = false) => {
26
+ if (defer) return int.deferReply(options).catch(e => this._debug(e));
27
+ if (int.replied || int.deferred) return int.editReply(options).catch(e => this._debug(e));
28
+ return int.reply(options).catch(e => this._debug(e));
29
+ };
30
+ switch (customId) {
31
+ case this.prefix: {
32
+ if (this.options?.ticket?.limitOnePerUser && hasTicket({ userId: member.id, guild, token: this.options.encryptToken, prefix: this.options.prefix })) return send({ embeds: [ embed(`❌ You can only create one (${this.options.prefix}) ticket at a time.`, { guild }) ], ephemeral: true })
33
+ if (this.options.support?.ignore?.length) {
34
+ if (this.options.support.ignore.some(c => member.roles.cache.has(c))) return send({ embeds: [embed(`❌ You can no longer create tickets, if you believe this is a mistake contact one of the staff members.`)], ephemeral: true });
35
+ }
36
+ if (this.options.modal?.enabled) return int.showModal(this.modal({ title: this.options.modal.title, components: this.options.modal.questions?.length >= 1 ? 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)}` }] })) : [] })).catch(e => this._debug(e));
37
+ return this.handleCreate({ guild, member, category, send })
38
+ };
39
+
40
+ case `${this.prefix}:close`: {
41
+ if (this.options.support?.canOnlyCloseTickets && !member.permissions.has("MANAGE_GUILD")) {
42
+ if (!this.getSupportIds.users?.includes?.(member.id)) {
43
+ if (!this.getSupportIds.roles?.some?.(c => member.roles.cache.has(c))) return send({ ephemeral: true, embeds: [{ author: { name: `Only support staff can close tickets`, iconURL: "https://cdn.discordapp.com/emojis/781955502035697745.gif" }, color: 0xFF0000 }] })
44
+ }
45
+ }
46
+ return send({
47
+ ephemeral: true,
48
+ embeds: [ embed(`🤔 Are you sure you want to close the ticket?`, { color: 0xFF000, guild }) ],
49
+ components: [
50
+ { type: 1, components: [
51
+ button({ title: "Yes, close the ticket!", style: 3, emoji: { id: "807031399563264030" }, id: `${this.prefix}:close:confirm:${code(channel.topic?.split?.("ID: ")?.[1], "d", this.options.encryptToken)}${this.options?.ticket?.closeReason ? `:modal_submit` : ""}` })
52
+ ]
53
+ }]
54
+ })
55
+ }
56
+
57
+ case `${this.prefix}:modal_submit`: {
58
+ let [embed, fields, split] = [new MessageEmbed().setColor("ORANGE").setTimestamp().setTitle(`For Responses`).setFooter({ text: `ID: ${member.id}` }).setAuthor({ name: member.user.username, iconURL: member.user.displayAvatarURL({ dynamic: true }) }), [], false];
59
+ for (const c of int.fields.components) {
60
+ for (const cc of c.components) {
61
+ if (cc.value && cc.customId) {
62
+ fields.push({ name: cc.customId, value: cc.value });
63
+ if (cc.value.length <= 1024) embed.addField(cc.customId, cc.value);
64
+ else split = true
65
+ }
66
+ }
67
+ }
68
+ if (embed.length >= 6000 || split) {
69
+ return this.handleCreate({
70
+ guild, member, category, send, embeds: fields.map((v, i) => ({
71
+ title: `Form Response: ${v.name}`, color: embed.color, description: v.value,
72
+ author: i === 0 ? { name: member.user.username, iconURL: member.user.displayAvatarURL({ dynamic: true }) } : undefined,
73
+ timestamp: fields.length - 1 === i ? new Date() : undefined,
74
+ footer: fields.length - 1 === i ? { text: `ID: ${member.id}` } : undefined
75
+ }))
76
+ })
77
+ };
78
+ return this.handleCreate({ guild, member, category, send, embeds: [embed] })
79
+ }
80
+ };
81
+ if (customId.startsWith(`${this.prefix}:close:confirm`)) {
82
+ if (customId.includes(`:modal_submit`)) {
83
+ return int.showModal(modal({
84
+ id: `${customId.split(":modal_submit")[0]}:modal_complete`,
85
+ title: "Reason",
86
+ components: [ { type: 1, components: [ { type: 4, custom_id: "reason", label: "Reason", style: 2, min_length: 1, max_length: 1024, required: true, placeholder: "What's the reason for closing the ticket?" } ] } ]
87
+ }))
88
+ }
89
+ await send({ ephemeral: true }, true);
90
+ let reason = "No Reason Provided";
91
+ if (int.isModalSubmit()) reason = int.fields.getTextInputValue("reason")
92
+ let user = await this.options.client.users.fetch(customId.split("close:confirm:")[1].replace(/:modal_complete/gi, "")).catch(e => this._debug(e));
93
+ if (!user) return send({ embeds: [ embed(`❌ I was unable to fetch the user that opened the ticket.`) ] });
94
+ let messages = await fetchMessages(channel, 5000);
95
+ if (!messages?.length) return send({ embeds: [ embed(`❌ I was unable to close the ticket, I couldn't fetch the messages in this channel.`) ] });
96
+ let closed = await channel.delete(`${member.user.tag} (${member.id}) closed the ticket.`).catch(e => this._debug(e));
97
+ if (!closed) return send({ embeds: [ embed(`❌ I was unable to delete the channel and close the ticket.`) ] });
98
+ return this.closeTicket({ channel, guild, user, member, messages, reason });
99
+ };
100
+
101
+ if (customId.startsWith("unban:")) {
102
+ if (!int.memberPermissions?.has?.([ "BAN_MEMBERS" ])) return send({ embeds: [ embed(`❌ You need (Ban Members) in this server to complete this action!`) ], ephemeral: true });
103
+ let server = getAppealServer(this.options);
104
+ if (!server) return send({ embeds: [embed(`❌ I was unable to find the appeal server!`)], ephemeral: true });
105
+ let mod = server.members.resolve(member.id) || await server.members.fetch(member.id).catch(e => this._debug(e));
106
+ if (!mod) return send({ embeds: [embed(`❌ I was unable to find you in ${server.name}!`)], ephemeral: true });
107
+ if (!mod.permissions?.has?.([ "BAN_MEMBERS" ])) return send({ embeds: [embed(`❌ You need (Ban Members) in ${server.name} to complete this action!`)], ephemeral: true });
108
+ return int.showModal(modal({
109
+ id: `unban_modal:${customId.split(":")[1]}`,
110
+ title: `Unban From ${server.name}`,
111
+ components: [ { type: 1, components: [ { type: 4, label: "Reason", custom_id: "reason", style: 2, min_length: 1, max_length: 512, required: true, value: `No Reason Provided | By: ${member.user.tag} (${member.id})` } ] } ]
112
+ })).catch(e => this._debug(e));
113
+ }
114
+
115
+ if (customId.startsWith("unban_modal:")) {
116
+ await send({ ephemeral: true }, true);
117
+ const [ , id ] = customId.split(":");
118
+ if (!int.memberPermissions?.has?.([ "BAN_MEMBERS" ])) return send({ embeds: [ embed(`❌ You need (Ban Members) in this server to complete this action!`)] });
119
+ let server = getAppealServer(this.options);
120
+ if (!server) return send({ embeds: [embed(`❌ I was unable to find the appeal server!`)] });
121
+ let mod = server.members.resolve(member.id) || await server.members.fetch(member.id).catch(e => this._debug(e));
122
+ if (!mod) return send({ embeds: [embed(`❌ I was unable to find you in ${server.name}!`)] });
123
+ if (!mod.permissions?.has?.([ "BAN_MEMBERS" ])) return send({ embeds: [embed(`❌ You need (Ban Members) in ${server.name} to complete this action!`)] });
124
+ let isBanned = await server.bans.fetch({ user: id, force: true }).catch(e => this._debug(e));
125
+ if (!isBanned) return send({ embeds: [embed(`❌ User (<@${id}>) isn't banned in the main server!`)] });
126
+ return server.bans.remove(id, int.fields.getTextInputValue("reason") ?? `No Reason Provided | By: ${member.user.tag} (${member.id})`)
127
+ .then(() => {
128
+ int.message.edit({ components: [ { type: 1, components: [ button({ title: "Unbanned!", style: 3, id: "_ _", disabled: true, emoji: { id: `476629550797684736` } }) ] } ] }).catch(e => this._debug(e));
129
+ send({ embeds: [embed(`✅ Successfully unbanned <@${id}> from ${server.name}!`)] })
130
+ })
131
+ .catch(e => send({ embeds: [ { title: "ERROR", fields: [ { name: "\u200b", value: `❌ Unable to unban <@${id} from ${server.name}!` } ], description: `\`\`\`js\n${e.message ?? e.stack}\`\`\``, color: 0xFF0000 } ] }))
132
+ }
133
+ };
134
+ };
135
+
136
+ /** @private */
137
+ async handleCreate({ guild, member, category, send, embeds = [] } = {}) {
138
+ await send({ ephemeral: true }, true);
139
+ let [ permissions, allow, { appeals }, sendBanReason ] = [
140
+ [],
141
+ ["ADD_REACTIONS", "ATTACH_FILES", "CREATE_INSTANT_INVITE", "EMBED_LINKS", "READ_MESSAGE_HISTORY", "VIEW_CHANNEL", "USE_EXTERNAL_EMOJIS", "SEND_MESSAGES"],
142
+ this.options ?? {},
143
+ null
144
+ ];
145
+ if (this.getSupportIds.roles.length) for (const sup of this.getSupportIds.roles) {
146
+ let role = guild.roles.resolve(sup);
147
+ if (role) permissions.push({ type: "role", id: sup, allow });
148
+ };
149
+
150
+ if (this.getSupportIds.users.length) for (const uId of this.getSupportIds.users) {
151
+ let member = guild.members.resolve(uId) || await guild.members.fetch(uId).catch((e) => {
152
+ if (e?.stack?.includes?.("Unknown Member")) this.options.support.users = this.options.support.users.filter(c => c !== uId);
153
+ return this._debug(e);
154
+ });
155
+ if (member) permissions.push({ type: "member", id: uId, allow });
156
+ }
157
+ if (appeals?.enabled) {
158
+ let server = getAppealServer(this.options);
159
+ if (server) {
160
+ let ban = await server.bans.fetch({ user: member.id, force: true }).catch(e => this._debug(e));
161
+ if (!ban) return send(
162
+ typeof appeals.embeds?.not_banned === "object" ?
163
+ appeals.embeds.not_banned :
164
+ { embeds: [ embed(`❌ You can't open this ticket due to you not being banned in the main server!`, { guild, color: 0xFF0000 }) ] }
165
+ );
166
+ sendBanReason = {
167
+ embeds: [ embed(ban?.reason ?? "No Reason Provided", { title: "Ban Reason", guild: server }) ],
168
+ components: [
169
+ {
170
+ type: 1, components: [
171
+ button({ id: `unban:${member.id}`, style: 4, title: "Unban", emoji: { name: "🔒" } })
172
+ ]
173
+ }
174
+ ]
175
+ }
176
+ }
177
+ }
178
+ let channel = await guild.channels.create(`${this.options.prefix}-${generate().slice(0, 5).replace(/-|_/g, "")}`, {
179
+ type: "GUILD_TEXT", parent: category, reason: `Ticket created by: @${member.user.tag} (${member.id})`,
180
+ topic: `ID: ${code(member.id, "e", this.options.encryptToken)}`,
181
+ permissionOverwrites: [
182
+ { 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"] },
183
+ { 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"] },
184
+ { type: "role", id: guild.id, deny: ["VIEW_CHANNEL"] },
185
+ ...permissions
186
+ ]
187
+ }).catch(e => this._debug(e));
188
+ if (!channel) return send({ embeds: [embed(`❌ I was unable to create the ticket channel, if this keeps happening contact one of the staff members via their DMs!`)] });
189
+ let msg = await channel.send({
190
+ content: (this.options.ticket?.open || 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.`,
191
+ embeds: (this.options.ticket?.open || this.options.ticketOpen)?.embeds || [ embed(undefined, { title: `Support will be with you shortly.`, color: 0xF50DE3, guild, footer: { text: `To close the ticket, press the button below` } }) ],
192
+ components: [ { type: 1, components: [ button({ id: `${this.prefix}:close`, title: `Close Ticket`, style: 4, emoji: { name: "🔒" } }) ] }]
193
+ }).catch(e => this._debug(e));
194
+ if (!msg) return null;
195
+ if (sendBanReason) await channel.send(sendBanReason).catch(e => this._debug(e));
196
+ if (embeds?.length <= 10) for await (const embed of embeds) await channel.send({ embeds: [embed] }).catch(e => this._debug(e));
197
+ if (this.webhookOptions.id && this.webhookOptions.token) webhook(this.webhookOptions)
198
+ .embed(embed(`${de.user} User: ${member.user.toString()} \`@${member.user.tag}\` (${member.id})\n${de.channel} Channel: \`#${channel.name}\` (${channel.id})`, {
199
+ title: `TIcket: Opened`,
200
+ color: 0xFF000,
201
+ footer: { text: `Ticket ID: ${channel.name.split("-")[1]}` },
202
+ guild
203
+ })).send().catch(e => this._debug(e));
204
+ return send({
205
+ embeds: [ embed(channel.toString(), { color: 0xFF000, author: { name: `Ticket Created!`, icon_url: `https://cdn.discordapp.com/emojis/476629550797684736.gif` } }) ],
206
+ components: [{ type: 1, components: [button({ title: "Go to ticket", url: msg.url })] }]
207
+ })
208
+ }
209
+
210
+ async starterMessage(channelId, options) {
211
+ let channel = this.options.client.channels.resolve(channelId);
212
+ if (!channel) return Promise.reject(`No channel found for: ${channelId}`);
213
+ if (!channel.isText()) return Promise.reject(`The channel ID provided isn't a text-based-channel`);
214
+ 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})`);
215
+ return channel.send({ content: options?.content, files: options?.attachments, embeds: options?.embeds, components: options?.components || [{ type: 1, components: [this.button()] }] })
216
+ .then(() => console.log(`Sent the starter message in ${channel.name} (${channel.id})`))
217
+ };
218
+ };
package/package.json CHANGED
@@ -1,18 +1,24 @@
1
1
  {
2
2
  "name": "@elara-services/tickets",
3
- "version": "2.3.0",
3
+ "version": "3.0.1",
4
4
  "description": "Helper for tickets",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "typings": "index.d.ts",
8
8
  "author": "SUPERCHIEFYT (Elara-Discord-Bots, Elara-Services)",
9
9
  "license": "MIT",
10
+ "scripts": {
11
+ "lint": "eslint ."
12
+ },
10
13
  "optionalDependencies": {
11
- "discord.js": "13.8.1"
14
+ "discord.js": "13.9.0"
12
15
  },
13
16
  "dependencies": {
14
17
  "@elara-services/packages": "5.0.0",
15
18
  "discord-hook": "2.0.0",
16
19
  "shortid": "2.2.16"
20
+ },
21
+ "devDependencies": {
22
+ "eslint": "8.20.0"
17
23
  }
18
24
  }