Files
EpicNext-Cms/src/lib/services/habboassets.ts
T
openhands 17847545dd
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m52s
Improvements: remove dead config, fix ESM, add URL validation, unify types, add missing logging
- Remove .prettierrc (dead config, Biome replaces Prettier)
- Rename lighthouserc.json to lighthouserc.cjs with module.exports for ESM compat
- Add logger.warn to empty catch blocks in auth, register, site-settings, prisma-cache, redis, security, rate-limit
- Unify ActionResult type: action-helper.ts uses 'ok' consistent with safe-action-shared.ts
- Add noUnusedLocals + noUnusedParameters to tsconfig + fix 25 pre-existing unused vars
- Replace barrel export src/types/index.ts with direct @/types/common imports
- Make trustHost conditional (development only) in auth.ts
- Add pre-flight URL validation to update-Nitrov3.sh to catch image.library.url misconfigurations
- Improve NITRO_IMAGE_LIBRARY_URL content validation in pre-flight & post-compute checks
2026-07-26 20:28:11 +02:00

172 lines
4.3 KiB
TypeScript

/**
* Badge search — powered by official Habbo external texts.
*
* Hotel locale comes from CMS setting `habbo_gamedata_hotel`.
* Badge images from images.habbo.com/c_images/album1584/.
*/
export type { HabboAssetBadge } from "@/types/furni";
import {
type HabboGamedataHotel,
habboExternalTextsUrl,
} from "@/lib/habbo-gamedata-hotel";
import { logger } from "@/lib/logger";
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
import type { HabboAssetBadge } from "@/types/furni";
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 cacheHotel: HabboGamedataHotel | null = null;
let cacheTimestamp = 0;
let loadingPromise: Promise<BadgeEntry[]> | null = null;
/**
* Fetch and parse all badges from official Habbo external texts.
* Cached in memory for 30 minutes (per configured hotel).
*/
async function loadBadges(): Promise<{
badges: BadgeEntry[];
hotel: HabboGamedataHotel;
}> {
const hotel = await getHabboGamedataHotel();
const now = Date.now();
if (badgeCache && cacheHotel === hotel && now - cacheTimestamp < CACHE_TTL) {
return { badges: badgeCache, hotel };
}
if (loadingPromise) {
const badges = await loadingPromise;
return { badges, hotel: cacheHotel ?? hotel };
}
loadingPromise = (async () => {
const url = habboExternalTextsUrl(hotel);
try {
const res = await fetch(url, {
signal: AbortSignal.timeout(20000),
});
if (!res.ok) {
logger.warn(`[badges] Fetch failed: ${res.status}`);
return badgeCache || [];
}
const text = await res.text();
const lines = text.split("\n");
const nameMap = new Map<string, string>();
const descMap = new Map<string, string>();
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();
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);
}
}
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) || "",
});
}
badges.sort((a, b) => a.code.localeCompare(b.code));
console.debug(
`[badges] Loaded ${badges.length} badges from habbo.${hotel}`,
);
badgeCache = badges;
cacheHotel = hotel;
cacheTimestamp = Date.now();
return badges;
} catch (err) {
logger.warn("[badges] Load error", { error: (err as Error).message });
return badgeCache || [];
} finally {
loadingPromise = null;
}
})();
const badges = await loadingPromise;
return { badges, hotel: cacheHotel ?? hotel };
}
/**
* 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, // API compat; locale comes from CMS settings
): Promise<HabboAssetBadge[]> {
const { badges, hotel } = 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 {
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,
url_habbo: `${BADGE_IMAGE_BASE}/${b.code}.gif`,
url_habboassets: `${BADGE_IMAGE_BASE}/${b.code}.gif`,
}));
}
/** Clear the badge cache */
export function clearBadgeCache(): void {
badgeCache = null;
cacheHotel = null;
cacheTimestamp = 0;
}