UNPKG

moeralib

Version:

Library to interact with Moera decentralized social network

391 lines (390 loc) 13.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Caller = exports.MoeraNodeCallError = exports.MoeraNodeConnectionError = exports.MoeraNodeApiError = exports.MoeraNodeError = void 0; exports.moeraRoot = moeraRoot; const lodash_clonedeep_1 = __importDefault(require("lodash.clonedeep")); const validate_1 = require("./validate"); const util_1 = require("../util"); const schema_1 = require("../schema"); /** * Generic node error. */ class MoeraNodeError extends Error { /** * @param {string} name - request name * @param {string} message - error message */ constructor(name, message) { super(name + ": Node error: " + message); } } exports.MoeraNodeError = MoeraNodeError; /** * Node returned an error response. */ class MoeraNodeApiError extends MoeraNodeError { /** * @param {string} name - request name * @param {Result} result - node response */ constructor(name, result) { super(name, result.message); this.errorCode = result.errorCode; } } exports.MoeraNodeApiError = MoeraNodeApiError; /** * Error while connecting the node. */ class MoeraNodeConnectionError extends Error { /** * @param {string} message - error message */ constructor(message) { super("Node connection error: " + message); } } exports.MoeraNodeConnectionError = MoeraNodeConnectionError; /** * Missing context of the call (authentication parameters or node URL). */ class MoeraNodeCallError extends Error { /** * @param {string} message - error message */ constructor(message) { super(message); } } exports.MoeraNodeCallError = MoeraNodeCallError; function encodeBody(decoded, format) { var _a; if (decoded == null || typeof decoded === "string") { return decoded; } if (format === "application") { return (_a = decoded.text) !== null && _a !== void 0 ? _a : null; } return JSON.stringify(decoded); } function encodeBodies(data) { if (Array.isArray(data)) { return data.map(item => encodeBodies(item)); } const encoded = (0, lodash_clonedeep_1.default)(data); if (data.mediaCaptions != null) { encoded.mediaCaptions = data.mediaCaptions.map((item) => encodeBodies(item)); } if (data.body != null) { encoded.body = encodeBody(data.body, data.bodyFormat); } if (data.bodyPreview != null) { encoded.bodyPreview = encodeBody(data.bodyPreview, data.bodyFormat); } if (data.bodySrc != null) { encoded.bodySrc = encodeBody(data.bodySrc, data.bodySrcFormat); } if (data.captionSrc != null) { encoded.captionSrc = encodeBody(data.captionSrc, data.captionSrcFormat); } return encoded; } function decodeBody(name, encoded, format) { if (format === "application") { return { text: encoded }; } let body; try { body = JSON.parse(encoded); } catch (e) { if (e instanceof SyntaxError) { throw new MoeraNodeError(name, "Invalid body encoding: " + e.message); } throw e; } const { valid, errors } = (0, validate_1.validateSchema)("Body", body); if (!valid) { throw new MoeraNodeError(name, "Invalid body: " + (0, schema_1.formatSchemaErrors)(errors)); } return body; } function decodeBodies(name, data) { if (Array.isArray(data)) { return data.map(item => decodeBodies(name, item)); } const decoded = (0, lodash_clonedeep_1.default)(data); if (data.stories != null) { decoded.stories = data.stories.map((item) => decodeBodies(name, item)); } if (data.comments != null) { decoded.comments = data.comments.map((item) => decodeBodies(name, item)); } if (data.mediaCaptions != null) { decoded.mediaCaptions = data.mediaCaptions.map((item) => decodeBodies(name, item)); } if (data.comment != null) { decoded.comment = decodeBodies(name, data.comment); } if (data.posting != null) { decoded.posting = decodeBodies(name, data.posting); } if (data.body != null) { decoded.body = decodeBody(name, data.body, data.bodyFormat); } if (data.bodyPreview != null) { decoded.bodyPreview = decodeBody(name, data.bodyPreview, data.bodyFormat); } if (data.bodySrc != null) { decoded.bodySrc = decodeBody(name, data.bodySrc, data.bodySrcFormat); } if (data.captionSrc != null) { decoded.captionSrc = decodeBody(name, data.captionSrc, data.captionSrcFormat); } return decoded; } /** * Convert partial node URL to a standardized form. * * @param {string} url - partial URL * @return {string} standard URL */ function moeraRoot(url) { if (!url.startsWith("http:") && !url.startsWith("https:")) { url = "https://" + url; } if (url.endsWith("/")) { url = url.slice(0, -1); } if (url.endsWith("/api")) { url = url.slice(0, -4); } if (!url.endsWith("/moera")) { url += "/moera"; } return url; } const FETCH_TIMEOUT = 10000; // ms const UPDATE_TIMEOUT = 60000; // ms const LARGE_BODY_MIN = 65536; function abortSignal(method, body) { let signal; if (method === "POST" || method === "PUT") { const largeBody = body != null && (typeof body === "string" ? body.length > LARGE_BODY_MIN : true); signal = AbortSignal.timeout(largeBody ? UPDATE_TIMEOUT : FETCH_TIMEOUT); } else { signal = AbortSignal.timeout(FETCH_TIMEOUT); } return signal; } class Caller { constructor() { /** * API endpoint URL of the node. */ this.root = null; this._rootSecret = null; this._token = null; this._carte = null; this._carteSource = null; this._authMethod = "none"; } /** * Set node URL. * * @param {string} url */ nodeUrl(url) { this.root = moeraRoot(url); } /** * Set root secret for authentication. * * @param {string} secret */ rootSecret(secret) { this._rootSecret = secret; } /** * Set admin token for authentication. * * @param {string} token */ token(token) { this._token = token; } /** * Set carte for authentication. * * @param {string} carte */ carte(carte) { this._carte = carte; } /** * Set a source of cartes for authentication. * * @param {CarteSource} carteSource */ carteSource(carteSource) { this._carteSource = carteSource; } /** * Select authentication method for the following requests. * * @param {NodeAuth} authMethod */ authMethod(authMethod) { this._authMethod = authMethod; } /** * Switch off authentication for the following requests. */ noAuth() { this.authMethod("none"); } /** * Select carte authentication for the following requests. */ auth() { this.authMethod("peer"); } /** * Select admin token authentication for the following requests. */ authAdmin() { this.authMethod("admin"); } /** * Select root admin secret authentication for the following requests. */ authRootAdmin() { this.authMethod("root-admin"); } /** * Generic method for making node API requests. * * @param {string} name - request name (for error messages) * @param {string} location - request path * @param {Partial<Record<string, string | number | boolean | null>> | null} params - query parameters, mapping * name to value, ``null`` values are skipped * @param {string} method - request method (one of 'GET', 'POST', 'PUT', 'DELETE'), the default is 'GET' * @param {Structure | Structure[] | Buffer | null} body - request body * @param {string | null} contentType - content-type of the request body, when it is not a JSON structure * @param {boolean} auth - `true` (default) to authenticate the request, `false` otherwise * @param {string} schema - JSON schema to validate the response * @param {boolean} bodies - `true` to decode `Body` structures in the response, `false` (default) otherwise * @param {boolean} srcBodies - `true` to encode `Body` structures in the request, `false` (default) otherwise * @return {Promise<any>} */ call(name_1, location_1, _a) { return __awaiter(this, arguments, void 0, function* (name, location, { params = null, method = "GET", body = null, contentType = null, auth = true, schema, bodies = false, srcBodies = false }) { let bodyEncoded; if (body != null && !Buffer.isBuffer(body)) { if (srcBodies) { body = encodeBodies(body); } bodyEncoded = JSON.stringify(body); } else { bodyEncoded = body; } const headers = { "Accept": "application/json", "Content-Type": contentType !== null && contentType !== void 0 ? contentType : "application/json" }; let bearer = null; if (auth) { switch (this._authMethod) { case "peer": if (this._carteSource != null) { bearer = "carte:" + (yield this._carteSource.getCarte()); } else if (this._carte != null) { bearer = "carte:" + this._carte; } else { throw new MoeraNodeCallError("Carte is not set"); } break; case "admin": if (this._token == null) { throw new MoeraNodeCallError("Token is not set"); } bearer = "token:" + this._token; break; case "root-admin": if (this._rootSecret == null) { throw new MoeraNodeCallError("Root secret is not set"); } bearer = "secret:" + this._rootSecret; break; } } if (bearer != null) { headers["Authorization"] = `Bearer ${bearer}`; } if (this.root == null) { throw new MoeraNodeCallError("Node URL is not set"); } const url = (0, util_1.urlWithParameters)(this.root + "/api" + location, params); const signal = abortSignal(method, bodyEncoded); let response; try { response = yield fetch(url, { method, headers, body: bodyEncoded, signal }); } catch (e) { throw new MoeraNodeConnectionError(String(e)); } let data; try { if (schema === "blob" && response.ok) { data = yield response.blob(); } else { data = yield response.json(); } } catch (e) { if (!response.ok) { throw new MoeraNodeError(name, "Server returned error status"); } else { throw new MoeraNodeError(name, "Server returned empty result"); } } if (!response.ok) { const { valid } = (0, validate_1.validateSchema)("Result", data); if (!valid) { throw new MoeraNodeError(name, "Server returned error status"); } throw new MoeraNodeApiError(name, data); } if (schema !== "blob") { const result = (0, validate_1.validateSchema)(schema, data); const { valid, errors } = result; if (!valid) { throw new MoeraNodeError(name, "Server returned incorrect response" + (0, schema_1.formatSchemaErrors)(errors)); } if (bodies) { data = decodeBodies(name, data); } } return data; }); } } exports.Caller = Caller;