From 94510bbfb0f8d6c79632ea04448fbc8ad52e50ee Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 13 Dec 2023 21:39:18 +0100 Subject: [PATCH] v3.5.0 push 4 --- commands/reopen.js | 2 +- commands/stats.js | 53 +++++ core/dynamicdatabase/databases/stats.js | 2 +- core/statsManager.js | 245 +++++++++++++++++++++++- core/ticketActions/ticketAutoclose.js | 9 + core/ticketActions/ticketCloser.js | 28 ++- core/ticketActions/ticketOpener.js | 19 ++ core/ticketActions/ticketReopener.js | 14 +- core/transcriptSystem/manager.js | 4 + index.js | 3 +- 10 files changed, 368 insertions(+), 11 deletions(-) create mode 100644 commands/stats.js diff --git a/commands/reopen.js b/commands/reopen.js index d43082b..033ef0f 100644 --- a/commands/reopen.js +++ b/commands/reopen.js @@ -39,7 +39,7 @@ module.exports = () => { if (hiddendata.length < 1) return interaction.editReply({embeds:[bot.errorLog.notInATicket]}) const ticketId = hiddendata.find(d => d.key == "type").value - if (hiddendata.data.find(h => h.key == "pendingdelete")) return interaction.editReply({embeds:[bot.errorLog.warning("Warning!","You can't re-open a ticket while it's being deleted!")]}) + if (hiddendata.find(h => h.key == "pendingdelete")) return interaction.editReply({embeds:[bot.errorLog.warning("Warning!","You can't re-open a ticket while it's being deleted!")]}) await interaction.editReply({embeds:[bot.embeds.commands.reopenEmbed(interaction.user)],components:[bot.buttons.close.openRowNormal]}) diff --git a/commands/stats.js b/commands/stats.js new file mode 100644 index 0000000..70fa147 --- /dev/null +++ b/commands/stats.js @@ -0,0 +1,53 @@ +/** +!!! NEW !!! +The /stats command + +(well, it's not finished yet 😉) +================================================= +client.on("messageCreate",async (msg) => { + if (!msg.content.startsWith("!test global")) return + msg.channel.send({ + content:"testing `GLOBAL_STATS`", + embeds:[await createGlobalStatsEmbed()] + }) +}) +client.on("messageCreate",async (msg) => { + if (!msg.content.startsWith("!test ticket")) return + + if (!existStats("ticket","STATUS",msg.channel.id)){ + msg.channel.send({embeds:[bot.errorLog.notInATicket]}) + return + } + + msg.channel.send({ + content:"testing `TICKET_STATS`", + embeds:[await createTicketStatsEmbed(msg.guild,msg.channel.id)] + }) +}) +client.on("messageCreate",async (msg) => { + if (!msg.content.startsWith("!test user")) return + const mention = msg.mentions.users.first() + + if (mention){ + if (!existStats("user","TICKETS_CREATED",mention.id)){ + msg.channel.send({embeds:[bot.errorLog.serverError("This user can't be found in the stats database!")]}) + return + } + + msg.channel.send({ + content:"testing `USER_STATS`", + embeds:[await createUserStatsEmbed(msg.guild,mention,msg.channel.id)] + }) + }else{ + if (!existStats("user","TICKETS_CREATED",msg.author.id)){ + msg.channel.send({embeds:[bot.errorLog.serverError("This user can't be found in the stats database!")]}) + return + } + + msg.channel.send({ + content:"testing `USER_STATS`", + embeds:[await createUserStatsEmbed(msg.guild,msg.author,msg.channel.id)] + }) + } +}) + */ \ No newline at end of file diff --git a/core/dynamicdatabase/databases/stats.js b/core/dynamicdatabase/databases/stats.js index daa5f8a..2190e8a 100644 --- a/core/dynamicdatabase/databases/stats.js +++ b/core/dynamicdatabase/databases/stats.js @@ -50,7 +50,7 @@ exports.set = (category,key,value) => { exports.get = (category,key) => { const currentData = getData() const result = currentData.find((d) => (d.category == category) && (d.key == key)) - return result + return (result) ? result.value : undefined } /** diff --git a/core/statsManager.js b/core/statsManager.js index 2e11f40..213c8e7 100644 --- a/core/statsManager.js +++ b/core/statsManager.js @@ -1,11 +1,13 @@ +const { EmbedBuilder, Guild, TextChannel, Message, User } = require('discord.js') const bot = require('../index') const client = bot.client const config = bot.config const log = bot.errorLog.log const l = bot.language +const permissionChecker = require("./utils/permisssionChecker") /**@typedef {"TICKETS_CREATED"|"TICKETS_CLOSED"|"TICKETS_DELETED"|"TICKETS_REOPENED"|"TICKETS_AUTOCLOSED"|"TRANSCRIPTS_CREATED"|"TIME_SINCE_UPDATE"} OTGlobalStatsType */ -/**@typedef {"TICKET_AGE"|"CREATED_BY"} OTTicketStatsType */ +/**@typedef {"CREATED_AT"|"CREATED_BY"|"STATUS"} OTTicketStatsType */ /**@typedef {"TICKETS_CREATED"|"TICKETS_CLOSED"|"TICKETS_DELETED"|"TICKETS_REOPENED"} OTUserStatsType */ /** * @callback OTStatsCallback @@ -32,7 +34,7 @@ const updateGlobalStats = (type,callback) => { * @param {OTStatsCallback} callback */ const updateTicketStats = (type,id,callback) => { - const currentstat = bot.statsStorage.get("ticket_"+id,type) + const currentstat = bot.statsStorage.get("ticket"+id,type) const newstat = callback(currentstat) if (["string","number","boolean"].includes(typeof newstat)){ bot.statsStorage.set("ticket"+id,type,newstat) @@ -59,7 +61,7 @@ const updateUserStats = (type,id,callback) => { * @param {String} [id] */ const removeStats = (scope,type,id) => { - const newScope = (scope == "global") ? scope : scope+"_"+id + const newScope = (scope == "global") ? scope : scope+id bot.statsStorage.delete(newScope,type) } @@ -70,7 +72,7 @@ const removeStats = (scope,type,id) => { * @param {String} [id] */ const existStats = (scope,type,id) => { - const newScope = (scope == "global") ? scope : scope+"_"+id + const newScope = (scope == "global") ? scope : scope+id return (typeof bot.statsStorage.get(newScope,type) == "undefined") ? false : true } @@ -81,7 +83,7 @@ const existStats = (scope,type,id) => { * @param {String} [id] */ const getStats = (scope,type,id) => { - const newScope = (scope == "global") ? scope : scope+"_"+id + const newScope = (scope == "global") ? scope : scope+id return bot.statsStorage.get(newScope,type) } @@ -120,6 +122,230 @@ const startupStatsManager = () => { return 0 }) } + + //HANDLE MEMBER LEAVE (clear the database) + client.on("guildMemberRemove",(member) => { + if (existStats("user","TICKETS_CREATED",member.id)){ + removeStats("user","TICKETS_CREATED",member.id) + } + if (existStats("user","TICKETS_CLOSED",member.id)){ + removeStats("user","TICKETS_CLOSED",member.id) + } + if (existStats("user","TICKETS_DELETED",member.id)){ + removeStats("user","TICKETS_DELETED",member.id) + } + if (existStats("user","TICKETS_REOPENED",member.id)){ + removeStats("user","TICKETS_REOPENED",member.id) + } + }) +} + +/** + * @returns {Promise<{startDate:Number, updateDate:Number, version:String, ticketsCreated:Number, ticketsClosed:Number, ticketsDeleted:Number, ticketsReopened:Number, ticketsAutoclosed:Number, transcriptsCreated:Number}>} + */ +const getGlobalStats = async () => { + //live data + const startDate = new Date().getTime() - client.uptime + const version = (require("../package.json")).version + + //saved data + const ticketsCreated = getStats("global","TICKETS_CREATED") + const ticketsClosed = getStats("global","TICKETS_CLOSED") + const ticketsDeleted = getStats("global","TICKETS_DELETED") + const ticketsReopened = getStats("global","TICKETS_REOPENED") + const ticketsAutoclosed = getStats("global","TICKETS_AUTOCLOSED") + const transcriptsCreated = getStats("global","TRANSCRIPTS_CREATED") + const timeSinceUpdate = getStats("global","TIME_SINCE_UPDATE") + + return { + startDate, + updateDate: (typeof timeSinceUpdate == "undefined") ? new Date().getTime() : timeSinceUpdate, + version, + + ticketsCreated: (typeof ticketsCreated == "undefined") ? 0 : ticketsCreated, + ticketsClosed: (typeof ticketsClosed == "undefined") ? 0 : ticketsClosed, + ticketsDeleted: (typeof ticketsDeleted == "undefined") ? 0 : ticketsDeleted, + ticketsReopened: (typeof ticketsReopened == "undefined") ? 0 : ticketsReopened, + ticketsAutoclosed: (typeof ticketsAutoclosed == "undefined") ? 0 : ticketsAutoclosed, + transcriptsCreated: (typeof transcriptsCreated == "undefined") ? 0 : transcriptsCreated, + } +} + +/** + * + * @param {discord.TextChannel} channel + * @param {Number} limit + * @returns {Promise} + */ +const getmessages = async (channel,limit) => { + const final = [] + var lastId = "" + + while (true) { + const options = {limit:100} + if (lastId) options.before = lastId + + const messages = await channel.messages.fetch(options) + messages.forEach(msg => {final.push(msg)}) + lastId = messages.last().id + + if (messages.size != 100 || final >= limit) { + break + } + } + return final +} + +/** + * @param {Guild} guild + * @param {String} channelid + * @returns {Promise<{createdAt:Number, createdBy:String, messageAmount:Number, participants:String[], ticketName:String, status:"open"|"closed"|"reopened"|"autoclosed"}>} + */ +const getTicketStats = async (guild,channelid) => { + //live data + /**@type {TextChannel|undefined} */ + const channel = guild.channels.cache.find((c) => c.id == channelid) + const messageAmount = (channel) ? (await getmessages(channel,5000)).length : 0 + const participants = (channel) ? channel.members.map((m) => m.user.username) : [] + + //saved data + const createdAt = getStats("ticket","CREATED_AT",channelid) + const status = getStats("ticket","STATUS",channelid) + + const TEMPcreatedBy = getStats("ticket","CREATED_BY",channelid) + const TEMPcreatedByUser = (TEMPcreatedBy) ? guild.members.cache.find((m) => m.id == TEMPcreatedBy) : undefined + const createdBy = (TEMPcreatedByUser) ? TEMPcreatedByUser.user.username : "an unknown person 😄" + + return { + createdAt: (typeof createdAt == "undefined") ? new Date().getTime() : createdAt, + createdBy: (typeof createdBy == "undefined") ? 0 : createdBy, + messageAmount, + participants:participants, + ticketName:channel.name, + status: (typeof status == "undefined") ? "open" : status + } +} + +/** + * @param {Guild} guild + * @param {User} user + * @param {String} [channelid] + * @returns {Promise<{ticketsCreated:Number, ticketsClosed:Number, ticketsDeleted:Number, ticketsReopened:Number, role:"globaladmin"|"ticketadmin"|"member", userName:String}>} + */ +const getUserStats = async (guild,user,channelid) => { + //live data + const hiddendata = bot.hiddenData.readHiddenData(channelid) + const ticketId = (hiddendata.length < 1) ? false : hiddendata.find(d => d.key == "type").value + + const isGlobalAdmin = permissionChecker.global(user.id,guild.id) + const isTicketAdmin = (ticketId) ? permissionChecker.ticket(user.id,guild.id,ticketId) : false + const role = (isGlobalAdmin) ? "globaladmin" : ((isTicketAdmin) ? "ticketadmin" : "member") + + //saved data + const ticketsCreated = getStats("user","TICKETS_CREATED",user.id) + const ticketsClosed = getStats("user","TICKETS_CLOSED",user.id) + const ticketsDeleted = getStats("user","TICKETS_DELETED",user.id) + const ticketsReopened = getStats("user","TICKETS_REOPENED",user.id) + + return { + ticketsCreated: (typeof ticketsCreated == "undefined") ? 0 : ticketsCreated, + ticketsClosed: (typeof ticketsClosed == "undefined") ? 0 : ticketsClosed, + ticketsDeleted: (typeof ticketsDeleted == "undefined") ? 0 : ticketsDeleted, + ticketsReopened: (typeof ticketsReopened == "undefined") ? 0 : ticketsReopened, + role, + userName:user.username + } +} + +const createGlobalStatsEmbed = async () => { + const stats = await getGlobalStats() + + const sentences = [ + `Tickets Created: \`${stats.ticketsCreated}\``, + `Tickets Closed: \`${stats.ticketsClosed}\``, + `Tickets Deleted: \`${stats.ticketsDeleted}\``, + `Tickets Autoclosed: \`${stats.ticketsAutoclosed}\``, + `Tickets Reopened: \`${stats.ticketsReopened}\``, + `Startup Date: `, + `Latest Update: ` + ] + + const embed = new EmbedBuilder() + .setColor(config.color) + .setTitle("📊 Global Stats!") + .setDescription(sentences.slice(0,5).join("\n")) + .addFields({ + name:"System Stats:", + value:sentences.slice(5,7).join("\n") + }) + .setFooter({text:`Bot Version: v${stats.version}`}) + return embed +} + +/** + * @param {Guild} guild + * @param {String} channelid + */ +const createTicketStatsEmbed = async (guild,channelid) => { + const stats = await getTicketStats(guild,channelid) + + if (stats.status == "open"){ + var newStatus = `🟢 \`Open\`` + }else if (stats.status == "reopened"){ + var newStatus = `🟢 \`Reopened\`` + }else if (stats.status == "closed"){ + var newStatus = `🔴 \`Closed\`` + }else if (stats.status == "autoclosed"){ + var newStatus = `🔴 \`Autoclosed\`` + } + + const sentences = [ + `Ticket Created On: `, + `Ticket Created By: \`${stats.createdBy}\``, + `Messages Sent: \`${stats.messageAmount}\``, + `Status: ${newStatus}` + ] + + const embed = new EmbedBuilder() + .setColor(config.color) + .setTitle(`📊 Ticket Stats for #${stats.ticketName}!`) + .setDescription(sentences.join("\n")) + .addFields({ + name:"Participants:", + value:stats.participants.join("\n") + }) + return embed +} + +/** + * @param {Guild} guild + * @param {User} user + * @param {String} channelid + */ +const createUserStatsEmbed = async (guild,user,channelid) => { + const stats = await getUserStats(guild,user,channelid) + + if (stats.role == "globaladmin"){ + var newRole = `🟢 \`Global Admin\`` + }else if (stats.role == "ticketadmin"){ + var newRole = `🟡 \`Ticket Admin\`` + }else if (stats.role == "member"){ + var newRole = `🔴 \`Member\`` + } + + const sentences = [ + `Role: ${newRole}`, + `Tickets Created: \`${stats.ticketsCreated}\``, + `Tickets Closed: \`${stats.ticketsClosed}\``, + `Tickets Deleted: \`${stats.ticketsDeleted}\``, + `Tickets Reopened: \`${stats.ticketsReopened}\`` + ] + + const embed = new EmbedBuilder() + .setColor(config.color) + .setTitle(`📊 User Stats for ${stats.userName}!`) + .setDescription(sentences.join("\n")) + return embed } module.exports = { @@ -131,5 +357,12 @@ module.exports = { existStats, getStats, - startupStatsManager + startupStatsManager, + + getGlobalStats, + getTicketStats, + getUserStats, + createGlobalStatsEmbed, + createTicketStatsEmbed, + createUserStatsEmbed } \ No newline at end of file diff --git a/core/ticketActions/ticketAutoclose.js b/core/ticketActions/ticketAutoclose.js index 03ced20..55678d6 100644 --- a/core/ticketActions/ticketAutoclose.js +++ b/core/ticketActions/ticketAutoclose.js @@ -43,6 +43,15 @@ module.exports = () => { require("./ticketCloser").closeManager(clientMember,channel,prefix,"close","OTautoclose",true) storage.delete("autocloseTickets",channel.id) + //STATS + bot.statsManager.updateGlobalStats("TICKETS_AUTOCLOSED",(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) + bot.statsManager.updateTicketStats("STATUS",channel.id,(current) => { + return "autoclosed" + }) + channel.send({embeds:[bot.embeds.commands.autocloseSignalEmbed(client.user,t.value)],components:[bot.buttons.close.closeCommandRow]}) } diff --git a/core/ticketActions/ticketCloser.js b/core/ticketActions/ticketCloser.js index 999e15e..16d06e5 100755 --- a/core/ticketActions/ticketCloser.js +++ b/core/ticketActions/ticketCloser.js @@ -79,6 +79,19 @@ exports.closeManager = async (member,channel,prefix,mode,reason,nomessage) => { require("../api/modules/events").onTicketDelete(user,channel,guild,new Date(),{name:channel.name,status:"deleted",ticketOptions:ticketData}) + //STATS + bot.statsManager.updateGlobalStats("TICKETS_DELETED",(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) + bot.statsManager.updateUserStats("TICKETS_DELETED",user.id,(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) + bot.statsManager.removeStats("ticket","CREATED_BY",channel.id) + bot.statsManager.removeStats("ticket","CREATED_AT",channel.id) + bot.statsManager.removeStats("ticket","STATUS",channel.id) + hiddendata.push({key:"pendingdelete",value:"true"}) bot.hiddenData.writeHiddenData(channel.id,hiddendata) @@ -177,13 +190,26 @@ exports.closeManager = async (member,channel,prefix,mode,reason,nomessage) => { //send api event require("../api/modules/events").onTicketClose(user,channel,guild,new Date(),{name:channel.name,status:"closed",ticketOptions:ticketData},newReason) + + //STATS + bot.statsManager.updateGlobalStats("TICKETS_CLOSED",(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) + bot.statsManager.updateUserStats("TICKETS_CLOSED",user.id,(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) + bot.statsManager.updateTicketStats("STATUS",channel.id,(current) => { + return "closed" + }) } /** * * @param {discord.TextChannel} channel * @param {Number} limit - * @returns + * @returns {Promise} */ const getmessages = async (channel,limit) => { const final = [] diff --git a/core/ticketActions/ticketOpener.js b/core/ticketActions/ticketOpener.js index 4be69e6..300693c 100755 --- a/core/ticketActions/ticketOpener.js +++ b/core/ticketActions/ticketOpener.js @@ -214,6 +214,25 @@ module.exports = () => { log("system","created new ticket",[{key:"ticket",value:ticketName},{key:"user",value:interaction.user.username}]) require("../api/modules/events").onTicketOpen(interaction.user,ticketChannel,interaction.guild,new Date(),{name:ticketName,status:"open",ticketOptions:currentTicketOptions}) + //STATS: + bot.statsManager.updateGlobalStats("TICKETS_CREATED",(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) + bot.statsManager.updateUserStats("TICKETS_CREATED",interaction.user.id,(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) + bot.statsManager.updateTicketStats("CREATED_BY",ticketChannel.id,(current) => { + return interaction.user.id + }) + bot.statsManager.updateTicketStats("CREATED_AT",ticketChannel.id,(current) => { + return new Date().getTime() + }) + bot.statsManager.updateTicketStats("STATUS",ticketChannel.id,(current) => { + return "open" + }) + const channelbutton = new discord.ActionRowBuilder() .addComponents([ new discord.ButtonBuilder() diff --git a/core/ticketActions/ticketReopener.js b/core/ticketActions/ticketReopener.js index 4e809fa..e01aa45 100755 --- a/core/ticketActions/ticketReopener.js +++ b/core/ticketActions/ticketReopener.js @@ -46,9 +46,21 @@ const reopenTicket = (guild,channel,user) => { } } }) - channel.permissionOverwrites.set(permissionsArray) require("../api/modules/events").onTicketReopen(user,channel,guild,new Date(),{name:channel.name,status:"reopened",ticketOptions:false}) + + //STATS + bot.statsManager.updateGlobalStats("TICKETS_REOPENED",(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) + bot.statsManager.updateUserStats("TICKETS_REOPENED",user.id,(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) + bot.statsManager.updateTicketStats("STATUS",channel.id,(current) => { + return "reopened" + }) } exports.reopenTicket = reopenTicket \ No newline at end of file diff --git a/core/transcriptSystem/manager.js b/core/transcriptSystem/manager.js index 99d8b59..f517bb9 100644 --- a/core/transcriptSystem/manager.js +++ b/core/transcriptSystem/manager.js @@ -35,6 +35,10 @@ const checkAvailability = async () => { */ module.exports = async (messages,guild,channel,user,reason) => { require("../api/modules/events").onTranscriptCreation(messages,channel,guild,new Date()) + bot.statsManager.updateGlobalStats("TRANSCRIPTS_CREATED",(current) => { + if (typeof current != "undefined") return current+1 + return 1 + }) const msglist = await channel.messages.fetchPinned() const chName = channel.name diff --git a/index.js b/index.js index f49aa3d..fe6a2a8 100644 --- a/index.js +++ b/index.js @@ -111,7 +111,8 @@ exports.buttons = { } this.errorLog.log("debug","loaded buttons & embeds") -require("./core/statsManager").startupStatsManager() +exports.statsManager = require("./core/statsManager") +this.statsManager.startupStatsManager() //START CLIENT LOGIN PROCESS client.on('ready',async () => {