import "server-only"; import { and, eq, gte, inArray } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import { integer, numeric, pgTable, text } from "drizzle-orm/pg-core"; import postgres from "postgres"; import { buildVoiceLeaderboard, buildXpLeaderboard, type VoiceSourceRow, } from "@/lib/leaderboard/ranking"; const userLevels = pgTable("user_levels", { userId: text("user_id").notNull(), username: text("username").notNull(), displayName: text("display_name"), xp: numeric("xp", { precision: 40, scale: 0 }).notNull(), level: integer("level").notNull(), }); const voiceTime = pgTable("voice_time", { userId: text("user_id").notNull(), channelId: text("channel_id").notNull(), totalSeconds: numeric("total_seconds", { precision: 40, scale: 0 }).notNull(), }); type LeaderboardClient = ReturnType; const globalForLeaderboard = globalThis as typeof globalThis & { erikaLeaderboardReadOnlyClient?: LeaderboardClient; }; function getLeaderboardDatabase() { const connectionString = process.env.LEADERBOARD_DATABASE_URL; if (!connectionString) { throw new Error("LEADERBOARD_DATABASE_URL is not configured."); } if (!globalForLeaderboard.erikaLeaderboardReadOnlyClient) { globalForLeaderboard.erikaLeaderboardReadOnlyClient = postgres(connectionString, { max: 3, idle_timeout: 20, connect_timeout: 5, connection: { application_name: "erika-leaderboard", default_transaction_read_only: true, statement_timeout: 15_000, }, }); } return drizzle(globalForLeaderboard.erikaLeaderboardReadOnlyClient); } export async function getXpLeaderboardPage( requestedPage: number, searchQuery?: string, ) { const rows = await getLeaderboardDatabase() .select({ userId: userLevels.userId, username: userLevels.username, displayName: userLevels.displayName, level: userLevels.level, totalXp: userLevels.xp, }) .from(userLevels) .where(gte(userLevels.level, 1)); return buildXpLeaderboard(rows, requestedPage, searchQuery); } export async function getMemberXpStats(userId: string) { const database = getLeaderboardDatabase(); const [sourceRows, leaderboard] = await Promise.all([ database .select({ level: userLevels.level, totalXp: userLevels.xp, }) .from(userLevels) .where(eq(userLevels.userId, userId)) .limit(1), getXpLeaderboardPage(1, userId), ]); const rankedRow = leaderboard.rows.find((row) => row.userId === userId); const sourceRow = sourceRows[0]; return { level: sourceRow?.level ?? 0, totalXp: sourceRow?.totalXp ?? "0", rank: rankedRow?.rank ?? null, }; } export async function getVoiceLeaderboardPage( currentChannelIds: string[], requestedPage: number, selectedChannelId?: string, searchQuery?: string, ) { const scopedChannelIds = selectedChannelId ? [selectedChannelId] : currentChannelIds; if (scopedChannelIds.length === 0) { return buildVoiceLeaderboard([], currentChannelIds, requestedPage, selectedChannelId, searchQuery); } const rows: VoiceSourceRow[] = await getLeaderboardDatabase() .select({ userId: voiceTime.userId, channelId: voiceTime.channelId, totalSeconds: voiceTime.totalSeconds, username: userLevels.username, displayName: userLevels.displayName, }) .from(voiceTime) .leftJoin(userLevels, eq(voiceTime.userId, userLevels.userId)) .where( and( inArray(voiceTime.channelId, scopedChannelIds), gte(voiceTime.totalSeconds, "0"), ), ); return buildVoiceLeaderboard( rows, currentChannelIds, requestedPage, selectedChannelId, searchQuery, ); } export async function getMemberVoiceStats( userId: string, currentChannelIds: string[], ) { const leaderboard = await getVoiceLeaderboardPage( currentChannelIds, 1, undefined, userId, ); const rankedRow = leaderboard.rows.find((row) => row.userId === userId); return { totalSeconds: rankedRow?.totalSeconds ?? "0", rank: rankedRow?.rank ?? null, }; }