"use server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; export interface DiscordRole { id: string; name: string; color: number; position: number; } // --- In-memory caches with TTL --- interface CacheEntry { data: T; expiresAt: number } const rolesCache: { entry: CacheEntry | null } = { entry: null }; const profileCache = new Map>(); const CACHE_TTL = 60_000; // 60 seconds function getCached(entry: CacheEntry | null | undefined): T | null { if (entry && Date.now() < entry.expiresAt) return entry.data; return null; } /** Internal: fetch guild roles without auth check (for use by other server functions). */ async function getGuildRolesInternal(): Promise { const cached = getCached(rolesCache.entry); if (cached) return cached; const token = process.env.DISCORD_BOT_TOKEN; const guildId = process.env.DISCORD_GUILD_ID; if (!token || !guildId) return []; try { const res = await fetch(`https://discord.com/api/v10/guilds/${guildId}/roles`, { headers: { Authorization: `Bot ${token}` }, signal: AbortSignal.timeout(5000), }); if (!res.ok) return []; const roles: any[] = await res.json(); const result = roles.map(r => ({ id: r.id, name: r.name, color: r.color, position: r.position, })).sort((a, b) => b.position - a.position); rolesCache.entry = { data: result, expiresAt: Date.now() + CACHE_TTL }; return result; } catch { return []; } } /** Public: fetch guild roles with admin auth check. */ export async function getGuildRoles(): Promise { const session = await getServerSession(authOptions); const discordId = (session?.user as any)?.discordId; const admins = (process.env.ADMIN_DISCORD_IDS || "").split(","); if (!discordId || !admins.includes(discordId)) { throw new Error("Unauthorized"); } return getGuildRolesInternal(); } export async function getGuildMemberRoles(discordId: string): Promise { const token = process.env.DISCORD_BOT_TOKEN; const guildId = process.env.DISCORD_GUILD_ID; if (!token || !guildId || !discordId) return []; try { const res = await fetch(`https://discord.com/api/v10/guilds/${guildId}/members/${discordId}`, { headers: { Authorization: `Bot ${token}` }, cache: "no-store", // Always fresh for access control signal: AbortSignal.timeout(5000), }); if (!res.ok) return []; const member = await res.json(); return member.roles || []; } catch { return []; } } export async function getDiscordMemberProfile(discordId: string) { const cached = getCached(profileCache.get(discordId)); if (cached) return cached; const token = process.env.DISCORD_BOT_TOKEN; const guildId = process.env.DISCORD_GUILD_ID; if (!token || !guildId || !discordId) return null; try { const [memberRes, allRoles] = await Promise.all([ fetch(`https://discord.com/api/v10/guilds/${guildId}/members/${discordId}`, { headers: { Authorization: `Bot ${token}` }, cache: "no-store", signal: AbortSignal.timeout(5000), }), getGuildRolesInternal(), ]); if (!memberRes.ok) return null; const member = await memberRes.json(); // Map member role IDs to actual role objects const memberRoleIds = member.roles || []; const roles = memberRoleIds .map((id: string) => allRoles.find(r => r.id === id)) .filter(Boolean); // Sort roles by position (descending) roles.sort((a: any, b: any) => (b?.position || 0) - (a?.position || 0)); const user = member.user; let avatarUrl = null; if (user.avatar) { const ext = user.avatar.startsWith("a_") ? "gif" : "png"; avatarUrl = `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=128`; } let bannerUrl = null; if (user.banner) { const ext = user.banner.startsWith("a_") ? "gif" : "png"; bannerUrl = `https://cdn.discordapp.com/banners/${user.id}/${user.banner}.${ext}?size=512`; } const result = { id: user.id, username: user.username, globalName: user.global_name || null, avatarUrl, bannerUrl, accentColor: user.accent_color || null, roles, }; profileCache.set(discordId, { data: result, expiresAt: Date.now() + CACHE_TTL }); return result; } catch (e) { console.error("Failed to fetch member profile", e); return null; } }