From 0d316e3d3e1c9f4e3f45a293962ce55671e67de7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 26 May 2021 15:00:43 -0600 Subject: [PATCH 01/32] Move useful docs to ICreateClientOpts --- src/@types/IIdentityServerProvider.ts | 24 +++++ src/matrix.ts | 121 +++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 src/@types/IIdentityServerProvider.ts diff --git a/src/@types/IIdentityServerProvider.ts b/src/@types/IIdentityServerProvider.ts new file mode 100644 index 00000000000..7b905e316b3 --- /dev/null +++ b/src/@types/IIdentityServerProvider.ts @@ -0,0 +1,24 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface IIdentityServerProvider { + /** + * Gets an access token for use against the identity server, + * for the associated client. + * @returns {Promise} Resolves to the access token. + */ + getAccessToken(): Promise; +} diff --git a/src/matrix.ts b/src/matrix.ts index 92f374a1607..fb818fe1d4b 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -27,6 +27,7 @@ import { LocalIndexedDBStoreBackend } from "./store/indexeddb-local-backend"; import { RemoteIndexedDBStoreBackend } from "./store/indexeddb-remote-backend"; import { MatrixScheduler } from "./scheduler"; import { MatrixClient } from "./client"; +import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; export * from "./client"; export * from "./http-api"; @@ -113,19 +114,95 @@ export function setCryptoStoreFactory(fac) { export interface ICreateClientOpts { baseUrl: string; + idBaseUrl?: string; + + /** + * The data store used for sync data from the homeserver. If not specified, + * this client will not store any HTTP responses. The `createClient` helper + * will create a default store if needed. + */ store?: Store; + + /** + * A store to be used for end-to-end crypto session data. If not specified, + * end-to-end crypto will be disabled. The `createClient` helper will create + * a default store if needed. + */ cryptoStore?: CryptoStore; + + /** + * The scheduler to use. If not + * specified, this client will not retry requests on failure. This client + * will supply its own processing function to + * {@link module:scheduler~MatrixScheduler#setProcessFunction}. + */ scheduler?: MatrixScheduler; + + /** + * The function to invoke for HTTP + * requests. The value of this property is typically require("request") + * as it returns a function which meets the required interface. See + * {@link requestFunction} for more information. + */ request?: Request; + userId?: string; + + /** + * A unique identifier for this device; used for tracking things like crypto + * keys and access tokens. If not specified, end-to-end encryption will be + * disabled. + */ deviceId?: string; + accessToken?: string; - identityServer?: any; + + /** + * Identity server provider to retrieve the user's access token when accessing + * the identity server. See also https://github.com/vector-im/element-web/issues/10615 + * which seeks to replace the previous approach of manual access tokens params + * with this callback throughout the SDK. + */ + identityServer?: IIdentityServerProvider; + + /** + * The default maximum amount of + * time to wait before timing out HTTP requests. If not specified, there is no timeout. + */ localTimeoutMs?: number; + + /** + * Set to true to use + * Authorization header instead of query param to send the access token to the server. + * + * Default false. + */ useAuthorizationHeader?: boolean; + + /** + * Set to true to enable + * improved timeline support ({@link module:client~MatrixClient#getEventTimeline getEventTimeline}). It is + * disabled by default for compatibility with older clients - in particular to + * maintain support for back-paginating the live timeline after a '/sync' + * result with a gap. + */ timelineSupport?: boolean; + + /** + * Extra query parameters to append + * to all requests with this client. Useful for application services which require + * ?user_id=. + */ queryParams?: Record; + + /** + * Device data exported with + * "exportDevice" method that must be imported to recreate this device. + * Should only be useful for devices with end-to-end crypto enabled. + * If provided, deviceId and userId should **NOT** be provided at the top + * level (they are present in the exported data). + */ deviceToImport?: { olmDevice: { pickledAccount: string; @@ -135,14 +212,52 @@ export interface ICreateClientOpts { userId: string; deviceId: string; }; + + /** + * Key used to pickle olm objects or other sensitive data. + */ pickleKey?: string; + + /** + * A store to be used for end-to-end crypto session data. Most data has been + * migrated out of here to `cryptoStore` instead. If not specified, + * end-to-end crypto will be disabled. The `createClient` helper + * _will not_ create this store at the moment. + */ sessionStore?: any; + + /** + * Set to true to enable client-side aggregation of event relations + * via `EventTimelineSet#getRelationsForEvent`. + * This feature is currently unstable and the API may change without notice. + */ unstableClientRelationAggregation?: boolean; + verificationMethods?: Array; + + /** + * Whether relaying calls through a TURN server should be forced. Default false. + */ forceTURN?: boolean; - iceCandidatePoolSize?: number, - supportsCallTransfer?: boolean, + + /** + * Up to this many ICE candidates will be gathered when an incoming call arrives. + * Gathering does not send data to the caller, but will communicate with the configured TURN + * server. Default 0. + */ + iceCandidatePoolSize?: number; + + /** + * True to advertise support for call transfers to other parties on Matrix calls. Default false. + */ + supportsCallTransfer?: boolean; + + /** + * Whether to allow a fallback ICE server should be used for negotiating a + * WebRTC connection if the homeserver doesn't provide any servers. Defaults to false. + */ fallbackICEServerAllowed?: boolean; + cryptoCallbacks?: ICryptoCallbacks; } From caab5befaae6cea6d2f2a5765564414847174c40 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 2 Jun 2021 13:35:29 -0600 Subject: [PATCH 02/32] Rename client.js -> 1client.ts for future commits --- src/{client.js => 1client.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{client.js => 1client.ts} (100%) diff --git a/src/client.js b/src/1client.ts similarity index 100% rename from src/client.js rename to src/1client.ts From 8a1d34c419d615d7a34ac0c9161de1ab1b9a91b6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 27 May 2021 23:09:40 -0600 Subject: [PATCH 03/32] [Combined] First pass of JS->TS for MatrixClient --- src/1client.ts | 10370 ++++++++++++++++++------------------ src/@types/partials.ts | 28 + src/@types/requests.ts | 67 + src/@types/signed.ts | 21 + src/crypto/api.ts | 131 + src/crypto/dehydration.ts | 15 +- src/crypto/keybackup.ts | 70 + src/event-mapper.ts | 48 + src/matrix.ts | 167 +- src/sync.api.ts | 26 + src/utils.ts | 2 +- 11 files changed, 5502 insertions(+), 5443 deletions(-) create mode 100644 src/@types/partials.ts create mode 100644 src/@types/requests.ts create mode 100644 src/@types/signed.ts create mode 100644 src/crypto/api.ts create mode 100644 src/crypto/keybackup.ts create mode 100644 src/event-mapper.ts create mode 100644 src/sync.api.ts diff --git a/src/1client.ts b/src/1client.ts index cb60705efe2..b50b1569651 100644 --- a/src/1client.ts +++ b/src/1client.ts @@ -1,8 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018-2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,47 +19,95 @@ limitations under the License. * @module client */ -import url from "url"; import { EventEmitter } from "events"; -import { MatrixBaseApis } from "./base-apis"; -import { Filter } from "./filter"; import { SyncApi } from "./sync"; import { EventStatus, MatrixEvent } from "./models/event"; -import { EventTimeline } from "./models/event-timeline"; -import { SearchResult } from "./models/search-result"; import { StubStore } from "./store/stub"; -import { createNewMatrixCall } from "./webrtc/call"; +import { createNewMatrixCall, MatrixCall } from "./webrtc/call"; +import { Filter } from "./filter"; import { CallEventHandler } from './webrtc/callEventHandler'; import * as utils from './utils'; import { sleep } from './utils'; -import { - MatrixError, - PREFIX_MEDIA_R0, - PREFIX_UNSTABLE, - retryNetworkOperation, -} from "./http-api"; -import { getHttpUriForMxc } from "./content-repo"; -import * as ContentHelpers from "./content-helpers"; +import { Group } from "./models/group"; +import { EventTimeline } from "./models/event-timeline"; +import { PushAction, PushProcessor } from "./pushprocessor"; +import { PREFIX_MEDIA_R0, PREFIX_UNSTABLE, retryNetworkOperation, } from "./http-api"; +import {AutoDiscovery} from "./autodiscovery"; import * as olmlib from "./crypto/olmlib"; +import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; import { ReEmitter } from './ReEmitter'; import { RoomList } from './crypto/RoomList'; import { logger } from './logger'; -import { Crypto, isCryptoAvailable, fixBackupKey } from './crypto'; +import { Crypto, DeviceInfo, fixBackupKey, isCryptoAvailable } from './crypto'; import { decodeRecoveryKey } from './crypto/recoverykey'; import { keyFromAuthData } from './crypto/key_passphrase'; -import { randomString } from './randomstring'; -import { PushProcessor } from "./pushprocessor"; -import { encodeBase64, decodeBase64 } from "./crypto/olmlib"; import { User } from "./models/user"; -import { AutoDiscovery } from "./autodiscovery"; -import { DEHYDRATION_ALGORITHM } from "./crypto/dehydration"; +import { getHttpUriForMxc } from "./content-repo"; +import {SearchResult} from "./models/search-result"; +import { DEHYDRATION_ALGORITHM, IDehydratedDevice, IDehydratedDeviceKeyInfo } from "./crypto/dehydration"; +import { + IKeyBackupPrepareOpts, + IKeyBackupRestoreOpts, + IKeyBackupRestoreResult, + IKeyBackupRoomSessions, + IKeyBackupSession, + IKeyBackupTrustInfo, + IKeyBackupVersion +} from "./crypto/keybackup"; +import { PkDecryption } from "olm"; +import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; +import type Request from "request"; +import { MatrixScheduler } from "./scheduler"; +import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from "./matrix"; +import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; +import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store"; +import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store"; +import { MemoryStore } from "./store/memory"; +import { LocalIndexedDBStoreBackend } from "./store/indexeddb-local-backend"; +import { RemoteIndexedDBStoreBackend } from "./store/indexeddb-remote-backend"; +import { SyncState } from "./sync.api"; +import { EventTimelineSet } from "./models/event-timeline-set"; +import { VerificationRequest } from "./crypto/verification/request/VerificationRequest"; +import { Base as Verification } from "./crypto/verification/Base"; +import * as ContentHelpers from "./content-helpers"; +import { + CrossSigningKey, + IAddSecretStorageKeyOpts, + ICreateSecretStorageOpts, + IEncryptedEventInfo, + IImportRoomKeysOpts, + IRecoveryKey, + ISecretStorageKey +} from "./crypto/api"; +import { CrossSigningInfo, UserTrustLevel } from "./crypto/CrossSigning"; +import { Room } from "./models/Room"; +import { + IEventSearchOpts, + IGuestAccessOpts, + IJoinRoomOpts, + IPaginateOpts, + IPresenceOpts, + IRedactOpts, ISearchOpts, + ISendEventResponse +} from "./@types/requests"; +import { EventType } from "./@types/event"; +import { IImageInfo } from "./@types/partials"; +import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper"; +import url from "url"; +import { randomString } from "./randomstring"; + +export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend; + +export type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore; + +export type Callback = (err: Error | any | null, data?: any) => void; const SCROLLBACK_DELAY_MS = 3000; -export const CRYPTO_ENABLED = isCryptoAvailable(); +export const CRYPTO_ENABLED: boolean = isCryptoAvailable(); const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes -function keysFromRecoverySession(sessions, decryptionKey, roomId) { +function keysFromRecoverySession(sessions: IKeyBackupRoomSessions, decryptionKey: PkDecryption, roomId: string) { const keys = []; for (const [sessionId, sessionData] of Object.entries(sessions)) { try { @@ -77,7 +122,7 @@ function keysFromRecoverySession(sessions, decryptionKey, roomId) { return keys; } -function keyFromRecoverySession(session, decryptionKey) { +function keyFromRecoverySession(session: IKeyBackupSession, decryptionKey: PkDecryption) { return JSON.parse(decryptionKey.decrypt( session.session_data.ephemeral, session.session_data.mac, @@ -85,4463 +130,3578 @@ function keyFromRecoverySession(session, decryptionKey) { )); } -/** - * Construct a Matrix Client. Only directly construct this if you want to use - * custom modules. Normally, {@link createClient} should be used - * as it specifies 'sensible' defaults for these modules. - * @constructor - * @extends {external:EventEmitter} - * @extends {module:base-apis~MatrixBaseApis} - * - * @param {Object} opts The configuration options for this client. - * @param {string} opts.baseUrl Required. The base URL to the client-server - * HTTP API. - * @param {string} opts.idBaseUrl Optional. The base identity server URL for - * identity server requests. - * @param {Function} opts.request Required. The function to invoke for HTTP - * requests. The value of this property is typically require("request") - * as it returns a function which meets the required interface. See - * {@link requestFunction} for more information. - * - * @param {string} opts.accessToken The access_token for this user. - * - * @param {string} opts.userId The user ID for this user. - * - * @param {Object} opts.deviceToImport Device data exported with - * "exportDevice" method that must be imported to recreate this device. - * Should only be useful for devices with end-to-end crypto enabled. - * If provided, opts.deviceId and opts.userId should **NOT** be provided - * (they are present in the exported data). - * - * @param {string} opts.pickleKey Key used to pickle olm objects or other - * sensitive data. - * - * @param {IdentityServerProvider} [opts.identityServer] - * Optional. A provider object with one function `getAccessToken`, which is a - * callback that returns a Promise of an identity access token to supply - * with identity requests. If the object is unset, no access token will be - * supplied. - * See also https://github.com/vector-im/element-web/issues/10615 which seeks to - * replace the previous approach of manual access tokens params with this - * callback throughout the SDK. - * - * @param {Object=} opts.store - * The data store used for sync data from the homeserver. If not specified, - * this client will not store any HTTP responses. The `createClient` helper - * will create a default store if needed. - * - * @param {module:store/session/webstorage~WebStorageSessionStore} opts.sessionStore - * A store to be used for end-to-end crypto session data. Most data has been - * migrated out of here to `cryptoStore` instead. If not specified, - * end-to-end crypto will be disabled. The `createClient` helper - * _will not_ create this store at the moment. - * - * @param {module:crypto.store.base~CryptoStore} opts.cryptoStore - * A store to be used for end-to-end crypto session data. If not specified, - * end-to-end crypto will be disabled. The `createClient` helper will create - * a default store if needed. - * - * @param {string=} opts.deviceId A unique identifier for this device; used for - * tracking things like crypto keys and access tokens. If not specified, - * end-to-end crypto will be disabled. - * - * @param {Object} opts.scheduler Optional. The scheduler to use. If not - * specified, this client will not retry requests on failure. This client - * will supply its own processing function to - * {@link module:scheduler~MatrixScheduler#setProcessFunction}. - * - * @param {Object} opts.queryParams Optional. Extra query parameters to append - * to all requests with this client. Useful for application services which require - * ?user_id=. - * - * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of - * time to wait before timing out HTTP requests. If not specified, there is no timeout. - * - * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use - * Authorization header instead of query param to send the access token to the server. - * - * @param {boolean} [opts.timelineSupport = false] Set to true to enable - * improved timeline support ({@link - * module:client~MatrixClient#getEventTimeline getEventTimeline}). It is - * disabled by default for compatibility with older clients - in particular to - * maintain support for back-paginating the live timeline after a '/sync' - * result with a gap. - * - * @param {boolean} [opts.unstableClientRelationAggregation = false] - * Optional. Set to true to enable client-side aggregation of event relations - * via `EventTimelineSet#getRelationsForEvent`. - * This feature is currently unstable and the API may change without notice. - * - * @param {Array} [opts.verificationMethods] Optional. The verification method - * that the application can handle. Each element should be an item from {@link - * module:crypto~verificationMethods verificationMethods}, or a class that - * implements the {$link module:crypto/verification/Base verifier interface}. - * - * @param {boolean} [opts.forceTURN] - * Optional. Whether relaying calls through a TURN server should be forced. - * - * * @param {boolean} [opts.iceCandidatePoolSize] - * Optional. Up to this many ICE candidates will be gathered when an incoming call arrives. - * Gathering does not send data to the caller, but will communicate with the configured TURN - * server. Default 0. - * - * @param {boolean} [opts.supportsCallTransfer] - * Optional. True to advertise support for call transfers to other parties on Matrix calls. - * - * @param {boolean} [opts.fallbackICEServerAllowed] - * Optional. Whether to allow a fallback ICE server should be used for negotiating a - * WebRTC connection if the homeserver doesn't provide any servers. Defaults to false. - * - * @param {boolean} [opts.usingExternalCrypto] - * Optional. Whether to allow sending messages to encrypted rooms when encryption - * is not available internally within this SDK. This is useful if you are using an external - * E2E proxy, for example. Defaults to false. - * - * @param {object} opts.cryptoCallbacks Optional. Callbacks for crypto and cross-signing. - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @param {function} [opts.cryptoCallbacks.getCrossSigningKey] - * Optional. Function to call when a cross-signing private key is needed. - * Secure Secret Storage will be used by default if this is unset. - * Args: - * {string} type The type of key needed. Will be one of "master", - * "self_signing", or "user_signing" - * {Uint8Array} publicKey The public key matching the expected private key. - * This can be passed to checkPrivateKey() along with the private key - * in order to check that a given private key matches what is being - * requested. - * Should return a promise that resolves with the private key as a - * UInt8Array or rejects with an error. - * - * @param {function} [opts.cryptoCallbacks.saveCrossSigningKeys] - * Optional. Called when new private keys for cross-signing need to be saved. - * Secure Secret Storage will be used by default if this is unset. - * Args: - * {object} keys the private keys to save. Map of key name to private key - * as a UInt8Array. The getPrivateKey callback above will be called - * with the corresponding key name when the keys are required again. - * - * @param {function} [opts.cryptoCallbacks.shouldUpgradeDeviceVerifications] - * Optional. Called when there are device-to-device verifications that can be - * upgraded into cross-signing verifications. - * Args: - * {object} users The users whose device verifications can be - * upgraded to cross-signing verifications. This will be a map of user IDs - * to objects with the properties `devices` (array of the user's devices - * that verified their cross-signing key), and `crossSigningInfo` (the - * user's cross-signing information) - * Should return a promise which resolves with an array of the user IDs who - * should be cross-signed. - * - * @param {function} [opts.cryptoCallbacks.getSecretStorageKey] - * Optional. Function called when an encryption key for secret storage - * is required. One or more keys will be described in the keys object. - * The callback function should return a promise with an array of: - * [, ] or null if it cannot provide - * any of the keys. - * Args: - * {object} keys Information about the keys: - * { - * keys: { - * : { - * "algorithm": "m.secret_storage.v1.aes-hmac-sha2", - * "passphrase": { - * "algorithm": "m.pbkdf2", - * "iterations": 500000, - * "salt": "..." - * }, - * "iv": "...", - * "mac": "..." - * }, ... - * } - * } - * {string} name the name of the value we want to read out of SSSS, for UI purposes. - * - * @param {function} [opts.cryptoCallbacks.cacheSecretStorageKey] - * Optional. Function called when a new encryption key for secret storage - * has been created. This allows the application a chance to cache this key if - * desired to avoid user prompts. - * Args: - * {string} keyId the ID of the new key - * {object} keyInfo Infomation about the key as above for `getSecretStorageKey` - * {Uint8Array} key the new private key - * - * @param {function} [opts.cryptoCallbacks.onSecretRequested] - * Optional. Function called when a request for a secret is received from another - * device. - * Args: - * {string} name The name of the secret being requested. - * {string} userId The user ID of the client requesting - * {string} deviceId The device ID of the client requesting the secret. - * {string} requestId The ID of the request. Used to match a - * corresponding `crypto.secrets.request_cancelled`. The request ID will be - * unique per sender, device pair. - * {DeviceTrustLevel} deviceTrust: The trust status of the device requesting - * the secret as returned by {@link module:client~MatrixClient#checkDeviceTrust}. - */ -export function MatrixClient(opts) { - opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); - opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl); +interface IOlmDevice { + pickledAccount: string; + sessions: Array>; + pickleKey: string; +} + +interface IExportedDevice { + olmDevice: IOlmDevice; + userId: string; + deviceId: string; +} + +export interface ICreateClientOpts { + baseUrl: string; + + idBaseUrl?: string; + + /** + * The data store used for sync data from the homeserver. If not specified, + * this client will not store any HTTP responses. The `createClient` helper + * will create a default store if needed. + */ + store?: Store; + + /** + * A store to be used for end-to-end crypto session data. If not specified, + * end-to-end crypto will be disabled. The `createClient` helper will create + * a default store if needed. + */ + cryptoStore?: CryptoStore; + + /** + * The scheduler to use. If not + * specified, this client will not retry requests on failure. This client + * will supply its own processing function to + * {@link module:scheduler~MatrixScheduler#setProcessFunction}. + */ + scheduler?: MatrixScheduler; + + /** + * The function to invoke for HTTP + * requests. The value of this property is typically require("request") + * as it returns a function which meets the required interface. See + * {@link requestFunction} for more information. + */ + request?: Request; + + userId?: string; + + /** + * A unique identifier for this device; used for tracking things like crypto + * keys and access tokens. If not specified, end-to-end encryption will be + * disabled. + */ + deviceId?: string; + + accessToken?: string; + + /** + * Identity server provider to retrieve the user's access token when accessing + * the identity server. See also https://github.com/vector-im/element-web/issues/10615 + * which seeks to replace the previous approach of manual access tokens params + * with this callback throughout the SDK. + */ + identityServer?: IIdentityServerProvider; + + /** + * The default maximum amount of + * time to wait before timing out HTTP requests. If not specified, there is no timeout. + */ + localTimeoutMs?: number; + + /** + * Set to true to use + * Authorization header instead of query param to send the access token to the server. + * + * Default false. + */ + useAuthorizationHeader?: boolean; + + /** + * Set to true to enable + * improved timeline support ({@link module:client~MatrixClient#getEventTimeline getEventTimeline}). It is + * disabled by default for compatibility with older clients - in particular to + * maintain support for back-paginating the live timeline after a '/sync' + * result with a gap. + */ + timelineSupport?: boolean; + + /** + * Extra query parameters to append + * to all requests with this client. Useful for application services which require + * ?user_id=. + */ + queryParams?: Record; + + /** + * Device data exported with + * "exportDevice" method that must be imported to recreate this device. + * Should only be useful for devices with end-to-end crypto enabled. + * If provided, deviceId and userId should **NOT** be provided at the top + * level (they are present in the exported data). + */ + deviceToImport?: IExportedDevice; + + /** + * Key used to pickle olm objects or other sensitive data. + */ + pickleKey?: string; + + /** + * A store to be used for end-to-end crypto session data. Most data has been + * migrated out of here to `cryptoStore` instead. If not specified, + * end-to-end crypto will be disabled. The `createClient` helper + * _will not_ create this store at the moment. + */ + sessionStore?: any; + + /** + * Set to true to enable client-side aggregation of event relations + * via `EventTimelineSet#getRelationsForEvent`. + * This feature is currently unstable and the API may change without notice. + */ + unstableClientRelationAggregation?: boolean; + + verificationMethods?: Array; + + /** + * Whether relaying calls through a TURN server should be forced. Default false. + */ + forceTURN?: boolean; + + /** + * Up to this many ICE candidates will be gathered when an incoming call arrives. + * Gathering does not send data to the caller, but will communicate with the configured TURN + * server. Default 0. + */ + iceCandidatePoolSize?: number; + + /** + * True to advertise support for call transfers to other parties on Matrix calls. Default false. + */ + supportsCallTransfer?: boolean; + + /** + * Whether to allow a fallback ICE server should be used for negotiating a + * WebRTC connection if the homeserver doesn't provide any servers. Defaults to false. + */ + fallbackICEServerAllowed?: boolean; + + cryptoCallbacks?: ICryptoCallbacks; +} - MatrixBaseApis.call(this, opts); +export interface IMatrixClientCreateOpts extends ICreateClientOpts { + /** + * Whether to allow sending messages to encrypted rooms when encryption + * is not available internally within this SDK. This is useful if you are using an external + * E2E proxy, for example. Defaults to false. + */ + usingExternalCrypto?: boolean; +} - this.olmVersion = null; // Populated after initCrypto is done +export interface IStartClientOpts { + /** + * The event limit= to apply to initial sync. Default: 8. + */ + initialSyncLimit?: number; + + /** + * True to put archived=true on the /initialSync request. Default: false. + */ + includeArchivedRooms?: boolean; + + /** + * True to do /profile requests on every invite event if the displayname/avatar_url is not known for this user ID. Default: false. + */ + resolveInvitesToProfiles?: boolean; + + /** + * Controls where pending messages appear in a room's timeline. If "chronological", messages will + * appear in the timeline when the call to sendEvent was made. If "detached", + * pending messages will appear in a separate list, accessbile via {@link module:models/room#getPendingEvents}. + * Default: "chronological". + */ + pendingEventOrdering?: "chronological" | "detached"; + + /** + * The number of milliseconds to wait on /sync. Default: 30000 (30 seconds). + */ + pollTimeout?: number; + + /** + * The filter to apply to /sync calls. This will override the opts.initialSyncLimit, which would + * normally result in a timeline limit filter. + */ + filter?: Filter; + + /** + * True to perform syncing without automatically updating presence. + */ + disablePresence?: boolean; + + /** + * True to not load all membership events during initial sync but fetch them when needed by calling + * `loadOutOfBandMembers` This will override the filter option at this moment. + */ + lazyLoadMembers?: boolean; + + /** + * The number of seconds between polls to /.well-known/matrix/client, undefined to disable. + * This should be in the order of hours. Default: undefined. + */ + clientWellKnownPollPeriod?: number; +} - this.reEmitter = new ReEmitter(this); +export interface IStoredClientOpts extends IStartClientOpts { + crypto: Crypto; + canResetEntireTimeline: (roomId: string) => boolean; +} - this.usingExternalCrypto = opts.usingExternalCrypto; +/** + * Represents a Matrix Client. Only directly construct this if you want to use + * custom modules. Normally, {@link createClient} should be used + * as it specifies 'sensible' defaults for these modules. + */ +export class MatrixClient extends EventEmitter { + public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; + + public reEmitter = new ReEmitter(this); + public olmVersion: number = null; // populated after initCrypto + public usingExternalCrypto = false; + public store: Store; + public deviceId?: string; + public credentials: {userId?: string}; + public pickleKey: string; + public scheduler: MatrixScheduler; + public clientRunning = false; + public timelineSupport = false; + public urlPreviewCache: {[key: string]: Promise} = {}; // TODO: @@TR + public unstableClientRelationAggregation = false; + + private canSupportVoip = false; + private callEventHandler: CallEventHandler; + private syncingRetry = null; // TODO: @@TR + private peekSync: SyncApi = null; + private isGuestAccount = false; + private ongoingScrollbacks = {}; // TODO: @@TR + private notifTimelineSet: EventTimelineSet = null; + private crypto: Crypto; + private cryptoStore: CryptoStore; + private sessionStore: any; // TODO: @@TR + private verificationMethods: string[]; + private cryptoCallbacks: ICryptoCallbacks; + private forceTURN = false; + private iceCandidatePoolSize = 0; + private supportsCallTransfer = false; + private fallbackICEServerAllowed = false; + private roomList: RoomList; + private syncApi: SyncApi; + private pushRules: any; // TODO: @@TR + private syncLeftRoomsPromise: Promise; + private syncedLeftRooms = false; + private clientOpts: IStoredClientOpts; + private clientWellKnownIntervalID: number; + private canResetTimelineCallback: Callback; - this.store = opts.store || new StubStore(); + // The pushprocessor caches useful things, so keep one and re-use it + private pushProcessor = new PushProcessor(this); - this.deviceId = opts.deviceId || null; + // Promise to a response of the server's /versions response + // TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020 + private serverVersionsPromise: Promise; - const userId = (opts.userId || null); - this.credentials = { - userId: userId, + private cachedCapabilities: { + capabilities: Record; + expiration: number; }; + private clientWellKnown: any; + private clientWellKnownPromise: Promise; + private turnServers: any[] = []; // TODO: @@TR + private turnServersExpiry = 0; + private checkTurnServersIntervalID: number; + private exportedOlmDeviceToImport: IOlmDevice; + + constructor(opts: IMatrixClientCreateOpts) { + super(); + + opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); + opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl); + + this.usingExternalCrypto = opts.usingExternalCrypto; + this.store = opts.store || new StubStore(); + this.deviceId = opts.deviceId || null; + + const userId = opts.userId || null; + this.credentials = {userId}; + + if (opts.deviceToImport) { + if (this.deviceId) { + logger.warn( + 'not importing device because device ID is provided to ' + + 'constructor independently of exported data', + ); + } else if (this.credentials.userId) { + logger.warn( + 'not importing device because user ID is provided to ' + + 'constructor independently of exported data', + ); + } else if (!opts.deviceToImport.deviceId) { + logger.warn('not importing device because no device ID in exported data'); + } else { + this.deviceId = opts.deviceToImport.deviceId; + this.credentials.userId = opts.deviceToImport.userId; + // will be used during async initialization of the crypto + this.exportedOlmDeviceToImport = opts.deviceToImport.olmDevice; + } + } else if (opts.pickleKey) { + this.pickleKey = opts.pickleKey; + } - if (opts.deviceToImport) { - if (this.deviceId) { - logger.warn( - 'not importing device because' - + ' device ID is provided to constructor' - + ' independently of exported data', - ); - } else if (this.credentials.userId) { - logger.warn( - 'not importing device because' - + ' user ID is provided to constructor' - + ' independently of exported data', - ); - } else if (!(opts.deviceToImport.deviceId)) { - logger.warn('not importing device because no device ID in exported data'); - } else { - this.deviceId = opts.deviceToImport.deviceId; - this.credentials.userId = opts.deviceToImport.userId; - // will be used during async initialization of the crypto - this._exportedOlmDeviceToImport = opts.deviceToImport.olmDevice; + this.scheduler = opts.scheduler; + if (this.scheduler) { + this.scheduler.setProcessFunction(async (eventToSend) => { + const room = this.getRoom(eventToSend.getRoomId()); + if (eventToSend.status !== EventStatus.SENDING) { + this.updatePendingEventStatus(room, eventToSend, EventStatus.SENDING); + } + const res = await sendEventHttpRequest(this, eventToSend); + if (room) { + // ensure we update pending event before the next scheduler run so that any listeners to event id + // updates on the synchronous event emitter get a chance to run first. + room.updatePendingEvent(eventToSend, EventStatus.SENT, res.event_id); + } + return res; + }); } - } else if (opts.pickleKey) { - this.pickleKey = opts.pickleKey; - } - this.scheduler = opts.scheduler; - if (this.scheduler) { - const self = this; - this.scheduler.setProcessFunction(async function(eventToSend) { - const room = self.getRoom(eventToSend.getRoomId()); - if (eventToSend.status !== EventStatus.SENDING) { - _updatePendingEventStatus(room, eventToSend, - EventStatus.SENDING); - } - const res = await _sendEventHttpRequest(self, eventToSend); - if (room) { - // ensure we update pending event before the next scheduler run so that any listeners to event id - // updates on the synchronous event emitter get a chance to run first. - room.updatePendingEvent(eventToSend, EventStatus.SENT, res.event_id); + // try constructing a MatrixCall to see if we are running in an environment + // which has WebRTC. If we are, listen for and handle m.call.* events. + const call = createNewMatrixCall(this, undefined, undefined); + if (call) { + this.callEventHandler = new CallEventHandler(this); + this.canSupportVoip = true; + // Start listening for calls after the initial sync is done + // We do not need to backfill the call event buffer + // with encrypted events that might never get decrypted + this.on("sync", () => this.startCallEventHandler()); + } + + this.timelineSupport = Boolean(opts.timelineSupport); + this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation; + + this.cryptoStore = opts.cryptoStore; + this.sessionStore = opts.sessionStore; + this.verificationMethods = opts.verificationMethods; + this.cryptoCallbacks = opts.cryptoCallbacks || {}; + + this.forceTURN = opts.forceTURN || false; + this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize; + this.supportsCallTransfer = opts.supportsCallTransfer || false; + this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false; + + // List of which rooms have encryption enabled: separate from crypto because + // we still want to know which rooms are encrypted even if crypto is disabled: + // we don't want to start sending unencrypted events to them. + this.roomList = new RoomList(this.cryptoStore); + + // The SDK doesn't really provide a clean way for events to recalculate the push + // actions for themselves, so we have to kinda help them out when they are encrypted. + // We do this so that push rules are correctly executed on events in their decrypted + // state, such as highlights when the user's name is mentioned. + this.on("Event.decrypted", (event) => { + const oldActions = event.getPushActions(); + const actions = this.pushProcessor.actionsForEvent(event); + event.setPushActions(actions); // Might as well while we're here + + const room = this.getRoom(event.getRoomId()); + if (!room) return; + + const currentCount = room.getUnreadNotificationCount("highlight"); + + // Ensure the unread counts are kept up to date if the event is encrypted + // We also want to make sure that the notification count goes up if we already + // have encrypted events to avoid other code from resetting 'highlight' to zero. + const oldHighlight = oldActions && oldActions.tweaks + ? !!oldActions.tweaks.highlight : false; + const newHighlight = actions && actions.tweaks + ? !!actions.tweaks.highlight : false; + if (oldHighlight !== newHighlight || currentCount > 0) { + // TODO: Handle mentions received while the client is offline + // See also https://github.com/vector-im/element-web/issues/9069 + if (!room.hasUserReadEvent(this.getUserId(), event.getId())) { + let newCount = currentCount; + if (newHighlight && !oldHighlight) newCount++; + if (!newHighlight && oldHighlight) newCount--; + room.setUnreadNotificationCount("highlight", newCount); + + // Fix 'Mentions Only' rooms from not having the right badge count + const totalCount = room.getUnreadNotificationCount('total'); + if (totalCount < newCount) { + room.setUnreadNotificationCount('total', newCount); + } + } } - return res; }); - } - this.clientRunning = false; - - // try constructing a MatrixCall to see if we are running in an environment - // which has WebRTC. If we are, listen for and handle m.call.* events. - const call = createNewMatrixCall(this); - this._supportsVoip = false; - if (call) { - this._callEventHandler = new CallEventHandler(this); - this._supportsVoip = true; - // Start listening for calls after the initial sync is done - // We do not need to backfill the call event buffer - // with encrypted events that might never get decrypted - this.on("sync", this._startCallEventHandler); - } else { - this._callEventHandler = null; - } - this._syncingRetry = null; - this._syncApi = null; - this._peekSync = null; - this._isGuest = false; - this._ongoingScrollbacks = {}; - this.timelineSupport = Boolean(opts.timelineSupport); - this.urlPreviewCache = {}; // key=preview key, value=Promise for preview (may be an error) - this._notifTimelineSet = null; - this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation; - - this._crypto = null; - this._cryptoStore = opts.cryptoStore; - this._sessionStore = opts.sessionStore; - this._verificationMethods = opts.verificationMethods; - this._cryptoCallbacks = opts.cryptoCallbacks || {}; - - this._forceTURN = opts.forceTURN || false; - this._iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize; - this._supportsCallTransfer = opts.supportsCallTransfer || false; - this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false; - - // List of which rooms have encryption enabled: separate from crypto because - // we still want to know which rooms are encrypted even if crypto is disabled: - // we don't want to start sending unencrypted events to them. - this._roomList = new RoomList(this._cryptoStore); - // The pushprocessor caches useful things, so keep one and re-use it - this._pushProcessor = new PushProcessor(this); + // Like above, we have to listen for read receipts from ourselves in order to + // correctly handle notification counts on encrypted rooms. + // This fixes https://github.com/vector-im/element-web/issues/9421 + this.on("Room.receipt", (event, room) => { + if (room && this.isRoomEncrypted(room.roomId)) { + // Figure out if we've read something or if it's just informational + const content = event.getContent(); + const isSelf = Object.keys(content).filter(eid => { + return Object.keys(content[eid]['m.read']).includes(this.getUserId()); + }).length > 0; - // Promise to a response of the server's /versions response - // TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020 - this._serverVersionsPromise = null; + if (!isSelf) return; - this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp } + // Work backwards to determine how many events are unread. We also set + // a limit for how back we'll look to avoid spinning CPU for too long. + // If we hit the limit, we assume the count is unchanged. + const maxHistory = 20; + const events = room.getLiveTimeline().getEvents(); - this._clientWellKnown = undefined; - this._clientWellKnownPromise = undefined; + let highlightCount = 0; - this._turnServers = []; - this._turnServersExpiry = 0; - this._checkTurnServersIntervalID = null; + for (let i = events.length - 1; i >= 0; i--) { + if (i === events.length - maxHistory) return; // limit reached - // The SDK doesn't really provide a clean way for events to recalculate the push - // actions for themselves, so we have to kinda help them out when they are encrypted. - // We do this so that push rules are correctly executed on events in their decrypted - // state, such as highlights when the user's name is mentioned. - this.on("Event.decrypted", (event) => { - const oldActions = event.getPushActions(); - const actions = this._pushProcessor.actionsForEvent(event); - event.setPushActions(actions); // Might as well while we're here + const event = events[i]; - const room = this.getRoom(event.getRoomId()); - if (!room) return; - - const currentCount = room.getUnreadNotificationCount("highlight"); - - // Ensure the unread counts are kept up to date if the event is encrypted - // We also want to make sure that the notification count goes up if we already - // have encrypted events to avoid other code from resetting 'highlight' to zero. - const oldHighlight = oldActions && oldActions.tweaks - ? !!oldActions.tweaks.highlight : false; - const newHighlight = actions && actions.tweaks - ? !!actions.tweaks.highlight : false; - if (oldHighlight !== newHighlight || currentCount > 0) { - // TODO: Handle mentions received while the client is offline - // See also https://github.com/vector-im/element-web/issues/9069 - if (!room.hasUserReadEvent(this.getUserId(), event.getId())) { - let newCount = currentCount; - if (newHighlight && !oldHighlight) newCount++; - if (!newHighlight && oldHighlight) newCount--; - room.setUnreadNotificationCount("highlight", newCount); - - // Fix 'Mentions Only' rooms from not having the right badge count - const totalCount = room.getUnreadNotificationCount('total'); - if (totalCount < newCount) { - room.setUnreadNotificationCount('total', newCount); + if (room.hasUserReadEvent(this.getUserId(), event.getId())) { + // If the user has read the event, then the counting is done. + break; + } + + const pushActions = this.getPushActionsForEvent(event); + highlightCount += pushActions.tweaks && + pushActions.tweaks.highlight ? 1 : 0; } + + // Note: we don't need to handle 'total' notifications because the counts + // will come from the server. + room.setUnreadNotificationCount("highlight", highlightCount); } - } - }); + }); + } - // Like above, we have to listen for read receipts from ourselves in order to - // correctly handle notification counts on encrypted rooms. - // This fixes https://github.com/vector-im/element-web/issues/9421 - this.on("Room.receipt", (event, room) => { - if (room && this.isRoomEncrypted(room.roomId)) { - // Figure out if we've read something or if it's just informational - const content = event.getContent(); - const isSelf = Object.keys(content).filter(eid => { - return Object.keys(content[eid]['m.read']).includes(this.getUserId()); - }).length > 0; + /** + * High level helper method to begin syncing and poll for new events. To listen for these + * events, add a listener for {@link module:client~MatrixClient#event:"event"} + * via {@link module:client~MatrixClient#on}. Alternatively, listen for specific + * state change events. + * @param {Object=} opts Options to apply when syncing. + */ + public async startClient(opts: IStartClientOpts) { + if (this.clientRunning) { + // client is already running. + return; + } + this.clientRunning = true; + // backwards compat for when 'opts' was 'historyLen'. + if (typeof opts === "number") { + opts = { + initialSyncLimit: opts, + }; + } - if (!isSelf) return; + // Create our own user object artificially (instead of waiting for sync) + // so it's always available, even if the user is not in any rooms etc. + const userId = this.getUserId(); + if (userId) { + this.store.storeUser(new User(userId)); + } - // Work backwards to determine how many events are unread. We also set - // a limit for how back we'll look to avoid spinning CPU for too long. - // If we hit the limit, we assume the count is unchanged. - const maxHistory = 20; - const events = room.getLiveTimeline().getEvents(); + if (this.crypto) { + this.crypto.uploadDeviceKeys(); + this.crypto.start(); + } - let highlightCount = 0; + // periodically poll for turn servers if we support voip + if (this.canSupportVoip) { + this.checkTurnServersIntervalID = setInterval(() => { + this.checkTurnServers(); + }, TURN_CHECK_INTERVAL) as any as number; // XXX: Typecast because we know better + // noinspection ES6MissingAwait + this.checkTurnServers(); + } - for (let i = events.length - 1; i >= 0; i--) { - if (i === events.length - maxHistory) return; // limit reached + if (this.syncApi) { + // This shouldn't happen since we thought the client was not running + logger.error("Still have sync object whilst not running: stopping old one"); + this.syncApi.stop(); + } - const event = events[i]; + // shallow-copy the opts dict before modifying and storing it + this.clientOpts = Object.assign({}, opts); // XXX: Typecast because we're about to add the missing props + this.clientOpts.crypto = this.crypto; + this.clientOpts.canResetEntireTimeline = (roomId) => { + if (!this.canResetTimelineCallback) { + return false; + } + return this.canResetTimelineCallback(roomId); + }; + this.syncApi = new SyncApi(this, opts); + this.syncApi.sync(); + + if (opts.clientWellKnownPollPeriod !== undefined) { + this.clientWellKnownIntervalID = + setInterval(() => { + this.fetchClientWellKnown(); + }, 1000 * opts.clientWellKnownPollPeriod) as any as number; // XXX: Typecast because we know better + this.fetchClientWellKnown(); + } + } - if (room.hasUserReadEvent(this.getUserId(), event.getId())) { - // If the user has read the event, then the counting is done. - break; - } + /** + * High level helper method to stop the client from polling and allow a + * clean shutdown. + */ + public stopClient() { + logger.log('stopping MatrixClient'); - const pushActions = this.getPushActionsForEvent(event); - highlightCount += pushActions.tweaks && - pushActions.tweaks.highlight ? 1 : 0; - } + this.clientRunning = false; - // Note: we don't need to handle 'total' notifications because the counts - // will come from the server. - room.setUnreadNotificationCount("highlight", highlightCount); - } - }); -} -utils.inherits(MatrixClient, EventEmitter); -utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); + this.syncApi?.stop(); + this.syncApi = null; -/** - * Try to rehydrate a device if available. The client must have been - * initialized with a `cryptoCallback.getDehydrationKey` option, and this - * function must be called before initCrypto and startClient are called. - * - * @return {Promise} Resolves to undefined if a device could not be dehydrated, or - * to the new device ID if the dehydration was successful. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.rehydrateDevice = async function() { - if (this._crypto) { - throw new Error("Cannot rehydrate device after crypto is initialized"); - } + this.crypto?.stop(); + this.peekSync?.stopPeeking(); - if (!this._cryptoCallbacks.getDehydrationKey) { - return; - } + this.callEventHandler?.stop(); + this.callEventHandler = null; - const getDeviceResult = await this.getDehydratedDevice(); - if (!getDeviceResult) { - return; + global.clearInterval(this.checkTurnServersIntervalID); + if (this.clientWellKnownIntervalID !== undefined) { + global.clearInterval(this.clientWellKnownIntervalID); + } } - if (!getDeviceResult.device_data || !getDeviceResult.device_id) { - logger.info("no dehydrated device found"); - return; - } + /** + * Try to rehydrate a device if available. The client must have been + * initialized with a `cryptoCallback.getDehydrationKey` option, and this + * function must be called before initCrypto and startClient are called. + * + * @return {Promise} Resolves to undefined if a device could not be dehydrated, or + * to the new device ID if the dehydration was successful. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async rehydrateDevice(): Promise { + if (this.crypto) { + throw new Error("Cannot rehydrate device after crypto is initialized"); + } - const account = new global.Olm.Account(); - try { - const deviceData = getDeviceResult.device_data; - if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) { - logger.warn("Wrong algorithm for dehydrated device"); + if (!this.cryptoCallbacks.getDehydrationKey) { return; } - logger.log("unpickling dehydrated device"); - const key = await this._cryptoCallbacks.getDehydrationKey( - deviceData, - (k) => { - // copy the key so that it doesn't get clobbered - account.unpickle(new Uint8Array(k), deviceData.account); - }, - ); - account.unpickle(key, deviceData.account); - logger.log("unpickled device"); - - const rehydrateResult = await this._http.authedRequest( - undefined, - "POST", - "/dehydrated_device/claim", - undefined, - { - device_id: getDeviceResult.device_id, - }, - { - prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", - }, - ); - if (rehydrateResult.success === true) { - this.deviceId = getDeviceResult.device_id; - logger.info("using dehydrated device"); - const pickleKey = this.pickleKey || "DEFAULT_KEY"; - this._exportedOlmDeviceToImport = { - pickledAccount: account.pickle(pickleKey), - sessions: [], - pickleKey: pickleKey, - }; - account.free(); - return this.deviceId; - } else { - account.free(); - logger.info("not using dehydrated device"); + const getDeviceResult = await this.getDehydratedDevice(); + if (!getDeviceResult) { return; } - } catch (e) { - account.free(); - logger.warn("could not unpickle", e); - } -}; - -/** - * Get the current dehydrated device, if any - * @return {Promise} A promise of an object containing the dehydrated device - */ -MatrixClient.prototype.getDehydratedDevice = async function() { - try { - return await this._http.authedRequest( - undefined, - "GET", - "/dehydrated_device", - undefined, undefined, - { - prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", - }, - ); - } catch (e) { - logger.info("could not get dehydrated device", e.toString()); - return; - } -}; -/** - * Set the dehydration key. This will also periodically dehydrate devices to - * the server. - * - * @param {Uint8Array} key the dehydration key - * @param {object} [keyInfo] Information about the key. Primarily for - * information about how to generate the key from a passphrase. - * @param {string} [deviceDisplayName] The device display name for the - * dehydrated device. - * @return {Promise} A promise that resolves when the dehydrated device is stored. - */ -MatrixClient.prototype.setDehydrationKey = async function( - key, keyInfo = {}, deviceDisplayName = undefined, -) { - if (!(this._crypto)) { - logger.warn('not dehydrating device if crypto is not enabled'); - return; - } - return await this._crypto._dehydrationManager.setKeyAndQueueDehydration( - key, keyInfo, deviceDisplayName, - ); -}; + if (!getDeviceResult.device_data || !getDeviceResult.device_id) { + logger.info("no dehydrated device found"); + return; + } -/** - * Creates a new dehydrated device (without queuing periodic dehydration) - * @param {Uint8Array} key the dehydration key - * @param {object} [keyInfo] Information about the key. Primarily for - * information about how to generate the key from a passphrase. - * @param {string} [deviceDisplayName] The device display name for the - * dehydrated device. - * @return {Promise} the device id of the newly created dehydrated device - */ -MatrixClient.prototype.createDehydratedDevice = async function( - key, keyInfo = {}, deviceDisplayName = undefined, -) { - if (!(this._crypto)) { - logger.warn('not dehydrating device if crypto is not enabled'); - return; - } - await this._crypto._dehydrationManager.setKey( - key, keyInfo, deviceDisplayName, - ); - return await this._crypto._dehydrationManager.dehydrateDevice(); -}; - -MatrixClient.prototype.exportDevice = async function() { - if (!(this._crypto)) { - logger.warn('not exporting device if crypto is not enabled'); - return; - } - return { - userId: this.credentials.userId, - deviceId: this.deviceId, - olmDevice: await this._crypto._olmDevice.export(), - }; -}; + const account = new global.Olm.Account(); + try { + const deviceData = getDeviceResult.device_data; + if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) { + logger.warn("Wrong algorithm for dehydrated device"); + return; + } + logger.log("unpickling dehydrated device"); + const key = await this.cryptoCallbacks.getDehydrationKey( + deviceData, + (k) => { + // copy the key so that it doesn't get clobbered + account.unpickle(new Uint8Array(k), deviceData.account); + }, + ); + account.unpickle(key, deviceData.account); + logger.log("unpickled device"); + + const rehydrateResult = await this.http.authedRequest( + undefined, + "POST", + "/dehydrated_device/claim", + undefined, + { + device_id: getDeviceResult.device_id, + }, + { + prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", + }, + ); -/** - * Clear any data out of the persistent stores used by the client. - * - * @returns {Promise} Promise which resolves when the stores have been cleared. - */ -MatrixClient.prototype.clearStores = function() { - if (this._clientRunning) { - throw new Error("Cannot clear stores while client is running"); + if (rehydrateResult.success === true) { + this.deviceId = getDeviceResult.device_id; + logger.info("using dehydrated device"); + const pickleKey = this.pickleKey || "DEFAULT_KEY"; + this.exportedOlmDeviceToImport = { + pickledAccount: account.pickle(pickleKey), + sessions: [], + pickleKey: pickleKey, + }; + account.free(); + return this.deviceId; + } else { + account.free(); + logger.info("not using dehydrated device"); + return; + } + } catch (e) { + account.free(); + logger.warn("could not unpickle", e); + } } - const promises = []; - - promises.push(this.store.deleteAllData()); - if (this._cryptoStore) { - promises.push(this._cryptoStore.deleteAllData()); + /** + * Get the current dehydrated device, if any + * @return {Promise} A promise of an object containing the dehydrated device + */ + public async getDehydratedDevice(): Promise { + try { + return await this.http.authedRequest( + undefined, + "GET", + "/dehydrated_device", + undefined, undefined, + { + prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", + }, + ); + } catch (e) { + logger.info("could not get dehydrated device", e.toString()); + return; + } } - return Promise.all(promises); -}; -/** - * Get the user-id of the logged-in user - * - * @return {?string} MXID for the logged-in user, or null if not logged in - */ -MatrixClient.prototype.getUserId = function() { - if (this.credentials && this.credentials.userId) { - return this.credentials.userId; + /** + * Set the dehydration key. This will also periodically dehydrate devices to + * the server. + * + * @param {Uint8Array} key the dehydration key + * @param {IDehydratedDeviceKeyInfo} [keyInfo] Information about the key. Primarily for + * information about how to generate the key from a passphrase. + * @param {string} [deviceDisplayName] The device display name for the + * dehydrated device. + * @return {Promise} A promise that resolves when the dehydrated device is stored. + */ + public async setDehydrationKey(key: Uint8Array, keyInfo: IDehydratedDeviceKeyInfo, deviceDisplayName?: string): Promise { + if (!this.crypto) { + logger.warn('not dehydrating device if crypto is not enabled'); + return; + } + // XXX: Private member access. + return await this.crypto._dehydrationManager.setKeyAndQueueDehydration( + key, keyInfo, deviceDisplayName, + ); } - return null; -}; -/** - * Get the domain for this client's MXID - * @return {?string} Domain of this MXID - */ -MatrixClient.prototype.getDomain = function() { - if (this.credentials && this.credentials.userId) { - return this.credentials.userId.replace(/^.*?:/, ''); + /** + * Creates a new dehydrated device (without queuing periodic dehydration) + * @param {Uint8Array} key the dehydration key + * @param {IDehydratedDeviceKeyInfo} [keyInfo] Information about the key. Primarily for + * information about how to generate the key from a passphrase. + * @param {string} [deviceDisplayName] The device display name for the + * dehydrated device. + * @return {Promise} the device id of the newly created dehydrated device + */ + public async createDehydratedDevice(key: Uint8Array, keyInfo: IDehydratedDeviceKeyInfo, deviceDisplayName?: string): Promise { + if (!this.crypto) { + logger.warn('not dehydrating device if crypto is not enabled'); + return; + } + await this.crypto._dehydrationManager.setKey( + key, keyInfo, deviceDisplayName, + ); + // XXX: Private member access. + return await this.crypto._dehydrationManager.dehydrateDevice(); } - return null; -}; -/** - * Get the local part of the current user ID e.g. "foo" in "@foo:bar". - * @return {?string} The user ID localpart or null. - */ -MatrixClient.prototype.getUserIdLocalpart = function() { - if (this.credentials && this.credentials.userId) { - return this.credentials.userId.split(":")[0].substring(1); + public async exportDevice(): Promise { + if (!this.crypto) { + logger.warn('not exporting device if crypto is not enabled'); + return; + } + return { + userId: this.credentials.userId, + deviceId: this.deviceId, + // XXX: Private member access. + olmDevice: await this.crypto._olmDevice.export(), + }; } - return null; -}; - -/** - * Get the device ID of this client - * @return {?string} device ID - */ -MatrixClient.prototype.getDeviceId = function() { - return this.deviceId; -}; -/** - * Check if the runtime environment supports VoIP calling. - * @return {boolean} True if VoIP is supported. - */ -MatrixClient.prototype.supportsVoip = function() { - return this._supportsVoip; -}; + /** + * Clear any data out of the persistent stores used by the client. + * + * @returns {Promise} Promise which resolves when the stores have been cleared. + */ + public clearStores(): Promise { + if (this.clientRunning) { + throw new Error("Cannot clear stores while client is running"); + } -/** - * Set whether VoIP calls are forced to use only TURN - * candidates. This is the same as the forceTURN option - * when creating the client. - * @param {bool} forceTURN True to force use of TURN servers - */ -MatrixClient.prototype.setForceTURN = function(forceTURN) { - this._forceTURN = forceTURN; -}; + const promises = []; -/** - * Set whether to advertise transfer support to other parties on Matrix calls. - * @param {bool} supportsCallTransfer True to advertise the 'm.call.transferee' capability - */ -MatrixClient.prototype.setSupportsCallTransfer = function(supportsCallTransfer) { - this._supportsCallTransfer = supportsCallTransfer; -}; + promises.push(this.store.deleteAllData()); + if (this.cryptoStore) { + promises.push(this.cryptoStore.deleteAllData()); + } + return Promise.all(promises).then(); // .then to fix types + } -/** - * Creates a new call. - * The place*Call methods on the returned call can be used to actually place a call - * - * @param {string} roomId The room the call is to be placed in. - * @return {MatrixCall} the call or null if the browser doesn't support calling. - */ -MatrixClient.prototype.createCall = function(roomId) { - return createNewMatrixCall(this, roomId); -}; + /** + * Get the user-id of the logged-in user + * + * @return {?string} MXID for the logged-in user, or null if not logged in + */ + public getUserId(): string { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId; + } + return null; + } -/** - * Get the current sync state. - * @return {?string} the sync state, which may be null. - * @see module:client~MatrixClient#event:"sync" - */ -MatrixClient.prototype.getSyncState = function() { - if (!this._syncApi) { + /** + * Get the domain for this client's MXID + * @return {?string} Domain of this MXID + */ + public getDomain(): string { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId.replace(/^.*?:/, ''); + } return null; } - return this._syncApi.getSyncState(); -}; -/** - * Returns the additional data object associated with - * the current sync state, or null if there is no - * such data. - * Sync errors, if available, are put in the 'error' key of - * this object. - * @return {?Object} - */ -MatrixClient.prototype.getSyncStateData = function() { - if (!this._syncApi) { + /** + * Get the local part of the current user ID e.g. "foo" in "@foo:bar". + * @return {?string} The user ID localpart or null. + */ + public getUserIdLocalpart(): string { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId.split(":")[0].substring(1); + } return null; } - return this._syncApi.getSyncStateData(); -}; -/** - * Whether the initial sync has completed. - * @return {boolean} True if at least on sync has happened. - */ -MatrixClient.prototype.isInitialSyncComplete = function() { - const state = this.getSyncState(); - if (!state) { - return false; + /** + * Get the device ID of this client + * @return {?string} device ID + */ + public getDeviceId(): string { + return this.deviceId; + } + + /** + * Check if the runtime environment supports VoIP calling. + * @return {boolean} True if VoIP is supported. + */ + public supportsVoip(): boolean { + return this.canSupportVoip; + } + + /** + * Set whether VoIP calls are forced to use only TURN + * candidates. This is the same as the forceTURN option + * when creating the client. + * @param {boolean} force True to force use of TURN servers + */ + public setForceTURN(force: boolean) { + this.forceTURN = force; + } + + /** + * Set whether to advertise transfer support to other parties on Matrix calls. + * @param {boolean} support True to advertise the 'm.call.transferee' capability + */ + public setSupportsCallTransfer(support: boolean) { + this.supportsCallTransfer = support; + } + + /** + * Creates a new call. + * The place*Call methods on the returned call can be used to actually place a call + * + * @param {string} roomId The room the call is to be placed in. + * @return {MatrixCall} the call or null if the browser doesn't support calling. + */ + public createCall(roomId: string): MatrixCall { + return createNewMatrixCall(this, roomId); + } + + /** + * Get the current sync state. + * @return {?SyncState} the sync state, which may be null. + * @see module:client~MatrixClient#event:"sync" + */ + public getSyncState(): SyncState { + if (!this.syncApi) { + return null; + } + return this.syncApi.getSyncState(); + } + + /** + * Returns the additional data object associated with + * the current sync state, or null if there is no + * such data. + * Sync errors, if available, are put in the 'error' key of + * this object. + * @return {?Object} + */ + public getSyncStateData(): any { // TODO: Unify types. + if (!this.syncApi) { + return null; + } + return this.syncApi.getSyncStateData(); } - return state === "PREPARED" || state === "SYNCING"; -}; -/** - * Return whether the client is configured for a guest account. - * @return {boolean} True if this is a guest access_token (or no token is supplied). - */ -MatrixClient.prototype.isGuest = function() { - return this._isGuest; -}; + /** + * Whether the initial sync has completed. + * @return {boolean} True if at least one sync has happened. + */ + public isInitialSyncComplete(): boolean { + const state = this.getSyncState(); + if (!state) { + return false; + } + return state === SyncState.Prepared || state === SyncState.Syncing; + } + + /** + * Return whether the client is configured for a guest account. + * @return {boolean} True if this is a guest access_token (or no token is supplied). + */ + public isGuest(): boolean { + return this.isGuestAccount; + } + + /** + * Set whether this client is a guest account. This method is experimental + * and may change without warning. + * @param {boolean} guest True if this is a guest account. + */ + public setGuest(guest: boolean) { + // EXPERIMENTAL: + // If the token is a macaroon, it should be encoded in it that it is a 'guest' + // access token, which means that the SDK can determine this entirely without + // the dev manually flipping this flag. + this.isGuestAccount = guest; + } + + /** + * Return the provided scheduler, if any. + * @return {?module:scheduler~MatrixScheduler} The scheduler or null + */ + public getScheduler(): MatrixScheduler { + return this.scheduler; + } + + /** + * Retry a backed off syncing request immediately. This should only be used when + * the user explicitly attempts to retry their lost connection. + * @return {boolean} True if this resulted in a request being retried. + */ + public retryImmediately(): boolean { + return this.syncApi.retryImmediately(); + } + + /** + * Return the global notification EventTimelineSet, if any + * + * @return {EventTimelineSet} the globl notification EventTimelineSet + */ + public getNotifTimelineSet(): EventTimelineSet { + return this.notifTimelineSet; + } + + /** + * Set the global notification EventTimelineSet + * + * @param {EventTimelineSet} set + */ + public setNotifTimelineSet(set: EventTimelineSet) { + this.notifTimelineSet = set; + } + + /** + * Gets the capabilities of the homeserver. Always returns an object of + * capability keys and their options, which may be empty. + * @param {boolean} fresh True to ignore any cached values. + * @return {Promise} Resolves to the capabilities of the homeserver + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getCapabilities(fresh = false): Promise> { + const now = new Date().getTime(); + + if (this.cachedCapabilities && !fresh) { + if (now < this.cachedCapabilities.expiration) { + logger.log("Returning cached capabilities"); + return Promise.resolve(this.cachedCapabilities.capabilities); + } + } -/** - * Return the provided scheduler, if any. - * @return {?module:scheduler~MatrixScheduler} The scheduler or null - */ -MatrixClient.prototype.getScheduler = function() { - return this.scheduler; -}; + // We swallow errors because we need a default object anyhow + return this.http.authedRequest( + undefined, "GET", "/capabilities", + ).catch((e) => { + logger.error(e); + return null; // otherwise consume the error + }).then((r) => { + if (!r) r = {}; + const capabilities = r["capabilities"] || {}; + + // If the capabilities missed the cache, cache it for a shorter amount + // of time to try and refresh them later. + const cacheMs = Object.keys(capabilities).length + ? CAPABILITIES_CACHE_MS + : 60000 + (Math.random() * 5000); + + this.cachedCapabilities = { + capabilities: capabilities, + expiration: now + cacheMs, + }; -/** - * Set whether this client is a guest account. This method is experimental - * and may change without warning. - * @param {boolean} isGuest True if this is a guest account. - */ -MatrixClient.prototype.setGuest = function(isGuest) { - // EXPERIMENTAL: - // If the token is a macaroon, it should be encoded in it that it is a 'guest' - // access token, which means that the SDK can determine this entirely without - // the dev manually flipping this flag. - this._isGuest = isGuest; -}; + logger.log("Caching capabilities: ", capabilities); + return capabilities; + }); + } -/** - * Retry a backed off syncing request immediately. This should only be used when - * the user explicitly attempts to retry their lost connection. - * @return {boolean} True if this resulted in a request being retried. - */ -MatrixClient.prototype.retryImmediately = function() { - return this._syncApi.retryImmediately(); -}; + /** + * Initialise support for end-to-end encryption in this client + * + * You should call this method after creating the matrixclient, but *before* + * calling `startClient`, if you want to support end-to-end encryption. + * + * It will return a Promise which will resolve when the crypto layer has been + * successfully initialised. + */ + public async initCrypto(): Promise { + if (!isCryptoAvailable()) { + throw new Error( + `End-to-end encryption not supported in this js-sdk build: did ` + + `you remember to load the olm library?`, + ); + } -/** - * Return the global notification EventTimelineSet, if any - * - * @return {EventTimelineSet} the globl notification EventTimelineSet - */ -MatrixClient.prototype.getNotifTimelineSet = function() { - return this._notifTimelineSet; -}; + if (this.crypto) { + logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); + return; + } -/** - * Set the global notification EventTimelineSet - * - * @param {EventTimelineSet} notifTimelineSet - */ -MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) { - this._notifTimelineSet = notifTimelineSet; -}; + if (!this.sessionStore) { + // this is temporary, the sessionstore is supposed to be going away + throw new Error(`Cannot enable encryption: no sessionStore provided`); + } + if (!this.cryptoStore) { + // the cryptostore is provided by sdk.createClient, so this shouldn't happen + throw new Error(`Cannot enable encryption: no cryptoStore provided`); + } -/** - * Gets the capabilities of the homeserver. Always returns an object of - * capability keys and their options, which may be empty. - * @param {boolean} fresh True to ignore any cached values. - * @return {Promise} Resolves to the capabilities of the homeserver - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.getCapabilities = function(fresh=false) { - const now = new Date().getTime(); - - if (this._cachedCapabilities && !fresh) { - if (now < this._cachedCapabilities.expiration) { - logger.log("Returning cached capabilities"); - return Promise.resolve(this._cachedCapabilities.capabilities); - } - } - - // We swallow errors because we need a default object anyhow - return this._http.authedRequest( - undefined, "GET", "/capabilities", - ).catch((e) => { - logger.error(e); - return null; // otherwise consume the error - }).then((r) => { - if (!r) r = {}; - const capabilities = r["capabilities"] || {}; - - // If the capabilities missed the cache, cache it for a shorter amount - // of time to try and refresh them later. - const cacheMs = Object.keys(capabilities).length - ? CAPABILITIES_CACHE_MS - : 60000 + (Math.random() * 5000); - - this._cachedCapabilities = { - capabilities: capabilities, - expiration: now + cacheMs, - }; + logger.log("Crypto: Starting up crypto store..."); + await this.cryptoStore.startup(); - logger.log("Caching capabilities: ", capabilities); - return capabilities; - }); -}; + // initialise the list of encrypted rooms (whether or not crypto is enabled) + logger.log("Crypto: initialising roomlist..."); + await this.roomList.init(); -// Crypto bits -// =========== + const userId = this.getUserId(); + if (userId === null) { + throw new Error( + `Cannot enable encryption on MatrixClient with unknown userId: ` + + `ensure userId is passed in createClient().`, + ); + } + if (this.deviceId === null) { + throw new Error( + `Cannot enable encryption on MatrixClient with unknown deviceId: ` + + `ensure deviceId is passed in createClient().`, + ); + } -/** - * Initialise support for end-to-end encryption in this client - * - * You should call this method after creating the matrixclient, but *before* - * calling `startClient`, if you want to support end-to-end encryption. - * - * It will return a Promise which will resolve when the crypto layer has been - * successfully initialised. - */ -MatrixClient.prototype.initCrypto = async function() { - if (!isCryptoAvailable()) { - throw new Error( - `End-to-end encryption not supported in this js-sdk build: did ` + - `you remember to load the olm library?`, - ); - } - - if (this._crypto) { - logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); - return; - } - - if (!this._sessionStore) { - // this is temporary, the sessionstore is supposed to be going away - throw new Error(`Cannot enable encryption: no sessionStore provided`); - } - if (!this._cryptoStore) { - // the cryptostore is provided by sdk.createClient, so this shouldn't happen - throw new Error(`Cannot enable encryption: no cryptoStore provided`); - } - - logger.log("Crypto: Starting up crypto store..."); - await this._cryptoStore.startup(); - - // initialise the list of encrypted rooms (whether or not crypto is enabled) - logger.log("Crypto: initialising roomlist..."); - await this._roomList.init(); - - const userId = this.getUserId(); - if (userId === null) { - throw new Error( - `Cannot enable encryption on MatrixClient with unknown userId: ` + - `ensure userId is passed in createClient().`, - ); - } - if (this.deviceId === null) { - throw new Error( - `Cannot enable encryption on MatrixClient with unknown deviceId: ` + - `ensure deviceId is passed in createClient().`, - ); - } - - const crypto = new Crypto( - this, - this._sessionStore, - userId, this.deviceId, - this.store, - this._cryptoStore, - this._roomList, - this._verificationMethods, - ); - - this.reEmitter.reEmit(crypto, [ - "crypto.keyBackupFailed", - "crypto.keyBackupSessionsRemaining", - "crypto.roomKeyRequest", - "crypto.roomKeyRequestCancellation", - "crypto.warning", - "crypto.devicesUpdated", - "crypto.willUpdateDevices", - "deviceVerificationChanged", - "userTrustStatusChanged", - "crossSigning.keysChanged", - ]); - - logger.log("Crypto: initialising crypto object..."); - await crypto.init({ - exportedOlmDevice: this._exportedOlmDeviceToImport, - pickleKey: this.pickleKey, - }); - delete this._exportedOlmDeviceToImport; - - this.olmVersion = Crypto.getOlmVersion(); - - // if crypto initialisation was successful, tell it to attach its event - // handlers. - crypto.registerEventHandlers(this); - this._crypto = crypto; -}; - -/** - * Is end-to-end crypto enabled for this client. - * @return {boolean} True if end-to-end is enabled. - */ -MatrixClient.prototype.isCryptoEnabled = function() { - return this._crypto !== null; -}; - -/** - * Get the Ed25519 key for this device - * - * @return {?string} base64-encoded ed25519 key. Null if crypto is - * disabled. - */ -MatrixClient.prototype.getDeviceEd25519Key = function() { - if (!this._crypto) { - return null; - } - return this._crypto.getDeviceEd25519Key(); -}; - -/** - * Get the Curve25519 key for this device - * - * @return {?string} base64-encoded curve25519 key. Null if crypto is - * disabled. - */ -MatrixClient.prototype.getDeviceCurve25519Key = function() { - if (!this._crypto) { - return null; - } - return this._crypto.getDeviceCurve25519Key(); -}; - -/** - * Upload the device keys to the homeserver. - * @return {object} A promise that will resolve when the keys are uploaded. - */ -MatrixClient.prototype.uploadKeys = function() { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - return this._crypto.uploadDeviceKeys(); -}; - -/** - * Download the keys for a list of users and stores the keys in the session - * store. - * @param {Array} userIds The users to fetch. - * @param {bool} forceDownload Always download the keys even if cached. - * - * @return {Promise} A promise which resolves to a map userId->deviceId->{@link - * module:crypto~DeviceInfo|DeviceInfo}. - */ -MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) { - if (this._crypto === null) { - return Promise.reject(new Error("End-to-end encryption disabled")); - } - return this._crypto.downloadKeys(userIds, forceDownload); -}; - -/** - * Get the stored device keys for a user id - * - * @param {string} userId the user to list keys for. - * - * @return {module:crypto/deviceinfo[]} list of devices - */ -MatrixClient.prototype.getStoredDevicesForUser = function(userId) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.getStoredDevicesForUser(userId) || []; -}; - -/** - * Get the stored device key for a user id and device id - * - * @param {string} userId the user to list keys for. - * @param {string} deviceId unique identifier for the device - * - * @return {module:crypto/deviceinfo} device or null - */ -MatrixClient.prototype.getStoredDevice = function(userId, deviceId) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.getStoredDevice(userId, deviceId) || null; -}; - -/** - * Mark the given device as verified - * - * @param {string} userId owner of the device - * @param {string} deviceId unique identifier for the device or user's - * cross-signing public key ID. - * - * @param {boolean=} verified whether to mark the device as verified. defaults - * to 'true'. - * - * @returns {Promise} - * - * @fires module:client~event:MatrixClient"deviceVerificationChanged" - */ -MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified) { - if (verified === undefined) { - verified = true; - } - const prom = _setDeviceVerification(this, userId, deviceId, verified, null); - - // if one of the user's own devices is being marked as verified / unverified, - // check the key backup status, since whether or not we use this depends on - // whether it has a signature from a verified device - if (userId == this.credentials.userId) { - this._crypto.checkKeyBackup(); - } - return prom; -}; - -/** - * Mark the given device as blocked/unblocked - * - * @param {string} userId owner of the device - * @param {string} deviceId unique identifier for the device or user's - * cross-signing public key ID. - * - * @param {boolean=} blocked whether to mark the device as blocked. defaults - * to 'true'. - * - * @returns {Promise} - * - * @fires module:client~event:MatrixClient"deviceVerificationChanged" - */ -MatrixClient.prototype.setDeviceBlocked = function(userId, deviceId, blocked) { - if (blocked === undefined) { - blocked = true; - } - return _setDeviceVerification(this, userId, deviceId, null, blocked); -}; - -/** - * Mark the given device as known/unknown - * - * @param {string} userId owner of the device - * @param {string} deviceId unique identifier for the device or user's - * cross-signing public key ID. - * - * @param {boolean=} known whether to mark the device as known. defaults - * to 'true'. - * - * @returns {Promise} - * - * @fires module:client~event:MatrixClient"deviceVerificationChanged" - */ -MatrixClient.prototype.setDeviceKnown = function(userId, deviceId, known) { - if (known === undefined) { - known = true; - } - return _setDeviceVerification(this, userId, deviceId, null, null, known); -}; - -async function _setDeviceVerification( - client, userId, deviceId, verified, blocked, known, -) { - if (!client._crypto) { - throw new Error("End-to-End encryption disabled"); - } - await client._crypto.setDeviceVerification( - userId, deviceId, verified, blocked, known, - ); -} - -/** - * Request a key verification from another user, using a DM. - * - * @param {string} userId the user to request verification with - * @param {string} roomId the room to use for verification - * - * @returns {Promise} resolves to a VerificationRequest - * when the request has been sent to the other party. - */ -MatrixClient.prototype.requestVerificationDM = function(userId, roomId) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.requestVerificationDM(userId, roomId); -}; - -/** - * Finds a DM verification request that is already in progress for the given room id - * - * @param {string} roomId the room to use for verification - * - * @returns {module:crypto/verification/request/VerificationRequest?} the VerificationRequest that is in progress, if any - */ -MatrixClient.prototype.findVerificationRequestDMInProgress = function(roomId) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.findVerificationRequestDMInProgress(roomId); -}; - -/** - * Returns all to-device verification requests that are already in progress for the given user id - * - * @param {string} userId the ID of the user to query - * - * @returns {module:crypto/verification/request/VerificationRequest[]} the VerificationRequests that are in progress - */ -MatrixClient.prototype.getVerificationRequestsToDeviceInProgress = function(userId) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.getVerificationRequestsToDeviceInProgress(userId); -}; - -/** - * Request a key verification from another user. - * - * @param {string} userId the user to request verification with - * @param {Array} devices array of device IDs to send requests to. Defaults to - * all devices owned by the user - * - * @returns {Promise} resolves to a VerificationRequest - * when the request has been sent to the other party. - */ -MatrixClient.prototype.requestVerification = function(userId, devices) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.requestVerification(userId, devices); -}; - -/** - * Begin a key verification. - * - * @param {string} method the verification method to use - * @param {string} userId the user to verify keys with - * @param {string} deviceId the device to verify - * - * @returns {module:crypto/verification/Base} a verification object - */ -MatrixClient.prototype.beginKeyVerification = function( - method, userId, deviceId, -) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.beginKeyVerification(method, userId, deviceId); -}; - -/** - * Set the global override for whether the client should ever send encrypted - * messages to unverified devices. This provides the default for rooms which - * do not specify a value. - * - * @param {boolean} value whether to blacklist all unverified devices by default - */ -MatrixClient.prototype.setGlobalBlacklistUnverifiedDevices = function(value) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - this._crypto.setGlobalBlacklistUnverifiedDevices(value); -}; - -/** - * @return {boolean} whether to blacklist all unverified devices by default - */ -MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.getGlobalBlacklistUnverifiedDevices(); -}; - -/** - * Set whether sendMessage in a room with unknown and unverified devices - * should throw an error and not send them message. This has 'Global' for - * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently - * no room-level equivalent for this setting. - * - * This API is currently UNSTABLE and may change or be removed without notice. - * - * @param {boolean} value whether error on unknown devices - */ -MatrixClient.prototype.setGlobalErrorOnUnknownDevices = function(value) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - this._crypto.setGlobalErrorOnUnknownDevices(value); -}; - -/** - * @return {boolean} whether to error on unknown devices - * - * This API is currently UNSTABLE and may change or be removed without notice. - */ -MatrixClient.prototype.getGlobalErrorOnUnknownDevices = function() { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.getGlobalErrorOnUnknownDevices(); -}; - -/** - * Add methods that call the corresponding method in this._crypto - * - * @param {class} MatrixClient the class to add the method to - * @param {string} names the names of the methods to call - */ -function wrapCryptoFuncs(MatrixClient, names) { - for (const name of names) { - MatrixClient.prototype[name] = function(...args) { - if (!this._crypto) { // eslint-disable-line no-invalid-this - throw new Error("End-to-end encryption disabled"); - } - - return this._crypto[name](...args); // eslint-disable-line no-invalid-this - }; - } -} - -/** - * Get the user's cross-signing key ID. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#getCrossSigningId - * @param {string} [type=master] The type of key to get the ID of. One of - * "master", "self_signing", or "user_signing". Defaults to "master". - * - * @returns {string} the key ID - */ - -/** - * Get the cross signing information for a given user. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#getStoredCrossSigningForUser - * @param {string} userId the user ID to get the cross-signing info for. - * - * @returns {CrossSigningInfo} the cross signing information for the user. - */ - -/** - * Check whether a given user is trusted. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#checkUserTrust - * @param {string} userId The ID of the user to check. - * - * @returns {UserTrustLevel} - */ - -/** - * Check whether a given device is trusted. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#checkDeviceTrust - * @param {string} userId The ID of the user whose devices is to be checked. - * @param {string} deviceId The ID of the device to check - * - * @returns {DeviceTrustLevel} - */ - -/** - * Check the copy of our cross-signing key that we have in the device list and - * see if we can get the private key. If so, mark it as trusted. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#checkOwnCrossSigningTrust - */ - -/** - * Checks that a given cross-signing private key matches a given public key. - * This can be used by the getCrossSigningKey callback to verify that the - * private key it is about to supply is the one that was requested. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#checkCrossSigningPrivateKey - * @param {Uint8Array} privateKey The private key - * @param {string} expectedPublicKey The public key - * @returns {boolean} true if the key matches, otherwise false - */ - -/** - * Perform any background tasks that can be done before a message is ready to - * send, in order to speed up sending of the message. - * - * @function module:client~MatrixClient#prepareToEncrypt - * @param {module:models/room} room the room the event is in - */ - -/** - * Checks whether cross signing: - * - is enabled on this account and trusted by this device - * - has private keys either cached locally or stored in secret storage - * - * If this function returns false, bootstrapCrossSigning() can be used - * to fix things such that it returns true. That is to say, after - * bootstrapCrossSigning() completes successfully, this function should - * return true. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#isCrossSigningReady - * @return {bool} True if cross-signing is ready to be used on this device - */ - -/** - * Bootstrap cross-signing by creating keys if needed. If everything is already - * set up, then no changes are made, so this is safe to run to ensure - * cross-signing is ready for use. - * - * This function: - * - creates new cross-signing keys if they are not found locally cached nor in - * secret storage (if it has been setup) - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#bootstrapCrossSigning - * @param {function} opts.authUploadDeviceSigningKeys Function - * called to await an interactive auth flow when uploading device signing keys. - * @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys - * already exist. - * Args: - * {function} A function that makes the request requiring auth. Receives the - * auth data as an object. Can be called multiple times, first with an empty - * authDict, to obtain the flows. - */ - -wrapCryptoFuncs(MatrixClient, [ - "getCrossSigningId", - "getStoredCrossSigningForUser", - "checkUserTrust", - "checkDeviceTrust", - "checkOwnCrossSigningTrust", - "checkCrossSigningPrivateKey", - "legacyDeviceVerification", - "prepareToEncrypt", - "isCrossSigningReady", - "bootstrapCrossSigning", - "getCryptoTrustCrossSignedDevices", - "setCryptoTrustCrossSignedDevices", - "countSessionsNeedingBackup", -]); - -/** - * Get information about the encryption of an event - * - * @function module:client~MatrixClient#getEventEncryptionInfo - * - * @param {module:models/event.MatrixEvent} event event to be checked - * - * @return {object} An object with the fields: - * - encrypted: whether the event is encrypted (if not encrypted, some of the - * other properties may not be set) - * - senderKey: the sender's key - * - algorithm: the algorithm used to encrypt the event - * - authenticated: whether we can be sure that the owner of the senderKey - * sent the event - * - sender: the sender's device information, if available - * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match - * (only meaningful if `sender` is set) - */ - -/** - * Create a recovery key from a user-supplied passphrase. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#createRecoveryKeyFromPassphrase - * @param {string} password Passphrase string that can be entered by the user - * when restoring the backup as an alternative to entering the recovery key. - * Optional. - * @returns {Promise} Object with public key metadata, encoded private - * recovery key which should be disposed of after displaying to the user, - * and raw private key to avoid round tripping if needed. - */ - -/** - * Checks whether secret storage: - * - is enabled on this account - * - is storing cross-signing private keys - * - is storing session backup key (if enabled) - * - * If this function returns false, bootstrapSecretStorage() can be used - * to fix things such that it returns true. That is to say, after - * bootstrapSecretStorage() completes successfully, this function should - * return true. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#isSecretStorageReady - * @return {bool} True if secret storage is ready to be used on this device - */ - -/** - * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is - * already set up, then no changes are made, so this is safe to run to ensure secret - * storage is ready for use. - * - * This function - * - creates a new Secure Secret Storage key if no default key exists - * - if a key backup exists, it is migrated to store the key in the Secret - * Storage - * - creates a backup if none exists, and one is requested - * - migrates Secure Secret Storage to use the latest algorithm, if an outdated - * algorithm is found - * - * @function module:client~MatrixClient#bootstrapSecretStorage - * @param {function} [opts.createSecretStorageKey] Optional. Function - * called to await a secret storage key creation flow. - * Returns: - * {Promise} Object with public key metadata, encoded private - * recovery key which should be disposed of after displaying to the user, - * and raw private key to avoid round tripping if needed. - * @param {object} [opts.keyBackupInfo] The current key backup object. If passed, - * the passphrase and recovery key from this backup will be used. - * @param {bool} [opts.setupNewKeyBackup] If true, a new key backup version will be - * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo - * is supplied. - * @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist. - * @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's - * current key backup passphrase. Should return a promise that resolves with a Buffer - * containing the key, or rejects if the key cannot be obtained. - * Returns: - * {Promise} A promise which resolves to key creation data for - * SecretStorage#addKey: an object with `passphrase` etc fields. - */ - -/** - * Add a key for encrypting secrets. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#addSecretStorageKey - * @param {string} algorithm the algorithm used by the key - * @param {object} opts the options for the algorithm. The properties used - * depend on the algorithm given. - * @param {string} [keyName] the name of the key. If not given, a random - * name will be generated. - * - * @return {object} An object with: - * keyId: {string} the ID of the key - * keyInfo: {object} details about the key (iv, mac, passphrase) - */ - -/** - * Check whether we have a key with a given ID. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#hasSecretStorageKey - * @param {string} [keyId = default key's ID] The ID of the key to check - * for. Defaults to the default key ID if not provided. - * @return {boolean} Whether we have the key. - */ - -/** - * Store an encrypted secret on the server. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#storeSecret - * @param {string} name The name of the secret - * @param {string} secret The secret contents. - * @param {Array} keys The IDs of the keys to use to encrypt the secret or null/undefined - * to use the default (will throw if no default key is set). - */ - -/** - * Get a secret from storage. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#getSecret - * @param {string} name the name of the secret - * - * @return {string} the contents of the secret - */ - -/** - * Check if a secret is stored on the server. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#isSecretStored - * @param {string} name the name of the secret - * @param {boolean} checkKey check if the secret is encrypted by a trusted - * key - * - * @return {object?} map of key name to key info the secret is encrypted - * with, or null if it is not present or not encrypted with a trusted - * key - */ - -/** - * Request a secret from another device. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#requestSecret - * @param {string} name the name of the secret to request - * @param {string[]} devices the devices to request the secret from - * - * @return {string} the contents of the secret - */ - -/** - * Get the current default key ID for encrypting secrets. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#getDefaultSecretStorageKeyId - * - * @return {string} The default key ID or null if no default key ID is set - */ - -/** - * Set the current default key ID for encrypting secrets. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#setDefaultSecretStorageKeyId - * @param {string} keyId The new default key ID - */ - -/** - * Checks that a given secret storage private key matches a given public key. - * This can be used by the getSecretStorageKey callback to verify that the - * private key it is about to supply is the one that was requested. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @function module:client~MatrixClient#checkSecretStoragePrivateKey - * @param {Uint8Array} privateKey The private key - * @param {string} expectedPublicKey The public key - * @returns {boolean} true if the key matches, otherwise false - */ - -wrapCryptoFuncs(MatrixClient, [ - "getEventEncryptionInfo", - "createRecoveryKeyFromPassphrase", - "isSecretStorageReady", - "bootstrapSecretStorage", - "addSecretStorageKey", - "hasSecretStorageKey", - "storeSecret", - "getSecret", - "isSecretStored", - "requestSecret", - "getDefaultSecretStorageKeyId", - "setDefaultSecretStorageKeyId", - "checkSecretStorageKey", - "checkSecretStoragePrivateKey", -]); - -/** - * Get e2e information on the device that sent an event - * - * @param {MatrixEvent} event event to be checked - * - * @return {Promise} - */ -MatrixClient.prototype.getEventSenderDeviceInfo = async function(event) { - if (!this._crypto) { - return null; - } - - return this._crypto.getEventSenderDeviceInfo(event); -}; - -/** - * Check if the sender of an event is verified - * - * @param {MatrixEvent} event event to be checked - * - * @return {boolean} true if the sender of this event has been verified using - * {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}. - */ -MatrixClient.prototype.isEventSenderVerified = async function(event) { - const device = await this.getEventSenderDeviceInfo(event); - if (!device) { - return false; - } - return device.isVerified(); -}; - -/** - * Cancel a room key request for this event if one is ongoing and resend the - * request. - * @param {MatrixEvent} event event of which to cancel and resend the room - * key request. - * @return {Promise} A promise that will resolve when the key request is queued - */ -MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function(event) { - return event.cancelAndResendKeyRequest(this._crypto, this.getUserId()); -}; - -/** - * Enable end-to-end encryption for a room. This does not modify room state. - * Any messages sent before the returned promise resolves will be sent unencrypted. - * @param {string} roomId The room ID to enable encryption in. - * @param {object} config The encryption config for the room. - * @return {Promise} A promise that will resolve when encryption is set up. - */ -MatrixClient.prototype.setRoomEncryption = function(roomId, config) { - if (!this._crypto) { - throw new Error("End-to-End encryption disabled"); - } - return this._crypto.setRoomEncryption(roomId, config); -}; - -/** - * Whether encryption is enabled for a room. - * @param {string} roomId the room id to query. - * @return {bool} whether encryption is enabled. - */ -MatrixClient.prototype.isRoomEncrypted = function(roomId) { - const room = this.getRoom(roomId); - if (!room) { - // we don't know about this room, so can't determine if it should be - // encrypted. Let's assume not. - return false; - } - - // if there is an 'm.room.encryption' event in this room, it should be - // encrypted (independently of whether we actually support encryption) - const ev = room.currentState.getStateEvents("m.room.encryption", ""); - if (ev) { - return true; - } - - // we don't have an m.room.encrypted event, but that might be because - // the server is hiding it from us. Check the store to see if it was - // previously encrypted. - return this._roomList.isRoomEncrypted(roomId); -}; - -/** - * Forces the current outbound group session to be discarded such - * that another one will be created next time an event is sent. - * - * @param {string} roomId The ID of the room to discard the session for - * - * This should not normally be necessary. - */ -MatrixClient.prototype.forceDiscardSession = function(roomId) { - if (!this._crypto) { - throw new Error("End-to-End encryption disabled"); - } - this._crypto.forceDiscardSession(roomId); -}; - -/** - * Get a list containing all of the room keys - * - * This should be encrypted before returning it to the user. - * - * @return {Promise} a promise which resolves to a list of - * session export objects - */ -MatrixClient.prototype.exportRoomKeys = function() { - if (!this._crypto) { - return Promise.reject(new Error("End-to-end encryption disabled")); - } - return this._crypto.exportRoomKeys(); -}; - -/** - * Import a list of room keys previously exported by exportRoomKeys - * - * @param {Object[]} keys a list of session export objects - * @param {Object} opts - * @param {Function} opts.progressCallback called with an object that has a "stage" param - * - * @return {Promise} a promise which resolves when the keys - * have been imported - */ -MatrixClient.prototype.importRoomKeys = function(keys, opts) { - if (!this._crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this._crypto.importRoomKeys(keys, opts); -}; - -/** - * Force a re-check of the local key backup status against - * what's on the server. - * - * @returns {Object} Object with backup info (as returned by - * getKeyBackupVersion) in backupInfo and - * trust information (as returned by isKeyBackupTrusted) - * in trustInfo. - */ -MatrixClient.prototype.checkKeyBackup = function() { - return this._crypto.checkKeyBackup(); -}; - -/** - * Get information about the current key backup. - * @returns {Promise} Information object from API or null - */ -MatrixClient.prototype.getKeyBackupVersion = function() { - return this._http.authedRequest( - undefined, "GET", "/room_keys/version", undefined, undefined, - { prefix: PREFIX_UNSTABLE }, - ).then((res) => { - if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) { - const err = "Unknown backup algorithm: " + res.algorithm; - return Promise.reject(err); - } else if (!(typeof res.auth_data === "object") - || !res.auth_data.public_key) { - const err = "Invalid backup data returned"; - return Promise.reject(err); - } else { - return res; - } - }).catch((e) => { - if (e.errcode === 'M_NOT_FOUND') { - return null; - } else { - throw e; - } - }); -}; - -/** - * @param {object} info key backup info dict from getKeyBackupVersion() - * @return {object} { - * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device - * sigs: [ - * valid: [bool], - * device: [DeviceInfo], - * ] - * } - */ -MatrixClient.prototype.isKeyBackupTrusted = function(info) { - return this._crypto.isKeyBackupTrusted(info); -}; - -/** - * @returns {bool} true if the client is configured to back up keys to - * the server, otherwise false. If we haven't completed a successful check - * of key backup status yet, returns null. - */ -MatrixClient.prototype.getKeyBackupEnabled = function() { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - if (!this._crypto._checkedForBackup) { - return null; - } - return Boolean(this._crypto.backupKey); -}; - -/** - * Enable backing up of keys, using data previously returned from - * getKeyBackupVersion. - * - * @param {object} info Backup information object as returned by getKeyBackupVersion - */ -MatrixClient.prototype.enableKeyBackup = function(info) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - this._crypto.backupInfo = info; - if (this._crypto.backupKey) this._crypto.backupKey.free(); - this._crypto.backupKey = new global.Olm.PkEncryption(); - this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); - - this.emit('crypto.keyBackupStatus', true); - - // There may be keys left over from a partially completed backup, so - // schedule a send to check. - this._crypto.scheduleKeyBackupSend(); -}; - -/** - * Disable backing up of keys. - */ -MatrixClient.prototype.disableKeyBackup = function() { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - this._crypto.backupInfo = null; - if (this._crypto.backupKey) this._crypto.backupKey.free(); - this._crypto.backupKey = null; - - this.emit('crypto.keyBackupStatus', false); -}; - -/** - * Set up the data required to create a new backup version. The backup version - * will not be created and enabled until createKeyBackupVersion is called. - * - * @param {string} password Passphrase string that can be entered by the user - * when restoring the backup as an alternative to entering the recovery key. - * Optional. - * @param {boolean} [opts.secureSecretStorage = false] Whether to use Secure - * Secret Storage to store the key encrypting key backups. - * Optional, defaults to false. - * - * @returns {Promise} Object that can be passed to createKeyBackupVersion and - * additionally has a 'recovery_key' member with the user-facing recovery key string. - */ -MatrixClient.prototype.prepareKeyBackupVersion = async function( - password, - { secureSecretStorage = false } = {}, -) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - const { keyInfo, encodedPrivateKey, privateKey } = - await this.createRecoveryKeyFromPassphrase(password); - - if (secureSecretStorage) { - await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey)); - logger.info("Key backup private key stored in secret storage"); - } - - // Reshape objects into form expected for key backup - const authData = { - public_key: keyInfo.pubkey, - }; - if (keyInfo.passphrase) { - authData.private_key_salt = keyInfo.passphrase.salt; - authData.private_key_iterations = keyInfo.passphrase.iterations; - } - return { - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: authData, - recovery_key: encodedPrivateKey, - }; -}; - -/** - * Check whether the key backup private key is stored in secret storage. - * @return {Promise} map of key name to key info the secret is - * encrypted with, or null if it is not present or not encrypted with a - * trusted key - */ -MatrixClient.prototype.isKeyBackupKeyStored = async function() { - return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */); -}; - -/** - * Create a new key backup version and enable it, using the information return - * from prepareKeyBackupVersion. - * - * @param {object} info Info object from prepareKeyBackupVersion - * @returns {Promise} Object with 'version' param indicating the version created - */ -MatrixClient.prototype.createKeyBackupVersion = async function(info) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - const data = { - algorithm: info.algorithm, - auth_data: info.auth_data, - }; - - // Sign the backup auth data with the device key for backwards compat with - // older devices with cross-signing. This can probably go away very soon in - // favour of just signing with the cross-singing master key. - await this._crypto._signObject(data.auth_data); - - if ( - this._cryptoCallbacks.getCrossSigningKey && - this._crypto._crossSigningInfo.getId() - ) { - // now also sign the auth data with the cross-signing master key - // we check for the callback explicitly here because we still want to be able - // to create an un-cross-signed key backup if there is a cross-signing key but - // no callback supplied. - await this._crypto._crossSigningInfo.signObject(data.auth_data, "master"); - } - - const res = await this._http.authedRequest( - undefined, "POST", "/room_keys/version", undefined, data, - { prefix: PREFIX_UNSTABLE }, - ); - - // We could assume everything's okay and enable directly, but this ensures - // we run the same signature verification that will be used for future - // sessions. - await this.checkKeyBackup(); - if (!this.getKeyBackupEnabled()) { - logger.error("Key backup not usable even though we just created it"); - } - - return res; -}; - -MatrixClient.prototype.deleteKeyBackupVersion = function(version) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - // If we're currently backing up to this backup... stop. - // (We start using it automatically in createKeyBackupVersion - // so this is symmetrical). - if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) { - this.disableKeyBackup(); - } - - const path = utils.encodeUri("/room_keys/version/$version", { - $version: version, - }); - - return this._http.authedRequest( - undefined, "DELETE", path, undefined, undefined, - { prefix: PREFIX_UNSTABLE }, - ); -}; - -MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) { - let path; - if (sessionId !== undefined) { - path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { - $roomId: roomId, - $sessionId: sessionId, - }); - } else if (roomId !== undefined) { - path = utils.encodeUri("/room_keys/keys/$roomId", { - $roomId: roomId, - }); - } else { - path = "/room_keys/keys"; - } - const queryData = version === undefined ? undefined : { version: version }; - return { - path: path, - queryData: queryData, - }; -}; - -/** - * Back up session keys to the homeserver. - * @param {string} roomId ID of the room that the keys are for Optional. - * @param {string} sessionId ID of the session that the keys are for Optional. - * @param {integer} version backup version Optional. - * @param {object} data Object keys to send - * @return {Promise} a promise that will resolve when the keys - * are uploaded - */ -MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - const path = this._makeKeyBackupPath(roomId, sessionId, version); - return this._http.authedRequest( - undefined, "PUT", path.path, path.queryData, data, - { prefix: PREFIX_UNSTABLE }, - ); -}; - -/** - * Marks all group sessions as needing to be backed up and schedules them to - * upload in the background as soon as possible. - */ -MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function() { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - await this._crypto.scheduleAllGroupSessionsForBackup(); -}; - -/** - * Marks all group sessions as needing to be backed up without scheduling - * them to upload in the background. - * @returns {Promise} Resolves to the number of sessions requiring a backup. - */ -MatrixClient.prototype.flagAllGroupSessionsForBackup = function() { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - return this._crypto.flagAllGroupSessionsForBackup(); -}; - -MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { - try { - decodeRecoveryKey(recoveryKey); - return true; - } catch (e) { - return false; - } -}; - -/** - * Get the raw key for a key backup from the password - * Used when migrating key backups into SSSS - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @param {string} password Passphrase - * @param {object} backupInfo Backup metadata from `checkKeyBackup` - * @return {Promise} key backup key - */ -MatrixClient.prototype.keyBackupKeyFromPassword = function( - password, backupInfo, -) { - return keyFromAuthData(backupInfo.auth_data, password); -}; - -/** - * Get the raw key for a key backup from the recovery key - * Used when migrating key backups into SSSS - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @param {string} recoveryKey The recovery key - * @return {Uint8Array} key backup key - */ -MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function(recoveryKey) { - return decodeRecoveryKey(recoveryKey); -}; - -MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; - -/** - * Restore from an existing key backup via a passphrase. - * - * @param {string} password Passphrase - * @param {string} [targetRoomId] Room ID to target a specific room. - * Restores all rooms if omitted. - * @param {string} [targetSessionId] Session ID to target a specific session. - * Restores all sessions if omitted. - * @param {object} backupInfo Backup metadata from `checkKeyBackup` - * @param {object} opts Optional params such as callbacks - * @return {Promise} Status of restoration with `total` and `imported` - * key counts. - */ -MatrixClient.prototype.restoreKeyBackupWithPassword = async function( - password, targetRoomId, targetSessionId, backupInfo, opts, -) { - const privKey = await keyFromAuthData(backupInfo.auth_data, password); - return this._restoreKeyBackup( - privKey, targetRoomId, targetSessionId, backupInfo, opts, - ); -}; - -/** - * Restore from an existing key backup via a private key stored in secret - * storage. - * - * @param {object} backupInfo Backup metadata from `checkKeyBackup` - * @param {string} [targetRoomId] Room ID to target a specific room. - * Restores all rooms if omitted. - * @param {string} [targetSessionId] Session ID to target a specific session. - * Restores all sessions if omitted. - * @param {object} opts Optional params such as callbacks - * @return {Promise} Status of restoration with `total` and `imported` - * key counts. - */ -MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function( - backupInfo, targetRoomId, targetSessionId, opts, -) { - const storedKey = await this.getSecret("m.megolm_backup.v1"); - - // ensure that the key is in the right format. If not, fix the key and - // store the fixed version - const fixedKey = fixBackupKey(storedKey); - if (fixedKey) { - const [keyId] = await this._crypto.getSecretStorageKey(); - await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); - } - - const privKey = decodeBase64(fixedKey || storedKey); - return this._restoreKeyBackup( - privKey, targetRoomId, targetSessionId, backupInfo, opts, - ); -}; - -/** - * Restore from an existing key backup via an encoded recovery key. - * - * @param {string} recoveryKey Encoded recovery key - * @param {string} [targetRoomId] Room ID to target a specific room. - * Restores all rooms if omitted. - * @param {string} [targetSessionId] Session ID to target a specific session. - * Restores all sessions if omitted. - * @param {object} backupInfo Backup metadata from `checkKeyBackup` - * @param {object} opts Optional params such as callbacks - - * @return {Promise} Status of restoration with `total` and `imported` - * key counts. - */ -MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( - recoveryKey, targetRoomId, targetSessionId, backupInfo, opts, -) { - const privKey = decodeRecoveryKey(recoveryKey); - return this._restoreKeyBackup( - privKey, targetRoomId, targetSessionId, backupInfo, opts, - ); -}; - -/** - * Restore from an existing key backup using a cached key, or fail - * - * @param {string} [targetRoomId] Room ID to target a specific room. - * Restores all rooms if omitted. - * @param {string} [targetSessionId] Session ID to target a specific session. - * Restores all sessions if omitted. - * @param {object} backupInfo Backup metadata from `checkKeyBackup` - * @param {object} opts Optional params such as callbacks - * @return {Promise} Status of restoration with `total` and `imported` - * key counts. - */ -MatrixClient.prototype.restoreKeyBackupWithCache = async function( - targetRoomId, targetSessionId, backupInfo, opts, -) { - const privKey = await this._crypto.getSessionBackupPrivateKey(); - if (!privKey) { - throw new Error("Couldn't get key"); - } - return this._restoreKeyBackup( - privKey, targetRoomId, targetSessionId, backupInfo, opts, - ); -}; - -MatrixClient.prototype._restoreKeyBackup = function( - privKey, targetRoomId, targetSessionId, backupInfo, - { - cacheCompleteCallback, // For sequencing during tests - progressCallback, - }={}, -) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - let totalKeyCount = 0; - let keys = []; - - const path = this._makeKeyBackupPath( - targetRoomId, targetSessionId, backupInfo.version, - ); - - const decryption = new global.Olm.PkDecryption(); - let backupPubKey; - try { - backupPubKey = decryption.init_with_private_key(privKey); - } catch (e) { - decryption.free(); - throw e; - } - - // If the pubkey computed from the private data we've been given - // doesn't match the one in the auth_data, the user has enetered - // a different recovery key / the wrong passphrase. - if (backupPubKey !== backupInfo.auth_data.public_key) { - return Promise.reject({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }); - } - - // Cache the key, if possible. - // This is async. - this._crypto.storeSessionBackupPrivateKey(privKey) - .catch((e) => { - logger.warn("Error caching session backup key:", e); - }).then(cacheCompleteCallback); - - if (progressCallback) { - progressCallback({ - stage: "fetch", - }); - } - - return this._http.authedRequest( - undefined, "GET", path.path, path.queryData, undefined, - { prefix: PREFIX_UNSTABLE }, - ).then((res) => { - if (res.rooms) { - for (const [roomId, roomData] of Object.entries(res.rooms)) { - if (!roomData.sessions) continue; - - totalKeyCount += Object.keys(roomData.sessions).length; - const roomKeys = keysFromRecoverySession( - roomData.sessions, decryption, roomId, - ); - for (const k of roomKeys) { - k.room_id = roomId; - keys.push(k); - } - } - } else if (res.sessions) { - totalKeyCount = Object.keys(res.sessions).length; - keys = keysFromRecoverySession( - res.sessions, decryption, targetRoomId, keys, - ); - } else { - totalKeyCount = 1; - try { - const key = keyFromRecoverySession(res, decryption); - key.room_id = targetRoomId; - key.session_id = targetSessionId; - keys.push(key); - } catch (e) { - logger.log("Failed to decrypt megolm session from backup", e); - } - } + const crypto = new Crypto( + this, + this.sessionStore, + userId, this.deviceId, + this.store, + this.cryptoStore, + this.roomList, + this.verificationMethods, + ); - return this.importRoomKeys(keys, { - progressCallback, - untrusted: true, - source: "backup", + this.reEmitter.reEmit(crypto, [ + "crypto.keyBackupFailed", + "crypto.keyBackupSessionsRemaining", + "crypto.roomKeyRequest", + "crypto.roomKeyRequestCancellation", + "crypto.warning", + "crypto.devicesUpdated", + "crypto.willUpdateDevices", + "deviceVerificationChanged", + "userTrustStatusChanged", + "crossSigning.keysChanged", + ]); + + logger.log("Crypto: initialising crypto object..."); + await crypto.init({ + exportedOlmDevice: this.exportedOlmDeviceToImport, + pickleKey: this.pickleKey, }); - }).then(() => { - return this._crypto.setTrustedBackupPubKey(backupPubKey); - }).then(() => { - return { total: totalKeyCount, imported: keys.length }; - }).finally(() => { - decryption.free(); - }); -}; - -MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - const path = this._makeKeyBackupPath(roomId, sessionId, version); - return this._http.authedRequest( - undefined, "DELETE", path.path, path.queryData, undefined, - { prefix: PREFIX_UNSTABLE }, - ); -}; + delete this.exportedOlmDeviceToImport; + + this.olmVersion = Crypto.getOlmVersion(); + + // if crypto initialisation was successful, tell it to attach its event + // handlers. + crypto.registerEventHandlers(this); + this.crypto = crypto; + } + + /** + * Is end-to-end crypto enabled for this client. + * @return {boolean} True if end-to-end is enabled. + */ + public isCryptoEnabled(): boolean { + return !!this.crypto; + } + + /** + * Get the Ed25519 key for this device + * + * @return {?string} base64-encoded ed25519 key. Null if crypto is + * disabled. + */ + public getDeviceEd25519Key(): string { + if (!this.crypto) return null; + return this.crypto.getDeviceEd25519Key(); + } + + /** + * Get the Curve25519 key for this device + * + * @return {?string} base64-encoded curve25519 key. Null if crypto is + * disabled. + */ + public getDeviceCurve25519Key(): string { + if (!this.crypto) return null; + return this.crypto.getDeviceCurve25519Key(); + } + + /** + * Upload the device keys to the homeserver. + * @return {Promise} A promise that will resolve when the keys are uploaded. + */ + public uploadKeys(): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } -/** - * Share shared-history decryption keys with the given users. - * - * @param {string} roomId the room for which keys should be shared. - * @param {array} userIds a list of users to share with. The keys will be sent to - * all of the user's current devices. - */ -MatrixClient.prototype.sendSharedHistoryKeys = async function(roomId, userIds) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); + return this.crypto.uploadDeviceKeys(); + } + + /** + * Download the keys for a list of users and stores the keys in the session + * store. + * @param {Array} userIds The users to fetch. + * @param {bool} forceDownload Always download the keys even if cached. + * + * @return {Promise} A promise which resolves to a map userId->deviceId->{@link + * module:crypto~DeviceInfo|DeviceInfo}. + */ + public downloadKeys(userIds: string[], forceDownload: boolean): Promise>> { + if (!this.crypto) { + return Promise.reject(new Error("End-to-end encryption disabled")); + } + return this.crypto.downloadKeys(userIds, forceDownload); + } + + /** + * Get the stored device keys for a user id + * + * @param {string} userId the user to list keys for. + * + * @return {module:crypto/deviceinfo[]} list of devices + */ + public getStoredDevicesForUser(userId: string): DeviceInfo[] { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getStoredDevicesForUser(userId) || []; + } + + /** + * Get the stored device key for a user id and device id + * + * @param {string} userId the user to list keys for. + * @param {string} deviceId unique identifier for the device + * + * @return {module:crypto/deviceinfo} device or null + */ + public getStoredDevice(userId: string, deviceId: string): DeviceInfo { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getStoredDevice(userId, deviceId) || null; + } + + /** + * Mark the given device as verified + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device or user's + * cross-signing public key ID. + * + * @param {boolean=} verified whether to mark the device as verified. defaults + * to 'true'. + * + * @returns {Promise} + * + * @fires module:client~event:MatrixClient"deviceVerificationChanged" + */ + public setDeviceVerified(userId: string, deviceId: string, verified = true): Promise { + const prom = this.setDeviceVerification(userId, deviceId, verified, null, null); + + // if one of the user's own devices is being marked as verified / unverified, + // check the key backup status, since whether or not we use this depends on + // whether it has a signature from a verified device + if (userId == this.credentials.userId) { + this.crypto.checkKeyBackup(); + } + return prom; + } + + /** + * Mark the given device as blocked/unblocked + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device or user's + * cross-signing public key ID. + * + * @param {boolean=} blocked whether to mark the device as blocked. defaults + * to 'true'. + * + * @returns {Promise} + * + * @fires module:client~event:MatrixClient"deviceVerificationChanged" + */ + public setDeviceBlocked(userId: string, deviceId: string, blocked = true): Promise { + return this.setDeviceVerification(userId, deviceId, null, blocked, null); + } + + /** + * Mark the given device as known/unknown + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device or user's + * cross-signing public key ID. + * + * @param {boolean=} known whether to mark the device as known. defaults + * to 'true'. + * + * @returns {Promise} + * + * @fires module:client~event:MatrixClient"deviceVerificationChanged" + */ + public setDeviceKnown(userId: string, deviceId: string, known = true): Promise { + return this.setDeviceVerification(userId, deviceId, null, null, known); + } + + private async setDeviceVerification(userId: string, deviceId: string, verified: boolean, blocked: boolean, known: boolean): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + await this.crypto.setDeviceVerification(userId, deviceId, verified, blocked, known); + } + + /** + * Request a key verification from another user, using a DM. + * + * @param {string} userId the user to request verification with + * @param {string} roomId the room to use for verification + * + * @returns {Promise} resolves to a VerificationRequest + * when the request has been sent to the other party. + */ + public requestVerificationDM(userId: string, roomId: string): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.requestVerificationDM(userId, roomId); + } + + /** + * Finds a DM verification request that is already in progress for the given room id + * + * @param {string} roomId the room to use for verification + * + * @returns {module:crypto/verification/request/VerificationRequest?} the VerificationRequest that is in progress, if any + */ + public findVerificationRequestDMInProgress(roomId: string): VerificationRequest { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.findVerificationRequestDMInProgress(roomId); + } + + /** + * Returns all to-device verification requests that are already in progress for the given user id + * + * @param {string} userId the ID of the user to query + * + * @returns {module:crypto/verification/request/VerificationRequest[]} the VerificationRequests that are in progress + */ + public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getVerificationRequestsToDeviceInProgress(userId); + } + + /** + * Request a key verification from another user. + * + * @param {string} userId the user to request verification with + * @param {Array} devices array of device IDs to send requests to. Defaults to + * all devices owned by the user + * + * @returns {Promise} resolves to a VerificationRequest + * when the request has been sent to the other party. + */ + public requestVerification(userId: string, devices: string[]): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.requestVerification(userId, devices); + } + + /** + * Begin a key verification. + * + * @param {string} method the verification method to use + * @param {string} userId the user to verify keys with + * @param {string} deviceId the device to verify + * + * @returns {Verification} a verification object + */ + public beginKeyVerification(method: string, userId: string, deviceId: string): Verification { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.beginKeyVerification(method, userId, deviceId); + } + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. This provides the default for rooms which + * do not specify a value. + * + * @param {boolean} value whether to blacklist all unverified devices by default + */ + public setGlobalBlacklistUnverifiedDevices(value: boolean) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.setGlobalBlacklistUnverifiedDevices(value); } - const roomEncryption = this._roomList.getRoomEncryption(roomId); - if (!roomEncryption) { - // unknown room, or unencrypted room - logger.error("Unknown room. Not sharing decryption keys"); - return; + /** + * @return {boolean} whether to blacklist all unverified devices by default + */ + public getGlobalBlacklistUnverifiedDevices(): boolean { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getGlobalBlacklistUnverifiedDevices(); + } + + /** + * Set whether sendMessage in a room with unknown and unverified devices + * should throw an error and not send them message. This has 'Global' for + * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently + * no room-level equivalent for this setting. + * + * This API is currently UNSTABLE and may change or be removed without notice. + * + * @param {boolean} value whether error on unknown devices + */ + public setGlobalErrorOnUnknownDevices(value: boolean) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.setGlobalErrorOnUnknownDevices(value); } - const deviceInfos = await this._crypto.downloadKeys(userIds); - const devicesByUser = {}; - for (const [userId, devices] of Object.entries(deviceInfos)) { - devicesByUser[userId] = Object.values(devices); + /** + * @return {boolean} whether to error on unknown devices + * + * This API is currently UNSTABLE and may change or be removed without notice. + */ + public getGlobalErrorOnUnknownDevices(): boolean { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getGlobalErrorOnUnknownDevices(); + } + + /** + * Get the user's cross-signing key ID. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {CrossSigningKey} [type=master] The type of key to get the ID of. One of + * "master", "self_signing", or "user_signing". Defaults to "master". + * + * @returns {string} the key ID + */ + public getCrossSigningId(type = CrossSigningKey.Master): string { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getCrossSigningId(type); + } + + /** + * Get the cross signing information for a given user. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {string} userId the user ID to get the cross-signing info for. + * + * @returns {CrossSigningInfo} the cross signing information for the user. + */ + public getStoredCrossSigningForUser(userId: string): CrossSigningInfo { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getStoredCrossSigningForUser(userId); + } + + /** + * Check whether a given user is trusted. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {string} userId The ID of the user to check. + * + * @returns {UserTrustLevel} + */ + public checkUserTrust(userId: string): UserTrustLevel { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.checkUserTrust(userId); + } + + /** + * Check whether a given device is trusted. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#checkDeviceTrust + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {string} deviceId The ID of the device to check + * + * @returns {IDeviceTrustLevel} + */ + public checkDeviceTrust(userId: string, deviceId: string): IDeviceTrustLevel { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.checkDeviceTrust(userId, deviceId); } - const alg = this._crypto._getRoomDecryptor(roomId, roomEncryption.algorithm); - if (alg.sendSharedHistoryInboundSessions) { - await alg.sendSharedHistoryInboundSessions(devicesByUser); - } else { - logger.warning("Algorithm does not support sharing previous keys", roomEncryption.algorithm); + /** + * Check the copy of our cross-signing key that we have in the device list and + * see if we can get the private key. If so, mark it as trusted. + */ + public checkOwnCrossSigningTrust() { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.checkOwnCrossSigningTrust(); + } + + /** + * Checks that a given cross-signing private key matches a given public key. + * This can be used by the getCrossSigningKey callback to verify that the + * private key it is about to supply is the one that was requested. + * @param {Uint8Array} privateKey The private key + * @param {string} expectedPublicKey The public key + * @returns {boolean} true if the key matches, otherwise false + */ + public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.checkCrossSigningPrivateKey(privateKey, expectedPublicKey); } -}; - -// Group ops -// ========= -// Operations on groups that come down the sync stream (ie. ones the -// user is a member of or invited to) - -/** - * Get the group for the given group ID. - * This function will return a valid group for any group for which a Group event - * has been emitted. - * @param {string} groupId The group ID - * @return {Group} The Group or null if the group is not known or there is no data store. - */ -MatrixClient.prototype.getGroup = function(groupId) { - return this.store.getGroup(groupId); -}; - -/** - * Retrieve all known groups. - * @return {Group[]} A list of groups, or an empty list if there is no data store. - */ -MatrixClient.prototype.getGroups = function() { - return this.store.getGroups(); -}; - -/** - * Get the config for the media repository. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves with an object containing the config. - */ -MatrixClient.prototype.getMediaConfig = function(callback) { - return this._http.authedRequest( - callback, "GET", "/config", undefined, undefined, { - prefix: PREFIX_MEDIA_R0, - }, - ); -}; - -// Room ops -// ======== - -/** - * Get the room for the given room ID. - * This function will return a valid room for any room for which a Room event - * has been emitted. Note in particular that other events, eg. RoomState.members - * will be emitted for a room before this function will return the given room. - * @param {string} roomId The room ID - * @return {Room} The Room or null if it doesn't exist or there is no data store. - */ -MatrixClient.prototype.getRoom = function(roomId) { - return this.store.getRoom(roomId); -}; - -/** - * Retrieve all known rooms. - * @return {Room[]} A list of rooms, or an empty list if there is no data store. - */ -MatrixClient.prototype.getRooms = function() { - return this.store.getRooms(); -}; -/** - * Retrieve all rooms that should be displayed to the user - * This is essentially getRooms() with some rooms filtered out, eg. old versions - * of rooms that have been replaced or (in future) other rooms that have been - * marked at the protocol level as not to be displayed to the user. - * @return {Room[]} A list of rooms, or an empty list if there is no data store. - */ -MatrixClient.prototype.getVisibleRooms = function() { - const allRooms = this.store.getRooms(); - - const replacedRooms = new Set(); - for (const r of allRooms) { - const createEvent = r.currentState.getStateEvents('m.room.create', ''); - // invites are included in this list and we don't know their create events yet - if (createEvent) { - const predecessor = createEvent.getContent()['predecessor']; - if (predecessor && predecessor['room_id']) { - replacedRooms.add(predecessor['room_id']); - } + public legacyDeviceVerification(userId: string, deviceId: string, method: string): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); } + return this.crypto.legacyDeviceVerification(userId, deviceId, method); } - return allRooms.filter((r) => { - const tombstone = r.currentState.getStateEvents('m.room.tombstone', ''); - if (tombstone && replacedRooms.has(r.roomId)) { - return false; + /** + * Perform any background tasks that can be done before a message is ready to + * send, in order to speed up sending of the message. + * @param {module:models/room} room the room the event is in + */ + public prepareToEncrypt(room: Room) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); } - return true; - }); -}; - -/** - * Retrieve a user. - * @param {string} userId The user ID to retrieve. - * @return {?User} A user or null if there is no data store or the user does - * not exist. - */ -MatrixClient.prototype.getUser = function(userId) { - return this.store.getUser(userId); -}; - -/** - * Retrieve all known users. - * @return {User[]} A list of users, or an empty list if there is no data store. - */ -MatrixClient.prototype.getUsers = function() { - return this.store.getUsers(); -}; - -// User Account Data operations -// ============================ - -/** - * Set account data event for the current user. - * It will retry the request up to 5 times. - * @param {string} eventType The event type - * @param {Object} contents the contents object for the event - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setAccountData = function(eventType, contents, callback) { - const path = utils.encodeUri("/user/$userId/account_data/$type", { - $userId: this.credentials.userId, - $type: eventType, - }); - const promise = retryNetworkOperation(5, () => { - return this._http.authedRequest(undefined, "PUT", path, undefined, contents); - }); - if (callback) { - promise.then(result => callback(null, result), callback); - } - return promise; -}; - -/** - * Get account data event of given type for the current user. - * @param {string} eventType The event type - * @return {?object} The contents of the given account data event - */ -MatrixClient.prototype.getAccountData = function(eventType) { - return this.store.getAccountData(eventType); -}; - -/** - * Get account data event of given type for the current user. This variant - * gets account data directly from the homeserver if the local store is not - * ready, which can be useful very early in startup before the initial sync. - * @param {string} eventType The event type - * @return {Promise} Resolves: The contents of the given account - * data event. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.getAccountDataFromServer = async function(eventType) { - if (this.isInitialSyncComplete()) { - const event = this.store.getAccountData(eventType); - if (!event) { - return null; + return this.crypto.prepareToEncrypt(room); + } + + /** + * Checks whether cross signing: + * - is enabled on this account and trusted by this device + * - has private keys either cached locally or stored in secret storage + * + * If this function returns false, bootstrapCrossSigning() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapCrossSigning() completes successfully, this function should + * return true. + * @return {bool} True if cross-signing is ready to be used on this device + */ + public isCrossSigningReady(): boolean { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); } - // The network version below returns just the content, so this branch - // does the same to match. - return event.getContent(); - } - const path = utils.encodeUri("/user/$userId/account_data/$type", { - $userId: this.credentials.userId, - $type: eventType, - }); - try { - const result = await this._http.authedRequest( - undefined, "GET", path, undefined, - ); - return result; - } catch (e) { - if (e.data && e.data.errcode === 'M_NOT_FOUND') { - return null; + return this.crypto.isCrossSigningReady(); + } + + /** + * Bootstrap cross-signing by creating keys if needed. If everything is already + * set up, then no changes are made, so this is safe to run to ensure + * cross-signing is ready for use. + * + * This function: + * - creates new cross-signing keys if they are not found locally cached nor in + * secret storage (if it has been setup) + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {function} opts.authUploadDeviceSigningKeys Function + * called to await an interactive auth flow when uploading device signing keys. + * @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys + * already exist. + * Args: + * {function} A function that makes the request requiring auth. Receives the + * auth data as an object. Can be called multiple times, first with an empty + * authDict, to obtain the flows. + */ + public bootstrapCrossSigning(opts: { + authUploadDeviceSigningKeys: (makeRequest: (authData: any) => void) => Promise, + setupNewCrossSigning?: boolean, + }) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); } - throw e; + return this.crypto.bootstrapCrossSigning(opts); + } + /** + * Whether to trust a others users signatures of their devices. + * If false, devices will only be considered 'verified' if we have + * verified that device individually (effectively disabling cross-signing). + * + * Default: true + * + * @return {bool} True if trusting cross-signed devices + */ + public getCryptoTrustCrossSignedDevices() : boolean { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getCryptoTrustCrossSignedDevices(); } -}; - -/** - * Gets the users that are ignored by this client - * @returns {string[]} The array of users that are ignored (empty if none) - */ -MatrixClient.prototype.getIgnoredUsers = function() { - const event = this.getAccountData("m.ignored_user_list"); - if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return []; - return Object.keys(event.getContent()["ignored_users"]); -}; - -/** - * Sets the users that the current user should ignore. - * @param {string[]} userIds the user IDs to ignore - * @param {module:client.callback} [callback] Optional. - * @return {Promise} Resolves: Account data event - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setIgnoredUsers = function(userIds, callback) { - const content = { ignored_users: {} }; - userIds.map((u) => content.ignored_users[u] = {}); - return this.setAccountData("m.ignored_user_list", content, callback); -}; - -/** - * Gets whether or not a specific user is being ignored by this client. - * @param {string} userId the user ID to check - * @returns {boolean} true if the user is ignored, false otherwise - */ -MatrixClient.prototype.isUserIgnored = function(userId) { - return this.getIgnoredUsers().indexOf(userId) !== -1; -}; -// Room operations -// =============== + /** + * See getCryptoTrustCrossSignedDevices -/** - * Join a room. If you have already joined the room, this will no-op. - * @param {string} roomIdOrAlias The room ID or room alias to join. - * @param {Object} opts Options when joining the room. - * @param {boolean} opts.syncRoom True to do a room initial sync on the resulting - * room. If false, the returned Room object will have no current state. - * Default: true. - * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, - * the signing URL is passed in this parameter. - * @param {string[]} opts.viaServers The server names to try and join through in - * addition to those that are automatically chosen. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Room object. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { - // to help people when upgrading.. - if (utils.isFunction(opts)) { - throw new Error("Expected 'opts' object, got function."); - } - opts = opts || {}; - if (opts.syncRoom === undefined) { - opts.syncRoom = true; - } - - const room = this.getRoom(roomIdOrAlias); - if (room && room.hasMembershipState(this.credentials.userId, "join")) { - return Promise.resolve(room); + * This may be set before initCrypto() is called to ensure no races occur. + * + * @param {bool} val True to trust cross-signed devices + */ + public setCryptoTrustCrossSignedDevices(val: boolean) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.setCryptoTrustCrossSignedDevices(val); } - let sign_promise = Promise.resolve(); - - if (opts.inviteSignUrl) { - sign_promise = this._http.requestOtherUrl( - undefined, 'POST', - opts.inviteSignUrl, { mxid: this.credentials.userId }, - ); + /** + * Counts the number of end to end session keys that are waiting to be backed up + * @returns {Promise} Resolves to the number of sessions requiring backup + */ + public countSessionsNeedingBackup(): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.countSessionsNeedingBackup(); + } + + /** + * Get information about the encryption of an event + * + * @param {module:models/event.MatrixEvent} event event to be checked + * @returns {IEncryptedEventInfo} The event information. + */ + public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getEventEncryptionInfo(event); + } + + /** + * Create a recovery key from a user-supplied passphrase. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {string} password Passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * Optional. + * @returns {Promise} Object with public key metadata, encoded private + * recovery key which should be disposed of after displaying to the user, + * and raw private key to avoid round tripping if needed. + */ + public createRecoveryKeyFromPassphrase(password: string): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.createRecoveryKeyFromPassphrase(password); + } + + /** + * Checks whether secret storage: + * - is enabled on this account + * - is storing cross-signing private keys + * - is storing session backup key (if enabled) + * + * If this function returns false, bootstrapSecretStorage() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapSecretStorage() completes successfully, this function should + * return true. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @return {bool} True if secret storage is ready to be used on this device + */ + public isSecretStorageReady(): boolean { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.isSecretStorageReady(); + } + + /** + * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is + * already set up, then no changes are made, so this is safe to run to ensure secret + * storage is ready for use. + * + * This function + * - creates a new Secure Secret Storage key if no default key exists + * - if a key backup exists, it is migrated to store the key in the Secret + * Storage + * - creates a backup if none exists, and one is requested + * - migrates Secure Secret Storage to use the latest algorithm, if an outdated + * algorithm is found + * + * @param opts + */ + public bootstrapSecretStorage(opts: ICreateSecretStorageOpts): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.bootstrapSecretStorage(opts); + } + + /** + * Add a key for encrypting secrets. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {string} algorithm the algorithm used by the key + * @param {object} opts the options for the algorithm. The properties used + * depend on the algorithm given. + * @param {string} [keyName] the name of the key. If not given, a random name will be generated. + * + * @return {object} An object with: + * keyId: {string} the ID of the key + * keyInfo: {object} details about the key (iv, mac, passphrase) + */ + public addSecretStorageKey(algorithm: string, opts: IAddSecretStorageKeyOpts, keyName?: string): ISecretStorageKey { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.addSecretStorageKey(algorithm, opts, keyName); + } + + /** + * Check whether we have a key with a given ID. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {string} [keyId = default key's ID] The ID of the key to check + * for. Defaults to the default key ID if not provided. + * @return {boolean} Whether we have the key. + */ + public hasSecretStorageKey(keyId?:string): boolean { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.hasSecretStorageKey(keyId); + } + + /** + * Store an encrypted secret on the server. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {string} name The name of the secret + * @param {string} secret The secret contents. + * @param {Array} keys The IDs of the keys to use to encrypt the secret or null/undefined + * to use the default (will throw if no default key is set). + */ + public storeSecret(name: string, secret: string, keys?: string[]) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.storeSecret(name, secret, keys); + } + + /** + * Get a secret from storage. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {string} name the name of the secret + * + * @return {string} the contents of the secret + */ + public getSecret(name: string): string { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getSecret(name); + } + + /** + * Check if a secret is stored on the server. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {string} name the name of the secret + * @param {boolean} checkKey check if the secret is encrypted by a trusted + * key + * + * @return {object?} map of key name to key info the secret is encrypted + * with, or null if it is not present or not encrypted with a trusted + * key + */ + public isSecretStored(name: string, checkKey: boolean): Record { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.isSecretStored(name, checkKey); + } + + /** + * Request a secret from another device. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {string} name the name of the secret to request + * @param {string[]} devices the devices to request the secret from + * + * @return {string} the contents of the secret + */ + public requestSecret(name: string, devices: string[]): string { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.requestSecret(name, devices); + } + + /** + * Get the current default key ID for encrypting secrets. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @return {string} The default key ID or null if no default key ID is set + */ + public getDefaultSecretStorageKeyId(): string { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.getDefaultSecretStorageKeyId(); + } + + /** + * Set the current default key ID for encrypting secrets. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {string} keyId The new default key ID + */ + public setDefaultSecretStorageKeyId(keyId: string) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.setDefaultSecretStorageKeyId(keyId); + } + + /** + * Checks that a given secret storage private key matches a given public key. + * This can be used by the getSecretStorageKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {Uint8Array} privateKey The private key + * @param {string} expectedPublicKey The public key + * @returns {boolean} true if the key matches, otherwise false + */ + public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.checkSecretStoragePrivateKey(privateKey, expectedPublicKey); + } + + /** + * Get e2e information on the device that sent an event + * + * @param {MatrixEvent} event event to be checked + * + * @return {Promise} + */ + public getEventSenderDeviceInfo(event: MatrixEvent): Promise { + if (!this.crypto) { + return null; + } + return this.crypto.getEventSenderDeviceInfo(event); + } + + /** + * Check if the sender of an event is verified + * + * @param {MatrixEvent} event event to be checked + * + * @return {boolean} true if the sender of this event has been verified using + * {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}. + */ + public async isEventSenderVerified(event: MatrixEvent): Promise { + const device = await this.getEventSenderDeviceInfo(event); + if (!device) { + return false; + } + return device.isVerified(); + } + + /** + * Cancel a room key request for this event if one is ongoing and resend the + * request. + * @param {MatrixEvent} event event of which to cancel and resend the room + * key request. + * @return {Promise} A promise that will resolve when the key request is queued + */ + public cancelAndResendEventRoomKeyRequest(event: MatrixEvent): Promise { + return event.cancelAndResendKeyRequest(this.crypto, this.getUserId()); + } + + /** + * Enable end-to-end encryption for a room. This does not modify room state. + * Any messages sent before the returned promise resolves will be sent unencrypted. + * @param {string} roomId The room ID to enable encryption in. + * @param {object} config The encryption config for the room. + * @return {Promise} A promise that will resolve when encryption is set up. + */ + public setRoomEncryption(roomId: string, config: any): Promise { + if (!this.crypto) { + throw new Error("End-to-End encryption disabled"); + } + return this.crypto.setRoomEncryption(roomId, config); } - const queryString = {}; - if (opts.viaServers) { - queryString["server_name"] = opts.viaServers; - } + /** + * Whether encryption is enabled for a room. + * @param {string} roomId the room id to query. + * @return {bool} whether encryption is enabled. + */ + public isRoomEncrypted(roomId: string): boolean { + const room = this.getRoom(roomId); + if (!room) { + // we don't know about this room, so can't determine if it should be + // encrypted. Let's assume not. + return false; + } - const reqOpts = { qsStringifyOptions: { arrayFormat: 'repeat' } }; + // if there is an 'm.room.encryption' event in this room, it should be + // encrypted (independently of whether we actually support encryption) + const ev = room.currentState.getStateEvents("m.room.encryption", ""); + if (ev) { + return true; + } - const self = this; - const prom = new Promise((resolve, reject) => { - sign_promise.then(function(signed_invite_object) { - const data = {}; - if (signed_invite_object) { - data.third_party_signed = signed_invite_object; + // we don't have an m.room.encrypted event, but that might be because + // the server is hiding it from us. Check the store to see if it was + // previously encrypted. + return this.roomList.isRoomEncrypted(roomId); + } + + /** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * @param {string} roomId The ID of the room to discard the session for + * + * This should not normally be necessary. + */ + public forceDiscardSession(roomId: string) { + if (!this.crypto) { + throw new Error("End-to-End encryption disabled"); + } + this.crypto.forceDiscardSession(roomId); + } + + /** + * Get a list containing all of the room keys + * + * This should be encrypted before returning it to the user. + * + * @return {Promise} a promise which resolves to a list of + * session export objects + */ + public exportRoomKeys(): Promise { // TODO: Types + if (!this.crypto) { + return Promise.reject(new Error("End-to-end encryption disabled")); + } + return this.crypto.exportRoomKeys(); + } + + /** + * Import a list of room keys previously exported by exportRoomKeys + * + * @param {Object[]} keys a list of session export objects + * @param {Object} opts + * @param {Function} opts.progressCallback called with an object that has a "stage" param + * + * @return {Promise} a promise which resolves when the keys + * have been imported + */ + public importRoomKeys(keys: any[], opts: IImportRoomKeysOpts): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.importRoomKeys(keys, opts); + } + + /** + * Force a re-check of the local key backup status against + * what's on the server. + * + * @returns {Object} Object with backup info (as returned by + * getKeyBackupVersion) in backupInfo and + * trust information (as returned by isKeyBackupTrusted) + * in trustInfo. + */ + public checkKeyBackup(): IKeyBackupVersion { + return this.crypto.checkKeyBackup(); + } + + /** + * Get information about the current key backup. + * @returns {Promise} Information object from API or null + */ + public getKeyBackupVersion(): Promise { + return this.http.authedRequest( + undefined, "GET", "/room_keys/version", undefined, undefined, + { prefix: PREFIX_UNSTABLE }, + ).then((res) => { + if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) { + const err = "Unknown backup algorithm: " + res.algorithm; + return Promise.reject(err); + } else if (!(typeof res.auth_data === "object") + || !res.auth_data.public_key) { + const err = "Invalid backup data returned"; + return Promise.reject(err); + } else { + return res; } - - const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias }); - return self._http.authedRequest( - undefined, "POST", path, queryString, data, reqOpts); - }).then(function(res) { - const roomId = res.room_id; - const syncApi = new SyncApi(self, self._clientOpts); - const room = syncApi.createRoom(roomId); - if (opts.syncRoom) { - // v2 will do this for us - // return syncApi.syncRoom(room); + }).catch((e) => { + if (e.errcode === 'M_NOT_FOUND') { + return null; + } else { + throw e; } - return Promise.resolve(room); - }).then(function(room) { - _resolve(callback, resolve, room); - }, function(err) { - _reject(callback, reject, err); - }); - }); - return prom; -}; - -/** - * Resend an event. - * @param {MatrixEvent} event The event to resend. - * @param {Room} room Optional. The room the event is in. Will update the - * timeline entry if provided. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.resendEvent = function(event, room) { - _updatePendingEventStatus(room, event, EventStatus.SENDING); - return _sendEvent(this, room, event); -}; - -/** - * Cancel a queued or unsent event. - * - * @param {MatrixEvent} event Event to cancel - * @throws Error if the event is not in QUEUED or NOT_SENT state - */ -MatrixClient.prototype.cancelPendingEvent = function(event) { - if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) { - throw new Error("cannot cancel an event with status " + event.status); - } - - // first tell the scheduler to forget about it, if it's queued - if (this.scheduler) { - this.scheduler.removeEventFromQueue(event); - } - - // then tell the room about the change of state, which will remove it - // from the room's list of pending events. - const room = this.getRoom(event.getRoomId()); - _updatePendingEventStatus(room, event, EventStatus.CANCELLED); -}; - -/** - * @param {string} roomId - * @param {string} name - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setRoomName = function(roomId, name, callback) { - return this.sendStateEvent(roomId, "m.room.name", { name: name }, - undefined, callback); -}; - -/** - * @param {string} roomId - * @param {string} topic - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setRoomTopic = function(roomId, topic, callback) { - return this.sendStateEvent(roomId, "m.room.topic", { topic: topic }, - undefined, callback); -}; - -/** - * @param {string} roomId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.getRoomTags = function(roomId, callback) { - const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", { - $userId: this.credentials.userId, - $roomId: roomId, - }); - return this._http.authedRequest( - callback, "GET", path, undefined, - ); -}; - -/** - * @param {string} roomId - * @param {string} tagName name of room tag to be set - * @param {object} metadata associated with that tag to be stored - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setRoomTag = function(roomId, tagName, metadata, callback) { - const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { - $userId: this.credentials.userId, - $roomId: roomId, - $tag: tagName, - }); - return this._http.authedRequest( - callback, "PUT", path, undefined, metadata, - ); -}; - -/** - * @param {string} roomId - * @param {string} tagName name of room tag to be removed - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.deleteRoomTag = function(roomId, tagName, callback) { - const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { - $userId: this.credentials.userId, - $roomId: roomId, - $tag: tagName, - }); - return this._http.authedRequest( - callback, "DELETE", path, undefined, undefined, - ); -}; - -/** - * @param {string} roomId - * @param {string} eventType event type to be set - * @param {object} content event content - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setRoomAccountData = function(roomId, eventType, - content, callback) { - const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", { - $userId: this.credentials.userId, - $roomId: roomId, - $type: eventType, - }); - return this._http.authedRequest( - callback, "PUT", path, undefined, content, - ); -}; - -/** - * Set a user's power level. - * @param {string} roomId - * @param {string} userId - * @param {Number} powerLevel - * @param {MatrixEvent} event - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel, - event, callback) { - let content = { - users: {}, - }; - if (event && event.getType() === "m.room.power_levels") { - // take a copy of the content to ensure we don't corrupt - // existing client state with a failed power level change - content = utils.deepCopy(event.getContent()); - } - content.users[userId] = powerLevel; - const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", { - $roomId: roomId, - }); - return this._http.authedRequest( - callback, "PUT", path, undefined, content, - ); -}; - -/** - * @param {string} roomId - * @param {string} eventType - * @param {Object} content - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId, - callback) { - return this._sendCompleteEvent(roomId, { - type: eventType, - content: content, - }, txnId, callback); -}; -/** - * @param {string} roomId - * @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added. - * @param {string} txnId the txnId. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId, - callback) { - if (utils.isFunction(txnId)) { - callback = txnId; txnId = undefined; - } - - if (!txnId) { - txnId = this.makeTxnId(); - } - - // we always construct a MatrixEvent when sending because the store and - // scheduler use them. We'll extract the params back out if it turns out - // the client has no scheduler or store. - const localEvent = new MatrixEvent(Object.assign(eventObject, { - event_id: "~" + roomId + ":" + txnId, - user_id: this.credentials.userId, - sender: this.credentials.userId, - room_id: roomId, - origin_server_ts: new Date().getTime(), - })); - - const room = this.getRoom(roomId); - - // if this is a relation or redaction of an event - // that hasn't been sent yet (e.g. with a local id starting with a ~) - // then listen for the remote echo of that event so that by the time - // this event does get sent, we have the correct event_id - const targetId = localEvent.getAssociatedId(); - if (targetId && targetId.startsWith("~")) { - const target = room.getPendingEvents().find(e => e.getId() === targetId); - target.once("Event.localEventIdReplaced", () => { - localEvent.updateAssociatedId(target.getId()); }); } - const type = localEvent.getType(); - logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`); - - localEvent.setTxnId(txnId); - localEvent.setStatus(EventStatus.SENDING); - - // add this event immediately to the local store as 'sending'. - if (room) { - room.addPendingEvent(localEvent, txnId); - } - - // addPendingEvent can change the state to NOT_SENT if it believes - // that there's other events that have failed. We won't bother to - // try sending the event if the state has changed as such. - if (localEvent.status === EventStatus.NOT_SENT) { - return Promise.reject(new Error("Event blocked by other events not yet sent")); - } - - return _sendEvent(this, room, localEvent, callback); -}; - -// encrypts the event if necessary -// adds the event to the queue, or sends it -// marks the event as sent/unsent -// returns a promise which resolves with the result of the send request -function _sendEvent(client, room, event, callback) { - // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, - // so that we can handle synchronous and asynchronous exceptions with the - // same code path. - return Promise.resolve().then(function() { - const encryptionPromise = _encryptEventIfNeeded(client, event, room); - - if (!encryptionPromise) { - return null; - } - - _updatePendingEventStatus(room, event, EventStatus.ENCRYPTING); - return encryptionPromise.then(() => { - _updatePendingEventStatus(room, event, EventStatus.SENDING); - }); - }).then(function() { - let promise; - // this event may be queued - if (client.scheduler) { - // if this returns a promise then the scheduler has control now and will - // resolve/reject when it is done. Internally, the scheduler will invoke - // processFn which is set to this._sendEventHttpRequest so the same code - // path is executed regardless. - promise = client.scheduler.queueEvent(event); - if (promise && client.scheduler.getQueueForEvent(event).length > 1) { - // event is processed FIFO so if the length is 2 or more we know - // this event is stuck behind an earlier event. - _updatePendingEventStatus(room, event, EventStatus.QUEUED); - } - } - - if (!promise) { - promise = _sendEventHttpRequest(client, event); - if (room) { - promise = promise.then(res => { - room.updatePendingEvent(event, EventStatus.SENT, res.event_id); - return res; - }); - } + /** + * @param {object} info key backup info dict from getKeyBackupVersion() + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool], + * device: [DeviceInfo], + * ] + * } + */ + public isKeyBackupTrusted(info: IKeyBackupVersion): IKeyBackupTrustInfo { + return this.crypto.isKeyBackupTrusted(info); + } + + /** + * @returns {bool} true if the client is configured to back up keys to + * the server, otherwise false. If we haven't completed a successful check + * of key backup status yet, returns null. + */ + public getKeyBackupEnabled(): boolean { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); } - return promise; - }).then(function(res) { // the request was sent OK - if (callback) { - callback(null, res); + // XXX: Private member access + if (!this.crypto._checkedForBackup) { + return null; } - return res; - }, function(err) { - // the request failed to send. - logger.error("Error sending event", err.stack || err); - - try { - // set the error on the event before we update the status: - // updating the status emits the event, so the state should be - // consistent at that point. - event.error = err; - _updatePendingEventStatus(room, event, EventStatus.NOT_SENT); - // also put the event object on the error: the caller will need this - // to resend or cancel the event - err.event = event; - - if (callback) { - callback(err); - } - } catch (err2) { - logger.error("Exception in error handler!", err2.stack || err); + return Boolean(this.crypto.backupKey); + } + + /** + * Enable backing up of keys, using data previously returned from + * getKeyBackupVersion. + * + * @param {object} info Backup information object as returned by getKeyBackupVersion + */ + public enableKeyBackup(info: IKeyBackupVersion) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); } - throw err; - }); -} - -/** - * Encrypt an event according to the configuration of the room, if necessary. - * - * @param {MatrixClient} client - * - * @param {module:models/event.MatrixEvent} event event to be sent - * - * @param {module:models/room?} room destination room. Null if the destination - * is not a room we have seen over the sync pipe. - * - * @return {Promise?} Promise which resolves when the event has been - * encrypted, or null if nothing was needed - */ - -function _encryptEventIfNeeded(client, event, room) { - if (event.isEncrypted()) { - // this event has already been encrypted; this happens if the - // encryption step succeeded, but the send step failed on the first - // attempt. - return null; - } - - if (!client.isRoomEncrypted(event.getRoomId())) { - // looks like this room isn't encrypted. - return null; - } - - if (!client._crypto && client.usingExternalCrypto) { - // The client has opted to allow sending messages to encrypted - // rooms even if the room is encrypted, and we haven't setup - // crypto. This is useful for users of matrix-org/pantalaimon - return null; - } - - if (event.getType() === "m.reaction") { - // For reactions, there is a very little gained by encrypting the entire - // event, as relation data is already kept in the clear. Event - // encryption for a reaction effectively only obscures the event type, - // but the purpose is still obvious from the relation data, so nothing - // is really gained. It also causes quite a few problems, such as: - // * triggers notifications via default push rules - // * prevents server-side bundling for reactions - // The reaction key / content / emoji value does warrant encrypting, but - // this will be handled separately by encrypting just this value. - // See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642 - return null; - } - if (!client._crypto) { - throw new Error( - "This room is configured to use encryption, but your client does " + - "not support encryption.", - ); - } - - return client._crypto.encryptEvent(event, room); -} -/** - * Returns the eventType that should be used taking encryption into account - * for a given eventType. - * @param {MatrixClient} client the client - * @param {string} roomId the room for the events `eventType` relates to - * @param {string} eventType the event type - * @return {string} the event type taking encryption into account - */ -function _getEncryptedIfNeededEventType(client, roomId, eventType) { - if (eventType === "m.reaction") { - return eventType; - } - const isEncrypted = client.isRoomEncrypted(roomId); - return isEncrypted ? "m.room.encrypted" : eventType; -} + this.crypto.backupInfo = info; + if (this.crypto.backupKey) this.crypto.backupKey.free(); + this.crypto.backupKey = new global.Olm.PkEncryption(); + this.crypto.backupKey.set_recipient_key(info.auth_data.public_key); -function _updatePendingEventStatus(room, event, newStatus) { - if (room) { - room.updatePendingEvent(event, newStatus); - } else { - event.setStatus(newStatus); - } -} + this.emit('crypto.keyBackupStatus', true); -function _sendEventHttpRequest(client, event) { - let txnId = event.getTxnId(); - if (!txnId) { - txnId = client.makeTxnId(); - event.setTxnId(txnId); + // There may be keys left over from a partially completed backup, so + // schedule a send to check. + this.crypto.scheduleKeyBackupSend(); } - const pathParams = { - $roomId: event.getRoomId(), - $eventType: event.getWireType(), - $stateKey: event.getStateKey(), - $txnId: txnId, - }; + /** + * Disable backing up of keys. + */ + public disableKeyBackup() { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } - let path; - - if (event.isState()) { - let pathTemplate = "/rooms/$roomId/state/$eventType"; - if (event.getStateKey() && event.getStateKey().length > 0) { - pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey"; - } - path = utils.encodeUri(pathTemplate, pathParams); - } else if (event.isRedaction()) { - const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`; - path = utils.encodeUri(pathTemplate, Object.assign({ - $redactsEventId: event.event.redacts, - }, pathParams)); - } else { - path = utils.encodeUri( - "/rooms/$roomId/send/$eventType/$txnId", pathParams, - ); - } + this.crypto.backupInfo = null; + if (this.crypto.backupKey) this.crypto.backupKey.free(); + this.crypto.backupKey = null; + + this.emit('crypto.keyBackupStatus', false); + } + + /** + * Set up the data required to create a new backup version. The backup version + * will not be created and enabled until createKeyBackupVersion is called. + * + * @param {string} password Passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * Optional. + * @param {boolean} [opts.secureSecretStorage = false] Whether to use Secure + * Secret Storage to store the key encrypting key backups. + * Optional, defaults to false. + * + * @returns {Promise} Object that can be passed to createKeyBackupVersion and + * additionally has a 'recovery_key' member with the user-facing recovery key string. + */ + // TODO: Verify types + public async prepareKeyBackupVersion(password: string, opts: IKeyBackupPrepareOpts = {secureSecretStorage: false}): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } - return client._http.authedRequest( - undefined, "PUT", path, undefined, event.getWireContent(), - ).then((res) => { - logger.log( - `Event sent to ${event.getRoomId()} with event id ${res.event_id}`, - ); - return res; - }); -} + const { keyInfo, encodedPrivateKey, privateKey } = + await this.createRecoveryKeyFromPassphrase(password); -/** - * @param {string} roomId - * @param {string} eventId - * @param {string} [txnId] transaction id. One will be made up if not - * supplied. - * @param {object|module:client.callback} callbackOrOpts - * Options to pass on, may contain `reason`. - * Can be callback for backwards compatibility. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callbackOrOpts) { - const opts = typeof(callbackOrOpts) === 'object' ? callbackOrOpts : {}; - const reason = opts.reason; - const callback = typeof(callbackOrOpts) === 'function' ? callbackOrOpts : undefined; - return this._sendCompleteEvent(roomId, { - type: "m.room.redaction", - content: { reason: reason }, - redacts: eventId, - }, txnId, callback); -}; + if (opts.secureSecretStorage) { + await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey)); + logger.info("Key backup private key stored in secret storage"); + } -/** - * @param {string} roomId - * @param {Object} content - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendMessage = function(roomId, content, txnId, callback) { - if (utils.isFunction(txnId)) { - callback = txnId; txnId = undefined; - } - return this.sendEvent( - roomId, "m.room.message", content, txnId, callback, - ); -}; + // Reshape objects into form expected for key backup + const authData: any = { // TODO + public_key: keyInfo.pubkey, + }; + if (keyInfo.passphrase) { + authData.private_key_salt = keyInfo.passphrase.salt; + authData.private_key_iterations = keyInfo.passphrase.iterations; + } + return { + algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, + auth_data: authData, + recovery_key: encodedPrivateKey, + } as any; // TODO + } + + /** + * Check whether the key backup private key is stored in secret storage. + * @return {Promise} map of key name to key info the secret is + * encrypted with, or null if it is not present or not encrypted with a + * trusted key + */ + public isKeyBackupKeyStored(): Promise> { + return Promise.resolve(this.isSecretStored("m.megolm_backup.v1", false /* checkKey */)); + } + + /** + * Create a new key backup version and enable it, using the information return + * from prepareKeyBackupVersion. + * + * @param {object} info Info object from prepareKeyBackupVersion + * @returns {Promise} Object with 'version' param indicating the version created + */ + // TODO: Fix types + public async createKeyBackupVersion(info: IKeyBackupVersion): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } -/** - * @param {string} roomId - * @param {string} body - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendTextMessage = function(roomId, body, txnId, callback) { - const content = ContentHelpers.makeTextMessage(body); - return this.sendMessage(roomId, content, txnId, callback); -}; + const data = { + algorithm: info.algorithm, + auth_data: info.auth_data, + }; -/** - * @param {string} roomId - * @param {string} body - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendNotice = function(roomId, body, txnId, callback) { - const content = ContentHelpers.makeNotice(body); - return this.sendMessage(roomId, content, txnId, callback); -}; + // Sign the backup auth data with the device key for backwards compat with + // older devices with cross-signing. This can probably go away very soon in + // favour of just signing with the cross-singing master key. + // XXX: Private member access + await this.crypto._signObject(data.auth_data); -/** - * @param {string} roomId - * @param {string} body - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendEmoteMessage = function(roomId, body, txnId, callback) { - const content = ContentHelpers.makeEmoteMessage(body); - return this.sendMessage(roomId, content, txnId, callback); -}; + if ( + this.cryptoCallbacks.getCrossSigningKey && + // XXX: Private member access + this.crypto._crossSigningInfo.getId() + ) { + // now also sign the auth data with the cross-signing master key + // we check for the callback explicitly here because we still want to be able + // to create an un-cross-signed key backup if there is a cross-signing key but + // no callback supplied. + // XXX: Private member access + await this.crypto._crossSigningInfo.signObject(data.auth_data, "master"); + } -/** - * @param {string} roomId - * @param {string} url - * @param {Object} info - * @param {string} text - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendImageMessage = function(roomId, url, info, text, callback) { - if (utils.isFunction(text)) { - callback = text; text = undefined; - } - if (!text) { - text = "Image"; - } - const content = { - msgtype: "m.image", - url: url, - info: info, - body: text, - }; - return this.sendMessage(roomId, content, callback); -}; + const res = await this.http.authedRequest( + undefined, "POST", "/room_keys/version", undefined, data, + { prefix: PREFIX_UNSTABLE }, + ); -/** - * @param {string} roomId - * @param {string} url - * @param {Object} info - * @param {string} text - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendStickerMessage = function(roomId, url, info, text, callback) { - if (utils.isFunction(text)) { - callback = text; text = undefined; - } - if (!text) { - text = "Sticker"; + // We could assume everything's okay and enable directly, but this ensures + // we run the same signature verification that will be used for future + // sessions. + await this.checkKeyBackup(); + if (!this.getKeyBackupEnabled()) { + logger.error("Key backup not usable even though we just created it"); + } + + return res; } - const content = { - url: url, - info: info, - body: text, - }; - return this.sendEvent( - roomId, "m.sticker", content, callback, undefined, - ); -}; -/** - * @param {string} roomId - * @param {string} body - * @param {string} htmlBody - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendHtmlMessage = function(roomId, body, htmlBody, callback) { - const content = ContentHelpers.makeHtmlMessage(body, htmlBody); - return this.sendMessage(roomId, content, callback); -}; + public deleteKeyBackupVersion(version: string): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } -/** - * @param {string} roomId - * @param {string} body - * @param {string} htmlBody - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callback) { - const content = ContentHelpers.makeHtmlNotice(body, htmlBody); - return this.sendMessage(roomId, content, callback); -}; + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeyBackupVersion + // so this is symmetrical). + if (this.crypto.backupInfo && this.crypto.backupInfo.version === version) { + this.disableKeyBackup(); + } -/** - * @param {string} roomId - * @param {string} body - * @param {string} htmlBody - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback) { - const content = ContentHelpers.makeHtmlEmote(body, htmlBody); - return this.sendMessage(roomId, content, callback); -}; + const path = utils.encodeUri("/room_keys/version/$version", { + $version: version, + }); -/** - * Send a receipt. - * @param {Event} event The event being acknowledged - * @param {string} receiptType The kind of receipt e.g. "m.read" - * @param {object} opts Additional content to send alongside the receipt. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendReceipt = function(event, receiptType, opts, callback) { - if (typeof(opts) === 'function') { - callback = opts; - opts = {}; + return this.http.authedRequest( + undefined, "DELETE", path, undefined, undefined, + { prefix: PREFIX_UNSTABLE }, + ); } - if (this.isGuest()) { - return Promise.resolve({}); // guests cannot send receipts so don't bother. + private makeKeyBackupPath(roomId: string, sessionId: string, version: string): {path: string, queryData: any} { + let path; + if (sessionId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { + $roomId: roomId, + $sessionId: sessionId, + }); + } else if (roomId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId", { + $roomId: roomId, + }); + } else { + path = "/room_keys/keys"; + } + const queryData = version === undefined ? undefined : { version: version }; + return { + path: path, + queryData: queryData, + }; } - const path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", { - $roomId: event.getRoomId(), - $receiptType: receiptType, - $eventId: event.getId(), - }); - const promise = this._http.authedRequest( - callback, "POST", path, undefined, opts || {}, - ); + /** + * Back up session keys to the homeserver. + * @param {string} roomId ID of the room that the keys are for Optional. + * @param {string} sessionId ID of the session that the keys are for Optional. + * @param {integer} version backup version Optional. + * @param {object} data Object keys to send + * @return {Promise} a promise that will resolve when the keys + * are uploaded + */ + // TODO: Verify types + public sendKeyBackup(roomId: string, sessionId: string, version: string, data: any): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } - const room = this.getRoom(event.getRoomId()); - if (room) { - room._addLocalEchoReceipt(this.credentials.userId, event, receiptType); + const path = this.makeKeyBackupPath(roomId, sessionId, version); + return this.http.authedRequest( + undefined, "PUT", path.path, path.queryData, data, + { prefix: PREFIX_UNSTABLE }, + ); } - return promise; -}; -/** - * Send a read receipt. - * @param {Event} event The event that has been read. - * @param {object} opts The options for the read receipt. - * @param {boolean} opts.hidden True to prevent the receipt from being sent to - * other users and homeservers. Default false (send to everyone). This - * property is unstable and may change in the future. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendReadReceipt = async function(event, opts, callback) { - if (typeof(opts) === 'function') { - callback = opts; - opts = {}; - } - if (!opts) opts = {}; + /** + * Marks all group sessions as needing to be backed up and schedules them to + * upload in the background as soon as possible. + */ + public async scheduleAllGroupSessionsForBackup() { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } - const eventId = event.getId(); - const room = this.getRoom(event.getRoomId()); - if (room && room.hasPendingEvent(eventId)) { - throw new Error(`Cannot set read receipt to a pending event (${eventId})`); + await this.crypto.scheduleAllGroupSessionsForBackup(); } - const addlContent = { - "m.hidden": Boolean(opts.hidden), - }; - - return this.sendReceipt(event, "m.read", addlContent, callback); -}; + /** + * Marks all group sessions as needing to be backed up without scheduling + * them to upload in the background. + * @returns {Promise} Resolves to the number of sessions requiring a backup. + */ + public flagAllGroupSessionsForBackup(): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } -/** - * Set a marker to indicate the point in a room before which the user has read every - * event. This can be retrieved from room account data (the event type is `m.fully_read`) - * and displayed as a horizontal line in the timeline that is visually distinct to the - * position of the user's own read receipt. - * @param {string} roomId ID of the room that has been read - * @param {string} rmEventId ID of the event that has been read - * @param {string} rrEvent the event tracked by the read receipt. This is here for - * convenience because the RR and the RM are commonly updated at the same time as each - * other. The local echo of this receipt will be done if set. Optional. - * @param {object} opts Options for the read markers - * @param {object} opts.hidden True to hide the receipt from other users and homeservers. - * This property is unstable and may change in the future. - * @return {Promise} Resolves: the empty object, {}. - */ -MatrixClient.prototype.setRoomReadMarkers = async function( - roomId, rmEventId, rrEvent, opts, -) { - const room = this.getRoom(roomId); - if (room && room.hasPendingEvent(rmEventId)) { - throw new Error(`Cannot set read marker to a pending event (${rmEventId})`); + return this.crypto.flagAllGroupSessionsForBackup(); } - // Add the optional RR update, do local echo like `sendReceipt` - let rrEventId; - if (rrEvent) { - rrEventId = rrEvent.getId(); - if (room && room.hasPendingEvent(rrEventId)) { - throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`); - } - if (room) { - room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read"); + public isValidRecoveryKey(recoveryKey: string): boolean { + try { + decodeRecoveryKey(recoveryKey); + return true; + } catch (e) { + return false; } } - return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, opts); -}; + /** + * Get the raw key for a key backup from the password + * Used when migrating key backups into SSSS + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {string} password Passphrase + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @return {Promise} key backup key + */ + public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupVersion): Promise { + return keyFromAuthData(backupInfo.auth_data, password); + } + + /** + * Get the raw key for a key backup from the recovery key + * Used when migrating key backups into SSSS + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {string} recoveryKey The recovery key + * @return {Uint8Array} key backup key + */ + public keyBackupKeyFromRecoveryKey(recoveryKey: string): Uint8Array { + return decodeRecoveryKey(recoveryKey); + } + + /** + * Restore from an existing key backup via a passphrase. + * + * @param {string} password Passphrase + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @param {object} opts Optional params such as callbacks + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ + // TODO: Types + public async restoreKeyBackupWithPassword(password: string, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, opts: IKeyBackupRestoreOpts): Promise { + const privKey = await keyFromAuthData(backupInfo.auth_data, password); + return this.restoreKeyBackup( + privKey, targetRoomId, targetSessionId, backupInfo, opts, + ); + } -/** - * Get a preview of the given URL as of (roughly) the given point in time, - * described as an object with OpenGraph keys and associated values. - * Attributes may be synthesized where actual OG metadata is lacking. - * Caches results to prevent hammering the server. - * @param {string} url The URL to get preview data for - * @param {Number} ts The preferred point in time that the preview should - * describe (ms since epoch). The preview returned will either be the most - * recent one preceding this timestamp if available, or failing that the next - * most recent available preview. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Object of OG metadata. - * @return {module:http-api.MatrixError} Rejects: with an error response. - * May return synthesized attributes if the URL lacked OG meta. - */ -MatrixClient.prototype.getUrlPreview = function(url, ts, callback) { - // bucket the timestamp to the nearest minute to prevent excessive spam to the server - // Surely 60-second accuracy is enough for anyone. - ts = Math.floor(ts / 60000) * 60000; + /** + * Restore from an existing key backup via a private key stored in secret + * storage. + * + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @param {object} opts Optional params such as callbacks + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ + // TODO: Types + public async restoreKeyBackupWithSecretStorage(backupInfo: IKeyBackupVersion, targetRoomId: string, targetSessionId: string, opts: IKeyBackupRestoreOpts): Promise { + const storedKey = await this.getSecret("m.megolm_backup.v1"); + + // ensure that the key is in the right format. If not, fix the key and + // store the fixed version + const fixedKey = fixBackupKey(storedKey); + if (fixedKey) { + const [keyId] = await this.crypto.getSecretStorageKey(); + await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); + } + + const privKey = decodeBase64(fixedKey || storedKey); + return this.restoreKeyBackup( + privKey, targetRoomId, targetSessionId, backupInfo, opts, + ); + } - const key = ts + "_" + url; + /** + * Restore from an existing key backup via an encoded recovery key. + * + * @param {string} recoveryKey Encoded recovery key + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @param {object} opts Optional params such as callbacks + + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ + // TODO: Types + public restoreKeyBackupWithRecoveryKey(recoveryKey: string, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, opts: IKeyBackupRestoreOpts): Promise { + const privKey = decodeRecoveryKey(recoveryKey); + return this.restoreKeyBackup( + privKey, targetRoomId, targetSessionId, backupInfo, opts, + ); + } - // If there's already a request in flight (or we've handled it), return that instead. - const cachedPreview = this.urlPreviewCache[key]; - if (cachedPreview) { - if (callback) { - cachedPreview.then(callback).catch(callback); + // TODO: Types + public async restoreKeyBackupWithCache(targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, opts: IKeyBackupRestoreOpts): Promise { + const privKey = await this.crypto.getSessionBackupPrivateKey(); + if (!privKey) { + throw new Error("Couldn't get key"); } - return cachedPreview; + return this.restoreKeyBackup( + privKey, targetRoomId, targetSessionId, backupInfo, opts, + ); } - const resp = this._http.authedRequest( - callback, "GET", "/preview_url", { - url: url, - ts: ts, - }, undefined, { - prefix: PREFIX_MEDIA_R0, - }, - ); - // TODO: Expire the URL preview cache sometimes - this.urlPreviewCache[key] = resp; - return resp; -}; + private restoreKeyBackup(privKey: Uint8Array, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, opts: IKeyBackupRestoreOpts): Promise { + const {cacheCompleteCallback, progressCallback} = opts; -/** - * @param {string} roomId - * @param {boolean} isTyping - * @param {Number} timeoutMs - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.sendTyping = function(roomId, isTyping, timeoutMs, callback) { - if (this.isGuest()) { - return Promise.resolve({}); // guests cannot send typing notifications so don't bother. - } + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + let totalKeyCount = 0; + let keys = []; - const path = utils.encodeUri("/rooms/$roomId/typing/$userId", { - $roomId: roomId, - $userId: this.credentials.userId, - }); - const data = { - typing: isTyping, - }; - if (isTyping) { - data.timeout = timeoutMs ? timeoutMs : 20000; - } - return this._http.authedRequest( - callback, "PUT", path, undefined, data, - ); -}; + const path = this.makeKeyBackupPath( + targetRoomId, targetSessionId, backupInfo.version, + ); -/** - * Determines the history of room upgrades for a given room, as far as the - * client can see. Returns an array of Rooms where the first entry is the - * oldest and the last entry is the newest (likely current) room. If the - * provided room is not found, this returns an empty list. This works in - * both directions, looking for older and newer rooms of the given room. - * @param {string} roomId The room ID to search from - * @param {boolean} verifyLinks If true, the function will only return rooms - * which can be proven to be linked. For example, rooms which have a create - * event pointing to an old room which the client is not aware of or doesn't - * have a matching tombstone would not be returned. - * @return {Room[]} An array of rooms representing the upgrade - * history. - */ -MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=false) { - let currentRoom = this.getRoom(roomId); - if (!currentRoom) return []; - - const upgradeHistory = [currentRoom]; - - // Work backwards first, looking at create events. - let createEvent = currentRoom.currentState.getStateEvents("m.room.create", ""); - while (createEvent) { - logger.log(`Looking at ${createEvent.getId()}`); - const predecessor = createEvent.getContent()['predecessor']; - if (predecessor && predecessor['room_id']) { - logger.log(`Looking at predecessor ${predecessor['room_id']}`); - const refRoom = this.getRoom(predecessor['room_id']); - if (!refRoom) break; // end of the chain + const decryption = new global.Olm.PkDecryption(); + let backupPubKey; + try { + backupPubKey = decryption.init_with_private_key(privKey); + } catch (e) { + decryption.free(); + throw e; + } - if (verifyLinks) { - const tombstone = refRoom.currentState - .getStateEvents("m.room.tombstone", ""); + // If the pubkey computed from the private data we've been given + // doesn't match the one in the auth_data, the user has entered + // a different recovery key / the wrong passphrase. + if (backupPubKey !== backupInfo.auth_data.public_key) { + return Promise.reject({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }); + } - if (!tombstone - || tombstone.getContent()['replacement_room'] !== refRoom.roomId) { - break; + // Cache the key, if possible. + // This is async. + this.crypto.storeSessionBackupPrivateKey(privKey) + .catch((e) => { + logger.warn("Error caching session backup key:", e); + }).then(cacheCompleteCallback); + + if (progressCallback) { + progressCallback({ + stage: "fetch", + }); + } + + return this.http.authedRequest( + undefined, "GET", path.path, path.queryData, undefined, + { prefix: PREFIX_UNSTABLE }, + ).then((res) => { + if (res.rooms) { + // TODO: Types? + for (const [roomId, roomData] of Object.entries(res.rooms)) { + if (!roomData.sessions) continue; + + totalKeyCount += Object.keys(roomData.sessions).length; + const roomKeys = keysFromRecoverySession( + roomData.sessions, decryption, roomId, + ); + for (const k of roomKeys) { + k.room_id = roomId; + keys.push(k); + } + } + } else if (res.sessions) { + totalKeyCount = Object.keys(res.sessions).length; + keys = keysFromRecoverySession( + res.sessions, decryption, targetRoomId, + ); + } else { + totalKeyCount = 1; + try { + const key = keyFromRecoverySession(res, decryption); + key.room_id = targetRoomId; + key.session_id = targetSessionId; + keys.push(key); + } catch (e) { + logger.log("Failed to decrypt megolm session from backup", e); } } - // Insert at the front because we're working backwards from the currentRoom - upgradeHistory.splice(0, 0, refRoom); - createEvent = refRoom.currentState.getStateEvents("m.room.create", ""); - } else { - // No further create events to look at - break; - } + return this.importRoomKeys(keys, { + progressCallback, + untrusted: true, + source: "backup", + }); + }).then(() => { + return this.crypto.setTrustedBackupPubKey(backupPubKey); + }).then(() => { + return { total: totalKeyCount, imported: keys.length }; + }).finally(() => { + decryption.free(); + }); } - // Work forwards next, looking at tombstone events - let tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", ""); - while (tombstoneEvent) { - const refRoom = this.getRoom(tombstoneEvent.getContent()['replacement_room']); - if (!refRoom) break; // end of the chain - if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room + public deleteKeysFromBackup(roomId: string, sessionId: string, version: string): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } - if (verifyLinks) { - createEvent = refRoom.currentState.getStateEvents("m.room.create", ""); - if (!createEvent || !createEvent.getContent()['predecessor']) break; + const path = this.makeKeyBackupPath(roomId, sessionId, version); + return this.http.authedRequest( + undefined, "DELETE", path.path, path.queryData, undefined, + { prefix: PREFIX_UNSTABLE }, + ); + } - const predecessor = createEvent.getContent()['predecessor']; - if (predecessor['room_id'] !== currentRoom.roomId) break; + /** + * Share shared-history decryption keys with the given users. + * + * @param {string} roomId the room for which keys should be shared. + * @param {array} userIds a list of users to share with. The keys will be sent to + * all of the user's current devices. + */ + public async sendSharedHistoryKeys(roomId: string, userIds: string[]) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); } - // Push to the end because we're looking forwards - upgradeHistory.push(refRoom); - const roomIds = new Set(upgradeHistory.map((ref) => ref.roomId)); - if (roomIds.size < upgradeHistory.length) { - // The last room added to the list introduced a previous roomId - // To avoid recursion, return the last rooms - 1 - return upgradeHistory.slice(0, upgradeHistory.length - 1); + const roomEncryption = this.roomList.getRoomEncryption(roomId); + if (!roomEncryption) { + // unknown room, or unencrypted room + logger.error("Unknown room. Not sharing decryption keys"); + return; } - // Set the current room to the reference room so we know where we're at - currentRoom = refRoom; - tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", ""); - } + const deviceInfos = await this.crypto.downloadKeys(userIds); + const devicesByUser = {}; + for (const [userId, devices] of Object.entries(deviceInfos)) { + devicesByUser[userId] = Object.values(devices); + } - return upgradeHistory; -}; + // XXX: Private member access + const alg = this.crypto._getRoomDecryptor(roomId, roomEncryption.algorithm); + if (alg.sendSharedHistoryInboundSessions) { + await alg.sendSharedHistoryInboundSessions(devicesByUser); + } else { + logger.warn("Algorithm does not support sharing previous keys", roomEncryption.algorithm); + } + } -/** - * @param {string} roomId - * @param {string} userId - * @param {module:client.callback} callback Optional. - * @param {string} reason Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.invite = function(roomId, userId, callback, reason) { - return _membershipChange(this, roomId, userId, "invite", reason, - callback); -}; + /** + * Get the group for the given group ID. + * This function will return a valid group for any group for which a Group event + * has been emitted. + * @param {string} groupId The group ID + * @return {Group} The Group or null if the group is not known or there is no data store. + */ + public getGroup(groupId: string): Group { + return this.store.getGroup(groupId); + } + + /** + * Retrieve all known groups. + * @return {Group[]} A list of groups, or an empty list if there is no data store. + */ + public getGroups(): Group[] { + return this.store.getGroups(); + } + + /** + * Get the config for the media repository. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves with an object containing the config. + */ + public getMediaConfig(callback?: Callback): Promise { // TODO: Types + return this.http.authedRequest( + callback, "GET", "/config", undefined, undefined, { + prefix: PREFIX_MEDIA_R0, + }, + ); + } -/** - * Invite a user to a room based on their email address. - * @param {string} roomId The room to invite the user to. - * @param {string} email The email address to invite. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.inviteByEmail = function(roomId, email, callback) { - return this.inviteByThreePid( - roomId, "email", email, callback, - ); -}; + /** + * Get the room for the given room ID. + * This function will return a valid room for any room for which a Room event + * has been emitted. Note in particular that other events, eg. RoomState.members + * will be emitted for a room before this function will return the given room. + * @param {string} roomId The room ID + * @return {Room} The Room or null if it doesn't exist or there is no data store. + */ + public getRoom(roomId: string): Room { + return this.store.getRoom(roomId); + } + + /** + * Retrieve all known rooms. + * @return {Room[]} A list of rooms, or an empty list if there is no data store. + */ + public getRooms(): Room[] { + return this.store.getRooms(); + } + + /** + * Retrieve all rooms that should be displayed to the user + * This is essentially getRooms() with some rooms filtered out, eg. old versions + * of rooms that have been replaced or (in future) other rooms that have been + * marked at the protocol level as not to be displayed to the user. + * @return {Room[]} A list of rooms, or an empty list if there is no data store. + */ + public getVisibleRooms(): Room[] { + const allRooms = this.store.getRooms(); + + const replacedRooms = new Set(); + for (const r of allRooms) { + const createEvent = r.currentState.getStateEvents('m.room.create', ''); + // invites are included in this list and we don't know their create events yet + if (createEvent) { + const predecessor = createEvent.getContent()['predecessor']; + if (predecessor && predecessor['room_id']) { + replacedRooms.add(predecessor['room_id']); + } + } + } -/** - * Invite a user to a room based on a third-party identifier. - * @param {string} roomId The room to invite the user to. - * @param {string} medium The medium to invite the user e.g. "email". - * @param {string} address The address for the specified medium. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.inviteByThreePid = async function( - roomId, - medium, - address, - callback, -) { - const path = utils.encodeUri( - "/rooms/$roomId/invite", - { $roomId: roomId }, - ); - - const identityServerUrl = this.getIdentityServerUrl(true); - if (!identityServerUrl) { - return Promise.reject(new MatrixError({ - error: "No supplied identity server URL", - errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM", - })); + return allRooms.filter((r) => { + const tombstone = r.currentState.getStateEvents('m.room.tombstone', ''); + if (tombstone && replacedRooms.has(r.roomId)) { + return false; + } + return true; + }); } - const params = { - id_server: identityServerUrl, - medium: medium, - address: address, - }; - if ( - this.identityServer && - this.identityServer.getAccessToken && - await this.doesServerAcceptIdentityAccessToken() - ) { - const identityAccessToken = await this.identityServer.getAccessToken(); - if (identityAccessToken) { - params.id_access_token = identityAccessToken; + /** + * Retrieve a user. + * @param {string} userId The user ID to retrieve. + * @return {?User} A user or null if there is no data store or the user does + * not exist. + */ + public getUser(userId: string): User { + return this.store.getUser(userId); + } + + /** + * Retrieve all known users. + * @return {User[]} A list of users, or an empty list if there is no data store. + */ + public getUsers(): User[] { + return this.store.getUsers(); + } + + /** + * Set account data event for the current user. + * It will retry the request up to 5 times. + * @param {string} eventType The event type + * @param {Object} content the contents object for the event + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setAccountData(eventType: string, content: any, callback?: Callback): Promise { + const path = utils.encodeUri("/user/$userId/account_data/$type", { + $userId: this.credentials.userId, + $type: eventType, + }); + const promise = retryNetworkOperation(5, () => { + return this.http.authedRequest(undefined, "PUT", path, undefined, content); + }); + if (callback) { + promise.then(result => callback(null, result), callback); } + return promise; } - return this._http.authedRequest(callback, "POST", path, undefined, params); -}; - -/** - * @param {string} roomId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.leave = function(roomId, callback) { - return _membershipChange(this, roomId, undefined, "leave", undefined, - callback); -}; - -/** - * Leaves all rooms in the chain of room upgrades based on the given room. By - * default, this will leave all the previous and upgraded rooms, including the - * given room. To only leave the given room and any previous rooms, keeping the - * upgraded (modern) rooms untouched supply `false` to `includeFuture`. - * @param {string} roomId The room ID to start leaving at - * @param {boolean} includeFuture If true, the whole chain (past and future) of - * upgraded rooms will be left. - * @return {Promise} Resolves when completed with an object keyed - * by room ID and value of the error encountered when leaving or null. - */ -MatrixClient.prototype.leaveRoomChain = function(roomId, includeFuture=true) { - const upgradeHistory = this.getRoomUpgradeHistory(roomId); - - let eligibleToLeave = upgradeHistory; - if (!includeFuture) { - eligibleToLeave = []; - for (const room of upgradeHistory) { - eligibleToLeave.push(room); - if (room.roomId === roomId) { - break; + /** + * Get account data event of given type for the current user. + * @param {string} eventType The event type + * @return {?object} The contents of the given account data event + */ + public getAccountData(eventType: string): any { + return this.store.getAccountData(eventType); + } + + /** + * Get account data event of given type for the current user. This variant + * gets account data directly from the homeserver if the local store is not + * ready, which can be useful very early in startup before the initial sync. + * @param {string} eventType The event type + * @return {Promise} Resolves: The contents of the given account + * data event. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async getAccountDataFromServer(eventType: string): Promise { + if (this.isInitialSyncComplete()) { + const event = this.store.getAccountData(eventType); + if (!event) { + return null; + } + // The network version below returns just the content, so this branch + // does the same to match. + return event.getContent(); + } + const path = utils.encodeUri("/user/$userId/account_data/$type", { + $userId: this.credentials.userId, + $type: eventType, + }); + try { + return await this.http.authedRequest( + undefined, "GET", path, undefined, + ); + } catch (e) { + if (e.data && e.data.errcode === 'M_NOT_FOUND') { + return null; } + throw e; } } - const populationResults = {}; // {roomId: Error} - const promises = []; + /** + * Gets the users that are ignored by this client + * @returns {string[]} The array of users that are ignored (empty if none) + */ + public getIgnoredUsers(): string[] { + const event = this.getAccountData("m.ignored_user_list"); + if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return []; + return Object.keys(event.getContent()["ignored_users"]); + } + + /** + * Sets the users that the current user should ignore. + * @param {string[]} userIds the user IDs to ignore + * @param {module:client.callback} [callback] Optional. + * @return {Promise} Resolves: Account data event + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setIgnoredUsers(userIds: string[], callback?: Callback): Promise { + const content = { ignored_users: {} }; + userIds.map((u) => content.ignored_users[u] = {}); + return this.setAccountData("m.ignored_user_list", content, callback); + } + + /** + * Gets whether or not a specific user is being ignored by this client. + * @param {string} userId the user ID to check + * @returns {boolean} true if the user is ignored, false otherwise + */ + public isUserIgnored(userId: string): boolean { + return this.getIgnoredUsers().includes(userId); + } + + /** + * Join a room. If you have already joined the room, this will no-op. + * @param {string} roomIdOrAlias The room ID or room alias to join. + * @param {Object} opts Options when joining the room. + * @param {boolean} opts.syncRoom True to do a room initial sync on the resulting + * room. If false, the returned Room object will have no current state. + * Default: true. + * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, the signing URL is passed in this parameter. + * @param {string[]} opts.viaServers The server names to try and join through in addition to those that are automatically chosen. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Room object. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async joinRoom(roomIdOrAlias: string, opts: IJoinRoomOpts, callback?: Callback): Promise { + // to help people when upgrading.. + if (utils.isFunction(opts)) { + throw new Error("Expected 'opts' object, got function."); + } + opts = opts || {}; + if (opts.syncRoom === undefined) { + opts.syncRoom = true; + } - const doLeave = (roomId) => { - return this.leave(roomId).then(() => { - populationResults[roomId] = null; - }).catch((err) => { - populationResults[roomId] = err; - return null; // suppress error - }); - }; + const room = this.getRoom(roomIdOrAlias); + if (room && room.hasMembershipState(this.credentials.userId, "join")) { + return Promise.resolve(room); + } - for (const room of eligibleToLeave) { - promises.push(doLeave(room.roomId)); - } + let signPromise = Promise.resolve(); - return Promise.all(promises).then(() => populationResults); -}; + if (opts.inviteSignUrl) { + signPromise = this.http.requestOtherUrl( + undefined, 'POST', + opts.inviteSignUrl, { mxid: this.credentials.userId }, + ); + } -/** - * @param {string} roomId - * @param {string} userId - * @param {string} reason Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.ban = function(roomId, userId, reason, callback) { - return _membershipChange(this, roomId, userId, "ban", reason, - callback); -}; + const queryString = {}; + if (opts.viaServers) { + queryString["server_name"] = opts.viaServers; + } -/** - * @param {string} roomId - * @param {boolean} deleteRoom True to delete the room from the store on success. - * Default: true. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.forget = function(roomId, deleteRoom, callback) { - if (deleteRoom === undefined) { - deleteRoom = true; - } - const promise = _membershipChange(this, roomId, undefined, "forget", undefined, - callback); - if (!deleteRoom) { - return promise; - } - const self = this; - return promise.then(function(response) { - self.store.removeRoom(roomId); - self.emit("deleteRoom", roomId); - return response; - }); -}; + const reqOpts = { qsStringifyOptions: { arrayFormat: 'repeat' } }; -/** - * @param {string} roomId - * @param {string} userId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Object (currently empty) - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.unban = function(roomId, userId, callback) { - // unbanning != set their state to leave: this used to be - // the case, but was then changed so that leaving was always - // a revoking of priviledge, otherwise two people racing to - // kick / ban someone could end up banning and then un-banning - // them. - const path = utils.encodeUri("/rooms/$roomId/unban", { - $roomId: roomId, - }); - const data = { - user_id: userId, - }; - return this._http.authedRequest( - callback, "POST", path, undefined, data, - ); -}; + try { + const data: any = {}; + const signedInviteObj = await signPromise; + if (signedInviteObj) { + data['third_party_signed'] = signedInviteObj; + } -/** - * @param {string} roomId - * @param {string} userId - * @param {string} reason Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.kick = function(roomId, userId, reason, callback) { - return _setMembershipState( - this, roomId, userId, "leave", reason, callback, - ); -}; + const path = utils.encodeUri("/join/$roomid", {$roomid: roomIdOrAlias}); + const res = await this.http.authedRequest(undefined, "POST", path, queryString, data, reqOpts); -/** - * This is an internal method. - * @param {MatrixClient} client - * @param {string} roomId - * @param {string} userId - * @param {string} membershipValue - * @param {string} reason - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -function _setMembershipState(client, roomId, userId, membershipValue, reason, - callback) { - if (utils.isFunction(reason)) { - callback = reason; reason = undefined; + const roomId = res['room_id']; + const syncApi = new SyncApi(this, this.clientOpts); + const room = syncApi.createRoom(roomId); + if (opts.syncRoom) { + // v2 will do this for us + // return syncApi.syncRoom(room); + } + callback?.(null, room); + return room; + } catch (e) { + callback?.(e); + throw e; // rethrow for reject + } } - const path = utils.encodeUri( - "/rooms/$roomId/state/m.room.member/$userId", - { $roomId: roomId, $userId: userId }, - ); - - return client._http.authedRequest(callback, "PUT", path, undefined, { - membership: membershipValue, - reason: reason, - }); -} + /** + * Resend an event. + * @param {MatrixEvent} event The event to resend. + * @param {Room} room Optional. The room the event is in. Will update the + * timeline entry if provided. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public resendEvent(event: MatrixEvent, room: Room): Promise { + this.updatePendingEventStatus(room, event, EventStatus.SENDING); + return this.sendEvent(room, event); + } + + /** + * Cancel a queued or unsent event. + * + * @param {MatrixEvent} event Event to cancel + * @throws Error if the event is not in QUEUED or NOT_SENT state + */ + public cancelPendingEvent(event: MatrixEvent) { + if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) { + throw new Error("cannot cancel an event with status " + event.status); + } -/** - * This is an internal method. - * @param {MatrixClient} client - * @param {string} roomId - * @param {string} userId - * @param {string} membership - * @param {string} reason - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -function _membershipChange(client, roomId, userId, membership, reason, callback) { - if (utils.isFunction(reason)) { - callback = reason; reason = undefined; - } - - const path = utils.encodeUri("/rooms/$room_id/$membership", { - $room_id: roomId, - $membership: membership, - }); - return client._http.authedRequest( - callback, "POST", path, undefined, { - user_id: userId, // may be undefined e.g. on leave - reason: reason, - }, - ); -} + // first tell the scheduler to forget about it, if it's queued + if (this.scheduler) { + this.scheduler.removeEventFromQueue(event); + } -/** - * Obtain a dict of actions which should be performed for this event according - * to the push rules for this user. Caches the dict on the event. - * @param {MatrixEvent} event The event to get push actions for. - * @return {module:pushprocessor~PushAction} A dict of actions to perform. - */ -MatrixClient.prototype.getPushActionsForEvent = function(event) { - if (!event.getPushActions()) { - event.setPushActions(this._pushProcessor.actionsForEvent(event)); + // then tell the room about the change of state, which will remove it + // from the room's list of pending events. + const room = this.getRoom(event.getRoomId()); + this.updatePendingEventStatus(room, event, EventStatus.CANCELLED); + } + + /** + * @param {string} roomId + * @param {string} name + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setRoomName(roomId: string, name: string, callback?: Callback): Promise { + return this.sendStateEvent(roomId, "m.room.name", { name: name }, undefined, callback); + } + + /** + * @param {string} roomId + * @param {string} topic + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setRoomTopic(roomId: string, topic: string, callback?: Callback): Promise { + return this.sendStateEvent(roomId, "m.room.topic", { topic: topic }, undefined, callback); + } + + /** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getRoomTags(roomId: string, callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", { + $userId: this.credentials.userId, + $roomId: roomId, + }); + return this.http.authedRequest( + callback, "GET", path, undefined, + ); } - return event.getPushActions(); -}; -// Profile operations -// ================== + /** + * @param {string} roomId + * @param {string} tagName name of room tag to be set + * @param {object} metadata associated with that tag to be stored + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setRoomTag(roomId: string, tagName: string, metadata: any, callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { + $userId: this.credentials.userId, + $roomId: roomId, + $tag: tagName, + }); + return this.http.authedRequest( + callback, "PUT", path, undefined, metadata, + ); + } -/** - * @param {string} info The kind of info to set (e.g. 'avatar_url') - * @param {Object} data The JSON object to set. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setProfileInfo = function(info, data, callback) { - const path = utils.encodeUri("/profile/$userId/$info", { - $userId: this.credentials.userId, - $info: info, - }); - return this._http.authedRequest( - callback, "PUT", path, undefined, data, - ); -}; + /** + * @param {string} roomId + * @param {string} tagName name of room tag to be removed + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public deleteRoomTag(roomId: string, tagName: string, callback?: Callback): Promise { + const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { + $userId: this.credentials.userId, + $roomId: roomId, + $tag: tagName, + }); + return this.http.authedRequest( + callback, "DELETE", path, undefined, undefined, + ); + } -/** - * @param {string} name - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setDisplayName = async function(name, callback) { - const prom = await this.setProfileInfo( - "displayname", { displayname: name }, callback, - ); - // XXX: synthesise a profile update for ourselves because Synapse is broken and won't - const user = this.getUser(this.getUserId()); - if (user) { - user.displayName = name; - user.emit("User.displayName", user.events.presence, user); - } - return prom; -}; + /** + * @param {string} roomId + * @param {string} eventType event type to be set + * @param {object} content event content + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setRoomAccountData(roomId: string, eventType: string, content: any, callback?: Callback): Promise { + const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", { + $userId: this.credentials.userId, + $roomId: roomId, + $type: eventType, + }); + return this.http.authedRequest( + callback, "PUT", path, undefined, content, + ); + } -/** - * @param {string} url - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setAvatarUrl = async function(url, callback) { - const prom = await this.setProfileInfo( - "avatar_url", { avatar_url: url }, callback, - ); - // XXX: synthesise a profile update for ourselves because Synapse is broken and won't - const user = this.getUser(this.getUserId()); - if (user) { - user.avatarUrl = url; - user.emit("User.avatarUrl", user.events.presence, user); - } - return prom; -}; + /** + * Set a user's power level. + * @param {string} roomId + * @param {string} userId + * @param {Number} powerLevel + * @param {MatrixEvent} event + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setPowerLevel(roomId: string, userId: string, powerLevel: number, event: MatrixEvent, callback?: Callback): Promise { + let content = { + users: {}, + }; + if (event && event.getType() === "m.room.power_levels") { + // take a copy of the content to ensure we don't corrupt + // existing client state with a failed power level change + content = utils.deepCopy(event.getContent()); + } + content.users[userId] = powerLevel; + const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", { + $roomId: roomId, + }); + return this.http.authedRequest( + callback, "PUT", path, undefined, content, + ); + } -/** - * Turn an MXC URL into an HTTP one. This method is experimental and - * may change. - * @param {string} mxcUrl The MXC URL - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either - * "crop" or "scale". - * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs - * directly. Fetching such URLs will leak information about the user to - * anyone they share a room with. If false, will return null for such URLs. - * @return {?string} the avatar URL or null. - */ -MatrixClient.prototype.mxcUrlToHttp = - function(mxcUrl, width, height, resizeMethod, allowDirectLinks) { - return getHttpUriForMxc( - this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks, - ); -}; + /** + * @param {string} roomId + * @param {string} eventType + * @param {Object} content + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendEvent(roomId: string, eventType: string, content: any, txnId?: string, callback?: Callback): Promise { + return this.sendCompleteEvent(roomId, {type: eventType, content}, txnId, callback); + } + + /** + * @param {string} roomId + * @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added. + * @param {string} txnId the txnId. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + private sendCompleteEvent(roomId: string, eventObject: any, txnId: string, callback?: Callback): Promise { + if (utils.isFunction(txnId)) { + callback = txnId as any as Callback; // convert for legacy + txnId = undefined; + } -/** - * Sets a new status message for the user. The message may be null/falsey - * to clear the message. - * @param {string} newMessage The new message to set. - * @return {Promise} Resolves: to nothing - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) { - const type = "im.vector.user_status"; - return Promise.all(this.getRooms().map((room) => { - const isJoined = room.getMyMembership() === "join"; - const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2; - if (!isJoined || !looksLikeDm) { - return Promise.resolve(); + if (!txnId) { + txnId = this.makeTxnId(); } - // Check power level separately as it's a bit more expensive. - const maySend = room.currentState.mayClientSendStateEvent(type, this); - if (!maySend) { - return Promise.resolve(); + + // we always construct a MatrixEvent when sending because the store and + // scheduler use them. We'll extract the params back out if it turns out + // the client has no scheduler or store. + const localEvent = new MatrixEvent(Object.assign(eventObject, { + event_id: "~" + roomId + ":" + txnId, + user_id: this.credentials.userId, + sender: this.credentials.userId, + room_id: roomId, + origin_server_ts: new Date().getTime(), + })); + + const room = this.getRoom(roomId); + + // if this is a relation or redaction of an event + // that hasn't been sent yet (e.g. with a local id starting with a ~) + // then listen for the remote echo of that event so that by the time + // this event does get sent, we have the correct event_id + const targetId = localEvent.getAssociatedId(); + if (targetId && targetId.startsWith("~")) { + const target = room.getPendingEvents().find(e => e.getId() === targetId); + target.once("Event.localEventIdReplaced", () => { + localEvent.updateAssociatedId(target.getId()); + }); } - return this.sendStateEvent(room.roomId, type, { - status: newMessage, - }, this.getUserId()); - })); -}; -/** - * @param {Object} opts Options to apply - * @param {string} opts.presence One of "online", "offline" or "unavailable" - * @param {string} opts.status_msg The status message to attach. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - * @throws If 'presence' isn't a valid presence enum value. - */ -MatrixClient.prototype.setPresence = function(opts, callback) { - const path = utils.encodeUri("/presence/$userId/status", { - $userId: this.credentials.userId, - }); + const type = localEvent.getType(); + logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`); - if (typeof opts === "string") { - opts = { presence: opts }; - } + localEvent.setTxnId(txnId); + localEvent.setStatus(EventStatus.SENDING); - const validStates = ["offline", "online", "unavailable"]; - if (validStates.indexOf(opts.presence) == -1) { - throw new Error("Bad presence value: " + opts.presence); - } - return this._http.authedRequest( - callback, "PUT", path, undefined, opts, - ); -}; + // add this event immediately to the local store as 'sending'. + if (room) { + room.addPendingEvent(localEvent, txnId); + } -/** - * @param {string} userId The user to get presence for - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: The presence state for this user. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.getPresence = function(userId, callback) { - const path = utils.encodeUri("/presence/$userId/status", { - $userId: userId, - }); + // addPendingEvent can change the state to NOT_SENT if it believes + // that there's other events that have failed. We won't bother to + // try sending the event if the state has changed as such. + if (localEvent.status === EventStatus.NOT_SENT) { + return Promise.reject(new Error("Event blocked by other events not yet sent")); + } - return this._http.authedRequest(callback, "GET", path, undefined, undefined); -}; + return this.encryptAndSendEvent(room, localEvent, callback); + } + + /** + * encrypts the event if necessary; adds the event to the queue, or sends it; marks the event as sent/unsent + * @param room + * @param event + * @param callback + * @returns {Promise} returns a promise which resolves with the result of the send request + * @private + */ + private encryptAndSendEvent(room: Room, event: MatrixEvent, callback?: Callback): Promise { + // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, + // so that we can handle synchronous and asynchronous exceptions with the + // same code path. + return Promise.resolve().then(() => { + const encryptionPromise = this.encryptEventIfNeeded(event, room); + if (!encryptionPromise) return null; + + this.updatePendingEventStatus(room, event, EventStatus.ENCRYPTING); + return encryptionPromise.then(() => this.updatePendingEventStatus(room, event, EventStatus.SENDING)); + }).then(() => { + let promise: Promise; + if (this.scheduler) { + // if this returns a promise then the scheduler has control now and will + // resolve/reject when it is done. Internally, the scheduler will invoke + // processFn which is set to this._sendEventHttpRequest so the same code + // path is executed regardless. + promise = this.scheduler.queueEvent(event); + if (promise && this.scheduler.getQueueForEvent(event).length > 1) { + // event is processed FIFO so if the length is 2 or more we know + // this event is stuck behind an earlier event. + this.updatePendingEventStatus(room, event, EventStatus.QUEUED); + } + } -/** - * Retrieve older messages from the given room and put them in the timeline. - * - * If this is called multiple times whilst a request is ongoing, the same - * Promise will be returned. If there was a problem requesting scrollback, there - * will be a small delay before another request can be made (to prevent tight-looping - * when there is no connection). - * - * @param {Room} room The room to get older messages in. - * @param {Integer} limit Optional. The maximum number of previous events to - * pull in. Default: 30. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Room. If you are at the beginning - * of the timeline, Room.oldState.paginationToken will be - * null. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.scrollback = function(room, limit, callback) { - if (utils.isFunction(limit)) { - callback = limit; limit = undefined; - } - limit = limit || 30; - let timeToWaitMs = 0; - - let info = this._ongoingScrollbacks[room.roomId] || {}; - if (info.promise) { - return info.promise; - } else if (info.errorTs) { - const timeWaitedMs = Date.now() - info.errorTs; - timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0); - } - - if (room.oldState.paginationToken === null) { - return Promise.resolve(room); // already at the start. - } - // attempt to grab more events from the store first - const numAdded = this.store.scrollback(room, limit).length; - if (numAdded === limit) { - // store contained everything we needed. - return Promise.resolve(room); - } - // reduce the required number of events appropriately - limit = limit - numAdded; - - const self = this; - const prom = new Promise((resolve, reject) => { - // wait for a time before doing this request - // (which may be 0 in order not to special case the code paths) - sleep(timeToWaitMs).then(function() { - return self._createMessagesRequest( - room.roomId, - room.oldState.paginationToken, - limit, - 'b'); - }).then(function(res) { - const matrixEvents = res.chunk.map(_PojoToMatrixEventMapper(self)); - if (res.state) { - const stateEvents = res.state.map(_PojoToMatrixEventMapper(self)); - room.currentState.setUnknownStateEvents(stateEvents); + if (!promise) { + promise = this.sendEventHttpRequest(event); + if (room) { + promise = promise.then(res => { + room.updatePendingEvent(event, EventStatus.SENT, res['event_id']); + return res; + }); + } } - room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline()); - room.oldState.paginationToken = res.end; - if (res.chunk.length === 0) { - room.oldState.paginationToken = null; + + return promise; + }).then(res => { + callback?.(null, res); + return res; + }).catch(err => { + logger.error("Error sending event", err.stack || err); + try { + // set the error on the event before we update the status: + // updating the status emits the event, so the state should be + // consistent at that point. + event.error = err; + this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT); + // also put the event object on the error: the caller will need this + // to resend or cancel the event + err.event = event; + + callback?.(err); + } catch (e) { + logger.error("Exception in error handler!", e.stack || err); } - self.store.storeEvents(room, matrixEvents, res.end, true); - self._ongoingScrollbacks[room.roomId] = null; - _resolve(callback, resolve, room); - }, function(err) { - self._ongoingScrollbacks[room.roomId] = { - errorTs: Date.now(), - }; - _reject(callback, reject, err); + throw err; }); - }); + } - info = { - promise: prom, - errorTs: null, - }; + private encryptEventIfNeeded(event: MatrixEvent, room?: Room): Promise | null { + if (event.isEncrypted()) { + // this event has already been encrypted; this happens if the + // encryption step succeeded, but the send step failed on the first + // attempt. + return null; + } - this._ongoingScrollbacks[room.roomId] = info; - return prom; -}; + if (!this.isRoomEncrypted(event.getRoomId())) { + return null; + } -/** - * Get an EventTimeline for the given event - * - *

If the EventTimelineSet object already has the given event in its store, the - * corresponding timeline will be returned. Otherwise, a /context request is - * made, and used to construct an EventTimeline. - * - * @param {EventTimelineSet} timelineSet The timelineSet to look for the event in - * @param {string} eventId The ID of the event to look for - * - * @return {Promise} Resolves: - * {@link module:models/event-timeline~EventTimeline} including the given - * event - */ -MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { - // don't allow any timeline support unless it's been enabled. - if (!this.timelineSupport) { - throw new Error("timeline support is disabled. Set the 'timelineSupport'" + - " parameter to true when creating MatrixClient to enable" + - " it."); + if (!this.crypto && this.usingExternalCrypto) { + // The client has opted to allow sending messages to encrypted + // rooms even if the room is encrypted, and we haven't setup + // crypto. This is useful for users of matrix-org/pantalaimon + return null; + } + + if (event.getType() === EventType.Reaction) { + // For reactions, there is a very little gained by encrypting the entire + // event, as relation data is already kept in the clear. Event + // encryption for a reaction effectively only obscures the event type, + // but the purpose is still obvious from the relation data, so nothing + // is really gained. It also causes quite a few problems, such as: + // * triggers notifications via default push rules + // * prevents server-side bundling for reactions + // The reaction key / content / emoji value does warrant encrypting, but + // this will be handled separately by encrypting just this value. + // See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642 + return null; + } + + if (!this.crypto) { + throw new Error( + "This room is configured to use encryption, but your client does " + + "not support encryption.", + ); + } + + return this.crypto.encryptEvent(event, room); + } + + /** + * Returns the eventType that should be used taking encryption into account + * for a given eventType. + * @param {MatrixClient} client the client + * @param {string} roomId the room for the events `eventType` relates to + * @param {string} eventType the event type + * @return {string} the event type taking encryption into account + */ + private getEncryptedIfNeededEventType(roomId: string, eventType: string): string { + if (eventType === EventType.Reaction) return eventType; + return this.isRoomEncrypted(roomId) ? EventType.RoomMessageEncrypted : eventType; + } + + private updatePendingEventStatus(room: Room | null, event: MatrixEvent, newStatus: EventStatus) { + if (room) { + room.updatePendingEvent(event, newStatus); + } else { + event.setStatus(newStatus); + } } - if (timelineSet.getTimelineForEvent(eventId)) { - return Promise.resolve(timelineSet.getTimelineForEvent(eventId)); - } + private sendEventHttpRequest(event: MatrixEvent): Promise { + let txnId = event.getTxnId(); + if (!txnId) { + txnId = this.makeTxnId(); + event.setTxnId(txnId); + } - const path = utils.encodeUri( - "/rooms/$roomId/context/$eventId", { - $roomId: timelineSet.room.roomId, - $eventId: eventId, - }, - ); - let params = undefined; - if (this._clientOpts.lazyLoadMembers) { - params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) }; - } + const pathParams = { + $roomId: event.getRoomId(), + $eventType: event.getWireType(), + $stateKey: event.getStateKey(), + $txnId: txnId, + }; - // TODO: we should implement a backoff (as per scrollback()) to deal more - // nicely with HTTP errors. - const self = this; - const promise = - self._http.authedRequest(undefined, "GET", path, params, - ).then(function(res) { - if (!res.event) { - throw new Error("'event' not in '/context' result - homeserver too old?"); - } + let path; - // by the time the request completes, the event might have ended up in - // the timeline. - if (timelineSet.getTimelineForEvent(eventId)) { - return timelineSet.getTimelineForEvent(eventId); - } - - // we start with the last event, since that's the point at which we - // have known state. - // events_after is already backwards; events_before is forwards. - res.events_after.reverse(); - const events = res.events_after - .concat([res.event]) - .concat(res.events_before); - const matrixEvents = events.map(self.getEventMapper()); - - let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId()); - if (!timeline) { - timeline = timelineSet.addTimeline(); - timeline.initialiseState(res.state.map( - self.getEventMapper())); - timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end; + if (event.isState()) { + let pathTemplate = "/rooms/$roomId/state/$eventType"; + if (event.getStateKey() && event.getStateKey().length > 0) { + pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey"; + } + path = utils.encodeUri(pathTemplate, pathParams); + } else if (event.isRedaction()) { + const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`; + path = utils.encodeUri(pathTemplate, Object.assign({ + $redactsEventId: event.event.redacts, + }, pathParams)); } else { - const stateEvents = res.state.map(self.getEventMapper()); - timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents); - } - timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start); - - // there is no guarantee that the event ended up in "timeline" (we - // might have switched to a neighbouring timeline) - so check the - // room's index again. On the other hand, there's no guarantee the - // event ended up anywhere, if it was later redacted, so we just - // return the timeline we first thought of. - const tl = timelineSet.getTimelineForEvent(eventId) || timeline; - return tl; - }); - return promise; -}; - -/** - * Makes a request to /messages with the appropriate lazy loading filter set. - * XXX: if we do get rid of scrollback (as it's not used at the moment), - * we could inline this method again in paginateEventTimeline as that would - * then be the only call-site - * @param {string} roomId - * @param {string} fromToken - * @param {number} limit the maximum amount of events the retrieve - * @param {string} dir 'f' or 'b' - * @param {Filter} timelineFilter the timeline filter to pass - * @return {Promise} - */ -MatrixClient.prototype._createMessagesRequest = -function(roomId, fromToken, limit, dir, timelineFilter = undefined) { - const path = utils.encodeUri( - "/rooms/$roomId/messages", { $roomId: roomId }, - ); - if (limit === undefined) { - limit = 30; - } - const params = { - from: fromToken, - limit: limit, - dir: dir, - }; + path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams); + } - let filter = null; - if (this._clientOpts.lazyLoadMembers) { - // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, - // so the timelineFilter doesn't get written into it below - filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER); - } - if (timelineFilter) { - // XXX: it's horrific that /messages' filter parameter doesn't match - // /sync's one - see https://matrix.org/jira/browse/SPEC-451 - filter = filter || {}; - Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()); - } - if (filter) { - params.filter = JSON.stringify(filter); + return this.http.authedRequest( + undefined, "PUT", path, undefined, event.getWireContent(), + ).then((res) => { + logger.log(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`); + return res; + }); } - return this._http.authedRequest(undefined, "GET", path, params); -}; - -/** - * Take an EventTimeline, and back/forward-fill results. - * - * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline - * object to be updated - * @param {Object} [opts] - * @param {bool} [opts.backwards = false] true to fill backwards, - * false to go forwards - * @param {number} [opts.limit = 30] number of events to request - * - * @return {Promise} Resolves to a boolean: false if there are no - * events and we reached either end of the timeline; else true. - */ -MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) { - const isNotifTimeline = (eventTimeline.getTimelineSet() === this._notifTimelineSet); - // TODO: we should implement a backoff (as per scrollback()) to deal more - // nicely with HTTP errors. - opts = opts || {}; - const backwards = opts.backwards || false; + /** + * @param {string} roomId + * @param {string} eventId + * @param {string} [txnId] transaction id. One will be made up if not + * supplied. + * @param {object|module:client.callback} cbOrOpts + * Options to pass on, may contain `reason`. + * Can be callback for backwards compatibility. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public redactEvent(roomId: string, eventId: string, txnId?: string, cbOrOpts?: Callback | IRedactOpts): Promise { + const opts = typeof(cbOrOpts) === 'object' ? cbOrOpts : {}; + const reason = opts.reason; + const callback = typeof(cbOrOpts) === 'function' ? cbOrOpts : undefined; + return this.sendCompleteEvent(roomId, { + type: EventType.RoomRedaction, + content: { reason: reason }, + redacts: eventId, + }, txnId, callback); + } + + /** + * @param {string} roomId + * @param {Object} content + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendMessage(roomId: string, content: any, txnId: string, callback?: Callback): Promise { + if (utils.isFunction(txnId)) { + callback = txnId as any as Callback; // for legacy + txnId = undefined; + } + return this.sendEvent(roomId, "m.room.message", content, txnId, callback); + } + + /** + * @param {string} roomId + * @param {string} body + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendTextMessage(roomId: string, body: string, txnId?: string, callback?: Callback): Promise { + const content = ContentHelpers.makeTextMessage(body); + return this.sendMessage(roomId, content, txnId, callback); + } + + /** + * @param {string} roomId + * @param {string} body + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendNotice(roomId: string, body: string, txnId?: string, callback?: Callback): Promise { + const content = ContentHelpers.makeNotice(body); + return this.sendMessage(roomId, content, txnId, callback); + } + + /** + * @param {string} roomId + * @param {string} body + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendEmoteMessage(roomId: string, body: string, txnId?: string, callback?: Callback): Promise { + const content = ContentHelpers.makeEmoteMessage(body); + return this.sendMessage(roomId, content, txnId, callback); + } + + /** + * @param {string} roomId + * @param {string} url + * @param {Object} info + * @param {string} text + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendImageMessage(roomId: string, url: string, info?: IImageInfo, text = "Image", callback?: Callback): Promise { + if (utils.isFunction(text)) { + callback = text as any as Callback; // legacy + text = undefined; + } + const content = { + msgtype: "m.image", + url: url, + info: info, + body: text, + }; + return this.sendMessage(roomId, content, undefined, callback); + } + + /** + * @param {string} roomId + * @param {string} url + * @param {Object} info + * @param {string} text + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendStickerMessage(roomId: string, url: string, info?: IImageInfo, text = "Sticker", callback?: Callback): Promise { + if (utils.isFunction(text)) { + callback = text as any as Callback; // legacy + text = undefined; + } + const content = { + url: url, + info: info, + body: text, + }; + return this.sendEvent(roomId, EventType.Sticker, content, undefined, callback); + } + + /** + * @param {string} roomId + * @param {string} body + * @param {string} htmlBody + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendHtmlMessage(roomId: string, body: string, htmlBody: string, callback?: Callback): Promise { + const content = ContentHelpers.makeHtmlMessage(body, htmlBody); + return this.sendMessage(roomId, content, undefined, callback); + } + + /** + * @param {string} roomId + * @param {string} body + * @param {string} htmlBody + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendHtmlNotice(roomId: string, body: string, htmlBody: string, callback?: Callback): Promise { + const content = ContentHelpers.makeHtmlNotice(body, htmlBody); + return this.sendMessage(roomId, content, undefined, callback); + } + + /** + * @param {string} roomId + * @param {string} body + * @param {string} htmlBody + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendHtmlEmote(roomId: string, body: string, htmlBody: string, callback?: Callback): Promise { + const content = ContentHelpers.makeHtmlEmote(body, htmlBody); + return this.sendMessage(roomId, content, undefined, callback); + } + + /** + * Send a receipt. + * @param {Event} event The event being acknowledged + * @param {string} receiptType The kind of receipt e.g. "m.read" + * @param {object} opts Additional content to send alongside the receipt. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendReceipt(event: MatrixEvent, receiptType: string, body: any, callback?: Callback): Promise { + if (typeof(body) === 'function') { + callback = body as any as Callback; // legacy + body = {}; + } - if (isNotifTimeline) { - if (!backwards) { - throw new Error("paginateNotifTimeline can only paginate backwards"); + if (this.isGuest()) { + return Promise.resolve({}); // guests cannot send receipts so don't bother. } - } - const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; + const path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", { + $roomId: event.getRoomId(), + $receiptType: receiptType, + $eventId: event.getId(), + }); + const promise = this.http.authedRequest( + callback, "POST", path, undefined, body || {}, + ); - const token = eventTimeline.getPaginationToken(dir); - if (!token) { - // no token - no results. - return Promise.resolve(false); + const room = this.getRoom(event.getRoomId()); + if (room) { + room._addLocalEchoReceipt(this.credentials.userId, event, receiptType); + } + return promise; } - const pendingRequest = eventTimeline._paginationRequests[dir]; - - if (pendingRequest) { - // already a request in progress - return the existing promise - return pendingRequest; - } + /** + * Send a read receipt. + * @param {Event} event The event that has been read. + * @param {object} opts The options for the read receipt. + * @param {boolean} opts.hidden True to prevent the receipt from being sent to + * other users and homeservers. Default false (send to everyone). This + * property is unstable and may change in the future. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async sendReadReceipt(event: MatrixEvent, opts: {hidden?: boolean}, callback?: Callback): Promise { + if (typeof(opts) === 'function') { + callback = opts as any as Callback; // legacy + opts = {}; + } + if (!opts) opts = {}; - let path; - let params; - let promise; - const self = this; + const eventId = event.getId(); + const room = this.getRoom(event.getRoomId()); + if (room && room.hasPendingEvent(eventId)) { + throw new Error(`Cannot set read receipt to a pending event (${eventId})`); + } - if (isNotifTimeline) { - path = "/notifications"; - params = { - limit: ('limit' in opts) ? opts.limit : 30, - only: 'highlight', + const addlContent = { + "m.hidden": Boolean(opts.hidden), }; - if (token && token !== "end") { - params.from = token; + return this.sendReceipt(event, "m.read", addlContent, callback); + } + + /** + * Set a marker to indicate the point in a room before which the user has read every + * event. This can be retrieved from room account data (the event type is `m.fully_read`) + * and displayed as a horizontal line in the timeline that is visually distinct to the + * position of the user's own read receipt. + * @param {string} roomId ID of the room that has been read + * @param {string} rmEventId ID of the event that has been read + * @param {MatrixEvent} rrEvent the event tracked by the read receipt. This is here for + * convenience because the RR and the RM are commonly updated at the same time as each + * other. The local echo of this receipt will be done if set. Optional. + * @param {object} opts Options for the read markers + * @param {object} opts.hidden True to hide the receipt from other users and homeservers. + * This property is unstable and may change in the future. + * @return {Promise} Resolves: the empty object, {}. + */ + public async setRoomReadMarkers(roomId: string, rmEventId: string, rrEvent: MatrixEvent, opts: {hidden?: boolean}): Promise { + const room = this.getRoom(roomId); + if (room && room.hasPendingEvent(rmEventId)) { + throw new Error(`Cannot set read marker to a pending event (${rmEventId})`); } - promise = this._http.authedRequest( - undefined, "GET", path, params, undefined, - ).then(function(res) { - const token = res.next_token; - const matrixEvents = []; - - for (let i = 0; i < res.notifications.length; i++) { - const notification = res.notifications[i]; - const event = self.getEventMapper()(notification.event); - event.setPushActions( - PushProcessor.actionListToActionsObject(notification.actions), - ); - event.event.room_id = notification.room_id; // XXX: gutwrenching - matrixEvents[i] = event; - } - - eventTimeline.getTimelineSet() - .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); - - // if we've hit the end of the timeline, we need to stop trying to - // paginate. We need to keep the 'forwards' token though, to make sure - // we can recover from gappy syncs. - if (backwards && !res.next_token) { - eventTimeline.setPaginationToken(null, dir); - } - return res.next_token ? true : false; - }).finally(function() { - eventTimeline._paginationRequests[dir] = null; - }); - eventTimeline._paginationRequests[dir] = promise; - } else { - const room = this.getRoom(eventTimeline.getRoomId()); - if (!room) { - throw new Error("Unknown room " + eventTimeline.getRoomId()); - } - - promise = this._createMessagesRequest( - eventTimeline.getRoomId(), - token, - opts.limit, - dir, - eventTimeline.getFilter()); - promise.then(function(res) { - if (res.state) { - const roomState = eventTimeline.getState(dir); - const stateEvents = res.state.map(self.getEventMapper()); - roomState.setUnknownStateEvents(stateEvents); + // Add the optional RR update, do local echo like `sendReceipt` + let rrEventId; + if (rrEvent) { + rrEventId = rrEvent.getId(); + if (room && room.hasPendingEvent(rrEventId)) { + throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`); } - const token = res.end; - const matrixEvents = res.chunk.map(self.getEventMapper()); - eventTimeline.getTimelineSet() - .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); - - // if we've hit the end of the timeline, we need to stop trying to - // paginate. We need to keep the 'forwards' token though, to make sure - // we can recover from gappy syncs. - if (backwards && res.end == res.start) { - eventTimeline.setPaginationToken(null, dir); + if (room) { + room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read"); } - return res.end != res.start; - }).finally(function() { - eventTimeline._paginationRequests[dir] = null; - }); - eventTimeline._paginationRequests[dir] = promise; - } - - return promise; -}; - -/** - * Reset the notifTimelineSet entirely, paginating in some historical notifs as - * a starting point for subsequent pagination. - */ -MatrixClient.prototype.resetNotifTimelineSet = function() { - if (!this._notifTimelineSet) { - return; - } - - // FIXME: This thing is a total hack, and results in duplicate events being - // added to the timeline both from /sync and /notifications, and lots of - // slow and wasteful processing and pagination. The correct solution is to - // extend /messages or /search or something to filter on notifications. - - // use the fictitious token 'end'. in practice we would ideally give it - // the oldest backwards pagination token from /sync, but /sync doesn't - // know about /notifications, so we have no choice but to start paginating - // from the current point in time. This may well overlap with historical - // notifs which are then inserted into the timeline by /sync responses. - this._notifTimelineSet.resetLiveTimeline('end', null); - - // we could try to paginate a single event at this point in order to get - // a more valid pagination token, but it just ends up with an out of order - // timeline. given what a mess this is and given we're going to have duplicate - // events anyway, just leave it with the dummy token for now. - /* - this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), { - backwards: true, - limit: 1 - }); - */ -}; + } -/** - * Peek into a room and receive updates about the room. This only works if the - * history visibility for the room is world_readable. - * @param {String} roomId The room to attempt to peek into. - * @return {Promise} Resolves: Room object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.peekInRoom = function(roomId) { - if (this._peekSync) { - this._peekSync.stopPeeking(); - } - this._peekSync = new SyncApi(this, this._clientOpts); - return this._peekSync.peek(roomId); -}; + return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, opts); + } + + /** + * Get a preview of the given URL as of (roughly) the given point in time, + * described as an object with OpenGraph keys and associated values. + * Attributes may be synthesized where actual OG metadata is lacking. + * Caches results to prevent hammering the server. + * @param {string} url The URL to get preview data for + * @param {Number} ts The preferred point in time that the preview should + * describe (ms since epoch). The preview returned will either be the most + * recent one preceding this timestamp if available, or failing that the next + * most recent available preview. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Object of OG metadata. + * @return {module:http-api.MatrixError} Rejects: with an error response. + * May return synthesized attributes if the URL lacked OG meta. + */ + public getUrlPreview(url: string, ts: number, callback?: Callback): Promise { + // bucket the timestamp to the nearest minute to prevent excessive spam to the server + // Surely 60-second accuracy is enough for anyone. + ts = Math.floor(ts / 60000) * 60000; + + const key = ts + "_" + url; + + // If there's already a request in flight (or we've handled it), return that instead. + const cachedPreview = this.urlPreviewCache[key]; + if (cachedPreview) { + if (callback) { + cachedPreview.then(callback).catch(callback); + } + return cachedPreview; + } -/** - * Stop any ongoing room peeking. - */ -MatrixClient.prototype.stopPeeking = function() { - if (this._peekSync) { - this._peekSync.stopPeeking(); - this._peekSync = null; - } -}; + const resp = this.http.authedRequest( + callback, "GET", "/preview_url", { + url: url, + ts: ts, + }, undefined, { + prefix: PREFIX_MEDIA_R0, + }, + ); + // TODO: Expire the URL preview cache sometimes + this.urlPreviewCache[key] = resp; + return resp; + } + + /** + * @param {string} roomId + * @param {boolean} isTyping + * @param {Number} timeoutMs + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendTyping(roomId: string, isTyping: boolean, timeoutMs: number, callback?: Callback): Promise { + if (this.isGuest()) { + return Promise.resolve({}); // guests cannot send typing notifications so don't bother. + } -/** - * Set r/w flags for guest access in a room. - * @param {string} roomId The room to configure guest access in. - * @param {Object} opts Options - * @param {boolean} opts.allowJoin True to allow guests to join this room. This - * implicitly gives guests write access. If false or not given, guests are - * explicitly forbidden from joining the room. - * @param {boolean} opts.allowRead True to set history visibility to - * be world_readable. This gives guests read access *from this point forward*. - * If false or not given, history visibility is not modified. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setGuestAccess = function(roomId, opts) { - const writePromise = this.sendStateEvent(roomId, "m.room.guest_access", { - guest_access: opts.allowJoin ? "can_join" : "forbidden", - }); - - let readPromise = Promise.resolve(); - if (opts.allowRead) { - readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", { - history_visibility: "world_readable", + const path = utils.encodeUri("/rooms/$roomId/typing/$userId", { + $roomId: roomId, + $userId: this.credentials.userId, }); + const data: any = { + typing: isTyping, + }; + if (isTyping) { + data.timeout = timeoutMs ? timeoutMs : 20000; + } + return this.http.authedRequest( + callback, "PUT", path, undefined, data, + ); } - return Promise.all([readPromise, writePromise]); -}; - -// Registration/Login operations -// ============================= - -/** - * Requests an email verification token for the purposes of registration. - * This API requests a token from the homeserver. - * The doesServerRequireIdServerParam() method can be used to determine if - * the server requires the id_server parameter to be provided. - * - * Parameters and return value are as for requestEmailToken + /** + * Determines the history of room upgrades for a given room, as far as the + * client can see. Returns an array of Rooms where the first entry is the + * oldest and the last entry is the newest (likely current) room. If the + * provided room is not found, this returns an empty list. This works in + * both directions, looking for older and newer rooms of the given room. + * @param {string} roomId The room ID to search from + * @param {boolean} verifyLinks If true, the function will only return rooms + * which can be proven to be linked. For example, rooms which have a create + * event pointing to an old room which the client is not aware of or doesn't + * have a matching tombstone would not be returned. + * @return {Room[]} An array of rooms representing the upgrade + * history. + */ + public getRoomUpgradeHistory(roomId: string, verifyLinks = false): Room[] { + let currentRoom = this.getRoom(roomId); + if (!currentRoom) return []; + + const upgradeHistory = [currentRoom]; + + // Work backwards first, looking at create events. + let createEvent = currentRoom.currentState.getStateEvents("m.room.create", ""); + while (createEvent) { + logger.log(`Looking at ${createEvent.getId()}`); + const predecessor = createEvent.getContent()['predecessor']; + if (predecessor && predecessor['room_id']) { + logger.log(`Looking at predecessor ${predecessor['room_id']}`); + const refRoom = this.getRoom(predecessor['room_id']); + if (!refRoom) break; // end of the chain + + if (verifyLinks) { + const tombstone = refRoom.currentState + .getStateEvents("m.room.tombstone", ""); + + if (!tombstone + || tombstone.getContent()['replacement_room'] !== refRoom.roomId) { + break; + } + } - * @param {string} email As requestEmailToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken - */ -MatrixClient.prototype.requestRegisterEmailToken = function(email, clientSecret, - sendAttempt, nextLink) { - return this._requestTokenFromEndpoint( - "/register/email/requestToken", - { - email: email, - client_secret: clientSecret, - send_attempt: sendAttempt, - next_link: nextLink, - }, - ); -}; + // Insert at the front because we're working backwards from the currentRoom + upgradeHistory.splice(0, 0, refRoom); + createEvent = refRoom.currentState.getStateEvents("m.room.create", ""); + } else { + // No further create events to look at + break; + } + } -/** - * Requests a text message verification token for the purposes of registration. - * This API requests a token from the homeserver. - * The doesServerRequireIdServerParam() method can be used to determine if - * the server requires the id_server parameter to be provided. - * - * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which - * phoneNumber should be parsed relative to. - * @param {string} phoneNumber The phone number, in national or international format - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken - */ -MatrixClient.prototype.requestRegisterMsisdnToken = function(phoneCountry, phoneNumber, - clientSecret, sendAttempt, nextLink) { - return this._requestTokenFromEndpoint( - "/register/msisdn/requestToken", - { - country: phoneCountry, - phone_number: phoneNumber, - client_secret: clientSecret, - send_attempt: sendAttempt, - next_link: nextLink, - }, - ); -}; + // Work forwards next, looking at tombstone events + let tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", ""); + while (tombstoneEvent) { + const refRoom = this.getRoom(tombstoneEvent.getContent()['replacement_room']); + if (!refRoom) break; // end of the chain + if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room -/** - * Requests an email verification token for the purposes of adding a - * third party identifier to an account. - * This API requests a token from the homeserver. - * The doesServerRequireIdServerParam() method can be used to determine if - * the server requires the id_server parameter to be provided. - * If an account with the given email address already exists and is - * associated with an account other than the one the user is authed as, - * it will either send an email to the address informing them of this - * or return M_THREEPID_IN_USE (which one is up to the Home Server). - * - * @param {string} email As requestEmailToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken - */ -MatrixClient.prototype.requestAdd3pidEmailToken = function(email, clientSecret, - sendAttempt, nextLink) { - return this._requestTokenFromEndpoint( - "/account/3pid/email/requestToken", - { - email: email, - client_secret: clientSecret, - send_attempt: sendAttempt, - next_link: nextLink, - }, - ); -}; + if (verifyLinks) { + createEvent = refRoom.currentState.getStateEvents("m.room.create", ""); + if (!createEvent || !createEvent.getContent()['predecessor']) break; -/** - * Requests a text message verification token for the purposes of adding a - * third party identifier to an account. - * This API proxies the Identity Server /validate/email/requestToken API, - * adding specific behaviour for the addition of phone numbers to an - * account, as requestAdd3pidEmailToken. - * - * @param {string} phoneCountry As requestRegisterMsisdnToken - * @param {string} phoneNumber As requestRegisterMsisdnToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken - */ -MatrixClient.prototype.requestAdd3pidMsisdnToken = function(phoneCountry, phoneNumber, - clientSecret, sendAttempt, nextLink) { - return this._requestTokenFromEndpoint( - "/account/3pid/msisdn/requestToken", - { - country: phoneCountry, - phone_number: phoneNumber, - client_secret: clientSecret, - send_attempt: sendAttempt, - next_link: nextLink, - }, - ); -}; + const predecessor = createEvent.getContent()['predecessor']; + if (predecessor['room_id'] !== currentRoom.roomId) break; + } -/** - * Requests an email verification token for the purposes of resetting - * the password on an account. - * This API proxies the Identity Server /validate/email/requestToken API, - * adding specific behaviour for the password resetting. Specifically, - * if no account with the given email address exists, it may either - * return M_THREEPID_NOT_FOUND or send an email - * to the address informing them of this (which one is up to the Home Server). - * - * requestEmailToken calls the equivalent API directly on the ID server, - * therefore bypassing the password reset specific logic. - * - * @param {string} email As requestEmailToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @param {module:client.callback} callback Optional. As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken - */ -MatrixClient.prototype.requestPasswordEmailToken = function(email, clientSecret, - sendAttempt, nextLink) { - return this._requestTokenFromEndpoint( - "/account/password/email/requestToken", - { - email: email, - client_secret: clientSecret, - send_attempt: sendAttempt, - next_link: nextLink, - }, - ); -}; + // Push to the end because we're looking forwards + upgradeHistory.push(refRoom); + const roomIds = new Set(upgradeHistory.map((ref) => ref.roomId)); + if (roomIds.size < upgradeHistory.length) { + // The last room added to the list introduced a previous roomId + // To avoid recursion, return the last rooms - 1 + return upgradeHistory.slice(0, upgradeHistory.length - 1); + } -/** - * Requests a text message verification token for the purposes of resetting - * the password on an account. - * This API proxies the Identity Server /validate/email/requestToken API, - * adding specific behaviour for the password resetting, as requestPasswordEmailToken. - * - * @param {string} phoneCountry As requestRegisterMsisdnToken - * @param {string} phoneNumber As requestRegisterMsisdnToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken - */ -MatrixClient.prototype.requestPasswordMsisdnToken = function(phoneCountry, phoneNumber, - clientSecret, sendAttempt, nextLink) { - return this._requestTokenFromEndpoint( - "/account/password/msisdn/requestToken", - { - country: phoneCountry, - phone_number: phoneNumber, - client_secret: clientSecret, - send_attempt: sendAttempt, - next_link: nextLink, - }, - ); -}; + // Set the current room to the reference room so we know where we're at + currentRoom = refRoom; + tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", ""); + } -/** - * Internal utility function for requesting validation tokens from usage-specific - * requestToken endpoints. - * - * @param {string} endpoint The endpoint to send the request to - * @param {object} params Parameters for the POST request - * @return {Promise} Resolves: As requestEmailToken - */ -MatrixClient.prototype._requestTokenFromEndpoint = async function(endpoint, params) { - const postParams = Object.assign({}, params); + return upgradeHistory; + } + + /** + * @param {string} roomId + * @param {string} userId + * @param {module:client.callback} callback Optional. + * @param {string} reason Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public invite(roomId: string, userId: string, callback?: Callback, reason?: string): Promise { + return this.membershipChange(roomId, userId, "invite", reason, callback); + } + + /** + * Invite a user to a room based on their email address. + * @param {string} roomId The room to invite the user to. + * @param {string} email The email address to invite. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public inviteByEmail(roomId: string, email: string, callback?: Callback): Promise { + return this.inviteByThreePid(roomId, "email", email, callback); + } + + /** + * Invite a user to a room based on a third-party identifier. + * @param {string} roomId The room to invite the user to. + * @param {string} medium The medium to invite the user e.g. "email". + * @param {string} address The address for the specified medium. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async inviteByThreePid(roomId: string, medium: string, address: string, callback?: Callback): Promise { + const path = utils.encodeUri( + "/rooms/$roomId/invite", + { $roomId: roomId }, + ); - // If the HS supports separate add and bind, then requestToken endpoints - // don't need an IS as they are all validated by the HS directly. - if (!await this.doesServerSupportSeparateAddAndBind() && this.idBaseUrl) { - const idServerUrl = url.parse(this.idBaseUrl); - if (!idServerUrl.host) { - throw new Error("Invalid ID server URL: " + this.idBaseUrl); + const identityServerUrl = this.getIdentityServerUrl(true); + if (!identityServerUrl) { + return Promise.reject(new MatrixError({ + error: "No supplied identity server URL", + errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM", + })); } - postParams.id_server = idServerUrl.host; + const params = { + id_server: identityServerUrl, + medium: medium, + address: address, + }; if ( this.identityServer && @@ -4550,1086 +3710,1828 @@ MatrixClient.prototype._requestTokenFromEndpoint = async function(endpoint, para ) { const identityAccessToken = await this.identityServer.getAccessToken(); if (identityAccessToken) { - postParams.id_access_token = identityAccessToken; + params['id_access_token'] = identityAccessToken; } } - } - - return this._http.request( - undefined, "POST", endpoint, undefined, - postParams, - ); -}; - -// Push operations -// =============== -/** - * Get the room-kind push rule associated with a room. - * @param {string} scope "global" or device-specific. - * @param {string} roomId the id of the room. - * @return {object} the rule or undefined. - */ -MatrixClient.prototype.getRoomPushRule = function(scope, roomId) { - // There can be only room-kind push rule per room - // and its id is the room id. - if (this.pushRules) { - for (let i = 0; i < this.pushRules[scope].room.length; i++) { - const rule = this.pushRules[scope].room[i]; - if (rule.rule_id === roomId) { - return rule; + return this.http.authedRequest(callback, "POST", path, undefined, params); + } + + /** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public leave(roomId: string, callback?: Callback): Promise { + return this.membershipChange(roomId, undefined, "leave", undefined, callback); + } + + /** + * Leaves all rooms in the chain of room upgrades based on the given room. By + * default, this will leave all the previous and upgraded rooms, including the + * given room. To only leave the given room and any previous rooms, keeping the + * upgraded (modern) rooms untouched supply `false` to `includeFuture`. + * @param {string} roomId The room ID to start leaving at + * @param {boolean} includeFuture If true, the whole chain (past and future) of + * upgraded rooms will be left. + * @return {Promise} Resolves when completed with an object keyed + * by room ID and value of the error encountered when leaving or null. + */ + public leaveRoomChain(roomId: string, includeFuture = true): Promise<{[roomId: string]: Error | null}> { + const upgradeHistory = this.getRoomUpgradeHistory(roomId); + + let eligibleToLeave = upgradeHistory; + if (!includeFuture) { + eligibleToLeave = []; + for (const room of upgradeHistory) { + eligibleToLeave.push(room); + if (room.roomId === roomId) { + break; + } } } - } else { - throw new Error( - "SyncApi.sync() must be done before accessing to push rules.", - ); - } -}; -/** - * Set a room-kind muting push rule in a room. - * The operation also updates MatrixClient.pushRules at the end. - * @param {string} scope "global" or device-specific. - * @param {string} roomId the id of the room. - * @param {string} mute the mute state. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) { - const self = this; - let deferred; - let hasDontNotifyRule; + const populationResults = {}; // {roomId: Error} + const promises = []; - // Get the existing room-kind push rule if any - const roomPushRule = this.getRoomPushRule(scope, roomId); - if (roomPushRule) { - if (0 <= roomPushRule.actions.indexOf("dont_notify")) { - hasDontNotifyRule = true; - } - } + const doLeave = (roomId) => { + return this.leave(roomId).then(() => { + populationResults[roomId] = null; + }).catch((err) => { + populationResults[roomId] = err; + return null; // suppress error + }); + }; - if (!mute) { - // Remove the rule only if it is a muting rule - if (hasDontNotifyRule) { - deferred = this.deletePushRule(scope, "room", roomPushRule.rule_id); + for (const room of eligibleToLeave) { + promises.push(doLeave(room.roomId)); } - } else { - if (!roomPushRule) { - deferred = this.addPushRule(scope, "room", roomId, { - actions: ["dont_notify"], - }); - } else if (!hasDontNotifyRule) { - // Remove the existing one before setting the mute push rule - // This is a workaround to SYN-590 (Push rule update fails) - deferred = utils.defer(); - this.deletePushRule(scope, "room", roomPushRule.rule_id) - .then(function() { - self.addPushRule(scope, "room", roomId, { - actions: ["dont_notify"], - }).then(function() { - deferred.resolve(); - }, function(err) { - deferred.reject(err); - }); - }, function(err) { - deferred.reject(err); - }); - deferred = deferred.promise; + return Promise.all(promises).then(() => populationResults); + } + + /** + * @param {string} roomId + * @param {string} userId + * @param {string} reason Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public ban(roomId: string, userId: string, reason?: string, callback?: Callback) { + return this.membershipChange(roomId, userId, "ban", reason, callback); + } + + /** + * @param {string} roomId + * @param {boolean} deleteRoom True to delete the room from the store on success. + * Default: true. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public forget(roomId: string, deleteRoom?: boolean, callback?: Callback): Promise { + if (deleteRoom === undefined) { + deleteRoom = true; } + const promise = this.membershipChange(roomId, undefined, "forget", undefined, + callback); + if (!deleteRoom) { + return promise; + } + const self = this; + return promise.then(function(response) { + self.store.removeRoom(roomId); + self.emit("deleteRoom", roomId); + return response; + }); } - if (deferred) { - return new Promise((resolve, reject) => { - // Update this.pushRules when the operation completes - deferred.then(function() { - self.getPushRules().then(function(result) { - self.pushRules = result; - resolve(); - }, function(err) { - reject(err); - }); - }, function(err) { - // Update it even if the previous operation fails. This can help the - // app to recover when push settings has been modifed from another client - self.getPushRules().then(function(result) { - self.pushRules = result; - reject(err); - }, function(err2) { - reject(err); - }); - }); + /** + * @param {string} roomId + * @param {string} userId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Object (currently empty) + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public unban(roomId: string, userId: string, callback?: Callback): Promise { + // unbanning != set their state to leave: this used to be + // the case, but was then changed so that leaving was always + // a revoking of privilege, otherwise two people racing to + // kick / ban someone could end up banning and then un-banning + // them. + const path = utils.encodeUri("/rooms/$roomId/unban", { + $roomId: roomId, }); + const data = { + user_id: userId, + }; + return this.http.authedRequest( + callback, "POST", path, undefined, data, + ); } -}; -// Search -// ====== + /** + * @param {string} roomId + * @param {string} userId + * @param {string} reason Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public kick(roomId: string, userId: string, reason?: string, callback?: Callback): Promise { + return this.setMembershipState(roomId, userId, "leave", reason, callback); + } + + /** + * This is an internal method. + * @param {MatrixClient} client + * @param {string} roomId + * @param {string} userId + * @param {string} membershipValue + * @param {string} reason + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + private setMembershipState(roomId: string, userId: string, membershipValue: string, reason?: string, callback?: Callback) { + if (utils.isFunction(reason)) { + callback = reason as any as Callback; // legacy + reason = undefined; + } -/** - * Perform a server-side search for messages containing the given text. - * @param {Object} opts Options for the search. - * @param {string} opts.query The text to query. - * @param {string=} opts.keys The keys to search on. Defaults to all keys. One - * of "content.body", "content.name", "content.topic". - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.searchMessageText = function(opts, callback) { - const roomEvents = { - search_term: opts.query, - }; + const path = utils.encodeUri( + "/rooms/$roomId/state/m.room.member/$userId", + { $roomId: roomId, $userId: userId }, + ); - if ('keys' in opts) { - roomEvents.keys = opts.keys; + return this.http.authedRequest(callback, "PUT", path, undefined, { + membership: membershipValue, + reason: reason, + }); } - return this.search({ - body: { - search_categories: { - room_events: roomEvents, - }, - }, - }, callback); -}; + private membershipChange(roomId: string, userId: string, membership: string, reason?: string, callback?: Callback): Promise { + if (utils.isFunction(reason)) { + callback = reason as any as Callback; // legacy + reason = undefined; + } -/** - * Perform a server-side search for room events. - * - * The returned promise resolves to an object containing the fields: - * - * * {number} count: estimate of the number of results - * * {string} next_batch: token for back-pagination; if undefined, there are - * no more results - * * {Array} highlights: a list of words to highlight from the stemming - * algorithm - * * {Array} results: a list of results - * - * Each entry in the results list is a {module:models/search-result.SearchResult}. - * - * @param {Object} opts - * @param {string} opts.term the term to search for - * @param {Object} opts.filter a JSON filter object to pass in the request - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.searchRoomEvents = function(opts) { - // TODO: support groups - - const body = { - search_categories: { - room_events: { - search_term: opts.term, - filter: opts.filter, - order_by: "recent", - event_context: { - before_limit: 1, - after_limit: 1, - include_profile: true, - }, + const path = utils.encodeUri("/rooms/$room_id/$membership", { + $room_id: roomId, + $membership: membership, + }); + return this.http.authedRequest( + callback, "POST", path, undefined, { + user_id: userId, // may be undefined e.g. on leave + reason: reason, }, - }, - }; + ); + } - const searchResults = { - _query: body, - results: [], - highlights: [], - }; + /** + * Obtain a dict of actions which should be performed for this event according + * to the push rules for this user. Caches the dict on the event. + * @param {MatrixEvent} event The event to get push actions for. + * @return {module:pushprocessor~PushAction} A dict of actions to perform. + */ + public getPushActionsForEvent(event: MatrixEvent): PushAction { + if (!event.getPushActions()) { + event.setPushActions(this.pushProcessor.actionsForEvent(event)); + } + return event.getPushActions(); + } + + /** + * @param {string} info The kind of info to set (e.g. 'avatar_url') + * @param {Object} data The JSON object to set. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setProfileInfo(info: string, data: any, callback?: Callback): Promise { + const path = utils.encodeUri("/profile/$userId/$info", { + $userId: this.credentials.userId, + $info: info, + }); + return this.http.authedRequest( + callback, "PUT", path, undefined, data, + ); + } - return this.search({ body: body }).then( - this._processRoomEventsSearch.bind(this, searchResults), - ); -}; + /** + * @param {string} name + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: {} an empty object. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async setDisplayName(name: string, callback?: Callback): Promise { + const prom = await this.setProfileInfo( + "displayname", { displayname: name }, callback, + ); + // XXX: synthesise a profile update for ourselves because Synapse is broken and won't + const user = this.getUser(this.getUserId()); + if (user) { + user.displayName = name; + user.emit("User.displayName", user.events.presence, user); + } + return prom; + } + + /** + * @param {string} url + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: {} an empty object. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async setAvatarUrl(url: string, callback?: Callback): Promise { + const prom = await this.setProfileInfo( + "avatar_url", { avatar_url: url }, callback, + ); + // XXX: synthesise a profile update for ourselves because Synapse is broken and won't + const user = this.getUser(this.getUserId()); + if (user) { + user.avatarUrl = url; + user.emit("User.avatarUrl", user.events.presence, user); + } + return prom; + } + + /** + * Turn an MXC URL into an HTTP one. This method is experimental and + * may change. + * @param {string} mxcUrl The MXC URL + * @param {Number} width The desired width of the thumbnail. + * @param {Number} height The desired height of the thumbnail. + * @param {string} resizeMethod The thumbnail resize method to use, either + * "crop" or "scale". + * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * directly. Fetching such URLs will leak information about the user to + * anyone they share a room with. If false, will return null for such URLs. + * @return {?string} the avatar URL or null. + */ + public mxcUrlToHttp(mxcUrl: string, width: number, height: number, resizeMethod: string, allowDirectLinks: boolean): string | null { + return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks); + } + + /** + * Sets a new status message for the user. The message may be null/falsey + * to clear the message. + * @param {string} newMessage The new message to set. + * @return {Promise} Resolves: to nothing + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public _unstable_setStatusMessage(newMessage: string): Promise { + const type = "im.vector.user_status"; + return Promise.all(this.getRooms().map((room) => { + const isJoined = room.getMyMembership() === "join"; + const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2; + if (!isJoined || !looksLikeDm) { + return Promise.resolve(); + } + // Check power level separately as it's a bit more expensive. + const maySend = room.currentState.mayClientSendStateEvent(type, this); + if (!maySend) { + return Promise.resolve(); + } + return this.sendStateEvent(room.roomId, type, { + status: newMessage, + }, this.getUserId()); + })).then(); // .then to fix return type + } + + /** + * @param {Object} opts Options to apply + * @param {string} opts.presence One of "online", "offline" or "unavailable" + * @param {string} opts.status_msg The status message to attach. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws If 'presence' isn't a valid presence enum value. + */ + public setPresence(opts: IPresenceOpts, callback?: Callback): Promise { + const path = utils.encodeUri("/presence/$userId/status", { + $userId: this.credentials.userId, + }); -/** - * Take a result from an earlier searchRoomEvents call, and backfill results. - * - * @param {object} searchResults the results object to be updated - * @return {Promise} Resolves: updated result object - * @return {Error} Rejects: with an error response. - */ -MatrixClient.prototype.backPaginateRoomEventsSearch = function(searchResults) { - // TODO: we should implement a backoff (as per scrollback()) to deal more - // nicely with HTTP errors. + if (typeof opts === "string") { + opts = { presence: opts }; // legacy + } - if (!searchResults.next_batch) { - return Promise.reject(new Error("Cannot backpaginate event search any further")); + const validStates = ["offline", "online", "unavailable"]; + if (validStates.indexOf(opts.presence) === -1) { + throw new Error("Bad presence value: " + opts.presence); + } + return this.http.authedRequest( + callback, "PUT", path, undefined, opts, + ); } - if (searchResults.pendingRequest) { - // already a request in progress - return the existing promise - return searchResults.pendingRequest; - } + /** + * @param {string} userId The user to get presence for + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: The presence state for this user. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getPresence(userId: string, callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/presence/$userId/status", { + $userId: userId, + }); - const searchOpts = { - body: searchResults._query, - next_batch: searchResults.next_batch, - }; + return this.http.authedRequest(callback, "GET", path, undefined, undefined); + } + + /** + * Retrieve older messages from the given room and put them in the timeline. + * + * If this is called multiple times whilst a request is ongoing, the same + * Promise will be returned. If there was a problem requesting scrollback, there + * will be a small delay before another request can be made (to prevent tight-looping + * when there is no connection). + * + * @param {Room} room The room to get older messages in. + * @param {Integer} limit Optional. The maximum number of previous events to + * pull in. Default: 30. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Room. If you are at the beginning + * of the timeline, Room.oldState.paginationToken will be + * null. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public scrollback(room: Room, limit: number, callback?: Callback): Promise { + if (utils.isFunction(limit)) { + callback = limit as any as Callback; // legacy + limit = undefined; + } + limit = limit || 30; + let timeToWaitMs = 0; + + let info = this.ongoingScrollbacks[room.roomId] || {}; + if (info.promise) { + return info.promise; + } else if (info.errorTs) { + const timeWaitedMs = Date.now() - info.errorTs; + timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0); + } + + if (room.oldState.paginationToken === null) { + return Promise.resolve(room); // already at the start. + } + // attempt to grab more events from the store first + const numAdded = this.store.scrollback(room, limit).length; + if (numAdded === limit) { + // store contained everything we needed. + return Promise.resolve(room); + } + // reduce the required number of events appropriately + limit = limit - numAdded; + + const prom = new Promise((resolve, reject) => { + // wait for a time before doing this request + // (which may be 0 in order not to special case the code paths) + sleep(timeToWaitMs).then(() => { + return this.createMessagesRequest( + room.roomId, + room.oldState.paginationToken, + limit, + 'b'); + }).then((res) => { + const matrixEvents = res.chunk.map(this.getEventMapper()); + if (res.state) { + const stateEvents = res.state.map(this.getEventMapper()); + room.currentState.setUnknownStateEvents(stateEvents); + } + room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline()); + room.oldState.paginationToken = res.end; + if (res.chunk.length === 0) { + room.oldState.paginationToken = null; + } + this.store.storeEvents(room, matrixEvents, res.end, true); + this.ongoingScrollbacks[room.roomId] = null; + callback?.(null, room); + resolve(room); + }).catch((err) => { + this.ongoingScrollbacks[room.roomId] = { + errorTs: Date.now(), + }; + callback?.(err); + reject(err); + }); + }); - const promise = this.search(searchOpts).then( - this._processRoomEventsSearch.bind(this, searchResults), - ).finally(function() { - searchResults.pendingRequest = null; - }); - searchResults.pendingRequest = promise; + info = { + promise: prom, + errorTs: null, + }; - return promise; -}; + this.ongoingScrollbacks[room.roomId] = info; + return prom; + } + + /** + * @param {object} [options] + * @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client + * @param {bool} options.decrypt decrypt event proactively + * @return {Function} + */ + public getEventMapper(options?: MapperOpts): EventMapper { + return eventMapperFor(this, options); + } + + /** + * Get an EventTimeline for the given event + * + *

If the EventTimelineSet object already has the given event in its store, the + * corresponding timeline will be returned. Otherwise, a /context request is + * made, and used to construct an EventTimeline. + * + * @param {EventTimelineSet} timelineSet The timelineSet to look for the event in + * @param {string} eventId The ID of the event to look for + * + * @return {Promise} Resolves: + * {@link module:models/event-timeline~EventTimeline} including the given + * event + */ + public getEventTimeline(timelineSet: EventTimelineSet, eventId: string): EventTimeline { + // don't allow any timeline support unless it's been enabled. + if (!this.timelineSupport) { + throw new Error("timeline support is disabled. Set the 'timelineSupport'" + + " parameter to true when creating MatrixClient to enable" + + " it."); + } -/** - * helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the - * response from the API call and updates the searchResults - * - * @param {Object} searchResults - * @param {Object} response - * @return {Object} searchResults - * @private - */ -MatrixClient.prototype._processRoomEventsSearch = function(searchResults, response) { - const room_events = response.search_categories.room_events; - - searchResults.count = room_events.count; - searchResults.next_batch = room_events.next_batch; - - // combine the highlight list with our existing list; build an object - // to avoid O(N^2) fail - const highlights = {}; - room_events.highlights.forEach(function(hl) { - highlights[hl] = 1; - }); - searchResults.highlights.forEach(function(hl) { - highlights[hl] = 1; - }); - - // turn it back into a list. - searchResults.highlights = Object.keys(highlights); - - // append the new results to our existing results - const resultsLength = room_events.results ? room_events.results.length : 0; - for (let i = 0; i < resultsLength; i++) { - const sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper()); - searchResults.results.push(sr); - } - return searchResults; -}; + if (timelineSet.getTimelineForEvent(eventId)) { + return Promise.resolve(timelineSet.getTimelineForEvent(eventId)); + } -/** - * Populate the store with rooms the user has left. - * @return {Promise} Resolves: TODO - Resolved when the rooms have - * been added to the data store. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.syncLeftRooms = function() { - // Guard against multiple calls whilst ongoing and multiple calls post success - if (this._syncedLeftRooms) { - return Promise.resolve([]); // don't call syncRooms again if it succeeded. - } - if (this._syncLeftRoomsPromise) { - return this._syncLeftRoomsPromise; // return the ongoing request - } - const self = this; - const syncApi = new SyncApi(this, this._clientOpts); - this._syncLeftRoomsPromise = syncApi.syncLeftRooms(); + const path = utils.encodeUri( + "/rooms/$roomId/context/$eventId", { + $roomId: timelineSet.room.roomId, + $eventId: eventId, + }, + ); + + let params = undefined; + if (this.clientOpts.lazyLoadMembers) { + params = {filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)}; + } - // cleanup locks - this._syncLeftRoomsPromise.then(function(res) { - logger.log("Marking success of sync left room request"); - self._syncedLeftRooms = true; // flip the bit on success - }).finally(function() { - self._syncLeftRoomsPromise = null; // cleanup ongoing request state - }); + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + const promise = this.http.authedRequest(undefined, "GET", path, params).then((res) => { + if (!res.event) { + throw new Error("'event' not in '/context' result - homeserver too old?"); + } - return this._syncLeftRoomsPromise; -}; + // by the time the request completes, the event might have ended up in + // the timeline. + if (timelineSet.getTimelineForEvent(eventId)) { + return timelineSet.getTimelineForEvent(eventId); + } -// Filters -// ======= + // we start with the last event, since that's the point at which we + // have known state. + // events_after is already backwards; events_before is forwards. + res.events_after.reverse(); + const events = res.events_after + .concat([res.event]) + .concat(res.events_before); + const matrixEvents = events.map(this.getEventMapper()); + + let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId()); + if (!timeline) { + timeline = timelineSet.addTimeline(); + timeline.initialiseState(res.state.map(this.getEventMapper())); + timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end; + } else { + const stateEvents = res.state.map(this.getEventMapper()); + timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents); + } + timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start); + + // there is no guarantee that the event ended up in "timeline" (we + // might have switched to a neighbouring timeline) - so check the + // room's index again. On the other hand, there's no guarantee the + // event ended up anywhere, if it was later redacted, so we just + // return the timeline we first thought of. + return timelineSet.getTimelineForEvent(eventId) || timeline; + }); + return promise; + } -/** - * Create a new filter. - * @param {Object} content The HTTP body for the request - * @return {Filter} Resolves to a Filter object. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.createFilter = function(content) { - const self = this; - const path = utils.encodeUri("/user/$userId/filter", { - $userId: this.credentials.userId, - }); - return this._http.authedRequest( - undefined, "POST", path, undefined, content, - ).then(function(response) { - // persist the filter - const filter = Filter.fromJson( - self.credentials.userId, response.filter_id, content, + /** + * Makes a request to /messages with the appropriate lazy loading filter set. + * XXX: if we do get rid of scrollback (as it's not used at the moment), + * we could inline this method again in paginateEventTimeline as that would + * then be the only call-site + * @param {string} roomId + * @param {string} fromToken + * @param {number} limit the maximum amount of events the retrieve + * @param {string} dir 'f' or 'b' + * @param {Filter} timelineFilter the timeline filter to pass + * @return {Promise} + */ + private createMessagesRequest(roomId: string, fromToken: string, limit: number, dir: string, timelineFilter?: Filter): Promise { // TODO: Types + const path = utils.encodeUri( + "/rooms/$roomId/messages", { $roomId: roomId }, ); - self.store.storeFilter(filter); - return filter; - }); -}; + if (limit === undefined) { + limit = 30; + } + const params: any = { + from: fromToken, + limit: limit, + dir: dir, + }; -/** - * Retrieve a filter. - * @param {string} userId The user ID of the filter owner - * @param {string} filterId The filter ID to retrieve - * @param {boolean} allowCached True to allow cached filters to be returned. - * Default: True. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) { - if (allowCached) { - const filter = this.store.getFilter(userId, filterId); + let filter = null; + if (this.clientOpts.lazyLoadMembers) { + // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, + // so the timelineFilter doesn't get written into it below + filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER); + } + if (timelineFilter) { + // XXX: it's horrific that /messages' filter parameter doesn't match + // /sync's one - see https://matrix.org/jira/browse/SPEC-451 + filter = filter || {}; + Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()); + } if (filter) { - return Promise.resolve(filter); + params.filter = JSON.stringify(filter); + } + return this.http.authedRequest(undefined, "GET", path, params); + } + + /** + * Take an EventTimeline, and back/forward-fill results. + * + * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline + * object to be updated + * @param {Object} [opts] + * @param {bool} [opts.backwards = false] true to fill backwards, + * false to go forwards + * @param {number} [opts.limit = 30] number of events to request + * + * @return {Promise} Resolves to a boolean: false if there are no + * events and we reached either end of the timeline; else true. + */ + public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise { + const isNotifTimeline = (eventTimeline.getTimelineSet() === this._notifTimelineSet); + + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + opts = opts || {}; + const backwards = opts.backwards || false; + + if (isNotifTimeline) { + if (!backwards) { + throw new Error("paginateNotifTimeline can only paginate backwards"); + } } - } - const self = this; - const path = utils.encodeUri("/user/$userId/filter/$filterId", { - $userId: userId, - $filterId: filterId, - }); + const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; - return this._http.authedRequest( - undefined, "GET", path, undefined, undefined, - ).then(function(response) { - // persist the filter - const filter = Filter.fromJson( - userId, filterId, response, - ); - self.store.storeFilter(filter); - return filter; - }); -}; + const token = eventTimeline.getPaginationToken(dir); + if (!token) { + // no token - no results. + return Promise.resolve(false); + } -/** - * @param {string} filterName - * @param {Filter} filter - * @return {Promise} Filter ID - */ -MatrixClient.prototype.getOrCreateFilter = async function(filterName, filter) { - const filterId = this.store.getFilterIdByName(filterName); - let existingId = undefined; + const pendingRequest = eventTimeline._paginationRequests[dir]; - if (filterId) { - // check that the existing filter matches our expectations - try { - const existingFilter = - await this.getFilter(this.credentials.userId, filterId, true); - if (existingFilter) { - const oldDef = existingFilter.getDefinition(); - const newDef = filter.getDefinition(); - - if (utils.deepCompare(oldDef, newDef)) { - // super, just use that. - // debuglog("Using existing filter ID %s: %s", filterId, - // JSON.stringify(oldDef)); - existingId = filterId; - } + if (pendingRequest) { + // already a request in progress - return the existing promise + return pendingRequest; + } + + let path; + let params; + let promise; + const self = this; + + if (isNotifTimeline) { + path = "/notifications"; + params = { + limit: ('limit' in opts) ? opts.limit : 30, + only: 'highlight', + }; + + if (token && token !== "end") { + params.from = token; } - } catch (error) { - // Synapse currently returns the following when the filter cannot be found: - // { - // errcode: "M_UNKNOWN", - // name: "M_UNKNOWN", - // message: "No row found", - // } - if (error.errcode !== "M_UNKNOWN" && error.errcode !== "M_NOT_FOUND") { - throw error; + + promise = this.http.authedRequest( + undefined, "GET", path, params, undefined, + ).then(function(res) { + const token = res.next_token; + const matrixEvents = []; + + for (let i = 0; i < res.notifications.length; i++) { + const notification = res.notifications[i]; + const event = self.getEventMapper()(notification.event); + event.setPushActions( + PushProcessor.actionListToActionsObject(notification.actions), + ); + event.event.room_id = notification.room_id; // XXX: gutwrenching + matrixEvents[i] = event; + } + + eventTimeline.getTimelineSet() + .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); + + // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + if (backwards && !res.next_token) { + eventTimeline.setPaginationToken(null, dir); + } + return res.next_token ? true : false; + }).finally(function() { + eventTimeline._paginationRequests[dir] = null; + }); + eventTimeline._paginationRequests[dir] = promise; + } else { + const room = this.getRoom(eventTimeline.getRoomId()); + if (!room) { + throw new Error("Unknown room " + eventTimeline.getRoomId()); } + + promise = this.createMessagesRequest( + eventTimeline.getRoomId(), + token, + opts.limit, + dir, + eventTimeline.getFilter()); + promise.then(function(res) { + if (res.state) { + const roomState = eventTimeline.getState(dir); + const stateEvents = res.state.map(self.getEventMapper()); + roomState.setUnknownStateEvents(stateEvents); + } + const token = res.end; + const matrixEvents = res.chunk.map(self.getEventMapper()); + eventTimeline.getTimelineSet() + .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); + + // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + if (backwards && res.end == res.start) { + eventTimeline.setPaginationToken(null, dir); + } + return res.end != res.start; + }).finally(function() { + eventTimeline._paginationRequests[dir] = null; + }); + eventTimeline._paginationRequests[dir] = promise; + } + + return promise; + } + + /** + * Reset the notifTimelineSet entirely, paginating in some historical notifs as + * a starting point for subsequent pagination. + */ + public resetNotifTimelineSet() { + if (!this.notifTimelineSet) { + return; } - // if the filter doesn't exist anymore on the server, remove from store - if (!existingId) { - this.store.setFilterIdByName(filterName, undefined); + + // FIXME: This thing is a total hack, and results in duplicate events being + // added to the timeline both from /sync and /notifications, and lots of + // slow and wasteful processing and pagination. The correct solution is to + // extend /messages or /search or something to filter on notifications. + + // use the fictitious token 'end'. in practice we would ideally give it + // the oldest backwards pagination token from /sync, but /sync doesn't + // know about /notifications, so we have no choice but to start paginating + // from the current point in time. This may well overlap with historical + // notifs which are then inserted into the timeline by /sync responses. + this.notifTimelineSet.resetLiveTimeline('end', null); + + // we could try to paginate a single event at this point in order to get + // a more valid pagination token, but it just ends up with an out of order + // timeline. given what a mess this is and given we're going to have duplicate + // events anyway, just leave it with the dummy token for now. + /* + this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), { + backwards: true, + limit: 1 + }); + */ + } + + /** + * Peek into a room and receive updates about the room. This only works if the + * history visibility for the room is world_readable. + * @param {String} roomId The room to attempt to peek into. + * @return {Promise} Resolves: Room object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public peekInRoom(roomId: string): Promise { + if (this.peekSync) { + this.peekSync.stopPeeking(); } + this.peekSync = new SyncApi(this, this.clientOpts); + return this.peekSync.peek(roomId); } - if (existingId) { - return existingId; + /** + * Stop any ongoing room peeking. + */ + public stopPeeking() { + if (this.peekSync) { + this.peekSync.stopPeeking(); + this.peekSync = null; + } } - // create a new filter - const createdFilter = await this.createFilter(filter.getDefinition()); + /** + * Set r/w flags for guest access in a room. + * @param {string} roomId The room to configure guest access in. + * @param {Object} opts Options + * @param {boolean} opts.allowJoin True to allow guests to join this room. This + * implicitly gives guests write access. If false or not given, guests are + * explicitly forbidden from joining the room. + * @param {boolean} opts.allowRead True to set history visibility to + * be world_readable. This gives guests read access *from this point forward*. + * If false or not given, history visibility is not modified. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setGuestAccess(roomId: string, opts: IGuestAccessOpts): Promise { + const writePromise = this.sendStateEvent(roomId, "m.room.guest_access", { + guest_access: opts.allowJoin ? "can_join" : "forbidden", + }); - // debuglog("Created new filter ID %s: %s", createdFilter.filterId, - // JSON.stringify(createdFilter.getDefinition())); - this.store.setFilterIdByName(filterName, createdFilter.filterId); - return createdFilter.filterId; -}; + let readPromise = Promise.resolve(); + if (opts.allowRead) { + readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", { + history_visibility: "world_readable", + }); + } -/** - * Gets a bearer token from the Home Server that the user can - * present to a third party in order to prove their ownership - * of the Matrix account they are logged into. - * @return {Promise} Resolves: Token object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.getOpenIdToken = function() { - const path = utils.encodeUri("/user/$userId/openid/request_token", { - $userId: this.credentials.userId, - }); + return Promise.all([readPromise, writePromise]).then(); // .then() to hide results for contract + } + + /** + * Requests an email verification token for the purposes of registration. + * This API requests a token from the homeserver. + * The doesServerRequireIdServerParam() method can be used to determine if + * the server requires the id_server parameter to be provided. + * + * Parameters and return value are as for requestEmailToken + + * @param {string} email As requestEmailToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + public requestRegisterEmailToken(email: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + return this.requestTokenFromEndpoint( + "/register/email/requestToken", + { + email: email, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink, + }, + ); + } - return this._http.authedRequest( - undefined, "POST", path, undefined, {}, - ); -}; + /** + * Requests a text message verification token for the purposes of registration. + * This API requests a token from the homeserver. + * The doesServerRequireIdServerParam() method can be used to determine if + * the server requires the id_server parameter to be provided. + * + * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which + * phoneNumber should be parsed relative to. + * @param {string} phoneNumber The phone number, in national or international format + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + public requestRegisterMsisdnToken(phoneCountry: string, phoneNumber: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + return this.requestTokenFromEndpoint( + "/register/msisdn/requestToken", + { + country: phoneCountry, + phone_number: phoneNumber, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink, + }, + ); + } -// VoIP operations -// =============== + /** + * Requests an email verification token for the purposes of adding a + * third party identifier to an account. + * This API requests a token from the homeserver. + * The doesServerRequireIdServerParam() method can be used to determine if + * the server requires the id_server parameter to be provided. + * If an account with the given email address already exists and is + * associated with an account other than the one the user is authed as, + * it will either send an email to the address informing them of this + * or return M_THREEPID_IN_USE (which one is up to the Home Server). + * + * @param {string} email As requestEmailToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + public requestAdd3pidEmailToken(email: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + return this.requestTokenFromEndpoint( + "/account/3pid/email/requestToken", + { + email: email, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink, + }, + ); + } -MatrixClient.prototype._startCallEventHandler = function() { - if (this.isInitialSyncComplete()) { - this._callEventHandler.start(); - this.off("sync", this._startCallEventHandler); + /** + * Requests a text message verification token for the purposes of adding a + * third party identifier to an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the addition of phone numbers to an + * account, as requestAdd3pidEmailToken. + * + * @param {string} phoneCountry As requestRegisterMsisdnToken + * @param {string} phoneNumber As requestRegisterMsisdnToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + public requestAdd3pidMsisdnToken(phoneCountry: string, phoneNumber: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + return this.requestTokenFromEndpoint( + "/account/3pid/msisdn/requestToken", + { + country: phoneCountry, + phone_number: phoneNumber, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink, + }, + ); } -}; -/** - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype.turnServer = function(callback) { - return this._http.authedRequest(callback, "GET", "/voip/turnServer"); -}; + /** + * Requests an email verification token for the purposes of resetting + * the password on an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the password resetting. Specifically, + * if no account with the given email address exists, it may either + * return M_THREEPID_NOT_FOUND or send an email + * to the address informing them of this (which one is up to the Home Server). + * + * requestEmailToken calls the equivalent API directly on the ID server, + * therefore bypassing the password reset specific logic. + * + * @param {string} email As requestEmailToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @param {module:client.callback} callback Optional. As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + public requestPasswordEmailToken(email: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + return this.requestTokenFromEndpoint( + "/account/password/email/requestToken", + { + email: email, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink, + }, + ); + } -/** - * Get the TURN servers for this home server. - * @return {Array} The servers or an empty list. - */ -MatrixClient.prototype.getTurnServers = function() { - return this._turnServers || []; -}; + /** + * Requests a text message verification token for the purposes of resetting + * the password on an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the password resetting, as requestPasswordEmailToken. + * + * @param {string} phoneCountry As requestRegisterMsisdnToken + * @param {string} phoneNumber As requestRegisterMsisdnToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + public requestPasswordMsisdnToken(phoneCountry: string, phoneNumber: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + return this.requestTokenFromEndpoint( + "/account/password/msisdn/requestToken", + { + country: phoneCountry, + phone_number: phoneNumber, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink, + }, + ); + } -/** - * Get the unix timestamp (in seconds) at which the current - * TURN credentials (from getTurnServers) expire - * @return {number} The expiry timestamp, in seconds, or null if no credentials - */ -MatrixClient.prototype.getTurnServersExpiry = function() { - return this._turnServersExpiry; -}; - -MatrixClient.prototype._checkTurnServers = async function() { - if (!this._supportsVoip) { - return; - } - - let credentialsGood = false; - const remainingTime = this._turnServersExpiry - Date.now(); - if (remainingTime > TURN_CHECK_INTERVAL) { - logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones."); - credentialsGood = true; - } else { - logger.debug("Fetching new TURN credentials"); - try { - const res = await this.turnServer(); - if (res.uris) { - logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs"); - // map the response to a format that can be fed to RTCPeerConnection - const servers = { - urls: res.uris, - username: res.username, - credential: res.password, - }; - this._turnServers = [servers]; - // The TTL is in seconds but we work in ms - this._turnServersExpiry = Date.now() + (res.ttl * 1000); - credentialsGood = true; + /** + * Internal utility function for requesting validation tokens from usage-specific + * requestToken endpoints. + * + * @param {string} endpoint The endpoint to send the request to + * @param {object} params Parameters for the POST request + * @return {Promise} Resolves: As requestEmailToken + */ + private async requestTokenFromEndpoint(endpoint: string, params: any): Promise { + const postParams = Object.assign({}, params); + + // If the HS supports separate add and bind, then requestToken endpoints + // don't need an IS as they are all validated by the HS directly. + if (!await this.doesServerSupportSeparateAddAndBind() && this.idBaseUrl) { + const idServerUrl = url.parse(this.idBaseUrl); + if (!idServerUrl.host) { + throw new Error("Invalid ID server URL: " + this.idBaseUrl); } - } catch (err) { - logger.error("Failed to get TURN URIs", err); - // If we get a 403, there's no point in looping forever. - if (err.httpStatus === 403) { - logger.info("TURN access unavailable for this account: stopping credentials checks"); - if (this._checkTurnServersIntervalID !== null) global.clearInterval(this._checkTurnServersIntervalID); - this._checkTurnServersIntervalID = null; + postParams.id_server = idServerUrl.host; + + if ( + this.identityServer && + this.identityServer.getAccessToken && + await this.doesServerAcceptIdentityAccessToken() + ) { + const identityAccessToken = await this.identityServer.getAccessToken(); + if (identityAccessToken) { + postParams.id_access_token = identityAccessToken; + } } } - // otherwise, if we failed for whatever reason, try again the next time we're called. - } - return credentialsGood; -}; + return this.http.request( + undefined, "POST", endpoint, undefined, + postParams, + ); + } -/** - * Set whether to allow a fallback ICE server should be used for negotiating a - * WebRTC connection if the homeserver doesn't provide any servers. Defaults to - * false. - * - * @param {boolean} allow - */ -MatrixClient.prototype.setFallbackICEServerAllowed = function(allow) { - this._fallbackICEServerAllowed = allow; -}; + /** + * Get the room-kind push rule associated with a room. + * @param {string} scope "global" or device-specific. + * @param {string} roomId the id of the room. + * @return {object} the rule or undefined. + */ + public getRoomPushRule(scope: string, roomId: string): any { // TODO: Types + // There can be only room-kind push rule per room + // and its id is the room id. + if (this.pushRules) { + for (let i = 0; i < this.pushRules[scope].room.length; i++) { + const rule = this.pushRules[scope].room[i]; + if (rule.rule_id === roomId) { + return rule; + } + } + } else { + throw new Error( + "SyncApi.sync() must be done before accessing to push rules.", + ); + } + } -/** - * Get whether to allow a fallback ICE server should be used for negotiating a - * WebRTC connection if the homeserver doesn't provide any servers. Defaults to - * false. - * - * @returns {boolean} - */ -MatrixClient.prototype.isFallbackICEServerAllowed = function() { - return this._fallbackICEServerAllowed; -}; + /** + * Set a room-kind muting push rule in a room. + * The operation also updates MatrixClient.pushRules at the end. + * @param {string} scope "global" or device-specific. + * @param {string} roomId the id of the room. + * @param {string} mute the mute state. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setRoomMutePushRule(scope: string, roomId: string, mute: string): any { // TODO: Types + let deferred; + let hasDontNotifyRule; + + // Get the existing room-kind push rule if any + const roomPushRule = this.getRoomPushRule(scope, roomId); + if (roomPushRule) { + if (0 <= roomPushRule.actions.indexOf("dont_notify")) { + hasDontNotifyRule = true; + } + } -// Synapse-specific APIs -// ===================== + if (!mute) { + // Remove the rule only if it is a muting rule + if (hasDontNotifyRule) { + deferred = this.deletePushRule(scope, "room", roomPushRule.rule_id); + } + } else { + if (!roomPushRule) { + deferred = this.addPushRule(scope, "room", roomId, { + actions: ["dont_notify"], + }); + } else if (!hasDontNotifyRule) { + // Remove the existing one before setting the mute push rule + // This is a workaround to SYN-590 (Push rule update fails) + deferred = utils.defer(); + this.deletePushRule(scope, "room", roomPushRule.rule_id) + .then(() => { + this.addPushRule(scope, "room", roomId, { + actions: ["dont_notify"], + }).then(() => { + deferred.resolve(); + }).catch((err) => { + deferred.reject(err); + }); + }).catch((err) => { + deferred.reject(err); + }); + + deferred = deferred.promise; + } + } -/** - * Determines if the current user is an administrator of the Synapse homeserver. - * Returns false if untrue or the homeserver does not appear to be a Synapse - * homeserver. This function is implementation specific and may change - * as a result. - * @return {boolean} true if the user appears to be a Synapse administrator. - */ -MatrixClient.prototype.isSynapseAdministrator = function() { - const path = utils.encodeUri( - "/_synapse/admin/v1/users/$userId/admin", - { $userId: this.getUserId() }, - ); - return this._http.authedRequest( - undefined, 'GET', path, undefined, undefined, { prefix: '' }, - ).then(r => r['admin']); // pull out the specific boolean we want -}; + if (deferred) { + return new Promise((resolve, reject) => { + // Update this.pushRules when the operation completes + deferred.then(() => { + this.getPushRules().then((result) => { + this.pushRules = result; + resolve(); + }).catch((err) => { + reject(err); + }); + }).catch((err) => { + // Update it even if the previous operation fails. This can help the + // app to recover when push settings has been modifed from another client + this.getPushRules().then((result) => { + this.pushRules = result; + reject(err); + }).catch((err2) => { + reject(err); + }); + }); + }); + } + } -/** - * Performs a whois lookup on a user using Synapse's administrator API. - * This function is implementation specific and may change as a - * result. - * @param {string} userId the User ID to look up. - * @return {object} the whois response - see Synapse docs for information. - */ -MatrixClient.prototype.whoisSynapseUser = function(userId) { - const path = utils.encodeUri( - "/_synapse/admin/v1/whois/$userId", - { $userId: userId }, - ); - return this._http.authedRequest( - undefined, 'GET', path, undefined, undefined, { prefix: '' }, - ); -}; + public searchMessageText(opts: ISearchOpts, callback?: Callback): Promise { // TODO: Types + const roomEvents: any = { + search_term: opts.query, + }; -/** - * Deactivates a user using Synapse's administrator API. This - * function is implementation specific and may change as a result. - * @param {string} userId the User ID to deactivate. - * @return {object} the deactivate response - see Synapse docs for information. - */ -MatrixClient.prototype.deactivateSynapseUser = function(userId) { - const path = utils.encodeUri( - "/_synapse/admin/v1/deactivate/$userId", - { $userId: userId }, - ); - return this._http.authedRequest( - undefined, 'POST', path, undefined, undefined, { prefix: '' }, - ); -}; - -// Higher level APIs -// ================= - -// TODO: stuff to handle: -// local echo -// event dup suppression? - apparently we should still be doing this -// tracking current display name / avatar per-message -// pagination -// re-sending (including persisting pending messages to be sent) -// - Need a nice way to callback the app for arbitrary events like -// displayname changes -// due to ambiguity (or should this be on a chat-specific layer)? -// reconnect after connectivity outages + if ('keys' in opts) { + roomEvents.keys = opts.keys; + } -/** - * High level helper method to begin syncing and poll for new events. To listen for these - * events, add a listener for {@link module:client~MatrixClient#event:"event"} - * via {@link module:client~MatrixClient#on}. Alternatively, listen for specific - * state change events. - * @param {Object=} opts Options to apply when syncing. - * @param {Number=} opts.initialSyncLimit The event limit= to apply - * to initial sync. Default: 8. - * @param {Boolean=} opts.includeArchivedRooms True to put archived=true - * on the /initialSync request. Default: false. - * @param {Boolean=} opts.resolveInvitesToProfiles True to do /profile requests - * on every invite event if the displayname/avatar_url is not known for this user ID. - * Default: false. - * - * @param {String=} opts.pendingEventOrdering Controls where pending messages - * appear in a room's timeline. If "chronological", messages will appear - * in the timeline when the call to sendEvent was made. If - * "detached", pending messages will appear in a separate list, - * accessbile via {@link module:models/room#getPendingEvents}. Default: - * "chronological". - * - * @param {Number=} opts.pollTimeout The number of milliseconds to wait on /sync. - * Default: 30000 (30 seconds). - * - * @param {Filter=} opts.filter The filter to apply to /sync calls. This will override - * the opts.initialSyncLimit, which would normally result in a timeline limit filter. - * - * @param {Boolean=} opts.disablePresence True to perform syncing without automatically - * updating presence. - * @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during - * initial sync but fetch them when needed by calling `loadOutOfBandMembers` - * This will override the filter option at this moment. - * @param {Number=} opts.clientWellKnownPollPeriod The number of seconds between polls - * to /.well-known/matrix/client, undefined to disable. This should be in the order of hours. - * Default: undefined. - */ -MatrixClient.prototype.startClient = async function(opts) { - if (this.clientRunning) { - // client is already running. - return; - } - this.clientRunning = true; - // backwards compat for when 'opts' was 'historyLen'. - if (typeof opts === "number") { - opts = { - initialSyncLimit: opts, + return this.search({ + body: { + search_categories: { + room_events: roomEvents, + }, + }, + }, callback); + } + + /** + * Perform a server-side search for room events. + * + * The returned promise resolves to an object containing the fields: + * + * * {number} count: estimate of the number of results + * * {string} next_batch: token for back-pagination; if undefined, there are + * no more results + * * {Array} highlights: a list of words to highlight from the stemming + * algorithm + * * {Array} results: a list of results + * + * Each entry in the results list is a {module:models/search-result.SearchResult}. + * + * @param {Object} opts + * @param {string} opts.term the term to search for + * @param {Object} opts.filter a JSON filter object to pass in the request + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public searchRoomEvents(opts: IEventSearchOpts): Promise { // TODO: Types + // TODO: support groups + + const body = { + search_categories: { + room_events: { + search_term: opts.term, + filter: opts.filter, + order_by: "recent", + event_context: { + before_limit: 1, + after_limit: 1, + include_profile: true, + }, + }, + }, }; - } - // Create our own user object artificially (instead of waiting for sync) - // so it's always available, even if the user is not in any rooms etc. - const userId = this.getUserId(); - if (userId) { - this.store.storeUser(new User(userId)); - } + const searchResults = { + _query: body, + results: [], + highlights: [], + }; - if (this._crypto) { - this._crypto.uploadDeviceKeys(); - this._crypto.start(); - } + // TODO: @@TR: wtf is this + // prev: + /* + return this.search({ body: body }).then( + this._processRoomEventsSearch.bind(this, searchResults), + ); + */ + return this.search({ body: body }).then(res => this.processRoomEventsSearch(res, searchResults)); + } + + /** + * Take a result from an earlier searchRoomEvents call, and backfill results. + * + * @param {object} searchResults the results object to be updated + * @return {Promise} Resolves: updated result object + * @return {Error} Rejects: with an error response. + */ + public backPaginateRoomEventsSearch(searchResults: any): Promise { // TODO: Types + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + + if (!searchResults.next_batch) { + return Promise.reject(new Error("Cannot backpaginate event search any further")); + } - // periodically poll for turn servers if we support voip - if (this._supportsVoip) { - this._checkTurnServersIntervalID = setInterval(() => { - this._checkTurnServers(); - }, TURN_CHECK_INTERVAL); - this._checkTurnServers(); - } + if (searchResults.pendingRequest) { + // already a request in progress - return the existing promise + return searchResults.pendingRequest; + } - if (this._syncApi) { - // This shouldn't happen since we thought the client was not running - logger.error("Still have sync object whilst not running: stopping old one"); - this._syncApi.stop(); - } + const searchOpts = { + body: searchResults._query, + next_batch: searchResults.next_batch, + }; - // shallow-copy the opts dict before modifying and storing it - opts = Object.assign({}, opts); + // TODO: @@TR: wtf + const promise = this.search(searchOpts).then( + this.processRoomEventsSearch.bind(this, searchResults), + ).finally(function() { + searchResults.pendingRequest = null; + }); + searchResults.pendingRequest = promise; - opts.crypto = this._crypto; - opts.canResetEntireTimeline = (roomId) => { - if (!this._canResetTimelineCallback) { - return false; - } - return this._canResetTimelineCallback(roomId); - }; - this._clientOpts = opts; - this._syncApi = new SyncApi(this, opts); - this._syncApi.sync(); - - if (opts.clientWellKnownPollPeriod !== undefined) { - this._clientWellKnownIntervalID = - setInterval(() => { - this._fetchClientWellKnown(); - }, 1000 * opts.clientWellKnownPollPeriod); - this._fetchClientWellKnown(); - } -}; - -MatrixClient.prototype._fetchClientWellKnown = async function() { - // `getRawClientConfig` does not throw or reject on network errors, instead - // it absorbs errors and returns `{}`. - this._clientWellKnownPromise = AutoDiscovery.getRawClientConfig( - this.getDomain(), - ); - this._clientWellKnown = await this._clientWellKnownPromise; - this.emit("WellKnown.client", this._clientWellKnown); -}; - -MatrixClient.prototype.getClientWellKnown = function() { - return this._clientWellKnown; -}; - -MatrixClient.prototype.waitForClientWellKnown = function() { - return this._clientWellKnownPromise; -}; + return promise; + } -/** - * store client options with boolean/string/numeric values - * to know in the next session what flags the sync data was - * created with (e.g. lazy loading) - * @param {object} opts the complete set of client options - * @return {Promise} for store operation */ -MatrixClient.prototype._storeClientOptions = function() { - const primTypes = ["boolean", "string", "number"]; - const serializableOpts = Object.entries(this._clientOpts) - .filter(([key, value]) => { - return primTypes.includes(typeof value); - }) - .reduce((obj, [key, value]) => { - obj[key] = value; - return obj; - }, {}); - return this.store.storeClientOptions(serializableOpts); -}; + /** + * helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the + * response from the API call and updates the searchResults + * + * @param {Object} searchResults + * @param {Object} response + * @return {Object} searchResults + * @private + */ + private processRoomEventsSearch(searchResults: any, response: any): any { + const room_events = response.search_categories.room_events; + + searchResults.count = room_events.count; + searchResults.next_batch = room_events.next_batch; + + // combine the highlight list with our existing list; build an object + // to avoid O(N^2) fail + const highlights = {}; + room_events.highlights.forEach((hl) => { + highlights[hl] = 1; + }); + searchResults.highlights.forEach((hl) => { + highlights[hl] = 1; + }); -/** - * Gets a set of room IDs in common with another user - * @param {string} userId The userId to check. - * @return {Promise} Resolves to a set of rooms - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixClient.prototype._unstable_getSharedRooms = async function(userId) { - if (!(await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"))) { - throw Error('Server does not support shared_rooms API'); - } - const path = utils.encodeUri("/uk.half-shot.msc2666/user/shared_rooms/$userId", { - $userId: userId, - }); - const res = await this._http.authedRequest( - undefined, "GET", path, undefined, undefined, - { prefix: PREFIX_UNSTABLE }, - ); - return res.joined; -}; + // turn it back into a list. + searchResults.highlights = Object.keys(highlights); -/** - * High level helper method to stop the client from polling and allow a - * clean shutdown. - */ -MatrixClient.prototype.stopClient = function() { - logger.log('stopping MatrixClient'); + // append the new results to our existing results + const resultsLength = room_events.results ? room_events.results.length : 0; + for (let i = 0; i < resultsLength; i++) { + const sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper()); + searchResults.results.push(sr); + } + return searchResults; + } + + /** + * Populate the store with rooms the user has left. + * @return {Promise} Resolves: TODO - Resolved when the rooms have + * been added to the data store. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public syncLeftRooms(): Promise { + // Guard against multiple calls whilst ongoing and multiple calls post success + if (this.syncedLeftRooms) { + return Promise.resolve([]); // don't call syncRooms again if it succeeded. + } + if (this.syncLeftRoomsPromise) { + return this.syncLeftRoomsPromise; // return the ongoing request + } + const syncApi = new SyncApi(this, this.clientOpts); + this.syncLeftRoomsPromise = syncApi.syncLeftRooms(); + + // cleanup locks + this.syncLeftRoomsPromise.then((res) => { + logger.log("Marking success of sync left room request"); + this.syncedLeftRooms = true; // flip the bit on success + }).finally(() => { + this.syncLeftRoomsPromise = null; // cleanup ongoing request state + }); - this.clientRunning = false; - // TODO: f.e. Room => self.store.storeRoom(room) ? - if (this._syncApi) { - this._syncApi.stop(); - this._syncApi = null; - } - if (this._crypto) { - this._crypto.stop(); - } - if (this._peekSync) { - this._peekSync.stopPeeking(); - } - if (this._callEventHandler) { - this._callEventHandler.stop(); - this._callEventHandler = null; + return this.syncLeftRoomsPromise; } - global.clearInterval(this._checkTurnServersIntervalID); - if (this._clientWellKnownIntervalID !== undefined) { - global.clearInterval(this._clientWellKnownIntervalID); + /** + * Create a new filter. + * @param {Object} content The HTTP body for the request + * @return {Filter} Resolves to a Filter object. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public createFilter(content: any): Promise { // TODO: Types + const path = utils.encodeUri("/user/$userId/filter", { + $userId: this.credentials.userId, + }); + return this.http.authedRequest(undefined, "POST", path, undefined, content).then((response) => { + // persist the filter + const filter = Filter.fromJson( + this.credentials.userId, response.filter_id, content, + ); + this.store.storeFilter(filter); + return filter; + }); } -}; -/** - * Get the API versions supported by the server, along with any - * unstable APIs it supports - * @return {Promise} The server /versions response - */ -MatrixClient.prototype.getVersions = function() { - if (this._serverVersionsPromise) { - return this._serverVersionsPromise; - } - - this._serverVersionsPromise = this._http.request( - undefined, // callback - "GET", "/_matrix/client/versions", - undefined, // queryParams - undefined, // data - { - prefix: '', - }, - ).catch((e) => { - // Need to unset this if it fails, otherwise we'll never retry - this._serverVersionsPromise = null; - // but rethrow the exception to anything that was waiting - throw e; - }); - - return this._serverVersionsPromise; -}; + /** + * Retrieve a filter. + * @param {string} userId The user ID of the filter owner + * @param {string} filterId The filter ID to retrieve + * @param {boolean} allowCached True to allow cached filters to be returned. + * Default: True. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getFilter(userId: string, filterId: string, allowCached: boolean): Promise { + if (allowCached) { + const filter = this.store.getFilter(userId, filterId); + if (filter) { + return Promise.resolve(filter); + } + } -/** - * Check if a particular spec version is supported by the server. - * @param {string} version The spec version (such as "r0.5.0") to check for. - * @return {Promise} Whether it is supported - */ -MatrixClient.prototype.isVersionSupported = async function(version) { - const { versions } = await this.getVersions(); - return versions && versions.includes(version); -}; + const path = utils.encodeUri("/user/$userId/filter/$filterId", { + $userId: userId, + $filterId: filterId, + }); -/** - * Query the server to see if it support members lazy loading - * @return {Promise} true if server supports lazy loading - */ -MatrixClient.prototype.doesServerSupportLazyLoading = async function() { - const response = await this.getVersions(); - if (!response) return false; + return this.http.authedRequest( + undefined, "GET", path, undefined, undefined, + ).then((response) => { + // persist the filter + const filter = Filter.fromJson( + userId, filterId, response, + ); + this.store.storeFilter(filter); + return filter; + }); + } - const versions = response["versions"]; - const unstableFeatures = response["unstable_features"]; + /** + * @param {string} filterName + * @param {Filter} filter + * @return {Promise} Filter ID + */ + public async getOrCreateFilter(filterName: string, filter: Filter): Promise { + const filterId = this.store.getFilterIdByName(filterName); + let existingId = undefined; - return (versions && versions.includes("r0.5.0")) - || (unstableFeatures && unstableFeatures["m.lazy_load_members"]); -}; + if (filterId) { + // check that the existing filter matches our expectations + try { + const existingFilter = + await this.getFilter(this.credentials.userId, filterId, true); + if (existingFilter) { + const oldDef = existingFilter.getDefinition(); + const newDef = filter.getDefinition(); + + if (utils.deepCompare(oldDef, newDef)) { + // super, just use that. + // debuglog("Using existing filter ID %s: %s", filterId, + // JSON.stringify(oldDef)); + existingId = filterId; + } + } + } catch (error) { + // Synapse currently returns the following when the filter cannot be found: + // { + // errcode: "M_UNKNOWN", + // name: "M_UNKNOWN", + // message: "No row found", + // } + if (error.errcode !== "M_UNKNOWN" && error.errcode !== "M_NOT_FOUND") { + throw error; + } + } + // if the filter doesn't exist anymore on the server, remove from store + if (!existingId) { + this.store.setFilterIdByName(filterName, undefined); + } + } -/** - * Query the server to see if the `id_server` parameter is required - * when registering with an 3pid, adding a 3pid or resetting password. - * @return {Promise} true if id_server parameter is required - */ -MatrixClient.prototype.doesServerRequireIdServerParam = async function() { - const response = await this.getVersions(); - if (!response) return true; + if (existingId) { + return existingId; + } - const versions = response["versions"]; + // create a new filter + const createdFilter = await this.createFilter(filter.getDefinition()); + + // debuglog("Created new filter ID %s: %s", createdFilter.filterId, + // JSON.stringify(createdFilter.getDefinition())); + this.store.setFilterIdByName(filterName, createdFilter.filterId); + return createdFilter.filterId; + } + + /** + * Gets a bearer token from the Home Server that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * @return {Promise} Resolves: Token object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getOpenIdToken(): Promise { // TODO: Types + const path = utils.encodeUri("/user/$userId/openid/request_token", { + $userId: this.credentials.userId, + }); - // Supporting r0.6.0 is the same as having the flag set to false - if (versions && versions.includes("r0.6.0")) { - return false; + return this.http.authedRequest( + undefined, "POST", path, undefined, {}, + ); } - const unstableFeatures = response["unstable_features"]; - if (!unstableFeatures) return true; - if (unstableFeatures["m.require_identity_server"] === undefined) { - return true; - } else { - return unstableFeatures["m.require_identity_server"]; + private startCallEventHandler() { + if (this.isInitialSyncComplete()) { + this.callEventHandler.start(); + this.off("sync", this.startCallEventHandler); + } } -}; -/** - * Query the server to see if the `id_access_token` parameter can be safely - * passed to the homeserver. Some homeservers may trigger errors if they are not - * prepared for the new parameter. - * @return {Promise} true if id_access_token can be sent - */ -MatrixClient.prototype.doesServerAcceptIdentityAccessToken = async function() { - const response = await this.getVersions(); - if (!response) return false; + /** + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public turnServer(callback?: Callback): Promise { // TODO: Types + return this.http.authedRequest(callback, "GET", "/voip/turnServer"); + } - const versions = response["versions"]; - const unstableFeatures = response["unstable_features"]; - return (versions && versions.includes("r0.6.0")) - || (unstableFeatures && unstableFeatures["m.id_access_token"]); -}; + /** + * Get the TURN servers for this home server. + * @return {Array} The servers or an empty list. + */ + public getTurnServers(): any[] { // TODO: Types + return this.turnServers || []; + } -/** - * Query the server to see if it supports separate 3PID add and bind functions. - * This affects the sequence of API calls clients should use for these operations, - * so it's helpful to be able to check for support. - * @return {Promise} true if separate functions are supported - */ -MatrixClient.prototype.doesServerSupportSeparateAddAndBind = async function() { - const response = await this.getVersions(); - if (!response) return false; + /** + * Get the unix timestamp (in seconds) at which the current + * TURN credentials (from getTurnServers) expire + * @return {number} The expiry timestamp, in seconds, or null if no credentials + */ + public getTurnServersExpiry(): number | null { + return this.turnServersExpiry; + } - const versions = response["versions"]; - const unstableFeatures = response["unstable_features"]; + private async checkTurnServers(): Promise { + if (!this.canSupportVoip) { + return; + } - return (versions && versions.includes("r0.6.0")) - || (unstableFeatures && unstableFeatures["m.separate_add_and_bind"]); -}; + let credentialsGood = false; + const remainingTime = this.turnServersExpiry - Date.now(); + if (remainingTime > TURN_CHECK_INTERVAL) { + logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones."); + credentialsGood = true; + } else { + logger.debug("Fetching new TURN credentials"); + try { + const res = await this.turnServer(); + if (res.uris) { + logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs"); + // map the response to a format that can be fed to RTCPeerConnection + const servers = { + urls: res.uris, + username: res.username, + credential: res.password, + }; + this.turnServers = [servers]; + // The TTL is in seconds but we work in ms + this.turnServersExpiry = Date.now() + (res.ttl * 1000); + credentialsGood = true; + } + } catch (err) { + logger.error("Failed to get TURN URIs", err); + // If we get a 403, there's no point in looping forever. + if (err.httpStatus === 403) { + logger.info("TURN access unavailable for this account: stopping credentials checks"); + if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID); + this.checkTurnServersIntervalID = null; + } + } + // otherwise, if we failed for whatever reason, try again the next time we're called. + } -/** - * Query the server to see if it lists support for an unstable feature - * in the /versions response - * @param {string} feature the feature name - * @return {Promise} true if the feature is supported - */ -MatrixClient.prototype.doesServerSupportUnstableFeature = async function(feature) { - const response = await this.getVersions(); - if (!response) return false; - const unstableFeatures = response["unstable_features"]; - return unstableFeatures && !!unstableFeatures[feature]; -}; + return credentialsGood; + } + + /** + * Set whether to allow a fallback ICE server should be used for negotiating a + * WebRTC connection if the homeserver doesn't provide any servers. Defaults to + * false. + * + * @param {boolean} allow + */ + public setFallbackICEServerAllowed(allow: boolean) { + this.fallbackICEServerAllowed = allow; + } + + /** + * Get whether to allow a fallback ICE server should be used for negotiating a + * WebRTC connection if the homeserver doesn't provide any servers. Defaults to + * false. + * + * @returns {boolean} + */ + public isFallbackICEServerAllowed(): boolean { + return this.fallbackICEServerAllowed; + } + + /** + * Determines if the current user is an administrator of the Synapse homeserver. + * Returns false if untrue or the homeserver does not appear to be a Synapse + * homeserver. This function is implementation specific and may change + * as a result. + * @return {boolean} true if the user appears to be a Synapse administrator. + */ + public isSynapseAdministrator(): Promise { + const path = utils.encodeUri( + "/_synapse/admin/v1/users/$userId/admin", + { $userId: this.getUserId() }, + ); + return this.http.authedRequest( + undefined, 'GET', path, undefined, undefined, { prefix: '' }, + ).then(r => r['admin']); // pull out the specific boolean we want + } + + /** + * Performs a whois lookup on a user using Synapse's administrator API. + * This function is implementation specific and may change as a + * result. + * @param {string} userId the User ID to look up. + * @return {object} the whois response - see Synapse docs for information. + */ + public whoisSynapseUser(userId: string): Promise { + const path = utils.encodeUri( + "/_synapse/admin/v1/whois/$userId", + { $userId: userId }, + ); + return this.http.authedRequest( + undefined, 'GET', path, undefined, undefined, { prefix: '' }, + ); + } -/** - * Query the server to see if it is forcing encryption to be enabled for - * a given room preset, based on the /versions response. - * @param {string} presetName The name of the preset to check. - * @returns {Promise} true if the server is forcing encryption - * for the preset. - */ -MatrixClient.prototype.doesServerForceEncryptionForPreset = async function(presetName) { - const response = await this.getVersions(); - if (!response) return false; - const unstableFeatures = response["unstable_features"]; - return unstableFeatures && !!unstableFeatures[`io.element.e2ee_forced.${presetName}`]; -}; + /** + * Deactivates a user using Synapse's administrator API. This + * function is implementation specific and may change as a result. + * @param {string} userId the User ID to deactivate. + * @return {object} the deactivate response - see Synapse docs for information. + */ + public deactivateSynapseUser(userId: string): Promise { + const path = utils.encodeUri( + "/_synapse/admin/v1/deactivate/$userId", + { $userId: userId }, + ); + return this.http.authedRequest( + undefined, 'POST', path, undefined, undefined, { prefix: '' }, + ); + } -/** - * Get if lazy loading members is being used. - * @return {boolean} Whether or not members are lazy loaded by this client - */ -MatrixClient.prototype.hasLazyLoadMembersEnabled = function() { - return !!this._clientOpts.lazyLoadMembers; -}; + private async fetchClientWellKnown() { + // `getRawClientConfig` does not throw or reject on network errors, instead + // it absorbs errors and returns `{}`. + this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig( + this.getDomain(), + ); + this.clientWellKnown = await this.clientWellKnownPromise; + this.emit("WellKnown.client", this.clientWellKnown); + } + + public getClientWellKnown(): any { + return this.clientWellKnown; + } + + public waitForClientWellKnown(): Promise { + return this.clientWellKnownPromise; + } + + /** + * store client options with boolean/string/numeric values + * to know in the next session what flags the sync data was + * created with (e.g. lazy loading) + * @param {object} opts the complete set of client options + * @return {Promise} for store operation + */ + private storeClientOptions() { + const primTypes = ["boolean", "string", "number"]; + const serializableOpts = Object.entries(this.clientOpts) + .filter(([key, value]) => { + return primTypes.includes(typeof value); + }) + .reduce((obj, [key, value]) => { + obj[key] = value; + return obj; + }, {}); + return this.store.storeClientOptions(serializableOpts); + } + + /** + * Gets a set of room IDs in common with another user + * @param {string} userId The userId to check. + * @return {Promise} Resolves to a set of rooms + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async _unstable_getSharedRooms(userId: string): Promise { + if (!(await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"))) { + throw Error('Server does not support shared_rooms API'); + } + const path = utils.encodeUri("/uk.half-shot.msc2666/user/shared_rooms/$userId", { + $userId: userId, + }); + const res = await this.http.authedRequest( + undefined, "GET", path, undefined, undefined, + { prefix: PREFIX_UNSTABLE }, + ); + return res.joined; + } -/** - * Set a function which is called when /sync returns a 'limited' response. - * It is called with a room ID and returns a boolean. It should return 'true' if the SDK - * can SAFELY remove events from this room. It may not be safe to remove events if there - * are other references to the timelines for this room, e.g because the client is - * actively viewing events in this room. - * Default: returns false. - * @param {Function} cb The callback which will be invoked. - */ -MatrixClient.prototype.setCanResetTimelineCallback = function(cb) { - this._canResetTimelineCallback = cb; -}; + /** + * Get the API versions supported by the server, along with any + * unstable APIs it supports + * @return {Promise} The server /versions response + */ + public getVersions(): Promise { // TODO: Types + if (this.serverVersionsPromise) { + return this.serverVersionsPromise; + } -/** - * Get the callback set via `setCanResetTimelineCallback`. - * @return {?Function} The callback or null - */ -MatrixClient.prototype.getCanResetTimelineCallback = function() { - return this._canResetTimelineCallback; -}; + this.serverVersionsPromise = this.http.request( + undefined, // callback + "GET", "/_matrix/client/versions", + undefined, // queryParams + undefined, // data + { + prefix: '', + }, + ).catch((e) => { + // Need to unset this if it fails, otherwise we'll never retry + this.serverVersionsPromise = null; + // but rethrow the exception to anything that was waiting + throw e; + }); -/** - * Returns relations for a given event. Handles encryption transparently, - * with the caveat that the amount of events returned might be 0, even though you get a nextBatch. - * When the returned promise resolves, all messages should have finished trying to decrypt. - * @param {string} roomId the room of the event - * @param {string} eventId the id of the event - * @param {string} relationType the rel_type of the relations requested - * @param {string} eventType the event type of the relations requested - * @param {Object} opts options with optional values for the request. - * @param {Object} opts.from the pagination token returned from a previous request as `nextBatch` to return following relations. - * @return {Object} an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available. - */ -MatrixClient.prototype.relations = -async function(roomId, eventId, relationType, eventType, opts = {}) { - const fetchedEventType = _getEncryptedIfNeededEventType(this, roomId, eventType); - const result = await this.fetchRelations( - roomId, - eventId, - relationType, - fetchedEventType, - opts); - const mapper = this.getEventMapper(); - let originalEvent; - if (result.original_event) { - originalEvent = mapper(result.original_event); - } - let events = result.chunk.map(mapper); - if (fetchedEventType === "m.room.encrypted") { - const allEvents = originalEvent ? events.concat(originalEvent) : events; - await Promise.all(allEvents.map(e => { - return new Promise(resolve => e.once("Event.decrypted", resolve)); - })); - events = events.filter(e => e.getType() === eventType); + return this.serverVersionsPromise; } - if (originalEvent && relationType === "m.replace") { - events = events.filter(e => e.getSender() === originalEvent.getSender()); - } - return { - originalEvent, - events, - nextBatch: result.next_batch, - }; -}; -function _reject(callback, reject, err) { - if (callback) { - callback(err); + /** + * Check if a particular spec version is supported by the server. + * @param {string} version The spec version (such as "r0.5.0") to check for. + * @return {Promise} Whether it is supported + */ + public async isVersionSupported(version: string): Promise { + const { versions } = await this.getVersions(); + return versions && versions.includes(version); } - reject(err); -} -function _resolve(callback, resolve, res) { - if (callback) { - callback(null, res); - } - resolve(res); -} + /** + * Query the server to see if it support members lazy loading + * @return {Promise} true if server supports lazy loading + */ + public async doesServerSupportLazyLoading(): Promise { + const response = await this.getVersions(); + if (!response) return false; -function _PojoToMatrixEventMapper(client, options = {}) { - const preventReEmit = Boolean(options.preventReEmit); - const decrypt = options.decrypt !== false; - function mapper(plainOldJsObject) { - const event = new MatrixEvent(plainOldJsObject); - if (event.isEncrypted()) { - if (!preventReEmit) { - client.reEmitter.reEmit(event, [ - "Event.decrypted", - ]); - } - if (decrypt) { - client.decryptEventIfNeeded(event); - } - } - if (!preventReEmit) { - client.reEmitter.reEmit(event, ["Event.replaced"]); - } - return event; - } - return mapper; -} + const versions = response["versions"]; + const unstableFeatures = response["unstable_features"]; -/** - * @param {object} [options] - * @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client - * @param {bool} options.decrypt decrypt event proactively - * @return {Function} - */ -MatrixClient.prototype.getEventMapper = function(options = undefined) { - return _PojoToMatrixEventMapper(this, options); -}; + return (versions && versions.includes("r0.5.0")) + || (unstableFeatures && unstableFeatures["m.lazy_load_members"]); + } -/** - * The app may wish to see if we have a key cached without - * triggering a user interaction. - * @return {object} - */ -MatrixClient.prototype.getCrossSigningCacheCallbacks = function() { - return this._crypto && this._crypto._crossSigningInfo.getCacheCallbacks(); -}; + /** + * Query the server to see if the `id_server` parameter is required + * when registering with an 3pid, adding a 3pid or resetting password. + * @return {Promise} true if id_server parameter is required + */ + public async doesServerRequireIdServerParam(): Promise { + const response = await this.getVersions(); + if (!response) return true; -// Identity Server Operations -// ========================== + const versions = response["versions"]; -/** - * Generates a random string suitable for use as a client secret. This - * method is experimental and may change. - * @return {string} A new client secret - */ -MatrixClient.prototype.generateClientSecret = function() { - return randomString(32); -}; + // Supporting r0.6.0 is the same as having the flag set to false + if (versions && versions.includes("r0.6.0")) { + return false; + } -/** - * Attempts to decrypt an event - * @param {MatrixEvent} event The event to decrypt - * @returns {Promise} A decryption promise - * @param {object} options - * @param {bool} options.isRetry True if this is a retry (enables more logging) - * @param {bool} options.emit Emits "event.decrypted" if set to true - */ -MatrixClient.prototype.decryptEventIfNeeded = function(event, options) { - if (event.shouldAttemptDecryption()) { - event.attemptDecryption(this._crypto, options); + const unstableFeatures = response["unstable_features"]; + if (!unstableFeatures) return true; + if (unstableFeatures["m.require_identity_server"] === undefined) { + return true; + } else { + return unstableFeatures["m.require_identity_server"]; + } } - if (event.isBeingDecrypted()) { - return event._decryptionPromise; - } else { - return Promise.resolve(); + /** + * Query the server to see if the `id_access_token` parameter can be safely + * passed to the homeserver. Some homeservers may trigger errors if they are not + * prepared for the new parameter. + * @return {Promise} true if id_access_token can be sent + */ + public async doesServerAcceptIdentityAccessToken(): Promise { + const response = await this.getVersions(); + if (!response) return false; + + const versions = response["versions"]; + const unstableFeatures = response["unstable_features"]; + return (versions && versions.includes("r0.6.0")) + || (unstableFeatures && unstableFeatures["m.id_access_token"]); + } + + /** + * Query the server to see if it supports separate 3PID add and bind functions. + * This affects the sequence of API calls clients should use for these operations, + * so it's helpful to be able to check for support. + * @return {Promise} true if separate functions are supported + */ + public async doesServerSupportSeparateAddAndBind(): Promise { + const response = await this.getVersions(); + if (!response) return false; + + const versions = response["versions"]; + const unstableFeatures = response["unstable_features"]; + + return (versions && versions.includes("r0.6.0")) + || (unstableFeatures && unstableFeatures["m.separate_add_and_bind"]); + } + + /** + * Query the server to see if it lists support for an unstable feature + * in the /versions response + * @param {string} feature the feature name + * @return {Promise} true if the feature is supported + */ + public async doesServerSupportUnstableFeature(feature: string): Promise { + const response = await this.getVersions(); + if (!response) return false; + const unstableFeatures = response["unstable_features"]; + return unstableFeatures && !!unstableFeatures[feature]; + } + + /** + * Query the server to see if it is forcing encryption to be enabled for + * a given room preset, based on the /versions response. + * @param {string} presetName The name of the preset to check. + * @returns {Promise} true if the server is forcing encryption + * for the preset. + */ + public async doesServerForceEncryptionForPreset(presetName: string): Promise { + const response = await this.getVersions(); + if (!response) return false; + const unstableFeatures = response["unstable_features"]; + return unstableFeatures && !!unstableFeatures[`io.element.e2ee_forced.${presetName}`]; + } + + /** + * Get if lazy loading members is being used. + * @return {boolean} Whether or not members are lazy loaded by this client + */ + public hasLazyLoadMembersEnabled(): boolean { + return !!this.clientOpts.lazyLoadMembers; + } + + /** + * Set a function which is called when /sync returns a 'limited' response. + * It is called with a room ID and returns a boolean. It should return 'true' if the SDK + * can SAFELY remove events from this room. It may not be safe to remove events if there + * are other references to the timelines for this room, e.g because the client is + * actively viewing events in this room. + * Default: returns false. + * @param {Function} cb The callback which will be invoked. + */ + public setCanResetTimelineCallback(cb: Callback) { + this.canResetTimelineCallback = cb; + } + + /** + * Get the callback set via `setCanResetTimelineCallback`. + * @return {?Function} The callback or null + */ + public getCanResetTimelineCallback(): Callback { + return this.canResetTimelineCallback; + } + + /** + * Returns relations for a given event. Handles encryption transparently, + * with the caveat that the amount of events returned might be 0, even though you get a nextBatch. + * When the returned promise resolves, all messages should have finished trying to decrypt. + * @param {string} roomId the room of the event + * @param {string} eventId the id of the event + * @param {string} relationType the rel_type of the relations requested + * @param {string} eventType the event type of the relations requested + * @param {Object} opts options with optional values for the request. + * @param {Object} opts.from the pagination token returned from a previous request as `nextBatch` to return following relations. + * @return {Object} an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available. + */ + public async relations(roomId: string, eventId: string, relationType: string, eventType: string, opts: {from: string}): Promise<{originalEvent: MatrixEvent, events: MatrixEvent[], nextBatch?: string}> { + const fetchedEventType = this.getEncryptedIfNeededEventType(roomId, eventType); + const result = await this.fetchRelations( + roomId, + eventId, + relationType, + fetchedEventType, + opts); + const mapper = this.getEventMapper(); + let originalEvent; + if (result.original_event) { + originalEvent = mapper(result.original_event); + } + let events = result.chunk.map(mapper); + if (fetchedEventType === "m.room.encrypted") { + const allEvents = originalEvent ? events.concat(originalEvent) : events; + await Promise.all(allEvents.map(e => { + return new Promise(resolve => e.once("Event.decrypted", resolve)); + })); + events = events.filter(e => e.getType() === eventType); + } + if (originalEvent && relationType === "m.replace") { + events = events.filter(e => e.getSender() === originalEvent.getSender()); + } + return { + originalEvent, + events, + nextBatch: result.next_batch, + }; } -}; -// MatrixClient Event JSDocs + /** + * The app may wish to see if we have a key cached without + * triggering a user interaction. + * @return {object} + */ + public getCrossSigningCacheCallbacks(): any { // TODO: Types + // XXX: Private member access + return this.crypto?._crossSigningInfo.getCacheCallbacks(); + } + + /** + * Generates a random string suitable for use as a client secret. This + * method is experimental and may change. + * @return {string} A new client secret + */ + public generateClientSecret(): string { + return randomString(32); + } + + /** + * Attempts to decrypt an event + * @param {MatrixEvent} event The event to decrypt + * @returns {Promise} A decryption promise + * @param {object} options + * @param {bool} options.isRetry True if this is a retry (enables more logging) + * @param {bool} options.emit Emits "event.decrypted" if set to true + */ + public decryptEventIfNeeded(event: MatrixEvent, options: {emit: boolean, isRetry: boolean}): Promise { + if (event.shouldAttemptDecryption()) { + event.attemptDecryption(this.crypto, options); + } + + if (event.isBeingDecrypted()) { + return event._decryptionPromise; + } else { + return Promise.resolve(); + } + } +} /** * Fires whenever the SDK receives a new event. @@ -5778,7 +5680,7 @@ MatrixClient.prototype.decryptEventIfNeeded = function(event, options) { * }); */ - /** +/** * Fires whenever the sdk learns about a new group. This event * is experimental and may change. * @event module:client~MatrixClient#"Group" @@ -5789,7 +5691,7 @@ MatrixClient.prototype.decryptEventIfNeeded = function(event, options) { * }); */ - /** +/** * Fires whenever a new Room is added. This will fire when you are invited to a * room, as well as when you join a room. This event is experimental and * may change. @@ -5801,7 +5703,7 @@ MatrixClient.prototype.decryptEventIfNeeded = function(event, options) { * }); */ - /** +/** * Fires whenever a Room is removed. This will fire when you forget a room. * This event is experimental and may change. * @event module:client~MatrixClient#"deleteRoom" @@ -5971,87 +5873,3 @@ MatrixClient.prototype.decryptEventIfNeeded = function(event, options) { * @event module:client~MatrixClient#"WellKnown.client" * @param {object} data The JSON object returned by the server */ - -// EventEmitter JSDocs - -/** - * The {@link https://nodejs.org/api/events.html|EventEmitter} class. - * @external EventEmitter - * @see {@link https://nodejs.org/api/events.html} - */ - -/** - * Adds a listener to the end of the listeners array for the specified event. - * No checks are made to see if the listener has already been added. Multiple - * calls passing the same combination of event and listener will result in the - * listener being added multiple times. - * @function external:EventEmitter#on - * @param {string} event The event to listen for. - * @param {Function} listener The function to invoke. - * @return {EventEmitter} for call chaining. - */ - -/** - * Alias for {@link external:EventEmitter#on}. - * @function external:EventEmitter#addListener - * @param {string} event The event to listen for. - * @param {Function} listener The function to invoke. - * @return {EventEmitter} for call chaining. - */ - -/** - * Adds a one time listener for the event. This listener is invoked only - * the next time the event is fired, after which it is removed. - * @function external:EventEmitter#once - * @param {string} event The event to listen for. - * @param {Function} listener The function to invoke. - * @return {EventEmitter} for call chaining. - */ - -/** - * Remove a listener from the listener array for the specified event. - * Caution: changes array indices in the listener array behind the - * listener. - * @function external:EventEmitter#removeListener - * @param {string} event The event to listen for. - * @param {Function} listener The function to invoke. - * @return {EventEmitter} for call chaining. - */ - -/** - * Removes all listeners, or those of the specified event. It's not a good idea - * to remove listeners that were added elsewhere in the code, especially when - * it's on an emitter that you didn't create (e.g. sockets or file streams). - * @function external:EventEmitter#removeAllListeners - * @param {string} event Optional. The event to remove listeners for. - * @return {EventEmitter} for call chaining. - */ - -/** - * Execute each of the listeners in order with the supplied arguments. - * @function external:EventEmitter#emit - * @param {string} event The event to emit. - * @param {Function} listener The function to invoke. - * @return {boolean} true if event had listeners, false otherwise. - */ - -/** - * By default EventEmitters will print a warning if more than 10 listeners are - * added for a particular event. This is a useful default which helps finding - * memory leaks. Obviously not all Emitters should be limited to 10. This - * function allows that to be increased. Set to zero for unlimited. - * @function external:EventEmitter#setMaxListeners - * @param {Number} n The max number of listeners. - * @return {EventEmitter} for call chaining. - */ - -// MatrixClient Callback JSDocs - -/** - * The standard MatrixClient callback interface. Functions which accept this - * will specify 2 return arguments. These arguments map to the 2 parameters - * specified in this callback. - * @callback module:client.callback - * @param {Object} err The error value, the "rejected" value or null. - * @param {Object} data The data returned, the "resolved" value. - */ diff --git a/src/@types/partials.ts b/src/@types/partials.ts new file mode 100644 index 00000000000..d2b286e8303 --- /dev/null +++ b/src/@types/partials.ts @@ -0,0 +1,28 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface IImageInfo { + size?: number; + mimetype?: string; + thumbnail_info?: { + w?: number; + h?: number; + size?: number; + mimetype?: string; + }; + w?: number; + h?: number; +} diff --git a/src/@types/requests.ts b/src/@types/requests.ts new file mode 100644 index 00000000000..5cd5ee744eb --- /dev/null +++ b/src/@types/requests.ts @@ -0,0 +1,67 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface IJoinRoomOpts { + /** + * True to do a room initial sync on the resulting + * room. If false, the returned Room object will have no current state. + * Default: true. + */ + syncRoom?: boolean; + + /** + * If the caller has a keypair 3pid invite, the signing URL is passed in this parameter. + */ + inviteSignUrl?: string; + + /** + * The server names to try and join through in addition to those that are automatically chosen. + */ + viaServers?: string[]; +} + +export interface IRedactOpts { + reason?: string; +} + +export interface ISendEventResponse { + event_id: string; +} + +export interface IPresenceOpts { + presence: "online" | "offline" | "unavailable"; + status_msg?: string; +} + +export interface IPaginateOpts { + backwards?: boolean; + limit?: number; +} + +export interface IGuestAccessOpts { + allowJoin: boolean; + allowRead: boolean; +} + +export interface ISearchOpts { + keys?: string[]; + query: string; +} + +export interface IEventSearchOpts { + filter: any; // TODO: Types + term: string; +} diff --git a/src/@types/signed.ts b/src/@types/signed.ts new file mode 100644 index 00000000000..4c39825cc17 --- /dev/null +++ b/src/@types/signed.ts @@ -0,0 +1,21 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface ISignatures { + [entity: string]: { + [keyId: string]: string; + }; +} diff --git a/src/crypto/api.ts b/src/crypto/api.ts new file mode 100644 index 00000000000..835136019bd --- /dev/null +++ b/src/crypto/api.ts @@ -0,0 +1,131 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { DeviceInfo } from "./deviceinfo"; +import { IKeyBackupVersion } from "./keybackup"; +import { ISecretStorageKeyInfo } from "../matrix"; + +// TODO: Merge this with crypto.js once converted + +export enum CrossSigningKey { + Master = "master", + SelfSigning = "self_signing", + UserSigning = "user_signing", +} + +export interface IEncryptedEventInfo { + /** + * whether the event is encrypted (if not encrypted, some of the other properties may not be set) + */ + encrypted: boolean; + + /** + * the sender's key + */ + senderKey: string; + + /** + * the algorithm used to encrypt the event + */ + algorithm: string; + + /** + * whether we can be sure that the owner of the senderKey sent the event + */ + authenticated: boolean; + + /** + * the sender's device information, if available + */ + sender?: DeviceInfo; + + /** + * if the event's ed25519 and curve25519 keys don't match (only meaningful if `sender` is set) + */ + mismatchedSender: boolean; +} + +export interface IRecoveryKey { + keyInfo: { + pubkey: Uint8Array; + passphrase?: { + algorithm: string; + iterations: number; + salt: string; + }; + }; + privateKey: Uint8Array; + encodedPrivateKey: string; +} + +export interface ICreateSecretStorageOpts { + /** + * Function called to await a secret storage key creation flow. + * Returns: + * {Promise} Object with public key metadata, encoded private + * recovery key which should be disposed of after displaying to the user, + * and raw private key to avoid round tripping if needed. + */ + createSecretStorageKey?: () => Promise; + + /** + * The current key backup object. If passed, + * the passphrase and recovery key from this backup will be used. + */ + keyBackupInfo?: IKeyBackupVersion; + + /** + * If true, a new key backup version will be + * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo + * is supplied. + */ + setupNewKeyBackup?: boolean; + + /** + * Reset even if keys already exist. + */ + setupNewSecretStorage?: boolean; + + /** + * Function called to get the user's + * current key backup passphrase. Should return a promise that resolves with a Buffer + * containing the key, or rejects if the key cannot be obtained. + */ + getKeyBackupPassphrase?: () => Promise; +} + +export interface ISecretStorageKey { + keyId: string; + keyInfo: ISecretStorageKeyInfo; +} + +export interface IAddSecretStorageKeyOpts { + // depends on algorithm + // TODO: Types +} + +export interface IImportOpts { + stage: string; // TODO: Enum + successes: number; + failures: number; + total: number; +} + +export interface IImportRoomKeysOpts { + progressCallback: (stage: IImportOpts) => void; + untrusted?: boolean; + source?: string; // TODO: Enum +} diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 2fcae9cea6d..38477d59a3a 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,10 +19,23 @@ import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store'; import { decryptAES, encryptAES } from './aes'; import anotherjson from "another-json"; import { logger } from '../logger'; +import { ISecretStorageKeyInfo } from "../matrix"; // FIXME: these types should eventually go in a different file type Signatures = Record>; +export interface IDehydratedDevice { + device_id: string; + device_data: ISecretStorageKeyInfo & { + algorithm: string; + account: string; // pickle + }; +} + +export interface IDehydratedDeviceKeyInfo { + passphrase: string; +} + interface DeviceKeys { algorithms: Array; device_id: string; // eslint-disable-line camelcase diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts new file mode 100644 index 00000000000..1065f256a5d --- /dev/null +++ b/src/crypto/keybackup.ts @@ -0,0 +1,70 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ISignatures } from "../@types/signed"; +import { DeviceInfo } from "./deviceinfo"; + +export interface IKeyBackupSession { + first_message_index: number; + forwarded_count: number; + is_verified: boolean; + session_data: { + ciphertext: string; + ephemeral: string; + mac: string; + }; +} + +export interface IKeyBackupRoomSessions { + [sessionId: string]: IKeyBackupSession; +} + +export interface IKeyBackupVersion { + algorithm: string; + auth_data: { + public_key: string; + signatures: ISignatures; + }; + count: number; + etag: string; + version: string; // number contained within +} + +// TODO: Verify types +export interface IKeyBackupTrustInfo { + /** + * is the backup trusted, true if there is a sig that is valid & from a trusted device + */ + usable: boolean[]; + sigs: { + valid: boolean[]; + device: DeviceInfo[]; + }[]; +} + +export interface IKeyBackupPrepareOpts { + secureSecretStorage: boolean; +} + +export interface IKeyBackupRestoreResult { + total: number; + imported: number; +} + +export interface IKeyBackupRestoreOpts { + cacheCompleteCallback?: () => void; + progressCallback?: ({stage: string}) => void; +} diff --git a/src/event-mapper.ts b/src/event-mapper.ts new file mode 100644 index 00000000000..d2095f40562 --- /dev/null +++ b/src/event-mapper.ts @@ -0,0 +1,48 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient } from "./1client"; +import {MatrixEvent} from "./models/event"; + +export type EventMapper = (obj: any) => MatrixEvent; + +export interface MapperOpts { + preventReEmit?: boolean; + decrypt?: boolean; +} + +export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper { + const preventReEmit = Boolean(options.preventReEmit); + const decrypt = options.decrypt !== false; + function mapper(plainOldJsObject) { + const event = new MatrixEvent(plainOldJsObject); + if (event.isEncrypted()) { + if (!preventReEmit) { + client.reEmitter.reEmit(event, [ + "Event.decrypted", + ]); + } + if (decrypt) { + client.decryptEventIfNeeded(event); + } + } + if (!preventReEmit) { + client.reEmitter.reEmit(event, ["Event.replaced"]); + } + return event; + } + return mapper; +} diff --git a/src/matrix.ts b/src/matrix.ts index fb818fe1d4b..0cf0d96a432 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -1,7 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,18 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type Request from "request"; - import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; -import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store"; -import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store"; import { MemoryStore } from "./store/memory"; -import { StubStore } from "./store/stub"; -import { LocalIndexedDBStoreBackend } from "./store/indexeddb-local-backend"; -import { RemoteIndexedDBStoreBackend } from "./store/indexeddb-remote-backend"; import { MatrixScheduler } from "./scheduler"; import { MatrixClient } from "./client"; -import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; +import { ICreateClientOpts } from "./1client"; export * from "./client"; export * from "./http-api"; @@ -95,11 +86,6 @@ export function wrapRequest(wrapper) { }; } -type Store = - StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend; - -type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore; - let cryptoStoreFactory = () => new MemoryCryptoStore; /** @@ -112,155 +98,6 @@ export function setCryptoStoreFactory(fac) { cryptoStoreFactory = fac; } -export interface ICreateClientOpts { - baseUrl: string; - - idBaseUrl?: string; - - /** - * The data store used for sync data from the homeserver. If not specified, - * this client will not store any HTTP responses. The `createClient` helper - * will create a default store if needed. - */ - store?: Store; - - /** - * A store to be used for end-to-end crypto session data. If not specified, - * end-to-end crypto will be disabled. The `createClient` helper will create - * a default store if needed. - */ - cryptoStore?: CryptoStore; - - /** - * The scheduler to use. If not - * specified, this client will not retry requests on failure. This client - * will supply its own processing function to - * {@link module:scheduler~MatrixScheduler#setProcessFunction}. - */ - scheduler?: MatrixScheduler; - - /** - * The function to invoke for HTTP - * requests. The value of this property is typically require("request") - * as it returns a function which meets the required interface. See - * {@link requestFunction} for more information. - */ - request?: Request; - - userId?: string; - - /** - * A unique identifier for this device; used for tracking things like crypto - * keys and access tokens. If not specified, end-to-end encryption will be - * disabled. - */ - deviceId?: string; - - accessToken?: string; - - /** - * Identity server provider to retrieve the user's access token when accessing - * the identity server. See also https://github.com/vector-im/element-web/issues/10615 - * which seeks to replace the previous approach of manual access tokens params - * with this callback throughout the SDK. - */ - identityServer?: IIdentityServerProvider; - - /** - * The default maximum amount of - * time to wait before timing out HTTP requests. If not specified, there is no timeout. - */ - localTimeoutMs?: number; - - /** - * Set to true to use - * Authorization header instead of query param to send the access token to the server. - * - * Default false. - */ - useAuthorizationHeader?: boolean; - - /** - * Set to true to enable - * improved timeline support ({@link module:client~MatrixClient#getEventTimeline getEventTimeline}). It is - * disabled by default for compatibility with older clients - in particular to - * maintain support for back-paginating the live timeline after a '/sync' - * result with a gap. - */ - timelineSupport?: boolean; - - /** - * Extra query parameters to append - * to all requests with this client. Useful for application services which require - * ?user_id=. - */ - queryParams?: Record; - - /** - * Device data exported with - * "exportDevice" method that must be imported to recreate this device. - * Should only be useful for devices with end-to-end crypto enabled. - * If provided, deviceId and userId should **NOT** be provided at the top - * level (they are present in the exported data). - */ - deviceToImport?: { - olmDevice: { - pickledAccount: string; - sessions: Array>; - pickleKey: string; - }; - userId: string; - deviceId: string; - }; - - /** - * Key used to pickle olm objects or other sensitive data. - */ - pickleKey?: string; - - /** - * A store to be used for end-to-end crypto session data. Most data has been - * migrated out of here to `cryptoStore` instead. If not specified, - * end-to-end crypto will be disabled. The `createClient` helper - * _will not_ create this store at the moment. - */ - sessionStore?: any; - - /** - * Set to true to enable client-side aggregation of event relations - * via `EventTimelineSet#getRelationsForEvent`. - * This feature is currently unstable and the API may change without notice. - */ - unstableClientRelationAggregation?: boolean; - - verificationMethods?: Array; - - /** - * Whether relaying calls through a TURN server should be forced. Default false. - */ - forceTURN?: boolean; - - /** - * Up to this many ICE candidates will be gathered when an incoming call arrives. - * Gathering does not send data to the caller, but will communicate with the configured TURN - * server. Default 0. - */ - iceCandidatePoolSize?: number; - - /** - * True to advertise support for call transfers to other parties on Matrix calls. Default false. - */ - supportsCallTransfer?: boolean; - - /** - * Whether to allow a fallback ICE server should be used for negotiating a - * WebRTC connection if the homeserver doesn't provide any servers. Defaults to false. - */ - fallbackICEServerAllowed?: boolean; - - cryptoCallbacks?: ICryptoCallbacks; -} - export interface ICryptoCallbacks { getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise; saveCrossSigningKeys?: (keys: Record) => void; diff --git a/src/sync.api.ts b/src/sync.api.ts new file mode 100644 index 00000000000..384e027f612 --- /dev/null +++ b/src/sync.api.ts @@ -0,0 +1,26 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// TODO: Merge this with sync.js once converted + +export enum SyncState { + Error = "ERROR", + Prepared = "PREPARED", + Stopped = "STOPPED", + Syncing = "SYNCING", + Catchup = "CATCHUP", + Reconnecting = "RECONNECTING", +} diff --git a/src/utils.ts b/src/utils.ts index 73e973a725b..a4a50153ac4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -398,7 +398,7 @@ export function ensureNoTrailingSlash(url: string): string { } // Returns a promise which resolves with a given value after the given number of ms -export function sleep(ms: number, value: T): Promise { +export function sleep(ms: number, value?: T): Promise { return new Promise((resolve => { setTimeout(resolve, ms, value); })); From 497c2dc8dfbe8d3db66af76ae9e17cac87ae8140 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 May 2021 22:35:54 -0600 Subject: [PATCH 04/32] Bring in BaseApis to MatrixClient --- src/1client.ts | 2272 +++++++++++++++++++++++++++++++++++++++- src/@types/requests.ts | 27 + 2 files changed, 2293 insertions(+), 6 deletions(-) diff --git a/src/1client.ts b/src/1client.ts index b50b1569651..1520adf6561 100644 --- a/src/1client.ts +++ b/src/1client.ts @@ -31,19 +31,27 @@ import { sleep } from './utils'; import { Group } from "./models/group"; import { EventTimeline } from "./models/event-timeline"; import { PushAction, PushProcessor } from "./pushprocessor"; -import { PREFIX_MEDIA_R0, PREFIX_UNSTABLE, retryNetworkOperation, } from "./http-api"; -import {AutoDiscovery} from "./autodiscovery"; +import { AutoDiscovery } from "./autodiscovery"; import * as olmlib from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; import { ReEmitter } from './ReEmitter'; import { RoomList } from './crypto/RoomList'; import { logger } from './logger'; +import { SERVICE_TYPES } from './service-types'; +import { + MatrixHttpApi, + PREFIX_IDENTITY_V2, + PREFIX_MEDIA_R0, + PREFIX_R0, + PREFIX_UNSTABLE, + retryNetworkOperation +} from "./http-api"; import { Crypto, DeviceInfo, fixBackupKey, isCryptoAvailable } from './crypto'; import { decodeRecoveryKey } from './crypto/recoverykey'; import { keyFromAuthData } from './crypto/key_passphrase'; import { User } from "./models/user"; import { getHttpUriForMxc } from "./content-repo"; -import {SearchResult} from "./models/search-result"; +import { SearchResult } from "./models/search-result"; import { DEHYDRATION_ALGORITHM, IDehydratedDevice, IDehydratedDeviceKeyInfo } from "./crypto/dehydration"; import { IKeyBackupPrepareOpts, @@ -82,19 +90,22 @@ import { import { CrossSigningInfo, UserTrustLevel } from "./crypto/CrossSigning"; import { Room } from "./models/Room"; import { + ICreateRoomOpts, IEventSearchOpts, IGuestAccessOpts, IJoinRoomOpts, IPaginateOpts, IPresenceOpts, - IRedactOpts, ISearchOpts, - ISendEventResponse + IRedactOpts, IRoomDirectoryOptions, + ISearchOpts, + ISendEventResponse, IUploadOpts } from "./@types/requests"; import { EventType } from "./@types/event"; import { IImageInfo } from "./@types/partials"; import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper"; import url from "url"; import { randomString } from "./randomstring"; +import { ReadStream } from "fs"; export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend; @@ -413,6 +424,11 @@ export class MatrixClient extends EventEmitter { private turnServersExpiry = 0; private checkTurnServersIntervalID: number; private exportedOlmDeviceToImport: IOlmDevice; + private baseUrl: string; + private idBaseUrl: string; + private identityServer: any; // TODO: @@TR + private http: MatrixHttpApi; + private txnCtr = 0; constructor(opts: IMatrixClientCreateOpts) { super(); @@ -427,6 +443,18 @@ export class MatrixClient extends EventEmitter { const userId = opts.userId || null; this.credentials = {userId}; + this.http = new MatrixHttpApi(this, { + baseUrl: opts.baseUrl, + idBaseUrl: opts.idBaseUrl, + accessToken: opts.accessToken, + request: opts.request, + prefix: PREFIX_R0, + onlyData: true, + extraParams: opts.queryParams, + localTimeoutMs: opts.localTimeoutMs, + useAuthorizationHeader: opts.useAuthorizationHeader, + }); + if (opts.deviceToImport) { if (this.deviceId) { logger.warn( @@ -5531,7 +5559,2239 @@ export class MatrixClient extends EventEmitter { return Promise.resolve(); } } -} + + private termsUrlForService(serviceType: SERVICE_TYPES, baseUrl: string) { + switch (serviceType) { + case SERVICE_TYPES.IS: + return baseUrl + PREFIX_IDENTITY_V2 + '/terms'; + case SERVICE_TYPES.IM: + return baseUrl + '/_matrix/integrations/v1/terms'; + default: + throw new Error('Unsupported service type'); + } + } + + /** + * Get the Homeserver URL of this client + * @return {string} Homeserver URL of this client + */ + public getHomeserverUrl(): string { + return this.baseUrl; + } + + /** + * Get the Identity Server URL of this client + * @param {boolean} stripProto whether or not to strip the protocol from the URL + * @return {string} Identity Server URL of this client + */ + public getIdentityServerUrl(stripProto = false): string { + if (stripProto && (this.idBaseUrl.startsWith("http://") || + this.idBaseUrl.startsWith("https://"))) { + return this.idBaseUrl.split("://")[1]; + } + return this.idBaseUrl; + } + + /** + * Set the Identity Server URL of this client + * @param {string} url New Identity Server URL + */ + public setIdentityServerUrl(url: string) { + this.idBaseUrl = utils.ensureNoTrailingSlash(url); + this.http.setIdBaseUrl(this.idBaseUrl); + } + + /** + * Get the access token associated with this account. + * @return {?String} The access_token or null + */ + public getAccessToken(): string { + return this.http.opts.accessToken || null; + } + + /** + * @return {boolean} true if there is a valid access_token for this client. + */ + public isLoggedIn(): boolean { + return this.http.opts.accessToken !== undefined; + } + + /** + * Make up a new transaction id + * + * @return {string} a new, unique, transaction id + */ + public makeTxnId(): string { + return "m" + new Date().getTime() + "." + (this.txnCtr++); + } + + /** + * Check whether a username is available prior to registration. An error response + * indicates an invalid/unavailable username. + * @param {string} username The username to check the availability of. + * @return {Promise} Resolves: to `true`. + */ + public isUsernameAvailable(username: string): Promise { + return this.http.authedRequest( + undefined, "GET", '/register/available', { username: username }, + ).then((response) => { + return response.available; + }); + } + + /** + * @param {string} username + * @param {string} password + * @param {string} sessionId + * @param {Object} auth + * @param {Object} bindThreepids Set key 'email' to true to bind any email + * threepid uses during registration in the ID server. Set 'msisdn' to + * true to bind msisdn. + * @param {string} guestAccessToken + * @param {string} inhibitLogin + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public register(username: string, password: string, sessionId: string, auth: any, bindThreepids: any, guestAccessToken: string, inhibitLogin: boolean, callback?: Callback): Promise { // TODO: Types (many) + // backwards compat + if (bindThreepids === true) { + bindThreepids = { email: true }; + } else if (bindThreepids === null || bindThreepids === undefined) { + bindThreepids = {}; + } + if (typeof inhibitLogin === 'function') { + callback = inhibitLogin; + inhibitLogin = undefined; + } + + if (sessionId) { + auth.session = sessionId; + } + + const params: any = { + auth: auth, + }; + if (username !== undefined && username !== null) { + params.username = username; + } + if (password !== undefined && password !== null) { + params.password = password; + } + if (bindThreepids.email) { + params.bind_email = true; + } + if (bindThreepids.msisdn) { + params.bind_msisdn = true; + } + if (guestAccessToken !== undefined && guestAccessToken !== null) { + params.guest_access_token = guestAccessToken; + } + if (inhibitLogin !== undefined && inhibitLogin !== null) { + params.inhibit_login = inhibitLogin; + } + // Temporary parameter added to make the register endpoint advertise + // msisdn flows. This exists because there are clients that break + // when given stages they don't recognise. This parameter will cease + // to be necessary once these old clients are gone. + // Only send it if we send any params at all (the password param is + // mandatory, so if we send any params, we'll send the password param) + if (password !== undefined && password !== null) { + params.x_show_msisdn = true; + } + + return this.registerRequest(params, undefined, callback); + } + + /** + * Register a guest account. + * This method returns the auth info needed to create a new authenticated client, + * Remember to call `setGuest(true)` on the (guest-)authenticated client, e.g: + * ```javascript + * const tmpClient = await sdk.createClient(MATRIX_INSTANCE); + * const { user_id, device_id, access_token } = tmpClient.registerGuest(); + * const client = createClient({ + * baseUrl: MATRIX_INSTANCE, + * accessToken: access_token, + * userId: user_id, + * deviceId: device_id, + * }) + * client.setGuest(true); + * ``` + * + * @param {Object=} opts Registration options + * @param {Object} opts.body JSON HTTP body to provide. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: JSON object that contains: + * { user_id, device_id, access_token, home_server } + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public registerGuest(opts: {body?: any}, callback?: Callback): Promise { // TODO: Types + opts = opts || {}; + opts.body = opts.body || {}; + return this.registerRequest(opts.body, "guest", callback); + } + + /** + * @param {Object} data parameters for registration request + * @param {string=} kind type of user to register. may be "guest" + * @param {module:client.callback=} callback + * @return {Promise} Resolves: to the /register response + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public registerRequest(data: any, kind: string, callback?: Callback): Promise { // TODO: Types + const params: any = {}; + if (kind) { + params.kind = kind; + } + + return this.http.request(callback, "POST", "/register", params, data); + } + + /** + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public loginFlows(callback?: Callback): Promise { // TODO: Types + return this.http.request(callback, "GET", "/login"); + } + + /** + * @param {string} loginType + * @param {Object} data + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public login(loginType: string, data: any, callback?: Callback): Promise { // TODO: Types + const login_data = { + type: loginType, + }; + + // merge data into login_data + utils.extend(login_data, data); + + return this.http.authedRequest( + (error, response) => { + if (response && response.access_token && response.user_id) { + this.http.opts.accessToken = response.access_token; + this.credentials = { + userId: response.user_id, + }; + } + + if (callback) { + callback(error, response); + } + }, "POST", "/login", undefined, login_data, + ); + } + + /** + * @param {string} user + * @param {string} password + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public loginWithPassword(user: string, password: string, callback?: Callback): Promise { // TODO: Types + return this.login("m.login.password", { + user: user, + password: password, + }, callback); + } + + /** + * @param {string} relayState URL Callback after SAML2 Authentication + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public loginWithSAML2(relayState: string, callback?: Callback): Promise { // TODO: Types + return this.login("m.login.saml2", { + relay_state: relayState, + }, callback); + } + + /** + * @param {string} redirectUrl The URL to redirect to after the HS + * authenticates with CAS. + * @return {string} The HS URL to hit to begin the CAS login process. + */ + public getCasLoginUrl(redirectUrl: string): Promise { + return this.getSsoLoginUrl(redirectUrl, "cas"); + } + + /** + * @param {string} redirectUrl The URL to redirect to after the HS + * authenticates with the SSO. + * @param {string} loginType The type of SSO login we are doing (sso or cas). + * Defaults to 'sso'. + * @param {string} idpId The ID of the Identity Provider being targeted, optional. + * @return {string} The HS URL to hit to begin the SSO login process. + */ + public getSsoLoginUrl(redirectUrl: string, loginType = "sso", idpId?: string): Promise { + let prefix = PREFIX_R0; + let url = "/login/" + loginType + "/redirect"; + if (idpId) { + url += "/" + idpId; + prefix = "/_matrix/client/unstable/org.matrix.msc2858"; + } + + return this.http.getUrl(url, { redirectUrl }, prefix); + } + + /** + * @param {string} token Login token previously received from homeserver + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public loginWithToken(token: string, callback?: Callback): Promise { // TODO: Types + return this.login("m.login.token", { + token: token, + }, callback); + } + + /** + * Logs out the current session. + * Obviously, further calls that require authorisation should fail after this + * method is called. The state of the MatrixClient object is not affected: + * it is up to the caller to either reset or destroy the MatrixClient after + * this method succeeds. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: On success, the empty object + */ + public logout(callback?: Callback): Promise<{}> { + return this.http.authedRequest( + callback, "POST", '/logout', + ); + } + + /** + * Deactivates the logged-in account. + * Obviously, further calls that require authorisation should fail after this + * method is called. The state of the MatrixClient object is not affected: + * it is up to the caller to either reset or destroy the MatrixClient after + * this method succeeds. + * @param {object} auth Optional. Auth data to supply for User-Interactive auth. + * @param {boolean} erase Optional. If set, send as `erase` attribute in the + * JSON request body, indicating whether the account should be erased. Defaults + * to false. + * @return {Promise} Resolves: On success, the empty object + */ + public deactivateAccount(auth?: any, erase?: boolean): Promise<{}> { + if (typeof(erase) === 'function') { + throw new Error( + 'deactivateAccount no longer accepts a callback parameter', + ); + } + + const body: any = {}; + if (auth) { + body.auth = auth; + } + if (erase !== undefined) { + body.erase = erase; + } + + return this.http.authedRequest( + undefined, "POST", '/account/deactivate', undefined, body, + ); + } + + /** + * Get the fallback URL to use for unknown interactive-auth stages. + * + * @param {string} loginType the type of stage being attempted + * @param {string} authSessionId the auth session ID provided by the homeserver + * + * @return {string} HS URL to hit to for the fallback interface + */ + public getFallbackAuthUrl(loginType: string, authSessionId: string): Promise { + const path = utils.encodeUri("/auth/$loginType/fallback/web", { + $loginType: loginType, + }); + + return this.http.getUrl(path, { + session: authSessionId, + }, PREFIX_R0); + } + + /** + * Create a new room. + * @param {Object} options a list of options to pass to the /createRoom API. + * @param {string} options.room_alias_name The alias localpart to assign to + * this room. + * @param {string} options.visibility Either 'public' or 'private'. + * @param {string[]} options.invite A list of user IDs to invite to this room. + * @param {string} options.name The name to give this room. + * @param {string} options.topic The topic to give this room. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: {room_id: {string}, + * room_alias: {string(opt)}} + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async createRoom(options: ICreateRoomOpts, callback?: Callback): Promise<{roomId: string, room_alias?: string}> { + // some valid options include: room_alias_name, visibility, invite + + // inject the id_access_token if inviting 3rd party addresses + const invitesNeedingToken = (options.invite_3pid || []) + .filter(i => !i.id_access_token); + if ( + invitesNeedingToken.length > 0 && + this.identityServer && + this.identityServer.getAccessToken && + await this.doesServerAcceptIdentityAccessToken() + ) { + const identityAccessToken = await this.identityServer.getAccessToken(); + if (identityAccessToken) { + for (const invite of invitesNeedingToken) { + invite.id_access_token = identityAccessToken; + } + } + } + + return this.http.authedRequest( + callback, "POST", "/createRoom", undefined, options, + ); + } + + /** + * Fetches relations for a given event + * @param {string} roomId the room of the event + * @param {string} eventId the id of the event + * @param {string} relationType the rel_type of the relations requested + * @param {string} eventType the event type of the relations requested + * @param {Object} opts options with optional values for the request. + * @param {Object} opts.from the pagination token returned from a previous request as `next_batch` to return following relations. + * @return {Object} the response, with chunk and next_batch. + */ + public async fetchRelations(roomId: string, eventId: string, relationType: string, eventType: string, opts: {from: string}): Promise { // TODO: Types + const queryParams: any = {}; + if (opts.from) { + queryParams.from = opts.from; + } + const queryString = utils.encodeParams(queryParams); + const path = utils.encodeUri( + "/rooms/$roomId/relations/$eventId/$relationType/$eventType?" + queryString, { + $roomId: roomId, + $eventId: eventId, + $relationType: relationType, + $eventType: eventType, + }); + return await this.http.authedRequest( + undefined, "GET", path, null, null, { + prefix: PREFIX_UNSTABLE, + }, + ); + } + + /** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public roomState(roomId: string, callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/rooms/$roomId/state", { $roomId: roomId }); + return this.http.authedRequest(callback, "GET", path); + } + + /** + * Get an event in a room by its event id. + * @param {string} roomId + * @param {string} eventId + * @param {module:client.callback} callback Optional. + * + * @return {Promise} Resolves to an object containing the event. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public fetchRoomEvent(roomId: string, eventId: string, callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri( + "/rooms/$roomId/event/$eventId", { + $roomId: roomId, + $eventId: eventId, + }, + ); + return this.http.authedRequest(callback, "GET", path); + } + + /** + * @param {string} roomId + * @param {string} includeMembership the membership type to include in the response + * @param {string} excludeMembership the membership type to exclude from the response + * @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: dictionary of userid to profile information + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public members(roomId: string, includeMembership?: string[], excludeMembership?: string[], atEventId?: string, callback?: Callback): Promise<{[userId: string]: any}> { + const queryParams: any = {}; + if (includeMembership) { + queryParams.membership = includeMembership; + } + if (excludeMembership) { + queryParams.not_membership = excludeMembership; + } + if (atEventId) { + queryParams.at = atEventId; + } + + const queryString = utils.encodeParams(queryParams); + + const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, + { $roomId: roomId }); + return this.http.authedRequest(callback, "GET", path); + } + + /** + * Upgrades a room to a new protocol version + * @param {string} roomId + * @param {string} newVersion The target version to upgrade to + * @return {Promise} Resolves: Object with key 'replacement_room' + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public upgradeRoom(roomId: string, newVersion: string): Promise<{replacement_room: string}> { + const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId }); + return this.http.authedRequest( + undefined, "POST", path, undefined, { new_version: newVersion }, + ); + } + + /** + * Retrieve a state event. + * @param {string} roomId + * @param {string} eventType + * @param {string} stateKey + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getStateEvent(roomId: string, eventType: string, stateKey: string, callback?: Callback): Promise { + const pathParams = { + $roomId: roomId, + $eventType: eventType, + $stateKey: stateKey, + }; + let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); + if (stateKey !== undefined) { + path = utils.encodeUri(path + "/$stateKey", pathParams); + } + return this.http.authedRequest( + callback, "GET", path, + ); + } + + /** + * @param {string} roomId + * @param {string} eventType + * @param {Object} content + * @param {string} stateKey + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public sendStateEvent(roomId: string, eventType: string, content: any, stateKey: string, callback?: Callback): Promise { // TODO: Types + const pathParams = { + $roomId: roomId, + $eventType: eventType, + $stateKey: stateKey, + }; + let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); + if (stateKey !== undefined) { + path = utils.encodeUri(path + "/$stateKey", pathParams); + } + return this.http.authedRequest( + callback, "PUT", path, undefined, content, + ); + } + + /** + * @param {string} roomId + * @param {Number} limit + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public roomInitialSync(roomId: string, limit: number, callback?: Callback): Promise { // TODO: Types + if (utils.isFunction(limit)) { + callback = limit as any as Callback; // legacy + limit = undefined; + } + const path = utils.encodeUri("/rooms/$roomId/initialSync", + { $roomId: roomId }, + ); + if (!limit) { + limit = 30; + } + return this.http.authedRequest( + callback, "GET", path, { limit: limit }, + ); + } + + /** + * Set a marker to indicate the point in a room before which the user has read every + * event. This can be retrieved from room account data (the event type is `m.fully_read`) + * and displayed as a horizontal line in the timeline that is visually distinct to the + * position of the user's own read receipt. + * @param {string} roomId ID of the room that has been read + * @param {string} rmEventId ID of the event that has been read + * @param {string} rrEventId ID of the event tracked by the read receipt. This is here + * for convenience because the RR and the RM are commonly updated at the same time as + * each other. Optional. + * @param {object} opts Options for the read markers. + * @param {object} opts.hidden True to hide the read receipt from other users. This + * property is currently unstable and may change in the future. + * @return {Promise} Resolves: the empty object, {}. + */ + public setRoomReadMarkersHttpRequest(roomId: string, rmEventId: string, rrEventId: string, opts: {hidden: boolean}): Promise<{}> { + const path = utils.encodeUri("/rooms/$roomId/read_markers", { + $roomId: roomId, + }); + + const content = { + "m.fully_read": rmEventId, + "m.read": rrEventId, + "m.hidden": Boolean(opts ? opts.hidden : false), + }; + + return this.http.authedRequest( + undefined, "POST", path, undefined, content, + ); + } + + /** + * @return {Promise} Resolves: A list of the user's current rooms + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getJoinedRooms(): Promise { + const path = utils.encodeUri("/joined_rooms", {}); + return this.http.authedRequest(undefined, "GET", path); + } + + /** + * Retrieve membership info. for a room. + * @param {string} roomId ID of the room to get membership for + * @return {Promise} Resolves: A list of currently joined users + * and their profile data. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getJoinedRoomMembers(roomId: string): Promise { // TODO: Types + const path = utils.encodeUri("/rooms/$roomId/joined_members", { + $roomId: roomId, + }); + return this.http.authedRequest(undefined, "GET", path); + } + + /** + * @param {Object} options Options for this request + * @param {string} options.server The remote server to query for the room list. + * Optional. If unspecified, get the local home + * server's public room list. + * @param {number} options.limit Maximum number of entries to return + * @param {string} options.since Token to paginate from + * @param {object} options.filter Filter parameters + * @param {string} options.filter.generic_search_term String to search for + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public publicRooms(options: IRoomDirectoryOptions, callback?: Callback): Promise { // TODO: Types + if (typeof(options) == 'function') { + callback = options; + options = {}; + } + if (options === undefined) { + options = {}; + } + + const query_params: any = {}; + if (options.server) { + query_params.server = options.server; + delete options.server; + } + + if (Object.keys(options).length === 0 && Object.keys(query_params).length === 0) { + return this.http.authedRequest(callback, "GET", "/publicRooms"); + } else { + return this.http.authedRequest( + callback, "POST", "/publicRooms", query_params, options, + ); + } + } + + /** + * Create an alias to room ID mapping. + * @param {string} alias The room alias to create. + * @param {string} roomId The room ID to link the alias to. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public createAlias(alias: string, roomId: string, callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/directory/room/$alias", { + $alias: alias, + }); + const data = { + room_id: roomId, + }; + return this.http.authedRequest( + callback, "PUT", path, undefined, data, + ); + } + + /** + * Delete an alias to room ID mapping. This alias must be on your local server + * and you must have sufficient access to do this operation. + * @param {string} alias The room alias to delete. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public deleteAlias(alias: string, callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/directory/room/$alias", { + $alias: alias, + }); + return this.http.authedRequest( + callback, "DELETE", path, undefined, undefined, + ); + } + + /** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: an object with an `aliases` property, containing an array of local aliases + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public unstableGetLocalAliases(roomId: string, callback?: Callback): Promise<{aliases: string[]}> { + const path = utils.encodeUri("/rooms/$roomId/aliases", + { $roomId: roomId }); + const prefix = PREFIX_UNSTABLE + "/org.matrix.msc2432"; + return this.http.authedRequest(callback, "GET", path, + null, null, { prefix }); + } + + /** + * Get room info for the given alias. + * @param {string} alias The room alias to resolve. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Object with room_id and servers. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getRoomIdForAlias(alias: string, callback?: Callback): Promise<{room_id: string, servers: string[]}> { + // TODO: deprecate this or resolveRoomAlias + const path = utils.encodeUri("/directory/room/$alias", { + $alias: alias, + }); + return this.http.authedRequest( + callback, "GET", path, + ); + } + + /** + * @param {string} roomAlias + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public resolveRoomAlias(roomAlias: string, callback?: Callback): Promise { // TODO: Types + // TODO: deprecate this or getRoomIdForAlias + const path = utils.encodeUri("/directory/room/$alias", { $alias: roomAlias }); + return this.http.request(callback, "GET", path); + } + + /** + * Get the visibility of a room in the current HS's room directory + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getRoomDirectoryVisibility(roomId: string, callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/directory/list/room/$roomId", { + $roomId: roomId, + }); + return this.http.authedRequest(callback, "GET", path); + } + + /** + * Set the visbility of a room in the current HS's room directory + * @param {string} roomId + * @param {string} visibility "public" to make the room visible + * in the public directory, or "private" to make + * it invisible. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setRoomDirectoryVisibility(roomId: string, visibility: "public" | "private", callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/directory/list/room/$roomId", { + $roomId: roomId, + }); + return this.http.authedRequest( + callback, "PUT", path, undefined, { "visibility": visibility }, + ); + } + + /** + * Set the visbility of a room bridged to a 3rd party network in + * the current HS's room directory. + * @param {string} networkId the network ID of the 3rd party + * instance under which this room is published under. + * @param {string} roomId + * @param {string} visibility "public" to make the room visible + * in the public directory, or "private" to make + * it invisible. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setRoomDirectoryVisibilityAppService(networkId: string, roomId: string, visibility: "public" | "private", callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", { + $networkId: networkId, + $roomId: roomId, + }); + return this.http.authedRequest( + callback, "PUT", path, undefined, { "visibility": visibility }, + ); + } + + /** + * Query the user directory with a term matching user IDs, display names and domains. + * @param {object} opts options + * @param {string} opts.term the term with which to search. + * @param {number} opts.limit the maximum number of results to return. The server will + * apply a limit if unspecified. + * @return {Promise} Resolves: an array of results. + */ + public searchUserDirectory(opts: {term: string, limit?: number}): Promise { // TODO: Types + const body: any = { + search_term: opts.term, + }; + + if (opts.limit !== undefined) { + body.limit = opts.limit; + } + + return this.http.authedRequest( + undefined, "POST", "/user_directory/search", undefined, body, + ); + } + + /** + * Upload a file to the media repository on the home server. + * + * @param {object} file The object to upload. On a browser, something that + * can be sent to XMLHttpRequest.send (typically a File). Under node.js, + * a a Buffer, String or ReadStream. + * + * @param {object} opts options object + * + * @param {string=} opts.name Name to give the file on the server. Defaults + * to file.name. + * + * @param {boolean=} opts.includeFilename if false will not send the filename, + * e.g for encrypted file uploads where filename leaks are undesirable. + * Defaults to true. + * + * @param {string=} opts.type Content-type for the upload. Defaults to + * file.type, or applicaton/octet-stream. + * + * @param {boolean=} opts.rawResponse Return the raw body, rather than + * parsing the JSON. Defaults to false (except on node.js, where it + * defaults to true for backwards compatibility). + * + * @param {boolean=} opts.onlyContentUri Just return the content URI, + * rather than the whole body. Defaults to false (except on browsers, + * where it defaults to true for backwards compatibility). Ignored if + * opts.rawResponse is true. + * + * @param {Function=} opts.callback Deprecated. Optional. The callback to + * invoke on success/failure. See the promise return values for more + * information. + * + * @param {Function=} opts.progressHandler Optional. Called when a chunk of + * data has been uploaded, with an object containing the fields `loaded` + * (number of bytes transferred) and `total` (total size, if known). + * + * @return {Promise} Resolves to response object, as + * determined by this.opts.onlyData, opts.rawResponse, and + * opts.onlyContentUri. Rejects with an error (usually a MatrixError). + */ + public uploadContent(file: File | String | Buffer | ReadStream, opts: IUploadOpts): Promise { // TODO: Advanced types + return this.http.uploadContent(file, opts); + } + + /** + * Cancel a file upload in progress + * @param {Promise} promise The promise returned from uploadContent + * @return {boolean} true if canceled, otherwise false + */ + public cancelUpload(promise: Promise): boolean { // TODO: Advanced types + return this.http.cancelUpload(promise); + } + + /** + * Get a list of all file uploads in progress + * @return {array} Array of objects representing current uploads. + * Currently in progress is element 0. Keys: + * - promise: The promise associated with the upload + * - loaded: Number of bytes uploaded + * - total: Total number of bytes to upload + */ + public getCurrentUploads(): {promise: Promise, loaded: number, total: number}[] { // TODO: Advanced types (promise) + return this.http.getCurrentUploads(); + } + + /** + * @param {string} userId + * @param {string} info The kind of info to retrieve (e.g. 'displayname', + * 'avatar_url'). + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getProfileInfo(userId: string, info: string, callback?: Callback): Promise { // TODO: Types + if (utils.isFunction(info)) { + callback = info as any as Callback; // legacy + info = undefined; + } + + const path = info ? + utils.encodeUri("/profile/$userId/$info", + { $userId: userId, $info: info }) : + utils.encodeUri("/profile/$userId", + { $userId: userId }); + return this.http.authedRequest(callback, "GET", path); + } + + /** + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getThreePids(callback?: Callback): Promise { // TODO: Types + const path = "/account/3pid"; + return this.http.authedRequest( + callback, "GET", path, undefined, undefined, + ); + } + + /** + * Add a 3PID to your homeserver account and optionally bind it to an identity + * server as well. An identity server is required as part of the `creds` object. + * + * This API is deprecated, and you should instead use `addThreePidOnly` + * for homeservers that support it. + * + * @param {Object} creds + * @param {boolean} bind + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public addThreePid(creds: any, bind: boolean, callback?: Callback): Promise { // TODO: Types + const path = "/account/3pid"; + const data = { + 'threePidCreds': creds, + 'bind': bind, + }; + return this.http.authedRequest( + callback, "POST", path, null, data, + ); + } + + /** + * Add a 3PID to your homeserver account. This API does not use an identity + * server, as the homeserver is expected to handle 3PID ownership validation. + * + * You can check whether a homeserver supports this API via + * `doesServerSupportSeparateAddAndBind`. + * + * @param {Object} data A object with 3PID validation data from having called + * `account/3pid//requestToken` on the homeserver. + * @return {Promise} Resolves: on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async addThreePidOnly(data: any): Promise { // TODO: Types + const path = "/account/3pid/add"; + const prefix = await this.isVersionSupported("r0.6.0") ? + PREFIX_R0 : PREFIX_UNSTABLE; + return this.http.authedRequest( + undefined, "POST", path, null, data, { prefix }, + ); + } + + /** + * Bind a 3PID for discovery onto an identity server via the homeserver. The + * identity server handles 3PID ownership validation and the homeserver records + * the new binding to track where all 3PIDs for the account are bound. + * + * You can check whether a homeserver supports this API via + * `doesServerSupportSeparateAddAndBind`. + * + * @param {Object} data A object with 3PID validation data from having called + * `validate//requestToken` on the identity server. It should also + * contain `id_server` and `id_access_token` fields as well. + * @return {Promise} Resolves: on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async bindThreePid(data: any): Promise { // TODO: Types + const path = "/account/3pid/bind"; + const prefix = await this.isVersionSupported("r0.6.0") ? + PREFIX_R0 : PREFIX_UNSTABLE; + return this.http.authedRequest( + undefined, "POST", path, null, data, { prefix }, + ); + } + + /** + * Unbind a 3PID for discovery on an identity server via the homeserver. The + * homeserver removes its record of the binding to keep an updated record of + * where all 3PIDs for the account are bound. + * + * @param {string} medium The threepid medium (eg. 'email') + * @param {string} address The threepid address (eg. 'bob@example.com') + * this must be as returned by getThreePids. + * @return {Promise} Resolves: on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async unbindThreePid(medium: string, address: string): Promise { + const path = "/account/3pid/unbind"; + const data = { + medium, + address, + id_server: this.getIdentityServerUrl(true), + }; + const prefix = await this.isVersionSupported("r0.6.0") ? + PREFIX_R0 : PREFIX_UNSTABLE; + return this.http.authedRequest( + undefined, "POST", path, null, data, { prefix }, + ); + } + + /** + * @param {string} medium The threepid medium (eg. 'email') + * @param {string} address The threepid address (eg. 'bob@example.com') + * this must be as returned by getThreePids. + * @return {Promise} Resolves: The server response on success + * (generally the empty JSON object) + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public deleteThreePid(medium: string, address: string): Promise { + const path = "/account/3pid/delete"; + const data = { + 'medium': medium, + 'address': address, + }; + return this.http.authedRequest(undefined, "POST", path, null, data); + } + + /** + * Make a request to change your password. + * @param {Object} authDict + * @param {string} newPassword The new desired password. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setPassword(authDict: any, newPassword: string, callback?: Callback): Promise { // TODO: Types + const path = "/account/password"; + const data = { + 'auth': authDict, + 'new_password': newPassword, + }; + + return this.http.authedRequest( + callback, "POST", path, null, data, + ); + } + + /** + * Gets all devices recorded for the logged-in user + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getDevices(): Promise { // TODO: Types + return this.http.authedRequest( + undefined, 'GET', "/devices", undefined, undefined, + ); + } + + /** + * Gets specific device details for the logged-in user + * @param {string} deviceId device to query + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getDevice(deviceId: string): Promise { // TODO: Types + const path = utils.encodeUri("/devices/$device_id", { + $device_id: deviceId, + }); + return this.http.authedRequest( + undefined, 'GET', path, undefined, undefined, + ); + } + + /** + * Update the given device + * + * @param {string} deviceId device to update + * @param {Object} body body of request + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setDeviceDetails(deviceId: string, body: any): Promise { // TODO: Types + const path = utils.encodeUri("/devices/$device_id", { + $device_id: deviceId, + }); + + return this.http.authedRequest(undefined, "PUT", path, undefined, body); + } + + /** + * Delete the given device + * + * @param {string} deviceId device to delete + * @param {object} auth Optional. Auth data to supply for User-Interactive auth. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public deleteDevice(deviceId: string, auth?: any): Promise { // TODO: Types + const path = utils.encodeUri("/devices/$device_id", { + $device_id: deviceId, + }); + + const body: any = {}; + + if (auth) { + body.auth = auth; + } + + return this.http.authedRequest(undefined, "DELETE", path, undefined, body); + } + + /** + * Delete multiple device + * + * @param {string[]} devices IDs of the devices to delete + * @param {object} auth Optional. Auth data to supply for User-Interactive auth. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public deleteMultipleDevices(devices: string[], auth?: any): Promise { // TODO: Types + const body: any = { devices }; + + if (auth) { + body.auth = auth; + } + + const path = "/delete_devices"; + return this.http.authedRequest(undefined, "POST", path, undefined, body); + } + + /** + * Gets all pushers registered for the logged-in user + * + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Array of objects representing pushers + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getPushers(callback?: Callback): Promise { // TODO: Types + const path = "/pushers"; + return this.http.authedRequest( + callback, "GET", path, undefined, undefined, + ); + } + + /** + * Adds a new pusher or updates an existing pusher + * + * @param {Object} pusher Object representing a pusher + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Empty json object on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setPusher(pusher: any, callback?: Callback): Promise { // TODO: Types + const path = "/pushers/set"; + return this.http.authedRequest( + callback, "POST", path, null, pusher, + ); + } + + /** + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getPushRules(callback?: Callback): Promise { // TODO: Types + return this._http.authedRequest(callback, "GET", "/pushrules/").then(rules => { + return PushProcessor.rewriteDefaultRules(rules); + }); + } + + /** + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {Object} body + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public addPushRule(scope: string, kind: string, ruleId: string, body: any, callback?: Callback): Promise { // TODO: Types + // NB. Scope not uri encoded because devices need the '/' + const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { + $kind: kind, + $ruleId: ruleId, + }); + return this.http.authedRequest( + callback, "PUT", path, undefined, body, + ); + } + + /** + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public deletePushRule(scope: string, kind: string, ruleId: string, callback?: Callback): Promise { // TODO: Types + // NB. Scope not uri encoded because devices need the '/' + const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { + $kind: kind, + $ruleId: ruleId, + }); + return this.http.authedRequest(callback, "DELETE", path); + } + + /** + * Enable or disable a push notification rule. + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {boolean} enabled + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setPushRuleEnabled(scope: string, kind: string, ruleId: string, enabled: boolean, callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", { + $kind: kind, + $ruleId: ruleId, + }); + return this.http.authedRequest( + callback, "PUT", path, undefined, { "enabled": enabled }, + ); + } + + /** + * Set the actions for a push notification rule. + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {array} actions + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setPushRuleActions(scope: string, kind: string, ruleId: string, actions: string[], callback?: Callback): Promise { // TODO: Types + const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", { + $kind: kind, + $ruleId: ruleId, + }); + return this.http.authedRequest( + callback, "PUT", path, undefined, { "actions": actions }, + ); + } + + /** + * Perform a server-side search. + * @param {Object} opts + * @param {string} opts.next_batch the batch token to pass in the query string + * @param {Object} opts.body the JSON object to pass to the request body. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public search(opts: {body: any, next_batch: string}, callback?: Callback): Promise { // TODO: Types + const queryParams: any = {}; + if (opts.next_batch) { + queryParams.next_batch = opts.next_batch; + } + return this.http.authedRequest( + callback, "POST", "/search", queryParams, opts.body, + ); + } + + /** + * Upload keys + * + * @param {Object} content body of upload request + * + * @param {Object=} opts this method no longer takes any opts, + * used to take opts.device_id but this was not removed from the spec as a redundant parameter + * + * @param {module:client.callback=} callback + * + * @return {Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ + public uploadKeysRequest(content: any, opts?: any, callback?: Callback): Promise { // TODO: Types + return this.http.authedRequest(callback, "POST", "/keys/upload", undefined, content); + } + + public uploadKeySignatures(content: any): Promise { // TODO: Types + return this.http.authedRequest( + undefined, "POST", '/keys/signatures/upload', undefined, + content, { + prefix: PREFIX_UNSTABLE, + }, + ); + } + + /** + * Download device keys + * + * @param {string[]} userIds list of users to get keys for + * + * @param {Object=} opts + * + * @param {string=} opts.token sync token to pass in the query request, to help + * the HS give the most recent results + * + * @return {Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ + public downloadKeysForUsers(userIds: string[], opts: {token?: string}): Promise { // TODO: Types + if (utils.isFunction(opts)) { + // opts used to be 'callback'. + throw new Error( + 'downloadKeysForUsers no longer accepts a callback parameter', + ); + } + opts = opts || {}; + + const content: any = { + device_keys: {}, + }; + if ('token' in opts) { + content.token = opts.token; + } + userIds.forEach((u) => { + content.device_keys[u] = []; + }); + + return this.http.authedRequest(undefined, "POST", "/keys/query", undefined, content); + } + + /** + * Claim one-time keys + * + * @param {string[]} devices a list of [userId, deviceId] pairs + * + * @param {string} [keyAlgorithm = signed_curve25519] desired key type + * + * @param {number} [timeout] the time (in milliseconds) to wait for keys from remote + * servers + * + * @return {Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ + public claimOneTimeKeys(devices: string[], keyAlgorithm = "signed_curve25519", timeout?: number): Promise { // TODO: Types + const queries = {}; + + if (keyAlgorithm === undefined) { + keyAlgorithm = "signed_curve25519"; + } + + for (let i = 0; i < devices.length; ++i) { + const userId = devices[i][0]; + const deviceId = devices[i][1]; + const query = queries[userId] || {}; + queries[userId] = query; + query[deviceId] = keyAlgorithm; + } + const content: any = { one_time_keys: queries }; + if (timeout) { + content.timeout = timeout; + } + const path = "/keys/claim"; + return this.http.authedRequest(undefined, "POST", path, undefined, content); + } + + /** + * Ask the server for a list of users who have changed their device lists + * between a pair of sync tokens + * + * @param {string} oldToken + * @param {string} newToken + * + * @return {Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ + public getKeyChanges(oldToken: string, newToken: string): Promise { // TODO: Types + const qps = { + from: oldToken, + to: newToken, + }; + + const path = "/keys/changes"; + return this.http.authedRequest(undefined, "GET", path, qps, undefined); + } + + public uploadDeviceSigningKeys(auth: any, keys: any): Promise { // TODO: Lots of types + const data = Object.assign({}, keys); + if (auth) Object.assign(data, { auth }); + return this.http.authedRequest( + undefined, "POST", "/keys/device_signing/upload", undefined, data, { + prefix: PREFIX_UNSTABLE, + }, + ); + } + + /** + * Register with an Identity Server using the OpenID token from the user's + * Homeserver, which can be retrieved via + * {@link module:client~MatrixClient#getOpenIdToken}. + * + * Note that the `/account/register` endpoint (as well as IS authentication in + * general) was added as part of the v2 API version. + * + * @param {object} hsOpenIdToken + * @return {Promise} Resolves: with object containing an Identity + * Server access token. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public registerWithIdentityServer(hsOpenIdToken: any): Promise { // TODO: Types + if (!this.idBaseUrl) { + throw new Error("No Identity Server base URL set"); + } + + const uri = this.idBaseUrl + PREFIX_IDENTITY_V2 + "/account/register"; + return this.http.requestOtherUrl( + undefined, "POST", uri, + null, hsOpenIdToken, + ); + } + + /** + * Requests an email verification token directly from an identity server. + * + * This API is used as part of binding an email for discovery on an identity + * server. The validation data that results should be passed to the + * `bindThreePid` method to complete the binding process. + * + * @param {string} email The email address to request a token for + * @param {string} clientSecret A secret binary string generated by the client. + * It is recommended this be around 16 ASCII characters. + * @param {number} sendAttempt If an identity server sees a duplicate request + * with the same sendAttempt, it will not send another email. + * To request another email to be sent, use a larger value for + * the sendAttempt param as was used in the previous request. + * @param {string} nextLink Optional If specified, the client will be redirected + * to this link after validation. + * @param {module:client.callback} callback Optional. + * @param {string} identityAccessToken The `access_token` field of the identity + * server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws Error if no identity server is set + */ + public async requestEmailToken(email: string, clientSecret: string, sendAttempt: number, nextLink: string, callback?: Callback, identityAccessToken?: string): Promise { // TODO: Types + const params = { + client_secret: clientSecret, + email: email, + send_attempt: sendAttempt, + next_link: nextLink, + }; + + return await this.http.idServerRequest( + callback, "POST", "/validate/email/requestToken", + params, PREFIX_IDENTITY_V2, identityAccessToken, + ); + } + + /** + * Requests a MSISDN verification token directly from an identity server. + * + * This API is used as part of binding a MSISDN for discovery on an identity + * server. The validation data that results should be passed to the + * `bindThreePid` method to complete the binding process. + * + * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in + * which phoneNumber should be parsed relative to. + * @param {string} phoneNumber The phone number, in national or international + * format + * @param {string} clientSecret A secret binary string generated by the client. + * It is recommended this be around 16 ASCII characters. + * @param {number} sendAttempt If an identity server sees a duplicate request + * with the same sendAttempt, it will not send another SMS. + * To request another SMS to be sent, use a larger value for + * the sendAttempt param as was used in the previous request. + * @param {string} nextLink Optional If specified, the client will be redirected + * to this link after validation. + * @param {module:client.callback} callback Optional. + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws Error if no identity server is set + */ + public async requestMsisdnToken(phoneCountry: string, phoneNumber: string, clientSecret: string, sendAttempt: number, nextLink: string, callback?: Callback, identityAccessToken?: string): Promise { // TODO: Types + const params = { + client_secret: clientSecret, + country: phoneCountry, + phone_number: phoneNumber, + send_attempt: sendAttempt, + next_link: nextLink, + }; + + return await this.http.idServerRequest( + callback, "POST", "/validate/msisdn/requestToken", + params, PREFIX_IDENTITY_V2, identityAccessToken, + ); + } + + /** + * Submits a MSISDN token to the identity server + * + * This is used when submitting the code sent by SMS to a phone number. + * The ID server has an equivalent API for email but the js-sdk does + * not expose this, since email is normally validated by the user clicking + * a link rather than entering a code. + * + * @param {string} sid The sid given in the response to requestToken + * @param {string} clientSecret A secret binary string generated by the client. + * This must be the same value submitted in the requestToken call. + * @param {string} msisdnToken The MSISDN token, as enetered by the user. + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: Object, currently with no parameters. + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws Error if No ID server is set + */ + public async submitMsisdnToken(sid: string, clientSecret: string, msisdnToken: string, identityAccessToken: string): Promise { // TODO: Types + const params = { + sid: sid, + client_secret: clientSecret, + token: msisdnToken, + }; + + return await this.http.idServerRequest( + undefined, "POST", "/validate/msisdn/submitToken", + params, PREFIX_IDENTITY_V2, identityAccessToken, + ); + } + + /** + * Submits a MSISDN token to an arbitrary URL. + * + * This is used when submitting the code sent by SMS to a phone number in the + * newer 3PID flow where the homeserver validates 3PID ownership (as part of + * `requestAdd3pidMsisdnToken`). The homeserver response may include a + * `submit_url` to specify where the token should be sent, and this helper can + * be used to pass the token to this URL. + * + * @param {string} url The URL to submit the token to + * @param {string} sid The sid given in the response to requestToken + * @param {string} clientSecret A secret binary string generated by the client. + * This must be the same value submitted in the requestToken call. + * @param {string} msisdnToken The MSISDN token, as enetered by the user. + * + * @return {Promise} Resolves: Object, currently with no parameters. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public submitMsisdnTokenOtherUrl(url: string, sid: string, clientSecret: string, msisdnToken: string): Promise { // TODO: Types + const params = { + sid: sid, + client_secret: clientSecret, + token: msisdnToken, + }; + + return this.http.requestOtherUrl( + undefined, "POST", url, undefined, params, + ); + } + + /** + * Gets the V2 hashing information from the identity server. Primarily useful for + * lookups. + * @param {string} identityAccessToken The access token for the identity server. + * @returns {Promise} The hashing information for the identity server. + */ + public getIdentityHashDetails(identityAccessToken: string): Promise { // TODO: Types + return this.http.idServerRequest( + undefined, "GET", "/hash_details", + null, PREFIX_IDENTITY_V2, identityAccessToken, + ); + } + + /** + * Performs a hashed lookup of addresses against the identity server. This is + * only supported on identity servers which have at least the version 2 API. + * @param {Array>} addressPairs An array of 2 element arrays. + * The first element of each pair is the address, the second is the 3PID medium. + * Eg: ["email@example.org", "email"] + * @param {string} identityAccessToken The access token for the identity server. + * @returns {Promise>} A collection of address mappings to + * found MXIDs. Results where no user could be found will not be listed. + */ + public async identityHashedLookup(addressPairs: [string, string][], identityAccessToken: string): Promise<{address: string, mxid: string}[]> { + const params = { + // addresses: ["email@example.org", "10005550000"], + // algorithm: "sha256", + // pepper: "abc123" + }; + + // Get hash information first before trying to do a lookup + const hashes = await this.getIdentityHashDetails(identityAccessToken); + if (!hashes || !hashes['lookup_pepper'] || !hashes['algorithms']) { + throw new Error("Unsupported identity server: bad response"); + } + + params['pepper'] = hashes['lookup_pepper']; + + const localMapping = { + // hashed identifier => plain text address + // For use in this function's return format + }; + + // When picking an algorithm, we pick the hashed over no hashes + if (hashes['algorithms'].includes('sha256')) { + // Abuse the olm hashing + const olmutil = new global.Olm.Utility(); + params["addresses"] = addressPairs.map(p => { + const addr = p[0].toLowerCase(); // lowercase to get consistent hashes + const med = p[1].toLowerCase(); + const hashed = olmutil.sha256(`${addr} ${med} ${params['pepper']}`) + .replace(/\+/g, '-').replace(/\//g, '_'); // URL-safe base64 + // Map the hash to a known (case-sensitive) address. We use the case + // sensitive version because the caller might be expecting that. + localMapping[hashed] = p[0]; + return hashed; + }); + params["algorithm"] = "sha256"; + } else if (hashes['algorithms'].includes('none')) { + params["addresses"] = addressPairs.map(p => { + const addr = p[0].toLowerCase(); // lowercase to get consistent hashes + const med = p[1].toLowerCase(); + const unhashed = `${addr} ${med}`; + // Map the unhashed values to a known (case-sensitive) address. We use + // the case sensitive version because the caller might be expecting that. + localMapping[unhashed] = p[0]; + return unhashed; + }); + params["algorithm"] = "none"; + } else { + throw new Error("Unsupported identity server: unknown hash algorithm"); + } + + const response = await this.http.idServerRequest( + undefined, "POST", "/lookup", + params, PREFIX_IDENTITY_V2, identityAccessToken, + ); + + if (!response || !response['mappings']) return []; // no results + + const foundAddresses = [/* {address: "plain@example.org", mxid} */]; + for (const hashed of Object.keys(response['mappings'])) { + const mxid = response['mappings'][hashed]; + const plainAddress = localMapping[hashed]; + if (!plainAddress) { + throw new Error("Identity server returned more results than expected"); + } + + foundAddresses.push({ address: plainAddress, mxid }); + } + return foundAddresses; + } + + /** + * Looks up the public Matrix ID mapping for a given 3rd party + * identifier from the Identity Server + * + * @param {string} medium The medium of the threepid, eg. 'email' + * @param {string} address The textual address of the threepid + * @param {module:client.callback} callback Optional. + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: A threepid mapping + * object or the empty object if no mapping + * exists + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async lookupThreePid(medium: string, address: string, callback?: Callback, identityAccessToken?: string): Promise { // TODO: Types + // Note: we're using the V2 API by calling this function, but our + // function contract requires a V1 response. We therefore have to + // convert it manually. + const response = await this.identityHashedLookup( + [[address, medium]], identityAccessToken, + ); + const result = response.find(p => p.address === address); + if (!result) { + if (callback) callback(null, {}); + return {}; + } + + const mapping = { + address, + medium, + mxid: result.mxid, + + // We can't reasonably fill these parameters: + // not_before + // not_after + // ts + // signatures + }; + + if (callback) callback(null, mapping); + return mapping; + } + + /** + * Looks up the public Matrix ID mappings for multiple 3PIDs. + * + * @param {Array.>} query Array of arrays containing + * [medium, address] + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: Lookup results from IS. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public async bulkLookupThreePids(query: [string, string][], identityAccessToken: string): Promise { // TODO: Types + // Note: we're using the V2 API by calling this function, but our + // function contract requires a V1 response. We therefore have to + // convert it manually. + const response = await this.identityHashedLookup( + // We have to reverse the query order to get [address, medium] pairs + query.map(p => [p[1], p[0]]), identityAccessToken, + ); + + const v1results = []; + for (const mapping of response) { + const originalQuery = query.find(p => p[1] === mapping.address); + if (!originalQuery) { + throw new Error("Identity sever returned unexpected results"); + } + + v1results.push([ + originalQuery[0], // medium + mapping.address, + mapping.mxid, + ]); + } + + return { threepids: v1results }; + } + + /** + * Get account info from the Identity Server. This is useful as a neutral check + * to verify that other APIs are likely to approve access by testing that the + * token is valid, terms have been agreed, etc. + * + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: an object with account info. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getIdentityAccount(identityAccessToken: string): Promise { // TODO: Types + return this.http.idServerRequest( + undefined, "GET", "/account", + undefined, PREFIX_IDENTITY_V2, identityAccessToken, + ); + } + + /** + * Send an event to a specific list of devices + * + * @param {string} eventType type of event to send + * @param {Object.>} contentMap + * content to send. Map from user_id to device_id to content object. + * @param {string=} txnId transaction id. One will be made up if not + * supplied. + * @return {Promise} Resolves to the result object + */ + public sendToDevice(eventType: string, contentMap: any, txnId?: string): Promise { // TODO: Types + const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", { + $eventType: eventType, + $txnId: txnId ? txnId : this.makeTxnId(), + }); + + const body = { + messages: contentMap, + }; + + const targets = Object.keys(contentMap).reduce((obj, key) => { + obj[key] = Object.keys(contentMap[key]); + return obj; + }, {}); + logger.log(`PUT ${path}`, targets); + + return this.http.authedRequest(undefined, "PUT", path, undefined, body); + } + + /** + * Get the third party protocols that can be reached using + * this HS + * @return {Promise} Resolves to the result object + */ + public getThirdpartyProtocols(): Promise { // TODO: Types + return this.http.authedRequest( + undefined, "GET", "/thirdparty/protocols", undefined, undefined, + ).then((response) => { + // sanity check + if (!response || typeof(response) !== 'object') { + throw new Error( + `/thirdparty/protocols did not return an object: ${response}`, + ); + } + return response; + }); + } + + /** + * Get information on how a specific place on a third party protocol + * may be reached. + * @param {string} protocol The protocol given in getThirdpartyProtocols() + * @param {object} params Protocol-specific parameters, as given in the + * response to getThirdpartyProtocols() + * @return {Promise} Resolves to the result object + */ + public getThirdpartyLocation(protocol: string, params: any): Promise { // TODO: Types + const path = utils.encodeUri("/thirdparty/location/$protocol", { + $protocol: protocol, + }); + + return this.http.authedRequest(undefined, "GET", path, params, undefined); + } + + /** + * Get information on how a specific user on a third party protocol + * may be reached. + * @param {string} protocol The protocol given in getThirdpartyProtocols() + * @param {object} params Protocol-specific parameters, as given in the + * response to getThirdpartyProtocols() + * @return {Promise} Resolves to the result object + */ + public getThirdpartyUser(protocol: string, params: any): Promise { // TODO: Types + const path = utils.encodeUri("/thirdparty/user/$protocol", { + $protocol: protocol, + }); + + return this.http.authedRequest(undefined, "GET", path, params, undefined); + } + + public getTerms(serviceType: SERVICE_TYPES, baseUrl: string): Promise { // TODO: Types + const url = this.termsUrlForService(serviceType, baseUrl); + return this.http.requestOtherUrl( + undefined, 'GET', url, + ); + } + + public agreeToTerms(serviceType: SERVICE_TYPES, baseUrl: string, accessToken: string, termsUrls: string[]): Promise { // TODO: Types + const url = this.termsUrlForService(serviceType, baseUrl); + const headers = { + Authorization: "Bearer " + accessToken, + }; + return this.http.requestOtherUrl( + undefined, 'POST', url, null, { user_accepts: termsUrls }, { headers }, + ); + } + + /** + * Reports an event as inappropriate to the server, which may then notify the appropriate people. + * @param {string} roomId The room in which the event being reported is located. + * @param {string} eventId The event to report. + * @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive. + * @param {string} reason The reason the content is being reported. May be blank. + * @returns {Promise} Resolves to an empty object if successful + */ + public reportEvent(roomId: string, eventId: string, score: number, reason: string): Promise { // TODO: Types + const path = utils.encodeUri("/rooms/$roomId/report/$eventId", { + $roomId: roomId, + $eventId: eventId, + }); + + return this.http.authedRequest(undefined, "POST", path, null, { score, reason }); + } + + /** + * Fetches or paginates a summary of a space as defined by MSC2946 + * @param {string} roomId The ID of the space-room to use as the root of the summary. + * @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace. + * @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true. + * @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true. + * @param {number?} limit The maximum number of rooms to return in total. + * @param {string?} batch The opaque token to paginate a previous summary request. + * @returns {Promise} the response, with next_batch, rooms, events fields. + */ + public getSpaceSummary(roomId: string, maxRoomsPerSpace?: number, suggestedOnly?: boolean, autoJoinOnly?: boolean, limit?: number, batch?: string): Promise { // TODO: Types + const path = utils.encodeUri("/rooms/$roomId/spaces", { + $roomId: roomId, + }); + + return this.http.authedRequest(undefined, "POST", path, null, { + max_rooms_per_space: maxRoomsPerSpace, + suggested_only: suggestedOnly, + auto_join_only: autoJoinOnly, + limit, + batch, + }, { + prefix: "/_matrix/client/unstable/org.matrix.msc2946", + }); + } + + // TODO: @@TR: Remove warning, eventually + // ====================================================== + // ** ANCIENT APIS BELOW ** + // ====================================================== + + /** + * @param {string} groupId + * @return {Promise} Resolves: Group summary object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getGroupSummary(groupId: string): Promise { + const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId }); + return this.http.authedRequest(undefined, "GET", path); + } + + /** + * @param {string} groupId + * @return {Promise} Resolves: Group profile object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getGroupProfile(groupId: string): Promise { + const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); + return this.http.authedRequest(undefined, "GET", path); + } + + /** + * @param {string} groupId + * @param {Object} profile The group profile object + * @param {string=} profile.name Name of the group + * @param {string=} profile.avatar_url MXC avatar URL + * @param {string=} profile.short_description A short description of the room + * @param {string=} profile.long_description A longer HTML description of the room + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setGroupProfile(groupId: string, profile: any): Promise { + const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); + return this.http.authedRequest( + undefined, "POST", path, undefined, profile, + ); + } + + /** + * @param {string} groupId + * @param {object} policy The join policy for the group. Must include at + * least a 'type' field which is 'open' if anyone can join the group + * the group without prior approval, or 'invite' if an invite is + * required to join. + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setGroupJoinPolicy(groupId: string, policy: any): Promise { + const path = utils.encodeUri( + "/groups/$groupId/settings/m.join_policy", + { $groupId: groupId }, + ); + return this.http.authedRequest( + undefined, "PUT", path, undefined, { + 'm.join_policy': policy, + }, + ); + } + + /** + * @param {string} groupId + * @return {Promise} Resolves: Group users list object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getGroupUsers(groupId: string): Promise { + const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId }); + return this.http.authedRequest(undefined, "GET", path); + } + + /** + * @param {string} groupId + * @return {Promise} Resolves: Group users list object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getGroupInvitedUsers(groupId: string): Promise { + const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId }); + return this.http.authedRequest(undefined, "GET", path); + } + + /** + * @param {string} groupId + * @return {Promise} Resolves: Group rooms list object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getGroupRooms(groupId: string): Promise { + const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId }); + return this.http.authedRequest(undefined, "GET", path); + } + + /** + * @param {string} groupId + * @param {string} userId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public inviteUserToGroup(groupId: string, userId: string): Promise { + const path = utils.encodeUri( + "/groups/$groupId/admin/users/invite/$userId", + { $groupId: groupId, $userId: userId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, {}); + } + + /** + * @param {string} groupId + * @param {string} userId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public removeUserFromGroup(groupId: string, userId: string): Promise { + const path = utils.encodeUri( + "/groups/$groupId/admin/users/remove/$userId", + { $groupId: groupId, $userId: userId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, {}); + } + + /** + * @param {string} groupId + * @param {string} userId + * @param {string} roleId Optional. + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public addUserToGroupSummary(groupId: string, userId: string, roleId: string): Promise { + const path = utils.encodeUri( + roleId ? + "/groups/$groupId/summary/$roleId/users/$userId" : + "/groups/$groupId/summary/users/$userId", + { $groupId: groupId, $roleId: roleId, $userId: userId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, {}); + } + + /** + * @param {string} groupId + * @param {string} userId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public removeUserFromGroupSummary(groupId: string, userId: string): Promise { + const path = utils.encodeUri( + "/groups/$groupId/summary/users/$userId", + { $groupId: groupId, $userId: userId }, + ); + return this.http.authedRequest(undefined, "DELETE", path, undefined, {}); + } + + /** + * @param {string} groupId + * @param {string} roomId + * @param {string} categoryId Optional. + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public addRoomToGroupSummary(groupId: string, roomId: string, categoryId: string): Promise { + const path = utils.encodeUri( + categoryId ? + "/groups/$groupId/summary/$categoryId/rooms/$roomId" : + "/groups/$groupId/summary/rooms/$roomId", + { $groupId: groupId, $categoryId: categoryId, $roomId: roomId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, {}); + } + + /** + * @param {string} groupId + * @param {string} roomId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public removeRoomFromGroupSummary(groupId: string, roomId: string): Promise { + const path = utils.encodeUri( + "/groups/$groupId/summary/rooms/$roomId", + { $groupId: groupId, $roomId: roomId }, + ); + return this.http.authedRequest(undefined, "DELETE", path, undefined, {}); + } + + /** + * @param {string} groupId + * @param {string} roomId + * @param {bool} isPublic Whether the room-group association is visible to non-members + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public addRoomToGroup(groupId: string, roomId: string, isPublic: boolean): Promise { + if (isPublic === undefined) { + isPublic = true; + } + const path = utils.encodeUri( + "/groups/$groupId/admin/rooms/$roomId", + { $groupId: groupId, $roomId: roomId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, + { "m.visibility": { type: isPublic ? "public" : "private" } }, + ); + } + + /** + * Configure the visibility of a room-group association. + * @param {string} groupId + * @param {string} roomId + * @param {bool} isPublic Whether the room-group association is visible to non-members + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public updateGroupRoomVisibility(groupId: string, roomId: string, isPublic: boolean): Promise { + // NB: The /config API is generic but there's not much point in exposing this yet as synapse + // is the only server to implement this. In future we should consider an API that allows + // arbitrary configuration, i.e. "config/$configKey". + + const path = utils.encodeUri( + "/groups/$groupId/admin/rooms/$roomId/config/m.visibility", + { $groupId: groupId, $roomId: roomId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, + { type: isPublic ? "public" : "private" }, + ); + } + + /** + * @param {string} groupId + * @param {string} roomId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public removeRoomFromGroup(groupId: string, roomId: string): Promise { + const path = utils.encodeUri( + "/groups/$groupId/admin/rooms/$roomId", + { $groupId: groupId, $roomId: roomId }, + ); + return this.http.authedRequest(undefined, "DELETE", path, undefined, {}); + } + + /** + * @param {string} groupId + * @param {Object} opts Additional options to send alongside the acceptance. + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public acceptGroupInvite(groupId: string, opts = null): Promise { + const path = utils.encodeUri( + "/groups/$groupId/self/accept_invite", + { $groupId: groupId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, opts || {}); + } + + /** + * @param {string} groupId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public joinGroup(groupId: string): Promise { + const path = utils.encodeUri( + "/groups/$groupId/self/join", + { $groupId: groupId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, {}); + } + + /** + * @param {string} groupId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public leaveGroup(groupId: string): Promise { + const path = utils.encodeUri( + "/groups/$groupId/self/leave", + { $groupId: groupId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, {}); + } + + /** + * @return {Promise} Resolves: The groups to which the user is joined + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getJoinedGroups(): Promise { + const path = utils.encodeUri("/joined_groups", {}); + return this.http.authedRequest(undefined, "GET", path); + } + + /** + * @param {Object} content Request content + * @param {string} content.localpart The local part of the desired group ID + * @param {Object} content.profile Group profile object + * @return {Promise} Resolves: Object with key group_id: id of the created group + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public createGroup(content: any): Promise{ + const path = utils.encodeUri("/create_group", {}); + return this.http.authedRequest( + undefined, "POST", path, undefined, content, + ); + } + + /** + * @param {string[]} userIds List of user IDs + * @return {Promise} Resolves: Object as exmaple below + * + * { + * "users": { + * "@bob:example.com": { + * "+example:example.com" + * } + * } + * } + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public getPublicisedGroups(userIds: string[]): Promise { + const path = utils.encodeUri("/publicised_groups", {}); + return this.http.authedRequest( + undefined, "POST", path, undefined, { user_ids: userIds }, + ); + } + + /** + * @param {string} groupId + * @param {bool} isPublic Whether the user's membership of this group is made public + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + public setGroupPublicity(groupId: string, isPublic: boolean): Promise { + const path = utils.encodeUri( + "/groups/$groupId/self/update_publicity", + { $groupId: groupId }, + ); + return this.http.authedRequest(undefined, "PUT", path, undefined, { + publicise: isPublic, + }); + } +} + /** * Fires whenever the SDK receives a new event. diff --git a/src/@types/requests.ts b/src/@types/requests.ts index 5cd5ee744eb..c6fda86459f 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Callback } from "../1client"; + export interface IJoinRoomOpts { /** * True to do a room initial sync on the resulting @@ -65,3 +67,28 @@ export interface IEventSearchOpts { filter: any; // TODO: Types term: string; } + +export interface ICreateRoomOpts { + room_alias_name?: string; + visibility?: "public" | "private"; + name?: string; + topic?: string; + invite_3pid?: any[]; // TODO: Types +} + +export interface IRoomDirectoryOptions { + server?: string; + limit?: number; + since?: string; + filter?: any & {generic_search_term: string}; // TODO: Types +} + +export interface IUploadOpts { + name?: string; + includeFilename?: boolean; + type?: string; + rawResponse?: boolean; + onlyContentUri?: boolean; + callback?: Callback; + progressHandler?: (state: {loaded: number, total: number}) => void; +} From 4030ec9c8b67ac6207bcc0a17ca4cd3a78b44644 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 May 2021 22:42:18 -0600 Subject: [PATCH 05/32] Fix easy typing errors --- src/1client.ts | 74 ++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/1client.ts b/src/1client.ts index 1520adf6561..a6a4a57b7dc 100644 --- a/src/1client.ts +++ b/src/1client.ts @@ -112,6 +112,7 @@ export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | Remot export type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore; export type Callback = (err: Error | any | null, data?: any) => void; +export type ResetTimelineCallback = (roomId: string) => boolean; const SCROLLBACK_DELAY_MS = 3000; export const CRYPTO_ENABLED: boolean = isCryptoAvailable(); @@ -358,7 +359,7 @@ export interface IStartClientOpts { export interface IStoredClientOpts extends IStartClientOpts { crypto: Crypto; - canResetEntireTimeline: (roomId: string) => boolean; + canResetEntireTimeline: ResetTimelineCallback; } /** @@ -405,7 +406,7 @@ export class MatrixClient extends EventEmitter { private syncedLeftRooms = false; private clientOpts: IStoredClientOpts; private clientWellKnownIntervalID: number; - private canResetTimelineCallback: Callback; + private canResetTimelineCallback: ResetTimelineCallback; // The pushprocessor caches useful things, so keep one and re-use it private pushProcessor = new PushProcessor(this); @@ -485,7 +486,7 @@ export class MatrixClient extends EventEmitter { if (eventToSend.status !== EventStatus.SENDING) { this.updatePendingEventStatus(room, eventToSend, EventStatus.SENDING); } - const res = await sendEventHttpRequest(this, eventToSend); + const res = await this.sendEventHttpRequest(eventToSend); if (room) { // ensure we update pending event before the next scheduler run so that any listeners to event id // updates on the synchronous event emitter get a chance to run first. @@ -1229,7 +1230,7 @@ export class MatrixClient extends EventEmitter { * Download the keys for a list of users and stores the keys in the session * store. * @param {Array} userIds The users to fetch. - * @param {bool} forceDownload Always download the keys even if cached. + * @param {boolean} forceDownload Always download the keys even if cached. * * @return {Promise} A promise which resolves to a map userId->deviceId->{@link * module:crypto~DeviceInfo|DeviceInfo}. @@ -1590,7 +1591,7 @@ export class MatrixClient extends EventEmitter { * to fix things such that it returns true. That is to say, after * bootstrapCrossSigning() completes successfully, this function should * return true. - * @return {bool} True if cross-signing is ready to be used on this device + * @return {boolean} True if cross-signing is ready to be used on this device */ public isCrossSigningReady(): boolean { if (!this.crypto) { @@ -1612,7 +1613,7 @@ export class MatrixClient extends EventEmitter { * * @param {function} opts.authUploadDeviceSigningKeys Function * called to await an interactive auth flow when uploading device signing keys. - * @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys + * @param {boolean} [opts.setupNewCrossSigning] Optional. Reset even if keys * already exist. * Args: * {function} A function that makes the request requiring auth. Receives the @@ -1635,7 +1636,7 @@ export class MatrixClient extends EventEmitter { * * Default: true * - * @return {bool} True if trusting cross-signed devices + * @return {boolean} True if trusting cross-signed devices */ public getCryptoTrustCrossSignedDevices() : boolean { if (!this.crypto) { @@ -1649,7 +1650,7 @@ export class MatrixClient extends EventEmitter { * This may be set before initCrypto() is called to ensure no races occur. * - * @param {bool} val True to trust cross-signed devices + * @param {boolean} val True to trust cross-signed devices */ public setCryptoTrustCrossSignedDevices(val: boolean) { if (!this.crypto) { @@ -1714,7 +1715,7 @@ export class MatrixClient extends EventEmitter { * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @return {bool} True if secret storage is ready to be used on this device + * @return {boolean} True if secret storage is ready to be used on this device */ public isSecretStorageReady(): boolean { if (!this.crypto) { @@ -1956,7 +1957,7 @@ export class MatrixClient extends EventEmitter { /** * Whether encryption is enabled for a room. * @param {string} roomId the room id to query. - * @return {bool} whether encryption is enabled. + * @return {boolean} whether encryption is enabled. */ public isRoomEncrypted(roomId: string): boolean { const room = this.getRoom(roomId); @@ -2082,7 +2083,7 @@ export class MatrixClient extends EventEmitter { } /** - * @returns {bool} true if the client is configured to back up keys to + * @returns {boolean} true if the client is configured to back up keys to * the server, otherwise false. If we haven't completed a successful check * of key backup status yet, returns null. */ @@ -2835,7 +2836,8 @@ export class MatrixClient extends EventEmitter { try { const data: any = {}; - const signedInviteObj = await signPromise; + // XXX: Explicit cast due to underlying types not existing + const signedInviteObj = >(await signPromise); if (signedInviteObj) { data['third_party_signed'] = signedInviteObj; } @@ -2866,9 +2868,9 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public resendEvent(event: MatrixEvent, room: Room): Promise { + public resendEvent(event: MatrixEvent, room: Room): Promise { // TODO: Types this.updatePendingEventStatus(room, event, EventStatus.SENDING); - return this.sendEvent(room, event); + return this.encryptAndSendEvent(room, event); } /** @@ -3505,7 +3507,7 @@ export class MatrixClient extends EventEmitter { * This property is unstable and may change in the future. * @return {Promise} Resolves: the empty object, {}. */ - public async setRoomReadMarkers(roomId: string, rmEventId: string, rrEvent: MatrixEvent, opts: {hidden?: boolean}): Promise { + public async setRoomReadMarkers(roomId: string, rmEventId: string, rrEvent: MatrixEvent, opts: {hidden?: boolean}): Promise { // TODO: Types const room = this.getRoom(roomId); if (room && room.hasPendingEvent(rmEventId)) { throw new Error(`Cannot set read marker to a pending event (${rmEventId})`); @@ -4124,7 +4126,7 @@ export class MatrixClient extends EventEmitter { room.oldState.paginationToken, limit, 'b'); - }).then((res) => { + }).then((res: any) => { // TODO: Types const matrixEvents = res.chunk.map(this.getEventMapper()); if (res.state) { const stateEvents = res.state.map(this.getEventMapper()); @@ -4159,8 +4161,8 @@ export class MatrixClient extends EventEmitter { /** * @param {object} [options] - * @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client - * @param {bool} options.decrypt decrypt event proactively + * @param {boolean} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client + * @param {boolean} options.decrypt decrypt event proactively * @return {Function} */ public getEventMapper(options?: MapperOpts): EventMapper { @@ -4297,7 +4299,7 @@ export class MatrixClient extends EventEmitter { * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline * object to be updated * @param {Object} [opts] - * @param {bool} [opts.backwards = false] true to fill backwards, + * @param {boolean} [opts.backwards = false] true to fill backwards, * false to go forwards * @param {number} [opts.limit = 30] number of events to request * @@ -4305,7 +4307,7 @@ export class MatrixClient extends EventEmitter { * events and we reached either end of the timeline; else true. */ public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise { - const isNotifTimeline = (eventTimeline.getTimelineSet() === this._notifTimelineSet); + const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet); // TODO: we should implement a backoff (as per scrollback()) to deal more // nicely with HTTP errors. @@ -4492,13 +4494,13 @@ export class MatrixClient extends EventEmitter { public setGuestAccess(roomId: string, opts: IGuestAccessOpts): Promise { const writePromise = this.sendStateEvent(roomId, "m.room.guest_access", { guest_access: opts.allowJoin ? "can_join" : "forbidden", - }); + }, ""); let readPromise = Promise.resolve(); if (opts.allowRead) { readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", { history_visibility: "world_readable", - }); + }, ""); } return Promise.all([readPromise, writePromise]).then(); // .then() to hide results for contract @@ -5466,7 +5468,7 @@ export class MatrixClient extends EventEmitter { * Default: returns false. * @param {Function} cb The callback which will be invoked. */ - public setCanResetTimelineCallback(cb: Callback) { + public setCanResetTimelineCallback(cb: ResetTimelineCallback) { this.canResetTimelineCallback = cb; } @@ -5474,7 +5476,7 @@ export class MatrixClient extends EventEmitter { * Get the callback set via `setCanResetTimelineCallback`. * @return {?Function} The callback or null */ - public getCanResetTimelineCallback(): Callback { + public getCanResetTimelineCallback(): ResetTimelineCallback { return this.canResetTimelineCallback; } @@ -5545,10 +5547,10 @@ export class MatrixClient extends EventEmitter { * @param {MatrixEvent} event The event to decrypt * @returns {Promise} A decryption promise * @param {object} options - * @param {bool} options.isRetry True if this is a retry (enables more logging) - * @param {bool} options.emit Emits "event.decrypted" if set to true + * @param {boolean} options.isRetry True if this is a retry (enables more logging) + * @param {boolean} options.emit Emits "event.decrypted" if set to true */ - public decryptEventIfNeeded(event: MatrixEvent, options: {emit: boolean, isRetry: boolean}): Promise { + public decryptEventIfNeeded(event: MatrixEvent, options?: {emit: boolean, isRetry: boolean}): Promise { if (event.shouldAttemptDecryption()) { event.attemptDecryption(this.crypto, options); } @@ -6146,7 +6148,7 @@ export class MatrixClient extends EventEmitter { * property is currently unstable and may change in the future. * @return {Promise} Resolves: the empty object, {}. */ - public setRoomReadMarkersHttpRequest(roomId: string, rmEventId: string, rrEventId: string, opts: {hidden: boolean}): Promise<{}> { + public setRoomReadMarkersHttpRequest(roomId: string, rmEventId: string, rrEventId: string, opts: {hidden?: boolean}): Promise<{}> { const path = utils.encodeUri("/rooms/$roomId/read_markers", { $roomId: roomId, }); @@ -6727,7 +6729,7 @@ export class MatrixClient extends EventEmitter { * @return {module:http-api.MatrixError} Rejects: with an error response. */ public getPushRules(callback?: Callback): Promise { // TODO: Types - return this._http.authedRequest(callback, "GET", "/pushrules/").then(rules => { + return this.http.authedRequest(callback, "GET", "/pushrules/").then(rules => { return PushProcessor.rewriteDefaultRules(rules); }); } @@ -6818,7 +6820,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public search(opts: {body: any, next_batch: string}, callback?: Callback): Promise { // TODO: Types + public search(opts: {body: any, next_batch?: string}, callback?: Callback): Promise { // TODO: Types const queryParams: any = {}; if (opts.next_batch) { queryParams.next_batch = opts.next_batch; @@ -7639,7 +7641,7 @@ export class MatrixClient extends EventEmitter { /** * @param {string} groupId * @param {string} roomId - * @param {bool} isPublic Whether the room-group association is visible to non-members + * @param {boolean} isPublic Whether the room-group association is visible to non-members * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -7660,7 +7662,7 @@ export class MatrixClient extends EventEmitter { * Configure the visibility of a room-group association. * @param {string} groupId * @param {string} roomId - * @param {bool} isPublic Whether the room-group association is visible to non-members + * @param {boolean} isPublic Whether the room-group association is visible to non-members * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -7777,7 +7779,7 @@ export class MatrixClient extends EventEmitter { /** * @param {string} groupId - * @param {bool} isPublic Whether the user's membership of this group is made public + * @param {boolean} isPublic Whether the user's membership of this group is made public * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -8058,7 +8060,7 @@ export class MatrixClient extends EventEmitter { * Fires whenever the stored devices for a user have changed * @event module:client~MatrixClient#"crypto.devicesUpdated" * @param {String[]} users A list of user IDs that were updated - * @param {bool} initialFetch If true, the store was empty (apart + * @param {boolean} initialFetch If true, the store was empty (apart * from our own device) and has been seeded. */ @@ -8066,14 +8068,14 @@ export class MatrixClient extends EventEmitter { * Fires whenever the stored devices for a user will be updated * @event module:client~MatrixClient#"crypto.willUpdateDevices" * @param {String[]} users A list of user IDs that will be updated - * @param {bool} initialFetch If true, the store is empty (apart + * @param {boolean} initialFetch If true, the store is empty (apart * from our own device) and is being seeded. */ /** * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() * @event module:client~MatrixClient#"crypto.keyBackupStatus" - * @param {bool} enabled true if key backup has been enabled, otherwise false + * @param {boolean} enabled true if key backup has been enabled, otherwise false * @example * matrixClient.on("crypto.keyBackupStatus", function(enabled){ * if (enabled) { From 92e18b32dcb9621516a2be58cc1e09cab01d7dc8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 May 2021 22:42:48 -0600 Subject: [PATCH 06/32] Import MatrixError --- src/1client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/1client.ts b/src/1client.ts index a6a4a57b7dc..4378d46c15e 100644 --- a/src/1client.ts +++ b/src/1client.ts @@ -32,6 +32,7 @@ import { Group } from "./models/group"; import { EventTimeline } from "./models/event-timeline"; import { PushAction, PushProcessor } from "./pushprocessor"; import { AutoDiscovery } from "./autodiscovery"; +import { MatrixError } from "./http-api"; import * as olmlib from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; import { ReEmitter } from './ReEmitter'; From f3b27d1e06a6234804536b76cf477b7176cb92df Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 May 2021 22:43:20 -0600 Subject: [PATCH 07/32] Cleanup --- src/1client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/1client.ts b/src/1client.ts index 4378d46c15e..bb560b7345d 100644 --- a/src/1client.ts +++ b/src/1client.ts @@ -386,7 +386,6 @@ export class MatrixClient extends EventEmitter { private canSupportVoip = false; private callEventHandler: CallEventHandler; - private syncingRetry = null; // TODO: @@TR private peekSync: SyncApi = null; private isGuestAccount = false; private ongoingScrollbacks = {}; // TODO: @@TR From f027ddaf35f17fe2d188d767ae0094686b8b1bea Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 May 2021 22:45:32 -0600 Subject: [PATCH 08/32] Autoformat --- src/1client.ts | 91 +++++++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/src/1client.ts b/src/1client.ts index bb560b7345d..81c05546026 100644 --- a/src/1client.ts +++ b/src/1client.ts @@ -376,12 +376,12 @@ export class MatrixClient extends EventEmitter { public usingExternalCrypto = false; public store: Store; public deviceId?: string; - public credentials: {userId?: string}; + public credentials: { userId?: string }; public pickleKey: string; public scheduler: MatrixScheduler; public clientRunning = false; public timelineSupport = false; - public urlPreviewCache: {[key: string]: Promise} = {}; // TODO: @@TR + public urlPreviewCache: { [key: string]: Promise } = {}; // TODO: @@TR public unstableClientRelationAggregation = false; private canSupportVoip = false; @@ -442,7 +442,7 @@ export class MatrixClient extends EventEmitter { this.deviceId = opts.deviceId || null; const userId = opts.userId || null; - this.credentials = {userId}; + this.credentials = { userId }; this.http = new MatrixHttpApi(this, { baseUrl: opts.baseUrl, @@ -1629,6 +1629,7 @@ export class MatrixClient extends EventEmitter { } return this.crypto.bootstrapCrossSigning(opts); } + /** * Whether to trust a others users signatures of their devices. * If false, devices will only be considered 'verified' if we have @@ -1638,7 +1639,7 @@ export class MatrixClient extends EventEmitter { * * @return {boolean} True if trusting cross-signed devices */ - public getCryptoTrustCrossSignedDevices() : boolean { + public getCryptoTrustCrossSignedDevices(): boolean { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1776,7 +1777,7 @@ export class MatrixClient extends EventEmitter { * for. Defaults to the default key ID if not provided. * @return {boolean} Whether we have the key. */ - public hasSecretStorageKey(keyId?:string): boolean { + public hasSecretStorageKey(keyId?: string): boolean { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2151,7 +2152,7 @@ export class MatrixClient extends EventEmitter { * additionally has a 'recovery_key' member with the user-facing recovery key string. */ // TODO: Verify types - public async prepareKeyBackupVersion(password: string, opts: IKeyBackupPrepareOpts = {secureSecretStorage: false}): Promise { + public async prepareKeyBackupVersion(password: string, opts: IKeyBackupPrepareOpts = { secureSecretStorage: false }): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2264,7 +2265,7 @@ export class MatrixClient extends EventEmitter { ); } - private makeKeyBackupPath(roomId: string, sessionId: string, version: string): {path: string, queryData: any} { + private makeKeyBackupPath(roomId: string, sessionId: string, version: string): { path: string, queryData: any } { let path; if (sessionId !== undefined) { path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { @@ -2454,7 +2455,7 @@ export class MatrixClient extends EventEmitter { } private restoreKeyBackup(privKey: Uint8Array, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, opts: IKeyBackupRestoreOpts): Promise { - const {cacheCompleteCallback, progressCallback} = opts; + const { cacheCompleteCallback, progressCallback } = opts; if (!this.crypto) { throw new Error("End-to-end encryption disabled"); @@ -2842,7 +2843,7 @@ export class MatrixClient extends EventEmitter { data['third_party_signed'] = signedInviteObj; } - const path = utils.encodeUri("/join/$roomid", {$roomid: roomIdOrAlias}); + const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias }); const res = await this.http.authedRequest(undefined, "POST", path, queryString, data, reqOpts); const roomId = res['room_id']; @@ -3027,7 +3028,7 @@ export class MatrixClient extends EventEmitter { * @return {module:http-api.MatrixError} Rejects: with an error response. */ public sendEvent(roomId: string, eventType: string, content: any, txnId?: string, callback?: Callback): Promise { - return this.sendCompleteEvent(roomId, {type: eventType, content}, txnId, callback); + return this.sendCompleteEvent(roomId, { type: eventType, content }, txnId, callback); } /** @@ -3277,9 +3278,9 @@ export class MatrixClient extends EventEmitter { * @return {module:http-api.MatrixError} Rejects: with an error response. */ public redactEvent(roomId: string, eventId: string, txnId?: string, cbOrOpts?: Callback | IRedactOpts): Promise { - const opts = typeof(cbOrOpts) === 'object' ? cbOrOpts : {}; + const opts = typeof (cbOrOpts) === 'object' ? cbOrOpts : {}; const reason = opts.reason; - const callback = typeof(cbOrOpts) === 'function' ? cbOrOpts : undefined; + const callback = typeof (cbOrOpts) === 'function' ? cbOrOpts : undefined; return this.sendCompleteEvent(roomId, { type: EventType.RoomRedaction, content: { reason: reason }, @@ -3436,7 +3437,7 @@ export class MatrixClient extends EventEmitter { * @return {module:http-api.MatrixError} Rejects: with an error response. */ public sendReceipt(event: MatrixEvent, receiptType: string, body: any, callback?: Callback): Promise { - if (typeof(body) === 'function') { + if (typeof (body) === 'function') { callback = body as any as Callback; // legacy body = {}; } @@ -3472,8 +3473,8 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async sendReadReceipt(event: MatrixEvent, opts: {hidden?: boolean}, callback?: Callback): Promise { - if (typeof(opts) === 'function') { + public async sendReadReceipt(event: MatrixEvent, opts: { hidden?: boolean }, callback?: Callback): Promise { + if (typeof (opts) === 'function') { callback = opts as any as Callback; // legacy opts = {}; } @@ -3507,7 +3508,7 @@ export class MatrixClient extends EventEmitter { * This property is unstable and may change in the future. * @return {Promise} Resolves: the empty object, {}. */ - public async setRoomReadMarkers(roomId: string, rmEventId: string, rrEvent: MatrixEvent, opts: {hidden?: boolean}): Promise { // TODO: Types + public async setRoomReadMarkers(roomId: string, rmEventId: string, rrEvent: MatrixEvent, opts: { hidden?: boolean }): Promise { // TODO: Types const room = this.getRoom(roomId); if (room && room.hasPendingEvent(rmEventId)) { throw new Error(`Cannot set read marker to a pending event (${rmEventId})`); @@ -3768,7 +3769,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves when completed with an object keyed * by room ID and value of the error encountered when leaving or null. */ - public leaveRoomChain(roomId: string, includeFuture = true): Promise<{[roomId: string]: Error | null}> { + public leaveRoomChain(roomId: string, includeFuture = true): Promise<{ [roomId: string]: Error | null }> { const upgradeHistory = this.getRoomUpgradeHistory(roomId); let eligibleToLeave = upgradeHistory; @@ -3831,7 +3832,7 @@ export class MatrixClient extends EventEmitter { return promise; } const self = this; - return promise.then(function(response) { + return promise.then((response) => { self.store.removeRoom(roomId); self.emit("deleteRoom", roomId); return response; @@ -4204,7 +4205,7 @@ export class MatrixClient extends EventEmitter { let params = undefined; if (this.clientOpts.lazyLoadMembers) { - params = {filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)}; + params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) }; } // TODO: we should implement a backoff (as per scrollback()) to deal more @@ -4353,7 +4354,7 @@ export class MatrixClient extends EventEmitter { promise = this.http.authedRequest( undefined, "GET", path, params, undefined, - ).then(function(res) { + ).then((res) => { const token = res.next_token; const matrixEvents = []; @@ -4377,7 +4378,7 @@ export class MatrixClient extends EventEmitter { eventTimeline.setPaginationToken(null, dir); } return res.next_token ? true : false; - }).finally(function() { + }).finally(() => { eventTimeline._paginationRequests[dir] = null; }); eventTimeline._paginationRequests[dir] = promise; @@ -4393,7 +4394,7 @@ export class MatrixClient extends EventEmitter { opts.limit, dir, eventTimeline.getFilter()); - promise.then(function(res) { + promise.then((res) => { if (res.state) { const roomState = eventTimeline.getState(dir); const stateEvents = res.state.map(self.getEventMapper()); @@ -4411,7 +4412,7 @@ export class MatrixClient extends EventEmitter { eventTimeline.setPaginationToken(null, dir); } return res.end != res.start; - }).finally(function() { + }).finally(() => { eventTimeline._paginationRequests[dir] = null; }); eventTimeline._paginationRequests[dir] = promise; @@ -4778,8 +4779,8 @@ export class MatrixClient extends EventEmitter { deferred.reject(err); }); }).catch((err) => { - deferred.reject(err); - }); + deferred.reject(err); + }); deferred = deferred.promise; } @@ -4909,7 +4910,7 @@ export class MatrixClient extends EventEmitter { // TODO: @@TR: wtf const promise = this.search(searchOpts).then( this.processRoomEventsSearch.bind(this, searchResults), - ).finally(function() { + ).finally(() => { searchResults.pendingRequest = null; }); searchResults.pendingRequest = promise; @@ -5492,7 +5493,7 @@ export class MatrixClient extends EventEmitter { * @param {Object} opts.from the pagination token returned from a previous request as `nextBatch` to return following relations. * @return {Object} an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available. */ - public async relations(roomId: string, eventId: string, relationType: string, eventType: string, opts: {from: string}): Promise<{originalEvent: MatrixEvent, events: MatrixEvent[], nextBatch?: string}> { + public async relations(roomId: string, eventId: string, relationType: string, eventType: string, opts: { from: string }): Promise<{ originalEvent: MatrixEvent, events: MatrixEvent[], nextBatch?: string }> { const fetchedEventType = this.getEncryptedIfNeededEventType(roomId, eventType); const result = await this.fetchRelations( roomId, @@ -5550,7 +5551,7 @@ export class MatrixClient extends EventEmitter { * @param {boolean} options.isRetry True if this is a retry (enables more logging) * @param {boolean} options.emit Emits "event.decrypted" if set to true */ - public decryptEventIfNeeded(event: MatrixEvent, options?: {emit: boolean, isRetry: boolean}): Promise { + public decryptEventIfNeeded(event: MatrixEvent, options?: { emit: boolean, isRetry: boolean }): Promise { if (event.shouldAttemptDecryption()) { event.attemptDecryption(this.crypto, options); } @@ -5728,7 +5729,7 @@ export class MatrixClient extends EventEmitter { * { user_id, device_id, access_token, home_server } * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public registerGuest(opts: {body?: any}, callback?: Callback): Promise { // TODO: Types + public registerGuest(opts: { body?: any }, callback?: Callback): Promise { // TODO: Types opts = opts || {}; opts.body = opts.body || {}; return this.registerRequest(opts.body, "guest", callback); @@ -5884,7 +5885,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: On success, the empty object */ public deactivateAccount(auth?: any, erase?: boolean): Promise<{}> { - if (typeof(erase) === 'function') { + if (typeof (erase) === 'function') { throw new Error( 'deactivateAccount no longer accepts a callback parameter', ); @@ -5935,7 +5936,7 @@ export class MatrixClient extends EventEmitter { * room_alias: {string(opt)}} * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async createRoom(options: ICreateRoomOpts, callback?: Callback): Promise<{roomId: string, room_alias?: string}> { + public async createRoom(options: ICreateRoomOpts, callback?: Callback): Promise<{ roomId: string, room_alias?: string }> { // some valid options include: room_alias_name, visibility, invite // inject the id_access_token if inviting 3rd party addresses @@ -5970,7 +5971,7 @@ export class MatrixClient extends EventEmitter { * @param {Object} opts.from the pagination token returned from a previous request as `next_batch` to return following relations. * @return {Object} the response, with chunk and next_batch. */ - public async fetchRelations(roomId: string, eventId: string, relationType: string, eventType: string, opts: {from: string}): Promise { // TODO: Types + public async fetchRelations(roomId: string, eventId: string, relationType: string, eventType: string, opts: { from: string }): Promise { // TODO: Types const queryParams: any = {}; if (opts.from) { queryParams.from = opts.from; @@ -6029,7 +6030,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: dictionary of userid to profile information * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public members(roomId: string, includeMembership?: string[], excludeMembership?: string[], atEventId?: string, callback?: Callback): Promise<{[userId: string]: any}> { + public members(roomId: string, includeMembership?: string[], excludeMembership?: string[], atEventId?: string, callback?: Callback): Promise<{ [userId: string]: any }> { const queryParams: any = {}; if (includeMembership) { queryParams.membership = includeMembership; @@ -6055,7 +6056,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: Object with key 'replacement_room' * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public upgradeRoom(roomId: string, newVersion: string): Promise<{replacement_room: string}> { + public upgradeRoom(roomId: string, newVersion: string): Promise<{ replacement_room: string }> { const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId }); return this.http.authedRequest( undefined, "POST", path, undefined, { new_version: newVersion }, @@ -6148,7 +6149,7 @@ export class MatrixClient extends EventEmitter { * property is currently unstable and may change in the future. * @return {Promise} Resolves: the empty object, {}. */ - public setRoomReadMarkersHttpRequest(roomId: string, rmEventId: string, rrEventId: string, opts: {hidden?: boolean}): Promise<{}> { + public setRoomReadMarkersHttpRequest(roomId: string, rmEventId: string, rrEventId: string, opts: { hidden?: boolean }): Promise<{}> { const path = utils.encodeUri("/rooms/$roomId/read_markers", { $roomId: roomId, }); @@ -6201,7 +6202,7 @@ export class MatrixClient extends EventEmitter { * @return {module:http-api.MatrixError} Rejects: with an error response. */ public publicRooms(options: IRoomDirectoryOptions, callback?: Callback): Promise { // TODO: Types - if (typeof(options) == 'function') { + if (typeof (options) == 'function') { callback = options; options = {}; } @@ -6267,7 +6268,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: an object with an `aliases` property, containing an array of local aliases * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public unstableGetLocalAliases(roomId: string, callback?: Callback): Promise<{aliases: string[]}> { + public unstableGetLocalAliases(roomId: string, callback?: Callback): Promise<{ aliases: string[] }> { const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId }); const prefix = PREFIX_UNSTABLE + "/org.matrix.msc2432"; @@ -6282,7 +6283,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: Object with room_id and servers. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public getRoomIdForAlias(alias: string, callback?: Callback): Promise<{room_id: string, servers: string[]}> { + public getRoomIdForAlias(alias: string, callback?: Callback): Promise<{ room_id: string, servers: string[] }> { // TODO: deprecate this or resolveRoomAlias const path = utils.encodeUri("/directory/room/$alias", { $alias: alias, @@ -6368,7 +6369,7 @@ export class MatrixClient extends EventEmitter { * apply a limit if unspecified. * @return {Promise} Resolves: an array of results. */ - public searchUserDirectory(opts: {term: string, limit?: number}): Promise { // TODO: Types + public searchUserDirectory(opts: { term: string, limit?: number }): Promise { // TODO: Types const body: any = { search_term: opts.term, }; @@ -6443,7 +6444,7 @@ export class MatrixClient extends EventEmitter { * - loaded: Number of bytes uploaded * - total: Total number of bytes to upload */ - public getCurrentUploads(): {promise: Promise, loaded: number, total: number}[] { // TODO: Advanced types (promise) + public getCurrentUploads(): { promise: Promise, loaded: number, total: number }[] { // TODO: Advanced types (promise) return this.http.getCurrentUploads(); } @@ -6820,7 +6821,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public search(opts: {body: any, next_batch?: string}, callback?: Callback): Promise { // TODO: Types + public search(opts: { body: any, next_batch?: string }, callback?: Callback): Promise { // TODO: Types const queryParams: any = {}; if (opts.next_batch) { queryParams.next_batch = opts.next_batch; @@ -6869,7 +6870,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: result object. Rejects: with * an error response ({@link module:http-api.MatrixError}). */ - public downloadKeysForUsers(userIds: string[], opts: {token?: string}): Promise { // TODO: Types + public downloadKeysForUsers(userIds: string[], opts: { token?: string }): Promise { // TODO: Types if (utils.isFunction(opts)) { // opts used to be 'callback'. throw new Error( @@ -7146,7 +7147,7 @@ export class MatrixClient extends EventEmitter { * @returns {Promise>} A collection of address mappings to * found MXIDs. Results where no user could be found will not be listed. */ - public async identityHashedLookup(addressPairs: [string, string][], identityAccessToken: string): Promise<{address: string, mxid: string}[]> { + public async identityHashedLookup(addressPairs: [string, string][], identityAccessToken: string): Promise<{ address: string, mxid: string }[]> { const params = { // addresses: ["email@example.org", "10005550000"], // algorithm: "sha256", @@ -7354,7 +7355,7 @@ export class MatrixClient extends EventEmitter { undefined, "GET", "/thirdparty/protocols", undefined, undefined, ).then((response) => { // sanity check - if (!response || typeof(response) !== 'object') { + if (!response || typeof (response) !== 'object') { throw new Error( `/thirdparty/protocols did not return an object: ${response}`, ); @@ -7750,7 +7751,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: Object with key group_id: id of the created group * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public createGroup(content: any): Promise{ + public createGroup(content: any): Promise { const path = utils.encodeUri("/create_group", {}); return this.http.authedRequest( undefined, "POST", path, undefined, content, From 67994f7a539ac7035d997bf21de48e245608b862 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 May 2021 22:46:25 -0600 Subject: [PATCH 09/32] Move new MatrixClient into place --- src/@types/requests.ts | 2 +- src/base-apis.js | 2427 --------------------------------- src/{1client.ts => client.ts} | 0 src/event-mapper.ts | 2 +- src/matrix.ts | 2 +- 5 files changed, 3 insertions(+), 2430 deletions(-) delete mode 100644 src/base-apis.js rename src/{1client.ts => client.ts} (100%) diff --git a/src/@types/requests.ts b/src/@types/requests.ts index c6fda86459f..6f6795eea6e 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Callback } from "../1client"; +import { Callback } from "../client"; export interface IJoinRoomOpts { /** diff --git a/src/base-apis.js b/src/base-apis.js deleted file mode 100644 index 25649f23dc8..00000000000 --- a/src/base-apis.js +++ /dev/null @@ -1,2427 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/** - * This is an internal module. MatrixBaseApis is currently only meant to be used - * by {@link client~MatrixClient}. - * - * @module base-apis - */ - -import { SERVICE_TYPES } from './service-types'; -import { logger } from './logger'; -import { PushProcessor } from "./pushprocessor"; -import * as utils from "./utils"; -import { MatrixHttpApi, PREFIX_IDENTITY_V2, PREFIX_R0, PREFIX_UNSTABLE } from "./http-api"; - -function termsUrlForService(serviceType, baseUrl) { - switch (serviceType) { - case SERVICE_TYPES.IS: - return baseUrl + PREFIX_IDENTITY_V2 + '/terms'; - case SERVICE_TYPES.IM: - return baseUrl + '/_matrix/integrations/v1/terms'; - default: - throw new Error('Unsupported service type'); - } -} - -/** - * Low-level wrappers for the Matrix APIs - * - * @constructor - * - * @param {Object} opts Configuration options - * - * @param {string} opts.baseUrl Required. The base URL to the client-server - * HTTP API. - * - * @param {string} opts.idBaseUrl Optional. The base identity server URL for - * identity server requests. - * - * @param {Function} opts.request Required. The function to invoke for HTTP - * requests. The value of this property is typically require("request") - * as it returns a function which meets the required interface. See - * {@link requestFunction} for more information. - * - * @param {string} opts.accessToken The access_token for this user. - * - * @param {IdentityServerProvider} [opts.identityServer] - * Optional. A provider object with one function `getAccessToken`, which is a - * callback that returns a Promise of an identity access token to supply - * with identity requests. If the object is unset, no access token will be - * supplied. - * See also https://github.com/vector-im/element-web/issues/10615 which seeks to - * replace the previous approach of manual access tokens params with this - * callback throughout the SDK. - * - * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of - * time to wait before timing out HTTP requests. If not specified, there is no - * timeout. - * - * @param {Object} opts.queryParams Optional. Extra query parameters to append - * to all requests with this client. Useful for application services which require - * ?user_id=. - * - * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use - * Authorization header instead of query param to send the access token to the server. - */ -export function MatrixBaseApis(opts) { - utils.checkObjectHasKeys(opts, ["baseUrl", "request"]); - - this.baseUrl = opts.baseUrl; - this.idBaseUrl = opts.idBaseUrl; - this.identityServer = opts.identityServer; - - const httpOpts = { - baseUrl: opts.baseUrl, - idBaseUrl: opts.idBaseUrl, - accessToken: opts.accessToken, - request: opts.request, - prefix: PREFIX_R0, - onlyData: true, - extraParams: opts.queryParams, - localTimeoutMs: opts.localTimeoutMs, - useAuthorizationHeader: opts.useAuthorizationHeader, - }; - this._http = new MatrixHttpApi(this, httpOpts); - - this._txnCtr = 0; -} - -/** - * Get the Homeserver URL of this client - * @return {string} Homeserver URL of this client - */ -MatrixBaseApis.prototype.getHomeserverUrl = function() { - return this.baseUrl; -}; - -/** - * Get the Identity Server URL of this client - * @param {boolean} stripProto whether or not to strip the protocol from the URL - * @return {string} Identity Server URL of this client - */ -MatrixBaseApis.prototype.getIdentityServerUrl = function(stripProto=false) { - if (stripProto && (this.idBaseUrl.startsWith("http://") || - this.idBaseUrl.startsWith("https://"))) { - return this.idBaseUrl.split("://")[1]; - } - return this.idBaseUrl; -}; - -/** - * Set the Identity Server URL of this client - * @param {string} url New Identity Server URL - */ -MatrixBaseApis.prototype.setIdentityServerUrl = function(url) { - this.idBaseUrl = utils.ensureNoTrailingSlash(url); - this._http.setIdBaseUrl(this.idBaseUrl); -}; - -/** - * Get the access token associated with this account. - * @return {?String} The access_token or null - */ -MatrixBaseApis.prototype.getAccessToken = function() { - return this._http.opts.accessToken || null; -}; - -/** - * @return {boolean} true if there is a valid access_token for this client. - */ -MatrixBaseApis.prototype.isLoggedIn = function() { - return this._http.opts.accessToken !== undefined; -}; - -/** - * Make up a new transaction id - * - * @return {string} a new, unique, transaction id - */ -MatrixBaseApis.prototype.makeTxnId = function() { - return "m" + new Date().getTime() + "." + (this._txnCtr++); -}; - -// Registration/Login operations -// ============================= - -/** - * Check whether a username is available prior to registration. An error response - * indicates an invalid/unavailable username. - * @param {string} username The username to check the availability of. - * @return {Promise} Resolves: to `true`. - */ -MatrixBaseApis.prototype.isUsernameAvailable = function(username) { - return this._http.authedRequest( - undefined, "GET", '/register/available', { username: username }, - ).then((response) => { - return response.available; - }); -}; - -/** - * @param {string} username - * @param {string} password - * @param {string} sessionId - * @param {Object} auth - * @param {Object} bindThreepids Set key 'email' to true to bind any email - * threepid uses during registration in the ID server. Set 'msisdn' to - * true to bind msisdn. - * @param {string} guestAccessToken - * @param {string} inhibitLogin - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.register = function( - username, password, - sessionId, auth, bindThreepids, guestAccessToken, inhibitLogin, - callback, -) { - // backwards compat - if (bindThreepids === true) { - bindThreepids = { email: true }; - } else if (bindThreepids === null || bindThreepids === undefined) { - bindThreepids = {}; - } - if (typeof inhibitLogin === 'function') { - callback = inhibitLogin; - inhibitLogin = undefined; - } - - if (sessionId) { - auth.session = sessionId; - } - - const params = { - auth: auth, - }; - if (username !== undefined && username !== null) { - params.username = username; - } - if (password !== undefined && password !== null) { - params.password = password; - } - if (bindThreepids.email) { - params.bind_email = true; - } - if (bindThreepids.msisdn) { - params.bind_msisdn = true; - } - if (guestAccessToken !== undefined && guestAccessToken !== null) { - params.guest_access_token = guestAccessToken; - } - if (inhibitLogin !== undefined && inhibitLogin !== null) { - params.inhibit_login = inhibitLogin; - } - // Temporary parameter added to make the register endpoint advertise - // msisdn flows. This exists because there are clients that break - // when given stages they don't recognise. This parameter will cease - // to be necessary once these old clients are gone. - // Only send it if we send any params at all (the password param is - // mandatory, so if we send any params, we'll send the password param) - if (password !== undefined && password !== null) { - params.x_show_msisdn = true; - } - - return this.registerRequest(params, undefined, callback); -}; - -/** - * Register a guest account. - * This method returns the auth info needed to create a new authenticated client, - * Remember to call `setGuest(true)` on the (guest-)authenticated client, e.g: - * ```javascript - * const tmpClient = await sdk.createClient(MATRIX_INSTANCE); - * const { user_id, device_id, access_token } = tmpClient.registerGuest(); - * const client = createClient({ - * baseUrl: MATRIX_INSTANCE, - * accessToken: access_token, - * userId: user_id, - * deviceId: device_id, - * }) - * client.setGuest(true); - * ``` - * - * @param {Object=} opts Registration options - * @param {Object} opts.body JSON HTTP body to provide. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: JSON object that contains: - * { user_id, device_id, access_token, home_server } - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.registerGuest = function(opts, callback) { - opts = opts || {}; - opts.body = opts.body || {}; - return this.registerRequest(opts.body, "guest", callback); -}; - -/** - * @param {Object} data parameters for registration request - * @param {string=} kind type of user to register. may be "guest" - * @param {module:client.callback=} callback - * @return {Promise} Resolves: to the /register response - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.registerRequest = function(data, kind, callback) { - const params = {}; - if (kind) { - params.kind = kind; - } - - return this._http.request( - callback, "POST", "/register", params, data, - ); -}; - -/** - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.loginFlows = function(callback) { - return this._http.request(callback, "GET", "/login"); -}; - -/** - * @param {string} loginType - * @param {Object} data - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.login = function(loginType, data, callback) { - const login_data = { - type: loginType, - }; - - // merge data into login_data - utils.extend(login_data, data); - - return this._http.authedRequest( - (error, response) => { - if (response && response.access_token && response.user_id) { - this._http.opts.accessToken = response.access_token; - this.credentials = { - userId: response.user_id, - }; - } - - if (callback) { - callback(error, response); - } - }, "POST", "/login", undefined, login_data, - ); -}; - -/** - * @param {string} user - * @param {string} password - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) { - return this.login("m.login.password", { - user: user, - password: password, - }, callback); -}; - -/** - * @param {string} relayState URL Callback after SAML2 Authentication - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) { - return this.login("m.login.saml2", { - relay_state: relayState, - }, callback); -}; - -/** - * @param {string} redirectUrl The URL to redirect to after the HS - * authenticates with CAS. - * @return {string} The HS URL to hit to begin the CAS login process. - */ -MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) { - return this.getSsoLoginUrl(redirectUrl, "cas"); -}; - -/** - * @param {string} redirectUrl The URL to redirect to after the HS - * authenticates with the SSO. - * @param {string} loginType The type of SSO login we are doing (sso or cas). - * Defaults to 'sso'. - * @param {string} idpId The ID of the Identity Provider being targeted, optional. - * @return {string} The HS URL to hit to begin the SSO login process. - */ -MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType, idpId) { - if (loginType === undefined) { - loginType = "sso"; - } - - let url = "/login/" + loginType + "/redirect"; - if (idpId) { - url += "/" + idpId; - } - - return this._http.getUrl(url, { redirectUrl }, PREFIX_R0); -}; - -/** - * @param {string} token Login token previously received from homeserver - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.loginWithToken = function(token, callback) { - return this.login("m.login.token", { - token: token, - }, callback); -}; - -/** - * Logs out the current session. - * Obviously, further calls that require authorisation should fail after this - * method is called. The state of the MatrixClient object is not affected: - * it is up to the caller to either reset or destroy the MatrixClient after - * this method succeeds. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: On success, the empty object - */ -MatrixBaseApis.prototype.logout = function(callback) { - return this._http.authedRequest( - callback, "POST", '/logout', - ); -}; - -/** - * Deactivates the logged-in account. - * Obviously, further calls that require authorisation should fail after this - * method is called. The state of the MatrixClient object is not affected: - * it is up to the caller to either reset or destroy the MatrixClient after - * this method succeeds. - * @param {object} auth Optional. Auth data to supply for User-Interactive auth. - * @param {boolean} erase Optional. If set, send as `erase` attribute in the - * JSON request body, indicating whether the account should be erased. Defaults - * to false. - * @return {Promise} Resolves: On success, the empty object - */ -MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) { - if (typeof(erase) === 'function') { - throw new Error( - 'deactivateAccount no longer accepts a callback parameter', - ); - } - - const body = {}; - if (auth) { - body.auth = auth; - } - if (erase !== undefined) { - body.erase = erase; - } - - return this._http.authedRequest( - undefined, "POST", '/account/deactivate', undefined, body, - ); -}; - -/** - * Get the fallback URL to use for unknown interactive-auth stages. - * - * @param {string} loginType the type of stage being attempted - * @param {string} authSessionId the auth session ID provided by the homeserver - * - * @return {string} HS URL to hit to for the fallback interface - */ -MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId) { - const path = utils.encodeUri("/auth/$loginType/fallback/web", { - $loginType: loginType, - }); - - return this._http.getUrl(path, { - session: authSessionId, - }, PREFIX_R0); -}; - -// Room operations -// =============== - -/** - * Create a new room. - * @param {Object} options a list of options to pass to the /createRoom API. - * @param {string} options.room_alias_name The alias localpart to assign to - * this room. - * @param {string} options.visibility Either 'public' or 'private'. - * @param {string[]} options.invite A list of user IDs to invite to this room. - * @param {string} options.name The name to give this room. - * @param {string} options.topic The topic to give this room. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {room_id: {string}, - * room_alias: {string(opt)}} - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.createRoom = async function(options, callback) { - // some valid options include: room_alias_name, visibility, invite - - // inject the id_access_token if inviting 3rd party addresses - const invitesNeedingToken = (options.invite_3pid || []) - .filter(i => !i.id_access_token); - if ( - invitesNeedingToken.length > 0 && - this.identityServer && - this.identityServer.getAccessToken && - await this.doesServerAcceptIdentityAccessToken() - ) { - const identityAccessToken = await this.identityServer.getAccessToken(); - if (identityAccessToken) { - for (const invite of invitesNeedingToken) { - invite.id_access_token = identityAccessToken; - } - } - } - - return this._http.authedRequest( - callback, "POST", "/createRoom", undefined, options, - ); -}; -/** - * Fetches relations for a given event - * @param {string} roomId the room of the event - * @param {string} eventId the id of the event - * @param {string} relationType the rel_type of the relations requested - * @param {string} eventType the event type of the relations requested - * @param {Object} opts options with optional values for the request. - * @param {Object} opts.from the pagination token returned from a previous request as `next_batch` to return following relations. - * @return {Object} the response, with chunk and next_batch. - */ -MatrixBaseApis.prototype.fetchRelations = - async function(roomId, eventId, relationType, eventType, opts) { - const queryParams = {}; - if (opts.from) { - queryParams.from = opts.from; - } - const queryString = utils.encodeParams(queryParams); - const path = utils.encodeUri( - "/rooms/$roomId/relations/$eventId/$relationType/$eventType?" + queryString, { - $roomId: roomId, - $eventId: eventId, - $relationType: relationType, - $eventType: eventType, - }); - const response = await this._http.authedRequest( - undefined, "GET", path, null, null, { - prefix: PREFIX_UNSTABLE, - }, - ); - return response; -}; - -/** - * @param {string} roomId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.roomState = function(roomId, callback) { - const path = utils.encodeUri("/rooms/$roomId/state", { $roomId: roomId }); - return this._http.authedRequest(callback, "GET", path); -}; - -/** - * Get an event in a room by its event id. - * @param {string} roomId - * @param {string} eventId - * @param {module:client.callback} callback Optional. - * - * @return {Promise} Resolves to an object containing the event. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.fetchRoomEvent = function(roomId, eventId, callback) { - const path = utils.encodeUri( - "/rooms/$roomId/event/$eventId", { - $roomId: roomId, - $eventId: eventId, - }, - ); - return this._http.authedRequest(callback, "GET", path); -}; - -/** - * @param {string} roomId - * @param {string} includeMembership the membership type to include in the response - * @param {string} excludeMembership the membership type to exclude from the response - * @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: dictionary of userid to profile information - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.members = -function(roomId, includeMembership, excludeMembership, atEventId, callback) { - const queryParams = {}; - if (includeMembership) { - queryParams.membership = includeMembership; - } - if (excludeMembership) { - queryParams.not_membership = excludeMembership; - } - if (atEventId) { - queryParams.at = atEventId; - } - - const queryString = utils.encodeParams(queryParams); - - const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, - { $roomId: roomId }); - return this._http.authedRequest(callback, "GET", path); -}; - -/** - * Upgrades a room to a new protocol version - * @param {string} roomId - * @param {string} newVersion The target version to upgrade to - * @return {Promise} Resolves: Object with key 'replacement_room' - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.upgradeRoom = function(roomId, newVersion) { - const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId }); - return this._http.authedRequest( - undefined, "POST", path, undefined, { new_version: newVersion }, - ); -}; - -/** - * @param {string} groupId - * @return {Promise} Resolves: Group summary object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getGroupSummary = function(groupId) { - const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId }); - return this._http.authedRequest(undefined, "GET", path); -}; - -/** - * @param {string} groupId - * @return {Promise} Resolves: Group profile object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getGroupProfile = function(groupId) { - const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); - return this._http.authedRequest(undefined, "GET", path); -}; - -/** - * @param {string} groupId - * @param {Object} profile The group profile object - * @param {string=} profile.name Name of the group - * @param {string=} profile.avatar_url MXC avatar URL - * @param {string=} profile.short_description A short description of the room - * @param {string=} profile.long_description A longer HTML description of the room - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setGroupProfile = function(groupId, profile) { - const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); - return this._http.authedRequest( - undefined, "POST", path, undefined, profile, - ); -}; - -/** - * @param {string} groupId - * @param {object} policy The join policy for the group. Must include at - * least a 'type' field which is 'open' if anyone can join the group - * the group without prior approval, or 'invite' if an invite is - * required to join. - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setGroupJoinPolicy = function(groupId, policy) { - const path = utils.encodeUri( - "/groups/$groupId/settings/m.join_policy", - { $groupId: groupId }, - ); - return this._http.authedRequest( - undefined, "PUT", path, undefined, { - 'm.join_policy': policy, - }, - ); -}; - -/** - * @param {string} groupId - * @return {Promise} Resolves: Group users list object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getGroupUsers = function(groupId) { - const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId }); - return this._http.authedRequest(undefined, "GET", path); -}; - -/** - * @param {string} groupId - * @return {Promise} Resolves: Group users list object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getGroupInvitedUsers = function(groupId) { - const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId }); - return this._http.authedRequest(undefined, "GET", path); -}; - -/** - * @param {string} groupId - * @return {Promise} Resolves: Group rooms list object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getGroupRooms = function(groupId) { - const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId }); - return this._http.authedRequest(undefined, "GET", path); -}; - -/** - * @param {string} groupId - * @param {string} userId - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.inviteUserToGroup = function(groupId, userId) { - const path = utils.encodeUri( - "/groups/$groupId/admin/users/invite/$userId", - { $groupId: groupId, $userId: userId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, {}); -}; - -/** - * @param {string} groupId - * @param {string} userId - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.removeUserFromGroup = function(groupId, userId) { - const path = utils.encodeUri( - "/groups/$groupId/admin/users/remove/$userId", - { $groupId: groupId, $userId: userId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, {}); -}; - -/** - * @param {string} groupId - * @param {string} userId - * @param {string} roleId Optional. - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.addUserToGroupSummary = function(groupId, userId, roleId) { - const path = utils.encodeUri( - roleId ? - "/groups/$groupId/summary/$roleId/users/$userId" : - "/groups/$groupId/summary/users/$userId", - { $groupId: groupId, $roleId: roleId, $userId: userId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, {}); -}; - -/** - * @param {string} groupId - * @param {string} userId - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.removeUserFromGroupSummary = function(groupId, userId) { - const path = utils.encodeUri( - "/groups/$groupId/summary/users/$userId", - { $groupId: groupId, $userId: userId }, - ); - return this._http.authedRequest(undefined, "DELETE", path, undefined, {}); -}; - -/** - * @param {string} groupId - * @param {string} roomId - * @param {string} categoryId Optional. - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.addRoomToGroupSummary = function(groupId, roomId, categoryId) { - const path = utils.encodeUri( - categoryId ? - "/groups/$groupId/summary/$categoryId/rooms/$roomId" : - "/groups/$groupId/summary/rooms/$roomId", - { $groupId: groupId, $categoryId: categoryId, $roomId: roomId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, {}); -}; - -/** - * @param {string} groupId - * @param {string} roomId - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.removeRoomFromGroupSummary = function(groupId, roomId) { - const path = utils.encodeUri( - "/groups/$groupId/summary/rooms/$roomId", - { $groupId: groupId, $roomId: roomId }, - ); - return this._http.authedRequest(undefined, "DELETE", path, undefined, {}); -}; - -/** - * @param {string} groupId - * @param {string} roomId - * @param {bool} isPublic Whether the room-group association is visible to non-members - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.addRoomToGroup = function(groupId, roomId, isPublic) { - if (isPublic === undefined) { - isPublic = true; - } - const path = utils.encodeUri( - "/groups/$groupId/admin/rooms/$roomId", - { $groupId: groupId, $roomId: roomId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, - { "m.visibility": { type: isPublic ? "public" : "private" } }, - ); -}; - -/** - * Configure the visibility of a room-group association. - * @param {string} groupId - * @param {string} roomId - * @param {bool} isPublic Whether the room-group association is visible to non-members - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.updateGroupRoomVisibility = function(groupId, roomId, isPublic) { - // NB: The /config API is generic but there's not much point in exposing this yet as synapse - // is the only server to implement this. In future we should consider an API that allows - // arbitrary configuration, i.e. "config/$configKey". - - const path = utils.encodeUri( - "/groups/$groupId/admin/rooms/$roomId/config/m.visibility", - { $groupId: groupId, $roomId: roomId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, - { type: isPublic ? "public" : "private" }, - ); -}; - -/** - * @param {string} groupId - * @param {string} roomId - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.removeRoomFromGroup = function(groupId, roomId) { - const path = utils.encodeUri( - "/groups/$groupId/admin/rooms/$roomId", - { $groupId: groupId, $roomId: roomId }, - ); - return this._http.authedRequest(undefined, "DELETE", path, undefined, {}); -}; - -/** - * @param {string} groupId - * @param {Object} opts Additional options to send alongside the acceptance. - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.acceptGroupInvite = function(groupId, opts = null) { - const path = utils.encodeUri( - "/groups/$groupId/self/accept_invite", - { $groupId: groupId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, opts || {}); -}; - -/** - * @param {string} groupId - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.joinGroup = function(groupId) { - const path = utils.encodeUri( - "/groups/$groupId/self/join", - { $groupId: groupId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, {}); -}; - -/** - * @param {string} groupId - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.leaveGroup = function(groupId) { - const path = utils.encodeUri( - "/groups/$groupId/self/leave", - { $groupId: groupId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, {}); -}; - -/** - * @return {Promise} Resolves: The groups to which the user is joined - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getJoinedGroups = function() { - const path = utils.encodeUri("/joined_groups"); - return this._http.authedRequest(undefined, "GET", path); -}; - -/** - * @param {Object} content Request content - * @param {string} content.localpart The local part of the desired group ID - * @param {Object} content.profile Group profile object - * @return {Promise} Resolves: Object with key group_id: id of the created group - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.createGroup = function(content) { - const path = utils.encodeUri("/create_group"); - return this._http.authedRequest( - undefined, "POST", path, undefined, content, - ); -}; - -/** - * @param {string[]} userIds List of user IDs - * @return {Promise} Resolves: Object as exmaple below - * - * { - * "users": { - * "@bob:example.com": { - * "+example:example.com" - * } - * } - * } - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getPublicisedGroups = function(userIds) { - const path = utils.encodeUri("/publicised_groups"); - return this._http.authedRequest( - undefined, "POST", path, undefined, { user_ids: userIds }, - ); -}; - -/** - * @param {string} groupId - * @param {bool} isPublic Whether the user's membership of this group is made public - * @return {Promise} Resolves: Empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setGroupPublicity = function(groupId, isPublic) { - const path = utils.encodeUri( - "/groups/$groupId/self/update_publicity", - { $groupId: groupId }, - ); - return this._http.authedRequest(undefined, "PUT", path, undefined, { - publicise: isPublic, - }); -}; - -/** - * Retrieve a state event. - * @param {string} roomId - * @param {string} eventType - * @param {string} stateKey - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, callback) { - const pathParams = { - $roomId: roomId, - $eventType: eventType, - $stateKey: stateKey, - }; - let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); - if (stateKey !== undefined) { - path = utils.encodeUri(path + "/$stateKey", pathParams); - } - return this._http.authedRequest( - callback, "GET", path, - ); -}; - -/** - * @param {string} roomId - * @param {string} eventType - * @param {Object} content - * @param {string} stateKey - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, stateKey, - callback) { - const pathParams = { - $roomId: roomId, - $eventType: eventType, - $stateKey: stateKey, - }; - let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); - if (stateKey !== undefined) { - path = utils.encodeUri(path + "/$stateKey", pathParams); - } - return this._http.authedRequest( - callback, "PUT", path, undefined, content, - ); -}; - -/** - * @param {string} roomId - * @param {Number} limit - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) { - if (utils.isFunction(limit)) { - callback = limit; limit = undefined; - } - const path = utils.encodeUri("/rooms/$roomId/initialSync", - { $roomId: roomId }, - ); - if (!limit) { - limit = 30; - } - return this._http.authedRequest( - callback, "GET", path, { limit: limit }, - ); -}; - -/** - * Set a marker to indicate the point in a room before which the user has read every - * event. This can be retrieved from room account data (the event type is `m.fully_read`) - * and displayed as a horizontal line in the timeline that is visually distinct to the - * position of the user's own read receipt. - * @param {string} roomId ID of the room that has been read - * @param {string} rmEventId ID of the event that has been read - * @param {string} rrEventId ID of the event tracked by the read receipt. This is here - * for convenience because the RR and the RM are commonly updated at the same time as - * each other. Optional. - * @param {object} opts Options for the read markers. - * @param {object} opts.hidden True to hide the read receipt from other users. This - * property is currently unstable and may change in the future. - * @return {Promise} Resolves: the empty object, {}. - */ -MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest = - function(roomId, rmEventId, rrEventId, opts) { - const path = utils.encodeUri("/rooms/$roomId/read_markers", { - $roomId: roomId, - }); - - const content = { - "m.fully_read": rmEventId, - "m.read": rrEventId, - "m.hidden": Boolean(opts ? opts.hidden : false), - }; - - return this._http.authedRequest( - undefined, "POST", path, undefined, content, - ); -}; - -/** - * @return {Promise} Resolves: A list of the user's current rooms - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getJoinedRooms = function() { - const path = utils.encodeUri("/joined_rooms"); - return this._http.authedRequest(undefined, "GET", path); -}; - -/** - * Retrieve membership info. for a room. - * @param {string} roomId ID of the room to get membership for - * @return {Promise} Resolves: A list of currently joined users - * and their profile data. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) { - const path = utils.encodeUri("/rooms/$roomId/joined_members", { - $roomId: roomId, - }); - return this._http.authedRequest(undefined, "GET", path); -}; - -// Room Directory operations -// ========================= - -/** - * @param {Object} options Options for this request - * @param {string} options.server The remote server to query for the room list. - * Optional. If unspecified, get the local home - * server's public room list. - * @param {number} options.limit Maximum number of entries to return - * @param {string} options.since Token to paginate from - * @param {object} options.filter Filter parameters - * @param {string} options.filter.generic_search_term String to search for - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.publicRooms = function(options, callback) { - if (typeof(options) == 'function') { - callback = options; - options = {}; - } - if (options === undefined) { - options = {}; - } - - const query_params = {}; - if (options.server) { - query_params.server = options.server; - delete options.server; - } - - if (Object.keys(options).length === 0 && Object.keys(query_params).length === 0) { - return this._http.authedRequest(callback, "GET", "/publicRooms"); - } else { - return this._http.authedRequest( - callback, "POST", "/publicRooms", query_params, options, - ); - } -}; - -/** - * Create an alias to room ID mapping. - * @param {string} alias The room alias to create. - * @param {string} roomId The room ID to link the alias to. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) { - const path = utils.encodeUri("/directory/room/$alias", { - $alias: alias, - }); - const data = { - room_id: roomId, - }; - return this._http.authedRequest( - callback, "PUT", path, undefined, data, - ); -}; - -/** - * Delete an alias to room ID mapping. This alias must be on your local server - * and you must have sufficient access to do this operation. - * @param {string} alias The room alias to delete. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.deleteAlias = function(alias, callback) { - const path = utils.encodeUri("/directory/room/$alias", { - $alias: alias, - }); - return this._http.authedRequest( - callback, "DELETE", path, undefined, undefined, - ); -}; - -/** - * @param {string} roomId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: an object with an `aliases` property, containing an array of local aliases - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.unstableGetLocalAliases = -function(roomId, callback) { - const path = utils.encodeUri("/rooms/$roomId/aliases", - { $roomId: roomId }); - const prefix = PREFIX_UNSTABLE + "/org.matrix.msc2432"; - return this._http.authedRequest(callback, "GET", path, - null, null, { prefix }); -}; - -/** - * Get room info for the given alias. - * @param {string} alias The room alias to resolve. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Object with room_id and servers. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) { - // TODO: deprecate this or resolveRoomAlias - const path = utils.encodeUri("/directory/room/$alias", { - $alias: alias, - }); - return this._http.authedRequest( - callback, "GET", path, - ); -}; - -/** - * @param {string} roomAlias - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) { - // TODO: deprecate this or getRoomIdForAlias - const path = utils.encodeUri("/directory/room/$alias", { $alias: roomAlias }); - return this._http.request(callback, "GET", path); -}; - -/** - * Get the visibility of a room in the current HS's room directory - * @param {string} roomId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getRoomDirectoryVisibility = - function(roomId, callback) { - const path = utils.encodeUri("/directory/list/room/$roomId", { - $roomId: roomId, - }); - return this._http.authedRequest(callback, "GET", path); -}; - -/** - * Set the visbility of a room in the current HS's room directory - * @param {string} roomId - * @param {string} visibility "public" to make the room visible - * in the public directory, or "private" to make - * it invisible. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setRoomDirectoryVisibility = - function(roomId, visibility, callback) { - const path = utils.encodeUri("/directory/list/room/$roomId", { - $roomId: roomId, - }); - return this._http.authedRequest( - callback, "PUT", path, undefined, { "visibility": visibility }, - ); -}; - -/** - * Set the visbility of a room bridged to a 3rd party network in - * the current HS's room directory. - * @param {string} networkId the network ID of the 3rd party - * instance under which this room is published under. - * @param {string} roomId - * @param {string} visibility "public" to make the room visible - * in the public directory, or "private" to make - * it invisible. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService = - function(networkId, roomId, visibility, callback) { - const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", { - $networkId: networkId, - $roomId: roomId, - }); - return this._http.authedRequest( - callback, "PUT", path, undefined, { "visibility": visibility }, - ); -}; - -// User Directory Operations -// ========================= - -/** - * Query the user directory with a term matching user IDs, display names and domains. - * @param {object} opts options - * @param {string} opts.term the term with which to search. - * @param {number} opts.limit the maximum number of results to return. The server will - * apply a limit if unspecified. - * @return {Promise} Resolves: an array of results. - */ -MatrixBaseApis.prototype.searchUserDirectory = function(opts) { - const body = { - search_term: opts.term, - }; - - if (opts.limit !== undefined) { - body.limit = opts.limit; - } - - return this._http.authedRequest( - undefined, "POST", "/user_directory/search", undefined, body, - ); -}; - -// Media operations -// ================ - -/** - * Upload a file to the media repository on the home server. - * - * @param {object} file The object to upload. On a browser, something that - * can be sent to XMLHttpRequest.send (typically a File). Under node.js, - * a a Buffer, String or ReadStream. - * - * @param {object} opts options object - * - * @param {string=} opts.name Name to give the file on the server. Defaults - * to file.name. - * - * @param {boolean=} opts.includeFilename if false will not send the filename, - * e.g for encrypted file uploads where filename leaks are undesirable. - * Defaults to true. - * - * @param {string=} opts.type Content-type for the upload. Defaults to - * file.type, or applicaton/octet-stream. - * - * @param {boolean=} opts.rawResponse Return the raw body, rather than - * parsing the JSON. Defaults to false (except on node.js, where it - * defaults to true for backwards compatibility). - * - * @param {boolean=} opts.onlyContentUri Just return the content URI, - * rather than the whole body. Defaults to false (except on browsers, - * where it defaults to true for backwards compatibility). Ignored if - * opts.rawResponse is true. - * - * @param {Function=} opts.callback Deprecated. Optional. The callback to - * invoke on success/failure. See the promise return values for more - * information. - * - * @param {Function=} opts.progressHandler Optional. Called when a chunk of - * data has been uploaded, with an object containing the fields `loaded` - * (number of bytes transferred) and `total` (total size, if known). - * - * @return {Promise} Resolves to response object, as - * determined by this.opts.onlyData, opts.rawResponse, and - * opts.onlyContentUri. Rejects with an error (usually a MatrixError). - */ -MatrixBaseApis.prototype.uploadContent = function(file, opts) { - return this._http.uploadContent(file, opts); -}; - -/** - * Cancel a file upload in progress - * @param {Promise} promise The promise returned from uploadContent - * @return {boolean} true if canceled, otherwise false - */ -MatrixBaseApis.prototype.cancelUpload = function(promise) { - return this._http.cancelUpload(promise); -}; - -/** - * Get a list of all file uploads in progress - * @return {array} Array of objects representing current uploads. - * Currently in progress is element 0. Keys: - * - promise: The promise associated with the upload - * - loaded: Number of bytes uploaded - * - total: Total number of bytes to upload - */ -MatrixBaseApis.prototype.getCurrentUploads = function() { - return this._http.getCurrentUploads(); -}; - -// Profile operations -// ================== - -/** - * @param {string} userId - * @param {string} info The kind of info to retrieve (e.g. 'displayname', - * 'avatar_url'). - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) { - if (utils.isFunction(info)) { - callback = info; info = undefined; - } - - const path = info ? - utils.encodeUri("/profile/$userId/$info", - { $userId: userId, $info: info }) : - utils.encodeUri("/profile/$userId", - { $userId: userId }); - return this._http.authedRequest(callback, "GET", path); -}; - -// Account operations -// ================== - -/** - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getThreePids = function(callback) { - const path = "/account/3pid"; - return this._http.authedRequest( - callback, "GET", path, undefined, undefined, - ); -}; - -/** - * Add a 3PID to your homeserver account and optionally bind it to an identity - * server as well. An identity server is required as part of the `creds` object. - * - * This API is deprecated, and you should instead use `addThreePidOnly` - * for homeservers that support it. - * - * @param {Object} creds - * @param {boolean} bind - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: on success - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) { - const path = "/account/3pid"; - const data = { - 'threePidCreds': creds, - 'bind': bind, - }; - return this._http.authedRequest( - callback, "POST", path, null, data, - ); -}; - -/** - * Add a 3PID to your homeserver account. This API does not use an identity - * server, as the homeserver is expected to handle 3PID ownership validation. - * - * You can check whether a homeserver supports this API via - * `doesServerSupportSeparateAddAndBind`. - * - * @param {Object} data A object with 3PID validation data from having called - * `account/3pid//requestToken` on the homeserver. - * @return {Promise} Resolves: on success - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.addThreePidOnly = async function(data) { - const path = "/account/3pid/add"; - const prefix = await this.isVersionSupported("r0.6.0") ? - PREFIX_R0 : PREFIX_UNSTABLE; - return this._http.authedRequest( - undefined, "POST", path, null, data, { prefix }, - ); -}; - -/** - * Bind a 3PID for discovery onto an identity server via the homeserver. The - * identity server handles 3PID ownership validation and the homeserver records - * the new binding to track where all 3PIDs for the account are bound. - * - * You can check whether a homeserver supports this API via - * `doesServerSupportSeparateAddAndBind`. - * - * @param {Object} data A object with 3PID validation data from having called - * `validate//requestToken` on the identity server. It should also - * contain `id_server` and `id_access_token` fields as well. - * @return {Promise} Resolves: on success - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.bindThreePid = async function(data) { - const path = "/account/3pid/bind"; - const prefix = await this.isVersionSupported("r0.6.0") ? - PREFIX_R0 : PREFIX_UNSTABLE; - return this._http.authedRequest( - undefined, "POST", path, null, data, { prefix }, - ); -}; - -/** - * Unbind a 3PID for discovery on an identity server via the homeserver. The - * homeserver removes its record of the binding to keep an updated record of - * where all 3PIDs for the account are bound. - * - * @param {string} medium The threepid medium (eg. 'email') - * @param {string} address The threepid address (eg. 'bob@example.com') - * this must be as returned by getThreePids. - * @return {Promise} Resolves: on success - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.unbindThreePid = async function(medium, address) { - const path = "/account/3pid/unbind"; - const data = { - medium, - address, - id_server: this.getIdentityServerUrl(true), - }; - const prefix = await this.isVersionSupported("r0.6.0") ? - PREFIX_R0 : PREFIX_UNSTABLE; - return this._http.authedRequest( - undefined, "POST", path, null, data, { prefix }, - ); -}; - -/** - * @param {string} medium The threepid medium (eg. 'email') - * @param {string} address The threepid address (eg. 'bob@example.com') - * this must be as returned by getThreePids. - * @return {Promise} Resolves: The server response on success - * (generally the empty JSON object) - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.deleteThreePid = function(medium, address) { - const path = "/account/3pid/delete"; - const data = { - 'medium': medium, - 'address': address, - }; - return this._http.authedRequest(undefined, "POST", path, null, data); -}; - -/** - * Make a request to change your password. - * @param {Object} authDict - * @param {string} newPassword The new desired password. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback) { - const path = "/account/password"; - const data = { - 'auth': authDict, - 'new_password': newPassword, - }; - - return this._http.authedRequest( - callback, "POST", path, null, data, - ); -}; - -// Device operations -// ================= - -/** - * Gets all devices recorded for the logged-in user - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getDevices = function() { - return this._http.authedRequest( - undefined, 'GET', "/devices", undefined, undefined, - ); -}; - -/** - * Gets specific device details for the logged-in user - * @param {string} device_id device to query - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getDevice = function(device_id) { - const path = utils.encodeUri("/devices/$device_id", { - $device_id: device_id, - }); - return this._http.authedRequest( - undefined, 'GET', path, undefined, undefined, - ); -}; - -/** - * Update the given device - * - * @param {string} device_id device to update - * @param {Object} body body of request - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) { - const path = utils.encodeUri("/devices/$device_id", { - $device_id: device_id, - }); - - return this._http.authedRequest(undefined, "PUT", path, undefined, body); -}; - -/** - * Delete the given device - * - * @param {string} device_id device to delete - * @param {object} auth Optional. Auth data to supply for User-Interactive auth. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) { - const path = utils.encodeUri("/devices/$device_id", { - $device_id: device_id, - }); - - const body = {}; - - if (auth) { - body.auth = auth; - } - - return this._http.authedRequest(undefined, "DELETE", path, undefined, body); -}; - -/** - * Delete multiple device - * - * @param {string[]} devices IDs of the devices to delete - * @param {object} auth Optional. Auth data to supply for User-Interactive auth. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) { - const body = { devices }; - - if (auth) { - body.auth = auth; - } - - const path = "/delete_devices"; - return this._http.authedRequest(undefined, "POST", path, undefined, body); -}; - -// Push operations -// =============== - -/** - * Gets all pushers registered for the logged-in user - * - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Array of objects representing pushers - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getPushers = function(callback) { - const path = "/pushers"; - return this._http.authedRequest( - callback, "GET", path, undefined, undefined, - ); -}; - -/** - * Adds a new pusher or updates an existing pusher - * - * @param {Object} pusher Object representing a pusher - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Empty json object on success - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setPusher = function(pusher, callback) { - const path = "/pushers/set"; - return this._http.authedRequest( - callback, "POST", path, null, pusher, - ); -}; - -/** - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getPushRules = function(callback) { - return this._http.authedRequest(callback, "GET", "/pushrules/").then(rules => { - return PushProcessor.rewriteDefaultRules(rules); - }); -}; - -/** - * @param {string} scope - * @param {string} kind - * @param {string} ruleId - * @param {Object} body - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callback) { - // NB. Scope not uri encoded because devices need the '/' - const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { - $kind: kind, - $ruleId: ruleId, - }); - return this._http.authedRequest( - callback, "PUT", path, undefined, body, - ); -}; - -/** - * @param {string} scope - * @param {string} kind - * @param {string} ruleId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback) { - // NB. Scope not uri encoded because devices need the '/' - const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { - $kind: kind, - $ruleId: ruleId, - }); - return this._http.authedRequest(callback, "DELETE", path); -}; - -/** - * Enable or disable a push notification rule. - * @param {string} scope - * @param {string} kind - * @param {string} ruleId - * @param {boolean} enabled - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind, - ruleId, enabled, callback) { - const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", { - $kind: kind, - $ruleId: ruleId, - }); - return this._http.authedRequest( - callback, "PUT", path, undefined, { "enabled": enabled }, - ); -}; - -/** - * Set the actions for a push notification rule. - * @param {string} scope - * @param {string} kind - * @param {string} ruleId - * @param {array} actions - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind, - ruleId, actions, callback) { - const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", { - $kind: kind, - $ruleId: ruleId, - }); - return this._http.authedRequest( - callback, "PUT", path, undefined, { "actions": actions }, - ); -}; - -// Search -// ====== - -/** - * Perform a server-side search. - * @param {Object} opts - * @param {string} opts.next_batch the batch token to pass in the query string - * @param {Object} opts.body the JSON object to pass to the request body. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.search = function(opts, callback) { - const queryparams = {}; - if (opts.next_batch) { - queryparams.next_batch = opts.next_batch; - } - return this._http.authedRequest( - callback, "POST", "/search", queryparams, opts.body, - ); -}; - -// Crypto -// ====== - -/** - * Upload keys - * - * @param {Object} content body of upload request - * - * @param {Object=} opts this method no longer takes any opts, - * used to take opts.device_id but this was not removed from the spec as a redundant parameter - * - * @param {module:client.callback=} callback - * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). - */ -MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) { - return this._http.authedRequest(callback, "POST", "/keys/upload", undefined, content); -}; - -MatrixBaseApis.prototype.uploadKeySignatures = function(content) { - return this._http.authedRequest( - undefined, "POST", '/keys/signatures/upload', undefined, - content, { - prefix: PREFIX_UNSTABLE, - }, - ); -}; - -/** - * Download device keys - * - * @param {string[]} userIds list of users to get keys for - * - * @param {Object=} opts - * - * @param {string=} opts.token sync token to pass in the query request, to help - * the HS give the most recent results - * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). - */ -MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) { - if (utils.isFunction(opts)) { - // opts used to be 'callback'. - throw new Error( - 'downloadKeysForUsers no longer accepts a callback parameter', - ); - } - opts = opts || {}; - - const content = { - device_keys: {}, - }; - if ('token' in opts) { - content.token = opts.token; - } - userIds.forEach((u) => { - content.device_keys[u] = []; - }); - - return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content); -}; - -/** - * Claim one-time keys - * - * @param {string[]} devices a list of [userId, deviceId] pairs - * - * @param {string} [key_algorithm = signed_curve25519] desired key type - * - * @param {number} [timeout] the time (in milliseconds) to wait for keys from remote - * servers - * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). - */ -MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm, timeout) { - const queries = {}; - - if (key_algorithm === undefined) { - key_algorithm = "signed_curve25519"; - } - - for (let i = 0; i < devices.length; ++i) { - const userId = devices[i][0]; - const deviceId = devices[i][1]; - const query = queries[userId] || {}; - queries[userId] = query; - query[deviceId] = key_algorithm; - } - const content = { one_time_keys: queries }; - if (timeout) { - content.timeout = timeout; - } - const path = "/keys/claim"; - return this._http.authedRequest(undefined, "POST", path, undefined, content); -}; - -/** - * Ask the server for a list of users who have changed their device lists - * between a pair of sync tokens - * - * @param {string} oldToken - * @param {string} newToken - * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). - */ -MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) { - const qps = { - from: oldToken, - to: newToken, - }; - - const path = "/keys/changes"; - return this._http.authedRequest(undefined, "GET", path, qps, undefined); -}; - -MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) { - const data = Object.assign({}, keys); - if (auth) Object.assign(data, { auth }); - return this._http.authedRequest( - undefined, "POST", "/keys/device_signing/upload", undefined, data, { - prefix: PREFIX_UNSTABLE, - }, - ); -}; - -// Identity Server Operations -// ========================== - -/** - * Register with an Identity Server using the OpenID token from the user's - * Homeserver, which can be retrieved via - * {@link module:client~MatrixClient#getOpenIdToken}. - * - * Note that the `/account/register` endpoint (as well as IS authentication in - * general) was added as part of the v2 API version. - * - * @param {object} hsOpenIdToken - * @return {Promise} Resolves: with object containing an Identity - * Server access token. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) { - if (!this.idBaseUrl) { - throw new Error("No Identity Server base URL set"); - } - - const uri = this.idBaseUrl + PREFIX_IDENTITY_V2 + "/account/register"; - return this._http.requestOtherUrl( - undefined, "POST", uri, - null, hsOpenIdToken, - ); -}; - -/** - * Requests an email verification token directly from an identity server. - * - * This API is used as part of binding an email for discovery on an identity - * server. The validation data that results should be passed to the - * `bindThreePid` method to complete the binding process. - * - * @param {string} email The email address to request a token for - * @param {string} clientSecret A secret binary string generated by the client. - * It is recommended this be around 16 ASCII characters. - * @param {number} sendAttempt If an identity server sees a duplicate request - * with the same sendAttempt, it will not send another email. - * To request another email to be sent, use a larger value for - * the sendAttempt param as was used in the previous request. - * @param {string} nextLink Optional If specified, the client will be redirected - * to this link after validation. - * @param {module:client.callback} callback Optional. - * @param {string} identityAccessToken The `access_token` field of the identity - * server `/account/register` response (see {@link registerWithIdentityServer}). - * - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - * @throws Error if no identity server is set - */ -MatrixBaseApis.prototype.requestEmailToken = async function( - email, - clientSecret, - sendAttempt, - nextLink, - callback, - identityAccessToken, -) { - const params = { - client_secret: clientSecret, - email: email, - send_attempt: sendAttempt, - next_link: nextLink, - }; - - return await this._http.idServerRequest( - callback, "POST", "/validate/email/requestToken", - params, PREFIX_IDENTITY_V2, identityAccessToken, - ); -}; - -/** - * Requests a MSISDN verification token directly from an identity server. - * - * This API is used as part of binding a MSISDN for discovery on an identity - * server. The validation data that results should be passed to the - * `bindThreePid` method to complete the binding process. - * - * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in - * which phoneNumber should be parsed relative to. - * @param {string} phoneNumber The phone number, in national or international - * format - * @param {string} clientSecret A secret binary string generated by the client. - * It is recommended this be around 16 ASCII characters. - * @param {number} sendAttempt If an identity server sees a duplicate request - * with the same sendAttempt, it will not send another SMS. - * To request another SMS to be sent, use a larger value for - * the sendAttempt param as was used in the previous request. - * @param {string} nextLink Optional If specified, the client will be redirected - * to this link after validation. - * @param {module:client.callback} callback Optional. - * @param {string} identityAccessToken The `access_token` field of the Identity - * Server `/account/register` response (see {@link registerWithIdentityServer}). - * - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - * @throws Error if no identity server is set - */ -MatrixBaseApis.prototype.requestMsisdnToken = async function( - phoneCountry, - phoneNumber, - clientSecret, - sendAttempt, - nextLink, - callback, - identityAccessToken, -) { - const params = { - client_secret: clientSecret, - country: phoneCountry, - phone_number: phoneNumber, - send_attempt: sendAttempt, - next_link: nextLink, - }; - - return await this._http.idServerRequest( - callback, "POST", "/validate/msisdn/requestToken", - params, PREFIX_IDENTITY_V2, identityAccessToken, - ); -}; - -/** - * Submits a MSISDN token to the identity server - * - * This is used when submitting the code sent by SMS to a phone number. - * The ID server has an equivalent API for email but the js-sdk does - * not expose this, since email is normally validated by the user clicking - * a link rather than entering a code. - * - * @param {string} sid The sid given in the response to requestToken - * @param {string} clientSecret A secret binary string generated by the client. - * This must be the same value submitted in the requestToken call. - * @param {string} msisdnToken The MSISDN token, as enetered by the user. - * @param {string} identityAccessToken The `access_token` field of the Identity - * Server `/account/register` response (see {@link registerWithIdentityServer}). - * - * @return {Promise} Resolves: Object, currently with no parameters. - * @return {module:http-api.MatrixError} Rejects: with an error response. - * @throws Error if No ID server is set - */ -MatrixBaseApis.prototype.submitMsisdnToken = async function( - sid, - clientSecret, - msisdnToken, - identityAccessToken, -) { - const params = { - sid: sid, - client_secret: clientSecret, - token: msisdnToken, - }; - - return await this._http.idServerRequest( - undefined, "POST", "/validate/msisdn/submitToken", - params, PREFIX_IDENTITY_V2, identityAccessToken, - ); -}; - -/** - * Submits a MSISDN token to an arbitrary URL. - * - * This is used when submitting the code sent by SMS to a phone number in the - * newer 3PID flow where the homeserver validates 3PID ownership (as part of - * `requestAdd3pidMsisdnToken`). The homeserver response may include a - * `submit_url` to specify where the token should be sent, and this helper can - * be used to pass the token to this URL. - * - * @param {string} url The URL to submit the token to - * @param {string} sid The sid given in the response to requestToken - * @param {string} clientSecret A secret binary string generated by the client. - * This must be the same value submitted in the requestToken call. - * @param {string} msisdnToken The MSISDN token, as enetered by the user. - * - * @return {Promise} Resolves: Object, currently with no parameters. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.submitMsisdnTokenOtherUrl = function( - url, - sid, - clientSecret, - msisdnToken, -) { - const params = { - sid: sid, - client_secret: clientSecret, - token: msisdnToken, - }; - - return this._http.requestOtherUrl( - undefined, "POST", url, undefined, params, - ); -}; - -/** - * Gets the V2 hashing information from the identity server. Primarily useful for - * lookups. - * @param {string} identityAccessToken The access token for the identity server. - * @returns {Promise} The hashing information for the identity server. - */ -MatrixBaseApis.prototype.getIdentityHashDetails = function(identityAccessToken) { - return this._http.idServerRequest( - undefined, "GET", "/hash_details", - null, PREFIX_IDENTITY_V2, identityAccessToken, - ); -}; - -/** - * Performs a hashed lookup of addresses against the identity server. This is - * only supported on identity servers which have at least the version 2 API. - * @param {Array>} addressPairs An array of 2 element arrays. - * The first element of each pair is the address, the second is the 3PID medium. - * Eg: ["email@example.org", "email"] - * @param {string} identityAccessToken The access token for the identity server. - * @returns {Promise>} A collection of address mappings to - * found MXIDs. Results where no user could be found will not be listed. - */ -MatrixBaseApis.prototype.identityHashedLookup = async function( - addressPairs, // [["email@example.org", "email"], ["10005550000", "msisdn"]] - identityAccessToken, -) { - const params = { - // addresses: ["email@example.org", "10005550000"], - // algorithm: "sha256", - // pepper: "abc123" - }; - - // Get hash information first before trying to do a lookup - const hashes = await this.getIdentityHashDetails(identityAccessToken); - if (!hashes || !hashes['lookup_pepper'] || !hashes['algorithms']) { - throw new Error("Unsupported identity server: bad response"); - } - - params['pepper'] = hashes['lookup_pepper']; - - const localMapping = { - // hashed identifier => plain text address - // For use in this function's return format - }; - - // When picking an algorithm, we pick the hashed over no hashes - if (hashes['algorithms'].includes('sha256')) { - // Abuse the olm hashing - const olmutil = new global.Olm.Utility(); - params["addresses"] = addressPairs.map(p => { - const addr = p[0].toLowerCase(); // lowercase to get consistent hashes - const med = p[1].toLowerCase(); - const hashed = olmutil.sha256(`${addr} ${med} ${params['pepper']}`) - .replace(/\+/g, '-').replace(/\//g, '_'); // URL-safe base64 - // Map the hash to a known (case-sensitive) address. We use the case - // sensitive version because the caller might be expecting that. - localMapping[hashed] = p[0]; - return hashed; - }); - params["algorithm"] = "sha256"; - } else if (hashes['algorithms'].includes('none')) { - params["addresses"] = addressPairs.map(p => { - const addr = p[0].toLowerCase(); // lowercase to get consistent hashes - const med = p[1].toLowerCase(); - const unhashed = `${addr} ${med}`; - // Map the unhashed values to a known (case-sensitive) address. We use - // the case sensitive version because the caller might be expecting that. - localMapping[unhashed] = p[0]; - return unhashed; - }); - params["algorithm"] = "none"; - } else { - throw new Error("Unsupported identity server: unknown hash algorithm"); - } - - const response = await this._http.idServerRequest( - undefined, "POST", "/lookup", - params, PREFIX_IDENTITY_V2, identityAccessToken, - ); - - if (!response || !response['mappings']) return []; // no results - - const foundAddresses = [/* {address: "plain@example.org", mxid} */]; - for (const hashed of Object.keys(response['mappings'])) { - const mxid = response['mappings'][hashed]; - const plainAddress = localMapping[hashed]; - if (!plainAddress) { - throw new Error("Identity server returned more results than expected"); - } - - foundAddresses.push({ address: plainAddress, mxid }); - } - return foundAddresses; -}; - -/** - * Looks up the public Matrix ID mapping for a given 3rd party - * identifier from the Identity Server - * - * @param {string} medium The medium of the threepid, eg. 'email' - * @param {string} address The textual address of the threepid - * @param {module:client.callback} callback Optional. - * @param {string} identityAccessToken The `access_token` field of the Identity - * Server `/account/register` response (see {@link registerWithIdentityServer}). - * - * @return {Promise} Resolves: A threepid mapping - * object or the empty object if no mapping - * exists - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.lookupThreePid = async function( - medium, - address, - callback, - identityAccessToken, -) { - // Note: we're using the V2 API by calling this function, but our - // function contract requires a V1 response. We therefore have to - // convert it manually. - const response = await this.identityHashedLookup( - [[address, medium]], identityAccessToken, - ); - const result = response.find(p => p.address === address); - if (!result) { - if (callback) callback(null, {}); - return {}; - } - - const mapping = { - address, - medium, - mxid: result.mxid, - - // We can't reasonably fill these parameters: - // not_before - // not_after - // ts - // signatures - }; - - if (callback) callback(null, mapping); - return mapping; -}; - -/** - * Looks up the public Matrix ID mappings for multiple 3PIDs. - * - * @param {Array.>} query Array of arrays containing - * [medium, address] - * @param {string} identityAccessToken The `access_token` field of the Identity - * Server `/account/register` response (see {@link registerWithIdentityServer}). - * - * @return {Promise} Resolves: Lookup results from IS. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.bulkLookupThreePids = async function( - query, - identityAccessToken, -) { - // Note: we're using the V2 API by calling this function, but our - // function contract requires a V1 response. We therefore have to - // convert it manually. - const response = await this.identityHashedLookup( - // We have to reverse the query order to get [address, medium] pairs - query.map(p => [p[1], p[0]]), identityAccessToken, - ); - - const v1results = []; - for (const mapping of response) { - const originalQuery = query.find(p => p[1] === mapping.address); - if (!originalQuery) { - throw new Error("Identity sever returned unexpected results"); - } - - v1results.push([ - originalQuery[0], // medium - mapping.address, - mapping.mxid, - ]); - } - - return { threepids: v1results }; -}; - -/** - * Get account info from the Identity Server. This is useful as a neutral check - * to verify that other APIs are likely to approve access by testing that the - * token is valid, terms have been agreed, etc. - * - * @param {string} identityAccessToken The `access_token` field of the Identity - * Server `/account/register` response (see {@link registerWithIdentityServer}). - * - * @return {Promise} Resolves: an object with account info. - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.getIdentityAccount = function( - identityAccessToken, -) { - return this._http.idServerRequest( - undefined, "GET", "/account", - undefined, PREFIX_IDENTITY_V2, identityAccessToken, - ); -}; - -// Direct-to-device messaging -// ========================== - -/** - * Send an event to a specific list of devices - * - * @param {string} eventType type of event to send - * @param {Object.>} contentMap - * content to send. Map from user_id to device_id to content object. - * @param {string=} txnId transaction id. One will be made up if not - * supplied. - * @return {Promise} Resolves to the result object - */ -MatrixBaseApis.prototype.sendToDevice = function( - eventType, contentMap, txnId, -) { - const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", { - $eventType: eventType, - $txnId: txnId ? txnId : this.makeTxnId(), - }); - - const body = { - messages: contentMap, - }; - - const targets = Object.keys(contentMap).reduce((obj, key) => { - obj[key] = Object.keys(contentMap[key]); - return obj; - }, {}); - logger.log(`PUT ${path}`, targets); - - return this._http.authedRequest(undefined, "PUT", path, undefined, body); -}; - -// Third party Lookup API -// ====================== - -/** - * Get the third party protocols that can be reached using - * this HS - * @return {Promise} Resolves to the result object - */ -MatrixBaseApis.prototype.getThirdpartyProtocols = function() { - return this._http.authedRequest( - undefined, "GET", "/thirdparty/protocols", undefined, undefined, - ).then((response) => { - // sanity check - if (!response || typeof(response) !== 'object') { - throw new Error( - `/thirdparty/protocols did not return an object: ${response}`, - ); - } - return response; - }); -}; - -/** - * Get information on how a specific place on a third party protocol - * may be reached. - * @param {string} protocol The protocol given in getThirdpartyProtocols() - * @param {object} params Protocol-specific parameters, as given in the - * response to getThirdpartyProtocols() - * @return {Promise} Resolves to the result object - */ -MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) { - const path = utils.encodeUri("/thirdparty/location/$protocol", { - $protocol: protocol, - }); - - return this._http.authedRequest(undefined, "GET", path, params, undefined); -}; - -/** - * Get information on how a specific user on a third party protocol - * may be reached. - * @param {string} protocol The protocol given in getThirdpartyProtocols() - * @param {object} params Protocol-specific parameters, as given in the - * response to getThirdpartyProtocols() - * @return {Promise} Resolves to the result object - */ -MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) { - const path = utils.encodeUri("/thirdparty/user/$protocol", { - $protocol: protocol, - }); - - return this._http.authedRequest(undefined, "GET", path, params, undefined); -}; - -MatrixBaseApis.prototype.getTerms = function(serviceType, baseUrl) { - const url = termsUrlForService(serviceType, baseUrl); - return this._http.requestOtherUrl( - undefined, 'GET', url, - ); -}; - -MatrixBaseApis.prototype.agreeToTerms = function( - serviceType, baseUrl, accessToken, termsUrls, -) { - const url = termsUrlForService(serviceType, baseUrl); - const headers = { - Authorization: "Bearer " + accessToken, - }; - return this._http.requestOtherUrl( - undefined, 'POST', url, null, { user_accepts: termsUrls }, { headers }, - ); -}; - -/** - * Reports an event as inappropriate to the server, which may then notify the appropriate people. - * @param {string} roomId The room in which the event being reported is located. - * @param {string} eventId The event to report. - * @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive. - * @param {string} reason The reason the content is being reported. May be blank. - * @returns {Promise} Resolves to an empty object if successful - */ -MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason) { - const path = utils.encodeUri("/rooms/$roomId/report/$eventId", { - $roomId: roomId, - $eventId: eventId, - }); - - return this._http.authedRequest(undefined, "POST", path, null, { score, reason }); -}; - -/** - * Fetches or paginates a summary of a space as defined by MSC2946 - * @param {string} roomId The ID of the space-room to use as the root of the summary. - * @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace. - * @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true. - * @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true. - * @param {number?} limit The maximum number of rooms to return in total. - * @param {string?} batch The opaque token to paginate a previous summary request. - * @returns {Promise} the response, with next_batch, rooms, events fields. - */ -MatrixBaseApis.prototype.getSpaceSummary = function( - roomId, - maxRoomsPerSpace, - suggestedOnly, - autoJoinOnly, - limit, - batch, -) { - const path = utils.encodeUri("/rooms/$roomId/spaces", { - $roomId: roomId, - }); - - return this._http.authedRequest(undefined, "POST", path, null, { - max_rooms_per_space: maxRoomsPerSpace, - suggested_only: suggestedOnly, - auto_join_only: autoJoinOnly, - limit, - batch, - }, { - prefix: "/_matrix/client/unstable/org.matrix.msc2946", - }); -}; diff --git a/src/1client.ts b/src/client.ts similarity index 100% rename from src/1client.ts rename to src/client.ts diff --git a/src/event-mapper.ts b/src/event-mapper.ts index d2095f40562..bc04f5e7187 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "./1client"; +import { MatrixClient } from "./client"; import {MatrixEvent} from "./models/event"; export type EventMapper = (obj: any) => MatrixEvent; diff --git a/src/matrix.ts b/src/matrix.ts index 0cf0d96a432..2288d9b71fc 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -18,7 +18,7 @@ import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; import { MemoryStore } from "./store/memory"; import { MatrixScheduler } from "./scheduler"; import { MatrixClient } from "./client"; -import { ICreateClientOpts } from "./1client"; +import { ICreateClientOpts } from "./client"; export * from "./client"; export * from "./http-api"; From 486369e97c9879514fcc795eee017f80c5ee3322 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 May 2021 22:48:25 -0600 Subject: [PATCH 10/32] Clean up "base-apis" find&replace --- src/crypto/CrossSigning.js | 2 +- src/crypto/algorithms/base.js | 4 ++-- src/crypto/index.js | 2 +- src/crypto/olmlib.js | 4 ++-- src/crypto/verification/Base.js | 5 +++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 2143f0c7555..00788917e58 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -725,7 +725,7 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) { /** * Request cross-signing keys from another device during verification. * - * @param {module:base-apis~MatrixBaseApis} baseApis base Matrix API interface + * @param {MatrixClient} baseApis base Matrix API interface * @param {string} userId The user ID being verified * @param {string} deviceId The device ID being verified */ diff --git a/src/crypto/algorithms/base.js b/src/crypto/algorithms/base.js index b9125761cb4..87b8a82c01e 100644 --- a/src/crypto/algorithms/base.js +++ b/src/crypto/algorithms/base.js @@ -46,7 +46,7 @@ export const DECRYPTION_CLASSES = {}; * @param {string} params.deviceId The identifier for this device. * @param {module:crypto} params.crypto crypto core * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper - * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * @param {MatrixClient} baseApis base matrix api interface * @param {string} params.roomId The ID of the room we will be sending to * @param {object} params.config The body of the m.room.encryption event */ @@ -102,7 +102,7 @@ export class EncryptionAlgorithm { * @param {string} params.userId The UserID for the local user * @param {module:crypto} params.crypto crypto core * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper - * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * @param {MatrixClient} baseApis base matrix api interface * @param {string=} params.roomId The ID of the room we will be receiving * from. Null for to-device events. */ diff --git a/src/crypto/index.js b/src/crypto/index.js index ae8de2af11b..6bce27ef4df 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -97,7 +97,7 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200; * * @internal * - * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * @param {MatrixClient} baseApis base matrix api interface * * @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore * Store to be used for end-to-end crypto session data diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 5be11040b41..74120d6436f 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -118,7 +118,7 @@ export async function encryptMessageForDevice( * * @param {module:crypto/OlmDevice} olmDevice * - * @param {module:base-apis~MatrixBaseApis} baseApis + * @param {MatrixClient} baseApis * * @param {object} devicesByUser * map from userid to list of devices to ensure sessions for @@ -168,7 +168,7 @@ export async function getExistingOlmSessions( * * @param {module:crypto/OlmDevice} olmDevice * - * @param {module:base-apis~MatrixBaseApis} baseApis + * @param {MatrixClient} baseApis * * @param {object} devicesByUser * map from userid to list of devices to ensure sessions for diff --git a/src/crypto/verification/Base.js b/src/crypto/verification/Base.js index fd23668ac39..929654cf95e 100644 --- a/src/crypto/verification/Base.js +++ b/src/crypto/verification/Base.js @@ -49,9 +49,10 @@ export class VerificationBase extends EventEmitter { * * @class * - * @param {module:base-apis~Channel} channel the verification channel to send verification messages over. + * TODO: Channel types + * @param {Channel} channel the verification channel to send verification messages over. * - * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * @param {MatrixClient} baseApis base matrix api interface * * @param {string} userId the user ID that is being verified * From 4ef50bef555dc5df43e982fbabcc5bc503018348 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 May 2021 22:56:40 -0600 Subject: [PATCH 11/32] define this.identityServer --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 81c05546026..6f53ff84f31 100644 --- a/src/client.ts +++ b/src/client.ts @@ -383,6 +383,7 @@ export class MatrixClient extends EventEmitter { public timelineSupport = false; public urlPreviewCache: { [key: string]: Promise } = {}; // TODO: @@TR public unstableClientRelationAggregation = false; + public identityServer: IIdentityServerProvider; private canSupportVoip = false; private callEventHandler: CallEventHandler; @@ -427,7 +428,6 @@ export class MatrixClient extends EventEmitter { private exportedOlmDeviceToImport: IOlmDevice; private baseUrl: string; private idBaseUrl: string; - private identityServer: any; // TODO: @@TR private http: MatrixHttpApi; private txnCtr = 0; From 48888e530e73b0117c29e8caeefd31dc96e64ece Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 20:09:38 -0600 Subject: [PATCH 12/32] Defer types --- src/client.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/client.ts b/src/client.ts index 6f53ff84f31..8e025b5226c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -107,9 +107,10 @@ import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper"; import url from "url"; import { randomString } from "./randomstring"; import { ReadStream } from "fs"; +import { WebStorageSessionStore } from "./store/session/webstorage"; export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend; - +export type SessionStore = WebStorageSessionStore; export type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore; export type Callback = (err: Error | any | null, data?: any) => void; @@ -259,7 +260,7 @@ export interface ICreateClientOpts { * end-to-end crypto will be disabled. The `createClient` helper * _will not_ create this store at the moment. */ - sessionStore?: any; + sessionStore?: SessionStore; /** * Set to true to enable client-side aggregation of event relations @@ -381,7 +382,7 @@ export class MatrixClient extends EventEmitter { public scheduler: MatrixScheduler; public clientRunning = false; public timelineSupport = false; - public urlPreviewCache: { [key: string]: Promise } = {}; // TODO: @@TR + public urlPreviewCache: { [key: string]: Promise } = {}; // TODO: Types public unstableClientRelationAggregation = false; public identityServer: IIdentityServerProvider; @@ -389,11 +390,11 @@ export class MatrixClient extends EventEmitter { private callEventHandler: CallEventHandler; private peekSync: SyncApi = null; private isGuestAccount = false; - private ongoingScrollbacks = {}; // TODO: @@TR + private ongoingScrollbacks:{[roomId: string]: {promise?: Promise, errorTs: number}} = {}; // TODO: Types private notifTimelineSet: EventTimelineSet = null; private crypto: Crypto; private cryptoStore: CryptoStore; - private sessionStore: any; // TODO: @@TR + private sessionStore: SessionStore; private verificationMethods: string[]; private cryptoCallbacks: ICryptoCallbacks; private forceTURN = false; @@ -402,7 +403,7 @@ export class MatrixClient extends EventEmitter { private fallbackICEServerAllowed = false; private roomList: RoomList; private syncApi: SyncApi; - private pushRules: any; // TODO: @@TR + private pushRules: any; // TODO: Types private syncLeftRoomsPromise: Promise; private syncedLeftRooms = false; private clientOpts: IStoredClientOpts; @@ -422,7 +423,7 @@ export class MatrixClient extends EventEmitter { }; private clientWellKnown: any; private clientWellKnownPromise: Promise; - private turnServers: any[] = []; // TODO: @@TR + private turnServers: any[] = []; // TODO: Types private turnServersExpiry = 0; private checkTurnServersIntervalID: number; private exportedOlmDeviceToImport: IOlmDevice; @@ -7456,7 +7457,8 @@ export class MatrixClient extends EventEmitter { }); } - // TODO: @@TR: Remove warning, eventually + // TODO: Remove this warning, alongside the functions + // See https://github.com/vector-im/element-web/issues/17532 // ====================================================== // ** ANCIENT APIS BELOW ** // ====================================================== From 07ee25675650e76bfbb783347b437c75fcd9b1ea Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 20:10:36 -0600 Subject: [PATCH 13/32] Incorporate https://github.com/matrix-org/matrix-js-sdk/pull/1720 --- src/client.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index 8e025b5226c..893ddd2bd7e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5836,14 +5836,12 @@ export class MatrixClient extends EventEmitter { * @return {string} The HS URL to hit to begin the SSO login process. */ public getSsoLoginUrl(redirectUrl: string, loginType = "sso", idpId?: string): Promise { - let prefix = PREFIX_R0; let url = "/login/" + loginType + "/redirect"; if (idpId) { url += "/" + idpId; - prefix = "/_matrix/client/unstable/org.matrix.msc2858"; } - return this.http.getUrl(url, { redirectUrl }, prefix); + return this.http.getUrl(url, { redirectUrl }, PREFIX_R0); } /** From e1edd84700ef0b2b871185a5e93bfb2b3192fb79 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 20:39:22 -0600 Subject: [PATCH 14/32] Early pass to fix runtime/build errors --- spec/integ/devicelist-integ-spec.js | 16 +++---- spec/integ/matrix-client-crypto.spec.js | 2 +- spec/integ/matrix-client-methods.spec.js | 2 +- spec/integ/megolm-integ.spec.js | 6 +-- spec/test-utils.js | 10 ++--- spec/unit/crypto.spec.js | 16 +++---- spec/unit/crypto/algorithms/megolm.spec.js | 44 +++++++++---------- spec/unit/crypto/backup.spec.js | 24 +++++----- spec/unit/crypto/crypto-utils.js | 2 +- spec/unit/crypto/secrets.spec.js | 14 +++--- spec/unit/crypto/verification/request.spec.js | 2 +- spec/unit/crypto/verification/sas.spec.js | 12 ++--- spec/unit/crypto/verification/util.js | 2 +- spec/unit/matrix-client.spec.js | 8 ++-- spec/unit/room.spec.js | 2 +- src/client.ts | 30 +++++++------ src/crypto/CrossSigning.js | 6 +-- src/crypto/EncryptionSetup.js | 4 +- src/crypto/SecretStorage.js | 8 ++-- src/crypto/dehydration.ts | 4 +- src/crypto/index.js | 16 +++---- src/crypto/verification/Base.js | 2 +- src/models/relations.js | 2 +- src/models/room.js | 14 +++--- src/sync.js | 26 ++++------- src/webrtc/call.ts | 14 +++--- src/webrtc/callEventHandler.ts | 2 +- 27 files changed, 142 insertions(+), 148 deletions(-) diff --git a/spec/integ/devicelist-integ-spec.js b/spec/integ/devicelist-integ-spec.js index 96bebe5c62b..cdad8a90512 100644 --- a/spec/integ/devicelist-integ-spec.js +++ b/spec/integ/devicelist-integ-spec.js @@ -165,7 +165,7 @@ describe("DeviceList management:", function() { aliceTestClient.httpBackend.flush('/keys/query', 1).then( () => aliceTestClient.httpBackend.flush('/send/', 1), ), - aliceTestClient.client._crypto._deviceList.saveIfDirty(), + aliceTestClient.client.crypto._deviceList.saveIfDirty(), ]); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { @@ -202,7 +202,7 @@ describe("DeviceList management:", function() { return aliceTestClient.httpBackend.flush('/keys/query', 1); }).then((flushed) => { expect(flushed).toEqual(0); - return aliceTestClient.client._crypto._deviceList.saveIfDirty(); + return aliceTestClient.client.crypto._deviceList.saveIfDirty(); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -235,7 +235,7 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@bob:xyz']); }).then(() => { - return aliceTestClient.client._crypto._deviceList.saveIfDirty(); + return aliceTestClient.client.crypto._deviceList.saveIfDirty(); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -256,7 +256,7 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@chris:abc']); }).then(() => { - return aliceTestClient.client._crypto._deviceList.saveIfDirty(); + return aliceTestClient.client.crypto._deviceList.saveIfDirty(); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -286,7 +286,7 @@ describe("DeviceList management:", function() { }, ); await aliceTestClient.httpBackend.flush('/keys/query', 1); - await aliceTestClient.client._crypto._deviceList.saveIfDirty(); + await aliceTestClient.client.crypto._deviceList.saveIfDirty(); aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -322,7 +322,7 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client._crypto._deviceList.saveIfDirty(); + await aliceTestClient.client.crypto._deviceList.saveIfDirty(); aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -358,7 +358,7 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client._crypto._deviceList.saveIfDirty(); + await aliceTestClient.client.crypto._deviceList.saveIfDirty(); aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -379,7 +379,7 @@ describe("DeviceList management:", function() { anotherTestClient.httpBackend.when('GET', '/sync').respond( 200, getSyncResponse([])); await anotherTestClient.flushSync(); - await anotherTestClient.client._crypto._deviceList.saveIfDirty(); + await anotherTestClient.client.crypto._deviceList.saveIfDirty(); anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; diff --git a/spec/integ/matrix-client-crypto.spec.js b/spec/integ/matrix-client-crypto.spec.js index cfe85678b97..6bb1a494bc8 100644 --- a/spec/integ/matrix-client-crypto.spec.js +++ b/spec/integ/matrix-client-crypto.spec.js @@ -159,7 +159,7 @@ function aliDownloadsKeys() { // check that the localStorage is updated as we expect (not sure this is // an integration test, but meh) return Promise.all([p1, p2]).then(() => { - return aliTestClient.client._crypto._deviceList.saveIfDirty(); + return aliTestClient.client.crypto._deviceList.saveIfDirty(); }).then(() => { aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const devices = data.devices[bobUserId]; diff --git a/spec/integ/matrix-client-methods.spec.js b/spec/integ/matrix-client-methods.spec.js index 742beec2789..b133b456e7d 100644 --- a/spec/integ/matrix-client-methods.spec.js +++ b/spec/integ/matrix-client-methods.spec.js @@ -336,7 +336,7 @@ describe("MatrixClient", function() { var b = JSON.parse(JSON.stringify(o)); delete(b.signatures); delete(b.unsigned); - return client._crypto._olmDevice.sign(anotherjson.stringify(b)); + return client.crypto._olmDevice.sign(anotherjson.stringify(b)); }; logger.log("Ed25519: " + ed25519key); diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index 8461bc385f4..513043410c5 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -998,7 +998,7 @@ describe("megolm", function() { ...rawEvent, room: ROOM_ID, }); - return event.attemptDecryption(testClient.client._crypto, true).then(() => { + return event.attemptDecryption(testClient.client.crypto, true).then(() => { expect(event.isKeySourceUntrusted()).toBeTruthy(); }); }).then(() => { @@ -1013,14 +1013,14 @@ describe("megolm", function() { event: true, }); event._senderCurve25519Key = testSenderKey; - return testClient.client._crypto._onRoomKeyEvent(event); + return testClient.client.crypto._onRoomKeyEvent(event); }).then(() => { const event = testUtils.mkEvent({ event: true, ...rawEvent, room: ROOM_ID, }); - return event.attemptDecryption(testClient.client._crypto, true).then(() => { + return event.attemptDecryption(testClient.client.crypto, true).then(() => { expect(event.isKeySourceUntrusted()).toBeFalsy(); }); }); diff --git a/spec/test-utils.js b/spec/test-utils.js index d308b6d3507..dcde2d6fea9 100644 --- a/spec/test-utils.js +++ b/spec/test-utils.js @@ -357,12 +357,12 @@ export function setHttpResponses( ); const httpReq = httpResponseObj.request.bind(httpResponseObj); - client._http = [ + client.http = [ "authedRequest", "authedRequestWithPrefix", "getContentUri", "request", "requestWithPrefix", "uploadContent", ].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}); - client._http.authedRequest.mockImplementation(httpReq); - client._http.authedRequestWithPrefix.mockImplementation(httpReq); - client._http.requestWithPrefix.mockImplementation(httpReq); - client._http.request.mockImplementation(httpReq); + client.http.authedRequest.mockImplementation(httpReq); + client.http.authedRequestWithPrefix.mockImplementation(httpReq); + client.http.requestWithPrefix.mockImplementation(httpReq); + client.http.request.mockImplementation(httpReq); } diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 22f0b2d0d7d..d1e707fd3b9 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -65,7 +65,7 @@ describe("Crypto", function() { 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; device.keys["ed25519:FLIBBLE"] = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; - client._crypto._deviceList.getDeviceByIdentityKey = () => device; + client.crypto._deviceList.getDeviceByIdentityKey = () => device; encryptionInfo = client.getEventEncryptionInfo(event); expect(encryptionInfo.encrypted).toBeTruthy(); @@ -213,7 +213,7 @@ describe("Crypto", function() { async function keyshareEventForEvent(event, index) { const eventContent = event.getWireContent(); - const key = await aliceClient._crypto._olmDevice + const key = await aliceClient.crypto._olmDevice .getInboundGroupSessionKey( roomId, eventContent.sender_key, eventContent.session_id, index, @@ -273,19 +273,19 @@ describe("Crypto", function() { await Promise.all(events.map(async (event) => { // alice encrypts each event, and then bob tries to decrypt // them without any keys, so that they'll be in pending - await aliceClient._crypto.encryptEvent(event, aliceRoom); + await aliceClient.crypto.encryptEvent(event, aliceRoom); event._clearEvent = {}; event._senderCurve25519Key = null; event._claimedEd25519Key = null; try { - await bobClient._crypto.decryptEvent(event); + await bobClient.crypto.decryptEvent(event); } catch (e) { // we expect this to fail because we don't have the // decryption keys yet } })); - const bobDecryptor = bobClient._crypto._getRoomDecryptor( + const bobDecryptor = bobClient.crypto._getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); @@ -302,7 +302,7 @@ describe("Crypto", function() { expect(events[0].getContent().msgtype).toBe("m.bad.encrypted"); expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); - const cryptoStore = bobClient._cryptoStore; + const cryptoStore = bobClient.cryptoStore; const eventContent = events[0].getWireContent(); const senderKey = eventContent.sender_key; const sessionId = eventContent.session_id; @@ -344,7 +344,7 @@ describe("Crypto", function() { }, }); await aliceClient.cancelAndResendEventRoomKeyRequest(event); - const cryptoStore = aliceClient._cryptoStore; + const cryptoStore = aliceClient.cryptoStore; const roomKeyRequestBody = { algorithm: olmlib.MEGOLM_ALGORITHM, room_id: "!someroom", @@ -377,7 +377,7 @@ describe("Crypto", function() { // key requests get queued until the sync has finished, but we don't // let the client set up enough for that to happen, so gut-wrench a bit // to force it to send now. - aliceClient._crypto._outgoingRoomKeyRequestManager.sendQueuedRequests(); + aliceClient.crypto._outgoingRoomKeyRequestManager.sendQueuedRequests(); jest.runAllTimers(); await Promise.resolve(); expect(aliceClient.sendToDevice).toBeCalledTimes(1); diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index d4b9e12471b..0a28e0f82a2 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -362,9 +362,9 @@ describe("MegolmDecryption", function() { bobClient1.initCrypto(), bobClient2.initCrypto(), ]); - const aliceDevice = aliceClient._crypto._olmDevice; - const bobDevice1 = bobClient1._crypto._olmDevice; - const bobDevice2 = bobClient2._crypto._olmDevice; + const aliceDevice = aliceClient.crypto._olmDevice; + const bobDevice1 = bobClient1.crypto._olmDevice; + const bobDevice2 = bobClient2.crypto._olmDevice; const encryptionCfg = { "algorithm": "m.megolm.v1.aes-sha2", @@ -401,10 +401,10 @@ describe("MegolmDecryption", function() { }, }; - aliceClient._crypto._deviceList.storeDevicesForUser( + aliceClient.crypto._deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); - aliceClient._crypto._deviceList.downloadKeys = async function(userIds) { + aliceClient.crypto._deviceList.downloadKeys = async function(userIds) { return this._getDevicesFromStore(userIds); }; @@ -445,7 +445,7 @@ describe("MegolmDecryption", function() { body: "secret", }, }); - await aliceClient._crypto.encryptEvent(event, room); + await aliceClient.crypto.encryptEvent(event, room); expect(run).toBe(true); @@ -465,8 +465,8 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - const aliceDevice = aliceClient._crypto._olmDevice; - const bobDevice = bobClient._crypto._olmDevice; + const aliceDevice = aliceClient.crypto._olmDevice; + const bobDevice = bobClient.crypto._olmDevice; const encryptionCfg = { "algorithm": "m.megolm.v1.aes-sha2", @@ -505,10 +505,10 @@ describe("MegolmDecryption", function() { }, }; - aliceClient._crypto._deviceList.storeDevicesForUser( + aliceClient.crypto._deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); - aliceClient._crypto._deviceList.downloadKeys = async function(userIds) { + aliceClient.crypto._deviceList.downloadKeys = async function(userIds) { return this._getDevicesFromStore(userIds); }; @@ -543,7 +543,7 @@ describe("MegolmDecryption", function() { event_id: "$event", content: {}, }); - await aliceClient._crypto.encryptEvent(event, aliceRoom); + await aliceClient.crypto.encryptEvent(event, aliceRoom); await sendPromise; }); @@ -558,11 +558,11 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - const bobDevice = bobClient._crypto._olmDevice; + const bobDevice = bobClient.crypto._olmDevice; const roomId = "!someroom"; - aliceClient._crypto._onToDeviceEvent(new MatrixEvent({ + aliceClient.crypto._onToDeviceEvent(new MatrixEvent({ type: "org.matrix.room_key.withheld", sender: "@bob:example.com", content: { @@ -575,7 +575,7 @@ describe("MegolmDecryption", function() { }, })); - await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({ + await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({ type: "m.room.encrypted", sender: "@bob:example.com", event_id: "$event", @@ -601,14 +601,14 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - aliceClient._crypto.downloadKeys = async () => {}; - const bobDevice = bobClient._crypto._olmDevice; + aliceClient.crypto.downloadKeys = async () => {}; + const bobDevice = bobClient.crypto._olmDevice; const roomId = "!someroom"; const now = Date.now(); - aliceClient._crypto._onToDeviceEvent(new MatrixEvent({ + aliceClient.crypto._onToDeviceEvent(new MatrixEvent({ type: "org.matrix.room_key.withheld", sender: "@bob:example.com", content: { @@ -625,7 +625,7 @@ describe("MegolmDecryption", function() { setTimeout(resolve, 100); }); - await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({ + await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({ type: "m.room.encrypted", sender: "@bob:example.com", event_id: "$event", @@ -652,15 +652,15 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - const bobDevice = bobClient._crypto._olmDevice; - aliceClient._crypto.downloadKeys = async () => {}; + const bobDevice = bobClient.crypto._olmDevice; + aliceClient.crypto.downloadKeys = async () => {}; const roomId = "!someroom"; const now = Date.now(); // pretend we got an event that we can't decrypt - aliceClient._crypto._onToDeviceEvent(new MatrixEvent({ + aliceClient.crypto._onToDeviceEvent(new MatrixEvent({ type: "m.room.encrypted", sender: "@bob:example.com", content: { @@ -675,7 +675,7 @@ describe("MegolmDecryption", function() { setTimeout(resolve, 100); }); - await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({ + await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({ type: "m.room.encrypted", sender: "@bob:example.com", event_id: "$event", diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index a9fb93d644b..70e1470e874 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -272,7 +272,7 @@ describe("MegolmBackup", function() { }); let numCalls = 0; return new Promise((resolve, reject) => { - client._http.authedRequest = function( + client.http.authedRequest = function( callback, method, path, queryParams, data, opts, ) { ++numCalls; @@ -292,7 +292,7 @@ describe("MegolmBackup", function() { resolve(); return Promise.resolve({}); }; - client._crypto.backupGroupSession( + client.crypto.backupGroupSession( "roomId", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", [], @@ -336,7 +336,7 @@ describe("MegolmBackup", function() { await resetCrossSigningKeys(client); let numCalls = 0; await new Promise((resolve, reject) => { - client._http.authedRequest = function( + client.http.authedRequest = function( callback, method, path, queryParams, data, opts, ) { ++numCalls; @@ -442,7 +442,7 @@ describe("MegolmBackup", function() { }); let numCalls = 0; return new Promise((resolve, reject) => { - client._http.authedRequest = function( + client.http.authedRequest = function( callback, method, path, queryParams, data, opts, ) { ++numCalls; @@ -468,7 +468,7 @@ describe("MegolmBackup", function() { ); } }; - client._crypto.backupGroupSession( + client.crypto.backupGroupSession( "roomId", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", [], @@ -506,7 +506,7 @@ describe("MegolmBackup", function() { }); it('can restore from backup', function() { - client._http.authedRequest = function() { + client.http.authedRequest = function() { return Promise.resolve(KEY_BACKUP_DATA); }; return client.restoreKeyBackupWithRecoveryKey( @@ -523,7 +523,7 @@ describe("MegolmBackup", function() { }); it('can restore backup by room', function() { - client._http.authedRequest = function() { + client.http.authedRequest = function() { return Promise.resolve({ rooms: { [ROOM_ID]: { @@ -546,15 +546,15 @@ describe("MegolmBackup", function() { it('has working cache functions', async function() { const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]); - await client._crypto.storeSessionBackupPrivateKey(key); - const result = await client._crypto.getSessionBackupPrivateKey(); + await client.crypto.storeSessionBackupPrivateKey(key); + const result = await client.crypto.getSessionBackupPrivateKey(); expect(new Uint8Array(result)).toEqual(key); }); it('caches session backup keys as it encounters them', async function() { - const cachedNull = await client._crypto.getSessionBackupPrivateKey(); + const cachedNull = await client.crypto.getSessionBackupPrivateKey(); expect(cachedNull).toBeNull(); - client._http.authedRequest = function() { + client.http.authedRequest = function() { return Promise.resolve(KEY_BACKUP_DATA); }; await new Promise((resolve) => { @@ -566,7 +566,7 @@ describe("MegolmBackup", function() { { cacheCompleteCallback: resolve }, ); }); - const cachedKey = await client._crypto.getSessionBackupPrivateKey(); + const cachedKey = await client.crypto.getSessionBackupPrivateKey(); expect(cachedKey).not.toBeNull(); }); }); diff --git a/spec/unit/crypto/crypto-utils.js b/spec/unit/crypto/crypto-utils.js index ec8a8f82a39..dcc9db16a75 100644 --- a/spec/unit/crypto/crypto-utils.js +++ b/spec/unit/crypto/crypto-utils.js @@ -6,7 +6,7 @@ export async function resetCrossSigningKeys(client, { level, authUploadDeviceSigningKeys = async func => await func(), } = {}) { - const crypto = client._crypto; + const crypto = client.crypto; const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys); try { diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index 2198256b3a3..2a7b056a0bf 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -47,7 +47,7 @@ async function makeTestClient(userInfo, options) { await client.initCrypto(); // No need to download keys for these tests - client._crypto.downloadKeys = async function() {}; + client.crypto.downloadKeys = async function() {}; return client; } @@ -234,11 +234,11 @@ describe("Secrets", function() { }, ); - const vaxDevice = vax.client._crypto._olmDevice; - const osborne2Device = osborne2.client._crypto._olmDevice; - const secretStorage = osborne2.client._crypto._secretStorage; + const vaxDevice = vax.client.crypto._olmDevice; + const osborne2Device = osborne2.client.crypto._olmDevice; + const secretStorage = osborne2.client.crypto._secretStorage; - osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", { + osborne2.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", { "VAX": { user_id: "@alice:example.com", device_id: "VAX", @@ -249,7 +249,7 @@ describe("Secrets", function() { }, }, }); - vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", { + vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { "Osborne2": { user_id: "@alice:example.com", device_id: "Osborne2", @@ -265,7 +265,7 @@ describe("Secrets", function() { const otks = (await osborne2Device.getOneTimeKeys()).curve25519; await osborne2Device.markKeysAsPublished(); - await vax.client._crypto._olmDevice.createOutboundSession( + await vax.client.crypto._olmDevice.createOutboundSession( osborne2Device.deviceCurve25519Key, Object.values(otks)[0], ); diff --git a/spec/unit/crypto/verification/request.spec.js b/spec/unit/crypto/verification/request.spec.js index 113934385ea..11275e6fcdc 100644 --- a/spec/unit/crypto/verification/request.spec.js +++ b/spec/unit/crypto/verification/request.spec.js @@ -49,7 +49,7 @@ describe("verification request integration tests with crypto layer", function() verificationMethods: [verificationMethods.SAS], }, ); - alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() { + alice.client.crypto._deviceList.getRawStoredDevicesForUser = function() { return { Dynabook: { keys: { diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index db517ea5c25..0a643d318ae 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -87,8 +87,8 @@ describe("SAS verification", function() { }, ); - const aliceDevice = alice.client._crypto._olmDevice; - const bobDevice = bob.client._crypto._olmDevice; + const aliceDevice = alice.client.crypto._olmDevice; + const bobDevice = bob.client.crypto._olmDevice; ALICE_DEVICES = { Osborne2: { @@ -114,14 +114,14 @@ describe("SAS verification", function() { }, }; - alice.client._crypto._deviceList.storeDevicesForUser( + alice.client.crypto._deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); alice.client.downloadKeys = () => { return Promise.resolve(); }; - bob.client._crypto._deviceList.storeDevicesForUser( + bob.client.crypto._deviceList.storeDevicesForUser( "@alice:example.com", ALICE_DEVICES, ); bob.client.downloadKeys = () => { @@ -296,9 +296,9 @@ describe("SAS verification", function() { await resetCrossSigningKeys(bob.client); - bob.client._crypto._deviceList.storeCrossSigningForUser( + bob.client.crypto._deviceList.storeCrossSigningForUser( "@alice:example.com", { - keys: alice.client._crypto._crossSigningInfo.keys, + keys: alice.client.crypto._crossSigningInfo.keys, }, ); diff --git a/spec/unit/crypto/verification/util.js b/spec/unit/crypto/verification/util.js index 0ff4d241235..27f44553926 100644 --- a/spec/unit/crypto/verification/util.js +++ b/spec/unit/crypto/verification/util.js @@ -36,7 +36,7 @@ export async function makeTestClients(userInfos, options) { }); const client = clientMap[userId][deviceId]; const decryptionPromise = event.isEncrypted() ? - event.attemptDecryption(client._crypto) : + event.attemptDecryption(client.crypto) : Promise.resolve(); decryptionPromise.then( diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index a7feb8103ca..f627f2476ae 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -145,11 +145,11 @@ describe("MatrixClient", function() { userId: userId, }); // FIXME: We shouldn't be yanking _http like this. - client._http = [ + client.http = [ "authedRequest", "getContentUri", "request", "uploadContent", ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); - client._http.authedRequest.mockImplementation(httpReq); - client._http.request.mockImplementation(httpReq); + client.http.authedRequest.mockImplementation(httpReq); + client.http.request.mockImplementation(httpReq); // set reasonable working defaults acceptKeepalives = true; @@ -166,7 +166,7 @@ describe("MatrixClient", function() { // means they may call /events and then fail an expect() which will fail // a DIFFERENT test (pollution between tests!) - we return unresolved // promises to stop the client from continuing to run. - client._http.authedRequest.mockImplementation(function() { + client.http.authedRequest.mockImplementation(function() { return new Promise(() => {}); }); }); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index e67fff56b5a..fd3eeafca35 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1384,7 +1384,7 @@ describe("Room", function() { } expect(hasThrown).toEqual(true); - client._http.serverResponse = [memberEvent]; + client.http.serverResponse = [memberEvent]; await room.loadMembersIfNeeded(); const memberA = room.getMember("@user_a:bar"); expect(memberA.name).toEqual("User A"); diff --git a/src/client.ts b/src/client.ts index 893ddd2bd7e..37f1d168c2c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -385,21 +385,22 @@ export class MatrixClient extends EventEmitter { public urlPreviewCache: { [key: string]: Promise } = {}; // TODO: Types public unstableClientRelationAggregation = false; public identityServer: IIdentityServerProvider; + public sessionStore: SessionStore; // XXX: Intended private, used in code. + public http: MatrixHttpApi; // XXX: Intended private, used in code. + public crypto: Crypto; // XXX: Intended private, used in code. + public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code. + public callEventHandler: CallEventHandler; // XXX: Intended private, used in code. + public supportsCallTransfer = false; // XXX: Intended private, used in code. + public forceTURN = false; // XXX: Intended private, used in code. + public iceCandidatePoolSize = 0; // XXX: Intended private, used in code. private canSupportVoip = false; - private callEventHandler: CallEventHandler; private peekSync: SyncApi = null; private isGuestAccount = false; private ongoingScrollbacks:{[roomId: string]: {promise?: Promise, errorTs: number}} = {}; // TODO: Types private notifTimelineSet: EventTimelineSet = null; - private crypto: Crypto; private cryptoStore: CryptoStore; - private sessionStore: SessionStore; private verificationMethods: string[]; - private cryptoCallbacks: ICryptoCallbacks; - private forceTURN = false; - private iceCandidatePoolSize = 0; - private supportsCallTransfer = false; private fallbackICEServerAllowed = false; private roomList: RoomList; private syncApi: SyncApi; @@ -429,7 +430,6 @@ export class MatrixClient extends EventEmitter { private exportedOlmDeviceToImport: IOlmDevice; private baseUrl: string; private idBaseUrl: string; - private http: MatrixHttpApi; private txnCtr = 0; constructor(opts: IMatrixClientCreateOpts) { @@ -438,6 +438,8 @@ export class MatrixClient extends EventEmitter { opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl); + this.baseUrl = opts.baseUrl; + this.usingExternalCrypto = opts.usingExternalCrypto; this.store = opts.store || new StubStore(); this.deviceId = opts.deviceId || null; @@ -4168,7 +4170,7 @@ export class MatrixClient extends EventEmitter { * @return {Function} */ public getEventMapper(options?: MapperOpts): EventMapper { - return eventMapperFor(this, options); + return eventMapperFor(this, options || {}); } /** @@ -4264,7 +4266,8 @@ export class MatrixClient extends EventEmitter { * @param {Filter} timelineFilter the timeline filter to pass * @return {Promise} */ - private createMessagesRequest(roomId: string, fromToken: string, limit: number, dir: string, timelineFilter?: Filter): Promise { // TODO: Types + // XXX: Intended private, used in code. + public createMessagesRequest(roomId: string, fromToken: string, limit: number, dir: string, timelineFilter?: Filter): Promise { // TODO: Types const path = utils.encodeUri( "/rooms/$roomId/messages", { $roomId: roomId }, ); @@ -4928,7 +4931,7 @@ export class MatrixClient extends EventEmitter { * @return {Object} searchResults * @private */ - private processRoomEventsSearch(searchResults: any, response: any): any { + public processRoomEventsSearch(searchResults: any, response: any): any { // XXX: Intended private, used in code const room_events = response.search_categories.room_events; searchResults.count = room_events.count; @@ -5143,7 +5146,8 @@ export class MatrixClient extends EventEmitter { return this.turnServersExpiry; } - private async checkTurnServers(): Promise { + // XXX: Intended private, used in code. + public async checkTurnServers(): Promise { if (!this.canSupportVoip) { return; } @@ -5282,7 +5286,7 @@ export class MatrixClient extends EventEmitter { * @param {object} opts the complete set of client options * @return {Promise} for store operation */ - private storeClientOptions() { + public storeClientOptions() { // XXX: Intended private, used in code const primTypes = ["boolean", "string", "number"]; const serializableOpts = Object.entries(this.clientOpts) .filter(([key, value]) => { diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 00788917e58..e2af6ce3b5b 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -739,7 +739,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) // it. We return here in order to test. return new Promise((resolve, reject) => { const client = baseApis; - const original = client._crypto._crossSigningInfo; + const original = client.crypto._crossSigningInfo; // We already have all of the infrastructure we need to validate and // cache cross-signing keys, so instead of replicating that, here we set @@ -775,7 +775,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) // also request and cache the key backup key const backupKeyPromise = new Promise(async resolve => { - const cachedKey = await client._crypto.getSessionBackupPrivateKey(); + const cachedKey = await client.crypto.getSessionBackupPrivateKey(); if (!cachedKey) { logger.info("No cached backup key found. Requesting..."); const secretReq = client.requestSecret( @@ -785,7 +785,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) logger.info("Got key backup key, decoding..."); const decodedKey = decodeBase64(base64Key); logger.info("Decoded backup key, storing..."); - client._crypto.storeSessionBackupPrivateKey( + client.crypto.storeSessionBackupPrivateKey( Uint8Array.from(decodedKey), ); logger.info("Backup key stored. Starting backup restore..."); diff --git a/src/crypto/EncryptionSetup.js b/src/crypto/EncryptionSetup.js index d5421ea5038..d7f861a0335 100644 --- a/src/crypto/EncryptionSetup.js +++ b/src/crypto/EncryptionSetup.js @@ -204,7 +204,7 @@ export class EncryptionSetupOperation { // The backup is trusted because the user provided the private key. // Sign the backup with the cross signing key so the key backup can // be trusted via cross-signing. - await baseApis._http.authedRequest( + await baseApis.http.authedRequest( undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version, undefined, { algorithm: this._keyBackupInfo.algorithm, @@ -214,7 +214,7 @@ export class EncryptionSetupOperation { ); } else { // add new key backup - await baseApis._http.authedRequest( + await baseApis.http.authedRequest( undefined, "POST", "/room_keys/version", undefined, this._keyBackupInfo, { prefix: PREFIX_UNSTABLE }, diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index 5b20bc93472..f835ef26bcf 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -480,11 +480,11 @@ export class SecretStorage extends EventEmitter { }; const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key, + sender_key: this._baseApis.crypto._olmDevice.deviceCurve25519Key, ciphertext: {}, }; await olmlib.ensureOlmSessionsForDevices( - this._baseApis._crypto._olmDevice, + this._baseApis.crypto._olmDevice, this._baseApis, { [sender]: [ @@ -496,7 +496,7 @@ export class SecretStorage extends EventEmitter { encryptedContent.ciphertext, this._baseApis.getUserId(), this._baseApis.deviceId, - this._baseApis._crypto._olmDevice, + this._baseApis.crypto._olmDevice, sender, this._baseApis.getStoredDevice(sender, deviceId), payload, @@ -527,7 +527,7 @@ export class SecretStorage extends EventEmitter { if (requestControl) { // make sure that the device that sent it is one of the devices that // we requested from - const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey( + const deviceInfo = this._baseApis.crypto._deviceList.getDeviceByIdentityKey( olmlib.OLM_ALGORITHM, event.getSenderKey(), ); diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 38477d59a3a..81d9d17bf67 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -205,7 +205,7 @@ export class DehydrationManager { } logger.log("Uploading account to server"); - const dehydrateResult = await this.crypto._baseApis._http.authedRequest( + const dehydrateResult = await this.crypto._baseApis.http.authedRequest( undefined, "PUT", "/dehydrated_device", @@ -268,7 +268,7 @@ export class DehydrationManager { } logger.log("Uploading keys to server"); - await this.crypto._baseApis._http.authedRequest( + await this.crypto._baseApis.http.authedRequest( undefined, "POST", "/keys/upload/" + encodeURI(deviceId), diff --git a/src/crypto/index.js b/src/crypto/index.js index 6bce27ef4df..82df9dda133 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -232,7 +232,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId, // processing the response. this._sendKeyRequestsImmediately = false; - const cryptoCallbacks = this._baseApis._cryptoCallbacks || {}; + const cryptoCallbacks = this._baseApis.cryptoCallbacks || {}; const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this._olmDevice); this._crossSigningInfo = new CrossSigningInfo( @@ -495,7 +495,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({ } = {}) { logger.log("Bootstrapping cross-signing"); - const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks; + const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks; const builder = new EncryptionSetupBuilder( this._baseApis.store.accountData, delegateCryptoCallbacks, @@ -579,7 +579,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({ const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys; if ( crossSigningPrivateKeys.size && - !this._baseApis._cryptoCallbacks.saveCrossSigningKeys + !this._baseApis.cryptoCallbacks.saveCrossSigningKeys ) { const secretStorage = new SecretStorage( builder.accountDataClientAdapter, @@ -646,7 +646,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ getKeyBackupPassphrase, } = {}) { logger.log("Bootstrapping Secure Secret Storage"); - const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks; + const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks; const builder = new EncryptionSetupBuilder( this._baseApis.store.accountData, delegateCryptoCallbacks, @@ -681,7 +681,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ const ensureCanCheckPassphrase = async (keyId, keyInfo) => { if (!keyInfo.mac) { - const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey( + const key = await this._baseApis.cryptoCallbacks.getSecretStorageKey( { keys: { [keyId]: keyInfo } }, "", ); if (key) { @@ -801,7 +801,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ // If we have cross-signing private keys cached, store them in secret // storage if they are not there already. if ( - !this._baseApis._cryptoCallbacks.saveCrossSigningKeys && + !this._baseApis.cryptoCallbacks.saveCrossSigningKeys && await this.isCrossSigningReady() && (newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage)) ) { @@ -1071,7 +1071,7 @@ Crypto.prototype._afterCrossSigningLocalKeyChange = async function() { upload({ shouldEmit: true }); const shouldUpgradeCb = ( - this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications + this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications ); if (shouldUpgradeCb) { logger.info("Starting device verification upgrade"); @@ -1509,7 +1509,7 @@ Crypto.prototype._storeTrustedSelfKeys = async function(keys) { */ Crypto.prototype._checkDeviceVerifications = async function(userId) { const shouldUpgradeCb = ( - this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications + this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications ); if (!shouldUpgradeCb) { // Upgrading skipped when callback is not present. diff --git a/src/crypto/verification/Base.js b/src/crypto/verification/Base.js index 929654cf95e..9f03329c7b2 100644 --- a/src/crypto/verification/Base.js +++ b/src/crypto/verification/Base.js @@ -292,7 +292,7 @@ export class VerificationBase extends EventEmitter { await verifier(keyId, device, keyInfo); verifiedDevices.push(deviceId); } else { - const crossSigningInfo = this._baseApis._crypto._deviceList + const crossSigningInfo = this._baseApis.crypto._deviceList .getStoredCrossSigningForUser(userId); if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { await verifier(keyId, DeviceInfo.fromStorage({ diff --git a/src/models/relations.js b/src/models/relations.js index 1e24d534ef6..50b4ffd656f 100644 --- a/src/models/relations.js +++ b/src/models/relations.js @@ -332,7 +332,7 @@ export class Relations extends EventEmitter { }, null); if (lastReplacement?.shouldAttemptDecryption()) { - await lastReplacement.attemptDecryption(this._room._client._crypto); + await lastReplacement.attemptDecryption(this._room._client.crypto); } else if (lastReplacement?.isBeingDecrypted()) { await lastReplacement._decryptionPromise; } diff --git a/src/models/room.js b/src/models/room.js index 8050435876d..2e4229c7fba 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -192,13 +192,13 @@ export function Room(roomId, client, myUserId, opts) { if (this._opts.pendingEventOrdering == "detached") { this._pendingEventList = []; - const serializedPendingEventList = client._sessionStore.store.getItem(pendingEventsKey(this.roomId)); + const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId)); if (serializedPendingEventList) { JSON.parse(serializedPendingEventList) .forEach(async serializedEvent => { const event = new MatrixEvent(serializedEvent); if (event.getType() === "m.room.encrypted") { - await event.attemptDecryption(this._client._crypto); + await event.attemptDecryption(this._client.crypto); } event.setStatus(EventStatus.NOT_SENT); this.addPendingEvent(event, event.getTxnId()); @@ -255,7 +255,7 @@ Room.prototype.decryptCriticalEvents = function() { .slice(readReceiptTimelineIndex) .filter(event => event.shouldAttemptDecryption()) .reverse() - .map(event => event.attemptDecryption(this._client._crypto, { isRetry: true })); + .map(event => event.attemptDecryption(this._client.crypto, { isRetry: true })); return Promise.allSettled(decryptionPromises); }; @@ -272,7 +272,7 @@ Room.prototype.decryptAllEvents = function() { .getEvents() .filter(event => event.shouldAttemptDecryption()) .reverse() - .map(event => event.attemptDecryption(this._client._crypto, { isRetry: true })); + .map(event => event.attemptDecryption(this._client.crypto, { isRetry: true })); return Promise.allSettled(decryptionPromises); }; @@ -632,7 +632,7 @@ Room.prototype._loadMembersFromServer = async function() { }); const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, { $roomId: this.roomId }); - const http = this._client._http; + const http = this._client.http; const response = await http.authedRequest(undefined, "GET", path); return response.chunk; }; @@ -674,7 +674,7 @@ Room.prototype.loadMembersIfNeeded = function() { this.currentState.setOutOfBandMembers(result.memberEvents); // now the members are loaded, start to track the e2e devices if needed if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) { - this._client._crypto.trackRoomDevices(this.roomId); + this._client.crypto.trackRoomDevices(this.roomId); } return result.fromServer; }).catch((err) => { @@ -1387,7 +1387,7 @@ Room.prototype._savePendingEvents = function() { return isEventEncrypted || !isRoomEncrypted; }); - const { store } = this._client._sessionStore; + const { store } = this._client.sessionStore; if (this._pendingEventList.length > 0) { store.setItem( pendingEventsKey(this.roomId), diff --git a/src/sync.js b/src/sync.js index 8ad4af30b15..70a385c3580 100644 --- a/src/sync.js +++ b/src/sync.js @@ -209,7 +209,7 @@ SyncApi.prototype.syncLeftRooms = function() { getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter, ).then(function(filterId) { qps.filter = filterId; - return client._http.authedRequest( + return client.http.authedRequest( undefined, "GET", "/sync", qps, undefined, localTimeoutMs, ); }).then(function(data) { @@ -349,7 +349,7 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) { const self = this; // FIXME: gut wrenching; hard-coded timeout values - this.client._http.authedRequest(undefined, "GET", "/events", { + this.client.http.authedRequest(undefined, "GET", "/events", { room_id: peekRoom.roomId, timeout: 30 * 1000, from: token, @@ -551,7 +551,7 @@ SyncApi.prototype.sync = function() { } try { debuglog("Storing client options..."); - await this.client._storeClientOptions(); + await this.client.storeClientOptions(); debuglog("Stored client options"); } catch (err) { logger.error("Storing client options failed", err); @@ -815,7 +815,7 @@ SyncApi.prototype._sync = async function(syncOptions) { SyncApi.prototype._doSyncRequest = function(syncOptions, syncToken) { const qps = this._getSyncParams(syncOptions, syncToken); - return this.client._http.authedRequest( + return this.client.http.authedRequest( undefined, "GET", "/sync", qps, undefined, qps.timeout + BUFFER_PERIOD_MS, ); @@ -1426,7 +1426,7 @@ SyncApi.prototype._pokeKeepAlive = function(connDidFail) { } } - this.client._http.request( + this.client.http.request( undefined, // callback "GET", "/_matrix/client/versions", undefined, // queryParams @@ -1671,19 +1671,9 @@ SyncApi.prototype._processEventsForNotifs = function(room, timelineEventList) { * @return {string} */ SyncApi.prototype._getGuestFilter = function() { - const guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching - if (!guestRooms) { - return "{}"; - } - // we just need to specify the filter inline if we're a guest because guests - // can't create filters. - return JSON.stringify({ - room: { - timeline: { - limit: 20, - }, - }, - }); + // Dev note: This used to be conditional to return a filter of 20 events maximum, but + // the condition never went to the other branch. This is now hardcoded. + return "{}"; }; /** diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 595757ecd6a..c3b3f09f13c 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -511,7 +511,7 @@ export class MatrixCall extends EventEmitter { // make sure we have valid turn creds. Unless something's gone wrong, it should // poll and keep the credentials valid so this should be instant. - const haveTurnCreds = await this.client._checkTurnServers(); + const haveTurnCreds = await this.client.checkTurnServers(); if (!haveTurnCreds) { logger.warn("Failed to get TURN credentials! Proceeding with call anyway..."); } @@ -846,7 +846,7 @@ export class MatrixCall extends EventEmitter { }, } as MCallAnswer; - if (this.client._supportsCallTransfer) { + if (this.client.supportsCallTransfer) { answerContent.capabilities = { 'm.call.transferee': true, } @@ -1181,7 +1181,7 @@ export class MatrixCall extends EventEmitter { content.description = this.peerConn.localDescription; } - if (this.client._supportsCallTransfer) { + if (this.client.supportsCallTransfer) { content.capabilities = { 'm.call.transferee': true, } @@ -1579,14 +1579,14 @@ export class MatrixCall extends EventEmitter { private async placeCallWithConstraints(constraints: MediaStreamConstraints) { logger.log("Getting user media with constraints", constraints); // XXX Find a better way to do this - this.client._callEventHandler.calls.set(this.callId, this); + this.client.callEventHandler.calls.set(this.callId, this); this.setState(CallState.WaitLocalMedia); this.direction = CallDirection.Outbound; this.config = constraints; // make sure we have valid turn creds. Unless something's gone wrong, it should // poll and keep the credentials valid so this should be instant. - const haveTurnCreds = await this.client._checkTurnServers(); + const haveTurnCreds = await this.client.checkTurnServers(); if (!haveTurnCreds) { logger.warn("Failed to get TURN credentials! Proceeding with call anyway..."); } @@ -1608,7 +1608,7 @@ export class MatrixCall extends EventEmitter { const pc = new window.RTCPeerConnection({ iceTransportPolicy: this.forceTURN ? 'relay' : undefined, iceServers: this.turnServers, - iceCandidatePoolSize: this.client._iceCandidatePoolSize, + iceCandidatePoolSize: this.client.iceCandidatePoolSize, }); // 'connectionstatechange' would be better, but firefox doesn't implement that. @@ -1813,7 +1813,7 @@ export function createNewMatrixCall(client: any, roomId: string, options?: CallO roomId: roomId, turnServers: client.getTurnServers(), // call level options - forceTURN: client._forceTURN || optionsForceTURN, + forceTURN: client.forceTURN || optionsForceTURN, }; const call = new MatrixCall(opts); diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 6653d12a36d..4921c5dc9ab 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -155,7 +155,7 @@ export class CallEventHandler { const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now(); logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); call = createNewMatrixCall(this.client, event.getRoomId(), { - forceTURN: this.client._forceTURN, + forceTURN: this.client.forceTURN, }); if (!call) { logger.log( From a1a6ec6dfac897fe4a1dac196e969874ca363db3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 20:57:48 -0600 Subject: [PATCH 15/32] Fix remaining hot paths --- src/client.ts | 29 ++++++++++++++--------------- src/crypto/SecretStorage.js | 2 +- src/crypto/index.js | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/client.ts b/src/client.ts index 37f1d168c2c..276f0c787f8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -667,7 +667,7 @@ export class MatrixClient extends EventEmitter { } return this.canResetTimelineCallback(roomId); }; - this.syncApi = new SyncApi(this, opts); + this.syncApi = new SyncApi(this, this.clientOpts); this.syncApi.sync(); if (opts.clientWellKnownPollPeriod !== undefined) { @@ -1420,6 +1420,13 @@ export class MatrixClient extends EventEmitter { return this.crypto.beginKeyVerification(method, userId, deviceId); } + public checkSecretStorageKey(key: any, info: any): Promise { // TODO: Types + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.checkSecretStorageKey(key, info); + } + /** * Set the global override for whether the client should ever send encrypted * messages to unverified devices. This provides the default for rooms which @@ -4876,14 +4883,7 @@ export class MatrixClient extends EventEmitter { highlights: [], }; - // TODO: @@TR: wtf is this - // prev: - /* - return this.search({ body: body }).then( - this._processRoomEventsSearch.bind(this, searchResults), - ); - */ - return this.search({ body: body }).then(res => this.processRoomEventsSearch(res, searchResults)); + return this.search({ body: body }).then(res => this.processRoomEventsSearch(searchResults, res)); } /** @@ -4911,12 +4911,11 @@ export class MatrixClient extends EventEmitter { next_batch: searchResults.next_batch, }; - // TODO: @@TR: wtf - const promise = this.search(searchOpts).then( - this.processRoomEventsSearch.bind(this, searchResults), - ).finally(() => { - searchResults.pendingRequest = null; - }); + const promise = this.search(searchOpts) + .then(res => this.processRoomEventsSearch(searchResults, res)) + .finally(() => { + searchResults.pendingRequest = null; + }); searchResults.pendingRequest = promise; return promise; diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index f835ef26bcf..a1ed647f06b 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -444,7 +444,7 @@ export class SecretStorage extends EventEmitter { && this._incomingRequests[deviceId][content.request_id]) { logger.info("received request cancellation for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")"); - this.baseApis.emit("crypto.secrets.requestCancelled", { + this._baseApis.emit("crypto.secrets.requestCancelled", { user_id: sender, device_id: deviceId, request_id: content.request_id, diff --git a/src/crypto/index.js b/src/crypto/index.js index 82df9dda133..a36e78a2afa 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -495,7 +495,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({ } = {}) { logger.log("Bootstrapping cross-signing"); - const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks; + const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks; const builder = new EncryptionSetupBuilder( this._baseApis.store.accountData, delegateCryptoCallbacks, From 5c55dce13e26d75ae40454c333ac395e037278fa Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:03:17 -0600 Subject: [PATCH 16/32] Lint pass 1 --- src/@types/partials.ts | 2 +- src/@types/requests.ts | 13 ++++++++----- src/client.ts | 18 +++++++++++------- src/crypto/dehydration.ts | 4 ++-- src/crypto/keybackup.ts | 12 ++++++------ src/event-mapper.ts | 4 +++- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/@types/partials.ts b/src/@types/partials.ts index d2b286e8303..4daa935d037 100644 --- a/src/@types/partials.ts +++ b/src/@types/partials.ts @@ -17,7 +17,7 @@ limitations under the License. export interface IImageInfo { size?: number; mimetype?: string; - thumbnail_info?: { + thumbnail_info?: { // eslint-disable-line camelcase w?: number; h?: number; size?: number; diff --git a/src/@types/requests.ts b/src/@types/requests.ts index 6f6795eea6e..48c37967001 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -40,12 +40,12 @@ export interface IRedactOpts { } export interface ISendEventResponse { - event_id: string; + event_id: string; // eslint-disable-line camelcase } export interface IPresenceOpts { presence: "online" | "offline" | "unavailable"; - status_msg?: string; + status_msg?: string; // eslint-disable-line camelcase } export interface IPaginateOpts { @@ -69,18 +69,21 @@ export interface IEventSearchOpts { } export interface ICreateRoomOpts { - room_alias_name?: string; + room_alias_name?: string; // eslint-disable-line camelcase visibility?: "public" | "private"; name?: string; topic?: string; - invite_3pid?: any[]; // TODO: Types + // TODO: Types (next line) + invite_3pid?: any[]; // eslint-disable-line camelcase } export interface IRoomDirectoryOptions { server?: string; limit?: number; since?: string; - filter?: any & {generic_search_term: string}; // TODO: Types + + // TODO: Proper types + filter?: any & {generic_search_term: string}; // eslint-disable-line camelcase } export interface IUploadOpts { diff --git a/src/client.ts b/src/client.ts index 276f0c787f8..f316fa1c172 100644 --- a/src/client.ts +++ b/src/client.ts @@ -45,7 +45,7 @@ import { PREFIX_MEDIA_R0, PREFIX_R0, PREFIX_UNSTABLE, - retryNetworkOperation + retryNetworkOperation, } from "./http-api"; import { Crypto, DeviceInfo, fixBackupKey, isCryptoAvailable } from './crypto'; import { decodeRecoveryKey } from './crypto/recoverykey'; @@ -61,7 +61,7 @@ import { IKeyBackupRoomSessions, IKeyBackupSession, IKeyBackupTrustInfo, - IKeyBackupVersion + IKeyBackupVersion, } from "./crypto/keybackup"; import { PkDecryption } from "olm"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; @@ -86,7 +86,7 @@ import { IEncryptedEventInfo, IImportRoomKeysOpts, IRecoveryKey, - ISecretStorageKey + ISecretStorageKey, } from "./crypto/api"; import { CrossSigningInfo, UserTrustLevel } from "./crypto/CrossSigning"; import { Room } from "./models/Room"; @@ -99,7 +99,8 @@ import { IPresenceOpts, IRedactOpts, IRoomDirectoryOptions, ISearchOpts, - ISendEventResponse, IUploadOpts + ISendEventResponse, + IUploadOpts, } from "./@types/requests"; import { EventType } from "./@types/event"; import { IImageInfo } from "./@types/partials"; @@ -397,7 +398,7 @@ export class MatrixClient extends EventEmitter { private canSupportVoip = false; private peekSync: SyncApi = null; private isGuestAccount = false; - private ongoingScrollbacks:{[roomId: string]: {promise?: Promise, errorTs: number}} = {}; // TODO: Types + private ongoingScrollbacks: {[roomId: string]: {promise?: Promise, errorTs: number}} = {}; // TODO: Types private notifTimelineSet: EventTimelineSet = null; private cryptoStore: CryptoStore; private verificationMethods: string[]; @@ -816,7 +817,11 @@ export class MatrixClient extends EventEmitter { * dehydrated device. * @return {Promise} A promise that resolves when the dehydrated device is stored. */ - public async setDehydrationKey(key: Uint8Array, keyInfo: IDehydratedDeviceKeyInfo, deviceDisplayName?: string): Promise { + public async setDehydrationKey( + key: Uint8Array, + keyInfo: IDehydratedDeviceKeyInfo, + deviceDisplayName?: string + ): Promise { if (!this.crypto) { logger.warn('not dehydrating device if crypto is not enabled'); return; @@ -7798,7 +7803,6 @@ export class MatrixClient extends EventEmitter { } } - /** * Fires whenever the SDK receives a new event. *

diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 81d9d17bf67..5e431be62cb 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -25,8 +25,8 @@ import { ISecretStorageKeyInfo } from "../matrix"; type Signatures = Record>; export interface IDehydratedDevice { - device_id: string; - device_data: ISecretStorageKeyInfo & { + device_id: string; // eslint-disable-line camelcase + device_data: ISecretStorageKeyInfo & { // eslint-disable-line camelcase algorithm: string; account: string; // pickle }; diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index 1065f256a5d..9c816384e22 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -18,10 +18,10 @@ import { ISignatures } from "../@types/signed"; import { DeviceInfo } from "./deviceinfo"; export interface IKeyBackupSession { - first_message_index: number; - forwarded_count: number; - is_verified: boolean; - session_data: { + first_message_index: number; // eslint-disable-line camelcase + forwarded_count: number; // eslint-disable-line camelcase + is_verified: boolean; // eslint-disable-line camelcase + session_data: { // eslint-disable-line camelcase ciphertext: string; ephemeral: string; mac: string; @@ -34,8 +34,8 @@ export interface IKeyBackupRoomSessions { export interface IKeyBackupVersion { algorithm: string; - auth_data: { - public_key: string; + auth_data: { // eslint-disable-line camelcase + public_key: string; // eslint-disable-line camelcase signatures: ISignatures; }; count: number; diff --git a/src/event-mapper.ts b/src/event-mapper.ts index bc04f5e7187..84b8b306fc5 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { MatrixClient } from "./client"; -import {MatrixEvent} from "./models/event"; +import { MatrixEvent } from "./models/event"; export type EventMapper = (obj: any) => MatrixEvent; @@ -27,6 +27,7 @@ export interface MapperOpts { export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper { const preventReEmit = Boolean(options.preventReEmit); const decrypt = options.decrypt !== false; + function mapper(plainOldJsObject) { const event = new MatrixEvent(plainOldJsObject); if (event.isEncrypted()) { @@ -44,5 +45,6 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event } return event; } + return mapper; } From 263e55f25da43c4f8eb64c544b0b965ef6ee9a4e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:05:20 -0600 Subject: [PATCH 17/32] Build pass 1 --- src/client.ts | 4 ++-- src/webrtc/callFeed.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index f316fa1c172..aff5813c0da 100644 --- a/src/client.ts +++ b/src/client.ts @@ -63,7 +63,7 @@ import { IKeyBackupTrustInfo, IKeyBackupVersion, } from "./crypto/keybackup"; -import { PkDecryption } from "olm"; +import { PkDecryption } from "@matrix-org/olm"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import type Request from "request"; import { MatrixScheduler } from "./scheduler"; @@ -398,7 +398,7 @@ export class MatrixClient extends EventEmitter { private canSupportVoip = false; private peekSync: SyncApi = null; private isGuestAccount = false; - private ongoingScrollbacks: {[roomId: string]: {promise?: Promise, errorTs: number}} = {}; // TODO: Types + private ongoingScrollbacks: {[roomId: string]: {promise?: Promise, errorTs?: number}} = {}; // TODO: Types private notifTimelineSet: EventTimelineSet = null; private cryptoStore: CryptoStore; private verificationMethods: string[]; diff --git a/src/webrtc/callFeed.ts b/src/webrtc/callFeed.ts index 762fe63f8cd..f8e33f12057 100644 --- a/src/webrtc/callFeed.ts +++ b/src/webrtc/callFeed.ts @@ -16,7 +16,7 @@ limitations under the License. import EventEmitter from "events"; import { SDPStreamMetadataPurpose } from "./callEventTypes"; -import MatrixClient from "../client" +import { MatrixClient } from "../client" import { RoomMember } from "../models/room-member"; export enum CallFeedEvent { From 191f73e0d004a7c78ecb1ed2989e3f9008af6f2d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:07:11 -0600 Subject: [PATCH 18/32] Appease typescript --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index aff5813c0da..fca9931add5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -89,7 +89,7 @@ import { ISecretStorageKey, } from "./crypto/api"; import { CrossSigningInfo, UserTrustLevel } from "./crypto/CrossSigning"; -import { Room } from "./models/Room"; +import type { Room } from "./models/Room"; import { ICreateRoomOpts, IEventSearchOpts, From 2f87a4859e1f5e7368a5aa2cad04fc014c7326ad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:24:01 -0600 Subject: [PATCH 19/32] Lint pass 2 --- src/client.ts | 432 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 352 insertions(+), 80 deletions(-) diff --git a/src/client.ts b/src/client.ts index fca9931add5..eca1180350b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -820,7 +820,7 @@ export class MatrixClient extends EventEmitter { public async setDehydrationKey( key: Uint8Array, keyInfo: IDehydratedDeviceKeyInfo, - deviceDisplayName?: string + deviceDisplayName?: string, ): Promise { if (!this.crypto) { logger.warn('not dehydrating device if crypto is not enabled'); @@ -841,7 +841,11 @@ export class MatrixClient extends EventEmitter { * dehydrated device. * @return {Promise} the device id of the newly created dehydrated device */ - public async createDehydratedDevice(key: Uint8Array, keyInfo: IDehydratedDeviceKeyInfo, deviceDisplayName?: string): Promise { + public async createDehydratedDevice( + key: Uint8Array, + keyInfo: IDehydratedDeviceKeyInfo, + deviceDisplayName?: string, + ): Promise { if (!this.crypto) { logger.warn('not dehydrating device if crypto is not enabled'); return; @@ -1243,7 +1247,10 @@ export class MatrixClient extends EventEmitter { * @return {Promise} A promise which resolves to a map userId->deviceId->{@link * module:crypto~DeviceInfo|DeviceInfo}. */ - public downloadKeys(userIds: string[], forceDownload: boolean): Promise>> { + public downloadKeys( + userIds: string[], + forceDownload: boolean, + ): Promise>> { if (!this.crypto) { return Promise.reject(new Error("End-to-end encryption disabled")); } @@ -1341,7 +1348,13 @@ export class MatrixClient extends EventEmitter { return this.setDeviceVerification(userId, deviceId, null, null, known); } - private async setDeviceVerification(userId: string, deviceId: string, verified: boolean, blocked: boolean, known: boolean): Promise { + private async setDeviceVerification( + userId: string, + deviceId: string, + verified: boolean, + blocked: boolean, + known: boolean, + ): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2167,7 +2180,10 @@ export class MatrixClient extends EventEmitter { * additionally has a 'recovery_key' member with the user-facing recovery key string. */ // TODO: Verify types - public async prepareKeyBackupVersion(password: string, opts: IKeyBackupPrepareOpts = { secureSecretStorage: false }): Promise { + public async prepareKeyBackupVersion( + password: string, + opts: IKeyBackupPrepareOpts = { secureSecretStorage: false }, + ): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2398,7 +2414,13 @@ export class MatrixClient extends EventEmitter { * key counts. */ // TODO: Types - public async restoreKeyBackupWithPassword(password: string, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, opts: IKeyBackupRestoreOpts): Promise { + public async restoreKeyBackupWithPassword( + password: string, + targetRoomId: string, + targetSessionId: string, + backupInfo: IKeyBackupVersion, + opts: IKeyBackupRestoreOpts, + ): Promise { const privKey = await keyFromAuthData(backupInfo.auth_data, password); return this.restoreKeyBackup( privKey, targetRoomId, targetSessionId, backupInfo, opts, @@ -2419,7 +2441,12 @@ export class MatrixClient extends EventEmitter { * key counts. */ // TODO: Types - public async restoreKeyBackupWithSecretStorage(backupInfo: IKeyBackupVersion, targetRoomId: string, targetSessionId: string, opts: IKeyBackupRestoreOpts): Promise { + public async restoreKeyBackupWithSecretStorage( + backupInfo: IKeyBackupVersion, + targetRoomId: string, + targetSessionId: string, + opts: IKeyBackupRestoreOpts, + ): Promise { const storedKey = await this.getSecret("m.megolm_backup.v1"); // ensure that the key is in the right format. If not, fix the key and @@ -2451,7 +2478,13 @@ export class MatrixClient extends EventEmitter { * key counts. */ // TODO: Types - public restoreKeyBackupWithRecoveryKey(recoveryKey: string, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, opts: IKeyBackupRestoreOpts): Promise { + public restoreKeyBackupWithRecoveryKey( + recoveryKey: string, + targetRoomId: string, + targetSessionId: string, + backupInfo: IKeyBackupVersion, + opts: IKeyBackupRestoreOpts, + ): Promise { const privKey = decodeRecoveryKey(recoveryKey); return this.restoreKeyBackup( privKey, targetRoomId, targetSessionId, backupInfo, opts, @@ -2459,7 +2492,12 @@ export class MatrixClient extends EventEmitter { } // TODO: Types - public async restoreKeyBackupWithCache(targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, opts: IKeyBackupRestoreOpts): Promise { + public async restoreKeyBackupWithCache( + targetRoomId: string, + targetSessionId: string, + backupInfo: IKeyBackupVersion, + opts: IKeyBackupRestoreOpts, + ): Promise { const privKey = await this.crypto.getSessionBackupPrivateKey(); if (!privKey) { throw new Error("Couldn't get key"); @@ -2469,7 +2507,13 @@ export class MatrixClient extends EventEmitter { ); } - private restoreKeyBackup(privKey: Uint8Array, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, opts: IKeyBackupRestoreOpts): Promise { + private restoreKeyBackup( + privKey: Uint8Array, + targetRoomId: string, + targetSessionId: string, + backupInfo: IKeyBackupVersion, + opts: IKeyBackupRestoreOpts, + ): Promise { const { cacheCompleteCallback, progressCallback } = opts; if (!this.crypto) { @@ -2495,7 +2539,7 @@ export class MatrixClient extends EventEmitter { // doesn't match the one in the auth_data, the user has entered // a different recovery key / the wrong passphrase. if (backupPubKey !== backupInfo.auth_data.public_key) { - return Promise.reject({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }); + return Promise.reject(new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY })); } // Cache the key, if possible. @@ -3015,7 +3059,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public setPowerLevel(roomId: string, userId: string, powerLevel: number, event: MatrixEvent, callback?: Callback): Promise { + public setPowerLevel( + roomId: string, + userId: string, + powerLevel: number, + event: MatrixEvent, + callback?: Callback, + ): Promise { let content = { users: {}, }; @@ -3042,7 +3092,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendEvent(roomId: string, eventType: string, content: any, txnId?: string, callback?: Callback): Promise { + public sendEvent( + roomId: string, + eventType: string, + content: any, + txnId?: string, + callback?: Callback, + ): Promise { return this.sendCompleteEvent(roomId, { type: eventType, content }, txnId, callback); } @@ -3054,7 +3110,12 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - private sendCompleteEvent(roomId: string, eventObject: any, txnId: string, callback?: Callback): Promise { + private sendCompleteEvent( + roomId: string, + eventObject: any, + txnId: string, + callback?: Callback, + ): Promise { if (utils.isFunction(txnId)) { callback = txnId as any as Callback; // convert for legacy txnId = undefined; @@ -3248,7 +3309,6 @@ export class MatrixClient extends EventEmitter { event.setTxnId(txnId); } - const pathParams = { $roomId: event.getRoomId(), $eventType: event.getWireType(), @@ -3292,7 +3352,12 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public redactEvent(roomId: string, eventId: string, txnId?: string, cbOrOpts?: Callback | IRedactOpts): Promise { + public redactEvent( + roomId: string, + eventId: string, + txnId?: string, + cbOrOpts?: Callback | IRedactOpts, + ): Promise { const opts = typeof (cbOrOpts) === 'object' ? cbOrOpts : {}; const reason = opts.reason; const callback = typeof (cbOrOpts) === 'function' ? cbOrOpts : undefined; @@ -3327,7 +3392,12 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendTextMessage(roomId: string, body: string, txnId?: string, callback?: Callback): Promise { + public sendTextMessage( + roomId: string, + body: string, + txnId?: string, + callback?: Callback, + ): Promise { const content = ContentHelpers.makeTextMessage(body); return this.sendMessage(roomId, content, txnId, callback); } @@ -3353,7 +3423,12 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendEmoteMessage(roomId: string, body: string, txnId?: string, callback?: Callback): Promise { + public sendEmoteMessage( + roomId: string, + body: string, + txnId?: string, + callback?: Callback, + ): Promise { const content = ContentHelpers.makeEmoteMessage(body); return this.sendMessage(roomId, content, txnId, callback); } @@ -3367,7 +3442,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendImageMessage(roomId: string, url: string, info?: IImageInfo, text = "Image", callback?: Callback): Promise { + public sendImageMessage( + roomId: string, + url: string, + info?: IImageInfo, + text = "Image", + callback?: Callback, + ): Promise { if (utils.isFunction(text)) { callback = text as any as Callback; // legacy text = undefined; @@ -3390,7 +3471,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendStickerMessage(roomId: string, url: string, info?: IImageInfo, text = "Sticker", callback?: Callback): Promise { + public sendStickerMessage( + roomId: string, + url: string, + info?: IImageInfo, + text = "Sticker", + callback?: Callback, + ): Promise { if (utils.isFunction(text)) { callback = text as any as Callback; // legacy text = undefined; @@ -3411,7 +3498,12 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendHtmlMessage(roomId: string, body: string, htmlBody: string, callback?: Callback): Promise { + public sendHtmlMessage( + roomId: string, + body: string, + htmlBody: string, + callback?: Callback, + ): Promise { const content = ContentHelpers.makeHtmlMessage(body, htmlBody); return this.sendMessage(roomId, content, undefined, callback); } @@ -3424,7 +3516,12 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendHtmlNotice(roomId: string, body: string, htmlBody: string, callback?: Callback): Promise { + public sendHtmlNotice( + roomId: string, + body: string, + htmlBody: string, + callback?: Callback, + ): Promise { const content = ContentHelpers.makeHtmlNotice(body, htmlBody); return this.sendMessage(roomId, content, undefined, callback); } @@ -3437,7 +3534,12 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendHtmlEmote(roomId: string, body: string, htmlBody: string, callback?: Callback): Promise { + public sendHtmlEmote( + roomId: string, + body: string, + htmlBody: string, + callback?: Callback, + ): Promise { const content = ContentHelpers.makeHtmlEmote(body, htmlBody); return this.sendMessage(roomId, content, undefined, callback); } @@ -3523,7 +3625,12 @@ export class MatrixClient extends EventEmitter { * This property is unstable and may change in the future. * @return {Promise} Resolves: the empty object, {}. */ - public async setRoomReadMarkers(roomId: string, rmEventId: string, rrEvent: MatrixEvent, opts: { hidden?: boolean }): Promise { // TODO: Types + public async setRoomReadMarkers( + roomId: string, + rmEventId: string, + rrEvent: MatrixEvent, + opts: { hidden?: boolean }, + ): Promise { // TODO: Types const room = this.getRoom(roomId); if (room && room.hasPendingEvent(rmEventId)) { throw new Error(`Cannot set read marker to a pending event (${rmEventId})`); @@ -3846,10 +3953,9 @@ export class MatrixClient extends EventEmitter { if (!deleteRoom) { return promise; } - const self = this; return promise.then((response) => { - self.store.removeRoom(roomId); - self.emit("deleteRoom", roomId); + this.store.removeRoom(roomId); + this.emit("deleteRoom", roomId); return response; }); } @@ -3901,7 +4007,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - private setMembershipState(roomId: string, userId: string, membershipValue: string, reason?: string, callback?: Callback) { + private setMembershipState( + roomId: string, + userId: string, + membershipValue: string, + reason?: string, + callback?: Callback, + ) { if (utils.isFunction(reason)) { callback = reason as any as Callback; // legacy reason = undefined; @@ -3918,7 +4030,13 @@ export class MatrixClient extends EventEmitter { }); } - private membershipChange(roomId: string, userId: string, membership: string, reason?: string, callback?: Callback): Promise { + private membershipChange( + roomId: string, + userId: string, + membership: string, + reason?: string, + callback?: Callback, + ): Promise { if (utils.isFunction(reason)) { callback = reason as any as Callback; // legacy reason = undefined; @@ -4017,7 +4135,13 @@ export class MatrixClient extends EventEmitter { * anyone they share a room with. If false, will return null for such URLs. * @return {?string} the avatar URL or null. */ - public mxcUrlToHttp(mxcUrl: string, width: number, height: number, resizeMethod: string, allowDirectLinks: boolean): string | null { + public mxcUrlToHttp( + mxcUrl: string, + width: number, + height: number, + resizeMethod: string, + allowDirectLinks: boolean, + ): string | null { return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks); } @@ -4028,7 +4152,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: to nothing * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public _unstable_setStatusMessage(newMessage: string): Promise { + public _unstable_setStatusMessage(newMessage: string): Promise { // eslint-disable-line camelcase const type = "im.vector.user_status"; return Promise.all(this.getRooms().map((room) => { const isJoined = room.getMyMembership() === "join"; @@ -4279,7 +4403,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} */ // XXX: Intended private, used in code. - public createMessagesRequest(roomId: string, fromToken: string, limit: number, dir: string, timelineFilter?: Filter): Promise { // TODO: Types + public createMessagesRequest( + roomId: string, + fromToken: string, + limit: number, + dir: string, + timelineFilter?: Filter, + ): Promise { // TODO: Types const path = utils.encodeUri( "/rooms/$roomId/messages", { $roomId: roomId }, ); @@ -4355,7 +4485,6 @@ export class MatrixClient extends EventEmitter { let path; let params; let promise; - const self = this; if (isNotifTimeline) { path = "/notifications"; @@ -4376,7 +4505,7 @@ export class MatrixClient extends EventEmitter { for (let i = 0; i < res.notifications.length; i++) { const notification = res.notifications[i]; - const event = self.getEventMapper()(notification.event); + const event = this.getEventMapper()(notification.event); event.setPushActions( PushProcessor.actionListToActionsObject(notification.actions), ); @@ -4413,11 +4542,11 @@ export class MatrixClient extends EventEmitter { promise.then((res) => { if (res.state) { const roomState = eventTimeline.getState(dir); - const stateEvents = res.state.map(self.getEventMapper()); + const stateEvents = res.state.map(this.getEventMapper()); roomState.setUnknownStateEvents(stateEvents); } const token = res.end; - const matrixEvents = res.chunk.map(self.getEventMapper()); + const matrixEvents = res.chunk.map(this.getEventMapper()); eventTimeline.getTimelineSet() .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); @@ -4537,7 +4666,12 @@ export class MatrixClient extends EventEmitter { * @param {string} nextLink As requestEmailToken * @return {Promise} Resolves: As requestEmailToken */ - public requestRegisterEmailToken(email: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + public requestRegisterEmailToken( + email: string, + clientSecret: string, + sendAttempt: number, + nextLink: string, + ): Promise { return this.requestTokenFromEndpoint( "/register/email/requestToken", { @@ -4593,7 +4727,12 @@ export class MatrixClient extends EventEmitter { * @param {string} nextLink As requestEmailToken * @return {Promise} Resolves: As requestEmailToken */ - public requestAdd3pidEmailToken(email: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + public requestAdd3pidEmailToken( + email: string, + clientSecret: string, + sendAttempt: number, + nextLink: string, + ): Promise { return this.requestTokenFromEndpoint( "/account/3pid/email/requestToken", { @@ -4619,7 +4758,13 @@ export class MatrixClient extends EventEmitter { * @param {string} nextLink As requestEmailToken * @return {Promise} Resolves: As requestEmailToken */ - public requestAdd3pidMsisdnToken(phoneCountry: string, phoneNumber: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + public requestAdd3pidMsisdnToken( + phoneCountry: string, + phoneNumber: string, + clientSecret: string, + sendAttempt: number, + nextLink: string, + ): Promise { return this.requestTokenFromEndpoint( "/account/3pid/msisdn/requestToken", { @@ -4651,7 +4796,12 @@ export class MatrixClient extends EventEmitter { * @param {module:client.callback} callback Optional. As requestEmailToken * @return {Promise} Resolves: As requestEmailToken */ - public requestPasswordEmailToken(email: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + public requestPasswordEmailToken( + email: string, + clientSecret: string, + sendAttempt: number, + nextLink: string, + ): Promise { return this.requestTokenFromEndpoint( "/account/password/email/requestToken", { @@ -4676,7 +4826,13 @@ export class MatrixClient extends EventEmitter { * @param {string} nextLink As requestEmailToken * @return {Promise} Resolves: As requestEmailToken */ - public requestPasswordMsisdnToken(phoneCountry: string, phoneNumber: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + public requestPasswordMsisdnToken( + phoneCountry: string, + phoneNumber: string, + clientSecret: string, + sendAttempt: number, + nextLink: string, + ): Promise { return this.requestTokenFromEndpoint( "/account/password/msisdn/requestToken", { @@ -4795,8 +4951,8 @@ export class MatrixClient extends EventEmitter { deferred.reject(err); }); }).catch((err) => { - deferred.reject(err); - }); + deferred.reject(err); + }); deferred = deferred.promise; } @@ -4936,15 +5092,15 @@ export class MatrixClient extends EventEmitter { * @private */ public processRoomEventsSearch(searchResults: any, response: any): any { // XXX: Intended private, used in code - const room_events = response.search_categories.room_events; + const roomEvents = response.search_categories.room_events; // eslint-disable-line camelcase - searchResults.count = room_events.count; - searchResults.next_batch = room_events.next_batch; + searchResults.count = roomEvents.count; + searchResults.next_batch = roomEvents.next_batch; // combine the highlight list with our existing list; build an object // to avoid O(N^2) fail const highlights = {}; - room_events.highlights.forEach((hl) => { + roomEvents.highlights.forEach((hl) => { highlights[hl] = 1; }); searchResults.highlights.forEach((hl) => { @@ -4955,9 +5111,9 @@ export class MatrixClient extends EventEmitter { searchResults.highlights = Object.keys(highlights); // append the new results to our existing results - const resultsLength = room_events.results ? room_events.results.length : 0; + const resultsLength = roomEvents.results ? roomEvents.results.length : 0; for (let i = 0; i < resultsLength; i++) { - const sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper()); + const sr = SearchResult.fromJson(roomEvents.results[i], this.getEventMapper()); searchResults.results.push(sr); } return searchResults; @@ -5309,7 +5465,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves to a set of rooms * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async _unstable_getSharedRooms(userId: string): Promise { + public async _unstable_getSharedRooms(userId: string): Promise { // eslint-disable-line camelcase if (!(await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"))) { throw Error('Server does not support shared_rooms API'); } @@ -5502,7 +5658,13 @@ export class MatrixClient extends EventEmitter { * @param {Object} opts.from the pagination token returned from a previous request as `nextBatch` to return following relations. * @return {Object} an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available. */ - public async relations(roomId: string, eventId: string, relationType: string, eventType: string, opts: { from: string }): Promise<{ originalEvent: MatrixEvent, events: MatrixEvent[], nextBatch?: string }> { + public async relations( + roomId: string, + eventId: string, + relationType: string, + eventType: string, + opts: { from: string }, + ): Promise<{ originalEvent: MatrixEvent, events: MatrixEvent[], nextBatch?: string }> { const fetchedEventType = this.getEncryptedIfNeededEventType(roomId, eventType); const result = await this.fetchRelations( roomId, @@ -5665,7 +5827,16 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public register(username: string, password: string, sessionId: string, auth: any, bindThreepids: any, guestAccessToken: string, inhibitLogin: boolean, callback?: Callback): Promise { // TODO: Types (many) + public register( + username: string, + password: string, + sessionId: string, + auth: any, + bindThreepids: any, + guestAccessToken: string, + inhibitLogin: boolean, + callback?: Callback, + ): Promise { // TODO: Types (many) // backwards compat if (bindThreepids === true) { bindThreepids = { email: true }; @@ -5777,12 +5948,12 @@ export class MatrixClient extends EventEmitter { * @return {module:http-api.MatrixError} Rejects: with an error response. */ public login(loginType: string, data: any, callback?: Callback): Promise { // TODO: Types - const login_data = { + const loginData = { type: loginType, }; - // merge data into login_data - utils.extend(login_data, data); + // merge data into loginData + utils.extend(loginData, data); return this.http.authedRequest( (error, response) => { @@ -5796,7 +5967,7 @@ export class MatrixClient extends EventEmitter { if (callback) { callback(error, response); } - }, "POST", "/login", undefined, login_data, + }, "POST", "/login", undefined, loginData, ); } @@ -5943,7 +6114,10 @@ export class MatrixClient extends EventEmitter { * room_alias: {string(opt)}} * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async createRoom(options: ICreateRoomOpts, callback?: Callback): Promise<{ roomId: string, room_alias?: string }> { + public async createRoom( + options: ICreateRoomOpts, + callback?: Callback, + ): Promise<{ roomId: string, room_alias?: string }> { // eslint-disable-line camelcase // some valid options include: room_alias_name, visibility, invite // inject the id_access_token if inviting 3rd party addresses @@ -5978,7 +6152,13 @@ export class MatrixClient extends EventEmitter { * @param {Object} opts.from the pagination token returned from a previous request as `next_batch` to return following relations. * @return {Object} the response, with chunk and next_batch. */ - public async fetchRelations(roomId: string, eventId: string, relationType: string, eventType: string, opts: { from: string }): Promise { // TODO: Types + public async fetchRelations( + roomId: string, + eventId: string, + relationType: string, + eventType: string, + opts: { from: string }, + ): Promise { // TODO: Types const queryParams: any = {}; if (opts.from) { queryParams.from = opts.from; @@ -6037,7 +6217,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: dictionary of userid to profile information * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public members(roomId: string, includeMembership?: string[], excludeMembership?: string[], atEventId?: string, callback?: Callback): Promise<{ [userId: string]: any }> { + public members( + roomId: string, + includeMembership?: string[], + excludeMembership?: string[], + atEventId?: string, + callback?: Callback, + ): Promise<{ [userId: string]: any }> { const queryParams: any = {}; if (includeMembership) { queryParams.membership = includeMembership; @@ -6063,7 +6249,10 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: Object with key 'replacement_room' * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public upgradeRoom(roomId: string, newVersion: string): Promise<{ replacement_room: string }> { + public upgradeRoom( + roomId: string, + newVersion: string, + ): Promise<{ replacement_room: string }> { // eslint-disable-line camelcase const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId }); return this.http.authedRequest( undefined, "POST", path, undefined, { new_version: newVersion }, @@ -6103,7 +6292,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendStateEvent(roomId: string, eventType: string, content: any, stateKey: string, callback?: Callback): Promise { // TODO: Types + public sendStateEvent( + roomId: string, + eventType: string, + content: any, + stateKey: string, + callback?: Callback, + ): Promise { // TODO: Types const pathParams = { $roomId: roomId, $eventType: eventType, @@ -6156,7 +6351,12 @@ export class MatrixClient extends EventEmitter { * property is currently unstable and may change in the future. * @return {Promise} Resolves: the empty object, {}. */ - public setRoomReadMarkersHttpRequest(roomId: string, rmEventId: string, rrEventId: string, opts: { hidden?: boolean }): Promise<{}> { + public setRoomReadMarkersHttpRequest( + roomId: string, + rmEventId: string, + rrEventId: string, + opts: { hidden?: boolean }, + ): Promise<{}> { const path = utils.encodeUri("/rooms/$roomId/read_markers", { $roomId: roomId, }); @@ -6217,17 +6417,17 @@ export class MatrixClient extends EventEmitter { options = {}; } - const query_params: any = {}; + const queryParams: any = {}; if (options.server) { - query_params.server = options.server; + queryParams.server = options.server; delete options.server; } - if (Object.keys(options).length === 0 && Object.keys(query_params).length === 0) { + if (Object.keys(options).length === 0 && Object.keys(queryParams).length === 0) { return this.http.authedRequest(callback, "GET", "/publicRooms"); } else { return this.http.authedRequest( - callback, "POST", "/publicRooms", query_params, options, + callback, "POST", "/publicRooms", queryParams, options, ); } } @@ -6290,7 +6490,10 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: Object with room_id and servers. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public getRoomIdForAlias(alias: string, callback?: Callback): Promise<{ room_id: string, servers: string[] }> { + public getRoomIdForAlias( + alias: string, + callback?: Callback, + ): Promise<{ room_id: string, servers: string[] }> { // eslint-disable-line camelcase // TODO: deprecate this or resolveRoomAlias const path = utils.encodeUri("/directory/room/$alias", { $alias: alias, @@ -6336,7 +6539,11 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: result object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public setRoomDirectoryVisibility(roomId: string, visibility: "public" | "private", callback?: Callback): Promise { // TODO: Types + public setRoomDirectoryVisibility( + roomId: string, + visibility: "public" | "private", + callback?: Callback, + ): Promise { // TODO: Types const path = utils.encodeUri("/directory/list/room/$roomId", { $roomId: roomId, }); @@ -6358,7 +6565,12 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: result object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public setRoomDirectoryVisibilityAppService(networkId: string, roomId: string, visibility: "public" | "private", callback?: Callback): Promise { // TODO: Types + public setRoomDirectoryVisibilityAppService( + networkId: string, + roomId: string, + visibility: "public" | "private", + callback?: Callback, + ): Promise { // TODO: Types const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", { $networkId: networkId, $roomId: roomId, @@ -6789,7 +7001,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: result object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public setPushRuleEnabled(scope: string, kind: string, ruleId: string, enabled: boolean, callback?: Callback): Promise { // TODO: Types + public setPushRuleEnabled( + scope: string, + kind: string, + ruleId: string, + enabled: boolean, + callback?: Callback, + ): Promise { // TODO: Types const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", { $kind: kind, $ruleId: ruleId, @@ -6809,7 +7027,13 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: result object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public setPushRuleActions(scope: string, kind: string, ruleId: string, actions: string[], callback?: Callback): Promise { // TODO: Types + public setPushRuleActions( + scope: string, + kind: string, + ruleId: string, + actions: string[], + callback?: Callback, + ): Promise { // TODO: Types const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", { $kind: kind, $ruleId: ruleId, @@ -6828,7 +7052,10 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public search(opts: { body: any, next_batch?: string }, callback?: Callback): Promise { // TODO: Types + public search( + opts: { body: any, next_batch?: string }, // eslint-disable-line camelcase + callback?: Callback, + ): Promise { // TODO: Types const queryParams: any = {}; if (opts.next_batch) { queryParams.next_batch = opts.next_batch; @@ -7013,7 +7240,14 @@ export class MatrixClient extends EventEmitter { * @return {module:http-api.MatrixError} Rejects: with an error response. * @throws Error if no identity server is set */ - public async requestEmailToken(email: string, clientSecret: string, sendAttempt: number, nextLink: string, callback?: Callback, identityAccessToken?: string): Promise { // TODO: Types + public async requestEmailToken( + email: string, + clientSecret: string, + sendAttempt: number, + nextLink: string, + callback?: Callback, + identityAccessToken?: string, + ): Promise { // TODO: Types const params = { client_secret: clientSecret, email: email, @@ -7054,7 +7288,15 @@ export class MatrixClient extends EventEmitter { * @return {module:http-api.MatrixError} Rejects: with an error response. * @throws Error if no identity server is set */ - public async requestMsisdnToken(phoneCountry: string, phoneNumber: string, clientSecret: string, sendAttempt: number, nextLink: string, callback?: Callback, identityAccessToken?: string): Promise { // TODO: Types + public async requestMsisdnToken( + phoneCountry: string, + phoneNumber: string, + clientSecret: string, + sendAttempt: number, + nextLink: string, + callback?: Callback, + identityAccessToken?: string, + ): Promise { // TODO: Types const params = { client_secret: clientSecret, country: phoneCountry, @@ -7088,7 +7330,12 @@ export class MatrixClient extends EventEmitter { * @return {module:http-api.MatrixError} Rejects: with an error response. * @throws Error if No ID server is set */ - public async submitMsisdnToken(sid: string, clientSecret: string, msisdnToken: string, identityAccessToken: string): Promise { // TODO: Types + public async submitMsisdnToken( + sid: string, + clientSecret: string, + msisdnToken: string, + identityAccessToken: string, + ): Promise { // TODO: Types const params = { sid: sid, client_secret: clientSecret, @@ -7119,7 +7366,12 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: Object, currently with no parameters. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public submitMsisdnTokenOtherUrl(url: string, sid: string, clientSecret: string, msisdnToken: string): Promise { // TODO: Types + public submitMsisdnTokenOtherUrl( + url: string, + sid: string, + clientSecret: string, + msisdnToken: string, + ): Promise { // TODO: Types const params = { sid: sid, client_secret: clientSecret, @@ -7154,7 +7406,10 @@ export class MatrixClient extends EventEmitter { * @returns {Promise>} A collection of address mappings to * found MXIDs. Results where no user could be found will not be listed. */ - public async identityHashedLookup(addressPairs: [string, string][], identityAccessToken: string): Promise<{ address: string, mxid: string }[]> { + public async identityHashedLookup( + addressPairs: [string, string][], + identityAccessToken: string, + ): Promise<{ address: string, mxid: string }[]> { const params = { // addresses: ["email@example.org", "10005550000"], // algorithm: "sha256", @@ -7239,7 +7494,12 @@ export class MatrixClient extends EventEmitter { * exists * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async lookupThreePid(medium: string, address: string, callback?: Callback, identityAccessToken?: string): Promise { // TODO: Types + public async lookupThreePid( + medium: string, + address: string, + callback?: Callback, + identityAccessToken?: string, + ): Promise { // TODO: Types // Note: we're using the V2 API by calling this function, but our // function contract requires a V1 response. We therefore have to // convert it manually. @@ -7410,7 +7670,12 @@ export class MatrixClient extends EventEmitter { ); } - public agreeToTerms(serviceType: SERVICE_TYPES, baseUrl: string, accessToken: string, termsUrls: string[]): Promise { // TODO: Types + public agreeToTerms( + serviceType: SERVICE_TYPES, + baseUrl: string, + accessToken: string, + termsUrls: string[], + ): Promise { // TODO: Types const url = this.termsUrlForService(serviceType, baseUrl); const headers = { Authorization: "Bearer " + accessToken, @@ -7447,7 +7712,14 @@ export class MatrixClient extends EventEmitter { * @param {string?} batch The opaque token to paginate a previous summary request. * @returns {Promise} the response, with next_batch, rooms, events fields. */ - public getSpaceSummary(roomId: string, maxRoomsPerSpace?: number, suggestedOnly?: boolean, autoJoinOnly?: boolean, limit?: number, batch?: string): Promise { // TODO: Types + public getSpaceSummary( + roomId: string, + maxRoomsPerSpace?: number, + suggestedOnly?: boolean, + autoJoinOnly?: boolean, + limit?: number, + batch?: string, + ): Promise { // TODO: Types const path = utils.encodeUri("/rooms/$roomId/spaces", { $roomId: roomId, }); From dfb918adc3a5259edd87c9cee40ac29006894fc7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:26:29 -0600 Subject: [PATCH 20/32] Lint pass 3 --- src/client.ts | 8 +++++++- src/crypto/keybackup.ts | 2 +- src/crypto/verification/Base.js | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index eca1180350b..72c380ddb8d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4697,7 +4697,13 @@ export class MatrixClient extends EventEmitter { * @param {string} nextLink As requestEmailToken * @return {Promise} Resolves: As requestEmailToken */ - public requestRegisterMsisdnToken(phoneCountry: string, phoneNumber: string, clientSecret: string, sendAttempt: number, nextLink: string): Promise { + public requestRegisterMsisdnToken( + phoneCountry: string, + phoneNumber: string, + clientSecret: string, + sendAttempt: number, + nextLink: string, + ): Promise { return this.requestTokenFromEndpoint( "/register/msisdn/requestToken", { diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index 9c816384e22..c5a7979f2f7 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -66,5 +66,5 @@ export interface IKeyBackupRestoreResult { export interface IKeyBackupRestoreOpts { cacheCompleteCallback?: () => void; - progressCallback?: ({stage: string}) => void; + progressCallback?: ({ stage: string }) => void; } diff --git a/src/crypto/verification/Base.js b/src/crypto/verification/Base.js index 9f03329c7b2..a60b93b4d1f 100644 --- a/src/crypto/verification/Base.js +++ b/src/crypto/verification/Base.js @@ -50,7 +50,7 @@ export class VerificationBase extends EventEmitter { * @class * * TODO: Channel types - * @param {Channel} channel the verification channel to send verification messages over. + * @param {Object} channel the verification channel to send verification messages over. * * @param {MatrixClient} baseApis base matrix api interface * From 9307f9f3455d09aed2742a552f0828b9717801fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:27:16 -0600 Subject: [PATCH 21/32] Build pass 2 --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 72c380ddb8d..0987bb1bab7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -89,7 +89,7 @@ import { ISecretStorageKey, } from "./crypto/api"; import { CrossSigningInfo, UserTrustLevel } from "./crypto/CrossSigning"; -import type { Room } from "./models/Room"; +import { Room } from "./models/room"; import { ICreateRoomOpts, IEventSearchOpts, From 40f55b29641a50b0fbfd3cceb95dbb4b1f08178c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:29:31 -0600 Subject: [PATCH 22/32] Lint pass 4 --- src/crypto/verification/Base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/verification/Base.js b/src/crypto/verification/Base.js index a60b93b4d1f..2ac17cc88f2 100644 --- a/src/crypto/verification/Base.js +++ b/src/crypto/verification/Base.js @@ -49,8 +49,8 @@ export class VerificationBase extends EventEmitter { * * @class * - * TODO: Channel types * @param {Object} channel the verification channel to send verification messages over. + * TODO: Channel types * * @param {MatrixClient} baseApis base matrix api interface * From 71dc0bac56ed5da4d860f96569e6d614361e2371 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:47:47 -0600 Subject: [PATCH 23/32] Tests pass 1 --- spec/unit/crypto/cross-signing.spec.js | 56 +++++++++++++------------- spec/unit/crypto/secrets.spec.js | 24 +++++------ src/client.ts | 27 ++++++++----- src/crypto/api.ts | 4 +- 4 files changed, 58 insertions(+), 53 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index af2f2dabd7d..cd35e6ed596 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -64,8 +64,8 @@ describe("Cross Signing", function() { ); alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => { await olmlib.verifySignature( - alice._crypto._olmDevice, keys.master_key, "@alice:example.com", - "Osborne2", alice._crypto._olmDevice.deviceEd25519Key, + alice.crypto._olmDevice, keys.master_key, "@alice:example.com", + "Osborne2", alice.crypto._olmDevice.deviceEd25519Key, ); }); alice.uploadKeySignatures = async () => {}; @@ -138,7 +138,7 @@ describe("Cross Signing", function() { // set Alice's cross-signing key await resetCrossSigningKeys(alice); // Alice downloads Bob's device key - alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -202,12 +202,12 @@ describe("Cross Signing", function() { const uploadSigsPromise = new Promise((resolve, reject) => { alice.uploadKeySignatures = jest.fn(async (content) => { await olmlib.verifySignature( - alice._crypto._olmDevice, + alice.crypto._olmDevice, content["@alice:example.com"][ "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" ], "@alice:example.com", - "Osborne2", alice._crypto._olmDevice.deviceEd25519Key, + "Osborne2", alice.crypto._olmDevice.deviceEd25519Key, ); olmlib.pkVerify( content["@alice:example.com"]["Osborne2"], @@ -218,7 +218,7 @@ describe("Cross Signing", function() { }); }); - const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"] + const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"] .Osborne2; const aliceDevice = { user_id: "@alice:example.com", @@ -226,7 +226,7 @@ describe("Cross Signing", function() { }; aliceDevice.keys = deviceInfo.keys; aliceDevice.algorithms = deviceInfo.algorithms; - await alice._crypto._signObject(aliceDevice); + await alice.crypto._signObject(aliceDevice); olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com"); // feed sync result that includes master key, ssk, device key @@ -354,7 +354,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -383,7 +383,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobPubkey]: sig, }, }; - alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Bob's device key should be TOFU @@ -417,8 +417,8 @@ describe("Cross Signing", function() { null, aliceKeys, ); - alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com"); - alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {}; + alice.crypto._deviceList.startTrackingDeviceList("@bob:example.com"); + alice.crypto._deviceList.stopTrackingAllDeviceLists = () => {}; alice.uploadDeviceSigningKeys = async () => {}; alice.uploadKeySignatures = async () => {}; @@ -433,14 +433,14 @@ describe("Cross Signing", function() { ]); const keyChangePromise = new Promise((resolve, reject) => { - alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => { + alice.crypto._deviceList.once("userCrossSigningUpdated", (userId) => { if (userId === "@bob:example.com") { resolve(); } }); }); - const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"] + const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"] .Osborne2; const aliceDevice = { user_id: "@alice:example.com", @@ -448,7 +448,7 @@ describe("Cross Signing", function() { }; aliceDevice.keys = deviceInfo.keys; aliceDevice.algorithms = deviceInfo.algorithms; - await alice._crypto._signObject(aliceDevice); + await alice.crypto._signObject(aliceDevice); const bobOlmAccount = new global.Olm.Account(); bobOlmAccount.create(); @@ -602,7 +602,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -625,7 +625,7 @@ describe("Cross Signing", function() { "ed25519:Dynabook": "someOtherPubkey", }, }; - alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Bob's device key should be untrusted @@ -669,7 +669,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -697,7 +697,7 @@ describe("Cross Signing", function() { bobDevice.signatures = {}; bobDevice.signatures["@bob:example.com"] = {}; bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig; - alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Alice verifies Bob's SSK @@ -729,7 +729,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey2]: sskSig2, }, }; - alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -766,7 +766,7 @@ describe("Cross Signing", function() { // Alice gets new signature for device const sig2 = bobSigning2.sign(bobDeviceString); bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; - alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); @@ -801,20 +801,20 @@ describe("Cross Signing", function() { bob.uploadKeySignatures = async () => {}; // set Bob's cross-signing key await resetCrossSigningKeys(bob); - alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: { algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], keys: { - "curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key, - "ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key, + "curve25519:Dynabook": bob.crypto._olmDevice.deviceCurve25519Key, + "ed25519:Dynabook": bob.crypto._olmDevice.deviceEd25519Key, }, verified: 1, known: true, }, }); - alice._crypto._deviceList.storeCrossSigningForUser( + alice.crypto._deviceList.storeCrossSigningForUser( "@bob:example.com", - bob._crypto._crossSigningInfo.toStorage(), + bob.crypto._crossSigningInfo.toStorage(), ); alice.uploadDeviceSigningKeys = async () => {}; @@ -834,7 +834,7 @@ describe("Cross Signing", function() { expect(bobTrust.isTofu()).toBeTruthy(); // "forget" that Bob is trusted - delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"] + delete alice.crypto._deviceList._crossSigningInfo["@bob:example.com"] .keys.master.signatures["@alice:example.com"]; const bobTrust2 = alice.checkUserTrust("@bob:example.com"); @@ -844,9 +844,9 @@ describe("Cross Signing", function() { upgradePromise = new Promise((resolve) => { upgradeResolveFunc = resolve; }); - alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); + alice.crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); await new Promise((resolve) => { - alice._crypto.on("userTrustStatusChanged", resolve); + alice.crypto.on("userTrustStatusChanged", resolve); }); await upgradePromise; diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index 2a7b056a0bf..8cc134be97b 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -99,11 +99,11 @@ describe("Secrets", function() { }, }, ); - alice._crypto._crossSigningInfo.setKeys({ + alice.crypto._crossSigningInfo.setKeys({ master: signingkeyInfo, }); - const secretStorage = alice._crypto._secretStorage; + const secretStorage = alice.crypto._secretStorage; alice.setAccountData = async function(eventType, contents, callback) { alice.store.storeAccountDataEvents([ @@ -120,7 +120,7 @@ describe("Secrets", function() { const keyAccountData = { algorithm: SECRET_STORAGE_ALGORITHM_V1_AES, }; - await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master'); + await alice.crypto._crossSigningInfo.signObject(keyAccountData, 'master'); alice.store.storeAccountDataEvents([ new MatrixEvent({ @@ -249,7 +249,7 @@ describe("Secrets", function() { }, }, }); - vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { + vax.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", { "Osborne2": { user_id: "@alice:example.com", device_id: "Osborne2", @@ -334,8 +334,8 @@ describe("Secrets", function() { createSecretStorageKey, }); - const crossSigning = bob._crypto._crossSigningInfo; - const secretStorage = bob._crypto._secretStorage; + const crossSigning = bob.crypto._crossSigningInfo; + const secretStorage = bob.crypto._secretStorage; expect(crossSigning.getId()).toBeTruthy(); expect(await crossSigning.isStoredInSecretStorage(secretStorage)) @@ -376,10 +376,10 @@ describe("Secrets", function() { ]); this.emit("accountData", event); }; - bob._crypto.checkKeyBackup = async () => {}; + bob.crypto.checkKeyBackup = async () => {}; - const crossSigning = bob._crypto._crossSigningInfo; - const secretStorage = bob._crypto._secretStorage; + const crossSigning = bob.crypto._crossSigningInfo; + const secretStorage = bob.crypto._secretStorage; // Set up cross-signing keys from scratch with specific storage key await bob.bootstrapCrossSigning({ @@ -394,7 +394,7 @@ describe("Secrets", function() { }); // Clear local cross-signing keys and read from secret storage - bob._crypto._deviceList.storeCrossSigningForUser( + bob.crypto._deviceList.storeCrossSigningForUser( "@bob:example.com", crossSigning.toStorage(), ); @@ -479,7 +479,7 @@ describe("Secrets", function() { }, }), ]); - alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { keys: { master: { user_id: "@alice:example.com", @@ -619,7 +619,7 @@ describe("Secrets", function() { }, }), ]); - alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { keys: { master: { user_id: "@alice:example.com", diff --git a/src/client.ts b/src/client.ts index 0987bb1bab7..50796d8c37d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -394,6 +394,8 @@ export class MatrixClient extends EventEmitter { public supportsCallTransfer = false; // XXX: Intended private, used in code. public forceTURN = false; // XXX: Intended private, used in code. public iceCandidatePoolSize = 0; // XXX: Intended private, used in code. + public idBaseUrl: string; + public baseUrl: string; private canSupportVoip = false; private peekSync: SyncApi = null; @@ -429,8 +431,6 @@ export class MatrixClient extends EventEmitter { private turnServersExpiry = 0; private checkTurnServersIntervalID: number; private exportedOlmDeviceToImport: IOlmDevice; - private baseUrl: string; - private idBaseUrl: string; private txnCtr = 0; constructor(opts: IMatrixClientCreateOpts) { @@ -671,11 +671,12 @@ export class MatrixClient extends EventEmitter { this.syncApi = new SyncApi(this, this.clientOpts); this.syncApi.sync(); - if (opts.clientWellKnownPollPeriod !== undefined) { + if (this.clientOpts.clientWellKnownPollPeriod !== undefined) { this.clientWellKnownIntervalID = + // XXX: Typecast on timer ID because we know better setInterval(() => { this.fetchClientWellKnown(); - }, 1000 * opts.clientWellKnownPollPeriod) as any as number; // XXX: Typecast because we know better + }, 1000 * this.clientOpts.clientWellKnownPollPeriod) as any as number; this.fetchClientWellKnown(); } } @@ -1249,7 +1250,7 @@ export class MatrixClient extends EventEmitter { */ public downloadKeys( userIds: string[], - forceDownload: boolean, + forceDownload?: boolean, ): Promise>> { if (!this.crypto) { return Promise.reject(new Error("End-to-end encryption disabled")); @@ -2512,9 +2513,10 @@ export class MatrixClient extends EventEmitter { targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, - opts: IKeyBackupRestoreOpts, + opts?: IKeyBackupRestoreOpts, ): Promise { - const { cacheCompleteCallback, progressCallback } = opts; + const cacheCompleteCallback = opts?.cacheCompleteCallback; + const progressCallback = opts?.progressCallback; if (!this.crypto) { throw new Error("End-to-end encryption disabled"); @@ -3376,7 +3378,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendMessage(roomId: string, content: any, txnId: string, callback?: Callback): Promise { + public sendMessage(roomId: string, content: any, txnId?: string, callback?: Callback): Promise { if (utils.isFunction(txnId)) { callback = txnId as any as Callback; // for legacy txnId = undefined; @@ -6008,7 +6010,7 @@ export class MatrixClient extends EventEmitter { * authenticates with CAS. * @return {string} The HS URL to hit to begin the CAS login process. */ - public getCasLoginUrl(redirectUrl: string): Promise { + public getCasLoginUrl(redirectUrl: string): string { return this.getSsoLoginUrl(redirectUrl, "cas"); } @@ -6020,7 +6022,7 @@ export class MatrixClient extends EventEmitter { * @param {string} idpId The ID of the Identity Provider being targeted, optional. * @return {string} The HS URL to hit to begin the SSO login process. */ - public getSsoLoginUrl(redirectUrl: string, loginType = "sso", idpId?: string): Promise { + public getSsoLoginUrl(redirectUrl: string, loginType = "sso", idpId?: string): string { let url = "/login/" + loginType + "/redirect"; if (idpId) { url += "/" + idpId; @@ -6648,7 +6650,10 @@ export class MatrixClient extends EventEmitter { * determined by this.opts.onlyData, opts.rawResponse, and * opts.onlyContentUri. Rejects with an error (usually a MatrixError). */ - public uploadContent(file: File | String | Buffer | ReadStream, opts: IUploadOpts): Promise { // TODO: Advanced types + public uploadContent( + file: File | String | Buffer | ReadStream | Blob, + opts: IUploadOpts, + ): Promise { // TODO: Advanced types return this.http.uploadContent(file, opts); } diff --git a/src/crypto/api.ts b/src/crypto/api.ts index 835136019bd..24528486e35 100644 --- a/src/crypto/api.ts +++ b/src/crypto/api.ts @@ -101,10 +101,10 @@ export interface ICreateSecretStorageOpts { /** * Function called to get the user's - * current key backup passphrase. Should return a promise that resolves with a Buffer + * current key backup passphrase. Should return a promise that resolves with a Uint8Array * containing the key, or rejects if the key cannot be obtained. */ - getKeyBackupPassphrase?: () => Promise; + getKeyBackupPassphrase?: () => Promise; } export interface ISecretStorageKey { From 9156bed961bc6926fbbfe4fbf24e2172ee22f025 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 22:03:37 -0600 Subject: [PATCH 24/32] Tests pass 2 --- spec/TestClient.js | 2 +- spec/unit/crypto/cross-signing.spec.js | 32 +++++++++++-------- .../verification/secret_request.spec.js | 2 +- spec/unit/matrix-client.spec.js | 2 +- spec/unit/room.spec.js | 2 +- src/client.ts | 1 + src/sync.js | 2 +- 7 files changed, 24 insertions(+), 19 deletions(-) diff --git a/spec/TestClient.js b/spec/TestClient.js index aa6db4f2a08..d4d756b9801 100644 --- a/spec/TestClient.js +++ b/spec/TestClient.js @@ -69,7 +69,7 @@ export function TestClient( this.deviceKeys = null; this.oneTimeKeys = {}; - this._callEventHandler = { + this.callEventHandler = { calls: new Map(), }; } diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index cd35e6ed596..56b86b26b7f 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -201,20 +201,24 @@ describe("Cross Signing", function() { const uploadSigsPromise = new Promise((resolve, reject) => { alice.uploadKeySignatures = jest.fn(async (content) => { - await olmlib.verifySignature( - alice.crypto._olmDevice, - content["@alice:example.com"][ - "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" - ], - "@alice:example.com", - "Osborne2", alice.crypto._olmDevice.deviceEd25519Key, - ); - olmlib.pkVerify( - content["@alice:example.com"]["Osborne2"], - "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", - "@alice:example.com", - ); - resolve(); + try { + await olmlib.verifySignature( + alice.crypto._olmDevice, + content["@alice:example.com"][ + "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" + ], + "@alice:example.com", + "Osborne2", alice.crypto._olmDevice.deviceEd25519Key, + ); + olmlib.pkVerify( + content["@alice:example.com"]["Osborne2"], + "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", + "@alice:example.com", + ); + resolve(); + } catch (e) { + reject(e); + } }); }); diff --git a/spec/unit/crypto/verification/secret_request.spec.js b/spec/unit/crypto/verification/secret_request.spec.js index 2af336af733..8c957327313 100644 --- a/spec/unit/crypto/verification/secret_request.spec.js +++ b/spec/unit/crypto/verification/secret_request.spec.js @@ -69,7 +69,7 @@ describe("self-verifications", () => { const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve()); const client = { - _crypto: { + crypto: { _crossSigningInfo, _secretStorage, storeSessionBackupPrivateKey, diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index f627f2476ae..98c6b127e4e 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -144,7 +144,7 @@ describe("MatrixClient", function() { scheduler: scheduler, userId: userId, }); - // FIXME: We shouldn't be yanking _http like this. + // FIXME: We shouldn't be yanking http like this. client.http = [ "authedRequest", "getContentUri", "request", "uploadContent", ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index fd3eeafca35..90d65ba8fb3 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1314,7 +1314,7 @@ describe("Room", function() { isRoomEncrypted: function() { return false; }, - _http: { + http: { serverResponse, authedRequest: function() { if (this.serverResponse instanceof Error) { diff --git a/src/client.ts b/src/client.ts index 50796d8c37d..f9c042a5cff 100644 --- a/src/client.ts +++ b/src/client.ts @@ -440,6 +440,7 @@ export class MatrixClient extends EventEmitter { opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl); this.baseUrl = opts.baseUrl; + this.idBaseUrl = opts.idBaseUrl; this.usingExternalCrypto = opts.usingExternalCrypto; this.store = opts.store || new StubStore(); diff --git a/src/sync.js b/src/sync.js index 70a385c3580..9929629c2bc 100644 --- a/src/sync.js +++ b/src/sync.js @@ -21,7 +21,7 @@ limitations under the License. * TODO: * This class mainly serves to take all the syncing logic out of client.js and * into a separate file. It's all very fluid, and this class gut wrenches a lot - * of MatrixClient props (e.g. _http). Given we want to support WebSockets as + * of MatrixClient props (e.g. http). Given we want to support WebSockets as * an alternative syncing API, we may want to have a proper syncing interface * for HTTP and WS at some point. */ From 2700f0acf6be97378dd0162d53f07dbae45ee010 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 22:10:44 -0600 Subject: [PATCH 25/32] Upstream build pass 1 --- src/@types/requests.ts | 1 + src/client.ts | 33 ++++++++++++++++++--------------- src/crypto/dehydration.ts | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/@types/requests.ts b/src/@types/requests.ts index 48c37967001..581b4a1b6c0 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -73,6 +73,7 @@ export interface ICreateRoomOpts { visibility?: "public" | "private"; name?: string; topic?: string; + preset?: string; // TODO: Types (next line) invite_3pid?: any[]; // eslint-disable-line camelcase } diff --git a/src/client.ts b/src/client.ts index f9c042a5cff..07376de80e3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1417,7 +1417,7 @@ export class MatrixClient extends EventEmitter { * @returns {Promise} resolves to a VerificationRequest * when the request has been sent to the other party. */ - public requestVerification(userId: string, devices: string[]): Promise { + public requestVerification(userId: string, devices?: string[]): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1510,7 +1510,7 @@ export class MatrixClient extends EventEmitter { * * @returns {string} the key ID */ - public getCrossSigningId(type = CrossSigningKey.Master): string { + public getCrossSigningId(type: CrossSigningKey | string = CrossSigningKey.Master): string { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2445,9 +2445,9 @@ export class MatrixClient extends EventEmitter { // TODO: Types public async restoreKeyBackupWithSecretStorage( backupInfo: IKeyBackupVersion, - targetRoomId: string, - targetSessionId: string, - opts: IKeyBackupRestoreOpts, + targetRoomId?: string, + targetSessionId?: string, + opts?: IKeyBackupRestoreOpts, ): Promise { const storedKey = await this.getSecret("m.megolm_backup.v1"); @@ -2866,7 +2866,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: Room object. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async joinRoom(roomIdOrAlias: string, opts: IJoinRoomOpts, callback?: Callback): Promise { + public async joinRoom(roomIdOrAlias: string, opts?: IJoinRoomOpts, callback?: Callback): Promise { // to help people when upgrading.. if (utils.isFunction(opts)) { throw new Error("Expected 'opts' object, got function."); @@ -3894,7 +3894,10 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves when completed with an object keyed * by room ID and value of the error encountered when leaving or null. */ - public leaveRoomChain(roomId: string, includeFuture = true): Promise<{ [roomId: string]: Error | null }> { + public leaveRoomChain( + roomId: string, + includeFuture = true, + ): Promise<{ [roomId: string]: Error | MatrixError | null }> { const upgradeHistory = this.getRoomUpgradeHistory(roomId); let eligibleToLeave = upgradeHistory; @@ -4140,10 +4143,10 @@ export class MatrixClient extends EventEmitter { */ public mxcUrlToHttp( mxcUrl: string, - width: number, - height: number, - resizeMethod: string, - allowDirectLinks: boolean, + width?: number, + height?: number, + resizeMethod?: string, + allowDirectLinks?: boolean, ): string | null { return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks); } @@ -6126,7 +6129,7 @@ export class MatrixClient extends EventEmitter { public async createRoom( options: ICreateRoomOpts, callback?: Callback, - ): Promise<{ roomId: string, room_alias?: string }> { // eslint-disable-line camelcase + ): Promise<{ room_id: string, room_alias?: string }> { // eslint-disable-line camelcase // some valid options include: room_alias_name, visibility, invite // inject the id_access_token if inviting 3rd party addresses @@ -6305,7 +6308,7 @@ export class MatrixClient extends EventEmitter { roomId: string, eventType: string, content: any, - stateKey: string, + stateKey = "", callback?: Callback, ): Promise { // TODO: Types const pathParams = { @@ -6653,7 +6656,7 @@ export class MatrixClient extends EventEmitter { */ public uploadContent( file: File | String | Buffer | ReadStream | Blob, - opts: IUploadOpts, + opts?: IUploadOpts, ): Promise { // TODO: Advanced types return this.http.uploadContent(file, opts); } @@ -6687,7 +6690,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public getProfileInfo(userId: string, info: string, callback?: Callback): Promise { // TODO: Types + public getProfileInfo(userId: string, info?: string, callback?: Callback): Promise { // TODO: Types if (utils.isFunction(info)) { callback = info as any as Callback; // legacy info = undefined; diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 5e431be62cb..f32daaeb71e 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -33,7 +33,7 @@ export interface IDehydratedDevice { } export interface IDehydratedDeviceKeyInfo { - passphrase: string; + passphrase?: string; } interface DeviceKeys { From 43abfbc537b823381ae1baf62a25819ae3f5448a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 22:20:13 -0600 Subject: [PATCH 26/32] Upstream build pass 2 --- src/client.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/client.ts b/src/client.ts index 07376de80e3..f5a7f712fc6 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4676,8 +4676,8 @@ export class MatrixClient extends EventEmitter { email: string, clientSecret: string, sendAttempt: number, - nextLink: string, - ): Promise { + nextLink?: string, + ): Promise { // TODO: Types return this.requestTokenFromEndpoint( "/register/email/requestToken", { @@ -4708,8 +4708,8 @@ export class MatrixClient extends EventEmitter { phoneNumber: string, clientSecret: string, sendAttempt: number, - nextLink: string, - ): Promise { + nextLink?: string, + ): Promise { // TODO: Types return this.requestTokenFromEndpoint( "/register/msisdn/requestToken", { @@ -4744,7 +4744,7 @@ export class MatrixClient extends EventEmitter { clientSecret: string, sendAttempt: number, nextLink: string, - ): Promise { + ): Promise { // TODO: Types return this.requestTokenFromEndpoint( "/account/3pid/email/requestToken", { @@ -4776,7 +4776,7 @@ export class MatrixClient extends EventEmitter { clientSecret: string, sendAttempt: number, nextLink: string, - ): Promise { + ): Promise { // TODO: Types return this.requestTokenFromEndpoint( "/account/3pid/msisdn/requestToken", { @@ -4813,7 +4813,7 @@ export class MatrixClient extends EventEmitter { clientSecret: string, sendAttempt: number, nextLink: string, - ): Promise { + ): Promise { // TODO: Types return this.requestTokenFromEndpoint( "/account/password/email/requestToken", { @@ -4844,7 +4844,7 @@ export class MatrixClient extends EventEmitter { clientSecret: string, sendAttempt: number, nextLink: string, - ): Promise { + ): Promise { // TODO: Types return this.requestTokenFromEndpoint( "/account/password/msisdn/requestToken", { @@ -5934,7 +5934,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: to the /register response * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public registerRequest(data: any, kind: string, callback?: Callback): Promise { // TODO: Types + public registerRequest(data: any, kind?: string, callback?: Callback): Promise { // TODO: Types const params: any = {}; if (kind) { params.kind = kind; @@ -6102,7 +6102,7 @@ export class MatrixClient extends EventEmitter { * * @return {string} HS URL to hit to for the fallback interface */ - public getFallbackAuthUrl(loginType: string, authSessionId: string): Promise { + public getFallbackAuthUrl(loginType: string, authSessionId: string): string { const path = utils.encodeUri("/auth/$loginType/fallback/web", { $loginType: loginType, }); @@ -6600,7 +6600,7 @@ export class MatrixClient extends EventEmitter { * apply a limit if unspecified. * @return {Promise} Resolves: an array of results. */ - public searchUserDirectory(opts: { term: string, limit?: number }): Promise { // TODO: Types + public searchUserDirectory(opts: { term: string, limit?: number }): Promise { // TODO: Types const body: any = { search_term: opts.term, }; @@ -6936,7 +6936,7 @@ export class MatrixClient extends EventEmitter { * @return {Promise} Resolves: Array of objects representing pushers * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public getPushers(callback?: Callback): Promise { // TODO: Types + public getPushers(callback?: Callback): Promise { // TODO: Types const path = "/pushers"; return this.http.authedRequest( callback, "GET", path, undefined, undefined, From bf9ba65ac49111d73e838c2567ea3f77c7ac5db9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 22:30:50 -0600 Subject: [PATCH 27/32] Fix olmVersion types --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index f5a7f712fc6..d8a5ed4657e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -374,7 +374,7 @@ export class MatrixClient extends EventEmitter { public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; public reEmitter = new ReEmitter(this); - public olmVersion: number = null; // populated after initCrypto + public olmVersion: string = null; // populated after initCrypto public usingExternalCrypto = false; public store: Store; public deviceId?: string; From b360fc8308ea6bb4cb3cbbbfbd4f324a1cd207ad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 2 Jun 2021 18:42:52 -0600 Subject: [PATCH 28/32] Fix test failure --- src/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index d8a5ed4657e..fe6c7b3a541 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1570,12 +1570,13 @@ export class MatrixClient extends EventEmitter { /** * Check the copy of our cross-signing key that we have in the device list and * see if we can get the private key. If so, mark it as trusted. + * @param {Object} opts TODO */ - public checkOwnCrossSigningTrust() { + public checkOwnCrossSigningTrust(opts: any): Promise { // TODO: Types if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.checkOwnCrossSigningTrust(); + return this.crypto.checkOwnCrossSigningTrust(opts); } /** From a3ac3692af3b770a7ea42258c66c66e44d0fa065 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 2 Jun 2021 22:05:53 -0600 Subject: [PATCH 29/32] private->protected --- src/client.ts | 55 +++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/client.ts b/src/client.ts index fe6c7b3a541..af71a9770b5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -397,41 +397,44 @@ export class MatrixClient extends EventEmitter { public idBaseUrl: string; public baseUrl: string; - private canSupportVoip = false; - private peekSync: SyncApi = null; - private isGuestAccount = false; - private ongoingScrollbacks: {[roomId: string]: {promise?: Promise, errorTs?: number}} = {}; // TODO: Types - private notifTimelineSet: EventTimelineSet = null; - private cryptoStore: CryptoStore; - private verificationMethods: string[]; - private fallbackICEServerAllowed = false; - private roomList: RoomList; - private syncApi: SyncApi; - private pushRules: any; // TODO: Types - private syncLeftRoomsPromise: Promise; - private syncedLeftRooms = false; - private clientOpts: IStoredClientOpts; - private clientWellKnownIntervalID: number; - private canResetTimelineCallback: ResetTimelineCallback; + // Note: these are all `protected` to let downstream consumers make mistakes if they want to. + // We don't technically support this usage, but have reasons to do this. + + protected canSupportVoip = false; + protected peekSync: SyncApi = null; + protected isGuestAccount = false; + protected ongoingScrollbacks: {[roomId: string]: {promise?: Promise, errorTs?: number}} = {}; // TODO: Types + protected notifTimelineSet: EventTimelineSet = null; + protected cryptoStore: CryptoStore; + protected verificationMethods: string[]; + protected fallbackICEServerAllowed = false; + protected roomList: RoomList; + protected syncApi: SyncApi; + protected pushRules: any; // TODO: Types + protected syncLeftRoomsPromise: Promise; + protected syncedLeftRooms = false; + protected clientOpts: IStoredClientOpts; + protected clientWellKnownIntervalID: number; + protected canResetTimelineCallback: ResetTimelineCallback; // The pushprocessor caches useful things, so keep one and re-use it - private pushProcessor = new PushProcessor(this); + protected pushProcessor = new PushProcessor(this); // Promise to a response of the server's /versions response // TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020 - private serverVersionsPromise: Promise; + protected serverVersionsPromise: Promise; - private cachedCapabilities: { + protected cachedCapabilities: { capabilities: Record; expiration: number; }; - private clientWellKnown: any; - private clientWellKnownPromise: Promise; - private turnServers: any[] = []; // TODO: Types - private turnServersExpiry = 0; - private checkTurnServersIntervalID: number; - private exportedOlmDeviceToImport: IOlmDevice; - private txnCtr = 0; + protected clientWellKnown: any; + protected clientWellKnownPromise: Promise; + protected turnServers: any[] = []; // TODO: Types + protected turnServersExpiry = 0; + protected checkTurnServersIntervalID: number; + protected exportedOlmDeviceToImport: IOlmDevice; + protected txnCtr = 0; constructor(opts: IMatrixClientCreateOpts) { super(); From 92ebd39391ffbf38e03a3304fcabf6695dfa1bae Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jun 2021 18:59:01 -0600 Subject: [PATCH 30/32] Reincorporate crypto changes https://github.com/matrix-org/matrix-js-sdk/pull/1697 --- src/client.ts | 183 +++++++++++++++++++------------------------------- src/matrix.ts | 1 + 2 files changed, 70 insertions(+), 114 deletions(-) diff --git a/src/client.ts b/src/client.ts index af71a9770b5..8a71d550fdd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -58,12 +58,9 @@ import { IKeyBackupPrepareOpts, IKeyBackupRestoreOpts, IKeyBackupRestoreResult, - IKeyBackupRoomSessions, - IKeyBackupSession, IKeyBackupTrustInfo, IKeyBackupVersion, } from "./crypto/keybackup"; -import { PkDecryption } from "@matrix-org/olm"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import type Request from "request"; import { MatrixScheduler } from "./scheduler"; @@ -109,6 +106,7 @@ import url from "url"; import { randomString } from "./randomstring"; import { ReadStream } from "fs"; import { WebStorageSessionStore } from "./store/session/webstorage"; +import { BackupManager } from "./crypto/backup"; export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend; export type SessionStore = WebStorageSessionStore; @@ -122,29 +120,6 @@ export const CRYPTO_ENABLED: boolean = isCryptoAvailable(); const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes -function keysFromRecoverySession(sessions: IKeyBackupRoomSessions, decryptionKey: PkDecryption, roomId: string) { - const keys = []; - for (const [sessionId, sessionData] of Object.entries(sessions)) { - try { - const decrypted = keyFromRecoverySession(sessionData, decryptionKey); - decrypted.session_id = sessionId; - decrypted.room_id = roomId; - keys.push(decrypted); - } catch (e) { - logger.log("Failed to decrypt megolm session from backup", e); - } - } - return keys; -} - -function keyFromRecoverySession(session: IKeyBackupSession, decryptionKey: PkDecryption) { - return JSON.parse(decryptionKey.decrypt( - session.session_data.ephemeral, - session.session_data.mac, - session.session_data.ciphertext, - )); -} - interface IOlmDevice { pickledAccount: string; sessions: Array>; @@ -1312,7 +1287,7 @@ export class MatrixClient extends EventEmitter { // check the key backup status, since whether or not we use this depends on // whether it has a signature from a verified device if (userId == this.credentials.userId) { - this.crypto.checkKeyBackup(); + this.checkKeyBackup(); } return prom; } @@ -2072,7 +2047,7 @@ export class MatrixClient extends EventEmitter { * in trustInfo. */ public checkKeyBackup(): IKeyBackupVersion { - return this.crypto.checkKeyBackup(); + return this.crypto._backupManager.checkKeyBackup(); } /** @@ -2114,7 +2089,7 @@ export class MatrixClient extends EventEmitter { * } */ public isKeyBackupTrusted(info: IKeyBackupVersion): IKeyBackupTrustInfo { - return this.crypto.isKeyBackupTrusted(info); + return this.crypto._backupManager.isKeyBackupTrusted(info); } /** @@ -2126,11 +2101,7 @@ export class MatrixClient extends EventEmitter { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - // XXX: Private member access - if (!this.crypto._checkedForBackup) { - return null; - } - return Boolean(this.crypto.backupKey); + return this.crypto._backupManager.getKeyBackupEnabled(); } /** @@ -2138,22 +2109,14 @@ export class MatrixClient extends EventEmitter { * getKeyBackupVersion. * * @param {object} info Backup information object as returned by getKeyBackupVersion + * @returns {Promise} Resolves when complete. */ - public enableKeyBackup(info: IKeyBackupVersion) { + public enableKeyBackup(info: IKeyBackupVersion): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - this.crypto.backupInfo = info; - if (this.crypto.backupKey) this.crypto.backupKey.free(); - this.crypto.backupKey = new global.Olm.PkEncryption(); - this.crypto.backupKey.set_recipient_key(info.auth_data.public_key); - - this.emit('crypto.keyBackupStatus', true); - - // There may be keys left over from a partially completed backup, so - // schedule a send to check. - this.crypto.scheduleKeyBackupSend(); + return this.crypto._backupManager.enableKeyBackup(info); } /** @@ -2164,11 +2127,7 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } - this.crypto.backupInfo = null; - if (this.crypto.backupKey) this.crypto.backupKey.free(); - this.crypto.backupKey = null; - - this.emit('crypto.keyBackupStatus', false); + this.crypto._backupManager.disableKeyBackup(); } /** @@ -2194,27 +2153,20 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } - const { keyInfo, encodedPrivateKey, privateKey } = - await this.createRecoveryKeyFromPassphrase(password); + // eslint-disable-next-line camelcase + const { algorithm, auth_data, recovery_key, privateKey } = + await this.crypto._backupManager.prepareKeyBackupVersion(password); if (opts.secureSecretStorage) { await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey)); logger.info("Key backup private key stored in secret storage"); } - // Reshape objects into form expected for key backup - const authData: any = { // TODO - public_key: keyInfo.pubkey, - }; - if (keyInfo.passphrase) { - authData.private_key_salt = keyInfo.passphrase.salt; - authData.private_key_iterations = keyInfo.passphrase.iterations; - } return { - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: authData, - recovery_key: encodedPrivateKey, - } as any; // TODO + algorithm, + auth_data, + recovery_key, + } as any; // TODO: Types } /** @@ -2240,6 +2192,8 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } + await this.crypto._backupManager.createKeyBackupVersion(info); + const data = { algorithm: info.algorithm, auth_data: info.auth_data, @@ -2288,8 +2242,8 @@ export class MatrixClient extends EventEmitter { // If we're currently backing up to this backup... stop. // (We start using it automatically in createKeyBackupVersion // so this is symmetrical). - if (this.crypto.backupInfo && this.crypto.backupInfo.version === version) { - this.disableKeyBackup(); + if (this.crypto._backupManager.version) { + this.crypto._backupManager.disableKeyBackup(); } const path = utils.encodeUri("/room_keys/version/$version", { @@ -2354,7 +2308,7 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } - await this.crypto.scheduleAllGroupSessionsForBackup(); + await this.crypto._backupManager.scheduleAllGroupSessionsForBackup(); } /** @@ -2367,7 +2321,7 @@ export class MatrixClient extends EventEmitter { throw new Error("End-to-end encryption disabled"); } - return this.crypto.flagAllGroupSessionsForBackup(); + return this.crypto._backupManager.flagAllGroupSessionsForBackup(); } public isValidRecoveryKey(recoveryKey: string): boolean { @@ -2513,7 +2467,7 @@ export class MatrixClient extends EventEmitter { ); } - private restoreKeyBackup( + private async restoreKeyBackup( privKey: Uint8Array, targetRoomId: string, targetSessionId: string, @@ -2526,6 +2480,7 @@ export class MatrixClient extends EventEmitter { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } + let totalKeyCount = 0; let keys = []; @@ -2533,47 +2488,42 @@ export class MatrixClient extends EventEmitter { targetRoomId, targetSessionId, backupInfo.version, ); - const decryption = new global.Olm.PkDecryption(); - let backupPubKey; + const algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => { return privKey; }); + try { - backupPubKey = decryption.init_with_private_key(privKey); - } catch (e) { - decryption.free(); - throw e; - } + // If the pubkey computed from the private data we've been given + // doesn't match the one in the auth_data, the user has entered + // a different recovery key / the wrong passphrase. + if (!await algorithm.keyMatches(privKey)) { + return Promise.reject({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }); + } - // If the pubkey computed from the private data we've been given - // doesn't match the one in the auth_data, the user has entered - // a different recovery key / the wrong passphrase. - if (backupPubKey !== backupInfo.auth_data.public_key) { - return Promise.reject(new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY })); - } + // Cache the key, if possible. + // This is async. + this.crypto.storeSessionBackupPrivateKey(privKey) + .catch((e) => { + logger.warn("Error caching session backup key:", e); + }).then(cacheCompleteCallback); - // Cache the key, if possible. - // This is async. - this.crypto.storeSessionBackupPrivateKey(privKey) - .catch((e) => { - logger.warn("Error caching session backup key:", e); - }).then(cacheCompleteCallback); + if (progressCallback) { + progressCallback({ + stage: "fetch", + }); + } - if (progressCallback) { - progressCallback({ - stage: "fetch", - }); - } + const res = await this.http.authedRequest( + undefined, "GET", path.path, path.queryData, undefined, + { prefix: PREFIX_UNSTABLE }, + ); - return this.http.authedRequest( - undefined, "GET", path.path, path.queryData, undefined, - { prefix: PREFIX_UNSTABLE }, - ).then((res) => { if (res.rooms) { - // TODO: Types? + // TODO: Types for (const [roomId, roomData] of Object.entries(res.rooms)) { if (!roomData.sessions) continue; totalKeyCount += Object.keys(roomData.sessions).length; - const roomKeys = keysFromRecoverySession( - roomData.sessions, decryption, roomId, + const roomKeys = await algorithm.decryptSessions( + roomData.sessions, ); for (const k of roomKeys) { k.room_id = roomId; @@ -2582,13 +2532,18 @@ export class MatrixClient extends EventEmitter { } } else if (res.sessions) { totalKeyCount = Object.keys(res.sessions).length; - keys = keysFromRecoverySession( - res.sessions, decryption, targetRoomId, + keys = await algorithm.decryptSessions( + res.sessions, ); + for (const k of keys) { + k.room_id = targetRoomId; + } } else { totalKeyCount = 1; try { - const key = keyFromRecoverySession(res, decryption); + const [key] = await algorithm.decryptSessions({ + [targetSessionId]: res, + }); key.room_id = targetRoomId; key.session_id = targetSessionId; keys.push(key); @@ -2596,19 +2551,19 @@ export class MatrixClient extends EventEmitter { logger.log("Failed to decrypt megolm session from backup", e); } } + } finally { + algorithm.free(); + } - return this.importRoomKeys(keys, { - progressCallback, - untrusted: true, - source: "backup", - }); - }).then(() => { - return this.crypto.setTrustedBackupPubKey(backupPubKey); - }).then(() => { - return { total: totalKeyCount, imported: keys.length }; - }).finally(() => { - decryption.free(); + await this.importRoomKeys(keys, { + progressCallback, + untrusted: true, + source: "backup", }); + + await this.checkKeyBackup(); + + return { total: totalKeyCount, imported: keys.length }; } public deleteKeysFromBackup(roomId: string, sessionId: string, version: string): Promise { diff --git a/src/matrix.ts b/src/matrix.ts index 2288d9b71fc..c9192f2ea03 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -118,6 +118,7 @@ export interface ICryptoCallbacks { keyInfo: ISecretStorageKeyInfo, checkFunc: (Uint8Array) => void, ) => Promise; + getBackupKey?: () => Promise; } // TODO: Move this to `SecretStorage` once converted From c2fae3bad8dde2d1a855cd2e512c419b9834dfc0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jun 2021 19:02:46 -0600 Subject: [PATCH 31/32] Fix missed conversion fallout --- spec/unit/crypto/backup.spec.js | 2 +- src/crypto/backup.ts | 46 ++++++++++++++++----------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index eeee60fae33..1bb8a39f8ec 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -581,7 +581,7 @@ describe("MegolmBackup", function() { const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, { algorithm: "this.algorithm.does.not.exist", }); - client._http.authedRequest = function() { + client.http.authedRequest = function() { return Promise.resolve(KEY_BACKUP_DATA); }; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index d513c24aa75..39c9006ffd0 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -84,7 +84,7 @@ export class BackupManager { private backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version public checkedForBackup: boolean; // Have we checked the server for a backup we can use? private sendingBackups: boolean; // Are we currently sending backups? - constructor(private readonly baseApis, public readonly getKey: GetKey) { + constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) { this.checkedForBackup = false; this.sendingBackups = false; } @@ -268,7 +268,7 @@ export class BackupManager { return ret; } - const trustedPubkey = this.baseApis._crypto._sessionStore.getLocalTrustedBackupPubKey(); + const trustedPubkey = this.baseApis.crypto._sessionStore.getLocalTrustedBackupPubKey(); if (backupInfo.auth_data.public_key === trustedPubkey) { logger.info("Backup public key " + trustedPubkey + " is trusted locally"); @@ -288,12 +288,12 @@ export class BackupManager { const sigInfo: SigInfo = { deviceId: keyIdParts[1] }; // first check to see if it's from our cross-signing key - const crossSigningId = this.baseApis._crypto._crossSigningInfo.getId(); + const crossSigningId = this.baseApis.crypto._crossSigningInfo.getId(); if (crossSigningId === sigInfo.deviceId) { sigInfo.crossSigningId = true; try { await verifySignature( - this.baseApis._crypto._olmDevice, + this.baseApis.crypto._olmDevice, backupInfo.auth_data, this.baseApis.getUserId(), sigInfo.deviceId, @@ -313,7 +313,7 @@ export class BackupManager { // Now look for a sig from a device // At some point this can probably go away and we'll just support // it being signed by the cross-signing master key - const device = this.baseApis._crypto._deviceList.getStoredDevice( + const device = this.baseApis.crypto._deviceList.getStoredDevice( this.baseApis.getUserId(), sigInfo.deviceId, ); if (device) { @@ -323,7 +323,7 @@ export class BackupManager { ); try { await verifySignature( - this.baseApis._crypto._olmDevice, + this.baseApis.crypto._olmDevice, backupInfo.auth_data, this.baseApis.getUserId(), device.deviceId, @@ -400,7 +400,7 @@ export class BackupManager { await this.checkKeyBackup(); // Backup version has changed or this backup version // has been deleted - this.baseApis._crypto.emit("crypto.keyBackupFailed", err.data.errcode); + this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode); throw err; } } @@ -423,13 +423,13 @@ export class BackupManager { * @returns {integer} Number of sessions backed up */ private async backupPendingKeys(limit: number): Promise { - const sessions = await this.baseApis._crypto._cryptoStore.getSessionsNeedingBackup(limit); + const sessions = await this.baseApis.crypto._cryptoStore.getSessionsNeedingBackup(limit); if (!sessions.length) { return 0; } - let remaining = await this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup(); - this.baseApis._crypto.emit("crypto.keyBackupSessionsRemaining", remaining); + let remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup(); + this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining); const data = {}; for (const session of sessions) { @@ -438,7 +438,7 @@ export class BackupManager { data[roomId] = { sessions: {} }; } - const sessionData = await this.baseApis._crypto._olmDevice.exportInboundGroupSession( + const sessionData = await this.baseApis.crypto._olmDevice.exportInboundGroupSession( session.senderKey, session.sessionId, session.sessionData, ); sessionData.algorithm = MEGOLM_ALGORITHM; @@ -446,13 +446,13 @@ export class BackupManager { const forwardedCount = (sessionData.forwarding_curve25519_key_chain || []).length; - const userId = this.baseApis._crypto._deviceList.getUserByIdentityKey( + const userId = this.baseApis.crypto._deviceList.getUserByIdentityKey( MEGOLM_ALGORITHM, session.senderKey, ); - const device = this.baseApis._crypto._deviceList.getDeviceByIdentityKey( + const device = this.baseApis.crypto._deviceList.getDeviceByIdentityKey( MEGOLM_ALGORITHM, session.senderKey, ); - const verified = this.baseApis._crypto._checkDeviceInfoTrust(userId, device).isVerified(); + const verified = this.baseApis.crypto._checkDeviceInfoTrust(userId, device).isVerified(); data[roomId]['sessions'][session.sessionId] = { first_message_index: sessionData.first_known_index, @@ -467,9 +467,9 @@ export class BackupManager { { rooms: data }, ); - await this.baseApis._crypto._cryptoStore.unmarkSessionsNeedingBackup(sessions); - remaining = await this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup(); - this.baseApis._crypto.emit("crypto.keyBackupSessionsRemaining", remaining); + await this.baseApis.crypto._cryptoStore.unmarkSessionsNeedingBackup(sessions); + remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup(); + this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining); return sessions.length; } @@ -477,7 +477,7 @@ export class BackupManager { public async backupGroupSession( senderKey: string, sessionId: string, ): Promise { - await this.baseApis._crypto._cryptoStore.markSessionsNeedingBackup([{ + await this.baseApis.crypto._cryptoStore.markSessionsNeedingBackup([{ senderKey: senderKey, sessionId: sessionId, }]); @@ -509,22 +509,22 @@ export class BackupManager { * (which will be equal to the number of sessions in the store). */ public async flagAllGroupSessionsForBackup(): Promise { - await this.baseApis._crypto._cryptoStore.doTxn( + await this.baseApis.crypto._cryptoStore.doTxn( 'readwrite', [ IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP, ], (txn) => { - this.baseApis._crypto._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { + this.baseApis.crypto._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { if (session !== null) { - this.baseApis._crypto._cryptoStore.markSessionsNeedingBackup([session], txn); + this.baseApis.crypto._cryptoStore.markSessionsNeedingBackup([session], txn); } }); }, ); - const remaining = await this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup(); + const remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup(); this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining); return remaining; } @@ -534,7 +534,7 @@ export class BackupManager { * @returns {Promise} Resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { - return this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup(); + return this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup(); } } From 382854c04cb2d743c083f1d669f393a2d0a0b5ff Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jun 2021 19:04:49 -0600 Subject: [PATCH 32/32] Appease linter --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 8a71d550fdd..35851849535 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2495,7 +2495,7 @@ export class MatrixClient extends EventEmitter { // doesn't match the one in the auth_data, the user has entered // a different recovery key / the wrong passphrase. if (!await algorithm.keyMatches(privKey)) { - return Promise.reject({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }); + return Promise.reject(new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY })); } // Cache the key, if possible.