@elara-services/tickets
Version:
Helper for tickets
578 lines (569 loc) • 27 kB
JavaScript
const { EmbedBuilder: MessageEmbed, InteractionType, ChannelType, ComponentType } = require("discord.js"),
base = require("./base"),
{ de, code, fetchMessages, hasTicket, embed, webhook, getAppealServer, perms, defs } = require("./util"),
{ generate, parser, discord, is } = require("@elara-services/utils"),
{
Interactions: { button },
} = require("@elara-services/packages");
module.exports = class Tickets extends base {
/**
* @param {import("@elara-services/tickets").TicketOptions} options
*/
constructor(options = {}) {
super(options);
this.options = options;
}
/**
* @param {import("discord.js").Interaction} int
*/
async run(int) {
if (!int) {
return;
}
if (int.isStringSelectMenu()) {
if (int.values[0] === this.prefix) {
int.customId = this.prefix;
int.componentType = ComponentType.Button;
}
}
if (int.isButton() || int.type === InteractionType.ModalSubmit) {
if (int.customId.startsWith("transcript")) {
return this.handleTicketButton(int);
}
let { guild, channel, member, customId } = int,
category = guild?.channels?.resolve?.(this.options.ticket?.category || channel?.parentId);
if (!guild?.available || !channel || !member || !category) {
return;
}
/**
* @param {import("discord.js").InteractionDeferReplyOptions|import("discord.js").InteractionReplyOptions} options
* @param {boolean} edit
* @param {boolean} defer
*/
const send = async (options = {}, defer = false) => {
if (defer) {
return int.deferReply(options).catch((e) => this._debug(e));
}
if (int.replied || int.deferred) {
return int.editReply(options).catch((e) => this._debug(e));
}
return int.reply(options).catch((e) => this._debug(e));
};
switch (customId) {
case this.prefix: {
if (
this.options?.ticket?.limitOnePerUser &&
hasTicket({
userId: member.id,
guild,
token: this.options.encryptToken,
prefix: this.options.prefix,
})
) {
return send({
embeds: [embed(this.str("TICKET_LIMIT_ONE_PER_USER")(this.options.prefix), { guild, str: (name) => this.str(name, this?.options?.lang) })],
ephemeral: true,
});
}
if (this.options.support?.ignore?.length) {
if (this.options.support.ignore.some((c) => member.roles?.cache?.has?.(c))) {
return send({
embeds: [embed(this.str("TICKET_BLOCKED"), { str: (name) => this.str(name, this?.options?.lang) })],
ephemeral: true,
});
}
}
if (this.options.modal?.enabled) {
return int
.showModal(
defs.modals.custom(
{
title: this.options.modal?.title,
components: defs.getModalComponents(this.options.modal.questions || []),
},
{ prefix: this.prefix, str: (name) => this.str(name, this?.options?.lang) },
),
)
.catch((e) => this._debug(e));
}
return this.handleCreate({ guild, member, category, send });
}
case `${this.prefix}:close`: {
if (this.options.support?.canOnlyCloseTickets && !member.permissions.has(perms.manage.guild)) {
if (!this.getSupportIds.users?.includes?.(member.id)) {
if (!this.getSupportIds.roles?.some?.((c) => member.roles?.cache?.has?.(c))) {
return send({
ephemeral: true,
embeds: [
{
author: {
name: this.str("ONLY_SUPPORT"),
iconURL: "https://cdn.discordapp.com/emojis/781955502035697745.gif",
},
color: 0xff0000,
},
],
});
}
}
}
const hasEmbeds = this.options.ticket?.close?.confirm?.embeds || [];
let embs = await Promise.all(
(hasEmbeds?.length
? hasEmbeds
: [
embed(undefined, {
description: this.str("TICKET_CLOSE_CONFIRM"),
title: `INFO`,
color: 0xff000,
guild,
str: (name) => this.str(name, this?.options?.lang),
}),
]
).map((c) => parser(c, { guild, member, user: member.user })),
);
const content = this.options.ticket?.close?.confirm?.content || undefined;
if (!is.array(embs) && !is.string(content)) {
embs = [
await parser(
embed(undefined, {
description: this.str("TICKET_CLOSE_CONFIRM"),
title: `INFO`,
color: 0xff000,
guild,
str: (name) => this.str(name, this?.options?.lang),
}),
{
guild,
member,
user: member.user,
},
),
];
}
return send({
ephemeral: true,
content,
embeds: embs,
components: [
{
type: 1,
components: [
button({
title: this.str("TICKET_CLOSE_CONFIRM_BUTTON"),
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` : ""}`,
}),
],
},
],
});
}
case `${this.prefix}:modal_submit`: {
let [embed, fields, split] = [
new MessageEmbed()
.setColor("Orange")
.setTimestamp()
.setTitle(this.str("FORM_RESPONSES"))
.setFooter({ text: this.str("ID")(member.id) })
.setAuthor({
name: member.user.username,
iconURL: member.user.displayAvatarURL({ dynamic: true }),
}),
[],
false,
];
for (const c of int.fields.components) {
for (const cc of c.components) {
if (cc.value && cc.customId) {
fields.push({ name: cc.customId, value: cc.value });
if (cc.value.length <= 1024) {
embed.addFields({ name: cc.customId, value: cc.value });
} else {
split = true;
}
}
}
}
if (embed.length >= 6000 || split) {
return this.handleCreate({
guild,
member,
category,
send,
embeds: fields.map((v, i) => ({
title: `${this.str("FORM_RESPONSE")}: ${v.name}`,
color: embed.color,
description: v.value,
author:
i === 0
? {
name: member.user.username,
iconURL: member.user.displayAvatarURL({
dynamic: true,
}),
}
: undefined,
timestamp: fields.length - 1 === i ? new Date() : undefined,
footer: fields.length - 1 === i ? { text: this.str("ID")(member.id) } : undefined,
})),
});
}
return this.handleCreate({
guild,
member,
category,
send,
embeds: [embed],
});
}
}
if (customId.startsWith(`${this.prefix}:close:confirm`)) {
if (customId.includes(`:modal_submit`)) {
return int.showModal(defs.modals.reason(customId, (name) => this.str(name, this?.options?.lang))).catch((e) => this._debug(e));
}
await send({ ephemeral: true }, true);
let reason = this.str("NO_REASON");
if (int.type === InteractionType.ModalSubmit) {
reason = int.fields.getTextInputValue("reason");
}
const user = await discord.user(this.options.client, customId.split("close:confirm:")[1].replace(/:modal_complete/gi, ""), { fetch: true, mock: false });
if (!user) {
return send({
embeds: [embed(this.str("NO_USER_FOUND"), { str: (name) => this.str(name, this?.options?.lang) })],
});
}
let messages = await fetchMessages(channel, 5000);
if (!messages?.length) {
return send({
embeds: [embed(this.str("NO_MESSAGES_FETCHED"), { str: (name) => this.str(name, this?.options?.lang) })],
});
}
let closed = await channel.delete(`${member.user.tag} (${member.id}) closed the ticket.`).catch((e) => this._debug(e));
if (!closed) {
return send({
embeds: [embed(this.str("NO_CHANNEL_DELETE"), { str: (name) => this.str(name, this?.options?.lang) })],
});
}
return this.closeTicket({
channel,
guild,
user,
member,
messages,
reason,
});
}
if (customId.startsWith(`${this.prefix}:unban:`)) {
if (!int.memberPermissions?.has?.(perms.members.ban)) {
return send({
embeds: [embed(this.str("NO_BAN_PERMS_USER"), { str: (name) => this.str(name, this?.options?.lang) })],
ephemeral: true,
});
}
let server = getAppealServer(this.options);
if (!server) {
return send({
embeds: [embed(this.str("NO_APPEAL_SERVER_FOUND"), { str: (name) => this.str(name, this?.options?.lang) })],
ephemeral: true,
});
}
const mod = await discord.member(server, member.id, true);
if (!mod) {
return send({
embeds: [embed(this.str("NOT_FOUND_IN_APPEAL_SERVER")(server), { str: (name) => this.str(name, this?.options?.lang) })],
ephemeral: true,
});
}
if (!mod.permissions?.has?.(perms.members.ban)) {
return send({
embeds: [embed(this.str("NO_BAN_PERMS_USER_IN_APPEAL_SERVER")(server), { str: (name) => this.str(name, this?.options?.lang) })],
ephemeral: true,
});
}
return int.showModal(defs.modals.unbanReason(customId, server, (name) => this.str(name, this?.options?.lang), member, this.prefix)).catch((e) => this._debug(e));
}
if (customId.startsWith(`${this.prefix}:unban_modal:`)) {
await send({ ephemeral: true }, true);
const [, , id] = customId.split(":");
if (!int.memberPermissions?.has?.(perms.members.ban)) {
return send({
embeds: [embed(this.str("NO_BAN_PERMS_USER"), { str: (name) => this.str(name, this?.options?.lang) })],
});
}
let server = getAppealServer(this.options);
if (!server) {
return send({
embeds: [embed(this.str("NO_APPEAL_SERVER_FOUND"), { str: (name) => this.str(name, this?.options?.lang) })],
});
}
const mod = await discord.member(server, member.id, true);
if (!mod) {
return send({
embeds: [embed(this.str("NOT_FOUND_IN_APPEAL_SERVER")(server), { str: (name) => this.str(name, this?.options?.lang) })],
});
}
if (!mod.permissions?.has?.(perms.members.ban)) {
return send({
embeds: [embed(this.str("NO_BAN_PERMS_USER_IN_APPEAL_SERVER")(server), { str: (name) => this.str(name, this?.options?.lang) })],
});
}
let isBanned = await server.bans.fetch({ user: id, force: true }).catch((e) => this._debug(e));
if (!isBanned) {
return send({
embeds: [embed(this.str("USER_NOT_BANNED")(id), { str: (name) => this.str(name, this?.options?.lang) })],
});
}
return server.bans
.remove(id, int.fields.getTextInputValue("reason") ?? `${this.str("NO_REASON")} | ${this.str("BY")}: ${member.user.tag} (${member.id})`)
.then(() => {
int.message
.edit({
components: [
{
type: 1,
components: [
button({
title: this.str("UNBANNED"),
style: 3,
id: "_ _",
disabled: true,
emoji: { id: `476629550797684736` },
}),
],
},
],
})
.catch((e) => this._debug(e));
send({
embeds: [embed(this.str("UNBAN_SUCCESS")(id, server.name), { str: (name) => this.str(name, this?.options?.lang) })],
});
})
.catch((e) =>
send({
embeds: [
{
title: this.str("ERROR"),
fields: [
{
name: "\u200b",
value: this.str("UNBAN_FAILED")(id, server.name),
},
],
description: `\`\`\`js\n${e.message ?? e.stack}\`\`\``,
color: 0xff0000,
},
],
}),
);
}
}
}
/**
* @param {object} [opts]
* @param {import("discord.js").Guild} opts.guild
* @param {import("discord.js").GuildMember} opts.member
* @param {string} opts.category
* @param {Function} opts.send
* @param {import("discord.js").APIEmbed[]} opts.embeds
* @private
*/
async handleCreate({ guild, member, category, send, embeds = [] } = {}) {
await send({ ephemeral: true }, true);
let [permissions, { appeals }, sendBanReason] = [[], this.options ?? {}, null];
if (this.getSupportIds.roles.length) {
for (const sup of this.getSupportIds.roles) {
let role = guild.roles.resolve(sup);
if (role) {
permissions.push({
type: "role",
id: sup,
allow: perms.allows.support,
});
}
}
}
if (this.getSupportIds.users.length) {
for await (const uId of this.getSupportIds.users) {
const member = await discord.member(guild, uId, true);
if (member) {
permissions.push({
type: "member",
id: uId,
allow: perms.allows.support,
});
}
}
}
if (appeals?.enabled) {
let server = getAppealServer(this.options);
if (server) {
let ban = await server.bans.fetch({ user: member.id, force: true }).catch((e) => this._debug(e));
if (!ban) {
return send(
typeof appeals.embeds?.not_banned === "object"
? await parser(appeals.embeds.not_banned, { guild: server, member, user: member.user })
: {
embeds: [
embed(this.str("NOT_BANNED_MAIN_SERVER"), {
guild,
color: 0xff0000,
str: (name) => this.str(name, this?.options?.lang),
}),
],
},
);
}
sendBanReason = {
embeds: [
embed(ban?.reason ?? this.str("NO_REASON"), {
title: this.str("BAN_REASON"),
guild: server,
str: (name) => this.str(name, this?.options?.lang),
}),
],
components: [
{
type: 1,
components: [
button({
id: `${this.prefix}:unban:${member.id}`,
style: 4,
title: this.str("UNBAN"),
emoji: { name: "🔒" },
}),
],
},
],
};
}
}
let channel = await guild.channels
.create({
name: `${this.options.prefix}-${generate(5)}`,
type: ChannelType.GuildText,
parent: category,
reason: `${this.str("OPEN_TICKET_AUDIT_REASON")} @${member.user.tag} (${member.id})`,
topic: `ID: ${code(member.id, "e", this.options.encryptToken)}`,
permissionOverwrites: [
{
type: "member",
id: this.options.client.user.id,
allow: perms.allows.client,
},
{
type: "member",
id: member.id,
allow: perms.allows.ticketUser,
deny: perms.denied.ticketUser,
},
{ type: "role", id: guild.id, deny: perms.denied.all },
...permissions,
],
})
.catch((e) => this._debug(e));
if (!channel) {
return send({ embeds: [embed(this.str("NO_CHANNEL_CREATE"), { str: (name) => this.str(name, this?.options?.lang) })] });
}
let embs = await Promise.all(
(
this.options.ticket?.open?.embeds || [
embed(undefined, {
title: this.str("OPEN_TICKET_MESSAGE"),
color: 0xf50de3,
guild,
footer: { text: this.str("OPEN_TICKET_MESSAGE_FOOTER") },
str: (name) => this.str(name, this?.options?.lang),
}),
]
).map((c) => parser(c, { guild, member, user: member.user })),
);
const content = this.options.ticket?.open?.content || `${member.user.toString()} ${this.str("OPEN_TICKET_MESSAGE_CONTENT")}`;
let msg = await channel
.send({
content: await parser(content, { guild, member, user: member.user }),
embeds: embs,
components: [
{
type: 1,
components: [
button({
id: `${this.prefix}:close`,
title: this.str("CLOSE_TICKET"),
style: 4,
emoji: { name: "🔒" },
}),
],
},
],
allowedMentions: {
users: [member.id, ...this.getSupportIds.users],
roles: [...this.getSupportIds.roles],
},
})
.catch((e) => this._debug(e));
if (!msg) {
return null;
}
if (sendBanReason) {
await channel.send(sendBanReason).catch((e) => this._debug(e));
}
if (embeds?.length <= 10) {
for await (const embed of embeds) {
await channel.send({ embeds: [embed] }).catch((e) => this._debug(e));
}
}
if (this.options.ticket?.supportCommentThread === true) {
const thread = await channel.threads
.create({
name: this.str("SUPPORT_TALK"),
invitable: false,
type: 12,
})
.catch((e) => this._debug(e));
if (thread && !this.getSupportIds.empty) {
await thread
.send({
content: `${this.getSupportIds.roles.length ? this.getSupportIds.roles.map((c) => `<@&${c}>`).join(" ") : ""}${this.getSupportIds.users.length ? `${this.getSupportIds.roles.length ? ` | ` : ""}${this.getSupportIds.users.map((c) => `<@${c}>`).join(" ")}` : ""}`,
})
.catch((e) => this._debug(e));
}
}
const webOpt = await this.webhookOptions;
if (webOpt.id && webOpt.token) {
webhook(webOpt)
.embed(
embed(`${de.user} ${this.str("USER")}: ${member.user.toString()} \`@${member.user.tag}\` (${member.id})\n${de.channel} ${this.str("CHANNEL")}: \`#${channel.name}\` (${channel.id})`, {
title: this.str("OPEN_TICKET_TITLE"),
color: 0xff000,
footer: {
text: `${this.str("TICKET_ID")} ${channel.name.split("-")[1]}`,
},
guild,
str: (name) => this.str(name, this?.options?.lang),
}),
)
.send()
.catch((e) => this._debug(e));
}
return send({
embeds: [
embed(channel.toString(), {
color: 0xff000,
author: {
name: this.str("OPEN_TICKET_CREATE"),
icon_url: `https://cdn.discordapp.com/emojis/476629550797684736.gif`,
},
str: (name) => this.str(name, this?.options?.lang),
}),
],
components: [
{
type: 1,
components: [button({ title: this.str("GO_TO_TICKET"), url: msg.url })],
},
],
});
}
};