Something went wrong. Try again.
Proxy util for accessing TLE server methods via more standard conventions.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260import express from 'express';import morgan from 'morgan';import bodyParser from 'body-parser';import cors from 'cors';import argumentMap from './argumentMap.json' with { type: 'json' };
const errorCodes = [ 1000, // Name not available. 1001, // Invalid password. 1002, // Object does not exist. 1003, // Too much information. 1004, // Password incorrect. 1005, // Contains invalid characters. 1006, // Session expired. 1007, // Overspend. 1008, // Underspend. 1009, // Invalid range. 1010, // Insufficient privileges. 1011, // Not enough resources in storage. 1012, // Not enough resources in production. 1013, // Missing prerequisites. 1014, // Captcha not valid. 1015, // Restricted for sitter logins. 1016, // Needs to solve a captcha. 1017, // Pending Parliamentary Vote 1018, // Already Voted 1100, // Empire not founded. 1101, // Empire not founded, and you tried to create it, but had the wrong password. 1200, // Game Over.] as const;
type ReqModule = string;type ReqMethod = string;type MethodRef = `/${ReqModule}/${ReqMethod}`;
type Argument = { name: string; required: boolean;};
type RPCRequest = { id: number; jsonrpc: '2.0'; method: ReqMethod; params: any;};
type RPCError = { jsonrpc: '2.0'; id: number; error: { code: (typeof errorCodes)[number]; message: string; data?: any; };};
// Maps each /module/method the v2 API serves to its legacy positional Perl RPC argument order,// derived from lib/Lacuna/RPC/**/*.pm + the OpenAPI spec. Covers ~1100 of the spec's ~1157// methods as a first pass - some couldn't be resolved automatically (a handful of buildings that// the spec exposes under a consolidated name with no single legacy equivalent, e.g. "beach", plus// a few one-off spec/backend naming or shape mismatches). Update this file directly as gaps are// found through testing.const namedArgumentMapper: Record<MethodRef, Argument[]> = argumentMap.positional as Record< MethodRef, Argument[]>;
// Special handling for Perl methods that take named arguments or hash refs or lists// Three versions of this are needed because the server accepts different forms. Fun!
// params => { session_id, ...args }const namedOnlyArgumentHashSet: Set<MethodRef> = new Set( argumentMap.namedOnlyArgumentHash as MethodRef[]);
// params => [{ session_id, ...args }]const namedOnlyArgumentListFirstPositionHashSet: Set<MethodRef> = new Set( argumentMap.namedOnlyArgumentListFirstPositionHash as MethodRef[]);
// params => [session_id, { ...args }]const namedOnlyArgumentListSecondPositionHashSet: Set<MethodRef> = new Set( argumentMap.namedOnlyArgumentListSecondPositionHash as MethodRef[]);
// Methods called before a session exists - never inject a session ID into these, even if the// client sends an Authorization header.const sessionlessMethods: Set<MethodRef> = new Set([ '/empire/login', '/empire/create', '/empire/found', '/empire/send_password_reset_message',]);
const port = process.env.PORT || 5999;const app = express();
app.set('trust proxy', 'uniquelocal');
app.use(cors());app.use(morgan('common', { immediate: true }));app.use(bodyParser.json());
app.post('/v2/:module/:method', async (req, res) => { const { module: reqModule, method: reqMethod } = req.params; const methodRef: MethodRef = `/${reqModule}/${reqMethod}`; const token = !sessionlessMethods.has(methodRef) && req.headers['authorization']?.startsWith('Token') ? req.headers['authorization'].split(' ')[1] : undefined;
const prepareBody = (data: any): RPCRequest | RPCError => { console.log('handling body', data); const body: RPCRequest = { id: 1, jsonrpc: '2.0', method: reqMethod, params: null, };
if (data['jsonrpc']) { body.id = data['id']; body.params = data['params']; } else if (Array.isArray(data) || (typeof data === 'object' && data !== null)) { body.params = data; }
// Every method the v2 API serves must be accounted for in the generated argument map - // either as a positional conversion or as a known named-only passthrough. A method // reaching neither means it wasn't found in the OpenAPI spec / Perl backend when the map // was generated (or the spec and backend have drifted since) - fail loudly rather than // guess at an unverified argument order. if ( !namedArgumentMapper[methodRef] && !namedOnlyArgumentListFirstPositionHashSet.has(methodRef) && !namedOnlyArgumentListSecondPositionHashSet.has(methodRef) && !namedOnlyArgumentHashSet.has(methodRef) ) { return { id: 1, jsonrpc: '2.0', error: { code: 1002, message: `Method ${methodRef} is not recognized by the v2 API's argument map.`, data: { methodRef }, }, }; }
// Use the mapping to convert named args into legacy positional arguments // This quasi-proxy approach allows us to maintain backwards compatibility with // previous iterations of the game/scripts as well as inject some validation when helpful. if (!Array.isArray(body.params) && !!namedArgumentMapper[methodRef]) { const newArgList = [...namedArgumentMapper[methodRef]];
for (const arg of newArgList) { if ( (arg.required && typeof body.params[arg.name] === 'undefined') || body.params[arg.name] === null ) { return { id: 1, jsonrpc: '2.0', error: { code: 1002, message: `Parameter ${arg.name} required by v2 API but not provided.`, data: { methodRef, schema: newArgList, }, }, }; } }
body.params = [...newArgList.map(({ name }) => body.params[name])]; }
// Certain backend methods only accept a hashref/hash-style method's (namedOnly) // single argument when it arrives as a one-element params array - sending the named // object directly as `params` makes it flatten the object's key/value pairs into // positional args instead, which crashes those methods (confirmed via curl against // /map/get_star_map: bare-object params throws "Can't use string (...) as a HASH ref"). // In this case we add the session ID into the same object as well, which you could call "combined" // However that said.. if ( namedOnlyArgumentListFirstPositionHashSet.has(methodRef) && !Array.isArray(body.params) && body.params !== null ) { body.params = [token ? { session_id: token, ...body.params } : body.params]; }
// .. certain methods want the session ID "separate" to the rest of the request. // And indeed.. else if ( namedOnlyArgumentListSecondPositionHashSet.has(methodRef) && !Array.isArray(body.params) && body.params !== null ) { // Always keep the hash in the second slot so a missing token fails as a session error // rather than the hash being read as the session ID. body.params = [token ?? null, body.params]; }
// .. there are cases when we need to support a bare hash being passed in as well! else if ( namedOnlyArgumentHashSet.has(methodRef) && !Array.isArray(body.params) && body.params !== null ) { body.params = token ? { session_id: token, ...body.params } : body.params; } else if (!!token && body.params !== null) { body.params = Array.isArray(body.params) ? [token, ...body.params] : { session_id: token, ...body.params }; }
return body; };
const body = prepareBody(req.body ?? {});
if ('error' in body) { console.error('Processing/validation error', body); return res.status(500).json(body); }
console.log(`hitting /${reqModule} with:`, body); try { const response = await fetch(`http://server:5000/${reqModule}`, { method: 'POST', headers: { // TODO pass real port through 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); const result = await response.json(); console.log('status', response.status);
res.status(response.status).json(result); } catch (e: unknown) { console.error('Unknown error', e); res.status(500).json({ id: 1, jsonrpc: '2.0', error: { code: 1002, message: 'An unknown server error occurred in the TLE infrastructure.', }, }); }});
app.listen(port, () => { console.log(`TLE Community v2 API app listening on port ${port}`);});