moeralib
Version:
Library to interact with Moera decentralized social network
379 lines (378 loc) • 12.9 kB
JavaScript
;
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.moeraRoot = exports.MoeraNodeCallError = exports.MoeraNodeConnectionError = exports.MoeraNodeApiError = exports.MoeraNodeError = void 0;
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.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);
}
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.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);
}
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;
}
exports.moeraRoot = moeraRoot;
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 = null;
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;