UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

281 lines 15.2 kB
"use strict"; 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