export interface Guild { id: string; name: string; icon: string | null; owner: boolean; permissions: string; features: string[]; botInGuild?: boolean; isBetaServer?: boolean; } export interface LeaderboardMember { user_id: string; username: string; avatar: string; level: number; xp: number; rank: number; } export interface Role { role_id: string; role_name: string; color: number; position: number; } export interface TrackRole { role_name: string; role_id: string; // Changed from id to role_id to match backend level: number; } export interface LevelTrack { [trackName: string]: TrackRole[]; } // ... (previous interfaces) export interface LevelBridge { in_role_id: string; out_role_id: string; } export interface LevelTracksResponse { tracks: LevelTrack[]; } export async function getGuildLeaderboard(guildId: string): Promise { try { if (!process.env.BOT_API_URL) return []; const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/leaderboard`, { method: "GET", headers: { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string }, next: { revalidate: 60 } }); if (response.ok) { return await response.json(); } console.error(`Fetch Leaderboard failed: ${response.status} ${response.statusText}`); return []; } catch (e) { console.error("Failed to fetch leaderboard", e); return []; } } export async function checkBotMembership(guildIds: string[]): Promise { try { if (!process.env.BOT_API_URL) return guildIds.map(() => false); const response = await fetch(`${process.env.BOT_API_URL}/api/is_bot_there`, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string }, body: JSON.stringify({ guild_ids: guildIds }), next: { revalidate: 0 } // Don't cache this as it might change }); if (response.ok) { const data = await response.json(); return data.results; } else { const errorText = await response.text(); console.error(`CheckBotMembership failed: ${response.status} ${response.statusText}`, errorText); } return guildIds.map(() => false); } catch (e) { console.error("Failed to check bot membership", e); return guildIds.map(() => false); } } export async function checkBetaServer(guildIds: string[]): Promise { try { if (!process.env.BOT_API_URL) return guildIds.map(() => false); // Helper to ensure IDs are numeric strings to prevent injection if we are doing manual JSON const safeGuildIds = guildIds.filter(id => /^\d+$/.test(id)); if (safeGuildIds.length === 0) return guildIds.map(() => false); // Use JSON.stringify which matches the backend expectation of strings const body = JSON.stringify({ guild_ids: guildIds }); const response = await fetch(`${process.env.BOT_API_URL}/api/is_beta_server`, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string }, body: body, next: { revalidate: 60 } }); if (response.ok) { const data = await response.json(); return data.results; } else { const errorText = await response.text(); console.error(`CheckBetaServer failed: ${response.status} ${response.statusText}`, errorText); } return guildIds.map(() => false); } catch (e) { console.error("Failed to check beta server", e); return guildIds.map(() => false); } } export async function getUserGuilds(accessToken: string): Promise { try { const response = await fetch("https://discord.com/api/users/@me/guilds", { headers: { Authorization: `Bearer ${accessToken}`, }, next: { revalidate: 60 } // Cache for 60 seconds }); if (response.ok) { const guilds: Guild[] = await response.json(); // Filter: Manage Guild (0x20) or Administrator (0x8) const managedGuilds = guilds.filter((guild: any) => { const perms = BigInt(guild.permissions); const MANAGE_GUILD = BigInt(0x20); const ADMINISTRATOR = BigInt(0x8); return (perms & MANAGE_GUILD) === MANAGE_GUILD || (perms & ADMINISTRATOR) === ADMINISTRATOR; }); // Check if bot is in these guilds and if they are beta servers const guildIds = managedGuilds.map(g => g.id); if (guildIds.length > 0) { const [memberships, betaStatuses] = await Promise.all([ checkBotMembership(guildIds), checkBetaServer(guildIds) ]); return managedGuilds.map((guild, index) => ({ ...guild, botInGuild: memberships[index] || false, isBetaServer: betaStatuses[index] || false })); } return managedGuilds; } return []; } catch (e) { console.error("Failed to fetch guilds", e); return []; } } export async function getGuildRoles(guildId: string, userToken?: string): Promise { try { if (!process.env.BOT_API_URL) return []; const headers: Record = { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string }; if (userToken) { headers["X-User-Token"] = userToken; } const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/roles`, { method: "GET", headers, next: { revalidate: 60 } }); if (response.ok) { const data = await response.json(); // Transform API response: { "0": { role_id, role_name, color, position }, ... } // to array of Role: { role_id, role_name, color, position } if (data && typeof data === 'object') { return Object.values(data).map((role: any) => ({ role_id: role.role_id || role.id, role_name: role.role_name || role.name, color: role.color ?? 0, position: role.position ?? 0 })); } return []; } console.error(`Fetch Guild Roles failed: ${response.status} ${response.statusText}`); return []; } catch (e) { console.error("Failed to fetch guild roles", e); return []; } } export async function getLevelTracks(guildId: string, userToken?: string): Promise { try { if (!process.env.BOT_API_URL) return []; const headers: Record = { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string }; if (userToken) { headers["X-User-Token"] = userToken; } const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, { method: "GET", headers, next: { revalidate: 0 } // Don't cache as this is frequently edited }); if (response.ok) { // WORKAROUND: Handle u64 precision loss (JS Numbers are f64) // Backend sends role_id as u64 (might be > 2^53). // We intercept the raw text and wrap role_id numeric values in quotes before parsing. const text = await response.text(); const safeText = text.replace(/"role_id":\s*(\d+)/g, '"role_id": "$1"'); return JSON.parse(safeText); } console.error(`Fetch Level Tracks failed: ${response.status} ${response.statusText}`); return []; } catch (e) { console.error("Failed to fetch level tracks", e); return []; } } // Type for role in track API export interface TrackRoleUpdate { role_id: string; // Changed to string to handle u64 precision on frontend level: number; } // Create a new track (POST) export async function createLevelTrack( guildId: string, data: { track_name: string; roles: TrackRoleUpdate[] }, userToken: string ): Promise<{ success: boolean; error?: string }> { try { if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" }; // REMOVED WORKAROUND: Backend now expects strings for u64 IDs const bodyJson = JSON.stringify(data); const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string, "X-User-Token": userToken }, body: bodyJson }); if (response.ok) { return { success: true }; } const errorText = await response.text(); return { success: false, error: errorText || `Failed with status ${response.status}` }; } catch (e) { console.error("Failed to create level track", e); return { success: false, error: "Internal error" }; } } // Update existing track (PATCH) export async function updateLevelTrack( guildId: string, data: { track_name: string; roles: TrackRoleUpdate[] }, userToken: string ): Promise<{ success: boolean; error?: string }> { try { if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" }; // REMOVED WORKAROUND: Backend now expects strings for u64 IDs const bodyJson = JSON.stringify(data); const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, { method: "PATCH", headers: { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string, "X-User-Token": userToken }, body: bodyJson }); if (response.ok) { return { success: true }; } const errorText = await response.text(); return { success: false, error: errorText || `Failed with status ${response.status}` }; } catch (e) { console.error("Failed to update level track", e); return { success: false, error: "Internal error" }; } } // Delete a track (DELETE) export async function deleteLevelTrack( guildId: string, trackName: string, userToken: string ): Promise<{ success: boolean; error?: string }> { try { if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" }; const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, { method: "DELETE", headers: { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string, "X-User-Token": userToken }, body: JSON.stringify({ track_name: trackName }) }); if (response.ok) { return { success: true }; } const errorText = await response.text(); return { success: false, error: errorText || `Failed with status ${response.status}` }; } catch (e) { console.error("Failed to delete level track", e); return { success: false, error: "Internal error" }; } } // ... (previous code) // Level Bridger API Helpers export async function getLevelBridges(guildId: string, userToken: string): Promise { try { if (!process.env.BOT_API_URL) return []; const headers: Record = { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string, "X-User-Token": userToken }; const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, { method: "GET", headers, next: { revalidate: 0 } }); if (response.ok) { // WORKAROUND: Handle u64 precision loss if needed, though usually these simple objects survive better // But let's be safe like we were with tracks if the backend sends huge numbers. const text = await response.text(); // Regex to safely wrap role_id's if they come as raw numbers const safeText = text.replace(/("in_role_id"|"out_role_id"):\s*(\d+)/g, '$1: "$2"'); return JSON.parse(safeText); } console.error(`Fetch Level Bridges failed: ${response.status} ${response.statusText}`); return []; } catch (e) { console.error("Failed to fetch level bridges", e); return []; } } export async function createLevelBridge( guildId: string, bridge: LevelBridge, userToken: string ): Promise<{ success: boolean; error?: string }> { try { if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" }; // REMOVED WORKAROUND: Backend now expects strings for u64 IDs const bodyJson = JSON.stringify(bridge); const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string, "X-User-Token": userToken }, body: bodyJson }); if (response.ok) { return { success: true }; } const errorText = await response.text(); return { success: false, error: errorText || `Failed with status ${response.status}` }; } catch (e) { console.error("Failed to create level bridge", e); return { success: false, error: "Internal error" }; } } export async function updateLevelBridge( guildId: string, bridge: LevelBridge, userToken: string ): Promise<{ success: boolean; error?: string }> { try { if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" }; const bodyJson = JSON.stringify(bridge); const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, { method: "PUT", headers: { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string, "X-User-Token": userToken }, body: bodyJson }); if (response.ok) { return { success: true }; } const errorText = await response.text(); return { success: false, error: errorText || `Failed with status ${response.status}` }; } catch (e) { console.error("Failed to update level bridge", e); return { success: false, error: "Internal error" }; } } export async function deleteLevelBridge( guildId: string, inRoleId: string, userToken: string ): Promise<{ success: boolean; error?: string }> { try { if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" }; const bodyJson = JSON.stringify({ in_role_id: inRoleId }); const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, { method: "DELETE", headers: { "Content-Type": "application/json", "X-API-Key": process.env.BOT_API_KEY as string, "X-User-Token": userToken }, body: bodyJson }); if (response.ok) { return { success: true }; } const errorText = await response.text(); return { success: false, error: errorText || `Failed with status ${response.status}` }; } catch (e) { console.error("Failed to delete level bridge", e); return { success: false, error: "Internal error" }; } }