all repos — stealth-developers @ 8bee8ce3815083afdc6fb78059396adc11ac73a8

feat(server): attach attachments to messages
vi did:web:vt3e.cat
Sat, 09 May 2026 01:18:40 +0100
commit

8bee8ce3815083afdc6fb78059396adc11ac73a8

parent

faa90c2a0c29f0a32a860279b4325fba5d01f646

2 files changed, 57 insertions(+), 7 deletions(-)

jump to
M src/server/discord.tssrc/server/discord.ts

@@ -5,6 +5,9 @@ import config from "@/config";

import type { Ticket } from "@/database/schema"; import client from "@/discord"; +import _logger from "@/utils/logging"; +const logger = _logger.child({ name: "server" }); + export type UserProfile = APIGuildMember | { user: APIUser }; export const cache = new Map<string, UserProfile | null>();

@@ -42,10 +45,11 @@ }

export async function getUsers(ids: string[]): Promise<void> { const uniqueIds = [...new Set(ids.filter(Boolean))]; - const missingIds = uniqueIds.filter((id) => !cache.has(id)); + const missingIds = uniqueIds.filter((id) => id !== "system" && !cache.has(id)); if (missingIds.length === 0) return; + logger.debug(missingIds, `fetching ${missingIds.length} users`); const guild = client.guilds.cache.get(config.discord.guild); if (guild) {
M src/server/index.tssrc/server/index.ts

@@ -1,9 +1,9 @@

-import { desc, eq, asc, and, isNotNull, ne, gte } from "drizzle-orm"; +import { desc, eq, asc, and, isNotNull, ne, gte, inArray, or } from "drizzle-orm"; import type { BunRequest } from "bun"; import _logger from "@/utils/logging"; import config from "@/config"; -import { db, tickets, ticketMessages, moderators, sessions } from "@/database"; +import { db, tickets, ticketMessages, moderators, sessions, attachments } from "@/database"; import { getUser, getUsers, hydrateTickets } from "./discord"; const logger = _logger.child({ name: "server" });

@@ -174,27 +174,73 @@ .from(ticketMessages)

.where(eq(ticketMessages.ticketId, ticketId)) .orderBy(asc(ticketMessages.createdAt)); + const mentionRegex = /<@!?(\d+)>/g; + const mentionedIds = messages.flatMap((m) => + [...m.content.matchAll(mentionRegex)].map((match) => match[1]), + ); + const idsToFetch = [ _ticket.authorId, _ticket.openedBy, _ticket.claimedBy, ...messages.map((m) => m.authorId), + ...mentionedIds, ].filter(Boolean) as string[]; await getUsers(idsToFetch); const [ticket] = await hydrateTickets([_ticket]); + const messageIds = messages.map((m) => m.id); + + const ticketAttachments = await db + .select() + .from(attachments) + .where( + or( + and(eq(attachments.ownerType, "ticket"), eq(attachments.ownerId, ticketId)), + messageIds.length > 0 + ? and( + eq(attachments.ownerType, "ticket_message"), + inArray(attachments.ownerId, messageIds), + ) + : undefined, + ), + ); + const newMessages = await Promise.all( messages.map(async (message) => { const author = await getUser(message.authorId); - return { ...message, author }; + const msgAttachments = ticketAttachments.filter( + (a) => a.ownerType === "ticket_message" && a.ownerId === message.id, + ); + + const mentions: Record<string, any> = {}; + const matches = [...message.content.matchAll(/<@!?(\d+)>/g)]; + for (const match of matches) { + const id = match[1]; + if (!mentions[id]) { + mentions[id] = await getUser(id); + } + } + + return { ...message, author, attachments: msgAttachments, mentions }; }), ); - return new Response(JSON.stringify({ ticket, messages: newMessages }), { - headers: { "Content-Type": "application/json" }, - }); + const mainTicketAttachments = ticketAttachments.filter( + (a) => a.ownerType === "ticket" && a.ownerId === ticketId, + ); + + return new Response( + JSON.stringify({ + ticket: { ...ticket, attachments: mainTicketAttachments }, + messages: newMessages, + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); }, DELETE: async (req: BunRequest) => { const user = await getSessionUser(req);