From e55438cc67a428a26b04bb22117501172a4ca85f Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:39:52 +0100 Subject: [PATCH] (v4.1) Added +/-10 Minor Features & Improvements --- src/actions/closeTicket.ts | 24 ++++++++ src/actions/createTicket.ts | 3 + src/actions/deleteTicket.ts | 80 +++++++++++++++++++++++++ src/builders/buttons.ts | 4 +- src/builders/embeds.ts | 65 ++++++++++---------- src/commands/close.ts | 12 ++++ src/commands/delete.ts | 20 +++++++ src/core/api/defaults/config.ts | 4 +- src/core/api/main.ts | 2 +- src/core/api/modules/client.ts | 19 +++--- src/core/api/openticket/transcript.ts | 39 +++++++++--- src/core/cli/quickSetup.ts | 2 +- src/data/framework/startScreenLoader.ts | 2 +- src/index.ts | 2 +- 14 files changed, 223 insertions(+), 55 deletions(-) diff --git a/src/actions/closeTicket.ts b/src/actions/closeTicket.ts index 1db612f..6742e38 100644 --- a/src/actions/closeTicket.ts +++ b/src/actions/closeTicket.ts @@ -222,6 +222,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start closing ticket if (params.data == "reason"){ @@ -309,6 +321,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start closing ticket if (params.data == "reason"){ diff --git a/src/actions/createTicket.ts b/src/actions/createTicket.ts index 4e79d7f..918378e 100644 --- a/src/actions/createTicket.ts +++ b/src/actions/createTicket.ts @@ -209,6 +209,9 @@ export const registerActions = async () => { const msg = await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:ticket-message").build(source,{guild,channel,user,ticket})).message) ticket.get("opendiscord:ticket-message").value = msg.id + + //pin ticket message (if required) + if (generalConfig.data.system.pinFirstTicketMessage && msg.pinnable) await msg.pin("Ticket Message") //manage stats await opendiscord.stats.get("opendiscord:ticket").setStat("opendiscord:messages-sent",ticket.id.value,1,"increase") diff --git a/src/actions/deleteTicket.ts b/src/actions/deleteTicket.ts index 4c368b7..7a50ddf 100644 --- a/src/actions/deleteTicket.ts +++ b/src/actions/deleteTicket.ts @@ -121,6 +121,14 @@ export const registerVerifyBars = async () => { new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { const permissionMode = generalConfig.data.system.permissions.delete + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (params.data == "no-transcript" && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + if (permissionMode == "none"){ //no permissions instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) @@ -167,6 +175,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start deleting ticket if (params.data == "reason"){ @@ -206,6 +226,14 @@ export const registerVerifyBars = async () => { new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { const permissionMode = generalConfig.data.system.permissions.delete + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (params.data == "no-transcript" && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + if (permissionMode == "none"){ //no permissions instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) @@ -252,6 +280,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start deleting ticket if (params.data == "reason"){ @@ -295,6 +335,14 @@ export const registerVerifyBars = async () => { new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { const permissionMode = generalConfig.data.system.permissions.delete + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (params.data == "no-transcript" && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + if (permissionMode == "none"){ //no permissions instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) @@ -341,6 +389,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start deleting ticket if (params.data == "reason"){ @@ -384,6 +444,14 @@ export const registerVerifyBars = async () => { new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { const permissionMode = generalConfig.data.system.permissions.delete + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (params.data == "no-transcript" && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + if (permissionMode == "none"){ //no permissions instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) @@ -430,6 +498,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start deleting ticket if (params.data == "reason"){ diff --git a/src/builders/buttons.ts b/src/builders/buttons.ts index fcdc172..cc9cba9 100644 --- a/src/builders/buttons.ts +++ b/src/builders/buttons.ts @@ -277,7 +277,7 @@ const ticketButtons = () => { instance.setMode("button") instance.setCustomId("od:pin-ticket_"+source) instance.setColor("gray") - instance.setEmoji("📌") + instance.setEmoji(generalConfig.data.system.pinEmoji) instance.setLabel(lang.getTranslation("actions.buttons.pin")) }) ) @@ -291,7 +291,7 @@ const ticketButtons = () => { instance.setMode("button") instance.setCustomId("od:unpin-ticket_"+source) instance.setColor("gray") - instance.setEmoji("📌") + instance.setEmoji(generalConfig.data.system.pinEmoji) instance.setLabel(lang.getTranslation("actions.buttons.unpin")) }) ) diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index efde0cf..7545ce4 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -596,8 +596,11 @@ const ticketEmbeds = () => { if (embedOptions.image) instance.setImage(embedOptions.image) if (embedOptions.timestamp) instance.setTimestamp(new Date()) if (embedOptions.description) instance.setDescription(embedOptions.description) - + if (ticket.option.get("opendiscord:questions").value.length > 0){ + //show config fields if mixing is allowed + if (generalConfig.data.system.displayFieldsWithQuestions) instance.addFields(...embedOptions.fields) + const answers = ticket.get("opendiscord:answers").value answers.forEach((answer) => { if (!answer.value || answer.value.length == 0) return @@ -629,7 +632,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔒",lang.getTranslation("actions.titles.close"))) instance.setDescription(lang.getTranslation("actions.descriptions.close")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -643,7 +646,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔓",lang.getTranslation("actions.titles.reopen"))) instance.setDescription(lang.getTranslation("actions.descriptions.reopen")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -657,7 +660,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("đŸ—‘ī¸",lang.getTranslation("actions.titles.delete"))) instance.setDescription(lang.getTranslation("actions.descriptions.delete")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -671,7 +674,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("👋",lang.getTranslation("actions.titles.claim"))) instance.setDescription(lang.getTranslation("actions.descriptions.claim")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -685,7 +688,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("â†Šī¸",lang.getTranslation("actions.titles.unclaim"))) instance.setDescription(lang.getTranslation("actions.descriptions.unclaim")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -697,9 +700,9 @@ const ticketEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.pin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.pin"))) instance.setDescription(lang.getTranslation("actions.descriptions.pin")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -711,9 +714,9 @@ const ticketEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.unpin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.unpin"))) instance.setDescription(lang.getTranslation("actions.descriptions.unpin")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -727,7 +730,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔄",lang.getTranslation("actions.titles.rename"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.rename",["`#"+data+"`"])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -741,7 +744,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔀",lang.getTranslation("actions.titles.move"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.move",["`"+data.get("opendiscord:name").value+"`"])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -755,7 +758,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.add"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.add",[discord.userMention(data.id)])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -769,7 +772,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.remove"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.remove",[discord.userMention(data.id)])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -801,10 +804,10 @@ const ticketEmbeds = () => { instance.setTitle(utilities.emojiTitle("â†Šī¸",lang.getTranslation("actions.titles.unclaim"))) instance.setDescription(lang.getTranslation("actions.logs.unclaimDm")) }else if (mode == "pin"){ - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.pin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.pin"))) instance.setDescription(lang.getTranslation("actions.logs.pinDm")) }else if (mode == "unpin"){ - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.unpin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.unpin"))) instance.setDescription(lang.getTranslation("actions.logs.unpinDm")) }else if (mode == "rename"){ instance.setTitle(utilities.emojiTitle("🔄",lang.getTranslation("actions.titles.rename"))) @@ -838,7 +841,7 @@ const ticketEmbeds = () => { //TODO TRANSLATION!!! {name:"Option"+":",value:"```"+(ticket.option.get("opendiscord:name").value)+"```",inline:false}, ) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```",inline:false}) if (mode == "close"){ instance.setTitle(utilities.emojiTitle("🔒",lang.getTranslation("actions.titles.close"))) @@ -856,10 +859,10 @@ const ticketEmbeds = () => { instance.setTitle(utilities.emojiTitle("â†Šī¸",lang.getTranslation("actions.titles.unclaim"))) instance.setDescription(lang.getTranslationWithParams("actions.logs.unclaimLog",[discord.userMention(user.id)])) }else if (mode == "pin"){ - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.pin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.pin"))) instance.setDescription(lang.getTranslationWithParams("actions.logs.pinLog",[discord.userMention(user.id)])) }else if (mode == "unpin"){ - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.unpin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.unpin"))) instance.setDescription(lang.getTranslationWithParams("actions.logs.unpinLog",[discord.userMention(user.id)])) }else if (mode == "rename"){ instance.setTitle(utilities.emojiTitle("🔄",lang.getTranslation("actions.titles.rename"))) @@ -912,7 +915,7 @@ const blacklistEmbeds = () => { if (blacklist){ instance.setDescription(lang.getTranslationWithParams("actions.descriptions.blacklistGetSuccess",[discord.userMention(data.id)])) - if (blacklist.reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+blacklist.reason+"```"}) + if (blacklist.reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(blacklist.reason ?? "/")+"```"}) }else instance.setDescription("*"+lang.getTranslationWithParams("actions.descriptions.blacklistGetEmpty",[discord.userMention(data.id)])+"*") }) @@ -928,7 +931,7 @@ const blacklistEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("đŸšĢ",lang.getTranslation("actions.titles.blacklistAdd"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.blacklistAdd",[discord.userMention(data.id)])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -942,7 +945,7 @@ const blacklistEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🆓",lang.getTranslation("actions.titles.blacklistRemove"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.blacklistRemove",[discord.userMention(data.id)])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -959,7 +962,7 @@ const blacklistEmbeds = () => { instance.setTitle(utilities.emojiTitle((mode == "add") ? "đŸšĢ" : "🆓",title)) instance.setTimestamp(new Date()) instance.setDescription(text) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -978,7 +981,7 @@ const blacklistEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setTimestamp(new Date()) instance.setDescription(text) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) } @@ -1082,7 +1085,7 @@ const transcriptEmbeds = () => { instance.setTimestamp(new Date()) instance.setDescription(lang.getTranslation("transcripts.errors.error")) instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) } @@ -1265,7 +1268,7 @@ const autoEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("âąī¸",lang.getTranslation("actions.titles.autocloseEnabled"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.autocloseEnabled",[time.toString()])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -1279,7 +1282,7 @@ const autoEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("âąī¸",lang.getTranslation("actions.titles.autodeleteEnabled"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.autodeleteEnabled",[time.toString()])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -1293,7 +1296,7 @@ const autoEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("âąī¸",lang.getTranslation("actions.titles.autocloseDisabled"))) instance.setDescription(lang.getTranslation("actions.descriptions.autocloseDisabled")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -1307,7 +1310,7 @@ const autoEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("âąī¸",lang.getTranslation("actions.titles.autodeleteDisabled"))) instance.setDescription(lang.getTranslation("actions.descriptions.autodeleteDisabled")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) } @@ -1337,7 +1340,7 @@ const extraEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🚨","Priority Changed")) //TODO TRANSLATION!!! instance.setDescription("The ticket priority has been changed to **"+priority.renderDisplayName()+"** by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -1364,7 +1367,7 @@ const extraEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔀","Ticket Transferred")) //TODO TRANSLATION!!! instance.setDescription("The ticket ownership has been transferred from "+discord.userMention(oldCreator.id)+" to "+discord.userMention(newCreator.id)+" by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) } \ No newline at end of file diff --git a/src/commands/close.ts b/src/commands/close.ts index ebb4616..56acc36 100644 --- a/src/commands/close.ts +++ b/src/commands/close.ts @@ -64,6 +64,18 @@ export const registerCommandResponders = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } const reason = instance.options.getString("reason",false) diff --git a/src/commands/delete.ts b/src/commands/delete.ts index 11d315f..0b08657 100644 --- a/src/commands/delete.ts +++ b/src/commands/delete.ts @@ -59,10 +59,30 @@ export const registerCommandResponders = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } const reason = instance.options.getString("reason",false) const withoutTranscript = instance.options.getBoolean("notranscript",false) ?? false + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (withoutTranscript && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + //start deleting ticket await instance.defer(false) await instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:delete-message").build(source,{guild,channel,user,ticket,reason})) diff --git a/src/core/api/defaults/config.ts b/src/core/api/defaults/config.ts index d29f39d..f828f3e 100644 --- a/src/core/api/defaults/config.ts +++ b/src/core/api/defaults/config.ts @@ -4,7 +4,7 @@ import { ODValidButtonColor, ODValidId } from "../modules/base" import * as discord from "discord.js" import { ODConfigManager, ODConfig, ODJsonConfig } from "../modules/config" -import { ODClientActivityStatus, ODClientActivityType } from "../modules/client" +import { ODClientActivityMode, ODClientActivityType } from "../modules/client" import { ODRoleUpdateMode } from "../openticket/role" /** (CONTRIBUTOR GUIDE) HOW TO ADD NEW CONFIG VARIABLES? @@ -77,7 +77,7 @@ export interface ODJsonConfig_DefaultStatusType { /**The type of status (e.g. playing, listening, custom, ...) */ type:Exclude, /**The mode/status of the bot (e.g. online, invisible, idle, do not disturb) */ - mode:ODClientActivityStatus + mode:ODClientActivityMode /**The text for the status. */ text:string, /**Additional text for the status. (visible below 'text') */ diff --git a/src/core/api/main.ts b/src/core/api/main.ts index 4738986..811d5d6 100644 --- a/src/core/api/main.ts +++ b/src/core/api/main.ts @@ -176,7 +176,7 @@ export class ODMain { this.panels = new ODPanelManager(this.debug) this.tickets = new ODTicketManager(this.debug,this.client) this.blacklist = new ODBlacklistManager(this.debug) - this.transcripts = new ODTranscriptManager_Default(this.debug,this.tickets,this.client) + this.transcripts = new ODTranscriptManager_Default(this.debug,this.tickets,this.client,this.permissions) this.roles = new ODRoleManager(this.debug) this.priorities = new ODPriorityManager_Default(this.debug) } diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts index d51423c..5638a99 100644 --- a/src/core/api/modules/client.ts +++ b/src/core/api/modules/client.ts @@ -335,10 +335,10 @@ export class ODClientManager { * Possible activity types for the bot. */ export type ODClientActivityType = ("playing"|"listening"|"watching"|"custom"|false) -/**## ODClientPermissions `type` +/**## ODClientActivityMode `type` * Possible activity statuses for the bot. */ -export type ODClientActivityStatus = ("online"|"invisible"|"idle"|"dnd") +export type ODClientActivityMode = ("online"|"invisible"|"idle"|"dnd") /**## ODClientActivityManager `class` @@ -358,8 +358,10 @@ export class ODClientActivityManager { type: ODClientActivityType = false /**The current status text */ text: string = "" - /**The current status status */ - status: ODClientActivityStatus = "online" + /**The current status mode */ + mode: ODClientActivityMode = "online" + /**Additional state text */ + state: string = "" /**The timer responsible for refreshing the status. Stop it using `clearInterval(interval)` */ interval?: NodeJS.Timeout @@ -374,10 +376,11 @@ export class ODClientActivityManager { } /**Update the status. When already initiated, it can take up to 10min to see the updated status in discord. */ - setStatus(type:ODClientActivityType, text:string, status:ODClientActivityStatus, forceUpdate?:boolean){ + setStatus(type:ODClientActivityType, text:string, mode:ODClientActivityMode, state:string, forceUpdate?:boolean){ this.type = type this.text = text - this.status = status + this.mode = mode + this.state = state if (forceUpdate) this.#updateClientActivity(this.type,this.text) } @@ -404,10 +407,10 @@ export class ODClientActivityManager { this.manager.client.user.setPresence({ activities:[{ type:this.#getStatusTypeEnum(type), - state:undefined, + state:this.state ? this.state : undefined, name:text, }], - status:this.status + status:this.mode }) } /**Get the enum that links to the correct type */ diff --git a/src/core/api/openticket/transcript.ts b/src/core/api/openticket/transcript.ts index 6a7d6a4..9234ec9 100644 --- a/src/core/api/openticket/transcript.ts +++ b/src/core/api/openticket/transcript.ts @@ -7,6 +7,7 @@ import { ODTicket, ODTicketManager } from "./ticket" import { ODMessageBuildResult } from "../modules/builder" import { ODClientManager } from "../modules/client" import * as discord from "discord.js" +import { ODPermissionManager_Default } from "#opendiscord-types" /**## ODTranscriptManager `class` * This is an Open Ticket transcript manager. @@ -21,10 +22,10 @@ export class ODTranscriptManager extends ODManager= 1000000000000) return {size:(Math.round((bytes/1000000000000)*100)/100),unit:"TB"} - if (bytes >= 1000000000) return {size:(Math.round((bytes/1000000000)*100)/100),unit:"GB"} - else if (bytes >= 1000000) return {size:(Math.round((bytes/1000000)*100)/100),unit:"MB"} - else if (bytes >= 1000) return {size:(Math.round((bytes/1000)*100)/100),unit:"KB"} - else return {size:bytes,unit:"B"} + if (bytes < 1024) return {size:Math.round(bytes),unit:"B"} + else if (bytes < 1024*1024) return {size:Math.round(bytes/1024),unit:"KB"} + else if (bytes < 1024*1024*1024) return {size:Math.round(bytes/(1024*1024)),unit:"MB"} + else if (bytes < 1024*1024*1024*1024) return {size:Math.round(bytes/(1024*1024*1024)),unit:"GB"} + else return {size:Math.round(bytes/(1024*1024*1024*1024)),unit:"TB"} } /**Get the `ODTranscriptEmojiData` from a discord.js component emoji. */ #handleComponentEmoji(message:discord.Message, rawEmoji:discord.APIMessageComponentEmoji|null): ODTranscriptEmojiData|null { @@ -439,6 +443,25 @@ export class ODTranscriptCollector { return userData } + /**Analyse the ticket for the amount of messages users & admins have sent in the ticket. */ + async ticketUserMessagesAnalysis(ticket:ODTicket,guild:discord.Guild,channel:discord.GuildTextBasedChannel){ + const messages = await this.collectAllMessages(ticket,{bots:true,client:true,users:true}) + if (!messages) return null + const parsedMessages = await this.convertMessagesToTranscriptData(messages) + let userMessages = 0 + let adminMessages = 0 + + for (const msg of parsedMessages){ + if (msg.author.tag || msg.author.id == this.#client.client.user.id) continue + const user = await this.#client.fetchUser(msg.author.id) + if (!user) continue + const isAdmin = this.#permissions.hasPermissions("support",await this.#permissions.getPermissions(user,channel,guild)) + if (isAdmin) adminMessages++ + else userMessages++ + } + + return {userMessages,adminMessages,totalMessages:userMessages+adminMessages} + } } /**## ODTranscriptCollectorIncludeSettings `interface` diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index d09a088..b26cc57 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -226,7 +226,7 @@ async function quickSetupLogin(token:string): Promise return new Promise(async (resolve) => { try{ client.readyListener = async () => { - client.activity.setStatus("custom","Configuring Open Ticket...","idle",true) + client.activity.setStatus("custom","Configuring Open Ticket...","idle","",true) resolve(client) } const success = await client.login(true) diff --git a/src/data/framework/startScreenLoader.ts b/src/data/framework/startScreenLoader.ts index b80d7dc..328e01b 100644 --- a/src/data/framework/startScreenLoader.ts +++ b/src/data/framework/startScreenLoader.ts @@ -31,7 +31,7 @@ export const loadAllStartScreenComponents = async () => { //STATS opendiscord.startscreen.add(new api.ODStartScreenPropertiesCategoryComponent("opendiscord:stats",2,"startup info",[ - {key:"status",value:ansis.bold(opendiscord.client.activity.getStatusType())+opendiscord.client.activity.text+" ("+opendiscord.client.activity.status+")"}, + {key:"status",value:ansis.bold(opendiscord.client.activity.getStatusType())+opendiscord.client.activity.text+" ("+opendiscord.client.activity.mode+")"}, {key:"options",value:"loaded "+ansis.bold(opendiscord.options.getLength().toString())+" options!"}, {key:"panels",value:"loaded "+ansis.bold(opendiscord.panels.getLength().toString())+" panels!"}, {key:"tickets",value:"loaded "+ansis.bold(opendiscord.tickets.getLength().toString())+" tickets!"}, diff --git a/src/index.ts b/src/index.ts index d2b5de5..d9fd98e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -412,7 +412,7 @@ const main = async () => { opendiscord.log("Loading client activity...","system") if (opendiscord.defaults.getDefault("clientActivityLoading")){ //load config status - if (generalConfig.data.status && generalConfig.data.status.enabled) opendiscord.client.activity.setStatus(generalConfig.data.status.type,generalConfig.data.status.text,generalConfig.data.status.mode) + if (generalConfig.data.status && generalConfig.data.status.enabled) opendiscord.client.activity.setStatus(generalConfig.data.status.type,generalConfig.data.status.text,generalConfig.data.status.mode,generalConfig.data.status.state) } await opendiscord.events.get("onClientActivityLoad").emit([opendiscord.client.activity,opendiscord.client]) await opendiscord.events.get("afterClientActivityLoaded").emit([opendiscord.client.activity,opendiscord.client])