feat(server): prep
vi did:web:vt3e.cat
Fri, 08 May 2026 19:47:52 +0100
3 files changed,
331 insertions(+),
251 deletions(-)
M
src/automod/index.ts
→
src/automod/index.ts
@@ -1,6 +1,6 @@
import { rest } from "@/discord"; import { - CategoryChannel, + CategoryChannel, ChannelType, type Client, type Message,@@ -28,9 +28,7 @@ const pendingReports = new Map<Snowflake, Pending>();
const cooldowns = new Map<Snowflake, NodeJS.Timeout>(); async function getLogChannel(client: Client) { - return client.channels.fetch( - IS_PROD ? "1493015326554587146" : "1493015912234881205", - ); + return client.channels.fetch(IS_PROD ? "1493015326554587146" : "1493015912234881205"); } type AutomodResult = { delete: true } | null;@@ -38,27 +36,18 @@ type AutomodSignals = {
messageCount?: number; lastMessage?: Message; }; -type AutomodFunction = ( - client: Client, - message: Message, -) => Promise<AutomodResult>; +type AutomodFunction = (client: Client, message: Message) => Promise<AutomodResult>; -async function fetchSignals( - client: Client, - message: Message, -): Promise<AutomodSignals> { +async function fetchSignals(client: Client, message: Message): Promise<AutomodSignals> { const signals: AutomodSignals = { messageCount: undefined, }; - const messages = (await rest.get( - `/guilds/${message.guildId}/messages/search`, - { - body: { - author_id: [message.author.id], - } as RESTGetAPIGuildMessagesSearchQuery, - }, - )) as RESTGetAPIGuildMessagesSearchResult; + const messages = (await rest.get(`/guilds/${message.guildId}/messages/search`, { + body: { + author_id: [message.author.id], + } as RESTGetAPIGuildMessagesSearchQuery, + })) as RESTGetAPIGuildMessagesSearchResult; if ("total_results" in messages) { signals.messageCount = messages.total_results;@@ -66,9 +55,7 @@ const [lastApiMessage] = messages.messages[1];
const channel = await client.channels.fetch(lastApiMessage.channel_id); if (channel?.isTextBased()) { - const lastMessage = await channel.messages - .fetch(lastApiMessage.id) - .catch(() => null); + const lastMessage = await channel.messages.fetch(lastApiMessage.id).catch(() => null); if (lastMessage) { signals.lastMessage = lastMessage; }@@ -80,21 +67,15 @@ }
export async function handleMessage(client: Client, message: Message) { if (message.author.bot || !message.guildId || !message.member) return; - const automodFunctions: AutomodFunction[] = [ - handleSpamMarker, - handleCryptoScamOCR, - ]; + const automodFunctions: AutomodFunction[] = [handleSpamMarker, handleCryptoScamOCR]; - if (message.channel.type == ChannelType.GuildText && message.channel.name.includes("ticket")) return; - if (message.channel.isThread()) return null; - if (message.content.includes("!bypass")) return null; + if (message.channel.type == ChannelType.GuildText && message.channel.name.includes("ticket")) + return; + if (message.channel.isThread()) return null; + if (message.content.includes("!bypass")) return null; if ((await hasManagerPermissions(message.member)) && IS_PROD) return null; for (const func of automodFunctions) { - logger.info( - { messageId: message.id, authorId: message.author.id }, - "checking message with automod function", - ); const result = await func(client, message); if (result?.delete) {@@ -126,10 +107,7 @@ )
.catch(() => null); } - const timer = setTimeout( - () => flushReport(client, message.author.id), - 30_000, - ); + const timer = setTimeout(() => flushReport(client, message.author.id), 30_000); pendingReports.set(message.author.id, { channels: new Set([message.channelId]), reasons: new Set([reason]),@@ -140,17 +118,11 @@ existing.channels.add(message.channelId);
existing.reasons.add(reason); clearTimeout(existing.timer); - existing.timer = setTimeout( - () => flushReport(client, message.author.id), - 30_000, - ); + existing.timer = setTimeout(() => flushReport(client, message.author.id), 30_000); } } -async function handleCryptoScamOCR( - client: Client, - message: Message, -): Promise<AutomodResult> { +async function handleCryptoScamOCR(client: Client, message: Message): Promise<AutomodResult> { if (message.attachments.size === 0) return null; if (pendingReports.has(message.author.id) || cooldowns.has(message.author.id)) {@@ -163,7 +135,7 @@ await reportToAutomod(
client, message, "User repeatedly sending images after being flagged", - "You sent an image indicative of your account being compromised or a phishing attempt, so we removed it." + "You sent an image indicative of your account being compromised or a phishing attempt, so we removed it.", ); return { delete: true };@@ -198,14 +170,9 @@ .replace(/[^\w\s]/g, " ")
.replace(/\s+/g, " ") .trim(); - logger.debug( - { messageId: message.id, extractedText: normalizedText }, - "OCR completed", - ); + logger.debug({ messageId: message.id, extractedText: normalizedText }, "OCR completed"); - let isScam = scamPatterns.some((pattern) => - normalizedText.includes(pattern), - ); + let isScam = scamPatterns.some((pattern) => normalizedText.includes(pattern)); if (!isScam) { const words = normalizedText.split(" ");@@ -217,9 +184,7 @@
for (let i = 0; i <= words.length - windowSize; i++) { const window = words.slice(i, i + windowSize).join(" "); const similarity = - 1 - - distance(pattern, window) / - Math.max(pattern.length, window.length); + 1 - distance(pattern, window) / Math.max(pattern.length, window.length); if (similarity > 0.7) { logger.info(@@ -287,20 +252,14 @@
return { delete: true }; } } catch (err) { - logger.error( - { err, messageId: message.id }, - "failed to process image for ocr", - ); + logger.error({ err, messageId: message.id }, "failed to process image for ocr"); } } return null; } -async function handleSpamMarker( - client: Client, - message: Message, -): Promise<AutomodResult> { +async function handleSpamMarker(client: Client, message: Message): Promise<AutomodResult> { const { content } = message; /*@@ -311,19 +270,11 @@ if a messsage contains a link marker, then we delete it.
if a message contains a word marker, we first check if the last message is older than an hour, if it is, we delete the message. */ - const linkMarkers = [ - "discord.gg/", - "discordapp.com/invite/", - "discord.com/invite/", - ]; + const linkMarkers = ["discord.gg/", "discordapp.com/invite/", "discord.com/invite/"]; const wordMarkers = ["check my bio"]; - const hasLinkMarker = linkMarkers.some((marker) => - content.toLowerCase().includes(marker), - ); - const hasWordMarker = wordMarkers.some((marker) => - content.toLowerCase().includes(marker), - ); + const hasLinkMarker = linkMarkers.some((marker) => content.toLowerCase().includes(marker)); + const hasWordMarker = wordMarkers.some((marker) => content.toLowerCase().includes(marker)); if (!hasLinkMarker && !hasWordMarker) return null; const signals = await fetchSignals(client, message);@@ -373,9 +324,6 @@ logChannel.send(
`<@${authorId}> posted flagged messages in channels: ${channelMentions}\n> ${reasons}`, ); - const cooldownTimer = setTimeout( - () => cooldowns.delete(authorId), - 5 * 60 * 1000, - ); + const cooldownTimer = setTimeout(() => cooldowns.delete(authorId), 5 * 60 * 1000); cooldowns.set(authorId, cooldownTimer); }
M
src/server/discord.ts
→
src/server/discord.ts
@@ -1,17 +1,99 @@
-import { type RESTGetAPIUserResult, Routes } from "discord-api-types/rest/v10"; -import rest from "../rest"; +import { type APIGuildMember, type APIUser } from "discord-api-types/v10"; +import type { GuildMember, User } from "discord.js"; + import config from "@/config"; +import type { Ticket } from "@/database/schema"; +import client from "@/discord"; -const cache = new Map<string, RESTGetAPIUserResult>(); +export type UserProfile = APIGuildMember | { user: APIUser }; -export async function getUser(id: string) { - const cached = cache.get(id); - if (cached) return cached; +export const cache = new Map<string, UserProfile | null>(); - const res = (await rest.get( - Routes.guildMember(config.discord.guild, id), - )) as RESTGetAPIUserResult; - cache.set(id, res); +function mapUser(user: User): APIUser { + return { + id: user.id, + username: user.username, + discriminator: user.discriminator, + global_name: user.globalName, + avatar: user.avatar, + bot: user.bot, + system: user.system, + banner: user.banner, + accent_color: user.accentColor, + public_flags: user.flags?.bitfield, + } as APIUser; +} - return res; +function mapMember(member: GuildMember): APIGuildMember { + return { + user: mapUser(member.user), + nick: member.nickname, + avatar: member.avatar, + roles: Array.from(member.roles.cache.keys()), + joined_at: member.joinedAt?.toISOString() || new Date().toISOString(), + deaf: member.voice.serverDeaf || false, + mute: member.voice.serverMute || false, + flags: member.flags.bitfield, + pending: member.pending, + premium_since: member.premiumSince?.toISOString() || null, + communication_disabled_until: member.communicationDisabledUntil?.toISOString() || null, + } as APIGuildMember; +} + +export async function getUsers(ids: string[]): Promise<void> { + const uniqueIds = [...new Set(ids.filter(Boolean))]; + const missingIds = uniqueIds.filter((id) => !cache.has(id)); + + if (missingIds.length === 0) return; + + const guild = client.guilds.cache.get(config.discord.guild); + + if (guild) { + try { + const members = await guild.members.fetch({ user: missingIds }); + members.forEach((member) => cache.set(member.id, mapMember(member))); + } catch (err) { + console.error("failed to fetch guild members chunk:", err); + } + } + + const stillMissing = missingIds.filter((id) => !cache.has(id)); + + if (stillMissing.length > 0) { + await Promise.all( + stillMissing.map(async (id) => { + try { + const user = await client.users.fetch(id); + cache.set(id, { user: mapUser(user) }); + } catch { + cache.set(id, null); + } + }), + ); + } +} + +export async function getUser(id: string): Promise<UserProfile | null> { + await getUsers([id]); + return cache.get(id) || null; +} + +export async function hydrateTickets(allTickets: Ticket[]) { + const allIds = new Set<string>(); + for (const ticket of allTickets) { + if (ticket.authorId) allIds.add(ticket.authorId); + if (ticket.openedBy) allIds.add(ticket.openedBy); + if (ticket.claimedBy) allIds.add(ticket.claimedBy); + if (ticket.closedBy && ticket.closedBy !== "reporter") allIds.add(ticket.closedBy); + } + + await getUsers(Array.from(allIds)); + + return allTickets.map((ticket) => ({ + ...ticket, + authorId: ticket.authorId ? cache.get(ticket.authorId) : null, + openedBy: ticket.openedBy ? cache.get(ticket.openedBy) : null, + claimedBy: ticket.claimedBy ? cache.get(ticket.claimedBy) : null, + closedBy: ticket.closedBy && ticket.closedBy !== "reporter" ? cache.get(ticket.closedBy) : null, + })); }
M
src/server/index.ts
→
src/server/index.ts
@@ -1,207 +1,257 @@
-import config from "@/config"; -import _logger from "@/utils/logging"; -import { db } from "@/database"; -import { tickets, ticketMessages, moderators, sessions, type Ticket } from "@/database/schema"; import { desc, eq, asc } from "drizzle-orm"; import type { BunRequest } from "bun"; -import { getUser } from "./discord"; + +import _logger from "@/utils/logging"; +import config from "@/config"; +import { db, tickets, ticketMessages, moderators, sessions } from "@/database"; +import { getUser, getUsers, hydrateTickets } from "./discord"; const logger = _logger.child({ name: "server" }); -const CLIENT_ID = config.discord.app_id -const CLIENT_SECRET = config.discord.client_secret -const REDIRECT_URI = 'http://127.0.0.1:3000/auth/callback'; +const CLIENT_ID = config.discord.app_id; +const CLIENT_SECRET = config.discord.client_secret; +const REDIRECT_URI = "http://127.0.0.1:3000/auth/callback"; -const FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:5173'; - -type AnonymisedTicket = Omit<Ticket, "authorId" | "anonymousId"> -function anonymiseTicket(ticket: Ticket): AnonymisedTicket { - const _ticket = ticket as Partial<Ticket> - delete _ticket.authorId; - return _ticket as AnonymisedTicket -} +const FRONTEND_URL = process.env.FRONTEND_URL || "http://127.0.0.1:5173"; async function getSessionUser(req: BunRequest) { - const cookieHeader = req.headers.get("Cookie"); - if (!cookieHeader) return null; - const sessionCookie = cookieHeader.split("; ").find(c => c.startsWith("session=")); - if (!sessionCookie) return null; - const sessionId = sessionCookie.split("=")[1]; + const cookieHeader = req.headers.get("Cookie"); + if (!cookieHeader) return null; + const sessionCookie = cookieHeader.split("; ").find((c) => c.startsWith("session=")); + if (!sessionCookie) return null; + const sessionId = sessionCookie.split("=")[1]; - const sessionList = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1); - if (sessionList.length === 0) return null; + const sessionList = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1); + if (sessionList.length === 0) return null; - return { id: sessionList[0].userId, username: sessionList[0].username }; + return { id: sessionList[0].userId, username: sessionList[0].username }; } const server = Bun.serve({ - hostname: "127.0.0.1", + hostname: "127.0.0.1", - routes: { - "/login": () => { - const redirectUri = "https://discord.com/oauth2/authorize?client_id=1350116449611677770&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fauth%2Fcallback&scope=identify+guilds" + routes: { + "/login": () => { + const redirectUri = + "https://discord.com/oauth2/authorize?client_id=1350116449611677770&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fauth%2Fcallback&scope=identify+guilds"; - return new Response(null, { - status: 302, - headers: { - Location: redirectUri, - }, - }); - }, - "/auth/callback": { - GET: async (req) => { - const url = new URL(req.url); - const code = url.searchParams.get("code"); - if (!code) return new Response("No code provided", { status: 400, statusText: "No code provided" }) + return new Response(null, { + status: 302, + headers: { + Location: redirectUri, + }, + }); + }, + "/auth/callback": { + GET: async (req) => { + const url = new URL(req.url); + const code = url.searchParams.get("code"); + if (!code) + return new Response("No code provided", { status: 400, statusText: "No code provided" }); - const params = new URLSearchParams(); - params.append("client_id", CLIENT_ID); - params.append("client_secret", CLIENT_SECRET); - params.append("grant_type", "authorization_code"); - params.append("code", code); - params.append("redirect_uri", REDIRECT_URI); + const params = new URLSearchParams(); + params.append("client_id", CLIENT_ID); + params.append("client_secret", CLIENT_SECRET); + params.append("grant_type", "authorization_code"); + params.append("code", code); + params.append("redirect_uri", REDIRECT_URI); - const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { - method: "POST", - body: params, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); + const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { + method: "POST", + body: params, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); - const data = await tokenResponse.json(); - if (data.error) return new Response(data.error_description, { status: 400, statusText: data.error }) + const data = await tokenResponse.json(); + if (data.error) + return new Response(data.error_description, { status: 400, statusText: data.error }); - const accessToken = data.access_token; - const userResponse = await fetch("https://discord.com/api/users/@me", { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); + const accessToken = data.access_token; + const userResponse = await fetch("https://discord.com/api/users/@me", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); - const userData = await userResponse.json(); + const userData = await userResponse.json(); - const sessionId = crypto.randomUUID(); - await db.insert(sessions).values({ - id: sessionId, - userId: userData.id, - username: userData.username - }); + const sessionId = crypto.randomUUID(); + await db.insert(sessions).values({ + id: sessionId, + userId: userData.id, + username: userData.username, + }); - return new Response(null, { - status: 302, - headers: { - Location: `${FRONTEND_URL}/tickets`, - "Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; SameSite=Lax`, - }, - }); - } - }, - "/me": { - GET: async (req: BunRequest) => { - const user = await getSessionUser(req); - if (!user) return new Response("Unauthorized", { status: 401 }); + return new Response(null, { + status: 302, + headers: { + Location: `${FRONTEND_URL}/tickets`, + "Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; SameSite=Lax`, + }, + }); + }, + }, + "/me": { + GET: async (req: BunRequest) => { + const user = await getSessionUser(req); + if (!user) return new Response("Unauthorized", { status: 401 }); - const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); - const isMod = modCheck.length > 0; + const modCheck = await db + .select() + .from(moderators) + .where(eq(moderators.user_id, user.id)) + .limit(1); + const isMod = modCheck.length > 0; - const profile = await getUser(user.id) + const profile = await getUser(user.id); - return new Response(JSON.stringify({ profile, isMod }), { - headers: { "Content-Type": "application/json" } - }); - } - }, - "/tickets": { - GET: async (req: BunRequest) => { - const user = await getSessionUser(req); - if (!user) return new Response(null, { status: 302, headers: { Location: "/login" } }); + return new Response(JSON.stringify({ profile, isMod }), { + headers: { "Content-Type": "application/json" }, + }); + }, + }, + "/tickets": { + GET: async (req: BunRequest) => { + const user = await getSessionUser(req); + if (!user) return new Response(null, { status: 302, headers: { Location: "/login" } }); + + const modCheck = await db + .select() + .from(moderators) + .where(eq(moderators.user_id, user.id)) + .limit(1); + if (modCheck.length === 0) { + return new Response("Forbidden: You must be a moderator to view tickets.", { + status: 403, + }); + } + + const url = new URL(req.url); + const limitStr = url.searchParams.get("limit") || "20"; + const limit = parseInt(limitStr, 10); + + const allTickets = await db + .select() + .from(tickets) + .limit(limit) + .orderBy(desc(tickets.createdAt)); + + const hydratedTickets = await hydrateTickets(allTickets); - const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); - if (modCheck.length === 0) { - return new Response("Forbidden: You must be a moderator to view tickets.", { status: 403 }); - } + return new Response(JSON.stringify(hydratedTickets), { + headers: { "Content-Type": "application/json" }, + }); + }, + }, + "/tickets/:id": { + GET: async (req: BunRequest) => { + const user = await getSessionUser(req); + if (!user) return new Response(null, { status: 302, headers: { Location: "/login" } }); - const url = new URL(req.url); - const limitStr = url.searchParams.get("limit") || "50"; - const limit = parseInt(limitStr, 10); + const modCheck = await db + .select() + .from(moderators) + .where(eq(moderators.user_id, user.id)) + .limit(1); + if (modCheck.length === 0) + return new Response("Forbidden: You must be a moderator to view tickets.", { + status: 403, + }); - const allTickets = await db.select().from(tickets).limit(limit).orderBy(desc(tickets.createdAt)); - const anonymisedTickets = allTickets.map(anonymiseTicket); + const { id } = req.params; + const ticketId = parseInt(id, 10); + if (isNaN(ticketId)) return new Response("Invalid ID", { status: 400 }); - return new Response(JSON.stringify(anonymisedTickets), { headers: { "Content-Type": "application/json" } }); - } - }, - "/tickets/:id": { - GET: async (req: BunRequest) => { - const user = await getSessionUser(req); - if (!user) return new Response(null, { status: 302, headers: { Location: "/login" } }); + const ticketList = await db.select().from(tickets).where(eq(tickets.id, ticketId)).limit(1); + const _ticket = ticketList[0]; + if (!_ticket) return new Response("Ticket not found", { status: 404 }); - const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); - if (modCheck.length === 0) return new Response("Forbidden: You must be a moderator to view tickets.", { status: 403 }); + const messages = await db + .select() + .from(ticketMessages) + .where(eq(ticketMessages.ticketId, ticketId)) + .orderBy(asc(ticketMessages.createdAt)); - const { id } = req.params; - const ticketId = parseInt(id, 10); - if (isNaN(ticketId)) return new Response("Invalid ID", { status: 400 }); + const idsToFetch = [ + _ticket.authorId, + _ticket.openedBy, + _ticket.claimedBy, + ...messages.map((m) => m.authorId), + ].filter(Boolean) as string[]; - const ticketList = await db.select().from(tickets).where(eq(tickets.id, ticketId)).limit(1); - const _ticket = ticketList[0]; - if (!_ticket) return new Response("Ticket not found", { status: 404 }); - const ticket = anonymiseTicket(_ticket); + await getUsers(idsToFetch); - const messages = await db.select().from(ticketMessages).where(eq(ticketMessages.ticketId, ticketId)).orderBy(asc(ticketMessages.createdAt)); + const [ticket] = await hydrateTickets([_ticket]); - const authors = [...new Set(messages.map(m => m.authorId))]; - const authorData = await Promise.all(authors.map(async (authorId) => { - try { - return await getUser(authorId); - } catch { - return { id: authorId, username: "Unknown" }; - } - })); + const newMessages = await Promise.all( + messages.map(async (message) => { + const author = await getUser(message.authorId); + return { ...message, author }; + }), + ); - const newMessages = messages.map((message) => { - const author = authorData.find((a) => a.id === message.authorId); - return { ...message, author }; - }); + return new Response(JSON.stringify({ ticket, messages: newMessages }), { + headers: { "Content-Type": "application/json" }, + }); + }, + DELETE: async (req: BunRequest) => { + const user = await getSessionUser(req); + if (!user) return new Response("Unauthorized", { status: 401 }); - return new Response(JSON.stringify({ ticket, messages: newMessages }), { headers: { "Content-Type": "application/json" } }); - }, - DELETE: async (req: BunRequest) => { - const user = await getSessionUser(req); - if (!user) return new Response("Unauthorized", { status: 401 }); + const modCheck = await db + .select() + .from(moderators) + .where(eq(moderators.user_id, user.id)) + .limit(1); + if (modCheck.length === 0) + return new Response("Forbidden: You must be a moderator to delete tickets.", { + status: 403, + }); - const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); - if (modCheck.length === 0) return new Response("Forbidden: You must be a moderator to delete tickets.", { status: 403 }); + const { id } = req.params; + const ticketId = parseInt(id, 10); + if (isNaN(ticketId)) return new Response("Invalid ID", { status: 400 }); - const { id } = req.params; - const ticketId = parseInt(id, 10); - if (isNaN(ticketId)) return new Response("Invalid ID", { status: 400 }); + const deleted = await db.delete(tickets).where(eq(tickets.id, ticketId)).returning(); + if (deleted.length === 0) return new Response("Ticket not found", { status: 404 }); - const deleted = await db.delete(tickets).where(eq(tickets.id, ticketId)).returning(); - if (deleted.length === 0) return new Response("Ticket not found", { status: 404 }); + return new Response(JSON.stringify({ success: true }), { + headers: { "Content-Type": "application/json" }, + }); + }, + }, + "/moderators": { + GET: async (req: BunRequest) => { + const user = await getSessionUser(req); + if (!user) return new Response("Unauthorized", { status: 401 }); - return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } }); - } - }, - "/moderators": { - GET: async (req: BunRequest) => { - const user = await getSessionUser(req); - if (!user) return new Response("Unauthorized", { status: 401 }); + const modCheck = await db + .select() + .from(moderators) + .where(eq(moderators.user_id, user.id)) + .limit(1); + if (modCheck.length === 0) return new Response("Forbidden", { status: 403 }); - const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); - if (modCheck.length === 0) return new Response("Forbidden", { status: 403 }); + const mods = await db.select().from(moderators); + const ids = mods.map((mod) => mod.user_id); - const mods = await db.select().from(moderators); - const ids = mods.map(mod => mod.user_id); + await getUsers(ids); - const moderatorUsers = await Promise.all(ids.map(id => getUser(id))); - const hydrated = mods.map((mod, index) => ({ ...moderatorUsers[index], pronouns: mod })); + const hydrated = await Promise.all( + mods.map(async (mod) => { + const profile = await getUser(mod.user_id); + return { ...profile, pronouns: mod }; + }), + ); - return new Response(JSON.stringify(hydrated), { headers: { "Content-Type": "application/json" } }); - } - } - }, + return new Response(JSON.stringify(hydrated), { + headers: { "Content-Type": "application/json" }, + }); + }, + }, + }, }); const PORT = process.env.PORT || 3000;