mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
81 lines
5.7 KiB
JavaScript
81 lines
5.7 KiB
JavaScript
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;
|
|
}
|