diff --git a/src/autodiscovery.js b/src/autodiscovery.ts similarity index 74% rename from src/autodiscovery.js rename to src/autodiscovery.ts index 1ccbbaa86..19d2aa9f8 100644 --- a/src/autodiscovery.js +++ b/src/autodiscovery.ts @@ -17,79 +17,19 @@ limitations under the License. /** @module auto-discovery */ +import { IClientWellKnown, IWellKnownConfig } from "./client"; import { logger } from './logger'; import { URL as NodeURL } from "url"; // Dev note: Auto discovery is part of the spec. // See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery -/** - * Description for what an automatically discovered client configuration - * would look like. Although this is a class, it is recommended that it - * be treated as an interface definition rather than as a class. - * - * Additional properties than those defined here may be present, and - * should follow the Java package naming convention. - */ -class DiscoveredClientConfig { // eslint-disable-line no-unused-vars - // Dev note: this is basically a copy/paste of the .well-known response - // object as defined in the spec. It does have additional information, - // however. Overall, this exists to serve as a place for documentation - // and not functionality. - // See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client - - constructor() { - /** - * The homeserver configuration the client should use. This will - * always be present on the object. - * @type {{state: string, base_url: string}} The configuration. - */ - this["m.homeserver"] = { - /** - * The lookup result state. If this is anything other than - * AutoDiscovery.SUCCESS then base_url may be falsey. Additionally, - * if this is not AutoDiscovery.SUCCESS then the client should - * assume the other properties in the client config (such as - * the identity server configuration) are not valid. - */ - state: AutoDiscovery.PROMPT, - - /** - * If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT - * then this will contain a human-readable (English) message - * for what went wrong. If the state is none of those previously - * mentioned, this will be falsey. - */ - error: "Something went wrong", - - /** - * The base URL clients should use to talk to the homeserver, - * particularly for the login process. May be falsey if the - * state is not AutoDiscovery.SUCCESS. - */ - base_url: "https://matrix.org", - }; - - /** - * The identity server configuration the client should use. This - * will always be present on teh object. - * @type {{state: string, base_url: string}} The configuration. - */ - this["m.identity_server"] = { - /** - * The lookup result state. If this is anything other than - * AutoDiscovery.SUCCESS then base_url may be falsey. - */ - state: AutoDiscovery.PROMPT, - - /** - * The base URL clients should use for interacting with the - * identity server. May be falsey if the state is not - * AutoDiscovery.SUCCESS. - */ - base_url: "https://vector.im", - }; - } +export enum AutoDiscoveryAction { + SUCCESS = "SUCCESS", + IGNORE = "IGNORE", + PROMPT = "PROMPT", + FAIL_PROMPT = "FAIL_PROMPT", + FAIL_ERROR = "FAIL_ERROR", } /** @@ -102,55 +42,36 @@ export class AutoDiscovery { // translate the meaning of the states in the spec, but also // support our own if needed. - static get ERROR_INVALID() { - return "Invalid homeserver discovery response"; - } + public static readonly ERROR_INVALID = "Invalid homeserver discovery response"; - static get ERROR_GENERIC_FAILURE() { - return "Failed to get autodiscovery configuration from server"; - } + public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server"; - static get ERROR_INVALID_HS_BASE_URL() { - return "Invalid base_url for m.homeserver"; - } + public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver"; - static get ERROR_INVALID_HOMESERVER() { - return "Homeserver URL does not appear to be a valid Matrix homeserver"; - } + public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver"; - static get ERROR_INVALID_IS_BASE_URL() { - return "Invalid base_url for m.identity_server"; - } + public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server"; - static get ERROR_INVALID_IDENTITY_SERVER() { - return "Identity server URL does not appear to be a valid identity server"; - } + // eslint-disable-next-line + public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server"; - static get ERROR_INVALID_IS() { - return "Invalid identity server discovery response"; - } + public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response"; - static get ERROR_MISSING_WELLKNOWN() { - return "No .well-known JSON file found"; - } + public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found"; - static get ERROR_INVALID_JSON() { - return "Invalid JSON"; - } + public static readonly ERROR_INVALID_JSON = "Invalid JSON"; - static get ALL_ERRORS() { - return [ - AutoDiscovery.ERROR_INVALID, - AutoDiscovery.ERROR_GENERIC_FAILURE, - AutoDiscovery.ERROR_INVALID_HS_BASE_URL, - AutoDiscovery.ERROR_INVALID_HOMESERVER, - AutoDiscovery.ERROR_INVALID_IS_BASE_URL, - AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, - AutoDiscovery.ERROR_INVALID_IS, - AutoDiscovery.ERROR_MISSING_WELLKNOWN, - AutoDiscovery.ERROR_INVALID_JSON, - ]; - } + public static readonly ALL_ERRORS = [ + AutoDiscovery.ERROR_INVALID, + AutoDiscovery.ERROR_GENERIC_FAILURE, + AutoDiscovery.ERROR_INVALID_HS_BASE_URL, + AutoDiscovery.ERROR_INVALID_HOMESERVER, + AutoDiscovery.ERROR_INVALID_IS_BASE_URL, + AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, + AutoDiscovery.ERROR_INVALID_IS, + AutoDiscovery.ERROR_MISSING_WELLKNOWN, + AutoDiscovery.ERROR_INVALID_JSON, + ]; /** * The auto discovery failed. The client is expected to communicate @@ -158,7 +79,7 @@ export class AutoDiscovery { * @return {string} * @constructor */ - static get FAIL_ERROR() { return "FAIL_ERROR"; } + public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR; /** * The auto discovery failed, however the client may still recover @@ -169,7 +90,7 @@ export class AutoDiscovery { * @return {string} * @constructor */ - static get FAIL_PROMPT() { return "FAIL_PROMPT"; } + public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT; /** * The auto discovery didn't fail but did not find anything of @@ -178,14 +99,14 @@ export class AutoDiscovery { * @return {string} * @constructor */ - static get PROMPT() { return "PROMPT"; } + public static readonly PROMPT = AutoDiscoveryAction.PROMPT; /** * The auto discovery was successful. * @return {string} * @constructor */ - static get SUCCESS() { return "SUCCESS"; } + public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS; /** * Validates and verifies client configuration information for purposes @@ -199,7 +120,7 @@ export class AutoDiscovery { * configuration, which may include error states. Rejects on unexpected * failure, not when verification fails. */ - static async fromDiscoveryConfig(wellknown) { + public static async fromDiscoveryConfig(wellknown: string): Promise { // Step 1 is to get the config, which is provided to us here. // We default to an error state to make the first few checks easier to @@ -240,7 +161,7 @@ export class AutoDiscovery { // Step 2: Make sure the homeserver URL is valid *looking*. We'll make // sure it points to a homeserver in Step 3. - const hsUrl = this._sanitizeWellKnownUrl( + const hsUrl = this.sanitizeWellKnownUrl( wellknown["m.homeserver"]["base_url"], ); if (!hsUrl) { @@ -250,7 +171,7 @@ export class AutoDiscovery { } // Step 3: Make sure the homeserver URL points to a homeserver. - const hsVersions = await this._fetchWellKnownObject( + const hsVersions = await this.fetchWellKnownObject( `${hsUrl}/_matrix/client/versions`, ); if (!hsVersions || !hsVersions.raw["versions"]) { @@ -272,7 +193,7 @@ export class AutoDiscovery { }; // Step 5: Try to pull out the identity server configuration - let isUrl = ""; + let isUrl: string | boolean = ""; if (wellknown["m.identity_server"]) { // We prepare a failing identity server response to save lines later // in this branch. @@ -287,7 +208,7 @@ export class AutoDiscovery { // Step 5a: Make sure the URL is valid *looking*. We'll make sure it // points to an identity server in Step 5b. - isUrl = this._sanitizeWellKnownUrl( + isUrl = this.sanitizeWellKnownUrl( wellknown["m.identity_server"]["base_url"], ); if (!isUrl) { @@ -299,10 +220,10 @@ export class AutoDiscovery { // Step 5b: Verify there is an identity server listening on the provided // URL. - const isResponse = await this._fetchWellKnownObject( + const isResponse = await this.fetchWellKnownObject( `${isUrl}/_matrix/identity/api/v1`, ); - if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") { + if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) { logger.error("Invalid /api/v1 response"); failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; @@ -317,7 +238,7 @@ export class AutoDiscovery { // Step 6: Now that the identity server is valid, or never existed, // populate the IS section. - if (isUrl && isUrl.length > 0) { + if (isUrl && isUrl.toString().length > 0) { clientConfig["m.identity_server"] = { state: AutoDiscovery.SUCCESS, error: null, @@ -359,7 +280,7 @@ export class AutoDiscovery { * configuration, which may include error states. Rejects on unexpected * failure, not when discovery fails. */ - static async findClientConfig(domain) { + public static async findClientConfig(domain: string): Promise { if (!domain || typeof(domain) !== "string" || domain.length === 0) { throw new Error("'domain' must be a string of non-zero length"); } @@ -395,13 +316,13 @@ export class AutoDiscovery { // Step 1: Actually request the .well-known JSON file and make sure it // at least has a homeserver definition. - const wellknown = await this._fetchWellKnownObject( + const wellknown = await this.fetchWellKnownObject( `https://${domain}/.well-known/matrix/client`, ); - if (!wellknown || wellknown.action !== "SUCCESS") { + if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) { logger.error("No response or error when parsing .well-known"); if (wellknown.reason) logger.error(wellknown.reason); - if (wellknown.action === "IGNORE") { + if (wellknown.action === AutoDiscoveryAction.IGNORE) { clientConfig["m.homeserver"] = { state: AutoDiscovery.PROMPT, error: null, @@ -427,12 +348,12 @@ export class AutoDiscovery { * @returns {Promise} Resolves to the domain's client config. Can * be an empty object. */ - static async getRawClientConfig(domain) { + public static async getRawClientConfig(domain: string): Promise { if (!domain || typeof(domain) !== "string" || domain.length === 0) { throw new Error("'domain' must be a string of non-zero length"); } - const response = await this._fetchWellKnownObject( + const response = await this.fetchWellKnownObject( `https://${domain}/.well-known/matrix/client`, ); if (!response) return {}; @@ -447,7 +368,7 @@ export class AutoDiscovery { * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid. * @private */ - static _sanitizeWellKnownUrl(url) { + private static sanitizeWellKnownUrl(url: string): string | boolean { if (!url) return false; try { @@ -495,8 +416,9 @@ export class AutoDiscovery { * @return {Promise} Resolves to the returned state. * @private */ - static async _fetchWellKnownObject(url) { + private static async fetchWellKnownObject(url: string): Promise { return new Promise(function(resolve, reject) { + // eslint-disable-next-line const request = require("./matrix").getRequest(); if (!request) throw new Error("No request library available"); request( @@ -505,10 +427,10 @@ export class AutoDiscovery { if (err || response && (response.statusCode < 200 || response.statusCode >= 300) ) { - let action = "FAIL_PROMPT"; + let action = AutoDiscoveryAction.FAIL_PROMPT; let reason = (err ? err.message : null) || "General failure"; if (response && response.statusCode === 404) { - action = "IGNORE"; + action = AutoDiscoveryAction.IGNORE; reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN; } resolve({ raw: {}, action: action, reason: reason, error: err }); @@ -516,7 +438,7 @@ export class AutoDiscovery { } try { - resolve({ raw: JSON.parse(body), action: "SUCCESS" }); + resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS }); } catch (e) { let reason = AutoDiscovery.ERROR_INVALID; if (e.name === "SyntaxError") { @@ -524,7 +446,7 @@ export class AutoDiscovery { } resolve({ raw: {}, - action: "FAIL_PROMPT", + action: AutoDiscoveryAction.FAIL_PROMPT, reason: reason, error: e, }); diff --git a/src/client.ts b/src/client.ts index 76b40097f..52649965d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -31,7 +31,7 @@ import { sleep } from './utils'; import { Group } from "./models/group"; import { Direction, EventTimeline } from "./models/event-timeline"; import { IActionsObject, PushProcessor } from "./pushprocessor"; -import { AutoDiscovery } from "./autodiscovery"; +import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery"; import * as olmlib from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice"; @@ -476,14 +476,19 @@ interface IServerVersions { unstable_features: Record; } -interface IClientWellKnown { +export interface IClientWellKnown { [key: string]: any; - "m.homeserver": { - base_url: string; - }; - "m.identity_server"?: { - base_url: string; - }; + "m.homeserver"?: IWellKnownConfig; + "m.identity_server"?: IWellKnownConfig; +} + +export interface IWellKnownConfig { + raw?: any; // todo typings + action?: AutoDiscoveryAction; + reason?: string; + error?: Error | string; + // eslint-disable-next-line + base_url?: string | null; } interface IKeyBackupPath { diff --git a/src/errors.js b/src/errors.ts similarity index 71% rename from src/errors.js rename to src/errors.ts index 186fc6911..324e04c46 100644 --- a/src/errors.js +++ b/src/errors.ts @@ -1,6 +1,6 @@ // can't just do InvalidStoreError extends Error // because of http://babeljs.io/docs/usage/caveats/#classes -export function InvalidStoreError(reason, value) { +export function InvalidStoreError(reason: string, value: boolean): void { const message = `Store is invalid because ${reason}, ` + `please stop the client, delete all data and start the client again`; const instance = Reflect.construct(Error, [message]); @@ -13,16 +13,16 @@ export function InvalidStoreError(reason, value) { InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING"; InvalidStoreError.prototype = Object.create(Error.prototype, { - constructor: { - value: Error, - enumerable: false, - writable: true, - configurable: true, - }, + constructor: { + value: Error, + enumerable: false, + writable: true, + configurable: true, + }, }); Reflect.setPrototypeOf(InvalidStoreError, Error); -export function InvalidCryptoStoreError(reason) { +export function InvalidCryptoStoreError(reason: string): void { const message = `Crypto store is invalid because ${reason}, ` + `please stop the client, delete all data and start the client again`; const instance = Reflect.construct(Error, [message]); @@ -35,18 +35,17 @@ export function InvalidCryptoStoreError(reason) { InvalidCryptoStoreError.TOO_NEW = "TOO_NEW"; InvalidCryptoStoreError.prototype = Object.create(Error.prototype, { - constructor: { - value: Error, - enumerable: false, - writable: true, - configurable: true, - }, + constructor: { + value: Error, + enumerable: false, + writable: true, + configurable: true, + }, }); Reflect.setPrototypeOf(InvalidCryptoStoreError, Error); export class KeySignatureUploadError extends Error { - constructor(message, value) { - super(message); - this.value = value; - } + constructor(message: string, public value: { failures: any }) { // TODO: types + super(message); + } } diff --git a/src/realtime-callbacks.js b/src/realtime-callbacks.ts similarity index 68% rename from src/realtime-callbacks.js rename to src/realtime-callbacks.ts index 5fa2008ca..68e41be53 100644 --- a/src/realtime-callbacks.js +++ b/src/realtime-callbacks.ts @@ -31,17 +31,22 @@ import { logger } from './logger'; const TIMER_CHECK_PERIOD_MS = 1000; // counter, for making up ids to return from setTimeout -let _count = 0; +let count = 0; // the key for our callback with the real global.setTimeout -let _realCallbackKey; +let realCallbackKey: NodeJS.Timeout | number; // a sorted list of the callbacks to be run. // each is an object with keys [runAt, func, params, key]. -const _callbackList = []; +const callbackList: { + runAt: number; + func: (...params: any[]) => void; + params: any[]; + key: number; +}[] = []; // var debuglog = logger.log.bind(logger); -const debuglog = function() {}; +const debuglog = function(...params: any[]) {}; /** * Replace the function used by this module to get the current time. @@ -52,10 +57,10 @@ const debuglog = function() {}; * * @internal */ -export function setNow(f) { - _now = f || Date.now; +export function setNow(f: () => number): void { + now = f || Date.now; } -let _now = Date.now; +let now = Date.now; /** * reimplementation of window.setTimeout, which will call the callback if @@ -67,17 +72,16 @@ let _now = Date.now; * @return {Number} an identifier for this callback, which may be passed into * clearTimeout later. */ -export function setTimeout(func, delayMs) { +export function setTimeout(func: (...params: any[]) => void, delayMs: number, ...params: any[]): number { delayMs = delayMs || 0; if (delayMs < 0) { delayMs = 0; } - const params = Array.prototype.slice.call(arguments, 2); - const runAt = _now() + delayMs; - const key = _count++; + const runAt = now() + delayMs; + const key = count++; debuglog("setTimeout: scheduling cb", key, "at", runAt, - "(delay", delayMs, ")"); + "(delay", delayMs, ")"); const data = { runAt: runAt, func: func, @@ -87,13 +91,13 @@ export function setTimeout(func, delayMs) { // figure out where it goes in the list const idx = binarySearch( - _callbackList, function(el) { + callbackList, function(el) { return el.runAt - runAt; }, ); - _callbackList.splice(idx, 0, data); - _scheduleRealCallback(); + callbackList.splice(idx, 0, data); + scheduleRealCallback(); return key; } @@ -103,68 +107,69 @@ export function setTimeout(func, delayMs) { * * @param {Number} key result from an earlier setTimeout call */ -export function clearTimeout(key) { - if (_callbackList.length === 0) { +export function clearTimeout(key: number): void { + if (callbackList.length === 0) { return; } // remove the element from the list let i; - for (i = 0; i < _callbackList.length; i++) { - const cb = _callbackList[i]; + for (i = 0; i < callbackList.length; i++) { + const cb = callbackList[i]; if (cb.key == key) { - _callbackList.splice(i, 1); + callbackList.splice(i, 1); break; } } // iff it was the first one in the list, reschedule our callback. if (i === 0) { - _scheduleRealCallback(); + scheduleRealCallback(); } } -// use the real global.setTimeout to schedule a callback to _runCallbacks. -function _scheduleRealCallback() { - if (_realCallbackKey) { - global.clearTimeout(_realCallbackKey); +// use the real global.setTimeout to schedule a callback to runCallbacks. +function scheduleRealCallback(): void { + if (realCallbackKey) { + global.clearTimeout(realCallbackKey as NodeJS.Timeout); } - const first = _callbackList[0]; + const first = callbackList[0]; if (!first) { - debuglog("_scheduleRealCallback: no more callbacks, not rescheduling"); + debuglog("scheduleRealCallback: no more callbacks, not rescheduling"); return; } - const now = _now(); - const delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS); + const timestamp = now(); + const delayMs = Math.min(first.runAt - timestamp, TIMER_CHECK_PERIOD_MS); - debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs); - _realCallbackKey = global.setTimeout(_runCallbacks, delayMs); + debuglog("scheduleRealCallback: now:", timestamp, "delay:", delayMs); + realCallbackKey = global.setTimeout(runCallbacks, delayMs); } -function _runCallbacks() { +function runCallbacks(): void { let cb; - const now = _now(); - debuglog("_runCallbacks: now:", now); + const timestamp = now(); + debuglog("runCallbacks: now:", timestamp); // get the list of things to call const callbacksToRun = []; + // eslint-disable-next-line while (true) { - const first = _callbackList[0]; - if (!first || first.runAt > now) { + const first = callbackList[0]; + if (!first || first.runAt > timestamp) { break; } - cb = _callbackList.shift(); - debuglog("_runCallbacks: popping", cb.key); + cb = callbackList.shift(); + debuglog("runCallbacks: popping", cb.key); callbacksToRun.push(cb); } // reschedule the real callback before running our functions, to // keep the codepaths the same whether or not our functions // register their own setTimeouts. - _scheduleRealCallback(); + scheduleRealCallback(); for (let i = 0; i < callbacksToRun.length; i++) { cb = callbacksToRun[i]; @@ -172,7 +177,7 @@ function _runCallbacks() { cb.func.apply(global, cb.params); } catch (e) { logger.error("Uncaught exception in callback function", - e.stack || e); + e.stack || e); } } } @@ -182,7 +187,7 @@ function _runCallbacks() { * returns the index of the last element for which func returns * greater than zero, or array.length if no such element exists. */ -function binarySearch(array, func) { +function binarySearch(array: T[], func: (v: T) => number): number { // min is inclusive, max exclusive. let min = 0; let max = array.length;