const { EmbedBuilder, PermissionsBitField } = require('discord.js');
const axios = require('axios');
const path = require('path');
const fs = require('fs');
const SocksProxyAgent = require('socks-proxy-agent');
const archiver = require('archiver');
const { Storage } = require('megajs');
module.exports = {
name: 'ticketclose',
description: 'Close an existing support ticket and generate an HTML transcript.',
async execute(interaction) {
await interaction.deferReply({ephemeral: true});
const channelName = interaction.channel.name;
const ticketCreatorId = interaction.channel.topic;
const ticketCreator = interaction.guild.members.cache.get(ticketCreatorId);
if (!channelName.startsWith('🧾⏐ticket-') && !channelName.startsWith('🛒⏐ticket-') && !channelName.startsWith('📝⏐ticket-') && !channelName.startsWith('💾⏐ticket-')) {
return interaction.editReply('This command can only be used in ticket channels.');
}
const sortedMessages = await fetchAllMessages(interaction.channel);
sortedMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
await interaction.followUp({content: 'Generating the HTML transcript...', ephemeral: true});
const transcriptsFolderPath = path.join(__dirname, '..', 'transcripts');
const imagesFolderPath = path.join(transcriptsFolderPath, 'images');
if (!fs.existsSync(transcriptsFolderPath)) {
fs.mkdirSync(transcriptsFolderPath, {recursive: true});
}
if (!fs.existsSync(imagesFolderPath)) {
fs.mkdirSync(imagesFolderPath);
}
let htmlContent = `
`;
htmlContent += ``;
for (const message of sortedMessages) {
const formattedDate = `${message.createdAt.toLocaleDateString()} ${message.createdAt.toLocaleTimeString()}`;
htmlContent += `${formattedDate} ${message.author.username}: ${message.content}
`;
// Handling embeds in the message
message.embeds.forEach(embed => {
htmlContent += "";
if (embed.title) {
htmlContent += `
${embed.title}
`;
}
if (embed.description) {
htmlContent += `
${embed.description}
`;
}
if (embed.fields) {
embed.fields.forEach(field => {
htmlContent += `
${field.name}: ${field.value}
`;
});
}
if (embed.image) {
htmlContent += `
`;
}
if (embed.thumbnail) {
htmlContent += `
`;
}
if (embed.footer) {
htmlContent += ``;
}
htmlContent += "
"; // Closing embed div
});
// Handling attachments (images and videos)
if (message.attachments.size > 0) {
await Promise.all(message.attachments.map(async (attachment) => {
// Add a delay before processing each attachment
await delay(3000); // Adjust the 1000 milliseconds delay as needed
// Remove query parameters from the URL and extract the file extension
const attachmentUrl = attachment.url.split('?')[0];
const fileExtension = attachmentUrl.split('.').pop();
const attachmentFilename = path.join(imagesFolderPath, `${attachment.id}.${fileExtension}`);
if (/\.(jpeg|jpg|gif|png)$/.test(attachmentUrl)) {
// Handle image files
const imageBuffer = await fetchImage(attachmentUrl);
fs.writeFileSync(attachmentFilename, imageBuffer);
htmlContent += `
`;
} else if (/\.(mp4|webm|ogg)$/.test(attachmentUrl)) {
// Handle video files
const videoBuffer = await fetchImage(attachmentUrl);
fs.writeFileSync(attachmentFilename, videoBuffer);
htmlContent += ``;
}
}));
}
}
htmlContent += ``;
const sanitizedChannelName = channelName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
const filename = `transcript_${sanitizedChannelName}.html`;
const filepath = path.join(transcriptsFolderPath, filename);
fs.writeFileSync(filepath, htmlContent);
const zipFilePath = path.join(transcriptsFolderPath, `transcript_${sanitizedChannelName}.zip`);
const output = fs.createWriteStream(zipFilePath);
const archive = archiver('zip', { zlib: { level: 9 }});
archive.pipe(output);
archive.file(filepath, { name: filename });
archive.directory(imagesFolderPath, 'images');
archive.finalize();
output.on('close', async () => {
const fileSize = fs.statSync(zipFilePath).size;
const fileSizeLimit = 8 * 1024 * 1024; // 8 MB for standard Discord users
if (fileSize > fileSizeLimit) {
console.log(`ZIP file is too large to upload: ${zipFilePath}`);
try {
const megaLink = await uploadToMega(zipFilePath);
const transcriptEmbed = createTranscriptEmbed(interaction, ticketCreatorId, megaLink); // Use createTranscriptEmbed function
const transcriptsChannelId = 'removed';
const transcriptsChannel = interaction.guild.channels.cache.get(transcriptsChannelId);
await transcriptsChannel.send({ embeds: [transcriptEmbed] });
console.log('Transcript uploaded to Mega and link sent in Discord.');
} catch (error) {
console.error('Error uploading to Mega:', error);
await interaction.followUp({ content: 'There was an error uploading the transcript to Mega.', ephemeral: true });
}
} else {
// If the file is within size limit, send the file along with the embed
const transcriptEmbed = createTranscriptEmbed(interaction, ticketCreatorId,null); // No Mega link needed
const transcriptsChannelId = 'removed';
const transcriptsChannel = interaction.guild.channels.cache.get(transcriptsChannelId);
await transcriptsChannel.send({ embeds: [transcriptEmbed], files: [zipFilePath] });
console.log('Transcript uploaded to Discord.');
}
if (ticketCreator) {
const dmEmbed = createDmEmbed(interaction);
ticketCreator.send({ embeds: [dmEmbed] }).catch(error => console.error(`Could not send DM to ${ticketCreator.user.tag}`, error));
}
// Close the ticket
interaction.channel.delete();
// Clean up images folder
fs.rmSync(imagesFolderPath, { recursive: true, force: true });
// The ZIP file remains saved on the server
console.log(`Transcript saved locally: ${zipFilePath}`);
}).on('error', (err) => {
console.error(`Error creating ZIP file: ${err}`);
});
}
};
async function fetchAllMessages(channel) {
let allMessages = [];
let lastID;
while (true) {
const options = { limit: 100 };
if (lastID) options.before = lastID;
const fetchedMessages = await channel.messages.fetch(options);
if (fetchedMessages.size === 0) break;
allMessages = allMessages.concat([...fetchedMessages.values()]);
lastID = fetchedMessages.last().id;
}
return allMessages;
}
async function fetchImage(url) {
try {
console.log(`Attempting to fetch image from URL: ${url}`);
const response = await axios.get(url, {
responseType: 'arraybuffer',
headers: {
'User-Agent': 'RemovedforPrivacy/1.0',
'Authorization': `Bot ${botToken}` //try bot token for auth, no work.
}
});
console.log(`Image fetched successfully: ${url}`);
return Buffer.from(response.data, 'binary');
} catch (error) {
console.error(`Error fetching image from URL: ${url}`, error);
//Retry once after a delay if it's a 5xx server error
if (error.response && error.response.status >= 500) {
console.log(`Retrying image fetch for URL: ${url} after delay...`);
await delay(1000); // Wait for 1 second before retrying
try {
const retryResponse = await axios.get(url, {
responseType: 'arraybuffer',
headers: {
'Authorization': `Bot ${botToken}` //did not work.
}
});
console.log(`Image fetched successfully after retry: ${url}`);
return Buffer.from(retryResponse.data, 'binary');
} catch (retryError) {
console.error(`Retry failed for image URL: ${url}`, retryError);
return null;
}
} else {
return null;
}
}
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function uploadToMega(filePath) {
try {
console.log(`Starting Mega upload for file: ${filePath}`);
const storage = await new Storage({
email: '',
password: ''
}).ready;
const fileData = fs.readFileSync(filePath);
const file = await storage.upload(path.basename(filePath), fileData).complete;
// Attempt to obtain the link correctly
const megaLink = typeof file.link === 'function' ? file.link() : file.link;
console.log('File uploaded to Mega successfully! Link:', megaLink);
return megaLink;
} catch (error) {
console.error('Error during Mega upload:', error);
throw error;
}
}
const currentDate = new Date();
const unixTimestamp = Math.floor(currentDate.getTime() / 1000); // Convert to seconds
const timestampString = ``;
function createTranscriptEmbed(interaction, ticketCreatorId, megaLink) {
// Format the timestamp
const ticketCreator = interaction.guild.members.cache.get(ticketCreatorId);
const ticketCreatorName = ticketCreator ? ticketCreator.user.username : 'Unknown User';
return new EmbedBuilder()
.setColor(0x0099FF)
.setTitle('Ticket Transcript')
.setDescription(`Details of the closed ticket. [Download Transcript](${megaLink})`)
.setAuthor({
name: interaction.guild.name,
iconURL: interaction.guild.iconURL(),
url: 'https://discord.com/' // Replace with a relevant URL if needed
})
.setThumbnail(interaction.user.displayAvatarURL())
.addFields(
{
name: 'Opened By',
value: ticketCreatorName
},
{
name: 'Closed By',
value: interaction.user.username
},
{
name: 'Closed At',
value: timestampString
}
)
.setTimestamp();
}
function createDmEmbed(interaction) {
return new EmbedBuilder()
.setColor('#ffa400')
.setTitle('Your Ticket Has Been Closed')
.setDescription(`Your ticket in ${interaction.guild.name} has been successfully closed.`)
.addFields({ name: 'Ticket', value: interaction.channel.name })
.setTimestamp();
}