const server = new URL(process.env.RADICADE_WEB_SERVER || 'https://ns.radicade.app'); if (!['https:', 'http:'].includes(server.protocol) || server.username || server.password || server.search || server.hash || server.pathname !== '/') throw new Error('RADICADE_WEB_SERVER must be a server origin'); export const backendOrigin = server.origin; const galleryIds = (process.env.RADICADE_GALLERY_IDS || '2').split(',').filter(x => /^\d+$/.test(x)); const memory = new Map(); const inflight = new Map(); async function json(route) { if (memory.get(route)?.until > Date.now()) return memory.get(route).data; if (inflight.has(route)) return inflight.get(route); const pending = (async () => { const r = await fetch(backendOrigin + route, {redirect:'error',signal:AbortSignal.timeout(20000)}); if (!r.ok) throw new Error(`Radicade ${route.split('?')[0]} returned ${r.status}`); const data = await r.json(); memory.set(route,{data,until:Date.now()+15000}); return data; })(); inflight.set(route,pending); try { return await pending; } finally { inflight.delete(route); } } export function publicRooms(rows) { return rows.filter(x => x.Accessibility === 1 && x.State === 0 && !x.IsDorm); } export function roomCard(x) { return {RoomId:String(x.RoomId),Name:x.Name,Description:x.Description ?? null,ImageName:x.ImageName || 'default', CreatorAccountId:x.CreatorAccountId == null ? null : String(x.CreatorAccountId),Accessibility:1,IsRRO:!!x.IsRRO,IsDorm:false, Stats:{CheerCount:x.Stats?.CheerCount ?? 0,VisitCount:x.Stats?.VisitCount ?? 0}}; } export function accountCard(x) { return {accountId:String(x.accountId),username:x.username,displayName:x.displayName,profileImage:x.profileImage || 'default', isJunior:!!x.isJunior,platforms:x.platforms ?? 0,createdAt:iso(x.createdAt),identityFlags:x.identityFlags ?? 0, personalPronouns:x.personalPronouns ?? 0,isMetaPlatformBlocked:false}; } function iso(date) { return /(?:Z|[+-]\d\d:\d\d)$/.test(date) ? date : date+'Z'; } export function photoCard(x) { return {...x,Id:String(x.Id),PlayerId:String(x.PlayerId),RoomId:x.RoomId == null ? null : String(x.RoomId), PlayerEventId:x.PlayerEventId == null ? null : String(x.PlayerEventId),Description:x.Description ?? null, CreatedAt:iso(x.CreatedAt),TaggedPlayerIds:(x.TaggedPlayerIds || []).map(String)}; } async function rooms() { const data=await json('/rooms/rooms/hot'); return publicRooms(data.Results || []); } function ids(url) { const values=url.searchParams.getAll('id'); if (values.length>100 || values.some(x=>!/^\d{1,18}$/.test(x))) throw new Error('Invalid IDs'); return values; } function idQuery(values) { return new URLSearchParams(values.map(x=>['id',String(x)])).toString(); } export async function serveRadicade(url,req,res) { if (!url.pathname.startsWith('/_public/') && url.pathname !== '/_adapter/status') return false; try { let data; if (url.pathname === '/_adapter/status') data={backend:backendOrigin,mode:'radicade',photoFeed:'public galleries of room creators and configured accounts',galleryIds}; else if (url.pathname === '/_public/rooms/featuredrooms/current') { const list=await rooms(); list.sort((a,b)=>(b.Stats?.VisitCount||0)-(a.Stats?.VisitCount||0)); data={Rooms:list.slice(0,6).map(x=>({RoomId:String(x.RoomId)}))}; } else if (url.pathname === '/_public/rooms/rooms/bulk') { const wanted=new Set(ids(url)); data=(await rooms()).filter(x=>wanted.has(String(x.RoomId))).map(roomCard); } else if (url.pathname === '/_public/accounts/account/bulk') { const wanted=ids(url); data=wanted.length ? (await json('/accounts/account/bulk?'+idQuery(wanted))).map(accountCard) : []; } else if (url.pathname === '/_public/apim/apis/api/images/v3/feed/global') { const skip=Number(url.searchParams.get('skip') || 0),take=Number(url.searchParams.get('take') || 10); if (!Number.isInteger(skip)||!Number.isInteger(take)||skip<0||skip>10000||take<1||take>100) throw new Error('Invalid pagination'); const creators=[...new Set([...galleryIds,...(await rooms()).map(x=>String(x.CreatorAccountId))])].slice(0,50); const images=[]; for(const id of creators) images.push(...await json('/api/images/v5/player/'+id)); const unique=[...new Map(images.filter(x=>x.Accessibility===1 && x.Type===1).map(x=>[String(x.Id),x])).values()]; unique.sort((a,b)=>new Date(b.CreatedAt)-new Date(a.CreatedAt)); data=unique.slice(skip,skip+take).map(photoCard); } else if (/^\/_public\/img\/[a-zA-Z0-9_.%-]{1,180}$/.test(url.pathname)) { const name=url.pathname.split('/').pop(); if (decodeURIComponent(name).includes('/') || decodeURIComponent(name).includes('\\') || name === '..') throw new Error('Invalid image'); const r=await fetch(backendOrigin+'/images/img/'+name,{redirect:'error',signal:AbortSignal.timeout(20000)}); if (!r.ok) throw new Error('Radicade image returned '+r.status); const type=r.headers.get('content-type') || ''; if (!type.startsWith('image/')) throw new Error('Unexpected image type'); res.writeHead(200,{'Content-Type':type,'Cache-Control':'public, max-age=60'}); res.end(req.method==='HEAD'?undefined:Buffer.from(await r.arrayBuffer())); return true; } else {res.writeHead(404,{'Content-Type':'application/json'});res.end('{"error":"adapter_route_not_connected"}');return true;} res.writeHead(200,{'Content-Type':'application/json','Cache-Control':'no-store','X-Radicade-Backend':backendOrigin}); res.end(req.method==='HEAD'?undefined:JSON.stringify(data)); } catch(error) { console.error('Radicade adapter:',error.message); res.writeHead(502,{'Content-Type':'application/json'});res.end(JSON.stringify({error:'radicade_unavailable',message:error.message})); } return true; }