Progress on message archiving

This commit is contained in:
Isaac 2022-09-30 17:09:35 +01:00
parent 9682fcf22b
commit 460138fb73
No known key found for this signature in database
GPG Key ID: 0DE40AE37BBA5C33
7 changed files with 416 additions and 129 deletions

View File

@ -22,7 +22,7 @@ model ArchivedChannel {
model ArchivedMessage { model ArchivedMessage {
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
authorId String @db.VarChar(19) authorId String @db.VarChar(19)
content Json content String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
deleted Boolean @default(false) deleted Boolean @default(false)
edited Boolean @default(false) edited Boolean @default(false)
@ -50,17 +50,17 @@ model ArchivedRole {
model ArchivedUser { model ArchivedUser {
archivedMessages ArchivedMessage[] archivedMessages ArchivedMessage[]
avatar String avatar String?
bot Boolean @default(false) bot Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
discriminator String @db.Char(4) discriminator String? @db.Char(4)
displayName String @db.Text displayName String? @db.Text
role ArchivedRole @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade)
roleId String @db.VarChar(19) roleId String? @db.VarChar(19)
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketId String @db.VarChar(19) ticketId String @db.VarChar(19)
userId String @db.VarChar(19) userId String @db.VarChar(19)
username String @db.Text username String? @db.Text
@@id([ticketId, userId]) @@id([ticketId, userId])
@@unique([ticketId, userId]) @@unique([ticketId, userId])

View File

@ -22,7 +22,7 @@ model ArchivedChannel {
model ArchivedMessage { model ArchivedMessage {
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
authorId String @db.VarChar(19) authorId String @db.VarChar(19)
content Json content String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
deleted Boolean @default(false) deleted Boolean @default(false)
edited Boolean @default(false) edited Boolean @default(false)
@ -50,17 +50,17 @@ model ArchivedRole {
model ArchivedUser { model ArchivedUser {
archivedMessages ArchivedMessage[] archivedMessages ArchivedMessage[]
avatar String avatar String?
bot Boolean @default(false) bot Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
discriminator String @db.Char(4) discriminator String? @db.Char(4)
displayName String @db.Text displayName String? @db.Text
role ArchivedRole @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade)
roleId String @db.VarChar(19) roleId String? @db.VarChar(19)
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketId String @db.VarChar(19) ticketId String @db.VarChar(19)
userId String @db.VarChar(19) userId String @db.VarChar(19)
username String @db.Text username String? @db.Text
@@id([ticketId, userId]) @@id([ticketId, userId])
@@unique([ticketId, userId]) @@unique([ticketId, userId])

View File

@ -50,17 +50,17 @@ model ArchivedRole {
model ArchivedUser { model ArchivedUser {
archivedMessages ArchivedMessage[] archivedMessages ArchivedMessage[]
avatar String avatar String?
bot Boolean @default(false) bot Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
discriminator String discriminator String?
displayName String displayName String?
role ArchivedRole @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade)
roleId String roleId String?
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketId String ticketId String
userId String userId String
username String username String?
@@id([ticketId, userId]) @@id([ticketId, userId])
@@unique([ticketId, userId]) @@unique([ticketId, userId])

View File

@ -18,7 +18,6 @@ module.exports = class CreateMessageCommand extends MessageCommand {
* @param {import("discord.js").MessageContextMenuCommandInteraction} interaction * @param {import("discord.js").MessageContextMenuCommandInteraction} interaction
*/ */
async run(interaction) { async run(interaction) {
// TODO: archive message
await useGuild(this.client, interaction, { referencesMessage: interaction.targetMessage.channelId + '/' + interaction.targetId }); await useGuild(this.client, interaction, { referencesMessage: interaction.targetMessage.channelId + '/' + interaction.targetId });
} }
}; };

View File

@ -1,11 +1,255 @@
const Cryptr = require('cryptr'); 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 { module.exports = class TicketArchiver {
constructor(client) { constructor(client) {
/** @type {import("client")} */ /** @type {import("client")} */
this.client = 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 },
// });
}
}; };

View File

@ -474,13 +474,24 @@ module.exports = class TicketManager {
.catch(() => this.client.log.warn('Failed to delete system pin message')); .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) { if (referencesMessage) {
referencesMessage = referencesMessage.split('/'); referencesMessage = referencesMessage.split('/');
/** @type {import("discord.js").Message} */ /** @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) { if (message) {
// 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({ await channel.send({
embeds: [ embeds: [
new ExtendedEmbedBuilder() new ExtendedEmbedBuilder()
@ -501,10 +512,13 @@ module.exports = class TicketManager {
iconURL: message.member?.displayAvatarURL(), iconURL: message.member?.displayAvatarURL(),
name: message.member?.displayName || 'Unknown', name: message.member?.displayName || 'Unknown',
}) })
.setDescription(message.content.substring(0, 1000) + message.content.length > 1000 ? '...' : ''), .setDescription(message.content.substring(0, 1000) + (message.content.length > 1000 ? '...' : '')),
], ],
}); });
} }
}
}
} else if (referencesTicketId) { } else if (referencesTicketId) {
// TODO: add portal url // TODO: add portal url
const ticket = await this.client.prisma.ticket.findUnique({ where: { id: referencesTicketId } }); const ticket = await this.client.prisma.ticket.findUnique({ where: { id: referencesTicketId } });
@ -554,9 +568,6 @@ module.exports = class TicketManager {
topic: topic ? cryptr.encrypt(topic) : null, topic: topic ? cryptr.encrypt(topic) : null,
}; };
if (referencesTicketId) data.referencesTicket = { connect: { id: referencesTicketId } }; 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 } }; if (answers) data.questionAnswers = { createMany: { data: answers } };
await interaction.editReply({ await interaction.editReply({
components: [], components: [],
@ -583,6 +594,16 @@ module.exports = class TicketManager {
await this.client.keyv.set(cacheKey, expiresAt, TTL); 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, { logTicketEvent(this.client, {
action: 'create', action: 'create',
target: { target: {

View File

@ -177,11 +177,34 @@ module.exports = class extends Listener {
}); });
} }
} else { } else {
// TODO: archive messages in tickets let ticket = await client.prisma.ticket.findUnique({
// TODO: first response 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: lastMessageAt
// TODO: auto tag
// TODO: staff status alert, working hours alerts // TODO: staff status alert, working hours alerts
} else {
// TODO: auto tag
}
} }
} }
}; };