From 3a47a7df3f5d5140066eef6a8efb69d20764c034 Mon Sep 17 00:00:00 2001 From: Isaac Date: Sat, 27 May 2023 01:56:29 +0100 Subject: [PATCH] feat: inactivity warnings and automatic closure (closes #299 and #305) --- src/i18n/en-GB.yml | 10 +++ src/listeners/client/ready.js | 116 ++++++++++++++++++++++++++++++---- 2 files changed, 112 insertions(+), 14 deletions(-) diff --git a/src/i18n/en-GB.yml b/src/i18n/en-GB.yml index 1f8f977..281c174 100644 --- a/src/i18n/en-GB.yml +++ b/src/i18n/en-GB.yml @@ -399,6 +399,11 @@ ticket: title: ❓ {requestedBy} wants to close this ticket wait_for_staff: ✋ Please wait for staff to close this ticket. wait_for_user: ✋ Please wait for the user to respond. + closing_soon: + description: | + This ticket will be closed due to inactivity . + Send a message to cancel this automation. + title: ⌛ This ticket will be closed soon created: description: "Your ticket channel has been created: {channel}." title: ✅ Ticket created @@ -406,6 +411,11 @@ ticket: description: Your changes have been saved. title: ✅ Ticket updated feedback: Thank you for your feedback. + inactive: + description: | + There hasn't been any activity in this channel since . + Please continue the conversation or {close} the ticket. + title: ⏰ This ticket is inactive offline: description: There aren't any staff members available at the moment, so it may diff --git a/src/listeners/client/ready.js b/src/listeners/client/ready.js index 53d5402..ec729ef 100644 --- a/src/listeners/client/ready.js +++ b/src/listeners/client/ready.js @@ -9,6 +9,13 @@ const { version } = require('../../../package.json'); const { msToMins } = require('../../lib/misc'); const sync = require('../../lib/sync'); const checkForUpdates = require('../../lib/updates'); +const { isStaff } = require('../../lib/users'); +const { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, +} = require('discord.js'); +const ExtendedEmbedBuilder = require('../../lib/embed'); module.exports = class extends Listener { constructor(client, options) { @@ -137,20 +144,101 @@ module.exports = class extends Listener { setInterval(() => checkForUpdates(client), ms('1w')); } - setInterval(() => { - // TODO: check lastMessageAt and set stale - // this.$stale.set(ticket.id, { - // closeAt: ticket.guild.autoClose ? Date.now() + ticket.guild.autoClose : null, - // closedBy: null, // null if set as stale due to inactivity - // message: sent, - // messages: 0, - // reason: 'inactivity', - // staleSince: Date.now(), - // }); + // send inactivity warnings and close stale tickets + const staleInterval = ms('5m'); + setInterval(async () => { + // close stale tickets + for (const [ticketId, $] of client.tickets.$stale) { + const autoCloseAfter = $.closeAt - $.staleSince; + const halfway = $.closeAt - (autoCloseAfter / 2); + if (Date.now() >= halfway && Date.now() < halfway + staleInterval) { + const channel = client.channels.cache.get(ticketId); + if (!channel) continue; + const { guild } = await client.prisma.ticket.findUnique({ + select: { guild: true }, + where: { id: ticketId }, + }); + const getMessage = client.i18n.getLocale(guild.locale); + await channel.send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(guild.primaryColour) + .setTitle(getMessage('ticket.closing_soon.title')) + .setDescription(getMessage('ticket.closing_soon.description', { timestamp: Math.floor($.closeAt / 1000) })), + ], + }); + } else if ($.closeAt < Date.now()) { + client.tickets.finallyClose(ticketId, $.reason); + } + } - // for (const [ticketId, $] of client.tickets.$stale) { - // // ⌛ - // } - }, ms('5m')); + const guilds = await client.prisma.guild.findMany({ + include: { + tickets: { + include: { category: true }, + where: { open: true }, + }, + }, + // where: { staleAfter: { not: null } }, + where: { staleAfter: { gte: staleInterval } }, + }); + + // set inactive tickets as stale + for (const guild of guilds) { + for (const ticket of guild.tickets) { + // if (ticket.lastMessageAt && ticket.lastMessageAt < Date.now() - guild.staleAfter) + if (ticket.lastMessageAt && Date.now() - ticket.lastMessageAt > guild.staleAfter) { + /** @type {import("discord.js").TextChannel} */ + const channel = client.channels.cache.get(ticket.id); + const messages = (await channel.messages.fetch({ limit: 5 })).filter(m => m.author.id !== client.user.id); + let ping = ''; + + if (messages.size > 0) { + const lastMessage = messages.first(); + const staff = await isStaff(channel.guild, lastMessage.author.id); + if (staff) ping = lastMessage.author.toString(); + else ping = ticket.category.pingRoles.map(r => `<@&${r}>`).join(' '); + } + + const getMessage = client.i18n.getLocale(guild.locale); + const closeComamnd = client.application.commands.cache.find(c => c.name === 'close'); + const sent = await channel.send({ + components: [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'close' })) + .setStyle(ButtonStyle.Danger) + .setEmoji(getMessage('buttons.close.emoji')) + .setLabel(getMessage('buttons.close.text')), + ), + ], + content: ping, + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: channel.guild.iconURL(), + text: guild.footer, + }) + .setColor(guild.primaryColour) + .setTitle(getMessage('ticket.inactive.title')) + .setDescription(getMessage('ticket.inactive.description', { + close: ``, + timestamp: Math.floor(ticket.lastMessageAt.getTime() / 1000), + })), + ], + }); + + client.tickets.$stale.set(ticket.id, { + closeAt: guild.autoClose ? Date.now() + guild.autoClose : null, + closedBy: null, + message: sent, + messages: 0, + reason: 'inactivity', + staleSince: Date.now(), + }); + } + } + } + }, staleInterval); } };