/*
Copyright 2015-2022 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.
*/
/**
* This is an internal module. See {@link MatrixClient} for the public class.
* @module client
*/
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk";
import { ISyncStateData, SyncApi, SyncState } from "./sync";
import {
EventStatus,
IContent,
IDecryptOptions,
IEvent,
MatrixEvent,
MatrixEventEvent,
MatrixEventHandlerMap,
} from "./models/event";
import { StubStore } from "./store/stub";
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
import * as utils from './utils';
import { replaceParam, QueryDict, sleep } from './utils';
import { Direction, EventTimeline } from "./models/event-timeline";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice";
import { IOlmDevice } from "./crypto/algorithms/megolm";
import { TypedReEmitter } from './ReEmitter';
import { IRoomEncryption, RoomList } from './crypto/RoomList';
import { logger } from './logger';
import { SERVICE_TYPES } from './service-types';
import {
HttpApiEvent,
HttpApiEventHandlerMap,
Upload,
UploadOpts,
MatrixError,
MatrixHttpApi,
Method,
retryNetworkOperation,
ClientPrefix,
MediaPrefix,
IdentityPrefix,
IHttpOpts,
FileType,
UploadResponse,
HTTPError,
} from "./http-api";
import {
Crypto,
CryptoEvent,
CryptoEventHandlerMap,
fixBackupKey,
IBootstrapCrossSigningOpts,
ICheckOwnCrossSigningTrustOpts,
IMegolmSessionData,
isCryptoAvailable,
VerificationMethod,
} from './crypto';
import { DeviceInfo, IDevice } from "./crypto/deviceinfo";
import { decodeRecoveryKey } from './crypto/recoverykey';
import { keyFromAuthData } from './crypto/key_passphrase';
import { User, UserEvent, UserEventHandlerMap } from "./models/user";
import { getHttpUriForMxc } from "./content-repo";
import { SearchResult } from "./models/search-result";
import {
DEHYDRATION_ALGORITHM,
IDehydratedDevice,
IDehydratedDeviceKeyInfo,
IDeviceKeys,
IOneTimeKey,
} from "./crypto/dehydration";
import {
IKeyBackupInfo,
IKeyBackupPrepareOpts,
IKeyBackupRestoreOpts,
IKeyBackupRestoreResult,
IKeyBackupRoomSessions,
IKeyBackupSession,
} from "./crypto/keybackup";
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
import { MatrixScheduler } from "./scheduler";
import {
IAuthData,
ICryptoCallbacks,
IMinimalEvent,
IRoomEvent,
IStateEvent,
NotificationCountType,
BeaconEvent,
BeaconEventHandlerMap,
RoomEvent,
RoomEventHandlerMap,
RoomMemberEvent,
RoomMemberEventHandlerMap,
RoomStateEvent,
RoomStateEventHandlerMap,
INotificationsResponse,
IFilterResponse,
ITagsResponse,
IStatusResponse,
IPushRule,
PushRuleActionName,
IAuthDict,
} from "./matrix";
import {
CrossSigningKey,
IAddSecretStorageKeyOpts,
ICreateSecretStorageOpts,
IEncryptedEventInfo,
IImportRoomKeysOpts,
IRecoveryKey,
ISecretStorageKeyInfo,
} from "./crypto/api";
import { EventTimelineSet } from "./models/event-timeline-set";
import { VerificationRequest } from "./crypto/verification/request/VerificationRequest";
import { VerificationBase as Verification } from "./crypto/verification/Base";
import * as ContentHelpers from "./content-helpers";
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
import { Room, RoomNameState } from "./models/room";
import {
IAddThreePidOnlyBody,
IBindThreePidBody,
IContextResponse,
ICreateRoomOpts,
IEventSearchOpts,
IGuestAccessOpts,
IJoinRoomOpts,
IPaginateOpts,
IPresenceOpts,
IRedactOpts,
IRelationsRequestOpts,
IRelationsResponse,
IRoomDirectoryOptions,
ISearchOpts,
ISendEventResponse,
} from "./@types/requests";
import {
EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MsgType,
PUSHER_ENABLED,
RelationType,
RoomCreateTypeField,
RoomType,
UNSTABLE_MSC3088_ENABLED,
UNSTABLE_MSC3088_PURPOSE,
UNSTABLE_MSC3089_TREE_SUBTYPE,
} from "./@types/event";
import { IdServerUnbindResult, IImageInfo, Preset, Visibility } from "./@types/partials";
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
import { randomString } from "./randomstring";
import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup";
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace";
import { ISignatures } from "./@types/signed";
import { IStore } from "./store";
import { ISecretRequest } from "./crypto/SecretStorage";
import {
IEventWithRoomId,
ISearchRequestBody,
ISearchResponse,
ISearchResults,
IStateEventWithRoomId,
SearchOrderBy,
} from "./@types/search";
import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse";
import { IHierarchyRoom } from "./@types/spaces";
import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules";
import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base";
import { MediaHandler } from "./webrtc/mediaHandler";
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { ReceiptType } from "./@types/read_receipts";
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk";
import {
FeatureSupport,
Thread,
THREAD_RELATION_TYPE,
determineFeatureSupport,
ThreadFilterType,
threadFilterTypeToFilter,
} from "./models/thread";
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { UnstableValue } from "./NamespacedValue";
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { MAIN_ROOM_TIMELINE } from "./models/read-receipt";
import { IgnoredInvites } from "./models/invites-ignorer";
import { UIARequest, UIAResponse } from "./@types/uia";
import { LocalNotificationSettings } from "./@types/local_notifications";
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
export type Store = IStore;
export type ResetTimelineCallback = (roomId: string) => boolean;
const SCROLLBACK_DELAY_MS = 3000;
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
export const UNSTABLE_MSC3852_LAST_SEEN_UA = new UnstableValue(
"last_seen_user_agent",
"org.matrix.msc3852.last_seen_user_agent",
);
interface IExportedDevice {
olmDevice: IExportedOlmDevice;
userId: string;
deviceId: string;
}
export interface IKeysUploadResponse {
one_time_key_counts: { // eslint-disable-line camelcase
[algorithm: string]: number;
};
}
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.
* Most supported environments have a global `fetch` registered to which this will fall back.
*/
fetchFn?: typeof global.fetch;
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;
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;
/**
* Method to generate room names for empty rooms and rooms names based on membership.
* Defaults to a built-in English handler with basic pluralisation.
*/
roomNameGenerator?: (roomId: string, state: RoomNameState) => string | null;
}
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;
}
export enum PendingEventOrdering {
Chronological = "chronological",
Detached = "detached",
}
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?: PendingEventOrdering;
/**
* The number of milliseconds to wait on /sync. Default: 30000 (30 seconds).
*/
pollTimeout?: number;
/**
* The filter to apply to /sync calls.
*/
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;
/**
* @experimental
*/
experimentalThreadSupport?: boolean;
/**
* @experimental
*/
slidingSync?: SlidingSync;
}
export interface IStoredClientOpts extends IStartClientOpts {
crypto?: Crypto;
canResetEntireTimeline: ResetTimelineCallback;
}
export enum RoomVersionStability {
Stable = "stable",
Unstable = "unstable",
}
export interface IRoomVersionsCapability {
default: string;
available: Record;
}
export interface ICapability {
enabled: boolean;
}
export interface IChangePasswordCapability extends ICapability {}
export interface IThreadsCapability extends ICapability {}
interface ICapabilities {
[key: string]: any;
"m.change_password"?: IChangePasswordCapability;
"m.room_versions"?: IRoomVersionsCapability;
"io.element.thread"?: IThreadsCapability;
}
/* eslint-disable camelcase */
export interface ICrossSigningKey {
keys: { [algorithm: string]: string };
signatures?: ISignatures;
usage: string[];
user_id: string;
}
enum CrossSigningKeyType {
MasterKey = "master_key",
SelfSigningKey = "self_signing_key",
UserSigningKey = "user_signing_key",
}
export type CrossSigningKeys = Record;
export interface ISignedKey {
keys: Record;
signatures: ISignatures;
user_id: string;
algorithms: string[];
device_id: string;
}
export type KeySignatures = Record>;
export interface IUploadKeySignaturesResponse {
failures: Record>;
}
export interface IPreviewUrlResponse {
[key: string]: undefined | string | number;
"og:title": string;
"og:type": string;
"og:url": string;
"og:image"?: string;
"og:image:type"?: string;
"og:image:height"?: number;
"og:image:width"?: number;
"og:description"?: string;
"matrix:image:size"?: number;
}
interface ITurnServerResponse {
uris: string[];
username: string;
password: string;
ttl: number;
}
export interface ITurnServer {
urls: string[];
username: string;
credential: string;
}
export interface IServerVersions {
versions: string[];
unstable_features: Record;
}
export const M_AUTHENTICATION = new UnstableValue(
"m.authentication",
"org.matrix.msc2965.authentication",
);
export interface IClientWellKnown {
[key: string]: any;
"m.homeserver"?: IWellKnownConfig;
"m.identity_server"?: IWellKnownConfig;
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
}
export interface IWellKnownConfig {
raw?: any; // todo typings
action?: AutoDiscoveryAction;
reason?: string;
error?: Error | string;
// eslint-disable-next-line
base_url?: string | null;
}
export interface IDelegatedAuthConfig { // MSC2965
/** The OIDC Provider/issuer the client should use */
issuer: string;
/** The optional URL of the web UI where the user can manage their account */
account?: string;
}
interface IKeyBackupPath {
path: string;
queryData?: {
version: string;
};
}
interface IMediaConfig {
[key: string]: any; // extensible
"m.upload.size"?: number;
}
interface IThirdPartySigned {
sender: string;
mxid: string;
token: string;
signatures: ISignatures;
}
interface IJoinRequestBody {
third_party_signed?: IThirdPartySigned;
}
interface ITagMetadata {
[key: string]: any;
order: number;
}
interface IMessagesResponse {
start: string;
end: string;
chunk: IRoomEvent[];
state: IStateEvent[];
}
interface IThreadedMessagesResponse {
prev_batch: string;
next_batch: string;
chunk: IRoomEvent[];
state: IStateEvent[];
}
export interface IRequestTokenResponse {
sid: string;
submit_url?: string;
}
export interface IRequestMsisdnTokenResponse extends IRequestTokenResponse {
msisdn: string;
success: boolean;
intl_fmt: string;
}
export interface IUploadKeysRequest {
device_keys?: Required;
one_time_keys?: Record;
"org.matrix.msc2732.fallback_keys"?: Record;
}
export interface IOpenIDToken {
access_token: string;
token_type: "Bearer" | string;
matrix_server_name: string;
expires_in: number;
}
interface IRoomInitialSyncResponse {
room_id: string;
membership: "invite" | "join" | "leave" | "ban";
messages?: {
start?: string;
end?: string;
chunk: IEventWithRoomId[];
};
state?: IStateEventWithRoomId[];
visibility: Visibility;
account_data?: IMinimalEvent[];
presence: Partial; // legacy and undocumented, api is deprecated so this won't get attention
}
interface IJoinedRoomsResponse {
joined_rooms: string[];
}
interface IJoinedMembersResponse {
joined: {
[userId: string]: {
display_name: string;
avatar_url: string;
};
};
}
export interface IRegisterRequestParams {
auth?: IAuthData;
username?: string;
password?: string;
refresh_token?: boolean;
guest_access_token?: string;
x_show_msisdn?: boolean;
bind_msisdn?: boolean;
bind_email?: boolean;
inhibit_login?: boolean;
initial_device_display_name?: string;
}
export interface IPublicRoomsChunkRoom {
room_id: string;
name?: string;
avatar_url?: string;
topic?: string;
canonical_alias?: string;
aliases?: string[];
world_readable: boolean;
guest_can_join: boolean;
num_joined_members: number;
room_type?: RoomType | string; // Added by MSC3827
}
interface IPublicRoomsResponse {
chunk: IPublicRoomsChunkRoom[];
next_batch?: string;
prev_batch?: string;
total_room_count_estimate?: number;
}
interface IUserDirectoryResponse {
results: {
user_id: string;
display_name?: string;
avatar_url?: string;
}[];
limited: boolean;
}
export interface IMyDevice {
device_id: string;
display_name?: string;
last_seen_ip?: string;
last_seen_ts?: number;
// UNSTABLE_MSC3852_LAST_SEEN_UA
last_seen_user_agent?: string;
"org.matrix.msc3852.last_seen_user_agent"?: string;
}
export interface Keys {
keys: { [keyId: string]: string };
usage: string[];
user_id: string;
}
export interface SigningKeys extends Keys {
signatures: ISignatures;
}
export interface DeviceKeys {
[deviceId: string]: IDeviceKeys & {
unsigned?: {
device_display_name: string;
};
};
}
export interface IDownloadKeyResult {
failures: { [serverName: string]: object };
device_keys: { [userId: string]: DeviceKeys };
// the following three fields were added in 1.1
master_keys?: { [userId: string]: Keys };
self_signing_keys?: { [userId: string]: SigningKeys };
user_signing_keys?: { [userId: string]: SigningKeys };
}
export interface IClaimOTKsResult {
failures: { [serverName: string]: object };
one_time_keys: {
[userId: string]: {
[deviceId: string]: {
[keyId: string]: {
key: string;
signatures: ISignatures;
};
};
};
};
}
export interface IFieldType {
regexp: string;
placeholder: string;
}
export interface IInstance {
desc: string;
icon?: string;
fields: object;
network_id: string;
// XXX: this is undocumented but we rely on it: https://github.com/matrix-org/matrix-doc/issues/3203
instance_id: string;
}
export interface IProtocol {
user_fields: string[];
location_fields: string[];
icon: string;
field_types: Record;
instances: IInstance[];
}
interface IThirdPartyLocation {
alias: string;
protocol: string;
fields: object;
}
interface IThirdPartyUser {
userid: string;
protocol: string;
fields: object;
}
interface IRoomSummary extends Omit {
room_type?: RoomType;
membership?: string;
is_encrypted: boolean;
}
interface IRoomKeysResponse {
sessions: IKeyBackupRoomSessions;
}
interface IRoomsKeysResponse {
rooms: Record;
}
interface IRoomHierarchy {
rooms: IHierarchyRoom[];
next_batch?: string;
}
interface ITimestampToEventResponse {
event_id: string;
origin_server_ts: string;
}
/* eslint-enable camelcase */
// We're using this constant for methods overloading and inspect whether a variable
// contains an eventId or not. This was required to ensure backwards compatibility
// of methods for threads
// Probably not the most graceful solution but does a good enough job for now
const EVENT_ID_PREFIX = "$";
export enum ClientEvent {
Sync = "sync",
Event = "event",
ToDeviceEvent = "toDeviceEvent",
AccountData = "accountData",
Room = "Room",
DeleteRoom = "deleteRoom",
SyncUnexpectedError = "sync.unexpectedError",
ClientWellKnown = "WellKnown.client",
TurnServers = "turnServers",
TurnServersError = "turnServers.error",
}
type RoomEvents = RoomEvent.Name
| RoomEvent.Redaction
| RoomEvent.RedactionCancelled
| RoomEvent.Receipt
| RoomEvent.Tags
| RoomEvent.LocalEchoUpdated
| RoomEvent.HistoryImportedWithinTimeline
| RoomEvent.AccountData
| RoomEvent.MyMembership
| RoomEvent.Timeline
| RoomEvent.TimelineReset;
type RoomStateEvents = RoomStateEvent.Events
| RoomStateEvent.Members
| RoomStateEvent.NewMember
| RoomStateEvent.Update
| RoomStateEvent.Marker
;
type CryptoEvents = CryptoEvent.KeySignatureUploadFailure
| CryptoEvent.KeyBackupStatus
| CryptoEvent.KeyBackupFailed
| CryptoEvent.KeyBackupSessionsRemaining
| CryptoEvent.RoomKeyRequest
| CryptoEvent.RoomKeyRequestCancellation
| CryptoEvent.VerificationRequest
| CryptoEvent.DeviceVerificationChanged
| CryptoEvent.UserTrustStatusChanged
| CryptoEvent.KeysChanged
| CryptoEvent.Warning
| CryptoEvent.DevicesUpdated
| CryptoEvent.WillUpdateDevices;
type MatrixEventEvents = MatrixEventEvent.Decrypted | MatrixEventEvent.Replaced | MatrixEventEvent.VisibilityChange;
type RoomMemberEvents = RoomMemberEvent.Name
| RoomMemberEvent.Typing
| RoomMemberEvent.PowerLevel
| RoomMemberEvent.Membership;
type UserEvents = UserEvent.AvatarUrl
| UserEvent.DisplayName
| UserEvent.Presence
| UserEvent.CurrentlyActive
| UserEvent.LastPresenceTs;
export type EmittedEvents = ClientEvent
| RoomEvents
| RoomStateEvents
| CryptoEvents
| MatrixEventEvents
| RoomMemberEvents
| UserEvents
| CallEvent // re-emitted by call.ts using Object.values
| CallEventHandlerEvent.Incoming
| HttpApiEvent.SessionLoggedOut
| HttpApiEvent.NoConsent
| BeaconEvent;
export type ClientEventHandlerMap = {
[ClientEvent.Sync]: (state: SyncState, lastState: SyncState | null, data?: ISyncStateData) => void;
[ClientEvent.Event]: (event: MatrixEvent) => void;
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
[ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void;
[ClientEvent.Room]: (room: Room) => void;
[ClientEvent.DeleteRoom]: (roomId: string) => void;
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
[ClientEvent.TurnServers]: (servers: ITurnServer[]) => void;
[ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void;
} & RoomEventHandlerMap
& RoomStateEventHandlerMap
& CryptoEventHandlerMap
& MatrixEventHandlerMap
& RoomMemberEventHandlerMap
& UserEventHandlerMap
& CallEventHandlerEventHandlerMap
& CallEventHandlerMap
& HttpApiEventHandlerMap
& BeaconEventHandlerMap;
const SSO_ACTION_PARAM = new UnstableValue("action", "org.matrix.msc3824.action");
/**
* 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 TypedEventEmitter {
public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
public reEmitter = new TypedReEmitter(this);
public olmVersion: [number, number, number] | null = null; // populated after initCrypto
public usingExternalCrypto = false;
public store: Store;
public deviceId: string | null;
public credentials: { userId: string | null };
public pickleKey?: string;
public scheduler?: MatrixScheduler;
public clientRunning = false;
public timelineSupport = false;
public urlPreviewCache: { [key: string]: Promise } = {};
public identityServer?: IIdentityServerProvider;
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.
public idBaseUrl?: string;
public baseUrl: string;
// 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 = null;
protected isGuestAccount = false;
protected ongoingScrollbacks: {[roomId: string]: {promise?: Promise, errorTs?: number}} = {};
protected notifTimelineSet: EventTimelineSet | null = null;
protected cryptoStore?: CryptoStore;
protected verificationMethods?: VerificationMethod[];
protected fallbackICEServerAllowed = false;
protected roomList: RoomList;
protected syncApi?: SlidingSyncSdk | SyncApi;
public roomNameGenerator?: ICreateClientOpts["roomNameGenerator"];
public pushRules?: IPushRules;
protected syncLeftRoomsPromise?: Promise;
protected syncedLeftRooms = false;
protected clientOpts?: IStoredClientOpts;
protected clientWellKnownIntervalID?: ReturnType;
protected canResetTimelineCallback?: ResetTimelineCallback;
public canSupport = new Map();
// The pushprocessor caches useful things, so keep one and re-use it
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
protected serverVersionsPromise?: Promise;
public cachedCapabilities?: {
capabilities: ICapabilities;
expiration: number;
};
protected clientWellKnown?: IClientWellKnown;
protected clientWellKnownPromise?: Promise;
protected turnServers: ITurnServer[] = [];
protected turnServersExpiry = 0;
protected checkTurnServersIntervalID?: ReturnType;
protected exportedOlmDeviceToImport?: IExportedOlmDevice;
protected txnCtr = 0;
protected mediaHandler = new MediaHandler(this);
protected pendingEventEncryption = new Map>();
private toDeviceMessageQueue: ToDeviceMessageQueue;
// A manager for determining which invites should be ignored.
public readonly ignoredInvites: IgnoredInvites;
constructor(opts: IMatrixClientCreateOpts) {
super();
opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl);
opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl);
this.baseUrl = opts.baseUrl;
this.idBaseUrl = opts.idBaseUrl;
this.identityServer = opts.identityServer;
this.usingExternalCrypto = opts.usingExternalCrypto ?? false;
this.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
const userId = opts.userId || null;
this.credentials = { userId };
this.http = new MatrixHttpApi(this as ConstructorParameters[0], {
fetchFn: opts.fetchFn,
baseUrl: opts.baseUrl,
idBaseUrl: opts.idBaseUrl,
accessToken: opts.accessToken,
prefix: ClientPrefix.R0,
onlyData: true,
extraParams: opts.queryParams,
localTimeoutMs: opts.localTimeoutMs,
useAuthorizationHeader: opts.useAuthorizationHeader,
});
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;
}
this.scheduler = opts.scheduler;
if (this.scheduler) {
this.scheduler.setProcessFunction(async (eventToSend: MatrixEvent) => {
const room = this.getRoom(eventToSend.getRoomId());
if (eventToSend.status !== EventStatus.SENDING) {
this.updatePendingEventStatus(room, eventToSend, EventStatus.SENDING);
}
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.
room.updatePendingEvent(eventToSend, EventStatus.SENT, res.event_id);
}
return res;
});
}
if (supportsMatrixCall()) {
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(ClientEvent.Sync, this.startCallEventHandler);
}
this.timelineSupport = Boolean(opts.timelineSupport);
this.cryptoStore = opts.cryptoStore;
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);
this.roomNameGenerator = opts.roomNameGenerator;
this.toDeviceMessageQueue = new ToDeviceMessageQueue(this);
// 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(MatrixEventEvent.Decrypted, (event) => {
fixNotificationCountOnDecryption(this, event);
});
// 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(RoomEvent.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 => {
for (const [key, value] of Object.entries(content[eid])) {
if (!utils.isSupportedReceiptType(key)) continue;
if (!value) continue;
if (Object.keys(value).includes(this.getUserId()!)) return true;
}
return false;
}).length > 0;
if (!isSelf) return;
// 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();
let highlightCount = 0;
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - maxHistory) return; // limit reached
const event = events[i];
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?.highlight ? 1 : 0;
}
// Note: we don't need to handle 'total' notifications because the counts
// will come from the server.
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
}
});
this.ignoredInvites = new IgnoredInvites(this);
}
/**
* 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): Promise {
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,
};
}
// 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));
}
if (this.crypto) {
this.crypto.uploadDeviceKeys();
this.crypto.start();
}
// periodically poll for turn servers if we support voip
if (this.canSupportVoip) {
this.checkTurnServersIntervalID = setInterval(() => {
this.checkTurnServers();
}, TURN_CHECK_INTERVAL);
// noinspection ES6MissingAwait
this.checkTurnServers();
}
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();
}
try {
await this.getVersions();
// This should be done with `canSupport`
// TODO: https://github.com/vector-im/element-web/issues/23643
const { threads, list, fwdPagination } = await this.doesServerSupportThread();
Thread.setServerSideSupport(threads);
Thread.setServerSideListSupport(list);
Thread.setServerSideFwdPaginationSupport(fwdPagination);
} catch (e) {
logger.error("Can't fetch server versions, continuing to initialise sync, this will be retried later", e);
}
// shallow-copy the opts dict before modifying and storing it
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
this.clientOpts.crypto = this.crypto;
this.clientOpts.canResetEntireTimeline = (roomId) => {
if (!this.canResetTimelineCallback) {
return false;
}
return this.canResetTimelineCallback(roomId);
};
if (this.clientOpts.slidingSync) {
this.syncApi = new SlidingSyncSdk(this.clientOpts.slidingSync, this, this.clientOpts);
} else {
this.syncApi = new SyncApi(this, this.clientOpts);
}
this.syncApi.sync();
if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
this.clientWellKnownIntervalID = setInterval(() => {
this.fetchClientWellKnown();
}, 1000 * this.clientOpts.clientWellKnownPollPeriod);
this.fetchClientWellKnown();
}
this.toDeviceMessageQueue.start();
}
/**
* High level helper method to stop the client from polling and allow a
* clean shutdown.
*/
public stopClient() {
this.crypto?.stop(); // crypto might have been initialised even if the client wasn't fully started
if (!this.clientRunning) return; // already stopped
logger.log('stopping MatrixClient');
this.clientRunning = false;
this.syncApi?.stop();
this.syncApi = undefined;
this.peekSync?.stopPeeking();
this.callEventHandler?.stop();
this.callEventHandler = undefined;
global.clearInterval(this.checkTurnServersIntervalID);
this.checkTurnServersIntervalID = undefined;
if (this.clientWellKnownIntervalID !== undefined) {
global.clearInterval(this.clientWellKnownIntervalID);
}
this.toDeviceMessageQueue.stop();
}
/**
* 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");
}
if (!this.cryptoCallbacks.getDehydrationKey) {
return;
}
const getDeviceResult = await this.getDehydratedDevice();
if (!getDeviceResult) {
return;
}
if (!getDeviceResult.device_data || !getDeviceResult.device_id) {
logger.info("no dehydrated device found");
return;
}
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<{ success: boolean }>(
Method.Post,
"/dehydrated_device/claim",
undefined,
{
device_id: getDeviceResult.device_id,
},
{
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
},
);
if (rehydrateResult.success) {
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);
}
}
/**
* 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(
Method.Get,
"/dehydrated_device",
undefined, undefined,
{
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
},
);
} catch (e) {
logger.info("could not get dehydrated device", e);
return;
}
}
/**
* 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;
}
return this.crypto.dehydrationManager.setKeyAndQueueDehydration(key, keyInfo, deviceDisplayName);
}
/**
* 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);
return this.crypto.dehydrationManager.dehydrateDevice();
}
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(),
};
}
/**
* 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");
}
const promises: Promise[] = [];
promises.push(this.store.deleteAllData());
if (this.cryptoStore) {
promises.push(this.cryptoStore.deleteAllData());
}
return Promise.all(promises).then(); // .then to fix types
}
/**
* 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 | null {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId;
}
return null;
}
/**
* Get the domain for this client's MXID
* @return {?string} Domain of this MXID
*/
public getDomain(): string | null {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId.replace(/^.*?:/, '');
}
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.
*/
public getUserIdLocalpart(): string | null {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId.split(":")[0].substring(1);
}
return null;
}
/**
* Get the device ID of this client
* @return {?string} device ID
*/
public getDeviceId(): string | null {
return this.deviceId;
}
/**
* Check if the runtime environment supports VoIP calling.
* @return {boolean} True if VoIP is supported.
*/
public supportsVoip(): boolean {
return this.canSupportVoip;
}
/**
* @returns {MediaHandler}
*/
public getMediaHandler(): MediaHandler {
return this.mediaHandler;
}
/**
* 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 | null {
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 | null {
return this.syncApi?.getSyncState() ?? null;
}
/**
* 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(): ISyncStateData | null {
if (!this.syncApi) {
return null;
}
return this.syncApi.getSyncStateData();
}
/**
* 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 undefined
*/
public getScheduler(): MatrixScheduler | undefined {
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.
* Will also retry any outbound to-device messages currently in the queue to be sent
* (retries of regular outgoing events are handled separately, per-event).
* @return {boolean} True if this resulted in a request being retried.
*/
public retryImmediately(): boolean {
// don't await for this promise: we just want to kick it off
this.toDeviceMessageQueue.sendQueue();
return this.syncApi?.retryImmediately() ?? false;
}
/**
* Return the global notification EventTimelineSet, if any
*
* @return {EventTimelineSet} the globl notification EventTimelineSet
*/
public getNotifTimelineSet(): EventTimelineSet | null {
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 this.http.authedRequest<{
capabilities?: ICapabilities;
}>(Method.Get, "/capabilities").catch((e: Error): void => {
// We swallow errors because we need a default object anyhow
logger.error(e);
}).then((r = {}) => {
const capabilities: ICapabilities = 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,
expiration: now + cacheMs,
};
logger.log("Caching capabilities: ", capabilities);
return capabilities;
});
}
/**
* 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?`,
);
}
if (this.crypto) {
logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
}
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,
userId,
this.deviceId,
this.store,
this.cryptoStore,
this.roomList,
this.verificationMethods!,
);
this.reEmitter.reEmit(crypto, [
CryptoEvent.KeyBackupFailed,
CryptoEvent.KeyBackupSessionsRemaining,
CryptoEvent.RoomKeyRequest,
CryptoEvent.RoomKeyRequestCancellation,
CryptoEvent.Warning,
CryptoEvent.DevicesUpdated,
CryptoEvent.WillUpdateDevices,
CryptoEvent.DeviceVerificationChanged,
CryptoEvent.UserTrustStatusChanged,
CryptoEvent.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 as Parameters[0]);
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 | null {
return this.crypto?.getDeviceEd25519Key() ?? null;
}
/**
* Get the Curve25519 key for this device
*
* @return {?string} base64-encoded curve25519 key. Null if crypto is
* disabled.
*/
public getDeviceCurve25519Key(): string | null {
return this.crypto?.getDeviceCurve25519Key() ?? null;
}
/**
* Upload the device keys to the homeserver.
* @return {Promise} A promise that will resolve when the keys are uploaded.
*/
public async uploadKeys(): Promise {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
await 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 {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}.
*/
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 | null {
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.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 | null,
blocked?: boolean | null,
known?: boolean | null,
): 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 | undefined {
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
* @deprecated Use `requestVerification` instead.
*/
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);
}
public checkSecretStorageKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise {
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
* 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);
}
/**
* @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);
}
/**
* @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 | string = CrossSigningKey.Master): string | null {
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 | null {
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 {DeviceTrustLevel}
*/
public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.checkDeviceTrust(userId, deviceId);
}
/**
* Check whether one of our own devices is cross-signed by our
* user's stored keys, regardless of whether we trust those keys yet.
*
* @param {string} deviceId The ID of the device to check
*
* @returns {boolean} true if the device is cross-signed
*/
public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.checkIfOwnDeviceCrossSigned(deviceId);
}
/**
* 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 ICheckOwnCrossSigningTrustOpts object
*/
public checkOwnCrossSigningTrust(opts?: ICheckOwnCrossSigningTrustOpts): Promise {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.checkOwnCrossSigningTrust(opts);
}
/**
* 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);
}
// deprecated: use requestVerification instead
public legacyDeviceVerification(
userId: string,
deviceId: string,
method: VerificationMethod,
): Promise {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.legacyDeviceVerification(userId, deviceId, method);
}
/**
* 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 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 {boolean} True if cross-signing is ready to be used on this device
*/
public isCrossSigningReady(): Promise {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
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 {boolean} [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: IBootstrapCrossSigningOpts) {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
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 {boolean} True if trusting cross-signed devices
*/
public getCryptoTrustCrossSignedDevices(): boolean {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.getCryptoTrustCrossSignedDevices();
}
/**
* See getCryptoTrustCrossSignedDevices
*
* @param {boolean} 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);
}
/**
* 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