@elara-services/tickets
Version:
Helper for tickets
318 lines (306 loc) • 12.4 kB
JavaScript
const { getWebhookInfo, displayMessages, de, getString, generateHTMLPage } = require("./util");
const {
Interactions: { button },
ButtonStyle,
} = require("@elara-services/packages");
const { is, status, isV13, embedComment, log, emitPackageMessage } = require("@elara-services/utils");
const { WebhookClient } = require("discord.js");
const { name, version, funding } = require("../package.json");
const { inspect } = require("util");
const required = ["client", "prefix", "encryptToken"];
const moment = require("moment");
module.exports = class Tickets {
/**
* @param {import("@elara-services/tickets").TicketOptions} options
*/
constructor(options) {
if (!is.object(options)) {
throw new Error(this.str("NO_CONSTRUCTOR_OPTIONS")());
}
for (const r of required) {
if (!(r in options)) {
throw new Error(this.str("NO_CONSTRUCTOR_OPTIONS")(`'${r}'`));
}
}
this.options = options;
if (options.suppressPatreon !== true) {
emitPackageMessage(`${name} - thanks`, () => {
log(`[${name}, v${version}]: Thanks for using the package!`, `Please support the packages via ${funding.map((c) => c.url).join(" OR ")}`);
});
}
}
get prefix() {
return `system:ticket:${this.options.prefix}`;
}
get webhookOptions() {
return getWebhookInfo(this.options, this.str("TICKETS"));
}
get getSupportIds() {
return {
roles: this.options.support?.roles || [],
users: this.options.support?.users || [],
empty: [...(this.options.support?.roles || []), ...(this.options.support?.users || [])].length ? false : true,
};
}
/**
* @param {import("discord.js").Interaction} interation
* @param {Tickets[]} tickets
*/
runMany(interation, tickets = []) {
if (!interation || !is.array(tickets)) {
return this;
}
for (const ticket of tickets) {
ticket.run(interation);
}
return this;
}
get manage() {
return {
add: async (channelId, userId, permType = "member") => {
const channel = this.options.client.channels.resolve(channelId);
if (!channel) {
return status.error(`Unable to find (${channelId}) channel`);
}
let mod = permType === "mod";
let perms = {};
if (isV13()) {
perms = {
VIEW_CHANNEL: true,
SEND_MESSAGES: true,
READ_MESSAGE_HISTORY: true,
ATTACH_FILES: true,
EMBED_LINKS: true,
USE_EXTERNAL_EMOJIS: true,
};
if (mod) {
perms["MANAGE_MESSAGES"] = true;
perms["MANAGE_THREADS"] = true;
}
} else {
perms = {
ViewChannel: true,
SendMessages: true,
ReadMessageHistory: true,
AttachFiles: true,
EmbedLinks: true,
UseExternalEmojis: true,
};
if (mod) {
perms["ManageMessages"] = true;
perms["ManageThreads"] = true;
}
}
return channel.permissionOverwrites
.edit(userId, perms, {
reason: `Added to the ticket.`,
})
.then(() => status.success(`Added to the ticket.`))
.catch((e) => status.error(e.message || e));
},
remove: async (channelId, userId) => {
const channel = this.options.client.channels.resolve(channelId);
if (!channel) {
return status.error(`Unable to find (${channelId}) channel`);
}
return channel.permissionOverwrites
.delete(userId, `Removed from the ticket.`)
.then(() => status.success(`Removed from the ticket.`))
.catch((e) => status.error(e.message || e));
},
};
}
/**
* @param {keyof typeof import("../languages/en-US")} name
*/
str(name, lang = "en-US") {
return getString(name, lang || this?.options?.lang || "en-US");
}
/** @private */
_debug(...args) {
if (!is.object(this.options)) {
return null;
}
if (this.options?.debug) {
log(inspect(...args, true, 50));
}
return null;
}
button(
options = {
style: 3,
label: this.str("CREATE_TICKET"),
emoji: { name: "📩" },
},
) {
return button({
id: options?.id || this.prefix,
style: options.style || 3,
title: options.label,
emoji: options.emoji,
});
}
/**
*
* @param {import("discord.js").ButtonInteraction} int
*/
async handleTicketButton(int) {
await int.deferUpdate().catch(() => null);
const msg = await int.message.fetch(true).catch(() => null);
const file = msg?.attachments.find((c) => c.name.endsWith(".txt"));
if (!file) {
return int
.followUp({
...embedComment(`Unable to find the .txt file.`),
ephemeral: true,
})
.catch(() => null);
}
const components = int.message.components.slice(0, 1);
components.push({
type: 1,
components: [
button({
title: `${this.str("TRANSCRIPT_VIEW")} (${moment().format("MM DD YYYY h:m:s")})`,
emoji: { id: "1059556038761787433" },
url: `https://view.elara.workers.dev/tickets?url=${file.url}`,
}),
],
});
return int.editReply({ components }).catch(console.log);
}
/**
* @param {object} opts
* @param {import("discord.js").GuildMember} opts.member
* @param {import("discord.js").TextChannel} opts.channel
* @param {import("discord.js").Guild} opts.guild
* @param {import("discord.js").User} opts.user
* @param {string} opts.reason
*/
async closeTicket({ member, messages, channel, guild, user, reason } = {}) {
const { id, token, username, avatar: avatarURL, threadId } = await this.webhookOptions;
if (!id || !token) {
return;
}
let embeds = [
{
author: {
name: guild.name,
icon_url: guild.iconURL({ dynamic: true }),
},
title: this.str("CLOSE_TICKET_TITLE"),
description: `${de.user} ${this.str("USER")}: ${user.toString()} \`@${user.tag}\` (${user.id})\n${de.user} ${this.str("CLOSED_BY")} ${member.toString()} (${member.id})\n${de.channel} ${this.str("CHANNEL")}: \`#${channel.name}\` (${channel.id})`,
color: 0xff0000,
fields: reason
? [
{
name: this.str("CLOSE_TICKET_FIELD_REASON"),
value: reason?.slice?.(0, 1024) ?? this.str("NO_REASON"),
},
]
: undefined,
timestamp: new Date(),
footer: {
text: `${this.str("TICKET_ID")} ${channel.name.split("-")[1]}`,
},
},
];
new WebhookClient({ id, token })
.send({
username,
avatarURL,
embeds,
threadId,
files: [
{
name: `${this.str("TRANSCRIPT")}.txt`,
attachment: Buffer.from(displayMessages(channel, messages.reverse(), channel.name.split("-")[1], this.options.prefix, (name) => this.str(name, this?.options?.lang))),
},
{
name: `${this.str("TRANSCRIPT")}.html`,
attachment: Buffer.from(generateHTMLPage(channel, messages.reverse(), channel.name.split("-")[1], this.options.prefix, (name) => this.str(name, this?.options?.lang))),
},
],
components: [
{
type: 1,
components: [
button({
id: `transcript`,
label: this.str("TRANSCRIPT"),
emoji: { id: `792290922749624320` },
style: ButtonStyle.SECONDARY,
}),
],
},
],
})
.then((m) => {
if (user) {
user.send({
embeds: [
{
author: {
name: guild.name,
icon_url: guild.iconURL({ dynamic: true }),
},
title: this.str("CLOSE_TICKET_TITLE"),
color: 0xff0000,
fields: reason
? [
{
name: this.str("CLOSE_TICKET_FIELD_REASON"),
value: reason?.slice?.(0, 1024) ?? this.str("NO_REASON"),
},
]
: undefined,
timestamp: new Date(),
footer: {
text: `${this.str("TICKET_ID")} ${channel.name.split("-")[1]}`,
},
},
],
components: [
{
type: 1,
components: [
{
type: 2,
style: 5,
label: this.str("TRANSCRIPT"),
emoji: { id: "792290922749624320" },
url: `https://view.elara.workers.dev/tickets?url=${m.attachments?.find?.((c) => c.filename.includes(".txt"))?.url}`,
},
],
},
],
files: [
{
name: `${this.str("TRANSCRIPT")}.html`,
attachment: Buffer.from(generateHTMLPage(channel, messages.reverse(), channel.name.split("-")[1], this.options.prefix, (name) => this.str(name, this?.options?.lang))),
},
],
}).catch((e) => this._debug(e));
}
})
.catch((e) => this._debug(e));
}
/**
* @param {string} channelId
* @param {import("discord.js").MessageOptions} options
*/
async starterMessage(channelId, options) {
let channel = this.options.client.channels.resolve(channelId);
if (!channel) {
return Promise.reject(this.str("NO_CHANNEL_FOUND")(channelId));
}
return channel
.send({
content: options?.content,
files: options?.attachments,
embeds: options?.embeds,
components: options?.components || [{ type: 1, components: [this.button()] }],
})
.then(() => console.log(`✅`));
}
};