diff --git a/.eslintrc.js b/.eslintrc.js index 1735739e326..6fc5b99a671 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,6 +31,9 @@ module.exports = { "no-async-promise-executor": "off", // We use a `logger` intermediary module "no-console": "error", + + // restrict EventEmitters to force callers to use TypedEventEmitter + "no-restricted-imports": ["error", "events"], }, overrides: [{ files: [ diff --git a/spec/integ/matrix-client-retrying.spec.ts b/spec/integ/matrix-client-retrying.spec.ts index d0335668f02..6f74e4188b8 100644 --- a/spec/integ/matrix-client-retrying.spec.ts +++ b/spec/integ/matrix-client-retrying.spec.ts @@ -1,4 +1,4 @@ -import { EventStatus } from "../../src/matrix"; +import { EventStatus, RoomEvent } from "../../src/matrix"; import { MatrixScheduler } from "../../src/scheduler"; import { Room } from "../../src/models/room"; import { TestClient } from "../TestClient"; @@ -95,7 +95,7 @@ describe("MatrixClient retrying", function() { // wait for the localecho of ev1 to be updated const p3 = new Promise((resolve, reject) => { - room.on("Room.localEchoUpdated", (ev0) => { + room.on(RoomEvent.LocalEchoUpdated, (ev0) => { if (ev0 === ev1) { resolve(); } diff --git a/spec/unit/ReEmitter.spec.ts b/spec/unit/ReEmitter.spec.ts index 3570b06fea1..4ce28429d12 100644 --- a/spec/unit/ReEmitter.spec.ts +++ b/spec/unit/ReEmitter.spec.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// eslint-disable-next-line no-restricted-imports import { EventEmitter } from "events"; import { ReEmitter } from "../../src/ReEmitter"; diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 3245a28c0ad..450a99af43e 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -1,4 +1,5 @@ import '../olm-loader'; +// eslint-disable-next-line no-restricted-imports import { EventEmitter } from "events"; import { Crypto } from "../../src/crypto"; diff --git a/spec/unit/crypto/crypto-utils.js b/spec/unit/crypto/crypto-utils.js index b54b1a18ebe..ecc6fc4b0ae 100644 --- a/spec/unit/crypto/crypto-utils.js +++ b/spec/unit/crypto/crypto-utils.js @@ -26,7 +26,7 @@ export async function resetCrossSigningKeys(client, { crypto.crossSigningInfo.keys = oldKeys; throw e; } - crypto.baseApis.emit("crossSigning.keysChanged", {}); + crypto.emit("crossSigning.keysChanged", {}); await crypto.afterCrossSigningLocalKeyChange(); } diff --git a/spec/unit/crypto/verification/secret_request.spec.js b/spec/unit/crypto/verification/secret_request.spec.js index 4b768311a3d..398edc10a60 100644 --- a/spec/unit/crypto/verification/secret_request.spec.js +++ b/spec/unit/crypto/verification/secret_request.spec.js @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { VerificationBase } from '../../../../src/crypto/verification/Base'; import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning'; import { encodeBase64 } from "../../../../src/crypto/olmlib"; import { setupWebcrypto, teardownWebcrypto } from './util'; +import { VerificationBase } from '../../../../src/crypto/verification/Base'; jest.useFakeTimers(); diff --git a/spec/unit/relations.spec.ts b/spec/unit/relations.spec.ts index 27370fba0e5..1b479ebbef8 100644 --- a/spec/unit/relations.spec.ts +++ b/spec/unit/relations.spec.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { EventTimelineSet } from "../../src/models/event-timeline-set"; -import { MatrixEvent } from "../../src/models/event"; +import { MatrixEvent, MatrixEventEvent } from "../../src/models/event"; import { Room } from "../../src/models/room"; import { Relations } from "../../src/models/relations"; @@ -103,7 +103,7 @@ describe("Relations", function() { // Add the target event first, then the relation event { const relationsCreated = new Promise(resolve => { - targetEvent.once("Event.relationsCreated", resolve); + targetEvent.once(MatrixEventEvent.RelationsCreated, resolve); }); const timelineSet = new EventTimelineSet(room, { @@ -118,7 +118,7 @@ describe("Relations", function() { // Add the relation event first, then the target event { const relationsCreated = new Promise(resolve => { - targetEvent.once("Event.relationsCreated", resolve); + targetEvent.once(MatrixEventEvent.RelationsCreated, resolve); }); const timelineSet = new EventTimelineSet(room, { diff --git a/src/ReEmitter.ts b/src/ReEmitter.ts index 03c13dd602e..5a352b8f077 100644 --- a/src/ReEmitter.ts +++ b/src/ReEmitter.ts @@ -16,16 +16,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +// eslint-disable-next-line no-restricted-imports import { EventEmitter } from "events"; -export class ReEmitter { - private target: EventEmitter; +import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter"; - constructor(target: EventEmitter) { - this.target = target; - } +export class ReEmitter { + constructor(private readonly target: EventEmitter) {} - reEmit(source: EventEmitter, eventNames: string[]) { + public reEmit(source: EventEmitter, eventNames: string[]): void { for (const eventName of eventNames) { // We include the source as the last argument for event handlers which may need it, // such as read receipt listeners on the client class which won't have the context @@ -48,3 +47,19 @@ export class ReEmitter { } } } + +export class TypedReEmitter< + Events extends string, + Arguments extends ListenerMap, +> extends ReEmitter { + constructor(target: TypedEventEmitter) { + super(target); + } + + public reEmit( + source: TypedEventEmitter, + eventNames: T[], + ): void { + super.reEmit(source, eventNames); + } +} diff --git a/src/client.ts b/src/client.ts index dafa4937f93..01c049a98d6 100644 --- a/src/client.ts +++ b/src/client.ts @@ -19,15 +19,22 @@ limitations under the License. * @module client */ -import { EventEmitter } from "events"; import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk"; import { ISyncStateData, SyncApi, SyncState } from "./sync"; -import { EventStatus, IContent, IDecryptOptions, IEvent, MatrixEvent } from "./models/event"; +import { + EventStatus, + IContent, + IDecryptOptions, + IEvent, + MatrixEvent, + MatrixEventEvent, + MatrixEventHandlerMap, +} from "./models/event"; import { StubStore } from "./store/stub"; -import { createNewMatrixCall, MatrixCall } from "./webrtc/call"; +import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall } from "./webrtc/call"; import { Filter, IFilterDefinition } from "./filter"; -import { CallEventHandler } from './webrtc/callEventHandler'; +import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler'; import * as utils from './utils'; import { sleep } from './utils'; import { Group } from "./models/group"; @@ -37,12 +44,12 @@ import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery"; import * as olmlib from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice"; -import { ReEmitter } from './ReEmitter'; +import { TypedReEmitter } from './ReEmitter'; import { IRoomEncryption, RoomList } from './crypto/RoomList'; import { logger } from './logger'; import { SERVICE_TYPES } from './service-types'; import { - FileType, + FileType, HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IUpload, MatrixError, @@ -58,6 +65,8 @@ import { } from "./http-api"; import { Crypto, + CryptoEvent, + CryptoEventHandlerMap, fixBackupKey, IBootstrapCrossSigningOpts, ICheckOwnCrossSigningTrustOpts, @@ -68,7 +77,7 @@ import { import { DeviceInfo, IDevice } from "./crypto/deviceinfo"; import { decodeRecoveryKey } from './crypto/recoverykey'; import { keyFromAuthData } from './crypto/key_passphrase'; -import { User } from "./models/user"; +import { User, UserEvent, UserEventHandlerMap } from "./models/user"; import { getHttpUriForMxc } from "./content-repo"; import { SearchResult } from "./models/search-result"; import { @@ -88,7 +97,20 @@ import { } from "./crypto/keybackup"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import { MatrixScheduler } from "./scheduler"; -import { IAuthData, ICryptoCallbacks, IMinimalEvent, IRoomEvent, IStateEvent, NotificationCountType } from "./matrix"; +import { + IAuthData, + ICryptoCallbacks, + IMinimalEvent, + IRoomEvent, + IStateEvent, + NotificationCountType, + RoomEvent, + RoomEventHandlerMap, + RoomMemberEvent, + RoomMemberEventHandlerMap, + RoomStateEvent, + RoomStateEventHandlerMap, +} from "./matrix"; import { CrossSigningKey, IAddSecretStorageKeyOpts, @@ -155,6 +177,7 @@ import { IThreepid } from "./@types/threepids"; import { CryptoStore } from "./crypto/store/base"; import { MediaHandler } from "./webrtc/mediaHandler"; import { IRefreshTokenResponse } from "./@types/auth"; +import { TypedEventEmitter } from "./models/typed-event-emitter"; export type Store = IStore; export type SessionStore = WebStorageSessionStore; @@ -453,7 +476,7 @@ export interface ISignedKey { } export type KeySignatures = Record>; -interface IUploadKeySignaturesResponse { +export interface IUploadKeySignaturesResponse { failures: Record 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.Group]: (group: Group) => void; + [ClientEvent.GroupProfile]: (group: Group) => void; + [ClientEvent.GroupMyMembership]: (group: Group) => void; +} & RoomEventHandlerMap + & RoomStateEventHandlerMap + & CryptoEventHandlerMap + & MatrixEventHandlerMap + & RoomMemberEventHandlerMap + & UserEventHandlerMap + & CallEventHandlerEventHandlerMap + & CallEventHandlerMap + & HttpApiEventHandlerMap; + /** * 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 { +export class MatrixClient extends TypedEventEmitter { public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; - public reEmitter = new ReEmitter(this); + public reEmitter = new TypedReEmitter(this); public olmVersion: [number, number, number] = null; // populated after initCrypto public usingExternalCrypto = false; public store: Store; @@ -836,7 +951,7 @@ export class MatrixClient extends EventEmitter { const userId = opts.userId || null; this.credentials = { userId }; - this.http = new MatrixHttpApi(this, { + this.http = new MatrixHttpApi(this as ConstructorParameters[0], { baseUrl: opts.baseUrl, idBaseUrl: opts.idBaseUrl, accessToken: opts.accessToken, @@ -897,7 +1012,7 @@ export class MatrixClient extends EventEmitter { // 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.on(ClientEvent.Sync, this.startCallEventHandler); } this.timelineSupport = Boolean(opts.timelineSupport); @@ -922,7 +1037,7 @@ export class MatrixClient extends EventEmitter { // 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) => { + this.on(MatrixEventEvent.Decrypted, (event) => { const oldActions = event.getPushActions(); const actions = this.getPushActionsForEvent(event, true); @@ -957,7 +1072,7 @@ export class MatrixClient extends EventEmitter { // 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) => { + 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(); @@ -992,7 +1107,7 @@ export class MatrixClient extends EventEmitter { // Note: we don't need to handle 'total' notifications because the counts // will come from the server. - room.setUnreadNotificationCount("highlight", highlightCount); + room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount); } }); } @@ -1557,16 +1672,16 @@ export class MatrixClient extends EventEmitter { ); this.reEmitter.reEmit(crypto, [ - "crypto.keyBackupFailed", - "crypto.keyBackupSessionsRemaining", - "crypto.roomKeyRequest", - "crypto.roomKeyRequestCancellation", - "crypto.warning", - "crypto.devicesUpdated", - "crypto.willUpdateDevices", - "deviceVerificationChanged", - "userTrustStatusChanged", - "crossSigning.keysChanged", + 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..."); @@ -1578,9 +1693,8 @@ export class MatrixClient extends EventEmitter { this.olmVersion = Crypto.getOlmVersion(); - // if crypto initialisation was successful, tell it to attach its event - // handlers. - crypto.registerEventHandlers(this); + // if crypto initialisation was successful, tell it to attach its event handlers. + crypto.registerEventHandlers(this as Parameters[0]); this.crypto = crypto; } @@ -1820,7 +1934,7 @@ export class MatrixClient extends EventEmitter { * @returns {Verification} a verification object * @deprecated Use `requestVerification` instead. */ - public beginKeyVerification(method: string, userId: string, deviceId: string): Verification { + public beginKeyVerification(method: string, userId: string, deviceId: string): Verification { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -3660,7 +3774,7 @@ export class MatrixClient extends EventEmitter { const targetId = localEvent.getAssociatedId(); if (targetId && targetId.startsWith("~")) { const target = room.getPendingEvents().find(e => e.getId() === targetId); - target.once("Event.localEventIdReplaced", () => { + target.once(MatrixEventEvent.LocalEventIdReplaced, () => { localEvent.updateAssociatedId(target.getId()); }); } @@ -4758,7 +4872,7 @@ export class MatrixClient extends EventEmitter { } return promise.then((response) => { this.store.removeRoom(roomId); - this.emit("deleteRoom", roomId); + this.emit(ClientEvent.DeleteRoom, roomId); return response; }); } @@ -4911,7 +5025,7 @@ export class MatrixClient extends EventEmitter { const user = this.getUser(this.getUserId()); if (user) { user.displayName = name; - user.emit("User.displayName", user.events.presence, user); + user.emit(UserEvent.DisplayName, user.events.presence, user); } return prom; } @@ -4928,7 +5042,7 @@ export class MatrixClient extends EventEmitter { const user = this.getUser(this.getUserId()); if (user) { user.avatarUrl = url; - user.emit("User.avatarUrl", user.events.presence, user); + user.emit(UserEvent.AvatarUrl, user.events.presence, user); } return prom; } @@ -6098,7 +6212,7 @@ export class MatrixClient extends EventEmitter { private startCallEventHandler = (): void => { if (this.isInitialSyncComplete()) { this.callEventHandler.start(); - this.off("sync", this.startCallEventHandler); + this.off(ClientEvent.Sync, this.startCallEventHandler); } }; @@ -6246,7 +6360,7 @@ export class MatrixClient extends EventEmitter { // it absorbs errors and returns `{}`. this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain()); this.clientWellKnown = await this.clientWellKnownPromise; - this.emit("WellKnown.client", this.clientWellKnown); + this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown); } public getClientWellKnown(): IClientWellKnown { @@ -6510,7 +6624,7 @@ export class MatrixClient extends EventEmitter { const allEvents = originalEvent ? events.concat(originalEvent) : events; await Promise.all(allEvents.map(e => { if (e.isEncrypted()) { - return new Promise(resolve => e.once("Event.decrypted", resolve)); + return new Promise(resolve => e.once(MatrixEventEvent.Decrypted, resolve)); } })); events = events.filter(e => e.getType() === eventType); diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index 077d705b846..21dd0ee1623 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -19,7 +19,6 @@ limitations under the License. * @module crypto/CrossSigning */ -import { EventEmitter } from 'events'; import { PkSigning } from "@matrix-org/olm"; import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib'; @@ -55,7 +54,7 @@ export interface ICrossSigningInfo { crossSigningVerifiedBefore: boolean; } -export class CrossSigningInfo extends EventEmitter { +export class CrossSigningInfo { public keys: Record = {}; public firstUse = true; // This tracks whether we've ever verified this user with any identity. @@ -79,9 +78,7 @@ export class CrossSigningInfo extends EventEmitter { public readonly userId: string, private callbacks: ICryptoCallbacks = {}, private cacheCallbacks: ICacheCallbacks = {}, - ) { - super(); - } + ) {} public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo { const res = new CrossSigningInfo(userId); diff --git a/src/crypto/DeviceList.ts b/src/crypto/DeviceList.ts index 6e951263cab..1de1f989496 100644 --- a/src/crypto/DeviceList.ts +++ b/src/crypto/DeviceList.ts @@ -20,8 +20,6 @@ limitations under the License. * Manages the list of other users' devices */ -import { EventEmitter } from 'events'; - import { logger } from '../logger'; import { DeviceInfo, IDevice } from './deviceinfo'; import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning'; @@ -31,6 +29,8 @@ import { chunkPromises, defer, IDeferred, sleep } from '../utils'; import { IDownloadKeyResult, MatrixClient } from "../client"; import { OlmDevice } from "./OlmDevice"; import { CryptoStore } from "./store/base"; +import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { CryptoEvent, CryptoEventHandlerMap } from "./index"; /* State transition diagram for DeviceList.deviceTrackingStatus * @@ -62,10 +62,12 @@ export enum TrackingStatus { export type DeviceInfoMap = Record>; +type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated; + /** * @alias module:crypto/DeviceList */ -export class DeviceList extends EventEmitter { +export class DeviceList extends TypedEventEmitter { private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {}; public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {}; @@ -634,7 +636,7 @@ export class DeviceList extends EventEmitter { }); const finished = (success: boolean): void => { - this.emit("crypto.willUpdateDevices", users, !this.hasFetched); + this.emit(CryptoEvent.WillUpdateDevices, users, !this.hasFetched); users.forEach((u) => { this.dirty = true; @@ -659,7 +661,7 @@ export class DeviceList extends EventEmitter { } }); this.saveIfDirty(); - this.emit("crypto.devicesUpdated", users, !this.hasFetched); + this.emit(CryptoEvent.DevicesUpdated, users, !this.hasFetched); this.hasFetched = true; }; @@ -867,7 +869,7 @@ class DeviceListUpdateSerialiser { // NB. Unlike most events in the js-sdk, this one is internal to the // js-sdk and is not re-emitted - this.deviceList.emit('userCrossSigningUpdated', userId); + this.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, userId); } } } diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index 27bcf7d780d..61ba34eaf99 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -14,17 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventEmitter } from "events"; - import { logger } from "../logger"; import { MatrixEvent } from "../models/event"; import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning"; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { Method, PREFIX_UNSTABLE } from "../http-api"; import { Crypto, IBootstrapCrossSigningOpts } from "./index"; -import { CrossSigningKeys, ICrossSigningKey, ICryptoCallbacks, ISignedKey, KeySignatures } from "../matrix"; +import { + ClientEvent, + CrossSigningKeys, + ClientEventHandlerMap, + ICrossSigningKey, + ICryptoCallbacks, + ISignedKey, + KeySignatures, +} from "../matrix"; import { ISecretStorageKeyInfo } from "./api"; import { IKeyBackupInfo } from "./keybackup"; +import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { IAccountDataClient } from "./SecretStorage"; interface ICrossSigningKeys { authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; @@ -256,7 +264,10 @@ export class EncryptionSetupOperation { * Catches account data set by SecretStorage during bootstrapping by * implementing the methods related to account data in MatrixClient */ -class AccountDataClientAdapter extends EventEmitter { +class AccountDataClientAdapter + extends TypedEventEmitter + implements IAccountDataClient { + // public readonly values = new Map(); /** @@ -303,7 +314,7 @@ class AccountDataClientAdapter extends EventEmitter { // and it seems to rely on this. return Promise.resolve().then(() => { const event = new MatrixEvent({ type, content }); - this.emit("accountData", event, lastEvent); + this.emit(ClientEvent.AccountData, event, lastEvent); return {}; }); } diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index f3cdb8683f3..b0c7891d0d6 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventEmitter } from 'stream'; - import { logger } from '../logger'; import * as olmlib from './olmlib'; +import { encodeBase64 } from './olmlib'; import { randomString } from '../randomstring'; -import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes'; -import { encodeBase64 } from "./olmlib"; -import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix'; +import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes'; +import { ClientEvent, ICryptoCallbacks, MatrixEvent } from '../matrix'; +import { ClientEventHandlerMap, MatrixClient } from "../client"; import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api'; +import { TypedEventEmitter } from '../models/typed-event-emitter'; export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; @@ -36,7 +36,7 @@ export interface ISecretRequest { cancel: (reason: string) => void; } -export interface IAccountDataClient extends EventEmitter { +export interface IAccountDataClient extends TypedEventEmitter { // Subset of MatrixClient (which also uses any for the event content) getAccountDataFromServer: (eventType: string) => Promise; getAccountData: (eventType: string) => MatrixEvent; @@ -98,17 +98,17 @@ export class SecretStorage { ev.getType() === 'm.secret_storage.default_key' && ev.getContent().key === keyId ) { - this.accountDataAdapter.removeListener('accountData', listener); + this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener); resolve(); } }; - this.accountDataAdapter.on('accountData', listener); + this.accountDataAdapter.on(ClientEvent.AccountData, listener); this.accountDataAdapter.setAccountData( 'm.secret_storage.default_key', { key: keyId }, ).catch(e => { - this.accountDataAdapter.removeListener('accountData', listener); + this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener); reject(e); }); }); diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 3577fade720..f3a6824d140 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -26,14 +26,13 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib"; import { DeviceInfo } from "./deviceinfo"; import { DeviceTrustLevel } from './CrossSigning'; import { keyFromPassphrase } from './key_passphrase'; -import { sleep } from "../utils"; +import { getCrypto, sleep } from "../utils"; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { encodeRecoveryKey } from './recoverykey'; -import { encryptAES, decryptAES, calculateKeyCheck } from './aes'; -import { getCrypto } from '../utils'; -import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup"; +import { calculateKeyCheck, decryptAES, encryptAES } from './aes'; +import { IAes256AuthData, ICurve25519AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup"; import { UnstableValue } from "../NamespacedValue"; -import { IMegolmSessionData } from "./index"; +import { CryptoEvent, IMegolmSessionData } from "./index"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; @@ -155,7 +154,7 @@ export class BackupManager { this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); - this.baseApis.emit('crypto.keyBackupStatus', true); + this.baseApis.emit(CryptoEvent.KeyBackupStatus, true); // There may be keys left over from a partially completed backup, so // schedule a send to check. @@ -173,7 +172,7 @@ export class BackupManager { this.backupInfo = undefined; - this.baseApis.emit('crypto.keyBackupStatus', false); + this.baseApis.emit(CryptoEvent.KeyBackupStatus, false); } public getKeyBackupEnabled(): boolean | null { @@ -458,7 +457,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(CryptoEvent.KeyBackupFailed, err.data.errcode); throw err; } } @@ -487,7 +486,7 @@ export class BackupManager { } let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); - this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining); + this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); const rooms: IKeyBackup["rooms"] = {}; for (const session of sessions) { @@ -524,7 +523,7 @@ export class BackupManager { await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions); remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); - this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining); + this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); return sessions.length; } @@ -580,7 +579,7 @@ export class BackupManager { ); const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); - this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining); + this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); return remaining; } diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 03650d069ad..14771da4312 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -22,27 +22,36 @@ limitations under the License. */ import anotherjson from "another-json"; -import { EventEmitter } from 'events'; -import { ReEmitter } from '../ReEmitter'; +import { TypedReEmitter } from '../ReEmitter'; import { logger } from '../logger'; import { IExportedDevice, OlmDevice } from "./OlmDevice"; import * as olmlib from "./olmlib"; import { DeviceInfoMap, DeviceList } from "./DeviceList"; import { DeviceInfo, IDevice } from "./deviceinfo"; +import type { DecryptionAlgorithm, EncryptionAlgorithm } from "./algorithms"; import * as algorithms from "./algorithms"; import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning'; import { EncryptionSetupBuilder } from "./EncryptionSetup"; import { + IAccountDataClient, + ISecretRequest, SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage, - SecretStorageKeyTuple, - ISecretRequest, SecretStorageKeyObject, + SecretStorageKeyTuple, } from './SecretStorage'; -import { IAddSecretStorageKeyOpts, ICreateSecretStorageOpts, IImportRoomKeysOpts, ISecretStorageKeyInfo } from "./api"; +import { + IAddSecretStorageKeyOpts, + ICreateSecretStorageOpts, + IEncryptedEventInfo, + IImportRoomKeysOpts, + IRecoveryKey, + ISecretStorageKeyInfo, +} from "./api"; import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; +import { VerificationBase } from "./verification/Base"; import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode'; import { SAS as SASVerification } from './verification/SAS'; import { keyFromPassphrase } from './key_passphrase'; @@ -52,21 +61,28 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel"; import { IllegalMethod } from "./verification/IllegalMethod"; import { KeySignatureUploadError } from "../errors"; -import { decryptAES, encryptAES, calculateKeyCheck } from './aes'; +import { calculateKeyCheck, decryptAES, encryptAES } from './aes'; import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration'; import { BackupManager } from "./backup"; import { IStore } from "../store"; -import { Room } from "../models/room"; -import { RoomMember } from "../models/room-member"; -import { MatrixEvent, EventStatus, IClearEvent, IEvent } from "../models/event"; -import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey, ICrossSigningKey } from "../client"; -import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base"; +import { Room, RoomEvent } from "../models/room"; +import { RoomMember, RoomMemberEvent } from "../models/room-member"; +import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event"; +import { + ClientEvent, + ICrossSigningKey, + IKeysUploadResponse, + ISignedKey, + IUploadKeySignaturesResponse, + MatrixClient, + SessionStore, +} from "../client"; import type { IRoomEncryption, RoomList } from "./RoomList"; -import { IRecoveryKey, IEncryptedEventInfo } from "./api"; import { IKeyBackupInfo } from "./keybackup"; import { ISyncStateData } from "../sync"; import { CryptoStore } from "./store/base"; import { IVerificationChannel } from "./verification/request/Channel"; +import { TypedEventEmitter } from "../models/typed-event-emitter"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -186,7 +202,45 @@ export interface IRequestsMap { setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void; } -export class Crypto extends EventEmitter { +export enum CryptoEvent { + DeviceVerificationChanged = "deviceVerificationChanged", + UserTrustStatusChanged = "userTrustStatusChanged", + UserCrossSigningUpdated = "userCrossSigningUpdated", + RoomKeyRequest = "crypto.roomKeyRequest", + RoomKeyRequestCancellation = "crypto.roomKeyRequestCancellation", + KeyBackupStatus = "crypto.keyBackupStatus", + KeyBackupFailed = "crypto.keyBackupFailed", + KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining", + KeySignatureUploadFailure = "crypto.keySignatureUploadFailure", + VerificationRequest = "crypto.verification.request", + Warning = "crypto.warning", + WillUpdateDevices = "crypto.willUpdateDevices", + DevicesUpdated = "crypto.devicesUpdated", + KeysChanged = "crossSigning.keysChanged", +} + +export type CryptoEventHandlerMap = { + [CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void; + [CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void; + [CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void; + [CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void; + [CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void; + [CryptoEvent.KeyBackupFailed]: (errcode: string) => void; + [CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void; + [CryptoEvent.KeySignatureUploadFailure]: ( + failures: IUploadKeySignaturesResponse["failures"], + source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification", + upload: (opts: { shouldEmit: boolean }) => Promise + ) => void; + [CryptoEvent.VerificationRequest]: (request: VerificationRequest) => void; + [CryptoEvent.Warning]: (type: string) => void; + [CryptoEvent.KeysChanged]: (data: {}) => void; + [CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void; + [CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void; + [CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void; +}; + +export class Crypto extends TypedEventEmitter { /** * @return {string} The version of Olm. */ @@ -201,8 +255,8 @@ export class Crypto extends EventEmitter { public readonly dehydrationManager: DehydrationManager; public readonly secretStorage: SecretStorage; - private readonly reEmitter: ReEmitter; - private readonly verificationMethods: any; // TODO types + private readonly reEmitter: TypedReEmitter; + private readonly verificationMethods: Map; public readonly supportedAlgorithms: string[]; private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager; private readonly toDeviceVerificationRequests: ToDeviceRequests; @@ -295,10 +349,10 @@ export class Crypto extends EventEmitter { private readonly clientStore: IStore, public readonly cryptoStore: CryptoStore, private readonly roomList: RoomList, - verificationMethods: any[], // TODO types + verificationMethods: Array, ) { super(); - this.reEmitter = new ReEmitter(this); + this.reEmitter = new TypedReEmitter(this); if (verificationMethods) { this.verificationMethods = new Map(); @@ -307,20 +361,21 @@ export class Crypto extends EventEmitter { if (defaultVerificationMethods[method]) { this.verificationMethods.set( method, - defaultVerificationMethods[method], + defaultVerificationMethods[method], ); } - } else if (method.NAME) { + } else if (method["NAME"]) { this.verificationMethods.set( - method.NAME, - method, + method["NAME"], + method as typeof VerificationBase, ); } else { logger.warn(`Excluding unknown verification method ${method}`); } } } else { - this.verificationMethods = defaultVerificationMethods; + this.verificationMethods = + new Map(Object.entries(defaultVerificationMethods)) as Map; } this.backupManager = new BackupManager(baseApis, async () => { @@ -358,8 +413,8 @@ export class Crypto extends EventEmitter { // XXX: This isn't removed at any point, but then none of the event listeners // this class sets seem to be removed at any point... :/ - this.deviceList.on('userCrossSigningUpdated', this.onDeviceListUserCrossSigningUpdated); - this.reEmitter.reEmit(this.deviceList, ["crypto.devicesUpdated", "crypto.willUpdateDevices"]); + this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated); + this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]); this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES); @@ -375,7 +430,7 @@ export class Crypto extends EventEmitter { this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks); // Yes, we pass the client twice here: see SecretStorage - this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks, baseApis); + this.secretStorage = new SecretStorage(baseApis as IAccountDataClient, cryptoCallbacks, baseApis); this.dehydrationManager = new DehydrationManager(this); // Assuming no app-supplied callback, default to getting from SSSS. @@ -487,7 +542,7 @@ export class Crypto extends EventEmitter { deviceTrust.isCrossSigningVerified() ) { const deviceObj = this.deviceList.getStoredDevice(userId, deviceId); - this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); + this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj); } } } @@ -1165,7 +1220,7 @@ export class Crypto extends EventEmitter { if (Object.keys(failures || []).length > 0) { if (shouldEmit) { this.baseApis.emit( - "crypto.keySignatureUploadFailure", + CryptoEvent.KeySignatureUploadFailure, failures, "afterCrossSigningLocalKeyChange", upload, // continuation @@ -1391,11 +1446,10 @@ export class Crypto extends EventEmitter { // that reset the keys this.storeTrustedSelfKeys(null); // emit cross-signing has been disabled - this.emit("crossSigning.keysChanged", {}); + this.emit(CryptoEvent.KeysChanged, {}); // as the trust for our own user has changed, // also emit an event for this - this.emit("userTrustStatusChanged", - this.userId, this.checkUserTrust(userId)); + this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId)); } } else { await this.checkDeviceVerifications(userId); @@ -1410,7 +1464,7 @@ export class Crypto extends EventEmitter { this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage()); } - this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId)); } }; @@ -1567,7 +1621,7 @@ export class Crypto extends EventEmitter { if (Object.keys(failures || []).length > 0) { if (shouldEmit) { this.baseApis.emit( - "crypto.keySignatureUploadFailure", + CryptoEvent.KeySignatureUploadFailure, failures, "checkOwnCrossSigningTrust", upload, @@ -1585,10 +1639,10 @@ export class Crypto extends EventEmitter { upload({ shouldEmit: true }); } - this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId)); if (masterChanged) { - this.baseApis.emit("crossSigning.keysChanged", {}); + this.emit(CryptoEvent.KeysChanged, {}); await this.afterCrossSigningLocalKeyChange(); } @@ -1675,18 +1729,14 @@ export class Crypto extends EventEmitter { * @param {external:EventEmitter} eventEmitter event source where we can register * for event notifications */ - public registerEventHandlers(eventEmitter: EventEmitter): void { - eventEmitter.on("RoomMember.membership", (event: MatrixEvent, member: RoomMember, oldMembership?: string) => { - try { - this.onRoomMembership(event, member, oldMembership); - } catch (e) { - logger.error("Error handling membership change:", e); - } - }); - - eventEmitter.on("toDeviceEvent", this.onToDeviceEvent); - eventEmitter.on("Room.timeline", this.onTimelineEvent); - eventEmitter.on("Event.decrypted", this.onTimelineEvent); + public registerEventHandlers(eventEmitter: TypedEventEmitter< + RoomMemberEvent.Membership | ClientEvent.ToDeviceEvent | RoomEvent.Timeline | MatrixEventEvent.Decrypted, + any + >): void { + eventEmitter.on(RoomMemberEvent.Membership, this.onMembership); + eventEmitter.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); + eventEmitter.on(RoomEvent.Timeline, this.onTimelineEvent); + eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent); } /** Start background processes related to crypto */ @@ -2070,9 +2120,7 @@ export class Crypto extends EventEmitter { if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) { this.storeTrustedSelfKeys(xsk.keys); // This will cause our own user trust to change, so emit the event - this.emit( - "userTrustStatusChanged", this.userId, this.checkUserTrust(userId), - ); + this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId)); } // Now sign the master key with our user signing key (unless it's ourself) @@ -2094,7 +2142,7 @@ export class Crypto extends EventEmitter { if (Object.keys(failures || []).length > 0) { if (shouldEmit) { this.baseApis.emit( - "crypto.keySignatureUploadFailure", + CryptoEvent.KeySignatureUploadFailure, failures, "setDeviceVerification", upload, @@ -2178,7 +2226,7 @@ export class Crypto extends EventEmitter { if (Object.keys(failures || []).length > 0) { if (shouldEmit) { this.baseApis.emit( - "crypto.keySignatureUploadFailure", + CryptoEvent.KeySignatureUploadFailure, failures, "setDeviceVerification", upload, // continuation @@ -2193,7 +2241,7 @@ export class Crypto extends EventEmitter { } const deviceObj = DeviceInfo.fromStorage(dev, deviceId); - this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); + this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj); return deviceObj; } @@ -3045,6 +3093,14 @@ export class Crypto extends EventEmitter { }); } + private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => { + try { + this.onRoomMembership(event, member, oldMembership); + } catch (e) { + logger.error("Error handling membership change:", e); + } + }; + private onToDeviceEvent = (event: MatrixEvent): void => { try { logger.log(`received to_device ${event.getType()} from: ` + @@ -3070,7 +3126,7 @@ export class Crypto extends EventEmitter { event.attemptDecryption(this); } // once the event has been decrypted, try again - event.once('Event.decrypted', (ev) => { + event.once(MatrixEventEvent.Decrypted, (ev) => { this.onToDeviceEvent(ev); }); } @@ -3219,15 +3275,15 @@ export class Crypto extends EventEmitter { reject(new Error("Event status set to CANCELLED.")); } }; - event.once("Event.localEventIdReplaced", eventIdListener); - event.on("Event.status", statusListener); + event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener); + event.on(MatrixEventEvent.Status, statusListener); }); } catch (err) { logger.error("error while waiting for the verification event to be sent: " + err.message); return; } finally { - event.removeListener("Event.localEventIdReplaced", eventIdListener); - event.removeListener("Event.status", statusListener); + event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener); + event.removeListener(MatrixEventEvent.Status, statusListener); } } let request = requestsMap.getRequest(event); @@ -3254,7 +3310,7 @@ export class Crypto extends EventEmitter { !request.invalid && // check it has enough events to pass the UNSENT stage !request.observeOnly; if (shouldEmit) { - this.baseApis.emit("crypto.verification.request", request); + this.baseApis.emit(CryptoEvent.VerificationRequest, request); } } @@ -3555,7 +3611,7 @@ export class Crypto extends EventEmitter { return; } - this.emit("crypto.roomKeyRequest", req); + this.emit(CryptoEvent.RoomKeyRequest, req); } /** @@ -3574,7 +3630,7 @@ export class Crypto extends EventEmitter { // we should probably only notify the app of cancellations we told it // about, but we don't currently have a record of that, so we just pass // everything through. - this.emit("crypto.roomKeyRequestCancellation", cancellation); + this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation); } /** diff --git a/src/crypto/verification/Base.ts b/src/crypto/verification/Base.ts index a47c0960716..68e9c96fc0a 100644 --- a/src/crypto/verification/Base.ts +++ b/src/crypto/verification/Base.ts @@ -20,8 +20,6 @@ limitations under the License. * @module crypto/verification/Base */ -import { EventEmitter } from 'events'; - import { MatrixEvent } from '../../models/event'; import { logger } from '../../logger'; import { DeviceInfo } from '../deviceinfo'; @@ -30,6 +28,7 @@ import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossS import { IVerificationChannel } from "./request/Channel"; import { MatrixClient } from "../../client"; import { VerificationRequest } from "./request/VerificationRequest"; +import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter"; const timeoutException = new Error("Verification timed out"); @@ -41,7 +40,18 @@ export class SwitchStartEventError extends Error { export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void; -export class VerificationBase extends EventEmitter { +export enum VerificationEvent { + Cancel = "cancel", +} + +export type VerificationEventHandlerMap = { + [VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void; +}; + +export class VerificationBase< + Events extends string, + Arguments extends ListenerMap, +> extends TypedEventEmitter { private cancelled = false; private _done = false; private promise: Promise = null; @@ -261,7 +271,7 @@ export class VerificationBase extends EventEmitter { } // Also emit a 'cancel' event that the app can listen for to detect cancellation // before calling verify() - this.emit('cancel', e); + this.emit(VerificationEvent.Cancel, e); } } diff --git a/src/crypto/verification/IllegalMethod.ts b/src/crypto/verification/IllegalMethod.ts index b752d7404d3..f01364a212f 100644 --- a/src/crypto/verification/IllegalMethod.ts +++ b/src/crypto/verification/IllegalMethod.ts @@ -20,7 +20,7 @@ limitations under the License. * @module crypto/verification/IllegalMethod */ -import { VerificationBase as Base } from "./Base"; +import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base"; import { IVerificationChannel } from "./request/Channel"; import { MatrixClient } from "../../client"; import { MatrixEvent } from "../../models/event"; @@ -30,7 +30,7 @@ import { VerificationRequest } from "./request/VerificationRequest"; * @class crypto/verification/IllegalMethod/IllegalMethod * @extends {module:crypto/verification/Base} */ -export class IllegalMethod extends Base { +export class IllegalMethod extends Base { public static factory( channel: IVerificationChannel, baseApis: MatrixClient, diff --git a/src/crypto/verification/QRCode.ts b/src/crypto/verification/QRCode.ts index 5b4c45ddaea..3c16c4955c9 100644 --- a/src/crypto/verification/QRCode.ts +++ b/src/crypto/verification/QRCode.ts @@ -19,7 +19,7 @@ limitations under the License. * @module crypto/verification/QRCode */ -import { VerificationBase as Base } from "./Base"; +import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base"; import { newKeyMismatchError, newUserCancelledError } from './Error'; import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib"; import { logger } from '../../logger'; @@ -31,15 +31,25 @@ import { MatrixEvent } from "../../models/event"; export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1"; export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1"; +interface IReciprocateQr { + confirm(): void; + cancel(): void; +} + +export enum QrCodeEvent { + ShowReciprocateQr = "show_reciprocate_qr", +} + +type EventHandlerMap = { + [QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void; +} & VerificationEventHandlerMap; + /** * @class crypto/verification/QRCode/ReciprocateQRCode * @extends {module:crypto/verification/Base} */ -export class ReciprocateQRCode extends Base { - public reciprocateQREvent: { - confirm(): void; - cancel(): void; - }; +export class ReciprocateQRCode extends Base { + public reciprocateQREvent: IReciprocateQr; public static factory( channel: IVerificationChannel, @@ -76,7 +86,7 @@ export class ReciprocateQRCode extends Base { confirm: resolve, cancel: () => reject(newUserCancelledError()), }; - this.emit("show_reciprocate_qr", this.reciprocateQREvent); + this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent); }); // 3. determine key to sign / mark as trusted diff --git a/src/crypto/verification/SAS.ts b/src/crypto/verification/SAS.ts index 5582ff4f462..a3599d5dc68 100644 --- a/src/crypto/verification/SAS.ts +++ b/src/crypto/verification/SAS.ts @@ -22,7 +22,7 @@ limitations under the License. import anotherjson from 'another-json'; import { Utility, SAS as OlmSAS } from "@matrix-org/olm"; -import { VerificationBase as Base, SwitchStartEventError } from "./Base"; +import { VerificationBase as Base, SwitchStartEventError, VerificationEventHandlerMap } from "./Base"; import { errorFactory, newInvalidMessageError, @@ -232,11 +232,19 @@ function intersection(anArray: T[], aSet: Set): T[] { return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : []; } +export enum SasEvent { + ShowSas = "show_sas", +} + +type EventHandlerMap = { + [SasEvent.ShowSas]: (sas: ISasEvent) => void; +} & VerificationEventHandlerMap; + /** * @alias module:crypto/verification/SAS * @extends {module:crypto/verification/Base} */ -export class SAS extends Base { +export class SAS extends Base { private waitingForAccept: boolean; public ourSASPubKey: string; public theirSASPubKey: string; @@ -371,7 +379,7 @@ export class SAS extends Base { cancel: () => reject(newUserCancelledError()), mismatch: () => reject(newMismatchedSASError()), }; - this.emit("show_sas", this.sasEvent); + this.emit(SasEvent.ShowSas, this.sasEvent); }); [e] = await Promise.all([ @@ -447,7 +455,7 @@ export class SAS extends Base { cancel: () => reject(newUserCancelledError()), mismatch: () => reject(newMismatchedSASError()), }; - this.emit("show_sas", this.sasEvent); + this.emit(SasEvent.ShowSas, this.sasEvent); }); [e] = await Promise.all([ diff --git a/src/crypto/verification/request/VerificationRequest.ts b/src/crypto/verification/request/VerificationRequest.ts index b6c0d9ef4bb..71611558f79 100644 --- a/src/crypto/verification/request/VerificationRequest.ts +++ b/src/crypto/verification/request/VerificationRequest.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventEmitter } from 'events'; - import { logger } from '../../../logger'; import { errorFactory, @@ -29,6 +27,7 @@ import { MatrixClient } from "../../../client"; import { MatrixEvent } from "../../../models/event"; import { VerificationBase } from "../Base"; import { VerificationMethod } from "../../index"; +import { TypedEventEmitter } from "../../../models/typed-event-emitter"; // How long after the event's timestamp that the request times out const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes @@ -76,13 +75,23 @@ interface ITransition { event?: MatrixEvent; } +export enum VerificationRequestEvent { + Change = "change", +} + +type EventHandlerMap = { + [VerificationRequestEvent.Change]: () => void; +}; + /** * State machine for verification requests. * Things that differ based on what channel is used to * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`. * @event "change" whenever the state of the request object has changed. */ -export class VerificationRequest extends EventEmitter { +export class VerificationRequest< + C extends IVerificationChannel = IVerificationChannel, +> extends TypedEventEmitter { private eventsByUs = new Map(); private eventsByThem = new Map(); private _observeOnly = false; @@ -104,7 +113,7 @@ export class VerificationRequest; constructor( public readonly channel: C, @@ -236,7 +245,7 @@ export class VerificationRequest { return this._verifier; } @@ -410,7 +419,10 @@ export class VerificationRequest { // need to allow also when unsent in case of to_device if (!this.observeOnly && !this._verifier) { const validStartPhase = @@ -453,7 +465,7 @@ export class VerificationRequest { if (!this.observeOnly && this._phase !== PHASE_CANCELLED) { this._declining = true; - this.emit("change"); + this.emit(VerificationRequestEvent.Change); if (this._verifier) { return this._verifier.cancel(errorFactory(code, reason)()); } else { @@ -471,7 +483,7 @@ export class VerificationRequest { if (!targetDevice) { targetDevice = this.targetDevice; } diff --git a/src/event-mapper.ts b/src/event-mapper.ts index 9b938486021..e6942cbd47b 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { MatrixClient } from "./client"; -import { IEvent, MatrixEvent } from "./models/event"; +import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event"; export type EventMapper = (obj: Partial) => MatrixEvent; @@ -33,7 +33,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event if (event.isEncrypted()) { if (!preventReEmit) { client.reEmitter.reEmit(event, [ - "Event.decrypted", + MatrixEventEvent.Decrypted, ]); } if (decrypt) { @@ -41,7 +41,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event } } if (!preventReEmit) { - client.reEmitter.reEmit(event, ["Event.replaced", "Event.visibilityChange"]); + client.reEmitter.reEmit(event, [ + MatrixEventEvent.Replaced, + MatrixEventEvent.VisibilityChange, + ]); } return event; } diff --git a/src/http-api.ts b/src/http-api.ts index fd016c731e8..2879ea68159 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -21,7 +21,6 @@ limitations under the License. */ import { parse as parseContentType, ParsedMediaType } from "content-type"; -import EventEmitter from "events"; import type { IncomingHttpHeaders, IncomingMessage } from "http"; import type { Request as _Request, CoreOptions } from "request"; @@ -35,6 +34,7 @@ import { IDeferred } from "./utils"; import { Callback } from "./client"; import * as utils from "./utils"; import { logger } from './logger'; +import { TypedEventEmitter } from "./models/typed-event-emitter"; /* TODO: @@ -164,6 +164,16 @@ export enum Method { export type FileType = Document | XMLHttpRequestBodyInit; +export enum HttpApiEvent { + SessionLoggedOut = "Session.logged_out", + NoConsent = "no_consent", +} + +export type HttpApiEventHandlerMap = { + [HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void; + [HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void; +}; + /** * Construct a MatrixHttpApi. * @constructor @@ -192,7 +202,10 @@ export type FileType = Document | XMLHttpRequestBodyInit; export class MatrixHttpApi { private uploads: IUpload[] = []; - constructor(private eventEmitter: EventEmitter, public readonly opts: IHttpOpts) { + constructor( + private eventEmitter: TypedEventEmitter, + public readonly opts: IHttpOpts, + ) { utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]); opts.onlyData = !!opts.onlyData; opts.useAuthorizationHeader = !!opts.useAuthorizationHeader; @@ -603,13 +616,9 @@ export class MatrixHttpApi { requestPromise.catch((err: MatrixError) => { if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) { - this.eventEmitter.emit("Session.logged_out", err); + this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err); } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') { - this.eventEmitter.emit( - "no_consent", - err.message, - err.data.consent_uri, - ); + this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri); } }); diff --git a/src/models/event-status.ts b/src/models/event-status.ts new file mode 100644 index 00000000000..faca97186c9 --- /dev/null +++ b/src/models/event-status.ts @@ -0,0 +1,40 @@ +/* +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. +*/ + +/** + * Enum for event statuses. + * @readonly + * @enum {string} + */ +export enum EventStatus { + /** The event was not sent and will no longer be retried. */ + NOT_SENT = "not_sent", + + /** The message is being encrypted */ + ENCRYPTING = "encrypting", + + /** The event is in the process of being sent. */ + SENDING = "sending", + + /** The event is in a queue waiting to be sent. */ + QUEUED = "queued", + + /** The event has been sent to the server, but we have not yet received the echo. */ + SENT = "sent", + + /** The event was cancelled before it was successfully sent. */ + CANCELLED = "cancelled", +} diff --git a/src/models/event-timeline-set.ts b/src/models/event-timeline-set.ts index 03408c08ba8..1fda0d977a4 100644 --- a/src/models/event-timeline-set.ts +++ b/src/models/event-timeline-set.ts @@ -18,16 +18,15 @@ limitations under the License. * @module models/event-timeline-set */ -import { EventEmitter } from "events"; - import { EventTimeline } from "./event-timeline"; -import { EventStatus, MatrixEvent } from "./event"; +import { EventStatus, MatrixEvent, MatrixEventEvent } from "./event"; import { logger } from '../logger'; import { Relations } from './relations'; -import { Room } from "./room"; +import { Room, RoomEvent } from "./room"; import { Filter } from "../filter"; import { EventType, RelationType } from "../@types/event"; import { RoomState } from "./room-state"; +import { TypedEventEmitter } from "./typed-event-emitter"; // var DEBUG = false; const DEBUG = true; @@ -57,7 +56,15 @@ export interface IRoomTimelineData { liveEvent?: boolean; } -export class EventTimelineSet extends EventEmitter { +type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset; + +export type EventTimelineSetHandlerMap = { + [RoomEvent.Timeline]: + (event: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData) => void; + [RoomEvent.TimelineReset]: (room: Room, eventTimelineSet: EventTimelineSet, resetAllTimelines: boolean) => void; +}; + +export class EventTimelineSet extends TypedEventEmitter { private readonly timelineSupport: boolean; private unstableClientRelationAggregation: boolean; private displayPendingEvents: boolean; @@ -247,7 +254,7 @@ export class EventTimelineSet extends EventEmitter { // Now we can swap the live timeline to the new one. this.liveTimeline = newTimeline; - this.emit("Room.timelineReset", this.room, this, resetAllTimelines); + this.emit(RoomEvent.TimelineReset, this.room, this, resetAllTimelines); } /** @@ -597,8 +604,7 @@ export class EventTimelineSet extends EventEmitter { timeline: timeline, liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache, }; - this.emit("Room.timeline", event, this.room, - Boolean(toStartOfTimeline), false, data); + this.emit(RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data); } /** @@ -652,7 +658,7 @@ export class EventTimelineSet extends EventEmitter { const data = { timeline: timeline, }; - this.emit("Room.timeline", removed, this.room, undefined, true, data); + this.emit(RoomEvent.Timeline, removed, this.room, undefined, true, data); } return removed; } @@ -819,7 +825,7 @@ export class EventTimelineSet extends EventEmitter { // If the event is currently encrypted, wait until it has been decrypted. if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { - event.once("Event.decrypted", () => { + event.once(MatrixEventEvent.Decrypted, () => { this.aggregateRelations(event); }); return; diff --git a/src/models/event.ts b/src/models/event.ts index 9b04ae0996c..47def019b47 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -20,49 +20,22 @@ limitations under the License. * @module models/event */ -import { EventEmitter } from 'events'; import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk"; import { logger } from '../logger'; import { VerificationRequest } from "../crypto/verification/request/VerificationRequest"; -import { - EventType, - MsgType, - RelationType, - EVENT_VISIBILITY_CHANGE_TYPE, -} from "../@types/event"; +import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event"; import { Crypto, IEventDecryptionResult } from "../crypto"; import { deepSortedObjectEntries } from "../utils"; import { RoomMember } from "./room-member"; -import { Thread, ThreadEvent } from "./thread"; +import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap } from "./thread"; import { IActionsObject } from '../pushprocessor'; -import { ReEmitter } from '../ReEmitter'; +import { TypedReEmitter } from '../ReEmitter'; import { MatrixError } from "../http-api"; +import { TypedEventEmitter } from "./typed-event-emitter"; +import { EventStatus } from "./event-status"; -/** - * Enum for event statuses. - * @readonly - * @enum {string} - */ -export enum EventStatus { - /** The event was not sent and will no longer be retried. */ - NOT_SENT = "not_sent", - - /** The message is being encrypted */ - ENCRYPTING = "encrypting", - - /** The event is in the process of being sent. */ - SENDING = "sending", - - /** The event is in a queue waiting to be sent. */ - QUEUED = "queued", - - /** The event has been sent to the server, but we have not yet received the echo. */ - SENT = "sent", - - /** The event was cancelled before it was successfully sent. */ - CANCELLED = "cancelled", -} +export { EventStatus } from "./event-status"; const interns: Record = {}; function intern(str: string): string { @@ -209,7 +182,29 @@ export interface IMessageVisibilityHidden { // A singleton implementing `IMessageVisibilityVisible`. const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true }); -export class MatrixEvent extends EventEmitter { +export enum MatrixEventEvent { + Decrypted = "Event.decrypted", + BeforeRedaction = "Event.beforeRedaction", + VisibilityChange = "Event.visibilityChange", + LocalEventIdReplaced = "Event.localEventIdReplaced", + Status = "Event.status", + Replaced = "Event.replaced", + RelationsCreated = "Event.relationsCreated", +} + +type EmittedEvents = MatrixEventEvent | ThreadEvent.Update; + +export type MatrixEventHandlerMap = { + [MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void; + [MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void; + [MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void; + [MatrixEventEvent.LocalEventIdReplaced]: (event: MatrixEvent) => void; + [MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus) => void; + [MatrixEventEvent.Replaced]: (event: MatrixEvent) => void; + [MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void; +} & ThreadEventHandlerMap; + +export class MatrixEvent extends TypedEventEmitter { private pushActions: IActionsObject = null; private _replacingEvent: MatrixEvent = null; private _localRedactionEvent: MatrixEvent = null; @@ -292,7 +287,7 @@ export class MatrixEvent extends EventEmitter { */ public verificationRequest: VerificationRequest = null; - private readonly reEmitter: ReEmitter; + private readonly reEmitter: TypedReEmitter; /** * Construct a Matrix Event object @@ -343,7 +338,7 @@ export class MatrixEvent extends EventEmitter { this.txnId = event.txn_id || null; this.localTimestamp = Date.now() - (this.getAge() ?? 0); - this.reEmitter = new ReEmitter(this); + this.reEmitter = new TypedReEmitter(this); } /** @@ -871,7 +866,7 @@ export class MatrixEvent extends EventEmitter { this.setPushActions(null); if (options.emit !== false) { - this.emit("Event.decrypted", this, err); + this.emit(MatrixEventEvent.Decrypted, this, err); } return; @@ -1030,7 +1025,7 @@ export class MatrixEvent extends EventEmitter { public markLocallyRedacted(redactionEvent: MatrixEvent): void { if (this._localRedactionEvent) return; - this.emit("Event.beforeRedaction", this, redactionEvent); + this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent); this._localRedactionEvent = redactionEvent; if (!this.event.unsigned) { this.event.unsigned = {}; @@ -1068,7 +1063,7 @@ export class MatrixEvent extends EventEmitter { }); } if (change) { - this.emit("Event.visibilityChange", this, visible); + this.emit(MatrixEventEvent.VisibilityChange, this, visible); } } } @@ -1100,7 +1095,7 @@ export class MatrixEvent extends EventEmitter { this._localRedactionEvent = null; - this.emit("Event.beforeRedaction", this, redactionEvent); + this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent); this._replacingEvent = null; // we attempt to replicate what we would see from the server if @@ -1263,7 +1258,7 @@ export class MatrixEvent extends EventEmitter { this.setStatus(null); if (this.getId() !== oldId) { // emit the event if it changed - this.emit("Event.localEventIdReplaced", this); + this.emit(MatrixEventEvent.LocalEventIdReplaced, this); } this.localTimestamp = Date.now() - this.getAge(); @@ -1286,12 +1281,12 @@ export class MatrixEvent extends EventEmitter { */ public setStatus(status: EventStatus): void { this.status = status; - this.emit("Event.status", this, status); + this.emit(MatrixEventEvent.Status, this, status); } public replaceLocalEventId(eventId: string): void { this.event.event_id = eventId; - this.emit("Event.localEventIdReplaced", this); + this.emit(MatrixEventEvent.LocalEventIdReplaced, this); } /** @@ -1340,7 +1335,7 @@ export class MatrixEvent extends EventEmitter { } if (this._replacingEvent !== newEvent) { this._replacingEvent = newEvent; - this.emit("Event.replaced", this); + this.emit(MatrixEventEvent.Replaced, this); this.invalidateExtensibleEvent(); } } @@ -1559,7 +1554,7 @@ export class MatrixEvent extends EventEmitter { public setThread(thread: Thread): void { this.thread = thread; this.setThreadId(thread.id); - this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]); + this.reEmitter.reEmit(thread, [ThreadEvent.Update]); } /** diff --git a/src/models/group.js b/src/models/group.js index 44fae31661e..29f0fb3846c 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -20,6 +20,7 @@ limitations under the License. * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ +// eslint-disable-next-line no-restricted-imports import { EventEmitter } from "events"; import * as utils from "../utils"; diff --git a/src/models/related-relations.ts b/src/models/related-relations.ts index 55db8e51056..539f94a1cd5 100644 --- a/src/models/related-relations.ts +++ b/src/models/related-relations.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Relations } from "./relations"; +import { Relations, RelationsEvent, EventHandlerMap } from "./relations"; import { MatrixEvent } from "./event"; +import { Listener } from "./typed-event-emitter"; export class RelatedRelations { private relations: Relations[]; @@ -28,11 +29,11 @@ export class RelatedRelations { return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []); } - public on(ev: string, fn: (...params) => void) { + public on(ev: T, fn: Listener) { this.relations.forEach(r => r.on(ev, fn)); } - public off(ev: string, fn: (...params) => void) { + public off(ev: T, fn: Listener) { this.relations.forEach(r => r.off(ev, fn)); } } diff --git a/src/models/relations.ts b/src/models/relations.ts index 29adaab6685..1bd70929700 100644 --- a/src/models/relations.ts +++ b/src/models/relations.ts @@ -14,12 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventEmitter } from 'events'; - -import { EventStatus, MatrixEvent, IAggregatedRelation } from './event'; +import { EventStatus, IAggregatedRelation, MatrixEvent, MatrixEventEvent } from './event'; import { Room } from './room'; import { logger } from '../logger'; import { RelationType } from "../@types/event"; +import { TypedEventEmitter } from "./typed-event-emitter"; + +export enum RelationsEvent { + Add = "Relations.add", + Remove = "Relations.remove", + Redaction = "Relations.redaction", +} + +export type EventHandlerMap = { + [RelationsEvent.Add]: (event: MatrixEvent) => void; + [RelationsEvent.Remove]: (event: MatrixEvent) => void; + [RelationsEvent.Redaction]: (event: MatrixEvent) => void; +}; /** * A container for relation events that supports easy access to common ways of @@ -29,7 +40,7 @@ import { RelationType } from "../@types/event"; * The typical way to get one of these containers is via * EventTimelineSet#getRelationsForEvent. */ -export class Relations extends EventEmitter { +export class Relations extends TypedEventEmitter { private relationEventIds = new Set(); private relations = new Set(); private annotationsByKey: Record> = {}; @@ -84,7 +95,7 @@ export class Relations extends EventEmitter { // If the event is in the process of being sent, listen for cancellation // so we can remove the event from the collection. if (event.isSending()) { - event.on("Event.status", this.onEventStatus); + event.on(MatrixEventEvent.Status, this.onEventStatus); } this.relations.add(event); @@ -97,9 +108,9 @@ export class Relations extends EventEmitter { this.targetEvent.makeReplaced(lastReplacement); } - event.on("Event.beforeRedaction", this.onBeforeRedaction); + event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.emit("Relations.add", event); + this.emit(RelationsEvent.Add, event); this.maybeEmitCreated(); } @@ -138,7 +149,7 @@ export class Relations extends EventEmitter { this.targetEvent.makeReplaced(lastReplacement); } - this.emit("Relations.remove", event); + this.emit(RelationsEvent.Remove, event); } /** @@ -150,14 +161,14 @@ export class Relations extends EventEmitter { private onEventStatus = (event: MatrixEvent, status: EventStatus) => { if (!event.isSending()) { // Sending is done, so we don't need to listen anymore - event.removeListener("Event.status", this.onEventStatus); + event.removeListener(MatrixEventEvent.Status, this.onEventStatus); return; } if (status !== EventStatus.CANCELLED) { return; } // Event was cancelled, remove from the collection - event.removeListener("Event.status", this.onEventStatus); + event.removeListener(MatrixEventEvent.Status, this.onEventStatus); this.removeEvent(event); }; @@ -255,9 +266,9 @@ export class Relations extends EventEmitter { this.targetEvent.makeReplaced(lastReplacement); } - redactedEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction); + redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.emit("Relations.redaction", redactedEvent); + this.emit(RelationsEvent.Redaction, redactedEvent); }; /** @@ -375,6 +386,6 @@ export class Relations extends EventEmitter { return; } this.creationEmitted = true; - this.targetEvent.emit("Event.relationsCreated", this.relationType, this.eventType); + this.targetEvent.emit(MatrixEventEvent.RelationsCreated, this.relationType, this.eventType); } } diff --git a/src/models/room-member.ts b/src/models/room-member.ts index fab65ba8809..a3d350583a0 100644 --- a/src/models/room-member.ts +++ b/src/models/room-member.ts @@ -18,16 +18,30 @@ limitations under the License. * @module models/room-member */ -import { EventEmitter } from "events"; - import { getHttpUriForMxc } from "../content-repo"; import * as utils from "../utils"; import { User } from "./user"; import { MatrixEvent } from "./event"; import { RoomState } from "./room-state"; import { logger } from "../logger"; +import { TypedEventEmitter } from "./typed-event-emitter"; +import { EventType } from "../@types/event"; + +export enum RoomMemberEvent { + Membership = "RoomMember.membership", + Name = "RoomMember.name", + PowerLevel = "RoomMember.powerLevel", + Typing = "RoomMember.typing", +} + +export type RoomMemberEventHandlerMap = { + [RoomMemberEvent.Membership]: (event: MatrixEvent, member: RoomMember, oldMembership: string | null) => void; + [RoomMemberEvent.Name]: (event: MatrixEvent, member: RoomMember, oldName: string | null) => void; + [RoomMemberEvent.PowerLevel]: (event: MatrixEvent, member: RoomMember) => void; + [RoomMemberEvent.Typing]: (event: MatrixEvent, member: RoomMember) => void; +}; -export class RoomMember extends EventEmitter { +export class RoomMember extends TypedEventEmitter { private _isOutOfBand = false; private _modified: number; public _requestedProfileInfo: boolean; // used by sync.ts @@ -107,7 +121,7 @@ export class RoomMember extends EventEmitter { public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void { const displayName = event.getDirectionalContent().displayname; - if (event.getType() !== "m.room.member") { + if (event.getType() !== EventType.RoomMember) { return; } @@ -150,11 +164,11 @@ export class RoomMember extends EventEmitter { if (oldMembership !== this.membership) { this.updateModifiedTime(); - this.emit("RoomMember.membership", event, this, oldMembership); + this.emit(RoomMemberEvent.Membership, event, this, oldMembership); } if (oldName !== this.name) { this.updateModifiedTime(); - this.emit("RoomMember.name", event, this, oldName); + this.emit(RoomMemberEvent.Name, event, this, oldName); } } @@ -196,7 +210,7 @@ export class RoomMember extends EventEmitter { // redraw everyone's level if the max has changed) if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) { this.updateModifiedTime(); - this.emit("RoomMember.powerLevel", powerLevelEvent, this); + this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this); } } @@ -222,7 +236,7 @@ export class RoomMember extends EventEmitter { } if (oldTyping !== this.typing) { this.updateModifiedTime(); - this.emit("RoomMember.typing", event, this); + this.emit(RoomMemberEvent.Typing, event, this); } } diff --git a/src/models/room-state.ts b/src/models/room-state.ts index e1fa9827093..59bf1ae0115 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -18,8 +18,6 @@ limitations under the License. * @module models/room-state */ -import { EventEmitter } from "events"; - import { RoomMember } from "./room-member"; import { logger } from '../logger'; import * as utils from "../utils"; @@ -27,6 +25,7 @@ import { EventType } from "../@types/event"; import { MatrixEvent } from "./event"; import { MatrixClient } from "../client"; import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials"; +import { TypedEventEmitter } from "./typed-event-emitter"; // possible statuses for out-of-band member loading enum OobStatus { @@ -35,7 +34,19 @@ enum OobStatus { Finished, } -export class RoomState extends EventEmitter { +export enum RoomStateEvent { + Events = "RoomState.events", + Members = "RoomState.members", + NewMember = "RoomState.newMember", +} + +export type RoomStateEventHandlerMap = { + [RoomStateEvent.Events]: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void; + [RoomStateEvent.Members]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void; + [RoomStateEvent.NewMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void; +}; + +export class RoomState extends TypedEventEmitter { private sentinels: Record = {}; // userId: RoomMember // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys) private displayNameToUserIds: Record = {}; @@ -307,7 +318,7 @@ export class RoomState extends EventEmitter { this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname); this.updateThirdPartyTokenCache(event); } - this.emit("RoomState.events", event, this, lastStateEvent); + this.emit(RoomStateEvent.Events, event, this, lastStateEvent); }); // update higher level data structures. This needs to be done AFTER the @@ -342,7 +353,7 @@ export class RoomState extends EventEmitter { member.setMembershipEvent(event, this); this.updateMember(member); - this.emit("RoomState.members", event, this, member); + this.emit(RoomStateEvent.Members, event, this, member); } else if (event.getType() === EventType.RoomPowerLevels) { // events with unknown state keys should be ignored // and should not aggregate onto members power levels @@ -357,7 +368,7 @@ export class RoomState extends EventEmitter { const oldLastModified = member.getLastModifiedTime(); member.setPowerLevelEvent(event); if (oldLastModified !== member.getLastModifiedTime()) { - this.emit("RoomState.members", event, this, member); + this.emit(RoomStateEvent.Members, event, this, member); } }); @@ -384,7 +395,7 @@ export class RoomState extends EventEmitter { // add member to members before emitting any events, // as event handlers often lookup the member this.members[userId] = member; - this.emit("RoomState.newMember", event, this, member); + this.emit(RoomStateEvent.NewMember, event, this, member); } return member; } @@ -397,8 +408,7 @@ export class RoomState extends EventEmitter { } private getStateEventMatching(event: MatrixEvent): MatrixEvent | null { - if (!this.events.has(event.getType())) return null; - return this.events.get(event.getType()).get(event.getStateKey()); + return this.events.get(event.getType())?.get(event.getStateKey()) ?? null; } private updateMember(member: RoomMember): void { @@ -503,7 +513,7 @@ export class RoomState extends EventEmitter { this.setStateEvent(stateEvent); this.updateMember(member); - this.emit("RoomState.members", stateEvent, this, member); + this.emit(RoomStateEvent.Members, stateEvent, this, member); } /** diff --git a/src/models/room.ts b/src/models/room.ts index e3cad8cb631..51329cf2168 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -18,18 +18,17 @@ limitations under the License. * @module models/room */ -import { EventEmitter } from "events"; - import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set"; import { Direction, EventTimeline } from "./event-timeline"; import { getHttpUriForMxc } from "../content-repo"; import * as utils from "../utils"; import { normalize } from "../utils"; -import { EventStatus, IEvent, MatrixEvent } from "./event"; +import { IEvent, MatrixEvent } from "./event"; +import { EventStatus } from "./event-status"; import { RoomMember } from "./room-member"; import { IRoomSummary, RoomSummary } from "./room-summary"; import { logger } from '../logger'; -import { ReEmitter } from '../ReEmitter'; +import { TypedReEmitter } from '../ReEmitter'; import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS, EVENT_VISIBILITY_CHANGE_TYPE, @@ -38,8 +37,9 @@ import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersio import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials"; import { Filter } from "../filter"; import { RoomState } from "./room-state"; -import { Thread, ThreadEvent } from "./thread"; +import { Thread, ThreadEvent, EventHandlerMap as ThreadHandlerMap } from "./thread"; import { Method } from "../http-api"; +import { TypedEventEmitter } from "./typed-event-emitter"; // These constants are used as sane defaults when the homeserver doesn't support // the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be @@ -143,8 +143,44 @@ export interface ICreateFilterOpts { prepopulateTimeline?: boolean; } -export class Room extends EventEmitter { - private readonly reEmitter: ReEmitter; +export enum RoomEvent { + MyMembership = "Room.myMembership", + Tags = "Room.tags", + AccountData = "Room.accountData", + Receipt = "Room.receipt", + Name = "Room.name", + Redaction = "Room.redaction", + RedactionCancelled = "Room.redactionCancelled", + LocalEchoUpdated = "Room.localEchoUpdated", + Timeline = "Room.timeline", + TimelineReset = "Room.timelineReset", +} + +type EmittedEvents = RoomEvent + | ThreadEvent.New + | ThreadEvent.Update + | RoomEvent.Timeline + | RoomEvent.TimelineReset; + +export type RoomEventHandlerMap = { + [RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void; + [RoomEvent.Tags]: (event: MatrixEvent, room: Room) => void; + [RoomEvent.AccountData]: (event: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => void; + [RoomEvent.Receipt]: (event: MatrixEvent, room: Room) => void; + [RoomEvent.Name]: (room: Room) => void; + [RoomEvent.Redaction]: (event: MatrixEvent, room: Room) => void; + [RoomEvent.RedactionCancelled]: (event: MatrixEvent, room: Room) => void; + [RoomEvent.LocalEchoUpdated]: ( + event: MatrixEvent, + room: Room, + oldEventId?: string, + oldStatus?: EventStatus, + ) => void; + [ThreadEvent.New]: (thread: Thread) => void; +} & ThreadHandlerMap; + +export class Room extends TypedEventEmitter { + private readonly reEmitter: TypedReEmitter; private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } // receipts should clobber based on receipt_type and user_id pairs hence // the form of this structure. This is sub-optimal for the exposed APIs @@ -287,7 +323,7 @@ export class Room extends EventEmitter { // In some cases, we add listeners for every displayed Matrix event, so it's // common to have quite a few more than the default limit. this.setMaxListeners(100); - this.reEmitter = new ReEmitter(this); + this.reEmitter = new TypedReEmitter(this); opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological; @@ -297,7 +333,8 @@ export class Room extends EventEmitter { // the subsequent ones are the filtered ones in no particular order. this.timelineSets = [new EventTimelineSet(this, opts)]; this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [ - "Room.timeline", "Room.timelineReset", + RoomEvent.Timeline, + RoomEvent.TimelineReset, ]); this.fixUpLegacyTimelineFields(); @@ -712,7 +749,7 @@ export class Room extends EventEmitter { if (membership === "leave") { this.cleanupAfterLeaving(); } - this.emit("Room.myMembership", this, membership, prevMembership); + this.emit(RoomEvent.MyMembership, this, membership, prevMembership); } } @@ -1285,7 +1322,10 @@ export class Room extends EventEmitter { } const opts = Object.assign({ filter: filter }, this.opts); const timelineSet = new EventTimelineSet(this, opts); - this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]); + this.reEmitter.reEmit(timelineSet, [ + RoomEvent.Timeline, + RoomEvent.TimelineReset, + ]); this.filteredTimelineSets[filter.filterId] = timelineSet; this.timelineSets.push(timelineSet); @@ -1418,9 +1458,8 @@ export class Room extends EventEmitter { this.threads.set(thread.id, thread); this.reEmitter.reEmit(thread, [ ThreadEvent.Update, - ThreadEvent.Ready, - "Room.timeline", - "Room.timelineReset", + RoomEvent.Timeline, + RoomEvent.TimelineReset, ]); if (!this.lastThread || this.lastThread.rootEvent.localTimestamp < rootEvent.localTimestamp) { @@ -1462,7 +1501,7 @@ export class Room extends EventEmitter { } } - this.emit("Room.redaction", event, this); + this.emit(RoomEvent.Redaction, event, this); // TODO: we stash user displaynames (among other things) in // RoomMember objects which are then attached to other events @@ -1584,7 +1623,7 @@ export class Room extends EventEmitter { } if (redactedEvent) { redactedEvent.markLocallyRedacted(event); - this.emit("Room.redaction", event, this); + this.emit(RoomEvent.Redaction, event, this); } } } else { @@ -1602,7 +1641,7 @@ export class Room extends EventEmitter { } } - this.emit("Room.localEchoUpdated", event, this, null, null); + this.emit(RoomEvent.LocalEchoUpdated, event, this, null, null); } /** @@ -1730,8 +1769,7 @@ export class Room extends EventEmitter { } } - this.emit("Room.localEchoUpdated", localEvent, this, - oldEventId, oldStatus); + this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus); } /** @@ -1815,7 +1853,7 @@ export class Room extends EventEmitter { } this.savePendingEvents(); - this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus); + this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus); } private revertRedactionLocalEcho(redactionEvent: MatrixEvent): void { @@ -1828,7 +1866,7 @@ export class Room extends EventEmitter { if (redactedEvent) { redactedEvent.unmarkLocallyRedacted(); // re-render after undoing redaction - this.emit("Room.redactionCancelled", redactionEvent, this); + this.emit(RoomEvent.RedactionCancelled, redactionEvent, this); // reapply relation now redaction failed if (redactedEvent.isRelation()) { this.aggregateNonLiveRelation(redactedEvent); @@ -1968,7 +2006,7 @@ export class Room extends EventEmitter { }); if (oldName !== this.name) { - this.emit("Room.name", this); + this.emit(RoomEvent.Name, this); } } @@ -2061,7 +2099,7 @@ export class Room extends EventEmitter { this.addReceiptsToStructure(event, synthetic); // send events after we've regenerated the structure & cache, otherwise things that // listened for the event would read stale data. - this.emit("Room.receipt", event, this); + this.emit(RoomEvent.Receipt, event, this); } /** @@ -2195,7 +2233,7 @@ export class Room extends EventEmitter { // XXX: we could do a deep-comparison to see if the tags have really // changed - but do we want to bother? - this.emit("Room.tags", event, this); + this.emit(RoomEvent.Tags, event, this); } /** @@ -2210,7 +2248,7 @@ export class Room extends EventEmitter { } const lastEvent = this.accountData[event.getType()]; this.accountData[event.getType()] = event; - this.emit("Room.accountData", event, this, lastEvent); + this.emit(RoomEvent.AccountData, event, this, lastEvent); } } diff --git a/src/models/thread.ts b/src/models/thread.ts index 9465cc6a988..4c63bdf4a13 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -14,25 +14,34 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "../matrix"; -import { ReEmitter } from "../ReEmitter"; +import { MatrixClient, RoomEvent } from "../matrix"; +import { TypedReEmitter } from "../ReEmitter"; import { RelationType } from "../@types/event"; import { IRelationsRequestOpts } from "../@types/requests"; -import { MatrixEvent, IThreadBundledRelationship } from "./event"; +import { IThreadBundledRelationship, MatrixEvent } from "./event"; import { Direction, EventTimeline } from "./event-timeline"; -import { EventTimelineSet } from './event-timeline-set'; +import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set'; import { Room } from './room'; import { TypedEventEmitter } from "./typed-event-emitter"; import { RoomState } from "./room-state"; export enum ThreadEvent { New = "Thread.new", - Ready = "Thread.ready", Update = "Thread.update", NewReply = "Thread.newReply", - ViewThread = "Thred.viewThread", + ViewThread = "Thread.viewThread", } +type EmittedEvents = Exclude + | RoomEvent.Timeline + | RoomEvent.TimelineReset; + +export type EventHandlerMap = { + [ThreadEvent.Update]: (thread: Thread) => void; + [ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void; + [ThreadEvent.ViewThread]: () => void; +} & EventTimelineSetHandlerMap; + interface IThreadOpts { initialEvents?: MatrixEvent[]; room: Room; @@ -42,15 +51,15 @@ interface IThreadOpts { /** * @experimental */ -export class Thread extends TypedEventEmitter { +export class Thread extends TypedEventEmitter { /** * A reference to all the events ID at the bottom of the threads */ - public readonly timelineSet; + public readonly timelineSet: EventTimelineSet; private _currentUserParticipated = false; - private reEmitter: ReEmitter; + private reEmitter: TypedReEmitter; private lastEvent: MatrixEvent; private replyCount = 0; @@ -75,11 +84,11 @@ export class Thread extends TypedEventEmitter { timelineSupport: true, pendingEvents: true, }); - this.reEmitter = new ReEmitter(this); + this.reEmitter = new TypedReEmitter(this); this.reEmitter.reEmit(this.timelineSet, [ - "Room.timeline", - "Room.timelineReset", + RoomEvent.Timeline, + RoomEvent.TimelineReset, ]); // If we weren't able to find the root event, it's probably missing @@ -94,8 +103,8 @@ export class Thread extends TypedEventEmitter { opts?.initialEvents?.forEach(event => this.addEvent(event)); - this.room.on("Room.localEchoUpdated", this.onEcho); - this.room.on("Room.timeline", this.onEcho); + this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho); + this.room.on(RoomEvent.Timeline, this.onEcho); } public get hasServerSideSupport(): boolean { @@ -103,7 +112,7 @@ export class Thread extends TypedEventEmitter { ?.capabilities?.[RelationType.Thread]?.enabled; } - onEcho = (event: MatrixEvent) => { + private onEcho = (event: MatrixEvent) => { if (this.timelineSet.eventIdToTimeline(event.getId())) { this.emit(ThreadEvent.Update, this); } @@ -139,10 +148,11 @@ export class Thread extends TypedEventEmitter { * the tail/root references if needed * Will fire "Thread.update" * @param event The event to add + * @param {boolean} toStartOfTimeline whether the event is being added + * to the start (and not the end) of the timeline. */ public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise { - // Add all incoming events to the thread's timeline set when there's - // no server support + // Add all incoming events to the thread's timeline set when there's no server support if (!this.hasServerSideSupport) { // all the relevant membership info to hydrate events with a sender // is held in the main room timeline diff --git a/src/models/typed-event-emitter.ts b/src/models/typed-event-emitter.ts index 5bbe750bace..691ec5ec350 100644 --- a/src/models/typed-event-emitter.ts +++ b/src/models/typed-event-emitter.ts @@ -14,13 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ +// eslint-disable-next-line no-restricted-imports import { EventEmitter } from "events"; -enum EventEmitterEvents { +export enum EventEmitterEvents { NewListener = "newListener", RemoveListener = "removeListener", + Error = "error", } +type AnyListener = (...args: any) => any; +export type ListenerMap = { [eventName in E]: AnyListener }; +type EventEmitterEventListener = (eventName: string, listener: AnyListener) => void; +type EventEmitterErrorListener = (error: Error) => void; + +export type Listener< + E extends string, + A extends ListenerMap, + T extends E | EventEmitterEvents, +> = T extends E ? A[T] + : T extends EventEmitterEvents ? EventEmitterErrorListener + : EventEmitterEventListener; + /** * Typed Event Emitter class which can act as a Base Model for all our model * and communication events. @@ -28,17 +43,26 @@ enum EventEmitterEvents { * to properly type this, so that our events are not stringly-based and prone * to silly typos. */ -export abstract class TypedEventEmitter extends EventEmitter { - public addListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { +export class TypedEventEmitter< + Events extends string, + Arguments extends ListenerMap, + SuperclassArguments extends ListenerMap = Arguments, +> extends EventEmitter { + public addListener( + event: T, + listener: Listener, + ): this { return super.addListener(event, listener); } - public emit(event: Events | EventEmitterEvents, ...args: any[]): boolean { + public emit(event: T, ...args: Parameters): boolean; + public emit(event: T, ...args: Parameters): boolean; + public emit(event: T, ...args: any[]): boolean { return super.emit(event, ...args); } public eventNames(): (Events | EventEmitterEvents)[] { - return super.eventNames() as Events[]; + return super.eventNames() as Array; } public listenerCount(event: Events | EventEmitterEvents): number { @@ -49,23 +73,38 @@ export abstract class TypedEventEmitter extends EventEmit return super.listeners(event); } - public off(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { + public off( + event: T, + listener: Listener, + ): this { return super.off(event, listener); } - public on(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { + public on( + event: T, + listener: Listener, + ): this { return super.on(event, listener); } - public once(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { + public once( + event: T, + listener: Listener, + ): this { return super.once(event, listener); } - public prependListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { + public prependListener( + event: T, + listener: Listener, + ): this { return super.prependListener(event, listener); } - public prependOnceListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { + public prependOnceListener( + event: T, + listener: Listener, + ): this { return super.prependOnceListener(event, listener); } @@ -73,7 +112,10 @@ export abstract class TypedEventEmitter extends EventEmit return super.removeAllListeners(event); } - public removeListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { + public removeListener( + event: T, + listener: Listener, + ): this { return super.removeListener(event, listener); } diff --git a/src/models/user.ts b/src/models/user.ts index 613a03a69ea..2e4b81875e4 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -18,12 +18,29 @@ limitations under the License. * @module models/user */ -import { EventEmitter } from "events"; - import { MatrixEvent } from "./event"; +import { TypedEventEmitter } from "./typed-event-emitter"; -export class User extends EventEmitter { - // eslint-disable-next-line camelcase +export enum UserEvent { + DisplayName = "User.displayName", + AvatarUrl = "User.avatarUrl", + Presence = "User.presence", + CurrentlyActive = "User.currentlyActive", + LastPresenceTs = "User.lastPresenceTs", + /* @deprecated */ + _UnstableStatusMessage = "User.unstable_statusMessage", +} + +export type UserEventHandlerMap = { + [UserEvent.DisplayName]: (event: MatrixEvent | undefined, user: User) => void; + [UserEvent.AvatarUrl]: (event: MatrixEvent | undefined, user: User) => void; + [UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void; + [UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void; + [UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void; + [UserEvent._UnstableStatusMessage]: (user: User) => void; +}; + +export class User extends TypedEventEmitter { private modified: number; // XXX these should be read-only @@ -94,25 +111,25 @@ export class User extends EventEmitter { const firstFire = this.events.presence === null; this.events.presence = event; - const eventsToFire = []; + const eventsToFire: UserEvent[] = []; if (event.getContent().presence !== this.presence || firstFire) { - eventsToFire.push("User.presence"); + eventsToFire.push(UserEvent.Presence); } if (event.getContent().avatar_url && event.getContent().avatar_url !== this.avatarUrl) { - eventsToFire.push("User.avatarUrl"); + eventsToFire.push(UserEvent.AvatarUrl); } if (event.getContent().displayname && event.getContent().displayname !== this.displayName) { - eventsToFire.push("User.displayName"); + eventsToFire.push(UserEvent.DisplayName); } if (event.getContent().currently_active !== undefined && event.getContent().currently_active !== this.currentlyActive) { - eventsToFire.push("User.currentlyActive"); + eventsToFire.push(UserEvent.CurrentlyActive); } this.presence = event.getContent().presence; - eventsToFire.push("User.lastPresenceTs"); + eventsToFire.push(UserEvent.LastPresenceTs); if (event.getContent().status_msg) { this.presenceStatusMsg = event.getContent().status_msg; @@ -213,7 +230,7 @@ export class User extends EventEmitter { if (!event.getContent()) this.unstable_statusMessage = ""; else this.unstable_statusMessage = event.getContent()["status"]; this.updateModifiedTime(); - this.emit("User.unstable_statusMessage", this); + this.emit(UserEvent._UnstableStatusMessage, this); } } diff --git a/src/store/indexeddb.ts b/src/store/indexeddb.ts index 51fa88d5f53..018f5abd197 100644 --- a/src/store/indexeddb.ts +++ b/src/store/indexeddb.ts @@ -16,8 +16,6 @@ limitations under the License. /* eslint-disable @babel/no-invalid-this */ -import { EventEmitter } from 'events'; - import { MemoryStore, IOpts as IBaseOpts } from "./memory"; import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend"; import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend"; @@ -27,6 +25,7 @@ import { logger } from '../logger'; import { ISavedSync } from "./index"; import { IIndexedDBBackend } from "./indexeddb-backend"; import { ISyncResponse } from "../sync-accumulator"; +import { TypedEventEmitter } from "../models/typed-event-emitter"; /** * This is an internal module. See {@link IndexedDBStore} for the public class. @@ -46,6 +45,10 @@ interface IOpts extends IBaseOpts { workerFactory?: () => Worker; } +type EventHandlerMap = { + "degraded": (e: Error) => void; +}; + export class IndexedDBStore extends MemoryStore { static exists(indexedDB: IDBFactory, dbName: string): Promise { return LocalIndexedDBStoreBackend.exists(indexedDB, dbName); @@ -59,7 +62,7 @@ export class IndexedDBStore extends MemoryStore { // the database, such that we can derive the set if users that have been // modified since we last saved. private userModifiedMap: Record = {}; // user_id : timestamp - private emitter = new EventEmitter(); + private emitter = new TypedEventEmitter(); /** * Construct a new Indexed Database store, which extends MemoryStore. diff --git a/src/store/local-storage-events-emitter.ts b/src/store/local-storage-events-emitter.ts index 18f15b59353..24524c63438 100644 --- a/src/store/local-storage-events-emitter.ts +++ b/src/store/local-storage-events-emitter.ts @@ -25,6 +25,15 @@ export enum LocalStorageErrors { QuotaExceededError = 'QuotaExceededError' } +type EventHandlerMap = { + [LocalStorageErrors.Global]: (error: Error) => void; + [LocalStorageErrors.SetItemError]: (error: Error) => void; + [LocalStorageErrors.GetItemError]: (error: Error) => void; + [LocalStorageErrors.RemoveItemError]: (error: Error) => void; + [LocalStorageErrors.ClearError]: (error: Error) => void; + [LocalStorageErrors.QuotaExceededError]: (error: Error) => void; +}; + /** * Used in element-web as a temporary hack to handle all the localStorage errors on the highest level possible * As of 15.11.2021 (DD/MM/YYYY) we're not properly handling local storage exceptions anywhere. @@ -33,5 +42,5 @@ export enum LocalStorageErrors { * maybe you should check out your disk, as it's probably dying and your session may die with it. * See: https://github.com/vector-im/element-web/issues/18423 */ -class LocalStorageErrorsEventsEmitter extends TypedEventEmitter {} +class LocalStorageErrorsEventsEmitter extends TypedEventEmitter {} export const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter(); diff --git a/src/store/memory.ts b/src/store/memory.ts index 7effd9f61d2..b29d3d3647a 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -24,7 +24,7 @@ import { Group } from "../models/group"; import { Room } from "../models/room"; import { User } from "../models/user"; import { IEvent, MatrixEvent } from "../models/event"; -import { RoomState } from "../models/room-state"; +import { RoomState, RoomStateEvent } from "../models/room-state"; import { RoomMember } from "../models/room-member"; import { Filter } from "../filter"; import { ISavedSync, IStore } from "./index"; @@ -126,7 +126,7 @@ export class MemoryStore implements IStore { this.rooms[room.roomId] = room; // add listeners for room member changes so we can keep the room member // map up-to-date. - room.currentState.on("RoomState.members", this.onRoomMember); + room.currentState.on(RoomStateEvent.Members, this.onRoomMember); // add existing members room.currentState.getMembers().forEach((m) => { this.onRoomMember(null, room.currentState, m); @@ -185,7 +185,7 @@ export class MemoryStore implements IStore { */ public removeRoom(roomId: string): void { if (this.rooms[roomId]) { - this.rooms[roomId].removeListener("RoomState.members", this.onRoomMember); + this.rooms[roomId].currentState.removeListener(RoomStateEvent.Members, this.onRoomMember); } delete this.rooms[roomId]; } diff --git a/src/sync.ts b/src/sync.ts index c0da84c44d4..043dfc62c04 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -23,8 +23,8 @@ limitations under the License. * for HTTP and WS at some point. */ -import { User } from "./models/user"; -import { NotificationCountType, Room } from "./models/room"; +import { User, UserEvent } from "./models/user"; +import { NotificationCountType, Room, RoomEvent } from "./models/room"; import { Group } from "./models/group"; import * as utils from "./utils"; import { IDeferred } from "./utils"; @@ -33,7 +33,7 @@ import { EventTimeline } from "./models/event-timeline"; import { PushProcessor } from "./pushprocessor"; import { logger } from './logger'; import { InvalidStoreError } from './errors'; -import { IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client"; +import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client"; import { Category, IEphemeral, @@ -53,6 +53,8 @@ import { MatrixError, Method } from "./http-api"; import { ISavedSync } from "./store"; import { EventType } from "./@types/event"; import { IPushRules } from "./@types/PushRules"; +import { RoomStateEvent } from "./models/room-state"; +import { RoomMemberEvent } from "./models/room-member"; const DEBUG = true; @@ -171,8 +173,10 @@ export class SyncApi { } if (client.getNotifTimelineSet()) { - client.reEmitter.reEmit(client.getNotifTimelineSet(), - ["Room.timeline", "Room.timelineReset"]); + client.reEmitter.reEmit(client.getNotifTimelineSet(), [ + RoomEvent.Timeline, + RoomEvent.TimelineReset, + ]); } } @@ -192,16 +196,17 @@ export class SyncApi { timelineSupport, unstableClientRelationAggregation, }); - client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", - "Room.redaction", - "Room.redactionCancelled", - "Room.receipt", "Room.tags", - "Room.timelineReset", - "Room.localEchoUpdated", - "Room.accountData", - "Room.myMembership", - "Room.replaceEvent", - "Room.visibilityChange", + client.reEmitter.reEmit(room, [ + RoomEvent.Name, + RoomEvent.Redaction, + RoomEvent.RedactionCancelled, + RoomEvent.Receipt, + RoomEvent.Tags, + RoomEvent.LocalEchoUpdated, + RoomEvent.AccountData, + RoomEvent.MyMembership, + RoomEvent.Timeline, + RoomEvent.TimelineReset, ]); this.registerStateListeners(room); return room; @@ -214,7 +219,10 @@ export class SyncApi { public createGroup(groupId: string): Group { const client = this.client; const group = new Group(groupId); - client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]); + client.reEmitter.reEmit(group, [ + ClientEvent.GroupProfile, + ClientEvent.GroupMyMembership, + ]); client.store.storeGroup(group); return group; } @@ -229,17 +237,18 @@ export class SyncApi { // to the client now. We need to add a listener for RoomState.members in // order to hook them correctly. (TODO: find a better way?) client.reEmitter.reEmit(room.currentState, [ - "RoomState.events", "RoomState.members", "RoomState.newMember", + RoomStateEvent.Events, + RoomStateEvent.Members, + RoomStateEvent.NewMember, ]); - room.currentState.on("RoomState.newMember", function(event, state, member) { + room.currentState.on(RoomStateEvent.NewMember, function(event, state, member) { member.user = client.getUser(member.userId); - client.reEmitter.reEmit( - member, - [ - "RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel", - "RoomMember.membership", - ], - ); + client.reEmitter.reEmit(member, [ + RoomMemberEvent.Name, + RoomMemberEvent.Typing, + RoomMemberEvent.PowerLevel, + RoomMemberEvent.Membership, + ]); }); } @@ -249,9 +258,9 @@ export class SyncApi { */ private deregisterStateListeners(room: Room): void { // could do with a better way of achieving this. - room.currentState.removeAllListeners("RoomState.events"); - room.currentState.removeAllListeners("RoomState.members"); - room.currentState.removeAllListeners("RoomState.newMember"); + room.currentState.removeAllListeners(RoomStateEvent.Events); + room.currentState.removeAllListeners(RoomStateEvent.Members); + room.currentState.removeAllListeners(RoomStateEvent.NewMember); } /** @@ -314,7 +323,7 @@ export class SyncApi { room.recalculate(); client.store.storeRoom(room); - client.emit("Room", room); + client.emit(ClientEvent.Room, room); this.processEventsForNotifs(room, events); }); @@ -362,7 +371,7 @@ export class SyncApi { user.setPresenceEvent(presenceEvent); client.store.storeUser(user); } - client.emit("event", presenceEvent); + client.emit(ClientEvent.Event, presenceEvent); }); } @@ -388,7 +397,7 @@ export class SyncApi { response.messages.start); client.store.storeRoom(this._peekRoom); - client.emit("Room", this._peekRoom); + client.emit(ClientEvent.Room, this._peekRoom); this.peekPoll(this._peekRoom); return this._peekRoom; @@ -445,7 +454,7 @@ export class SyncApi { user.setPresenceEvent(presenceEvent); this.client.store.storeUser(user); } - this.client.emit("event", presenceEvent); + this.client.emit(ClientEvent.Event, presenceEvent); }); // strip out events which aren't for the given room_id (e.g presence) @@ -840,7 +849,7 @@ export class SyncApi { logger.error("Caught /sync error", e.stack || e); // Emit the exception for client handling - this.client.emit("sync.unexpectedError", e); + this.client.emit(ClientEvent.SyncUnexpectedError, e); } // update this as it may have changed @@ -1073,7 +1082,7 @@ export class SyncApi { user.setPresenceEvent(presenceEvent); client.store.storeUser(user); } - client.emit("event", presenceEvent); + client.emit(ClientEvent.Event, presenceEvent); }); } @@ -1096,7 +1105,7 @@ export class SyncApi { client.pushRules = PushProcessor.rewriteDefaultRules(rules); } const prevEvent = prevEventsMap[accountDataEvent.getId()]; - client.emit("accountData", accountDataEvent, prevEvent); + client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent); return accountDataEvent; }, ); @@ -1149,7 +1158,7 @@ export class SyncApi { } } - client.emit("toDeviceEvent", toDeviceEvent); + client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent); }, ); } else { @@ -1201,10 +1210,10 @@ export class SyncApi { if (inviteObj.isBrandNewRoom) { room.recalculate(); client.store.storeRoom(room); - client.emit("Room", room); + client.emit(ClientEvent.Room, room); } stateEvents.forEach(function(e) { - client.emit("event", e); + client.emit(ClientEvent.Event, e); }); room.updateMyMembership("invite"); }); @@ -1325,13 +1334,13 @@ export class SyncApi { room.recalculate(); if (joinObj.isBrandNewRoom) { client.store.storeRoom(room); - client.emit("Room", room); + client.emit(ClientEvent.Room, room); } this.processEventsForNotifs(room, events); const processRoomEvent = async (e) => { - client.emit("event", e); + client.emit(ClientEvent.Event, e); if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) { await this.opts.crypto.onCryptoEvent(e); } @@ -1351,10 +1360,10 @@ export class SyncApi { await utils.promiseMapSeries(timelineEvents, processRoomEvent); await utils.promiseMapSeries(threadedEvents, processRoomEvent); ephemeralEvents.forEach(function(e) { - client.emit("event", e); + client.emit(ClientEvent.Event, e); }); accountDataEvents.forEach(function(e) { - client.emit("event", e); + client.emit(ClientEvent.Event, e); }); room.updateMyMembership("join"); @@ -1381,22 +1390,22 @@ export class SyncApi { room.recalculate(); if (leaveObj.isBrandNewRoom) { client.store.storeRoom(room); - client.emit("Room", room); + client.emit(ClientEvent.Room, room); } this.processEventsForNotifs(room, events); stateEvents.forEach(function(e) { - client.emit("event", e); + client.emit(ClientEvent.Event, e); }); timelineEvents.forEach(function(e) { - client.emit("event", e); + client.emit(ClientEvent.Event, e); }); threadedEvents.forEach(function(e) { - client.emit("event", e); + client.emit(ClientEvent.Event, e); }); accountDataEvents.forEach(function(e) { - client.emit("event", e); + client.emit(ClientEvent.Event, e); }); room.updateMyMembership("leave"); @@ -1551,7 +1560,7 @@ export class SyncApi { group.setMyMembership(sectionName); if (isBrandNew) { // Now we've filled in all the fields, emit the Group event - this.client.emit("Group", group); + this.client.emit(ClientEvent.Group, group); } } } @@ -1778,7 +1787,7 @@ export class SyncApi { const old = this.syncState; this.syncState = newState; this.syncStateData = data; - this.client.emit("sync", this.syncState, old, data); + this.client.emit(ClientEvent.Sync, this.syncState, old, data); } /** @@ -1796,8 +1805,11 @@ export class SyncApi { function createNewUser(client: MatrixClient, userId: string): User { const user = new User(userId); client.reEmitter.reEmit(user, [ - "User.avatarUrl", "User.displayName", "User.presence", - "User.currentlyActive", "User.lastPresenceTs", + UserEvent.AvatarUrl, + UserEvent.DisplayName, + UserEvent.Presence, + UserEvent.CurrentlyActive, + UserEvent.LastPresenceTs, ]); return user; } diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index e96928c0dd6..78c49097bb7 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -21,8 +21,6 @@ limitations under the License. * @module webrtc/call */ -import { EventEmitter } from 'events'; - import { logger } from '../logger'; import * as utils from '../utils'; import { MatrixEvent } from '../models/event'; @@ -47,6 +45,7 @@ import { import { CallFeed } from './callFeed'; import { MatrixClient } from "../client"; import { ISendEventResponse } from "../@types/requests"; +import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter"; // events: hangup, error(err), replaced(call), state(state, oldState) @@ -241,6 +240,21 @@ function genCallID(): string { return Date.now().toString() + randomString(16); } +export type CallEventHandlerMap = { + [CallEvent.DataChannel]: (channel: RTCDataChannel) => void; + [CallEvent.FeedsChanged]: (feeds: CallFeed[]) => void; + [CallEvent.Replaced]: (newCall: MatrixCall) => void; + [CallEvent.Error]: (error: CallError) => void; + [CallEvent.RemoteHoldUnhold]: (onHold: boolean) => void; + [CallEvent.LocalHoldUnhold]: (onHold: boolean) => void; + [CallEvent.LengthChanged]: (length: number) => void; + [CallEvent.State]: (state: CallState, oldState?: CallState) => void; + [CallEvent.Hangup]: () => void; + [CallEvent.AssertedIdentityChanged]: () => void; + /* @deprecated */ + [CallEvent.HoldUnhold]: (onHold: boolean) => void; +}; + /** * Construct a new Matrix Call. * @constructor @@ -252,7 +266,7 @@ function genCallID(): string { * @param {Array} opts.turnServers Optional. A list of TURN servers. * @param {MatrixClient} opts.client The Matrix Client instance to send events to. */ -export class MatrixCall extends EventEmitter { +export class MatrixCall extends TypedEventEmitter { public roomId: string; public callId: string; public state = CallState.Fledgling; @@ -1973,7 +1987,7 @@ export class MatrixCall extends EventEmitter { this.peerConn.close(); } if (shouldEmit) { - this.emit(CallEvent.Hangup, this); + this.emit(CallEvent.Hangup); } } @@ -1995,7 +2009,7 @@ export class MatrixCall extends EventEmitter { } private checkForErrorListener(): void { - if (this.listeners("error").length === 0) { + if (this.listeners(EventEmitterEvents.Error).length === 0) { throw new Error( "You MUST attach an error listener using call.on('error', function() {})", ); diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 6599971921e..f190bde6016 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -14,17 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent } from '../models/event'; +import { MatrixEvent, MatrixEventEvent } from '../models/event'; import { logger } from '../logger'; -import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call'; +import { CallDirection, CallErrorCode, CallState, createNewMatrixCall, MatrixCall } from './call'; import { EventType } from '../@types/event'; -import { MatrixClient } from '../client'; +import { ClientEvent, MatrixClient } from '../client'; import { MCallAnswer, MCallHangupReject } from "./callEventTypes"; +import { SyncState } from "../sync"; +import { RoomEvent } from "../models/room"; // Don't ring unless we'd be ringing for at least 3 seconds: the user needs some // time to press the 'accept' button const RING_GRACE_PERIOD = 3000; +export enum CallEventHandlerEvent { + Incoming = "Call.incoming", +} + +export type CallEventHandlerEventHandlerMap = { + [CallEventHandlerEvent.Incoming]: (call: MatrixCall) => void; +}; + export class CallEventHandler { client: MatrixClient; calls: Map; @@ -47,17 +57,17 @@ export class CallEventHandler { } public start() { - this.client.on("sync", this.evaluateEventBuffer); - this.client.on("Room.timeline", this.onRoomTimeline); + this.client.on(ClientEvent.Sync, this.evaluateEventBuffer); + this.client.on(RoomEvent.Timeline, this.onRoomTimeline); } public stop() { - this.client.removeListener("sync", this.evaluateEventBuffer); - this.client.removeListener("Room.timeline", this.onRoomTimeline); + this.client.removeListener(ClientEvent.Sync, this.evaluateEventBuffer); + this.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline); } private evaluateEventBuffer = async () => { - if (this.client.getSyncState() === "SYNCING") { + if (this.client.getSyncState() === SyncState.Syncing) { await Promise.all(this.callEventBuffer.map(event => { this.client.decryptEventIfNeeded(event); })); @@ -101,7 +111,7 @@ export class CallEventHandler { if (event.isBeingDecrypted() || event.isDecryptionFailure()) { // add an event listener for once the event is decrypted. - event.once("Event.decrypted", async () => { + event.once(MatrixEventEvent.Decrypted, async () => { if (!this.eventIsACall(event)) return; if (this.callEventBuffer.includes(event)) { @@ -221,7 +231,7 @@ export class CallEventHandler { call.hangup(CallErrorCode.Replaced, true); } } else { - this.client.emit("Call.incoming", call); + this.client.emit(CallEventHandlerEvent.Incoming, call); } return; } else if (type === EventType.CallCandidates) { diff --git a/src/webrtc/callFeed.ts b/src/webrtc/callFeed.ts index 0c23f3832ce..8f61afaa5d0 100644 --- a/src/webrtc/callFeed.ts +++ b/src/webrtc/callFeed.ts @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import EventEmitter from "events"; - import { SDPStreamMetadataPurpose } from "./callEventTypes"; import { MatrixClient } from "../client"; import { RoomMember } from "../models/room-member"; +import { TypedEventEmitter } from "../models/typed-event-emitter"; const POLLING_INTERVAL = 200; // ms export const SPEAKING_THRESHOLD = -60; // dB @@ -47,7 +46,14 @@ export enum CallFeedEvent { Speaking = "speaking", } -export class CallFeed extends EventEmitter { +type EventHandlerMap = { + [CallFeedEvent.NewStream]: (stream: MediaStream) => void; + [CallFeedEvent.MuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void; + [CallFeedEvent.VolumeChanged]: (volume: number) => void; + [CallFeedEvent.Speaking]: (speaking: boolean) => void; +}; + +export class CallFeed extends TypedEventEmitter { public stream: MediaStream; public userId: string; public purpose: SDPStreamMetadataPurpose;