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 {
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")
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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 });
}
};

View File

@ -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 },
// });
}
};

View File

@ -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: `<t:${Math.ceil(message.createdTimestamp / 1000)}:R>`,
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: `<t:${Math.ceil(message.createdTimestamp / 1000)}:R>`,
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: {

View File

@ -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
}
}
}
};