feat(tickets): ingest message edits & deletions
vi v@vt3e.cat
Tue, 07 Apr 2026 00:34:51 +0100
5 files changed,
95 insertions(+),
0 deletions(-)
M
src/database/schema/tickets.ts
→
src/database/schema/tickets.ts
@@ -46,6 +46,7 @@ ticketId: integer("ticket_id")
.references(() => tickets.id, { onDelete: "cascade" }) .notNull(), + messageId: text("message_id"), authorId: text("author_id").notNull(), authorType: text("author_type", { enum: ["user", "staff", "system"] })@@ -57,6 +58,10 @@
createdAt: integer("created_at", { mode: "timestamp" }) .default(sql`(unixepoch())`) .notNull(), + editedAt: integer("edited_at", { mode: "timestamp" }).$type<Date | null>(), + deletedAt: integer("deleted_at", { + mode: "timestamp", + }).$type<Date | null>(), }, (t) => [index("ticket_msgs_ticket_idx").on(t.ticketId)], );
M
src/events/messageCreate.ts
→
src/events/messageCreate.ts
@@ -213,6 +213,7 @@ .insert(ticketMessages)
.values({ ticketId: ticket.id, authorId: message.author.id, + messageId: message.id, authorType: authorType, content: anonymisedContent, createdAt: message.createdAt,
A
src/events/messageDelete.ts
@@ -0,0 +1,35 @@
+import { db, ticketMessages, tickets } from "@/database"; +import { loggers } from "@/utils/logging"; +import { type Client, Events, type Message } from "discord.js"; +import { and, eq } from "drizzle-orm"; + +const logger = loggers.events.child({ name: "messageDelete" }); + +export default { + event: Events.MessageDelete, + async execute(client: Client, message: Message) { + if (!client.user || message.author?.bot) return; + + const ticket = db + .select() + .from(tickets) + .where( + and( + eq(tickets.channelId, message.channelId), + eq(tickets.guildId, message.guildId ?? ""), + ), + ) + .get(); + + if (!ticket) return; + + try { + await db + .update(ticketMessages) + .set({ content: "(message deleted)" }) + .where(eq(ticketMessages.messageId, message.id)); + } catch (error) { + logger.error(error, "Failed to handle ticket message deletion"); + } + }, +};
A
src/events/messageUpdate.ts
@@ -0,0 +1,46 @@
+import { db, ticketMessages, tickets } from "@/database"; +import { loggers } from "@/utils/logging"; +import { type Client, Events, type Message } from "discord.js"; +import { and, eq } from "drizzle-orm"; + +const logger = loggers.events.child({ name: "messageUpdate" }); + +export default { + event: Events.MessageUpdate, + async execute(client: Client, oldMessage: Message, newMessage: Message) { + if (!client.user || newMessage.author?.bot) return; + if (oldMessage.content === newMessage.content) return; + + const ticket = db + .select() + .from(tickets) + .where( + and( + eq(tickets.channelId, newMessage.channelId), + eq(tickets.guildId, newMessage.guildId ?? ""), + ), + ) + .get(); + + if (!ticket) return; + + try { + const anonymisedContent = newMessage.content.replace( + new RegExp(ticket.authorId, "g"), + "-".repeat(ticket.authorId.length), + ); + + logger.debug( + `Anonymised content for message ${newMessage.id}: ${anonymisedContent}`, + ); + + await db + .update(ticketMessages) + .set({ content: anonymisedContent }) + .where(eq(ticketMessages.messageId, newMessage.id)) + .returning(); + } catch (error) { + logger.error(error, "Failed to update ticket message"); + } + }, +};
M
src/utils/logging.ts
→
src/utils/logging.ts
@@ -1,9 +1,17 @@
import { pino } from "pino"; +const logLevel = process.env.LOG_LEVEL + ? process.env.LOG_LEVEL + : process.env.NODE_ENV === "production" + ? "info" + : "debug"; + const logger = pino({ transport: { target: "pino-pretty", }, + + level: logLevel.toLowerCase(), }); export const loggers = {