2021-10-28 18:03:27 -05:00
const fs = require ( "fs" );
const Discord = require ( "discord.js" );
const yaml = require ( "js-yaml" );
2022-08-12 01:35:36 +01:00
2024-10-10 19:14:45 +01:00
const supportbot = yaml . load ( fs . readFileSync ( "./Configs/supportbot.yml" , "utf8" ));
const cmdconfig = yaml . load ( fs . readFileSync ( "./Configs/commands.yml" , "utf8" ));
const msgconfig = yaml . load ( fs . readFileSync ( "./Configs/messages.yml" , "utf8" ));
2021-10-28 18:03:27 -05:00
const Command = require ( "../Structures/Command.js" );
module . exports = new Command ({
2022-07-23 07:50:12 +01:00
name : cmdconfig . CloseTicket . Command ,
description : cmdconfig . CloseTicket . Description ,
2022-07-22 13:19:05 +01:00
type : Discord . ApplicationCommandType . ChatInput ,
2021-12-18 18:57:26 -06:00
options : [
2021-10-28 18:03:27 -05:00
{
name : "reason" ,
description : "Ticket Close Reason" ,
2022-07-22 13:19:05 +01:00
type : Discord . ApplicationCommandOptionType . String ,
2024-11-09 02:32:06 +00:00
}
2021-10-28 18:03:27 -05:00
],
2022-07-23 07:50:12 +01:00
permissions : cmdconfig . CloseTicket . Permission ,
2021-10-28 18:03:27 -05:00
async run ( interaction ) {
2022-01-30 00:07:04 -06:00
const { getRole , getChannel } = interaction . client ;
2024-06-12 12:54:31 +01:00
2022-08-12 01:35:36 +01:00
if ( supportbot . Ticket . Close . StaffOnly ) {
let SupportStaff = await getRole ( supportbot . Roles . StaffMember . Staff , interaction . guild );
let Admin = await getRole ( supportbot . Roles . StaffMember . Admin , interaction . guild );
2024-06-12 12:54:31 +01:00
2024-11-09 00:37:51 +00:00
if ( ! SupportStaff || ! Admin ) {
return interaction . reply ( "Some roles seem to be missing! Please check for errors when starting the bot." );
}
2022-08-12 01:35:36 +01:00
2024-06-12 12:54:31 +01:00
const NoPerms = new Discord . EmbedBuilder ()
. setTitle ( "Invalid Permissions!" )
2024-10-10 19:14:45 +01:00
. setDescription ( ` ${ msgconfig . Error . IncorrectPerms } \n\nRole Required: \` ${ supportbot . Roles . StaffMember . Staff } \` or \` ${ supportbot . Roles . StaffMember . Admin } \`` )
2024-11-09 02:32:06 +00:00
. setColor ( supportbot . Embed . Colours . Warn );
2021-12-18 19:39:59 -06:00
2024-11-09 00:37:51 +00:00
if ( ! interaction . member . roles . cache . has ( SupportStaff . id ) && ! interaction . member . roles . cache . has ( Admin . id )) {
2021-12-18 19:39:59 -06:00
return interaction . reply ({ embeds : [ NoPerms ] });
2024-11-09 00:37:51 +00:00
}
2021-12-18 19:39:59 -06:00
}
2024-06-12 12:54:31 +01:00
2024-11-09 02:32:06 +00:00
const isThread = interaction . channel . type === Discord . ChannelType . PrivateThread ;
2024-10-10 19:14:45 +01:00
if (
2024-11-09 02:32:06 +00:00
( supportbot . Ticket . TicketType === "threads" && ! isThread ) ||
2024-10-10 19:14:45 +01:00
( supportbot . Ticket . TicketType === "channels" && interaction . channel . type !== Discord . ChannelType . GuildText )
) {
2024-06-12 12:54:31 +01:00
const NotTicketChannel = new Discord . EmbedBuilder ()
. setTitle ( "Invalid Channel!" )
2024-10-10 19:14:45 +01:00
. setDescription ( `This command can only be used in a ${ supportbot . Ticket . TicketType === "threads" ? "ticket thread" : "ticket channel" } .` )
2024-11-09 02:32:06 +00:00
. setColor ( supportbot . Embed . Colours . Warn );
2024-06-12 12:54:31 +01:00
return interaction . reply ({ embeds : [ NotTicketChannel ], ephemeral : true });
}
2024-11-09 02:32:06 +00:00
await interaction . deferReply ();
2024-11-09 00:37:51 +00:00
let tickets ;
try {
tickets = JSON . parse ( fs . readFileSync ( "./Data/TicketData.json" , "utf8" ));
} catch ( err ) {
2024-11-09 02:32:06 +00:00
console . error ( "Error reading ticket data file:" , err );
2024-11-09 00:37:51 +00:00
return interaction . followUp ({ content : "There was an error loading ticket data." });
}
2024-06-12 12:54:31 +01:00
let TicketData = tickets . tickets . findIndex (( t ) => t . id === interaction . channel . id );
2021-12-18 22:14:11 -06:00
let ticket = tickets . tickets [ TicketData ];
2024-06-12 12:54:31 +01:00
2022-01-29 23:57:10 -06:00
if ( TicketData === - 1 ) {
2022-07-22 13:19:05 +01:00
const Exists = new Discord . EmbedBuilder ()
2021-10-28 18:03:27 -05:00
. setTitle ( "No Ticket Found!" )
2022-08-12 01:35:36 +01:00
. setDescription ( msgconfig . Error . NoValidTicket )
2024-11-09 02:32:06 +00:00
. setColor ( supportbot . Embed . Colours . Warn );
2021-12-04 20:57:23 -06:00
return interaction . followUp ({ embeds : [ Exists ] });
2021-10-28 18:03:27 -05:00
}
2024-06-12 12:54:31 +01:00
let reason = interaction . options ? . getString ( "reason" ) || "No Reason Provided." ;
2024-11-09 02:32:06 +00:00
const ticketUserId = ticket . user ;
2024-06-12 12:54:31 +01:00
2024-11-09 00:37:51 +00:00
if ( supportbot . Ticket . ReviewSystem . Enabled ) {
2024-11-09 02:32:06 +00:00
const rating = await collectReviewRating ( interaction , ticketUserId );
const comment = await collectReviewComment ( interaction , ticketUserId );
2024-11-09 00:37:51 +00:00
const reviewChannel = await getChannel ( supportbot . Ticket . ReviewSystem . Channel , interaction . guild );
const reviewer = supportbot . Ticket . ClaimTickets ? `<@ ${ ticket . claimedBy } >` : null ;
const reviewEmbed = new Discord . EmbedBuilder ()
. setTitle ( msgconfig . ReviewSystem . ReviewEmbed . Title )
. addFields (
{
name : msgconfig . ReviewSystem . ReviewEmbed . RatingTitle ,
value : ` ${ msgconfig . ReviewSystem . ReviewEmbed . ReviewEmoji . repeat ( rating ) } ` ,
inline : false
},
{
name : msgconfig . ReviewSystem . ReviewEmbed . CommentTitle ,
value : `\`\`\` ${ comment } \`\`\`` ,
inline : false
}
)
. setColor ( msgconfig . ReviewSystem . ReviewEmbed . Color );
if ( supportbot . Ticket . ClaimTickets . Enabled ) {
reviewEmbed . setDescription (
2024-11-09 02:32:06 +00:00
`** ${ msgconfig . ReviewSystem . ReviewEmbed . ReviewedStaffTitle } ** ${ reviewer } \n** ${ msgconfig . ReviewSystem . ReviewEmbed . ReviewedByTitle } ** <@ ${ ticketUserId } >` || `N/A`
2024-11-09 00:37:51 +00:00
);
} else {
reviewEmbed . setDescription (
2024-11-09 02:32:06 +00:00
`** ${ msgconfig . ReviewSystem . ReviewEmbed . ReviewedByTitle } ** <@ ${ ticketUserId } >`
2024-11-09 00:37:51 +00:00
);
}
2024-06-12 12:54:31 +01:00
2024-11-09 00:37:51 +00:00
if ( reviewChannel ) {
await reviewChannel . send ({ embeds : [ reviewEmbed ] });
}
2024-06-12 12:54:31 +01:00
2024-11-09 00:37:51 +00:00
if ( ! interaction . replied && ! interaction . deferred ) {
await interaction . followUp ({ embeds : [ reviewEmbed ] });
}
2024-06-12 12:54:31 +01:00
2024-11-09 00:37:51 +00:00
await handleCloseTicket ( interaction , reason , ticket , TicketData );
} else {
await handleCloseTicket ( interaction , reason , ticket , TicketData );
}
2024-06-12 12:54:31 +01:00
},
});
2024-11-09 02:32:06 +00:00
async function collectReviewRating ( interaction , ticketUserId ) {
2024-11-09 00:37:51 +00:00
const reviewPrompt = new Discord . EmbedBuilder ()
. setTitle ( msgconfig . ReviewSystem . Rate . Title )
2024-11-09 02:32:06 +00:00
. setDescription ( ` ${ msgconfig . ReviewSystem . Rate . Description } ` )
. setColor ( supportbot . Embed . Colours . General );
2024-11-09 00:37:51 +00:00
const stars = new Discord . ActionRowBuilder (). addComponents (
new Discord . StringSelectMenuBuilder ()
. setCustomId ( "starRating" )
. setPlaceholder ( "Select a rating" )
. addOptions ([
{ label : msgconfig . ReviewSystem . Stars . One , value : "1" },
{ label : msgconfig . ReviewSystem . Stars . Two , value : "2" },
{ label : msgconfig . ReviewSystem . Stars . Three , value : "3" },
{ label : msgconfig . ReviewSystem . Stars . Four , value : "4" },
{ label : msgconfig . ReviewSystem . Stars . Five , value : "5" },
])
);
await interaction . followUp ({
2024-11-09 02:32:06 +00:00
content : `<@ ${ ticketUserId } >` ,
2024-11-09 00:37:51 +00:00
embeds : [ reviewPrompt ],
components : [ stars ],
ephemeral : true ,
});
const starRating = await interaction . channel . awaitMessageComponent ({
componentType : Discord . ComponentType . StringSelect ,
2024-11-09 02:32:06 +00:00
filter : ( i ) => i . customId === "starRating" && i . user . id === ticketUserId ,
2024-11-09 00:37:51 +00:00
});
await starRating . deferUpdate ();
return starRating . values [ 0 ];
}
2024-11-09 02:32:06 +00:00
async function collectReviewComment ( interaction , ticketUserId ) {
2024-11-09 00:37:51 +00:00
const commentPrompt = new Discord . EmbedBuilder ()
. setTitle ( msgconfig . ReviewSystem . Comment . Title )
2024-11-09 02:32:06 +00:00
. setDescription ( ` ${ msgconfig . ReviewSystem . Comment . Description } ` )
. setColor ( supportbot . Embed . Colours . General );
2024-11-09 00:37:51 +00:00
await interaction . followUp ({
2024-11-09 02:32:06 +00:00
content : `<@ ${ ticketUserId } >` ,
2024-11-09 00:37:51 +00:00
embeds : [ commentPrompt ],
ephemeral : true ,
});
2024-11-09 02:32:06 +00:00
const filter = ( response ) => response . author . id === ticketUserId ;
const commentCollection = await interaction . channel . awaitMessages ({ filter , max : 1 });
2024-11-09 00:37:51 +00:00
const comment = commentCollection . first () ? . content ;
return comment && comment . toLowerCase () !== "no" ? comment : "No Comment Provided" ;
}
2024-06-12 12:54:31 +01:00
async function handleCloseTicket ( interaction , reason , ticket , TicketData ) {
const { getChannel } = interaction . client ;
2024-11-09 02:32:06 +00:00
let tickets ;
try {
tickets = JSON . parse ( fs . readFileSync ( "./Data/TicketData.json" , "utf8" ));
} catch ( err ) {
console . error ( "Error reading ticket data:" , err );
return interaction . followUp ( "Failed to load ticket data." );
}
2024-06-12 12:54:31 +01:00
let tUser = interaction . client . users . cache . get ( ticket . user );
2024-10-10 19:14:45 +01:00
let transcriptChannel = await getChannel ( supportbot . Ticket . Log . TicketDataLog , interaction . guild );
2024-06-12 12:54:31 +01:00
2024-11-09 02:32:06 +00:00
if ( ! transcriptChannel ) {
console . log ( "Transcript channel missing or inaccessible" );
return interaction . followUp ( "Error: Transcript log channel is missing or bot lacks permission." );
}
2024-06-12 12:54:31 +01:00
try {
2024-11-09 02:44:39 +00:00
// Fetch all messages from the channel
let allMessages = [];
let lastId = null ;
while ( true ) {
const options = { limit : 100 };
if ( lastId ) {
options . before = lastId ;
}
const messages = await interaction . channel . messages . fetch ( options );
if ( messages . size === 0 ) break ;
allMessages = [... allMessages , ... messages . values ()];
lastId = messages . last (). id ;
if ( messages . size < 100 ) break ;
}
2024-11-10 13:30:27 +00:00
// Sort messages by timestamp
2024-11-09 02:44:39 +00:00
allMessages . sort (( a , b ) => a . createdTimestamp - b . createdTimestamp );
2024-11-10 13:30:27 +00:00
// Create transcript data
2024-11-09 02:44:39 +00:00
const transcriptData = allMessages . map ( msg => ({
content : msg . content || "No content" ,
username : msg . author . username ,
userId : msg . author . id ,
avatar : msg . author . displayAvatarURL (),
timestamp : msg . createdAt . toISOString (),
attachments : Array . from ( msg . attachments . values ()). map ( att => ({
url : att . url ,
name : att . name
})),
embeds : msg . embeds . map ( embed => ({
title : embed . title ,
description : embed . description ,
fields : embed . fields
}))
}));
2024-11-10 13:30:27 +00:00
// Update ticket status
2024-06-12 12:54:31 +01:00
tickets . tickets [ TicketData ]. open = false ;
2024-11-09 02:44:39 +00:00
tickets . tickets [ TicketData ]. messages = transcriptData ;
2024-11-09 00:37:51 +00:00
fs . writeFileSync ( "./Data/TicketData.json" , JSON . stringify ( tickets , null , 4 ));
2024-06-12 12:54:31 +01:00
const transcriptEmbed = new Discord . EmbedBuilder ()
2024-10-10 19:14:45 +01:00
. setTitle ( msgconfig . TicketLog . Title )
2024-11-09 02:32:06 +00:00
. setColor ( msgconfig . TicketLog . Colour )
2024-11-09 00:37:51 +00:00
. setFooter ({ text : supportbot . Embed . Footer , iconURL : interaction . user . displayAvatarURL () })
2024-11-09 02:32:06 +00:00
. setDescription (
`> **Ticket:** ${ interaction . channel . name } (\` ${ interaction . channel . id } \`)\n` +
`> **User:** ${ tUser ? . tag || "Unknown User" } (\` ${ tUser ? . id || ticket . user } \`)\n` +
2024-11-09 02:44:39 +00:00
`> **Closed by:** <@ ${ interaction . user . id } >\n` +
`> **Message Count:** ${ transcriptData . length } `
2024-11-09 02:32:06 +00:00
)
2024-11-09 00:37:51 +00:00
. addFields ({ name : "Reason" , value : `\`\`\` ${ reason } \`\`\`` , inline : true });
2024-06-12 12:54:31 +01:00
2024-11-09 02:44:39 +00:00
const html = createTranscriptHTML (
{
id : interaction . channel . id ,
messages : transcriptData ,
name : interaction . channel . name
},
reason
);
2024-06-12 12:54:31 +01:00
2024-11-09 02:32:06 +00:00
const fileName = ` ${ interaction . channel . id } -transcript.html` ;
fs . writeFileSync ( `./Data/Transcripts/ ${ fileName } ` , html );
2024-06-12 12:54:31 +01:00
2024-11-09 02:32:06 +00:00
await transcriptChannel . send ({
2024-11-09 00:37:51 +00:00
embeds : [ transcriptEmbed ],
2024-11-09 02:44:39 +00:00
files : [{
attachment : `./Data/Transcripts/ ${ fileName } ` ,
2024-11-09 02:46:35 +00:00
name : `SPOILER_ ${ fileName } ` ,
2024-11-09 02:44:39 +00:00
spoiler : true
}]
2024-06-12 12:54:31 +01:00
});
2024-11-09 02:32:06 +00:00
2024-11-09 02:44:39 +00:00
if ( interaction . channel . type === Discord . ChannelType . GuildText ||
interaction . channel . type === Discord . ChannelType . PrivateThread ) {
2024-11-09 02:37:14 +00:00
await interaction . channel . delete ();
2024-11-09 02:32:06 +00:00
}
2024-11-09 00:37:51 +00:00
} catch ( err ) {
2024-11-09 02:32:06 +00:00
console . error ( "Error handling ticket close:" , err );
interaction . followUp ( "An error occurred while closing the ticket." );
2024-06-12 12:54:31 +01:00
}
2024-11-09 00:37:51 +00:00
}
2024-06-12 12:54:31 +01:00
2024-11-10 13:30:27 +00:00
function parseMarkdown ( content ) {
// Convert **bold** text to <strong> tags
content = content . replace ( /\*\*(.*?)\*\*/g , '<strong>$1</strong>' );
// Convert _italic_ text to <em> tags
content = content . replace ( /_(.*?)_/g , '<em>$1</em>' );
// Convert inline `code` to <code> tags
content = content . replace ( /`(.*?)`/g , '<code>$1</code>' );
// Convert [text](link) to <a> tags
content = content . replace ( /\[(.*?)\]\((.*?)\)/g , '<a href="$2" target="_blank">$1</a>' );
// Convert multiline code blocks ```code``` to <pre><code> tags
content = content . replace ( /```(.*?)```/gs, '<pre><code>$1</code></pre>');
return content ;
}
2024-11-09 02:32:06 +00:00
function createTranscriptHTML ( ticket , reason ) {
return `
2024-11-09 02:44:39 +00:00
<!DOCTYPE html>
2024-11-09 02:32:06 +00:00
<html>
<head>
2024-11-09 02:44:39 +00:00
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
2024-11-09 02:32:06 +00:00
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<style>
body {
background-color: #1a1a1a;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: white;
margin: 0;
2024-11-09 02:44:39 +00:00
padding: 20px;
2024-11-09 02:32:06 +00:00
}
.container {
2024-11-09 02:44:39 +00:00
max-width: 1000px;
margin: 0 auto;
background-color: rgba(0, 0, 0, 0.3);
2024-11-09 02:32:06 +00:00
border-radius: 15px;
padding: 20px;
}
.message {
2024-11-09 02:44:39 +00:00
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
}
.message-header {
2024-11-09 02:32:06 +00:00
display: flex;
align-items: center;
margin-bottom: 10px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
2024-11-09 02:44:39 +00:00
margin-right: 15px;
2024-11-09 02:32:06 +00:00
}
.username {
font-weight: bold;
2024-11-09 02:44:39 +00:00
color: #4CAF50;
2024-11-09 02:32:06 +00:00
}
.timestamp {
2024-11-09 02:44:39 +00:00
margin-left: auto;
color: #888;
font-size: 0.9em;
2024-11-09 02:32:06 +00:00
}
2024-11-09 02:44:39 +00:00
.content {
word-break: break-word;
}
.embed {
border-left: 4px solid #4CAF50;
padding-left: 10px;
margin: 10px 0;
background-color: rgba(0, 0, 0, 0.2);
}
.attachment {
2024-11-09 02:32:06 +00:00
display: inline-block;
2024-11-09 02:44:39 +00:00
background-color: rgba(76, 175, 80, 0.1);
border: 1px solid #4CAF50;
2024-11-09 02:32:06 +00:00
border-radius: 5px;
2024-11-09 02:44:39 +00:00
padding: 5px 10px;
margin: 5px 0;
color: #4CAF50;
text-decoration: none;
2024-11-09 02:32:06 +00:00
}
</style>
</head>
<body>
<div class="container">
2024-11-09 02:44:39 +00:00
<div class="mb-6">
<h1 class="text-3xl font-bold mb-4">Ticket Transcript</h1>
<p><strong>Channel:</strong> ${ ticket . name } </p>
<p><strong>Ticket ID:</strong> ${ ticket . id } </p>
<p><strong>Message Count:</strong> ${ ticket . messages . length } </p>
<p><strong>Close Reason:</strong> ${ reason } </p>
</div>
<div class="messages">
${ ticket . messages . map ( msg => `
2024-11-09 02:32:06 +00:00
<div class="message">
2024-11-09 02:44:39 +00:00
<div class="message-header">
<img src=" ${ msg . avatar } " alt="Avatar" class="avatar">
<span class="username"> ${ msg . username } </span>
<span class="timestamp"> ${ new Date ( msg . timestamp ). toLocaleString () } </span>
</div>
2024-11-09 02:32:06 +00:00
<div class="content">
2024-11-10 13:30:27 +00:00
${ parseMarkdown ( msg . content ) }
2024-11-09 02:44:39 +00:00
${ msg . embeds . map ( embed => `
<div class="embed">
${ embed . title ? `<div class="font-bold"> ${ embed . title } </div>` : '' }
${ embed . description ? `<div> ${ embed . description } </div>` : '' }
${ embed . fields . map ( field => `
<div class="mt-2">
<strong> ${ field . name } :</strong>
<div> ${ field . value } </div>
</div>
` ). join ( '' ) }
</div>
` ). join ( '' ) }
${ msg . attachments . map ( att => `
<a href=" ${ att . url } " class="attachment" target="_blank">
📎 ${ att . name }
</a>
` ). join ( '' ) }
2024-11-09 02:32:06 +00:00
</div>
2024-11-09 02:44:39 +00:00
</div>
` ). join ( '' ) }
2024-11-09 02:32:06 +00:00
</div>
</div>
</body>
</html>
2024-11-09 00:37:51 +00:00
` ;
2024-11-09 02:32:06 +00:00
}