From 460138fb7340e3820deee4850e5b5805f56874ba Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 30 Sep 2022 17:09:35 +0100 Subject: [PATCH] Progress on message archiving --- db/mysql/schema.prisma | 62 +++---- db/postgresql/schema.prisma | 62 +++---- db/sqlite/schema.prisma | 60 +++---- src/commands/message/create.js | 1 - src/lib/tickets/archiver.js | 248 +++++++++++++++++++++++++- src/lib/tickets/manager.js | 79 +++++--- src/listeners/client/messageCreate.js | 33 +++- 7 files changed, 416 insertions(+), 129 deletions(-) diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index fa22ce6..a3e1490 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -22,7 +22,7 @@ model ArchivedChannel { model ArchivedMessage { author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) authorId String @db.VarChar(19) - content Json + content String @db.Text createdAt DateTime @default(now()) deleted Boolean @default(false) edited Boolean @default(false) @@ -50,17 +50,17 @@ model ArchivedRole { model ArchivedUser { archivedMessages ArchivedMessage[] - avatar String + avatar String? bot Boolean @default(false) createdAt DateTime @default(now()) - discriminator String @db.Char(4) - displayName String @db.Text - role ArchivedRole @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) - roleId String @db.VarChar(19) + discriminator String? @db.Char(4) + displayName String? @db.Text + role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) + roleId String? @db.VarChar(19) ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) ticketId String @db.VarChar(19) userId String @db.VarChar(19) - username String @db.Text + username String? @db.Text @@id([ticketId, userId]) @@unique([ticketId, userId]) @@ -68,30 +68,30 @@ model ArchivedUser { } model Category { - channelName String - claiming Boolean @default(false) - createdAt DateTime @default(now()) - cooldown Int? - customTopic String? - description String - discordCategory String @db.VarChar(19) - emoji String - enableFeedback Boolean @default(false) - guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) - guildId String @db.VarChar(19) - id Int @id @default(autoincrement()) - image String? - memberLimit Int @default(1) - name String - openingMessage String @db.Text - pingRoles Json @default("[]") - questions Question[] - ratelimit Int? - requiredRoles Json @default("[]") - requireTopic Boolean @default(false) - staffRoles Json - tickets Ticket[] - totalLimit Int @default(50) + channelName String + claiming Boolean @default(false) + createdAt DateTime @default(now()) + cooldown Int? + customTopic String? + description String + discordCategory String @db.VarChar(19) + emoji String + enableFeedback Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + image String? + memberLimit Int @default(1) + name String + openingMessage String @db.Text + pingRoles Json @default("[]") + questions Question[] + ratelimit Int? + requiredRoles Json @default("[]") + requireTopic Boolean @default(false) + staffRoles Json + tickets Ticket[] + totalLimit Int @default(50) @@map("categories") } diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index c20cd83..c711a95 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -22,7 +22,7 @@ model ArchivedChannel { model ArchivedMessage { author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) authorId String @db.VarChar(19) - content Json + content String @db.Text createdAt DateTime @default(now()) deleted Boolean @default(false) edited Boolean @default(false) @@ -50,17 +50,17 @@ model ArchivedRole { model ArchivedUser { archivedMessages ArchivedMessage[] - avatar String + avatar String? bot Boolean @default(false) createdAt DateTime @default(now()) - discriminator String @db.Char(4) - displayName String @db.Text - role ArchivedRole @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) - roleId String @db.VarChar(19) + discriminator String? @db.Char(4) + displayName String? @db.Text + role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) + roleId String? @db.VarChar(19) ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) ticketId String @db.VarChar(19) userId String @db.VarChar(19) - username String @db.Text + username String? @db.Text @@id([ticketId, userId]) @@unique([ticketId, userId]) @@ -68,30 +68,30 @@ model ArchivedUser { } model Category { - channelName String - claiming Boolean @default(false) - createdAt DateTime @default(now()) - cooldown Int? - customTopic String? - description String - discordCategory String @db.VarChar(19) - emoji String - enableFeedback Boolean @default(false) - guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) - guildId String @db.VarChar(19) - id Int @id @default(autoincrement()) - image String? - memberLimit Int @default(1) - name String - openingMessage String @db.Text - pingRoles Json @default("[]") - questions Question[] - ratelimit Int? - requiredRoles Json @default("[]") - requireTopic Boolean @default(false) - staffRoles Json - tickets Ticket[] - totalLimit Int @default(50) + channelName String + claiming Boolean @default(false) + createdAt DateTime @default(now()) + cooldown Int? + customTopic String? + description String + discordCategory String @db.VarChar(19) + emoji String + enableFeedback Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + image String? + memberLimit Int @default(1) + name String + openingMessage String @db.Text + pingRoles Json @default("[]") + questions Question[] + ratelimit Int? + requiredRoles Json @default("[]") + requireTopic Boolean @default(false) + staffRoles Json + tickets Ticket[] + totalLimit Int @default(50) @@map("categories") } diff --git a/db/sqlite/schema.prisma b/db/sqlite/schema.prisma index f82e005..2a45cf5 100644 --- a/db/sqlite/schema.prisma +++ b/db/sqlite/schema.prisma @@ -50,17 +50,17 @@ model ArchivedRole { model ArchivedUser { archivedMessages ArchivedMessage[] - avatar String + avatar String? bot Boolean @default(false) createdAt DateTime @default(now()) - discriminator String - displayName String - role ArchivedRole @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) - roleId String + discriminator String? + displayName String? + role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) + roleId String? ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) ticketId String userId String - username String + username String? @@id([ticketId, userId]) @@unique([ticketId, userId]) @@ -68,30 +68,30 @@ model ArchivedUser { } model Category { - channelName String - claiming Boolean @default(false) - createdAt DateTime @default(now()) - cooldown Int? - customTopic String? - description String - discordCategory String - emoji String - enableFeedback Boolean @default(false) - guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) - guildId String - id Int @id @default(autoincrement()) - image String? - memberLimit Int @default(1) - name String - openingMessage String - pingRoles String @default("[]") - questions Question[] - ratelimit Int? - requiredRoles String @default("[]") - requireTopic Boolean @default(false) - staffRoles String - tickets Ticket[] - totalLimit Int @default(50) + channelName String + claiming Boolean @default(false) + createdAt DateTime @default(now()) + cooldown Int? + customTopic String? + description String + discordCategory String + emoji String + enableFeedback Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + id Int @id @default(autoincrement()) + image String? + memberLimit Int @default(1) + name String + openingMessage String + pingRoles String @default("[]") + questions Question[] + ratelimit Int? + requiredRoles String @default("[]") + requireTopic Boolean @default(false) + staffRoles String + tickets Ticket[] + totalLimit Int @default(50) @@map("categories") } diff --git a/src/commands/message/create.js b/src/commands/message/create.js index cda68bd..51dc5cf 100644 --- a/src/commands/message/create.js +++ b/src/commands/message/create.js @@ -18,7 +18,6 @@ module.exports = class CreateMessageCommand extends MessageCommand { * @param {import("discord.js").MessageContextMenuCommandInteraction} interaction */ async run(interaction) { - // TODO: archive message await useGuild(this.client, interaction, { referencesMessage: interaction.targetMessage.channelId + '/' + interaction.targetId }); } }; \ No newline at end of file diff --git a/src/lib/tickets/archiver.js b/src/lib/tickets/archiver.js index 7db83ef..7756976 100644 --- a/src/lib/tickets/archiver.js +++ b/src/lib/tickets/archiver.js @@ -1,11 +1,255 @@ const Cryptr = require('cryptr'); -// const cryptr = new Cryptr(process.env.ENCRYPTION_KEY); +const cryptr = new Cryptr(process.env.ENCRYPTION_KEY); + +/** + * Returns highest (roles.highest) hoisted role , or everyone + * @param {import("discord.js").GuildMember} member + * @returns {import("discord.js").Role} + */ +const hoistedRole = member => member.roles.hoist || member.guild.roles.everyone; module.exports = class TicketArchiver { constructor(client) { /** @type {import("client")} */ this.client = client; + this.encrypt = cryptr.encrypt; + this.decrypt = cryptr.decrypt; } - async addMessage() {} + /** Add or update a message + * @param {string} ticketId + * @param {import("discord.js").Message} message + * @param {boolean?} external + * @returns {import("@prisma/client").ArchivedMessage|boolean} + */ + async saveMessage(ticketId, message, external = false) { + if (this.client.config.overrides.disableArchives) return false; + + if (!message.member) { + try { + message.member = await message.guild.members.fetch(message.author.id); + } catch { + this.client.log.verbose('Failed to fetch member %s of %s', message.author.id, message.guild.id); + } + } + + const channels = message.mentions.channels; + const members = [...message.mentions.members]; + const roles = [...message.mentions.roles]; + + if (message.member) { + members.push(message.member); + roles.push(hoistedRole(message.member)); + } else { + this.client.log.warn('Message member does not exist'); + await this.client.prisma.archivedUser.upsert({ + create: {}, + update: {}, + where: { + ticketId_userId: { + ticketId, + userId: 'default', + }, + }, + }); + } + + for (const role of roles) { + const data = { + colour: role.hexColor.slice(1), + name: role.name, + roleId: role.id, + ticket: { connect: { id: ticketId } }, + }; + await this.client.prisma.archivedRole.upsert({ + create: data, + update: data, + where: { + ticketId_roleId: { + roleId: role.id, + ticketId, + }, + }, + }); + } + + for (const member of members) { + const data = { + avatar: member.avatar || member.user.avatar, // TODO: save avatar in user/avatars/ + bot: member.user.bot, + discriminator: member.user.discriminator, + displayName: member.displayName ? this.encrypt(member.displayName) : null, + roleId: !!member && hoistedRole(member).id, + ticketId, + userId: member.user.id, + username: this.encrypt(member.user.username), + }; + await this.client.prisma.archivedUser.upsert({ + create: data, + update: data, + where: { + ticketId_userId: { + ticketId, + userId: member.user.id, + }, + }, + }); + } + + const messageD = { + author: { + connect: { + ticketId_userId: { + ticketId, + userId: message.author?.id || 'default', + }, + }, + }, + content: cryptr.encrypt( + JSON.stringify({ + attachments: [...message.attachments.values()], + components: [...message.components.values()], + content: message.content, + embeds: message.embeds.map(embed => ({ embed })), + }), + ), + createdAt: message.createdAt, + edited: !!message.editedAt, + external, + id: message.id, + }; + + await this.client.prisma.ticket.update({ + data: { + archivedChannels: { + upsert: channels.map(channel => { + const data = { + channelId: channel.id, + name: channel.name, + }; + return { + create: data, + update: data, + where: { + ticketId_channelId: { + channelId: channel.id, + ticketId, + }, + }, + }; + }), + }, + archivedMessages: { + upsert: { + create: messageD, + update: messageD, + where: { id: message.id }, + }, + }, + }, + where: { id: ticketId }, + }); + + // await this.client.prisma.ticket.update({ + // data: { + // archivedChannels: { + // upsert: channels.map(channel => { + // const data = { + // channelId: channel.id, + // name: channel.name, + // }; + // return { + // create: data, + // update: data, + // where: { + // ticketId_channelId: { + // channelId: channel.id, + // ticketId, + // }, + // }, + // }; + // }), + // }, + // archivedRoles: { + // upsert: roles.map(role => { + // const data = { + // colour: role.hexColor.slice(1), + // name: role.name, + // roleId: role.id, + // }; + // return { + // create: data, + // update: data, + // where: { + // ticketId_roleId: { + // roleId: role.id, + // ticketId, + // }, + // }, + // }; + // }), + // }, + // archivedUsers: { + // upsert: members.map(member => { + // // message author might have left the server (this message could be external/referenced) + // const data = { + // avatar: member?.avatar || member.user?.avatar, // TODO: save avatar in user/avatars/ + // bot: member.user?.bot, + // discriminator: member.user?.discriminator, + // displayName: member?.displayName ? this.encrypt(member?.displayName) : null, + // // role: !!member && { + // // connectOrCreate: { + // // create: { + // // colour: hoistedRole(member).hexColor.slice(1), + // // name: hoistedRole(member).name, + // // roleId: hoistedRole(member).id, + // // ticket: { connect: { id: ticketId } }, + // // }, + // // where: { + // // roleId: hoistedRole(member).id, + // // ticketId, + // // }, + // // }, + // // }, + // roleId: !!member && hoistedRole(member).id, + // userId: member.user?.id || 'default', + // username: member.user?.username ? this.encrypt(member.user.username) : null, + // }; + // return { + // create: data, + // update: data, + // where: { + // ticketId_userId: { + // ticketId, + // userId: member.user.id, + // }, + // }, + // }; + // }), + // }, + // }, + // where: { id: ticketId }, + // }); + + // const messageD = { + // author: { connect: { id: message.author?.id || 'default' } }, + // content: cryptr.encrypt( + // JSON.stringify({ + // attachments: [...message.attachments.values()], + // components: [...message.components.values()], + // content: message.content, + // embeds: message.embeds.map(embed => ({ embed })), + // }), + // ), + // createdAt: message.createdAt, + // edited: !!message.editedAt, + // external, + // }; + + // return await this.client.prisma.archivedMessage.upsert({ + // create: messageD, + // update: messageD, + // where: { id: message.id }, + // }); + } }; \ No newline at end of file diff --git a/src/lib/tickets/manager.js b/src/lib/tickets/manager.js index 17a7618..40d76aa 100644 --- a/src/lib/tickets/manager.js +++ b/src/lib/tickets/manager.js @@ -474,36 +474,50 @@ module.exports = class TicketManager { .catch(() => this.client.log.warn('Failed to delete system pin message')); } - // TODO: referenced msg or ticket - + /** @type {import("discord.js").Message|undefined} */ + let message; if (referencesMessage) { referencesMessage = referencesMessage.split('/'); /** @type {import("discord.js").Message} */ - const message = await (await this.client.channels.fetch(referencesMessage[0]))?.messages.fetch(referencesMessage[1]); + message = await (await this.client.channels.fetch(referencesMessage[0]))?.messages.fetch(referencesMessage[1]); if (message) { - await channel.send({ - embeds: [ - new ExtendedEmbedBuilder() - .setColor(category.guild.primaryColour) - .setTitle(getMessage('ticket.references_message.title')) - .setDescription( - getMessage('ticket.references_message.description', { - author: message.author.toString(), - timestamp: ``, - url: message.url, - })), - new ExtendedEmbedBuilder({ - iconURL: guild.iconURL(), - text: category.guild.footer, - }) - .setColor(category.guild.primaryColour) - .setAuthor({ - iconURL: message.member?.displayAvatarURL(), - name: message.member?.displayName || 'Unknown', - }) - .setDescription(message.content.substring(0, 1000) + message.content.length > 1000 ? '...' : ''), - ], - }); + // not worth the effort of making system messages work atm + if (message.system) { + referencesMessage = null; + message = null; + } else { + if (!message.member) { + try { + message.member = await message.guild.members.fetch(message.author.id); + } catch { + this.client.log.verbose('Failed to fetch member %s of %s', message.author.id, message.guild.id); + } + await channel.send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setTitle(getMessage('ticket.references_message.title')) + .setDescription( + getMessage('ticket.references_message.description', { + author: message.author.toString(), + timestamp: ``, + url: message.url, + })), + new ExtendedEmbedBuilder({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.primaryColour) + .setAuthor({ + iconURL: message.member?.displayAvatarURL(), + name: message.member?.displayName || 'Unknown', + }) + .setDescription(message.content.substring(0, 1000) + (message.content.length > 1000 ? '...' : '')), + ], + }); + } + } + } } else if (referencesTicketId) { // TODO: add portal url @@ -554,9 +568,6 @@ module.exports = class TicketManager { topic: topic ? cryptr.encrypt(topic) : null, }; if (referencesTicketId) data.referencesTicket = { connect: { id: referencesTicketId } }; - let message; - if (referencesMessage) message = await this.client.prisma.archivedMessage.findUnique({ where: { id: referencesMessage[1] } }); - if (message) data.referencesMessage = { connect: { id: referencesMessage[0] } }; // only add if the message has been archived ^^ if (answers) data.questionAnswers = { createMany: { data: answers } }; await interaction.editReply({ components: [], @@ -583,6 +594,16 @@ module.exports = class TicketManager { await this.client.keyv.set(cacheKey, expiresAt, TTL); } + if (category.guild.archive && message) { + const row = await this.archiver.saveMessage(ticket.id, message, true); + if (row) { + await this.client.prisma.ticket.update({ + data: { referencesMessageId: row.id }, + where: { id: ticket.id }, + }); + } + } + logTicketEvent(this.client, { action: 'create', target: { diff --git a/src/listeners/client/messageCreate.js b/src/listeners/client/messageCreate.js index efa50f1..c90db50 100644 --- a/src/listeners/client/messageCreate.js +++ b/src/listeners/client/messageCreate.js @@ -177,11 +177,34 @@ module.exports = class extends Listener { }); } } else { - // TODO: archive messages in tickets - // TODO: first response - // TODO: lastMessageAt - // TODO: auto tag - // TODO: staff status alert, working hours alerts + let ticket = await client.prisma.ticket.findUnique({ + include: { guild: true }, + where: { id: message.channel.id }, + }); + + if (ticket) { + if (ticket.guild.archive) { + try { + await client.tickets.archiver.saveMessage(ticket.id, message); + } catch (error) { + client.log.warn('Failed to archive message', message.id); + client.log.error(error); + } + } + + if (ticket.firstResponseAt === null) { + ticket = await client.prisma.ticket.update({ + data: { firstResponseAt: new Date() }, + where: { id: ticket.id }, + }); + } + + // TODO: lastMessageAt + // TODO: staff status alert, working hours alerts + } else { + // TODO: auto tag + } + } } };