feat(api): use new types !
vi did:web:vt3e.cat
Sat, 16 May 2026 04:58:46 +0100
7 files changed,
257 insertions(+),
370 deletions(-)
M
pkgs/bot/package.json
→
pkgs/bot/package.json
@@ -1,5 +1,5 @@
{ - "name": "stealth-developers", + "name": "@stealth-developers/bot", "license": "AGPL-3.0-only", "author": { "name": "april",
M
pkgs/bot/src/discord.ts
→
pkgs/bot/src/discord.ts
@@ -19,11 +19,11 @@
client.on(Events.ClientReady, async (client) => { logger.info(`logged in as ${client.user?.tag}!`); - await Promise.all([registerEvents(client), registerInteractions(client)]); - logger.info("events and interactions registered!"); - console.log(); + // await Promise.all([registerEvents(client), registerInteractions(client)]); + // logger.info("events and interactions registered!"); + // console.log(); - startTicketWatcher(client); + // startTicketWatcher(client); }); export default client;
M
pkgs/bot/src/roblox/robloxUser.ts
→
pkgs/bot/src/roblox/robloxUser.ts
@@ -1,6 +1,7 @@
import { isApiErrorV2, roblox } from "@/roblox/client"; import { constructUserContainer, getBans } from "@/roblox/profile"; import type { RobloxUserId } from "@/roblox/types"; +import logger from "@/utils/logging"; import type { ChatInputCommandInteraction, MessageContextMenuCommandInteraction,
M
pkgs/bot/src/roblox/types/users.ts
→
pkgs/bot/src/roblox/types/users.ts
@@ -55,7 +55,8 @@ // -- cloud/v2/users/{user_id}:generateThumbnail -------------------------------
export interface GenerateThumbnailResponse { done: true; response: { - imageUri: string; + path: string; + imageUri?: string; "@type": "apis.roblox.com/roblox.open_cloud.cloud.v2.GenerateUserThumbnailResponse"; }; }
A
pkgs/bot/src/server/api.ts
@@ -0,0 +1,48 @@
+import { CDNRoutes, ImageFormat, RouteBases } from "discord-api-types/v10"; +import type { User as DiscordUser, GuildMember } from "discord.js"; +import type { Attachment } from "@/database"; +import { getUser } from "./discord"; +import type { + User as UserProfile, + Attachment as ApiMessageAttachment, +} from "@stealth-developers/api"; + +export function avatar(id: string, avatarHash?: string | null): string { + if (!avatarHash) { + return "https://cdn.discordapp.com/icons/895998762630127616/36c6e14d0933eb338826599b632df818.webp?size=80&quality=lossless"; + } + return `${RouteBases.cdn}${CDNRoutes.userAvatar(id, avatarHash, ImageFormat.WebP)}`; +} + +export function name(member: GuildMember): string { + return member.nickname ?? member.user.displayName ?? member.user.username; +} + +export function marshallIntoApiUser(user: GuildMember | DiscordUser): UserProfile { + const isMember = "roles" in user; + const discordUser = isMember ? user.user : user; + + return { + id: discordUser.id, + name: isMember ? name(user) : discordUser.displayName || discordUser.username, + avatarUrl: avatar(discordUser.id, discordUser.avatar), + isModerator: isMember ? user.roles.cache.has("Moderator") : false, + }; +} + +export async function intoApiAttachment(a: Attachment): Promise<ApiMessageAttachment> { + const user = a.authorId ? await getUser(a.authorId) : null; + + return { + id: a.id, + ownerType: a.ownerType as "ticket" | "ticket_message" | "bug", + owner: user, + fileName: a.fileName, + contentType: a.contentType, + size: a.size, + bucket: a.bucket, + key: a.key, + url: a.url, + uploadedAt: new Date(a.uploadedAt), + }; +}
M
pkgs/bot/src/server/discord.ts
→
pkgs/bot/src/server/discord.ts
@@ -1,54 +1,17 @@
-import type { APIGuildMember, 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"; - +import { marshallIntoApiUser } from "./api"; import _logger from "@/utils/logging"; -const logger = _logger.child({ name: "server" }); +import type { User as UserProfile, Ticket as ApiTicket } from "@stealth-developers/api"; -export type UserProfile = APIGuildMember | { user: APIUser }; +const logger = _logger.child({ name: "server" }); export const cache = new Map<string, UserProfile | null>(); -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; -} - -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) => id !== "system" && !cache.has(id), - ); +export async function getUsers(ids: (string | null | undefined)[]): Promise<void> { + const uniqueIds = [...new Set(ids.filter(Boolean) as string[])]; + const missingIds = uniqueIds.filter((id) => id !== "system" && !cache.has(id)); if (missingIds.length === 0) return;@@ -58,9 +21,9 @@
if (guild) { try { const members = await guild.members.fetch({ user: missingIds }); - members.forEach((member) => cache.set(member.id, mapMember(member))); + members.forEach((member) => cache.set(member.id, marshallIntoApiUser(member))); } catch (err) { - console.error("failed to fetch guild members chunk:", err); + logger.error({ err }, "failed to fetch guild members chunk"); } }@@ -71,7 +34,7 @@ await Promise.all(
stillMissing.map(async (id) => { try { const user = await client.users.fetch(id); - cache.set(id, { user: mapUser(user) }); + cache.set(id, marshallIntoApiUser(user)); } catch { cache.set(id, null); }@@ -85,26 +48,37 @@ await getUsers([id]);
return cache.get(id) || null; } -export async function hydrateTickets(allTickets: Ticket[]) { +export async function hydrateTickets(allTickets: Ticket[]): Promise<ApiTicket[]> { 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); + if (ticket.closedBy && ticket.closedBy !== "reporter") allIds.add(ticket.closedBy); } - await getUsers(Array.from(allIds)); + await getUsers([...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, - })); + return allTickets.map((ticket) => { + const fallbackSubject: UserProfile = { + id: ticket.authorId, + name: "Unknown User", + avatarUrl: "", + isModerator: false, + }; + + return { + number: String(ticket.id), + status: ticket.status as "open" | "closed" | "archived", + + closedBy: ticket.closedBy ? (cache.get(ticket.closedBy) ?? null) : null, + closedAt: ticket.closedAt ? new Date(ticket.closedAt) : null, + + topic: ticket.topic, + closeReason: ticket.closeReason, + privateReason: ticket.privateReason, + + openedBy: ticket.openedBy ? (cache.get(ticket.openedBy) ?? null) : null, + subject: cache.get(ticket.authorId) ?? fallbackSubject, + }; + }); }
M
pkgs/bot/src/server/index.ts
→
pkgs/bot/src/server/index.ts
@@ -1,27 +1,23 @@
-import { - desc, - eq, - asc, - and, - isNotNull, - ne, - gte, - inArray, - or, -} 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, { isProd } from "@/config"; -import { - db, - tickets, - ticketMessages, - moderators, - sessions, - attachments, -} from "@/database"; +import { db, tickets, ticketMessages, moderators, sessions, attachments } from "@/database"; import { getUser, getUsers, hydrateTickets } from "./discord"; +import { intoApiAttachment } from "./api"; + +import type { + TicketResponse, + TicketsResponse, + MeResponse, + ModeratorsResponse, + OverallStatsResponse, + LeaderboardResponse, + Message, + User, + Statistic, +} from "@stealth-developers/api"; const logger = _logger.child({ name: "server" });@@ -32,84 +28,83 @@ const HOST = isProd ? "https://tickets.vt3e.cat" : "http://127.0.0.1:3000";
const REDIRECT_URI = `${HOST}/auth/callback`; const FRONTEND_URL = isProd ? HOST : "http://127.0.0.1:5173"; -async function getSessionUser(req: BunRequest) { +async function authenticate(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; + if (!cookieHeader) return { user: null, isMod: false }; + + const sessionCookie = cookieHeader.split("; ").find((c) => c.startsWith("session=")); + if (!sessionCookie) return { user: null, isMod: false }; + const sessionId = sessionCookie.split("=")[1]; + const sessionList = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1); + if (sessionList.length === 0) return { user: null, isMod: false }; - const sessionList = await db + const userId = sessionList[0].userId; + const modCheck = await db .select() - .from(sessions) - .where(eq(sessions.id, sessionId)) + .from(moderators) + .where(eq(moderators.user_id, userId)) .limit(1); - if (sessionList.length === 0) return null; - return { id: sessionList[0].userId, username: sessionList[0].username }; + return { + user: { id: userId, username: sessionList[0].username }, + isMod: modCheck.length > 0, + }; +} + +function calculateStats(durations: number[]): { mean: number; median: number; count: number } { + if (durations.length === 0) return { mean: 0, median: 0, count: 0 }; + const sum = durations.reduce((a, b) => a + b, 0); + const sorted = [...durations].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + + return { + mean: sum / durations.length, + median: sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2, + count: durations.length, + }; } const server = Bun.serve({ hostname: "127.0.0.1", + port: process.env.PORT || 3000, routes: { "/api/login": () => { const redirectUri = `https://discord.com/oauth2/authorize?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=identify+guilds`; + return Response.redirect(redirectUri, 302); + }, - return new Response(null, { - status: 302, - headers: { - Location: redirectUri, - }, - }); - }, "/auth/callback": { - GET: async (req) => { + GET: async (req: BunRequest) => { 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", - }); + if (!code) return new Response("No code provided", { status: 400 }); - 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({ + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + grant_type: "authorization_code", + code, + 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, - }); + if (data.error) return new Response(data.error_description, { status: 400 }); - const accessToken = data.access_token; const userResponse = await fetch("https://discord.com/api/users/@me", { - headers: { - Authorization: `Bearer ${accessToken}`, - }, + headers: { Authorization: `Bearer ${data.access_token}` }, }); const userData = await userResponse.json(); - const sessionId = crypto.randomUUID(); + await db.insert(sessions).values({ id: sessionId, userId: userData.id,@@ -125,103 +120,53 @@ },
}); }, }, + "/api/me": { GET: async (req: BunRequest) => { - const user = await getSessionUser(req); + const { user, isMod } = await authenticate(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 profile = await getUser(user.id); + if (!profile) return new Response("Profile not found", { status: 404 }); - return new Response(JSON.stringify({ profile, isMod }), { - headers: { "Content-Type": "application/json" }, - }); + // Ensures the return strictly conforms to `MeResponse` (alias for User) + const response: MeResponse = { ...profile, isModerator: isMod }; + return Response.json(response); }, }, + "/api/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); - const isMod = modCheck.length > 0; + const { user, isMod } = await authenticate(req); + if (!user) return Response.redirect("/login", 302); const url = new URL(req.url); - const limitStr = url.searchParams.get("limit") || "20"; - const limit = Number.parseInt(limitStr, 10); + const limit = Number.parseInt(url.searchParams.get("limit") || "20", 10); - let allTickets; - if (isMod) { - allTickets = await db - .select() - .from(tickets) - .limit(limit) - .orderBy(desc(tickets.createdAt)); - } else { - allTickets = await db - .select() - .from(tickets) - .where(eq(tickets.authorId, user.id)) - .limit(limit) - .orderBy(desc(tickets.createdAt)); - } + const query = db.select().from(tickets).limit(limit).orderBy(desc(tickets.createdAt)); + const allTickets = isMod ? await query : await query.where(eq(tickets.authorId, user.id)); const hydratedTickets = await hydrateTickets(allTickets); - return new Response(JSON.stringify(hydratedTickets), { - headers: { "Content-Type": "application/json" }, - }); + const response: TicketsResponse = { tickets: hydratedTickets }; + return Response.json(response); }, }, + "/api/tickets/:id": { 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); - const isMod = modCheck.length > 0; + const { user, isMod } = await authenticate(req); + if (!user) return Response.redirect("/login", 302); - const { id } = req.params; - const ticketId = Number.parseInt(id, 10); + const ticketId = Number.parseInt(req.params.id, 10); if (isNaN(ticketId)) return new Response("Invalid ID", { status: 400 }); - const ticketList = await db - .select() - .from(tickets) - .where(eq(tickets.id, ticketId)) - .limit(1); + 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 }); if (!isMod && _ticket.authorId !== user.id) { - return new Response( - "Forbidden: You do not have permission to view this ticket.", - { - status: 403, - }, - ); + return new Response("Forbidden", { status: 403 }); } const messages = await db@@ -241,12 +186,11 @@ _ticket.openedBy,
_ticket.claimedBy, ...messages.map((m) => m.authorId), ...mentionedIds, - ].filter(Boolean) as string[]; + ]; await getUsers(idsToFetch); - const [ticket] = await hydrateTickets([_ticket]); - + const [hydratedTicket] = await hydrateTickets([_ticket]); const messageIds = messages.map((m) => m.id); const ticketAttachments = await db@@ -254,10 +198,7 @@ .select()
.from(attachments) .where( or( - and( - eq(attachments.ownerType, "ticket"), - eq(attachments.ownerId, ticketId), - ), + and(eq(attachments.ownerType, "ticket"), eq(attachments.ownerId, ticketId)), messageIds.length > 0 ? and( eq(attachments.ownerType, "ticket_message"),@@ -267,118 +208,83 @@ : undefined,
), ); - const newMessages = await Promise.all( + const newMessages: Message[] = await Promise.all( messages.map(async (message) => { const author = await getUser(message.authorId); - const msgAttachments = ticketAttachments.filter( - (a) => - a.ownerType === "ticket_message" && a.ownerId === message.id, + const _attachments = 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 mentions: Record<string, User> = {}; + for (const match of message.content.matchAll(mentionRegex)) { const id = match[1]; if (!mentions[id]) { - mentions[id] = await getUser(id); + const matchedUser = await getUser(id); + if (matchedUser) mentions[id] = matchedUser; } } return { - ...message, - author, - attachments: msgAttachments, - mentions, + id: message.id, + messageId: message.messageId, + createdAt: new Date(message.createdAt), + updatedAt: message.editedAt ? new Date(message.editedAt) : null, + deletedAt: message.deletedAt ? new Date(message.deletedAt) : null, + author: author as User, + authorType: message.authorType as "user" | "staff" | "system", + content: message.content, + attachments: await Promise.all(_attachments.map(intoApiAttachment)), + mentions: Object.values(mentions), }; }), ); - const mainTicketAttachments = ticketAttachments.filter( - (a) => a.ownerType === "ticket" && a.ownerId === ticketId, - ); + const response: TicketResponse = { + ticket: hydratedTicket, + messages: newMessages, + }; - return new Response( - JSON.stringify({ - ticket: { ...ticket, attachments: mainTicketAttachments }, - messages: newMessages, - }), - { - headers: { "Content-Type": "application/json" }, - }, - ); + return Response.json(response); }, + DELETE: async (req: BunRequest) => { - const user = await getSessionUser(req); + const { user, isMod } = await authenticate(req); if (!user) return new Response("Unauthorized", { status: 401 }); + if (!isMod) 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: You must be a moderator to delete tickets.", - { - status: 403, - }, - ); - - const { id } = req.params; - const ticketId = Number.parseInt(id, 10); + const ticketId = Number.parseInt(req.params.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" }, - }); + return Response.json({ success: true }); }, }, + "/api/moderators": { GET: async (req: BunRequest) => { - const user = await getSessionUser(req); + const { user, isMod } = await authenticate(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 }); + if (!isMod) return new Response("Forbidden", { status: 403 }); const mods = await db.select().from(moderators); - const ids = mods.map((mod) => mod.user_id); - - await getUsers(ids); + await getUsers(mods.map((mod) => mod.user_id)); - const hydrated = await Promise.all( - mods.map(async (mod) => { - const profile = await getUser(mod.user_id); - return { ...profile, pronouns: mod }; - }), + const response: ModeratorsResponse = await Promise.all( + mods.map(async (mod) => (await getUser(mod.user_id)) as User), ); - return new Response(JSON.stringify(hydrated), { - headers: { "Content-Type": "application/json" }, - }); + return Response.json(response); }, }, + "/api/stats/overall": { GET: async (req: BunRequest) => { const url = new URL(req.url); - const daysStr = url.searchParams.get("days"); - const days = daysStr ? Number.parseInt(daysStr, 10) : null; + const days = Number.parseInt(url.searchParams.get("days") || "0", 10); const guildId = url.searchParams.get("guildId"); - const cutoffDate = days - ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) - : null; + const cutoffDate = days ? new Date(Date.now() - days * 86400000) : null; const closedTickets = await db .select()@@ -397,54 +303,37 @@
const allDurations = closedTickets.map( (t) => t.closedAt!.getTime() - t.createdAt.getTime(), ); - - const getStats = (durations: number[]) => { - if (durations.length === 0) return { mean: 0, median: 0, count: 0 }; - const sum = durations.reduce((a, b) => a + b, 0); - const mean = sum / durations.length; - const sorted = [...durations].sort((a, b) => a - b); - const mid = Math.floor(sorted.length / 2); - const median = - sorted.length % 2 !== 0 - ? sorted[mid] - : (sorted[mid - 1] + sorted[mid]) / 2; - return { mean, median, count: durations.length }; - }; + const hourlyDurations = new Map<number, number[]>(); - const globalStats = getStats(allDurations); - - const hourlyDurations = new Map<number, number[]>(); for (const t of closedTickets) { const hour = t.closedAt!.getHours(); if (!hourlyDurations.has(hour)) hourlyDurations.set(hour, []); - hourlyDurations - .get(hour) - ?.push(t.closedAt!.getTime() - t.createdAt.getTime()); + hourlyDurations.get(hour)!.push(t.closedAt!.getTime() - t.createdAt.getTime()); } - const hourly = []; - for (let i = 0; i < 24; i++) { - const durations = hourlyDurations.get(i) || []; - if (durations.length === 0) continue; - const stats = getStats(durations); - hourly.push({ hour: i, ...stats }); - } + const response: OverallStatsResponse = { + global: calculateStats(allDurations), + hourly: Array.from( + { length: 24 }, + (_, hour) => + ({ + hour, + ...calculateStats(hourlyDurations.get(hour) || []), + }) as Statistic & { hour: number }, + ).filter((h) => h.count > 0), + }; - return new Response(JSON.stringify({ global: globalStats, hourly }), { - headers: { "Content-Type": "application/json" }, - }); + return Response.json(response); }, }, + "/api/stats/leaderboard": { GET: async (req: BunRequest) => { const url = new URL(req.url); - const daysStr = url.searchParams.get("days"); - const days = daysStr ? Number.parseInt(daysStr, 10) : null; + const days = Number.parseInt(url.searchParams.get("days") || "0", 10); const guildId = url.searchParams.get("guildId"); const orderBy = url.searchParams.get("order_by") || "median"; - const cutoffDate = days - ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) - : null; + const cutoffDate = days ? new Date(Date.now() - days * 86400000) : null; const closedTickets = await db .select()@@ -462,73 +351,47 @@ );
const modDurations = new Map<string, number[]>(); for (const t of closedTickets) { - const modId = t.closedBy; - if (!modId) continue; + const modId = t.closedBy!; if (!modDurations.has(modId)) modDurations.set(modId, []); - modDurations - .get(modId) - ?.push(t.closedAt!.getTime() - t.createdAt.getTime()); + modDurations.get(modId)!.push(t.closedAt!.getTime() - t.createdAt.getTime()); } - const getStats = (durations: number[]) => { - if (durations.length === 0) return { mean: 0, median: 0, count: 0 }; - const sum = durations.reduce((a, b) => a + b, 0); - const mean = sum / durations.length; - const sorted = [...durations].sort((a, b) => a - b); - const mid = Math.floor(sorted.length / 2); - const median = - sorted.length % 2 !== 0 - ? sorted[mid] - : (sorted[mid - 1] + sorted[mid]) / 2; - return { mean, median, count: durations.length }; - }; - - const modStats = Array.from(modDurations.entries()).map( - ([modId, durations]) => { - const stats = getStats(durations); - return { modId, ...stats }; - }, - ); + const modStats = Array.from(modDurations.entries()).map(([modId, durations]) => ({ + modId, + ...calculateStats(durations), + })); if (orderBy === "mean") modStats.sort((a, b) => a.mean - b.mean); - else if (orderBy === "count") - modStats.sort((a, b) => b.count - a.count); + else if (orderBy === "count") modStats.sort((a, b) => b.count - a.count); else modStats.sort((a, b) => a.median - b.median); - const idsToFetch = modStats.map((mod) => mod.modId); - await getUsers(idsToFetch); + await getUsers(modStats.map((m) => m.modId)); - const leaderboard = await Promise.all( - modStats.map(async (mod, i) => { - const profile = await getUser(mod.modId); - return { - rank: i + 1, - user: profile, - ...mod, - }; - }), + const response: LeaderboardResponse = await Promise.all( + modStats.map(async (mod, i) => ({ + rank: i + 1, + user: (await getUser(mod.modId)) as User, + mean: mod.mean, + median: mod.median, + count: mod.count, + })), ); - return new Response(JSON.stringify(leaderboard), { - headers: { "Content-Type": "application/json" }, - }); + return Response.json(response); }, }, - "/api/*": () => { - return new Response(null, { status: 404 }); - }, + + "/api/*": () => new Response(null, { status: 404 }), - "/*": async (request) => { + "/*": async (request: BunRequest) => { const { pathname } = new URL(request.url); const file = Bun.file(`../frontend/dist/${pathname}`); - if (await file.exists()) return new Response(file, { status: 200 }); + if (await file.exists()) return new Response(file); - const indexFile = Bun.file(`frontend/dist/index.html`); - return new Response(indexFile, { status: 200 }); + return new Response(Bun.file(`../frontend/dist/index.html`)); }, }, }); -const PORT = process.env.PORT || 3000; -logger.info(`server is running at ${server.hostname}:${PORT}`); +logger.info(`server is running at ${server.hostname}:${server.port}`);