/** * Badge search — powered by official Habbo external texts. * * Uses habbo.it/gamedata/external_flash_texts as the single source. * Badge images from images.habbo.com/c_images/album1584/. * No third-party APIs (habboassets.com) needed. */ export type { HabboAssetBadge } from "@/types/furni"; import type { HabboAssetBadge } from "@/types/furni"; const EXTERNAL_TEXTS_URL = "https://www.habbo.it/gamedata/external_flash_texts/1"; const BADGE_IMAGE_BASE = "https://images.habbo.com/c_images/album1584"; const CACHE_TTL = 30 * 60 * 1000; // 30 minutes interface BadgeEntry { code: string; name: string; description: string; } let badgeCache: BadgeEntry[] | null = null; let cacheTimestamp = 0; let loadingPromise: Promise | null = null; /** * Fetch and parse all badges from official Habbo external texts. * Cached in memory for 30 minutes. */ async function loadBadges(): Promise { const now = Date.now(); if (badgeCache && now - cacheTimestamp < CACHE_TTL) return badgeCache; if (loadingPromise) return loadingPromise; loadingPromise = (async () => { try { if (!EXTERNAL_TEXTS_URL) { console.warn("[badges] HABBO_EXTERNAL_TEXTS_URL not configured"); return badgeCache || []; } const res = await fetch(EXTERNAL_TEXTS_URL, { signal: AbortSignal.timeout(20000), }); if (!res.ok) { console.warn(`[badges] Fetch failed: ${res.status}`); return badgeCache || []; } const text = await res.text(); const lines = text.split("\n"); // Parse key=value format, extract badge_name_CODE and badge_desc_CODE const nameMap = new Map(); const descMap = new Map(); for (const line of lines) { const eqIdx = line.indexOf("="); if (eqIdx === -1) continue; const key = line.substring(0, eqIdx).trim(); const value = line.substring(eqIdx + 1).trim(); // Format: badge_name_CODE or CODE_badge_name let match = key.match(/^badge_name_(.+)$/); if (match) { nameMap.set(match[1], value); continue; } match = key.match(/^(.+)_badge_name$/); if (match) { nameMap.set(match[1], value); continue; } match = key.match(/^badge_desc_(.+)$/); if (match) { descMap.set(match[1], value); continue; } match = key.match(/^(.+)_badge_desc$/); if (match) { descMap.set(match[1], value); } } // Merge into badge entries const allCodes = new Set([...nameMap.keys(), ...descMap.keys()]); const badges: BadgeEntry[] = []; for (const code of allCodes) { badges.push({ code, name: nameMap.get(code) || "", description: descMap.get(code) || "", }); } // Sort alphabetically by code badges.sort((a, b) => a.code.localeCompare(b.code)); console.debug( `[badges] Loaded ${badges.length} badges from external texts`, ); badgeCache = badges; cacheTimestamp = now; return badges; } catch (err) { console.warn("[badges] Load error:", (err as Error).message); return badgeCache || []; } finally { loadingPromise = null; } })(); return loadingPromise; } /** * Search badges from official Habbo external texts. * Returns badges with image URLs from images.habbo.com. */ export async function searchBadges( term: string, limit = 100, offset = 0, _hotel: string | null = null, // kept for API compat, ignored (single source) ): Promise { const badges = await loadBadges(); const search = term.toLowerCase().trim(); let filtered = badges; if (search) { filtered = badges.filter( (b) => b.code.toLowerCase().includes(search) || b.name.toLowerCase().includes(search) || b.description.toLowerCase().includes(search), ); } else { // No search term: return newest (reverse sorted, last entries are newest) filtered = [...badges].reverse(); } const page = filtered.slice(offset, offset + limit); return page.map((b) => ({ id: 0, code: b.code, name: b.name, description: b.description, hotel: "it", url_habbo: `${BADGE_IMAGE_BASE}/${b.code}.gif`, url_habboassets: `${BADGE_IMAGE_BASE}/${b.code}.gif`, // same source, no fallback needed })); } /** Clear the badge cache */ export function clearBadgeCache(): void { badgeCache = null; cacheTimestamp = 0; }