raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
281 lines • 15.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.matrixShutdownEpic = exports.initMatrixEpic = void 0;
const t = __importStar(require("io-ts"));
const sortBy_1 = __importDefault(require("lodash/sortBy"));
const matrix_js_sdk_1 = require("matrix-js-sdk");
const logger_1 = require("matrix-js-sdk/lib/logger");
const rxjs_1 = require("rxjs");
const fetch_1 = require("rxjs/fetch");
const operators_1 = require("rxjs/operators");
const config_1 = require("../../config");
const constants_1 = require("../../constants");
const utils_1 = require("../../services/utils");
const utils_2 = require("../../utils");
const error_1 = require("../../utils/error");
const matrix_1 = require("../../utils/matrix");
const rx_1 = require("../../utils/rx");
const types_1 = require("../../utils/types");
const actions_1 = require("../actions");
/**
* Creates and returns a matrix filter. The filter reduces the size of the initial sync by
* filtering out broadcast rooms, emphemeral messages like receipts etc.
*
* @param matrix - The {@link MatrixClient} instance used to create the filter.
* @param notRooms - The ids of the rooms to filter out during sync.
* @returns Observable of the {@link Filter} that was created.
*/
async function createMatrixFilter(matrix, notRooms = []) {
const roomFilter = {
not_rooms: notRooms,
ephemeral: {
not_types: ['m.receipt', 'm.typing'],
},
timeline: {
limit: 0,
not_senders: [matrix.getUserId()],
},
};
const filterDefinition = {
room: roomFilter,
};
return matrix.createFilter(filterDefinition);
}
function startMatrixSync(action$, matrix, { matrix$, config$, init$ }) {
return action$.pipe((0, operators_1.filter)(actions_1.matrixSetup.is), (0, operators_1.take)(1), (0, operators_1.tap)(() => {
matrix$.next(matrix);
matrix$.complete();
}), (0, operators_1.mergeMap)(() => (0, rxjs_1.defer)(async () => Promise.all([
createMatrixFilter(matrix),
matrix.setPushRuleEnabled('global', matrix_js_sdk_1.PushRuleKind.Override, '.m.rule.master', true),
])).pipe(
// delay startClient (going online) to after raiden is synced
(0, operators_1.delayWhen)(() => init$.pipe((0, operators_1.ignoreElements)(), (0, operators_1.endWith)(true))), (0, operators_1.mergeMap)(async ([filter]) => matrix.startClient({ filter })), (0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), { onErrors: error_1.networkErrors, maxRetries: 3 }))), (0, operators_1.ignoreElements)());
}
/**
* Given a server name (schema defaults to https:// and is prepended if missing), returns HTTP GET
* round trip time (time to response)
*
* @param server - Server name with or without schema
* @param httpTimeout - Optional timeout for the HTTP request
* @returns Promise to a { server, rtt } object, where `rtt` may be NaN
*/
function matrixRTT$(server, httpTimeout) {
if (!server.includes('://'))
server = 'https://' + server;
return (0, rxjs_1.defer)(() => {
const start = Date.now();
return (0, fetch_1.fromFetch)(server + '/_matrix/client/versions').pipe((0, operators_1.timeout)(httpTimeout), (0, operators_1.map)(({ ok }) => (ok ? Date.now() : NaN)), (0, operators_1.catchError)(() => (0, rxjs_1.of)(NaN)), (0, operators_1.map)((end) => ({ server, rtt: end - start })));
});
}
const MatrixServerInfo = t.type({
active_servers: t.array(t.string),
all_servers: t.array(t.string),
});
/**
* Returns an observable of servers, sorted by response time
*
* @param matrixServerLookup - URL containing an YAML list of servers url
* @param httpTimeout - httpTimeout to limit queries
* @returns Observable of { server, rtt } objects, emitted in increasing rtt order
*/
function fetchSortedMatrixServers$(matrixServerLookup, httpTimeout) {
return (0, fetch_1.fromFetch)(matrixServerLookup).pipe((0, operators_1.mergeMap)(async (response) => {
(0, utils_2.assert)(response.ok, `Could not fetch server list from "${matrixServerLookup}" => ${response.status}`);
return response.json();
}), (0, operators_1.timeout)(httpTimeout), (0, operators_1.mergeMap)((data) => (0, types_1.decode)(MatrixServerInfo, data).active_servers), (0, operators_1.mergeMap)((server) => matrixRTT$(server, httpTimeout)), (0, operators_1.toArray)(), (0, operators_1.mergeMap)((rtts) => (0, sortBy_1.default)(rtts, ['rtt'])), (0, operators_1.filter)(({ rtt }) => !isNaN(rtt)), (0, operators_1.throwIfEmpty)(() => new error_1.RaidenError(error_1.ErrorCodes.TRNS_NO_MATRIX_SERVERS)));
}
/**
* Validate and setup a MatrixClient connected to server, possibly using previous 'setup' data
* May error if anything goes wrong.
*
* @param server - server URL, with schema
* @param setup - optional previous setup/credentials data
* @param deps - RaidenEpicDeps-like/partial object
* @param deps.address - Our address (to compose matrix user)
* @param deps.signer - Signer to be used to sign password and displayName
* @param deps.config$ - Config observable
* @returns Observable of one { matrix, server, setup } object
*/
function setupMatrixClient$(server, setup, { address, signer, config$ }) {
const homeserver = (0, matrix_1.getServerName)(server);
(0, utils_2.assert)(homeserver, [error_1.ErrorCodes.TRNS_NO_SERVERNAME, { server }]);
return config$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(({ pollingInterval }) => {
if (setup) {
// if matrixSetup was already issued before, and credentials are already in state
const matrix = (0, matrix_js_sdk_1.createClient)({
baseUrl: server,
userId: setup.userId,
accessToken: setup.accessToken,
deviceId: setup.deviceId,
});
return (0, rxjs_1.of)({ matrix, server, setup, pollingInterval });
}
else {
const matrix = (0, matrix_js_sdk_1.createClient)({ baseUrl: server });
const username = address.toLowerCase();
const userId = `@${username}:${homeserver}`;
// create password as signature of serverName, then try login or register
return (0, rxjs_1.from)(signer.signMessage(homeserver)).pipe((0, operators_1.mergeMap)((password) => (0, rxjs_1.defer)(async () => matrix.login('m.login.password', {
identifier: { type: 'm.id.user', user: username },
password,
device_id: constants_1.RAIDEN_DEVICE_ID,
})).pipe((0, operators_1.catchError)(async (err) => {
const registerData = { username, password, device_id: constants_1.RAIDEN_DEVICE_ID };
try {
return await matrix.registerRequest(registerData);
}
catch (e) {
// if register fails, throws login error as it's more informative
throw err;
}
}), (0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), { onErrors: error_1.networkErrors, maxRetries: 3 }))), (0, operators_1.mergeMap)(({ access_token, device_id, user_id }) => {
(0, utils_2.assert)(user_id === userId, ['Wrong login/register user_id', { user_id, userId }]);
// matrix.register implementation doesn't set returned credentials
// which would require an unnecessary additional login request if we didn't
// set it here, and login doesn't set deviceId, so we set all credential
// parameters again here after successful login or register
matrix.deviceId = device_id;
matrix.http.opts.accessToken = access_token;
matrix.credentials = { userId };
// displayName must be signature of full userId for our messages to be accepted
return (0, rxjs_1.from)(signer.signMessage(userId)).pipe((0, operators_1.map)((signedUserId) => ({
matrix,
server,
setup: {
userId,
accessToken: access_token,
deviceId: device_id,
displayName: signedUserId,
},
})));
}));
}
}),
// the APIs below are authenticated, and therefore also act as validator
(0, operators_1.mergeMap)(({ matrix, server, setup }) =>
// set these properties before starting sync
(0, rxjs_1.defer)(async () => matrix.setDisplayName(setup.displayName)).pipe((0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), { onErrors: error_1.networkErrors }), (0, operators_1.mapTo)({ matrix, server, setup }))));
}
/**
* Initialize matrix transport
* The matrix client instance will be outputed to RaidenEpicDeps.matrix$ AsyncSubject
* The setup info (including credentials, for persistence) will be the matrixSetup output action
*
* @param action$ - Observable of RaidenActions
* @param state$ - Observable of RaidenStates
* @param deps - RaidenEpicDeps members
* @param deps.address - Our address
* @param deps.signer - Signer instance
* @param deps.matrix$ - MatrixClient async subject
* @param deps.latest$ - Latest observable
* @param deps.config$ - Config observable
* @param deps.init$ - Init$ tasks subject
* @returns Observable of matrixSetup generated by initializing matrix client
*/
function initMatrixEpic(action$, {}, deps) {
const { matrix$, latest$, config$, init$ } = deps;
return (0, rxjs_1.combineLatest)([latest$, config$]).pipe((0, operators_1.first)(), // at startup
(0, operators_1.mergeMap)(([{ state }, { matrixServer, matrixServerLookup, httpTimeout }]) => {
const server = state.transport.server, setup = state.transport.setup;
// when matrix$ async subject completes, transport init task is completed
init$.next(matrix$);
const servers$Array = [];
if (matrixServer) {
// if config.matrixServer is set, we must use it (possibly re-using stored credentials,
// if matching), not fetch from lookup address
if (matrixServer === server)
servers$Array.push((0, rxjs_1.of)({ server, setup }));
// even if same server, also append without setup to retry if auth fails
servers$Array.push((0, rxjs_1.of)({ server: matrixServer }));
}
else {
// previously used server
if (server)
servers$Array.push((0, rxjs_1.of)({ server, setup }));
// server from PFSs, will prefer/pick matrixServer compatible with explicit PFS
servers$Array.push((0, utils_1.choosePfs$)(undefined, deps, true).pipe((0, operators_1.map)(({ matrixServer: server }) => ({ server }))));
// fetched servers list
// notice it may include stored server again, but no stored setup, which could be the
// cause of the first failure, so we allow it to try again (not necessarily first)
servers$Array.push(fetchSortedMatrixServers$(matrixServerLookup, httpTimeout));
}
let lastError;
const andSuppress = (err) => ((lastError = err), rxjs_1.EMPTY);
// on [re-]subscription (defer), pops next observable and subscribe to it
return (0, rxjs_1.defer)(() => servers$Array.shift() || rxjs_1.EMPTY).pipe((0, operators_1.catchError)(andSuppress), // servers$ may error, so store lastError
// serially, try setting up client and validate its credential
(0, operators_1.concatMap)(({ server, setup }) =>
// store and suppress any 'setupMatrixClient$' error
setupMatrixClient$(server, setup, deps).pipe((0, operators_1.catchError)(andSuppress))),
// on first setupMatrixClient$'s success, emit, complete and unsubscribe
(0, operators_1.first)(), (0, operators_1.tap)(({ matrix }) => matrix.setMaxListeners(30)),
// with errors suppressed, only possible error here is 'no element in sequence'
(0, operators_1.retryWhen)((err$) =>
// if there're more servers$ observables in queue, emit once to retry from defer;
// else, errors output with lastError to unsubscribe
err$.pipe((0, operators_1.mergeMap)(() => {
if (servers$Array.length)
return (0, rxjs_1.of)(null);
throw lastError;
}))));
}),
// on success
(0, operators_1.mergeMap)(({ matrix, server, setup }) => (0, rxjs_1.merge)(
// wait for matrixSetup through reducer, then resolves matrix$ with client and starts it
startMatrixSync(action$, matrix, deps),
// emit matrixSetup in parallel to be persisted in state
(0, rxjs_1.of)((0, actions_1.matrixSetup)({ server, setup })),
// monitor config.logger & disable or re-enable matrix's logger accordingly
config$.pipe((0, rx_1.pluckDistinct)('logger'), (0, operators_1.tap)((logger) => logger_1.logger.setLevel(logger || 'silent', false)), (0, operators_1.ignoreElements)()))), (0, rx_1.completeWith)(action$));
}
exports.initMatrixEpic = initMatrixEpic;
/**
* Calls matrix.stopClient when raiden is shutting down, i.e. action$ completes
*
* @param action$ - Observable of matrixSetup actions
* @param state$ - Observable of RaidenStates
* @param deps - RaidenEpicDeps members
* @param deps.matrix$ - MatrixClient async subject
* @returns Empty observable (whole side-effect on matrix instance)
*/
function matrixShutdownEpic(action$, {}, { matrix$ }) {
return action$.pipe((0, operators_1.withLatestFrom)(matrix$), (0, rx_1.lastMap)(async (pair) => {
if (!pair)
return;
const matrix = pair[1];
matrix.stopClient();
try {
await matrix.setPresence({ presence: 'offline', status_msg: '' });
}
catch (err) { }
}), (0, operators_1.ignoreElements)());
}
exports.matrixShutdownEpic = matrixShutdownEpic;
//# sourceMappingURL=init.js.map