export interface DiscordRole { id: string; name: string; color: number; position: number; } export interface DiscordMemberProfile { id: string; username: string; globalName: string | null; avatarUrl: string | null; bannerUrl: string | null; accentColor: number | null; roles: DiscordRole[]; } interface CacheEntry { data: T; expiresAt: number; } const rolesCache: { entry: CacheEntry | null } = { entry: null }; const profileCache = new Map>(); const CACHE_TTL = 60_000; const DEFAULT_DISCORD_GUILD_ID = "1302977523310923847"; function getCached(entry: CacheEntry | null | undefined): T | null { if (entry && Date.now() < entry.expiresAt) return entry.data; return null; } function getDiscordGuildId() { return process.env.DISCORD_GUILD_ID || DEFAULT_DISCORD_GUILD_ID; } export async function addDiscordGuildMember(discordId: string, accessToken: string): Promise { const token = process.env.DISCORD_BOT_TOKEN; const guildId = getDiscordGuildId(); if (!token || !guildId || !discordId || !accessToken) return false; try { const res = await fetch( `https://discord.com/api/v10/guilds/${encodeURIComponent(guildId)}/members/${encodeURIComponent(discordId)}`, { method: "PUT", headers: { Authorization: `Bot ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ access_token: accessToken }), signal: AbortSignal.timeout(5000), }, ); if (res.status === 201 || res.status === 204) return true; const body = await res.text().catch(() => ""); console.error( `Failed to add Discord member ${discordId} to guild ${guildId}: ${res.status}${body ? ` ${body.slice(0, 300)}` : ""}`, ); return false; } catch (error) { console.error(`Failed to add Discord member ${discordId} to guild ${guildId}`, error); return false; } } export async function getGuildRolesInternal(): Promise { const cached = getCached(rolesCache.entry); if (cached) return cached; const token = process.env.DISCORD_BOT_TOKEN; const guildId = getDiscordGuildId(); 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 = (await res.json()) as Array<{ id: string; name: string; color: number; position: number; }>; const result = roles .map((role) => ({ id: role.id, name: role.name, color: role.color, position: role.position, })) .sort((a, b) => b.position - a.position); rolesCache.entry = { data: result, expiresAt: Date.now() + CACHE_TTL }; return result; } catch { return []; } } export async function getGuildMemberRoles(discordId: string): Promise { const token = process.env.DISCORD_BOT_TOKEN; const guildId = getDiscordGuildId(); 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", signal: AbortSignal.timeout(5000), }); if (!res.ok) return []; const member = (await res.json()) as { roles?: string[] }; return member.roles ?? []; } catch { return []; } } export async function getDiscordMemberProfile(discordId: string): Promise { const cached = getCached(profileCache.get(discordId)); if (cached) return cached; const token = process.env.DISCORD_BOT_TOKEN; const guildId = getDiscordGuildId(); 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()) as { roles?: string[]; user: { id: string; username: string; global_name?: string | null; avatar?: string | null; banner?: string | null; accent_color?: number | null; }; }; const roles = (member.roles ?? []) .map((id) => allRoles.find((role) => role.id === id)) .filter((role): role is DiscordRole => Boolean(role)) .sort((a, b) => b.position - a.position); const { user } = member; const avatarUrl = user.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${user.avatar.startsWith("a_") ? "gif" : "png"}?size=128` : null; const bannerUrl = user.banner ? `https://cdn.discordapp.com/banners/${user.id}/${user.banner}.${user.banner.startsWith("a_") ? "gif" : "png"}?size=512` : null; 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 (error) { console.error("Failed to fetch member profile", error); return null; } }