@skybloxsystems/ticket-bot
Version:
521 lines (405 loc) • 25.1 kB
JavaScript
const discord = require('discord.js');
const jsdom = require('jsdom');
const fs = require('fs');
const path = require('path');
const purify = require('dompurify');
const static = require('./static');
// const escape = require('escape-html'); // replaced by he
const hljs = require('highlight.js');
const he = require('he');
const template = fs.readFileSync(path.join(__dirname, 'template.html'), 'utf8');
// copilot helped so much here
// copilot smart 🧠
/**
*
* @param {discord.Collection<string, discord.Message> | discord.Message[]} messages
* @param {discord.TextChannel} channel
*/
function generateTranscript(messages, channel, opts={ returnBuffer: false, fileName: 'transcript.html' }) {
const dom = new jsdom.JSDOM(template.replace('{{TITLE}}', channel.name));
const document = dom.window.document;
// const xss = new XSS.FilterXSS({
// whiteList: static.xssSettings
// }).process;
// Downside of DOMPurify is that it straight up removes the elements
// it doesn't escape it
// not good for use for stuff like message content.
const DOMPurify = purify(dom.window);
DOMPurify.setConfig({
ALLOWED_TAGS: []
});
const xss = DOMPurify.sanitize;
// Basic Info (header)
document.getElementsByClassName('preamble__guild-icon')[0].src = channel.guild.iconURL();
document.getElementById('guildname').textContent = channel.guild.name;
document.getElementById('ticketname').textContent = channel.name;
const transcript = document.getElementById('chatlog');
// Messages
for(const message of (Array.from(messages.values())).sort((a, b) => a.createdTimestamp - b.createdTimestamp)) {
// create message group
const messageGroup = document.createElement('div');
messageGroup.classList.add('chatlog__message-group');
// message reference
if(message.reference?.messageId) {
// create symbol
const referenceSymbol = document.createElement('div');
referenceSymbol.classList.add('chatlog__reference-symbol');
// create reference
const reference = document.createElement('div');
reference.classList.add('chatlog__reference');
const referencedMessage = messages instanceof discord.Collection ? messages.get(message.reference.messageId) : messages.find(m => m.id === message.reference.messageId);
const author = referencedMessage?.author ?? static.DummyUser;
reference.innerHTML =
`<img class="chatlog__reference-avatar" src="${author.avatarURL() ?? static.defaultPFP}" alt="Avatar" loading="lazy">
<span class="chatlog__reference-name" title="${author.username.replace(/"/g, '')}" style="color: ${author.hexAccentColor ?? '#FFFFFF'}">${author.bot ? `<span class="chatlog__bot-tag">BOT</span> ${xss(author.username)}` : xss(author.username)}</span>
<div class="chatlog__reference-content">
<span class="chatlog__reference-link" onclick="scrollToMessage(event, '${message.reference.messageId}')">
${referencedMessage ? (referencedMessage?.content ? `${formatContent(referencedMessage?.content, channel, false, true)}...` : '<em>Click to see attachment</em>') : '<em>Original message was deleted.</em>'}
</span>
</div>`;
messageGroup.appendChild(referenceSymbol);
messageGroup.appendChild(reference);
}
// message author pfp
const author = message.author ?? static.DummyUser;
const authorElement = document.createElement('div');
authorElement.classList.add('chatlog__author-avatar-container');
const authorAvatar = document.createElement('img');
authorAvatar.classList.add('chatlog__author-avatar');
authorAvatar.src = author.avatarURL() ?? static.defaultPFP;
authorAvatar.alt = 'Avatar';
authorAvatar.loading = 'lazy';
authorElement.appendChild(authorAvatar);
messageGroup.appendChild(authorElement);
// message content
const content = document.createElement('div');
content.classList.add('chatlog__messages');
// message author name
const authorName = document.createElement('span');
authorName.classList.add('chatlog__author-name');
authorName.title = xss(author.tag);
authorName.textContent = author.username;
authorName.setAttribute('data-user-id', author.id);
content.appendChild(authorName);
if(author.bot) {
const botTag = document.createElement('span');
botTag.classList.add('chatlog__bot-tag');
botTag.textContent = 'BOT';
content.appendChild(botTag);
}
// timestamp
const timestamp = document.createElement('span');
timestamp.classList.add('chatlog__timestamp');
timestamp.textContent = message.createdAt.toLocaleString();
content.appendChild(timestamp);
const messageContent = document.createElement('div');
messageContent.classList.add('chatlog__message');
messageContent.setAttribute('data-message-id', message.id);
messageContent.setAttribute('id', `message-${message.id}`);
messageContent.title = `Message sent: ${message.createdAt.toLocaleString()}`;
// message content
if(message.content) {
const messageContentContent = document.createElement('div');
messageContentContent.classList.add('chatlog__content');
const messageContentContentMarkdown = document.createElement('div');
messageContentContentMarkdown.classList.add('markdown');
const messageContentContentMarkdownSpan = document.createElement('span');
messageContentContentMarkdownSpan.classList.add('preserve-whitespace');
messageContentContentMarkdownSpan.innerHTML = formatContent(message.content, channel, message.webhookId !== null);
messageContentContentMarkdown.appendChild(messageContentContentMarkdownSpan);
messageContentContent.appendChild(messageContentContentMarkdown);
messageContent.appendChild(messageContentContent);
}
// message attachments
if(message.attachments && message.attachments.size > 0) {
for(const attachment of message.attachments.values()) {
const attachmentsDiv = document.createElement('div');
attachmentsDiv.classList.add('chatlog__attachment');
const attachmentType = attachment.name.split('.').pop();
if(['png', 'jpg', 'jpeg', 'gif'].includes(attachmentType)) {
const attachmentLink = document.createElement('a');
const attachmentImage = document.createElement('img');
attachmentImage.classList.add('chatlog__attachment-media');
attachmentImage.src = attachment.proxyURL ?? attachment.url;
attachmentImage.alt = 'Image attachment';
attachmentImage.loading = 'lazy';
attachmentImage.title = `Image: ${attachment.name} (${formatBytes(attachment.size)})`;
attachmentLink.appendChild(attachmentImage);
attachmentsDiv.appendChild(attachmentLink);
} else if(['mp4', 'webm'].includes(attachmentType)) {
const attachmentVideo = document.createElement('video');
attachmentVideo.classList.add('chatlog__attachment-media');
attachmentVideo.src = attachment.proxyURL ?? attachment.url;
attachmentVideo.alt = 'Video attachment';
attachmentVideo.controls = true;
attachmentVideo.title = `Video: ${attachment.name} (${formatBytes(attachment.size)})`;
attachmentsDiv.appendChild(attachmentVideo);
} else if(['mp3', 'ogg'].includes(attachmentType)) {
const attachmentAudio = document.createElement('audio');
attachmentAudio.classList.add('chatlog__attachment-media');
attachmentAudio.src = attachment.proxyURL ?? attachment.url;
attachmentAudio.alt = 'Audio attachment';
attachmentAudio.controls = true;
attachmentAudio.title = `Audio: ${attachment.name} (${formatBytes(attachment.size)})`;
attachmentsDiv.appendChild(attachmentAudio);
} else {
const attachmentGeneric = document.createElement('div');
attachmentGeneric.classList.add('chatlog__attachment-generic');
const attachmentGenericIcon = document.createElement('svg');
attachmentGenericIcon.classList.add('chatlog__attachment-generic-icon');
const attachmentGenericIconUse = document.createElement('use');
attachmentGenericIconUse.setAttribute('href', '#icon-attachment');
attachmentGenericIcon.appendChild(attachmentGenericIconUse);
attachmentGeneric.appendChild(attachmentGenericIcon);
const attachmentGenericName = document.createElement('div');
attachmentGenericName.classList.add('chatlog__attachment-generic-name');
const attachmentGenericNameLink = document.createElement('a');
attachmentGenericNameLink.href = attachment.proxyURL ?? attachment.url;
attachmentGenericNameLink.textContent = attachment.name;
attachmentGenericName.appendChild(attachmentGenericNameLink);
attachmentGeneric.appendChild(attachmentGenericName);
const attachmentGenericSize = document.createElement('div');
attachmentGenericSize.classList.add('chatlog__attachment-generic-size');
attachmentGenericSize.textContent = `${formatBytes(attachment.size)}`;
attachmentGeneric.appendChild(attachmentGenericSize);
attachmentsDiv.appendChild(attachmentGeneric);
}
messageContent.appendChild(attachmentsDiv);
}
}
content.appendChild(messageContent);
// embeds
if(message.embeds && message.embeds.length > 0) {
for(const embed of message.embeds) {
const embedDiv = document.createElement('div');
embedDiv.classList.add('chatlog__embed');
// embed color
if(embed.hexColor) {
const embedColorPill = document.createElement('div');
embedColorPill.classList.add('chatlog__embed-color-pill');
embedColorPill.style.backgroundColor = embed.hexColor;
embedDiv.appendChild(embedColorPill);
}
const embedContentContainer = document.createElement('div');
embedContentContainer.classList.add('chatlog__embed-content-container');
const embedContent = document.createElement('div');
embedContent.classList.add('chatlog__embed-content');
const embedText = document.createElement('div');
embedText.classList.add('chatlog__embed-text');
// embed author
if(embed.author?.name) {
const embedAuthor = document.createElement('div');
embedAuthor.classList.add('chatlog__embed-author');
if(embed.author.iconURL) {
const embedAuthorIcon = document.createElement('img');
embedAuthorIcon.classList.add('chatlog__embed-author-icon');
embedAuthorIcon.src = embed.author.iconURL;
embedAuthorIcon.alt = 'Author icon';
embedAuthorIcon.loading = 'lazy';
embedAuthorIcon.onerror = () => embedAuthorIcon.style.visibility = 'hidden';
embedAuthor.appendChild(embedAuthorIcon);
}
const embedAuthorName = document.createElement('span');
embedAuthorName.classList.add('chatlog__embed-author-name');
if(embed.author.url) {
const embedAuthorNameLink = document.createElement('a');
embedAuthorNameLink.classList.add('chatlog__embed-author-name-link');
embedAuthorNameLink.href = embed.author.url;
embedAuthorNameLink.textContent = embed.author.name;
embedAuthorName.appendChild(embedAuthorNameLink);
} else {
embedAuthorName.textContent = embed.author.name;
}
embedAuthor.appendChild(embedAuthorName);
embedText.appendChild(embedAuthor);
}
// embed title
if(embed.title) {
const embedTitle = document.createElement('div');
embedTitle.classList.add('chatlog__embed-title');
if(embed.url) {
const embedTitleLink = document.createElement('a');
embedTitleLink.classList.add('chatlog__embed-title-link');
embedTitleLink.href = embed.url;
const embedTitleMarkdown = document.createElement('div');
embedTitleMarkdown.classList.add('markdown', 'preserve-whitespace');
embedTitleMarkdown.textContent = embed.title;
embedTitleLink.appendChild(embedTitleMarkdown);
embedTitle.appendChild(embedTitleLink);
} else {
const embedTitleMarkdown = document.createElement('div');
embedTitleMarkdown.classList.add('markdown', 'preserve-whitespace');
embedTitleMarkdown.textContent = embed.title;
embedTitle.appendChild(embedTitleMarkdown);
}
embedText.appendChild(embedTitle);
}
// embed description
if(embed.description) {
const embedDescription = document.createElement('div');
embedDescription.classList.add('chatlog__embed-description');
const embedDescriptionMarkdown = document.createElement('div');
embedDescriptionMarkdown.classList.add('markdown', 'preserve-whitespace');
embedDescriptionMarkdown.innerHTML = formatContent(embed.description, channel, true);
embedDescription.appendChild(embedDescriptionMarkdown);
embedText.appendChild(embedDescription);
}
// embed fields
if(embed.fields && embed.fields.length > 0) {
const embedFields = document.createElement('div');
embedFields.classList.add('chatlog__embed-fields');
for(const field of embed.fields) {
const embedField = document.createElement('div');
embedField.classList.add(
...(!field.inline ? ['chatlog__embed-field'] : ['chatlog__embed-field', 'chatlog__embed-field--inline'])
);
// Field name
const embedFieldName = document.createElement('div');
embedFieldName.classList.add('chatlog__embed-field-name');
const embedFieldNameMarkdown = document.createElement('div');
embedFieldNameMarkdown.classList.add('markdown', 'preserve-whitespace');
embedFieldNameMarkdown.textContent = field.name;
embedFieldName.appendChild(embedFieldNameMarkdown);
embedField.appendChild(embedFieldName);
// Field value
const embedFieldValue = document.createElement('div');
embedFieldValue.classList.add('chatlog__embed-field-value');
const embedFieldValueMarkdown = document.createElement('div');
embedFieldValueMarkdown.classList.add('markdown', 'preserve-whitespace');
embedFieldValueMarkdown.innerHTML = formatContent(field.value, channel, true);
embedFieldValue.appendChild(embedFieldValueMarkdown);
embedField.appendChild(embedFieldValue);
embedFields.appendChild(embedField);
}
embedText.appendChild(embedFields);
}
embedContent.appendChild(embedText);
// embed thumbnail
if(embed.thumbnail?.proxyURL ?? embed.thumbnail?.url) {
const embedThumbnail = document.createElement('div');
embedThumbnail.classList.add('chatlog__embed-thumbnail-container');
const embedThumbnailLink = document.createElement('a');
embedThumbnailLink.classList.add('chatlog__embed-thumbnail-link');
embedThumbnailLink.href = embed.thumbnail.proxyURL ?? embed.thumbnail.url;
const embedThumbnailImage = document.createElement('img');
embedThumbnailImage.classList.add('chatlog__embed-thumbnail');
embedThumbnailImage.src = embed.thumbnail.proxyURL ?? embed.thumbnail.url;
embedThumbnailImage.alt = 'Thumbnail';
embedThumbnailImage.loading = 'lazy';
embedThumbnailLink.appendChild(embedThumbnailImage);
embedThumbnail.appendChild(embedThumbnailLink);
embedContent.appendChild(embedThumbnail);
}
embedContentContainer.appendChild(embedContent);
// embed image
if(embed.image) {
const embedImage = document.createElement('div');
embedImage.classList.add('chatlog__embed-image-container');
const embedImageLink = document.createElement('a');
embedImageLink.classList.add('chatlog__embed-image-link');
embedImageLink.href = embed.image.proxyURL ?? embed.image.url;
const embedImageImage = document.createElement('img');
embedImageImage.classList.add('chatlog__embed-image');
embedImageImage.src = embed.image.proxyURL ?? embed.image.url;
embedImageImage.alt = 'Image';
embedImageImage.loading = 'lazy';
embedImageLink.appendChild(embedImageImage);
embedImage.appendChild(embedImageLink);
embedContentContainer.appendChild(embedImage);
}
// footer
if(embed.footer?.text) {
const embedFooter = document.createElement('div');
embedFooter.classList.add('chatlog__embed-footer');
if(embed.footer.iconURL) {
const embedFooterIcon = document.createElement('img');
embedFooterIcon.classList.add('chatlog__embed-footer-icon');
embedFooterIcon.src = embed.footer.proxyIconURL ?? embed.footer.iconURL;
embedFooterIcon.alt = 'Footer icon';
embedFooterIcon.loading = 'lazy';
embedFooter.appendChild(embedFooterIcon);
}
const embedFooterText = document.createElement('span');
embedFooterText.classList.add('chatlog__embed-footer-text');
embedFooterText.textContent = embed.timestamp ? `${embed.footer.text} • ${new Date(embed.timestamp).toLocaleString()}` : embed.footer.text;
embedFooter.appendChild(embedFooterText);
embedContentContainer.appendChild(embedFooter);
}
embedDiv.appendChild(embedContentContainer);
content.appendChild(embedDiv);
}
}
messageGroup.appendChild(content);
transcript.appendChild(messageGroup);
}
return opts.returnBuffer ? Buffer.from(dom.serialize()) : new discord.MessageAttachment(Buffer.from(dom.serialize()), opts.fileName ?? 'transcript.html');
}
const languages = hljs.default.listLanguages();
/**
*
* @param {String} content
* @param {discord.TextChannel} context
* @param {Boolean} allowExtra Stuff that only webhooks can send or things that can only appear in a embed description (such as [embeded links](https://like.this))
* @returns {String}
*/
function formatContent(content, context, allowExtra=false, replyStyle=false, purify=he.escape) {
content = purify(content)
.replace(/\&\#x60;/g, '`') // we dont want ` to be escaped
.replace(/```(.+?)```/gs, code => {
if (!replyStyle) {
const split = code.slice(3, -3).split('\n');
let language = split.shift().trim().toLowerCase();
if (static.LanguageAliases[language])
language = static.LanguageAliases[language];
if (languages.includes(language)) {
const joined = he.unescape(split.join("\n"));
return `<div class="pre pre--multiline language-${language}">${
hljs.default.highlight(joined, {
language,
}).value
}</div>`;
} else {
return `<div class="pre pre--multiline nohighlight">${code
.slice(3, -3)
.trim()}</div>`;
}
} else {
const split = code.slice(3, -3).split('\n');
split.shift();
const joined = he.unescape(split.join('\n'));
return `<span class="pre pre--inline">${joined.substring(0, 42)}</span>`;
}
})
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/~~(.+?)~~/g, '<s>$1</s>')
.replace(/__(.+?)__/g, '<u>$1</u>')
.replace(/\_(.+?)\_/g, '<em>$1</em>')
.replace(/`(.+?)`/g, `<span class="pre pre--inline">$1</span>`)
.replace(/\|\|(.+?)\|\|/g, `<span class="spoiler-text spoiler-text--hidden" ${replyStyle ? '' : 'onclick="showSpoiler(event, this)"'}>$1</span>`)
.replace(/\<\;@!*&*([0-9]{16,20})\>\;/g, user => {
const userId = user.match(/[0-9]{16,20}/)[0];
const userInGuild = context.client?.users?.resolve(userId);
return `<span class="mention" title="${userInGuild?.tag ?? userId}">@${userInGuild?.username ?? "Unknown User"}</span>`
})
.replace(/\<\;#!*&*([0-9]{16,20})\>\;/g, channel => {
const channelId = channel.match(/[0-9]{16,20}/)[0];
const channelInGuild = context.guild?.channels.resolve(channelId);
const pre = channelInGuild ? channelInGuild.isText() ? '#' : channelInGuild.isVoice() ? '🔊' : '📁' : "#";
return `<span class="mention" title="${channelInGuild?.name ?? channelId}">${pre}${channelInGuild?.name ?? "Unknown Channel"}</span>`;
});
if(allowExtra) {
content = content
.replace(/\[(.+?)\]\((.+?)\)/g, `<a href="$2">$1</a>`)
}
return replyStyle ? content.replace(/(?:\r\n|\r|\n)/g, ' ') : content.replace(/(?:\r\n|\r|\n)/g, '<br />'); // do this last
}
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
module.exports = generateTranscript;