From e25489d35f4d442be40aa712ae3ddf570b723298 Mon Sep 17 00:00:00 2001 From: PAW122 <70094237+PAW122@users.noreply.github.com> Date: Tue, 24 Dec 2024 16:17:26 +0100 Subject: [PATCH] working ticket system --- .../{normal => moderation}/reaction_role.js | 0 commands/moderation/ticket_close.js | 59 +++++++-- commands/moderation/ticket_settings.js | 87 ++++++++++--- commands/moderation/ticket_stats.js | 21 ++- commands/normal/ticket.js | 37 +++--- handlers/emoji_handler.js | 15 +-- handlers/ticket_ratings_emoji_handler.js | 123 ++++++++++++++++++ main.js | 2 + 8 files changed, 289 insertions(+), 55 deletions(-) rename commands/{normal => moderation}/reaction_role.js (100%) create mode 100644 handlers/ticket_ratings_emoji_handler.js diff --git a/commands/normal/reaction_role.js b/commands/moderation/reaction_role.js similarity index 100% rename from commands/normal/reaction_role.js rename to commands/moderation/reaction_role.js diff --git a/commands/moderation/ticket_close.js b/commands/moderation/ticket_close.js index 4a6d287..e7248d9 100644 --- a/commands/moderation/ticket_close.js +++ b/commands/moderation/ticket_close.js @@ -4,7 +4,7 @@ jeżeli komenda zostanie użyta na */ -const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js"); +const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } = require("discord.js"); const Database = require("../../db/database"); const database = new Database(__dirname + "/../../db/files/servers.json"); @@ -14,11 +14,12 @@ const command = new SlashCommandBuilder() .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) .setDescription("Close ticket") -async function execute(interaction, client) { +async function execute(interaction, client) { const guild_id = interaction.guild.id; const channel_id = interaction.channel.id; - const settings = database.get(`${guild_id}.tickets_settings`); + database.init() + const settings = database.read(`${guild_id}.ticket_settings`); if (!settings || !settings.status) { return interaction.reply({ content: `Ticket system is disabled`, @@ -26,15 +27,55 @@ async function execute(interaction, client) { }); } - if(settings.collect_ratings) { - // send embed with emoji's - // handler will check for reactions + // check if ticket is pending + const pending_tickets = await database.read(`${guild_id}.pending_tickets.${channel_id}`); + if (pending_tickets) { + return interaction.reply({ + content: `Ticket is already closed`, + ephemeral: true + }); } - // save ticket_close_time in db & in tickets_handler.js add_new_pengind_channel() + if (settings.collect_ratings) { + // send embed with emoji's + const embed = new EmbedBuilder() + .setColor('#0099ff') + .setTitle('Ticket Closed') + .setDescription('Thank you for using our ticket system. \nPlease rate your experience on a scale of 1 to 5.') + .setTimestamp(); + await interaction.reply({ embeds: [embed] }); + const msg = await interaction.fetchReply(); + // console.log(msg); + await msg.react('1️⃣'); + await msg.react('2️⃣'); + await msg.react('3️⃣'); + await msg.react('4️⃣'); + await msg.react('5️⃣'); - + const message_id = msg.id; + + /* + dopuki Date.now() nie będzie >= niż ticket_close_time + to wtedy ticket nie zostanie zamknięty + else usuń wpis z db + */ + const data = { + ticket_close_time: Date.now() + 24 * 60 * 60 * 1000, + ticket_opinion: { + user_id: interaction.user.id, // closed by + emoji_list: ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣'], + }, + status: true // false = ticket closed + } + + // save reactions for emoji handler + database.write(`${guild_id}.pending_tickets.${channel_id}.${message_id}`, data) + + return await interaction.channel.send(`Ticket closed. Thank you for using our ticket system. \nPlease rate your experience on a scale of 1 to 5.\nTicket will be closed in 24h`); + } + + // save ticket_close_time in db & in tickets_handler.js add_new_pengind_channel() } async function help_message(interaction, client) { @@ -44,4 +85,4 @@ async function help_message(interaction, client) { }) } -module.exports = {command, execute, help_message} \ No newline at end of file +module.exports = { command, execute, help_message } \ No newline at end of file diff --git a/commands/moderation/ticket_settings.js b/commands/moderation/ticket_settings.js index c597d52..87c781a 100644 --- a/commands/moderation/ticket_settings.js +++ b/commands/moderation/ticket_settings.js @@ -20,7 +20,7 @@ const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js"); const Database = require("../../db/database"); const database = new Database(__dirname + "/../../db/files/servers.json"); -const BotLogs = require("../../handlers/bot_logs_handler") +const BotLogs = require("../../handlers/bot_logs_handler"); const BotLogsHandler = BotLogs.getInstance() const command = new SlashCommandBuilder() @@ -30,16 +30,19 @@ const command = new SlashCommandBuilder() .addBooleanOption(option => option .setName("collect_ratings") .setDescription("ask user for opinion after closing a ticket") + .setRequired(true) ) .addBooleanOption(option => option .setName("delete_on_close") .setDescription("delete ticket whe closed, false = move ticket channel to closed_tickets category") + .setRequired(true) ) .addBooleanOption(option => option .setName("status") .setDescription("turn function on/off") ) + // TODO: add option to set role for ticket review channels async function execute(interaction, client) { const collect_ratings = interaction.options.getBoolean("collect_ratings") || false; @@ -48,13 +51,6 @@ async function execute(interaction, client) { const guild_id = interaction.guild.id; - if (!collect_ratings || !delete_on_close) { - return await interaction.reply({ - content: `You need to provide all options`, - ephemeral: true - }); - } - let data = { tickets_category: null, closed_tickets_category_id: null, @@ -64,42 +60,91 @@ async function execute(interaction, client) { } // search for category names "tickets" - const ticket_category_id = interaction.guild.channels.cache.find(c => c.name === "tickets" && c.type === "GUILD_CATEGORY")?.id; + const ticket_category_id = interaction.guild.channels.cache.find( + c => c.name === "tickets" && c.type === 4 + )?.id; + if (!ticket_category_id) { + try { - const category = await interaction.guild.channels.create("tickets", { - type: "GUILD_CATEGORY" + // Tworzenie kategorii, jeśli nie istnieje + const category = await interaction.guild.channels.create({ + name: "tickets", + type: 4, // Typ 4 = GUILD_CATEGORY + permissionOverwrites: [ + { + id: interaction.guild.id, // ID serwera (domyślna rola @everyone) + deny: ['ViewChannel'], // Zablokuj widoczność kanału dla wszystkich + }, + { + id: interaction.client.user.id, // ID bota + allow: ['ViewChannel', 'SendMessages', 'ManageChannels'], // Daj botowi pełny dostęp + }, + { + id: interaction.guild.roles.everyone.id, // Alternatywnie ustawienie dla domyślnej roli @everyone + deny: ['ViewChannel'], // Zablokuj dostęp + }, + ], }); + + data.tickets_category = category.id; + + if (category) { - interaction.guild.channels.cache.find(c => c.name === "tickets" && c.type === "GUILD_CATEGORY").id - data.tickets_category = category.id + console.log("Category created:"); + + // Aktualizacja danych z ID nowej kategorii + data.tickets_category = category.id; } } catch (err) { - return await interaction.channel.send({ - content: `I can't create category for tickets`, + console.error("Error while creating category:", err); + + return await interaction.reply({ + content: `I can't create a category for tickets.`, ephemeral: true }); } + } else { + // Jeśli kategoria istnieje, ustaw dane na jej ID + data.tickets_category = ticket_category_id; } + // search for category names "closed_tickets" - const closed_tickets_category_id = interaction.guild.channels.cache.find(c => c.name === "closed_tickets" && c.type === "GUILD_CATEGORY")?.id; + const closed_tickets_category_id = interaction.guild.channels.cache.find + (c => c.name === "closed_tickets" && c.type === 4)?.id; + if (!closed_tickets_category_id) { try { - const category = await interaction.guild.channels.create("closed_tickets", { - type: "GUILD_CATEGORY" + const category = await interaction.guild.channels.create({ + name: "closed_tickets", + type: 4, // 4 = GUILD_CATEGORY + permissionOverwrites: [ + { + id: interaction.guild.id, // ID serwera (domyślna rola @everyone) + deny: ['ViewChannel'], // Zablokuj widoczność kanału dla wszystkich + }, + { + id: interaction.client.user.id, // ID bota + allow: ['ViewChannel', 'SendMessages', 'ManageChannels'], // Daj botowi pełny dostęp + }, + { + id: interaction.guild.roles.everyone.id, // Alternatywnie ustawienie dla domyślnej roli @everyone + deny: ['ViewChannel'], // Zablokuj dostęp + }, + ], }); if (category) { - interaction.guild.channels.cache.find(c => c.name === "closed_tickets" && c.type === "GUILD_CATEGORY").id data.closed_tickets_category_id = category.id } } catch (err) { - return await interaction.channel.send({ + return await interaction.reply({ content: `I can't create category for closed tickets`, ephemeral: true }); } - + } else { + data.closed_tickets_category_id = closed_tickets_category_id } if (!data.tickets_category) { diff --git a/commands/moderation/ticket_stats.js b/commands/moderation/ticket_stats.js index 39ca290..4a1d159 100644 --- a/commands/moderation/ticket_stats.js +++ b/commands/moderation/ticket_stats.js @@ -5,13 +5,32 @@ const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js"); +const Database = require("../../db/database"); +const database = new Database(__dirname + "/../../db/files/servers.json"); + const command = new SlashCommandBuilder() .setName("ticket_stats") .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .setDescription("Get ticket's statistics") async function execute(interaction, client) { - + const guild_id = interaction.guild.id; + const data = await database.read(`${guild_id}.closed_tickets`); + let ratings_list = [] + + Object.values(data).forEach(day => { + day.forEach(ticket => { + ratings_list.push(ticket.rating); + }); + }); + + const average_rating = ratings_list.reduce((a, b) => a + b, 0) / ratings_list.length; + + + return await interaction.reply({ + content: `Average rating: ${average_rating}`, + ephemeral: true + }); } async function help_message(interaction, client) { diff --git a/commands/normal/ticket.js b/commands/normal/ticket.js index 50b7963..f2998de 100644 --- a/commands/normal/ticket.js +++ b/commands/normal/ticket.js @@ -25,28 +25,23 @@ const command = new SlashCommandBuilder() .addStringOption(option => option .setName("title") .setDescription("set title for the ticket") + .setRequired(true) ) .addStringOption(option => option - .setName("Description") + .setName("description") .setDescription("ticket content") + .setRequired(true) ) -function execute(interaction, client) { +async function execute(interaction, client) { const title = interaction.options.getString("title"); const description = interaction.options.getString("Description"); - if (!title || !description) { - return interaction.reply({ - content: `You need to provide all options`, - ephemeral: true - }); - } - const guild_id = interaction.guild.id; // check in db is ticket system is enabled - const settings = database.get(`${guild_id}.tickets_settings`); + const settings = database.read(`${guild_id}.ticket_settings`); const ticket_category_id = settings.tickets_category; if(!settings || !settings.status || !ticket_category_id) { return interaction.reply({ @@ -56,10 +51,22 @@ function execute(interaction, client) { } // create ticket channel in tickets category - const ticket_channel = interaction.guild.channels.create(title, { - type: "GUILD_TEXT", - parent: ticket_category_id + const ticket_channel = await interaction.guild.channels.create({ + name: `ticket-${interaction.user.id}`, + type: 0, // text channel + parent: ticket_category_id, + permissionOverwrites: [ + { + id: interaction.guild.id, + deny: ['ViewChannel'], // Deny view access to everyone + }, + { + id: interaction.user.id, + allow: ['ViewChannel'], // Allow view access to the ticket creator + }, + ], }); + // check is the channel was created if (!ticket_channel) { return interaction.reply({ @@ -75,10 +82,10 @@ function execute(interaction, client) { embeds: [{ title: title, description: description, - color: "BLUE", + color: 15844367, timestamp: new Date(), footer: { - text: interaction.user.id + text: "use /ticket_close to close ticket" } }] }); diff --git a/handlers/emoji_handler.js b/handlers/emoji_handler.js index 62aa669..54ffdfb 100644 --- a/handlers/emoji_handler.js +++ b/handlers/emoji_handler.js @@ -1,7 +1,12 @@ -const { use } = require("../api/app"); const Database = require("../db/database") const db = new Database(process.cwd() + "/db/files/servers.json") +/* + TODO: + jezeli user zareaguje na wiadomość ktora nie istnieje, + usunac wpis z db aby nie marnowac czasu na sprawdzanie wiadomosci +*/ + async function addEmoji(client, reaction, user) { if (reaction.partial) { try { @@ -66,14 +71,6 @@ async function addEmoji(client, reaction, user) { } } - - - - - - - - } async function removeEmoji(client, reaction, user) { diff --git a/handlers/ticket_ratings_emoji_handler.js b/handlers/ticket_ratings_emoji_handler.js new file mode 100644 index 0000000..e1e43a6 --- /dev/null +++ b/handlers/ticket_ratings_emoji_handler.js @@ -0,0 +1,123 @@ +const Database = require("../db/database") +const db = new Database(process.cwd() + "/db/files/servers.json") + + +async function addRatingEmoji(client, reaction, user) { + if (reaction.partial) { + try { + await reaction.fetch(); + } catch (error) { + return; + } + } + + // Wyciągnięcie informacji + const guildId = reaction.message.guild?.id || 'Brak serwera'; // Jeśli to wiadomość prywatna + const channelId = reaction.message.channel.id; + const messageId = reaction.message.id; + const userId = user.id; + const emoji = reaction.emoji; + + if (!guildId || !channelId || !messageId || !userId || !emoji) { + return //console.log("emoji_handler error - no data") + } + + // db actions + db.init() + const data = await db.read(`${guildId}.pending_tickets.${channelId}.${messageId}`); + if (!data) return; + + if (data.status === true) { + // console.log("Dodawanie roli użytkownikowi:"); + // console.log(element.role_id); + + try { + const data = await db.read(`${guildId}.pending_tickets.${channelId}.${messageId}`); + if (!data) return; + const guild = client.guilds.cache.get(guildId); // Pobranie serwera + + const member = guild ? await guild.members.fetch(userId) : null; // Pobranie członka serwera, jeśli istnieje + if (!member) { + return; + } + + //get channel name + const channel = client.channels.cache.get(channelId); + const channelName = channel.name; + // sprawdz czy channelName zaczyna się od "ticket-" + if (channelName.startsWith('ticket-')) { + // usuń ticket- z nazwy kanału + const ticketAuthorId = channelName.slice(7); + // sprawdz czy osoba zostawiajaca reakcje to autor ticketu + if (userId !== ticketAuthorId) return; + } + + // odczytaj jaką emoji zostawil user + const emojiName = emoji.name; + + let rating = null; + let i = 0; + + data.ticket_opinion.emoji_list.forEach(element => { + if (element === emojiName) { + rating = i; + } else { + i++; + } + }); + + if (rating === null || rating > 4 || rating < 0) { + console.log("rating error") + console.log(rating) + return; + } + + // change scale from 0-4 to 1-5 + rating += 1; + + const now = new Date(); + now.setHours(0, 1, 0, 0); + const todayDateTimestamp = now.getTime(); + + const close_time = new Date(); + + const save_data = { + channelId: channelId, + messageId: messageId, + userId: userId, + rating: rating, + close_time: close_time + } + + await db.addToList(`${guildId}.closed_tickets.${todayDateTimestamp}`, save_data); + await db.write(`${guildId}.pending_tickets.${channelId}.${messageId}.status`, false); + + // save user option & move to ticket closed_tikcets_category + // & remove user permisions to see channel + channel.permissionOverwrites.edit(userId, { + ViewChannel: false + }); + + // move channel to closed_tickets_category + const settings = await db.read(`${guildId}.ticket_settings`) + if(!settings) return; + if(settings.delete_on_close) { + channel.delete(); + } else { + const closed_tickets_category = client.channels.cache.get(settings.closed_tickets_category_id); + if(!closed_tickets_category) return; + channel.setParent(closed_tickets_category); + } + + // & set status to false on pending_tickets db + + // console.log(`Rola ${element.role_id} została dodana użytkownikowi ${member.user.tag}.`); + } catch (err) { + console.error('Błąd podczas dodawania roli - ticket_ratings_emoji_handler:', err); + } + } + + +} + +module.exports = { addRatingEmoji } \ No newline at end of file diff --git a/main.js b/main.js index 37ebbc7..8fbff37 100644 --- a/main.js +++ b/main.js @@ -89,6 +89,7 @@ const InviteTracker = require("./handlers/invite_tracker") const { AudioDataStore } = require("./handlers/audio/cache") const AudioStore = AudioDataStore.getInstance() const {addEmoji, removeEmoji} = require("./handlers/emoji_handler") +const {addRatingEmoji } = require("./handlers/ticket_ratings_emoji_handler") // "/test" handlers require("./test/handlers/handler")(client) @@ -184,6 +185,7 @@ client.on('interactionCreate', async interaction => { // emoji's client.on(Events.MessageReactionAdd, async (reaction, user) => { addEmoji(client, reaction, user) + addRatingEmoji(client, reaction, user) }); client.on(Events.MessageReactionRemove, async (reaction, user) => {