1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Improve typing around event emitter handlers (#2180)

This commit is contained in:
Michael Telatynski
2022-02-22 12:18:07 +00:00
committed by GitHub
parent 1ac4cc4b11
commit 12e525b664
41 changed files with 906 additions and 416 deletions

View File

@@ -31,6 +31,9 @@ module.exports = {
"no-async-promise-executor": "off", "no-async-promise-executor": "off",
// We use a `logger` intermediary module // We use a `logger` intermediary module
"no-console": "error", "no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", "events"],
}, },
overrides: [{ overrides: [{
files: [ files: [

View File

@@ -1,4 +1,4 @@
import { EventStatus } from "../../src/matrix"; import { EventStatus, RoomEvent } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler"; import { MatrixScheduler } from "../../src/scheduler";
import { Room } from "../../src/models/room"; import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
@@ -95,7 +95,7 @@ describe("MatrixClient retrying", function() {
// wait for the localecho of ev1 to be updated // wait for the localecho of ev1 to be updated
const p3 = new Promise<void>((resolve, reject) => { const p3 = new Promise<void>((resolve, reject) => {
room.on("Room.localEchoUpdated", (ev0) => { room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
if (ev0 === ev1) { if (ev0 === ev1) {
resolve(); resolve();
} }

View File

@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { ReEmitter } from "../../src/ReEmitter"; import { ReEmitter } from "../../src/ReEmitter";

View File

@@ -1,4 +1,5 @@
import '../olm-loader'; import '../olm-loader';
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { Crypto } from "../../src/crypto"; import { Crypto } from "../../src/crypto";

View File

@@ -26,7 +26,7 @@ export async function resetCrossSigningKeys(client, {
crypto.crossSigningInfo.keys = oldKeys; crypto.crossSigningInfo.keys = oldKeys;
throw e; throw e;
} }
crypto.baseApis.emit("crossSigning.keysChanged", {}); crypto.emit("crossSigning.keysChanged", {});
await crypto.afterCrossSigningLocalKeyChange(); await crypto.afterCrossSigningLocalKeyChange();
} }

View File

@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { VerificationBase } from '../../../../src/crypto/verification/Base';
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning'; import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
import { encodeBase64 } from "../../../../src/crypto/olmlib"; import { encodeBase64 } from "../../../../src/crypto/olmlib";
import { setupWebcrypto, teardownWebcrypto } from './util'; import { setupWebcrypto, teardownWebcrypto } from './util';
import { VerificationBase } from '../../../../src/crypto/verification/Base';
jest.useFakeTimers(); jest.useFakeTimers();

View File

@@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { EventTimelineSet } from "../../src/models/event-timeline-set"; 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 { Room } from "../../src/models/room";
import { Relations } from "../../src/models/relations"; import { Relations } from "../../src/models/relations";
@@ -103,7 +103,7 @@ describe("Relations", function() {
// Add the target event first, then the relation event // Add the target event first, then the relation event
{ {
const relationsCreated = new Promise(resolve => { const relationsCreated = new Promise(resolve => {
targetEvent.once("Event.relationsCreated", resolve); targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
}); });
const timelineSet = new EventTimelineSet(room, { const timelineSet = new EventTimelineSet(room, {
@@ -118,7 +118,7 @@ describe("Relations", function() {
// Add the relation event first, then the target event // Add the relation event first, then the target event
{ {
const relationsCreated = new Promise(resolve => { const relationsCreated = new Promise(resolve => {
targetEvent.once("Event.relationsCreated", resolve); targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
}); });
const timelineSet = new EventTimelineSet(room, { const timelineSet = new EventTimelineSet(room, {

View File

@@ -16,16 +16,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
export class ReEmitter { export class ReEmitter {
private target: EventEmitter; constructor(private readonly target: EventEmitter) {}
constructor(target: EventEmitter) { public reEmit(source: EventEmitter, eventNames: string[]): void {
this.target = target;
}
reEmit(source: EventEmitter, eventNames: string[]) {
for (const eventName of eventNames) { for (const eventName of eventNames) {
// We include the source as the last argument for event handlers which may need it, // 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 // 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<Events>,
> extends ReEmitter {
constructor(target: TypedEventEmitter<Events, Arguments>) {
super(target);
}
public reEmit<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
source: TypedEventEmitter<ReEmittedEvents, any>,
eventNames: T[],
): void {
super.reEmit(source, eventNames);
}
}

View File

@@ -19,15 +19,22 @@ limitations under the License.
* @module client * @module client
*/ */
import { EventEmitter } from "events";
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk"; import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk";
import { ISyncStateData, SyncApi, SyncState } from "./sync"; 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 { StubStore } from "./store/stub";
import { createNewMatrixCall, MatrixCall } from "./webrtc/call"; import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall } from "./webrtc/call";
import { Filter, IFilterDefinition } from "./filter"; import { Filter, IFilterDefinition } from "./filter";
import { CallEventHandler } from './webrtc/callEventHandler'; import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
import * as utils from './utils'; import * as utils from './utils';
import { sleep } from './utils'; import { sleep } from './utils';
import { Group } from "./models/group"; import { Group } from "./models/group";
@@ -37,12 +44,12 @@ import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib"; import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice"; import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice";
import { ReEmitter } from './ReEmitter'; import { TypedReEmitter } from './ReEmitter';
import { IRoomEncryption, RoomList } from './crypto/RoomList'; import { IRoomEncryption, RoomList } from './crypto/RoomList';
import { logger } from './logger'; import { logger } from './logger';
import { SERVICE_TYPES } from './service-types'; import { SERVICE_TYPES } from './service-types';
import { import {
FileType, FileType, HttpApiEvent, HttpApiEventHandlerMap,
IHttpOpts, IHttpOpts,
IUpload, IUpload,
MatrixError, MatrixError,
@@ -58,6 +65,8 @@ import {
} from "./http-api"; } from "./http-api";
import { import {
Crypto, Crypto,
CryptoEvent,
CryptoEventHandlerMap,
fixBackupKey, fixBackupKey,
IBootstrapCrossSigningOpts, IBootstrapCrossSigningOpts,
ICheckOwnCrossSigningTrustOpts, ICheckOwnCrossSigningTrustOpts,
@@ -68,7 +77,7 @@ import {
import { DeviceInfo, IDevice } from "./crypto/deviceinfo"; import { DeviceInfo, IDevice } from "./crypto/deviceinfo";
import { decodeRecoveryKey } from './crypto/recoverykey'; import { decodeRecoveryKey } from './crypto/recoverykey';
import { keyFromAuthData } from './crypto/key_passphrase'; import { keyFromAuthData } from './crypto/key_passphrase';
import { User } from "./models/user"; import { User, UserEvent, UserEventHandlerMap } from "./models/user";
import { getHttpUriForMxc } from "./content-repo"; import { getHttpUriForMxc } from "./content-repo";
import { SearchResult } from "./models/search-result"; import { SearchResult } from "./models/search-result";
import { import {
@@ -88,7 +97,20 @@ import {
} from "./crypto/keybackup"; } from "./crypto/keybackup";
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
import { MatrixScheduler } from "./scheduler"; 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 { import {
CrossSigningKey, CrossSigningKey,
IAddSecretStorageKeyOpts, IAddSecretStorageKeyOpts,
@@ -155,6 +177,7 @@ import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base"; import { CryptoStore } from "./crypto/store/base";
import { MediaHandler } from "./webrtc/mediaHandler"; import { MediaHandler } from "./webrtc/mediaHandler";
import { IRefreshTokenResponse } from "./@types/auth"; import { IRefreshTokenResponse } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
export type Store = IStore; export type Store = IStore;
export type SessionStore = WebStorageSessionStore; export type SessionStore = WebStorageSessionStore;
@@ -453,7 +476,7 @@ export interface ISignedKey {
} }
export type KeySignatures = Record<string, Record<string, ICrossSigningKey | ISignedKey>>; export type KeySignatures = Record<string, Record<string, ICrossSigningKey | ISignedKey>>;
interface IUploadKeySignaturesResponse { export interface IUploadKeySignaturesResponse {
failures: Record<string, Record<string, { failures: Record<string, Record<string, {
errcode: string; errcode: string;
error: string; error: string;
@@ -747,15 +770,107 @@ interface ITimestampToEventResponse {
// Probably not the most graceful solution but does a good enough job for now // Probably not the most graceful solution but does a good enough job for now
const EVENT_ID_PREFIX = "$"; const EVENT_ID_PREFIX = "$";
export enum ClientEvent {
Sync = "sync",
Event = "event",
ToDeviceEvent = "toDeviceEvent",
AccountData = "accountData",
Room = "Room",
DeleteRoom = "deleteRoom",
SyncUnexpectedError = "sync.unexpectedError",
ClientWellKnown = "WellKnown.client",
/* @deprecated */
Group = "Group",
// The following enum members are both deprecated and in the wrong place, Groups haven't been TSified
GroupProfile = "Group.profile",
GroupMyMembership = "Group.myMembership",
}
type RoomEvents = RoomEvent.Name
| RoomEvent.Redaction
| RoomEvent.RedactionCancelled
| RoomEvent.Receipt
| RoomEvent.Tags
| RoomEvent.LocalEchoUpdated
| RoomEvent.AccountData
| RoomEvent.MyMembership
| RoomEvent.Timeline
| RoomEvent.TimelineReset;
type RoomStateEvents = RoomStateEvent.Events
| RoomStateEvent.Members
| RoomStateEvent.NewMember;
type CryptoEvents = CryptoEvent.KeySignatureUploadFailure
| CryptoEvent.KeyBackupStatus
| CryptoEvent.KeyBackupFailed
| CryptoEvent.KeyBackupSessionsRemaining
| CryptoEvent.RoomKeyRequest
| CryptoEvent.RoomKeyRequestCancellation
| CryptoEvent.VerificationRequest
| CryptoEvent.DeviceVerificationChanged
| CryptoEvent.UserTrustStatusChanged
| CryptoEvent.KeysChanged
| CryptoEvent.Warning
| CryptoEvent.DevicesUpdated
| CryptoEvent.WillUpdateDevices;
type MatrixEventEvents = MatrixEventEvent.Decrypted | MatrixEventEvent.Replaced | MatrixEventEvent.VisibilityChange;
type RoomMemberEvents = RoomMemberEvent.Name
| RoomMemberEvent.Typing
| RoomMemberEvent.PowerLevel
| RoomMemberEvent.Membership;
type UserEvents = UserEvent.AvatarUrl
| UserEvent.DisplayName
| UserEvent.Presence
| UserEvent.CurrentlyActive
| UserEvent.LastPresenceTs;
type EmittedEvents = ClientEvent
| RoomEvents
| RoomStateEvents
| CryptoEvents
| MatrixEventEvents
| RoomMemberEvents
| UserEvents
| CallEvent // re-emitted by call.ts using Object.values
| CallEventHandlerEvent.Incoming
| HttpApiEvent.SessionLoggedOut
| HttpApiEvent.NoConsent;
export type ClientEventHandlerMap = {
[ClientEvent.Sync]: (state: SyncState, lastState?: SyncState, data?: ISyncStateData) => void;
[ClientEvent.Event]: (event: MatrixEvent) => void;
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
[ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void;
[ClientEvent.Room]: (room: Room) => void;
[ClientEvent.DeleteRoom]: (roomId: string) => void;
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
[ClientEvent.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 * Represents a Matrix Client. Only directly construct this if you want to use
* custom modules. Normally, {@link createClient} should be used * custom modules. Normally, {@link createClient} should be used
* as it specifies 'sensible' defaults for these modules. * as it specifies 'sensible' defaults for these modules.
*/ */
export class MatrixClient extends EventEmitter { export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
public reEmitter = new ReEmitter(this); public reEmitter = new TypedReEmitter<EmittedEvents, ClientEventHandlerMap>(this);
public olmVersion: [number, number, number] = null; // populated after initCrypto public olmVersion: [number, number, number] = null; // populated after initCrypto
public usingExternalCrypto = false; public usingExternalCrypto = false;
public store: Store; public store: Store;
@@ -836,7 +951,7 @@ export class MatrixClient extends EventEmitter {
const userId = opts.userId || null; const userId = opts.userId || null;
this.credentials = { userId }; this.credentials = { userId };
this.http = new MatrixHttpApi(this, { this.http = new MatrixHttpApi(this as ConstructorParameters<typeof MatrixHttpApi>[0], {
baseUrl: opts.baseUrl, baseUrl: opts.baseUrl,
idBaseUrl: opts.idBaseUrl, idBaseUrl: opts.idBaseUrl,
accessToken: opts.accessToken, accessToken: opts.accessToken,
@@ -897,7 +1012,7 @@ export class MatrixClient extends EventEmitter {
// Start listening for calls after the initial sync is done // Start listening for calls after the initial sync is done
// We do not need to backfill the call event buffer // We do not need to backfill the call event buffer
// with encrypted events that might never get decrypted // with encrypted events that might never get decrypted
this.on("sync", this.startCallEventHandler); this.on(ClientEvent.Sync, this.startCallEventHandler);
} }
this.timelineSupport = Boolean(opts.timelineSupport); 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. // 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 // 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. // 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 oldActions = event.getPushActions();
const actions = this.getPushActionsForEvent(event, true); 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 // Like above, we have to listen for read receipts from ourselves in order to
// correctly handle notification counts on encrypted rooms. // correctly handle notification counts on encrypted rooms.
// This fixes https://github.com/vector-im/element-web/issues/9421 // 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)) { if (room && this.isRoomEncrypted(room.roomId)) {
// Figure out if we've read something or if it's just informational // Figure out if we've read something or if it's just informational
const content = event.getContent(); 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 // Note: we don't need to handle 'total' notifications because the counts
// will come from the server. // 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, [ this.reEmitter.reEmit(crypto, [
"crypto.keyBackupFailed", CryptoEvent.KeyBackupFailed,
"crypto.keyBackupSessionsRemaining", CryptoEvent.KeyBackupSessionsRemaining,
"crypto.roomKeyRequest", CryptoEvent.RoomKeyRequest,
"crypto.roomKeyRequestCancellation", CryptoEvent.RoomKeyRequestCancellation,
"crypto.warning", CryptoEvent.Warning,
"crypto.devicesUpdated", CryptoEvent.DevicesUpdated,
"crypto.willUpdateDevices", CryptoEvent.WillUpdateDevices,
"deviceVerificationChanged", CryptoEvent.DeviceVerificationChanged,
"userTrustStatusChanged", CryptoEvent.UserTrustStatusChanged,
"crossSigning.keysChanged", CryptoEvent.KeysChanged,
]); ]);
logger.log("Crypto: initialising crypto object..."); logger.log("Crypto: initialising crypto object...");
@@ -1578,9 +1693,8 @@ export class MatrixClient extends EventEmitter {
this.olmVersion = Crypto.getOlmVersion(); this.olmVersion = Crypto.getOlmVersion();
// if crypto initialisation was successful, tell it to attach its event // if crypto initialisation was successful, tell it to attach its event handlers.
// handlers. crypto.registerEventHandlers(this as Parameters<Crypto["registerEventHandlers"]>[0]);
crypto.registerEventHandlers(this);
this.crypto = crypto; this.crypto = crypto;
} }
@@ -1820,7 +1934,7 @@ export class MatrixClient extends EventEmitter {
* @returns {Verification} a verification object * @returns {Verification} a verification object
* @deprecated Use `requestVerification` instead. * @deprecated Use `requestVerification` instead.
*/ */
public beginKeyVerification(method: string, userId: string, deviceId: string): Verification { public beginKeyVerification(method: string, userId: string, deviceId: string): Verification<any, any> {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
@@ -3660,7 +3774,7 @@ export class MatrixClient extends EventEmitter {
const targetId = localEvent.getAssociatedId(); const targetId = localEvent.getAssociatedId();
if (targetId && targetId.startsWith("~")) { if (targetId && targetId.startsWith("~")) {
const target = room.getPendingEvents().find(e => e.getId() === targetId); const target = room.getPendingEvents().find(e => e.getId() === targetId);
target.once("Event.localEventIdReplaced", () => { target.once(MatrixEventEvent.LocalEventIdReplaced, () => {
localEvent.updateAssociatedId(target.getId()); localEvent.updateAssociatedId(target.getId());
}); });
} }
@@ -4758,7 +4872,7 @@ export class MatrixClient extends EventEmitter {
} }
return promise.then((response) => { return promise.then((response) => {
this.store.removeRoom(roomId); this.store.removeRoom(roomId);
this.emit("deleteRoom", roomId); this.emit(ClientEvent.DeleteRoom, roomId);
return response; return response;
}); });
} }
@@ -4911,7 +5025,7 @@ export class MatrixClient extends EventEmitter {
const user = this.getUser(this.getUserId()); const user = this.getUser(this.getUserId());
if (user) { if (user) {
user.displayName = name; user.displayName = name;
user.emit("User.displayName", user.events.presence, user); user.emit(UserEvent.DisplayName, user.events.presence, user);
} }
return prom; return prom;
} }
@@ -4928,7 +5042,7 @@ export class MatrixClient extends EventEmitter {
const user = this.getUser(this.getUserId()); const user = this.getUser(this.getUserId());
if (user) { if (user) {
user.avatarUrl = url; user.avatarUrl = url;
user.emit("User.avatarUrl", user.events.presence, user); user.emit(UserEvent.AvatarUrl, user.events.presence, user);
} }
return prom; return prom;
} }
@@ -6098,7 +6212,7 @@ export class MatrixClient extends EventEmitter {
private startCallEventHandler = (): void => { private startCallEventHandler = (): void => {
if (this.isInitialSyncComplete()) { if (this.isInitialSyncComplete()) {
this.callEventHandler.start(); 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 `{}`. // it absorbs errors and returns `{}`.
this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain()); this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain());
this.clientWellKnown = await this.clientWellKnownPromise; this.clientWellKnown = await this.clientWellKnownPromise;
this.emit("WellKnown.client", this.clientWellKnown); this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown);
} }
public getClientWellKnown(): IClientWellKnown { public getClientWellKnown(): IClientWellKnown {
@@ -6510,7 +6624,7 @@ export class MatrixClient extends EventEmitter {
const allEvents = originalEvent ? events.concat(originalEvent) : events; const allEvents = originalEvent ? events.concat(originalEvent) : events;
await Promise.all(allEvents.map(e => { await Promise.all(allEvents.map(e => {
if (e.isEncrypted()) { 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); events = events.filter(e => e.getType() === eventType);

View File

@@ -19,7 +19,6 @@ limitations under the License.
* @module crypto/CrossSigning * @module crypto/CrossSigning
*/ */
import { EventEmitter } from 'events';
import { PkSigning } from "@matrix-org/olm"; import { PkSigning } from "@matrix-org/olm";
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib'; import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
@@ -55,7 +54,7 @@ export interface ICrossSigningInfo {
crossSigningVerifiedBefore: boolean; crossSigningVerifiedBefore: boolean;
} }
export class CrossSigningInfo extends EventEmitter { export class CrossSigningInfo {
public keys: Record<string, ICrossSigningKey> = {}; public keys: Record<string, ICrossSigningKey> = {};
public firstUse = true; public firstUse = true;
// This tracks whether we've ever verified this user with any identity. // 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, public readonly userId: string,
private callbacks: ICryptoCallbacks = {}, private callbacks: ICryptoCallbacks = {},
private cacheCallbacks: ICacheCallbacks = {}, private cacheCallbacks: ICacheCallbacks = {},
) { ) {}
super();
}
public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo { public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
const res = new CrossSigningInfo(userId); const res = new CrossSigningInfo(userId);

View File

@@ -20,8 +20,6 @@ limitations under the License.
* Manages the list of other users' devices * Manages the list of other users' devices
*/ */
import { EventEmitter } from 'events';
import { logger } from '../logger'; import { logger } from '../logger';
import { DeviceInfo, IDevice } from './deviceinfo'; import { DeviceInfo, IDevice } from './deviceinfo';
import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning'; import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
@@ -31,6 +29,8 @@ import { chunkPromises, defer, IDeferred, sleep } from '../utils';
import { IDownloadKeyResult, MatrixClient } from "../client"; import { IDownloadKeyResult, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice"; import { OlmDevice } from "./OlmDevice";
import { CryptoStore } from "./store/base"; import { CryptoStore } from "./store/base";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { CryptoEvent, CryptoEventHandlerMap } from "./index";
/* State transition diagram for DeviceList.deviceTrackingStatus /* State transition diagram for DeviceList.deviceTrackingStatus
* *
@@ -62,10 +62,12 @@ export enum TrackingStatus {
export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>; export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
/** /**
* @alias module:crypto/DeviceList * @alias module:crypto/DeviceList
*/ */
export class DeviceList extends EventEmitter { export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHandlerMap> {
private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {}; private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {}; public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {};
@@ -634,7 +636,7 @@ export class DeviceList extends EventEmitter {
}); });
const finished = (success: boolean): void => { const finished = (success: boolean): void => {
this.emit("crypto.willUpdateDevices", users, !this.hasFetched); this.emit(CryptoEvent.WillUpdateDevices, users, !this.hasFetched);
users.forEach((u) => { users.forEach((u) => {
this.dirty = true; this.dirty = true;
@@ -659,7 +661,7 @@ export class DeviceList extends EventEmitter {
} }
}); });
this.saveIfDirty(); this.saveIfDirty();
this.emit("crypto.devicesUpdated", users, !this.hasFetched); this.emit(CryptoEvent.DevicesUpdated, users, !this.hasFetched);
this.hasFetched = true; this.hasFetched = true;
}; };
@@ -867,7 +869,7 @@ class DeviceListUpdateSerialiser {
// NB. Unlike most events in the js-sdk, this one is internal to the // NB. Unlike most events in the js-sdk, this one is internal to the
// js-sdk and is not re-emitted // js-sdk and is not re-emitted
this.deviceList.emit('userCrossSigningUpdated', userId); this.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, userId);
} }
} }
} }

View File

@@ -14,17 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EventEmitter } from "events";
import { logger } from "../logger"; import { logger } from "../logger";
import { MatrixEvent } from "../models/event"; import { MatrixEvent } from "../models/event";
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning"; import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { Method, PREFIX_UNSTABLE } from "../http-api"; import { Method, PREFIX_UNSTABLE } from "../http-api";
import { Crypto, IBootstrapCrossSigningOpts } from "./index"; 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 { ISecretStorageKeyInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup"; import { IKeyBackupInfo } from "./keybackup";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { IAccountDataClient } from "./SecretStorage";
interface ICrossSigningKeys { interface ICrossSigningKeys {
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
@@ -256,7 +264,10 @@ export class EncryptionSetupOperation {
* Catches account data set by SecretStorage during bootstrapping by * Catches account data set by SecretStorage during bootstrapping by
* implementing the methods related to account data in MatrixClient * implementing the methods related to account data in MatrixClient
*/ */
class AccountDataClientAdapter extends EventEmitter { class AccountDataClientAdapter
extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap>
implements IAccountDataClient {
//
public readonly values = new Map<string, MatrixEvent>(); public readonly values = new Map<string, MatrixEvent>();
/** /**
@@ -303,7 +314,7 @@ class AccountDataClientAdapter extends EventEmitter {
// and it seems to rely on this. // and it seems to rely on this.
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
const event = new MatrixEvent({ type, content }); const event = new MatrixEvent({ type, content });
this.emit("accountData", event, lastEvent); this.emit(ClientEvent.AccountData, event, lastEvent);
return {}; return {};
}); });
} }

View File

@@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EventEmitter } from 'stream';
import { logger } from '../logger'; import { logger } from '../logger';
import * as olmlib from './olmlib'; import * as olmlib from './olmlib';
import { encodeBase64 } from './olmlib';
import { randomString } from '../randomstring'; import { randomString } from '../randomstring';
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes'; import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
import { encodeBase64 } from "./olmlib"; import { ClientEvent, ICryptoCallbacks, MatrixEvent } from '../matrix';
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix'; import { ClientEventHandlerMap, MatrixClient } from "../client";
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api'; 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"; 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; cancel: (reason: string) => void;
} }
export interface IAccountDataClient extends EventEmitter { export interface IAccountDataClient extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap> {
// Subset of MatrixClient (which also uses any for the event content) // Subset of MatrixClient (which also uses any for the event content)
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>; getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
getAccountData: (eventType: string) => MatrixEvent; getAccountData: (eventType: string) => MatrixEvent;
@@ -98,17 +98,17 @@ export class SecretStorage {
ev.getType() === 'm.secret_storage.default_key' && ev.getType() === 'm.secret_storage.default_key' &&
ev.getContent().key === keyId ev.getContent().key === keyId
) { ) {
this.accountDataAdapter.removeListener('accountData', listener); this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
resolve(); resolve();
} }
}; };
this.accountDataAdapter.on('accountData', listener); this.accountDataAdapter.on(ClientEvent.AccountData, listener);
this.accountDataAdapter.setAccountData( this.accountDataAdapter.setAccountData(
'm.secret_storage.default_key', 'm.secret_storage.default_key',
{ key: keyId }, { key: keyId },
).catch(e => { ).catch(e => {
this.accountDataAdapter.removeListener('accountData', listener); this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
reject(e); reject(e);
}); });
}); });

View File

@@ -26,14 +26,13 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
import { DeviceInfo } from "./deviceinfo"; import { DeviceInfo } from "./deviceinfo";
import { DeviceTrustLevel } from './CrossSigning'; import { DeviceTrustLevel } from './CrossSigning';
import { keyFromPassphrase } from './key_passphrase'; import { keyFromPassphrase } from './key_passphrase';
import { sleep } from "../utils"; import { getCrypto, sleep } from "../utils";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { encodeRecoveryKey } from './recoverykey'; import { encodeRecoveryKey } from './recoverykey';
import { encryptAES, decryptAES, calculateKeyCheck } from './aes'; import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
import { getCrypto } from '../utils'; import { IAes256AuthData, ICurve25519AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { UnstableValue } from "../NamespacedValue"; import { UnstableValue } from "../NamespacedValue";
import { IMegolmSessionData } from "./index"; import { CryptoEvent, IMegolmSessionData } from "./index";
const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_KEYS_PER_REQUEST = 200;
@@ -155,7 +154,7 @@ export class BackupManager {
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); 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 // There may be keys left over from a partially completed backup, so
// schedule a send to check. // schedule a send to check.
@@ -173,7 +172,7 @@ export class BackupManager {
this.backupInfo = undefined; this.backupInfo = undefined;
this.baseApis.emit('crypto.keyBackupStatus', false); this.baseApis.emit(CryptoEvent.KeyBackupStatus, false);
} }
public getKeyBackupEnabled(): boolean | null { public getKeyBackupEnabled(): boolean | null {
@@ -458,7 +457,7 @@ export class BackupManager {
await this.checkKeyBackup(); await this.checkKeyBackup();
// Backup version has changed or this backup version // Backup version has changed or this backup version
// has been deleted // has been deleted
this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode); this.baseApis.crypto.emit(CryptoEvent.KeyBackupFailed, err.data.errcode);
throw err; throw err;
} }
} }
@@ -487,7 +486,7 @@ export class BackupManager {
} }
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); 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"] = {}; const rooms: IKeyBackup["rooms"] = {};
for (const session of sessions) { for (const session of sessions) {
@@ -524,7 +523,7 @@ export class BackupManager {
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions); await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining); this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
return sessions.length; return sessions.length;
} }
@@ -580,7 +579,7 @@ export class BackupManager {
); );
const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining); this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
return remaining; return remaining;
} }

View File

@@ -22,27 +22,36 @@ limitations under the License.
*/ */
import anotherjson from "another-json"; import anotherjson from "another-json";
import { EventEmitter } from 'events';
import { ReEmitter } from '../ReEmitter'; import { TypedReEmitter } from '../ReEmitter';
import { logger } from '../logger'; import { logger } from '../logger';
import { IExportedDevice, OlmDevice } from "./OlmDevice"; import { IExportedDevice, OlmDevice } from "./OlmDevice";
import * as olmlib from "./olmlib"; import * as olmlib from "./olmlib";
import { DeviceInfoMap, DeviceList } from "./DeviceList"; import { DeviceInfoMap, DeviceList } from "./DeviceList";
import { DeviceInfo, IDevice } from "./deviceinfo"; import { DeviceInfo, IDevice } from "./deviceinfo";
import type { DecryptionAlgorithm, EncryptionAlgorithm } from "./algorithms";
import * as algorithms from "./algorithms"; import * as algorithms from "./algorithms";
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning'; import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
import { EncryptionSetupBuilder } from "./EncryptionSetup"; import { EncryptionSetupBuilder } from "./EncryptionSetup";
import { import {
IAccountDataClient,
ISecretRequest,
SECRET_STORAGE_ALGORITHM_V1_AES, SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorage, SecretStorage,
SecretStorageKeyTuple,
ISecretRequest,
SecretStorageKeyObject, SecretStorageKeyObject,
SecretStorageKeyTuple,
} from './SecretStorage'; } from './SecretStorage';
import { IAddSecretStorageKeyOpts, ICreateSecretStorageOpts, IImportRoomKeysOpts, ISecretStorageKeyInfo } from "./api"; import {
IAddSecretStorageKeyOpts,
ICreateSecretStorageOpts,
IEncryptedEventInfo,
IImportRoomKeysOpts,
IRecoveryKey,
ISecretStorageKeyInfo,
} from "./api";
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager'; import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; 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 { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
import { SAS as SASVerification } from './verification/SAS'; import { SAS as SASVerification } from './verification/SAS';
import { keyFromPassphrase } from './key_passphrase'; import { keyFromPassphrase } from './key_passphrase';
@@ -52,21 +61,28 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel"; import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
import { IllegalMethod } from "./verification/IllegalMethod"; import { IllegalMethod } from "./verification/IllegalMethod";
import { KeySignatureUploadError } from "../errors"; import { KeySignatureUploadError } from "../errors";
import { decryptAES, encryptAES, calculateKeyCheck } from './aes'; import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration'; import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
import { BackupManager } from "./backup"; import { BackupManager } from "./backup";
import { IStore } from "../store"; import { IStore } from "../store";
import { Room } from "../models/room"; import { Room, RoomEvent } from "../models/room";
import { RoomMember } from "../models/room-member"; import { RoomMember, RoomMemberEvent } from "../models/room-member";
import { MatrixEvent, EventStatus, IClearEvent, IEvent } from "../models/event"; import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey, ICrossSigningKey } from "../client"; import {
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base"; ClientEvent,
ICrossSigningKey,
IKeysUploadResponse,
ISignedKey,
IUploadKeySignaturesResponse,
MatrixClient,
SessionStore,
} from "../client";
import type { IRoomEncryption, RoomList } from "./RoomList"; import type { IRoomEncryption, RoomList } from "./RoomList";
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup"; import { IKeyBackupInfo } from "./keybackup";
import { ISyncStateData } from "../sync"; import { ISyncStateData } from "../sync";
import { CryptoStore } from "./store/base"; import { CryptoStore } from "./store/base";
import { IVerificationChannel } from "./verification/request/Channel"; import { IVerificationChannel } from "./verification/request/Channel";
import { TypedEventEmitter } from "../models/typed-event-emitter";
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -186,7 +202,45 @@ export interface IRequestsMap {
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void; 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>
) => void;
[CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => 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<CryptoEvent, CryptoEventHandlerMap> {
/** /**
* @return {string} The version of Olm. * @return {string} The version of Olm.
*/ */
@@ -201,8 +255,8 @@ export class Crypto extends EventEmitter {
public readonly dehydrationManager: DehydrationManager; public readonly dehydrationManager: DehydrationManager;
public readonly secretStorage: SecretStorage; public readonly secretStorage: SecretStorage;
private readonly reEmitter: ReEmitter; private readonly reEmitter: TypedReEmitter<CryptoEvent, CryptoEventHandlerMap>;
private readonly verificationMethods: any; // TODO types private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>;
public readonly supportedAlgorithms: string[]; public readonly supportedAlgorithms: string[];
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager; private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
private readonly toDeviceVerificationRequests: ToDeviceRequests; private readonly toDeviceVerificationRequests: ToDeviceRequests;
@@ -295,10 +349,10 @@ export class Crypto extends EventEmitter {
private readonly clientStore: IStore, private readonly clientStore: IStore,
public readonly cryptoStore: CryptoStore, public readonly cryptoStore: CryptoStore,
private readonly roomList: RoomList, private readonly roomList: RoomList,
verificationMethods: any[], // TODO types verificationMethods: Array<keyof typeof defaultVerificationMethods | typeof VerificationBase>,
) { ) {
super(); super();
this.reEmitter = new ReEmitter(this); this.reEmitter = new TypedReEmitter(this);
if (verificationMethods) { if (verificationMethods) {
this.verificationMethods = new Map(); this.verificationMethods = new Map();
@@ -307,20 +361,21 @@ export class Crypto extends EventEmitter {
if (defaultVerificationMethods[method]) { if (defaultVerificationMethods[method]) {
this.verificationMethods.set( this.verificationMethods.set(
method, method,
defaultVerificationMethods[method], <typeof VerificationBase>defaultVerificationMethods[method],
); );
} }
} else if (method.NAME) { } else if (method["NAME"]) {
this.verificationMethods.set( this.verificationMethods.set(
method.NAME, method["NAME"],
method, method as typeof VerificationBase,
); );
} else { } else {
logger.warn(`Excluding unknown verification method ${method}`); logger.warn(`Excluding unknown verification method ${method}`);
} }
} }
} else { } else {
this.verificationMethods = defaultVerificationMethods; this.verificationMethods =
new Map(Object.entries(defaultVerificationMethods)) as Map<VerificationMethod, typeof VerificationBase>;
} }
this.backupManager = new BackupManager(baseApis, async () => { 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 // 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 class sets seem to be removed at any point... :/
this.deviceList.on('userCrossSigningUpdated', this.onDeviceListUserCrossSigningUpdated); this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
this.reEmitter.reEmit(this.deviceList, ["crypto.devicesUpdated", "crypto.willUpdateDevices"]); this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES); this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES);
@@ -375,7 +430,7 @@ export class Crypto extends EventEmitter {
this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks); this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
// Yes, we pass the client twice here: see SecretStorage // 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); this.dehydrationManager = new DehydrationManager(this);
// Assuming no app-supplied callback, default to getting from SSSS. // Assuming no app-supplied callback, default to getting from SSSS.
@@ -487,7 +542,7 @@ export class Crypto extends EventEmitter {
deviceTrust.isCrossSigningVerified() deviceTrust.isCrossSigningVerified()
) { ) {
const deviceObj = this.deviceList.getStoredDevice(userId, deviceId); 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 (Object.keys(failures || []).length > 0) {
if (shouldEmit) { if (shouldEmit) {
this.baseApis.emit( this.baseApis.emit(
"crypto.keySignatureUploadFailure", CryptoEvent.KeySignatureUploadFailure,
failures, failures,
"afterCrossSigningLocalKeyChange", "afterCrossSigningLocalKeyChange",
upload, // continuation upload, // continuation
@@ -1391,11 +1446,10 @@ export class Crypto extends EventEmitter {
// that reset the keys // that reset the keys
this.storeTrustedSelfKeys(null); this.storeTrustedSelfKeys(null);
// emit cross-signing has been disabled // emit cross-signing has been disabled
this.emit("crossSigning.keysChanged", {}); this.emit(CryptoEvent.KeysChanged, {});
// as the trust for our own user has changed, // as the trust for our own user has changed,
// also emit an event for this // also emit an event for this
this.emit("userTrustStatusChanged", this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
this.userId, this.checkUserTrust(userId));
} }
} else { } else {
await this.checkDeviceVerifications(userId); await this.checkDeviceVerifications(userId);
@@ -1410,7 +1464,7 @@ export class Crypto extends EventEmitter {
this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage()); 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 (Object.keys(failures || []).length > 0) {
if (shouldEmit) { if (shouldEmit) {
this.baseApis.emit( this.baseApis.emit(
"crypto.keySignatureUploadFailure", CryptoEvent.KeySignatureUploadFailure,
failures, failures,
"checkOwnCrossSigningTrust", "checkOwnCrossSigningTrust",
upload, upload,
@@ -1585,10 +1639,10 @@ export class Crypto extends EventEmitter {
upload({ shouldEmit: true }); upload({ shouldEmit: true });
} }
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
if (masterChanged) { if (masterChanged) {
this.baseApis.emit("crossSigning.keysChanged", {}); this.emit(CryptoEvent.KeysChanged, {});
await this.afterCrossSigningLocalKeyChange(); await this.afterCrossSigningLocalKeyChange();
} }
@@ -1675,18 +1729,14 @@ export class Crypto extends EventEmitter {
* @param {external:EventEmitter} eventEmitter event source where we can register * @param {external:EventEmitter} eventEmitter event source where we can register
* for event notifications * for event notifications
*/ */
public registerEventHandlers(eventEmitter: EventEmitter): void { public registerEventHandlers(eventEmitter: TypedEventEmitter<
eventEmitter.on("RoomMember.membership", (event: MatrixEvent, member: RoomMember, oldMembership?: string) => { RoomMemberEvent.Membership | ClientEvent.ToDeviceEvent | RoomEvent.Timeline | MatrixEventEvent.Decrypted,
try { any
this.onRoomMembership(event, member, oldMembership); >): void {
} catch (e) { eventEmitter.on(RoomMemberEvent.Membership, this.onMembership);
logger.error("Error handling membership change:", e); eventEmitter.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
} eventEmitter.on(RoomEvent.Timeline, this.onTimelineEvent);
}); eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent);
eventEmitter.on("toDeviceEvent", this.onToDeviceEvent);
eventEmitter.on("Room.timeline", this.onTimelineEvent);
eventEmitter.on("Event.decrypted", this.onTimelineEvent);
} }
/** Start background processes related to crypto */ /** Start background processes related to crypto */
@@ -2070,9 +2120,7 @@ export class Crypto extends EventEmitter {
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) { if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
this.storeTrustedSelfKeys(xsk.keys); this.storeTrustedSelfKeys(xsk.keys);
// This will cause our own user trust to change, so emit the event // This will cause our own user trust to change, so emit the event
this.emit( this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
"userTrustStatusChanged", this.userId, this.checkUserTrust(userId),
);
} }
// Now sign the master key with our user signing key (unless it's ourself) // 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 (Object.keys(failures || []).length > 0) {
if (shouldEmit) { if (shouldEmit) {
this.baseApis.emit( this.baseApis.emit(
"crypto.keySignatureUploadFailure", CryptoEvent.KeySignatureUploadFailure,
failures, failures,
"setDeviceVerification", "setDeviceVerification",
upload, upload,
@@ -2178,7 +2226,7 @@ export class Crypto extends EventEmitter {
if (Object.keys(failures || []).length > 0) { if (Object.keys(failures || []).length > 0) {
if (shouldEmit) { if (shouldEmit) {
this.baseApis.emit( this.baseApis.emit(
"crypto.keySignatureUploadFailure", CryptoEvent.KeySignatureUploadFailure,
failures, failures,
"setDeviceVerification", "setDeviceVerification",
upload, // continuation upload, // continuation
@@ -2193,7 +2241,7 @@ export class Crypto extends EventEmitter {
} }
const deviceObj = DeviceInfo.fromStorage(dev, deviceId); const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
return 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 => { private onToDeviceEvent = (event: MatrixEvent): void => {
try { try {
logger.log(`received to_device ${event.getType()} from: ` + logger.log(`received to_device ${event.getType()} from: ` +
@@ -3070,7 +3126,7 @@ export class Crypto extends EventEmitter {
event.attemptDecryption(this); event.attemptDecryption(this);
} }
// once the event has been decrypted, try again // once the event has been decrypted, try again
event.once('Event.decrypted', (ev) => { event.once(MatrixEventEvent.Decrypted, (ev) => {
this.onToDeviceEvent(ev); this.onToDeviceEvent(ev);
}); });
} }
@@ -3219,15 +3275,15 @@ export class Crypto extends EventEmitter {
reject(new Error("Event status set to CANCELLED.")); reject(new Error("Event status set to CANCELLED."));
} }
}; };
event.once("Event.localEventIdReplaced", eventIdListener); event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
event.on("Event.status", statusListener); event.on(MatrixEventEvent.Status, statusListener);
}); });
} catch (err) { } catch (err) {
logger.error("error while waiting for the verification event to be sent: " + err.message); logger.error("error while waiting for the verification event to be sent: " + err.message);
return; return;
} finally { } finally {
event.removeListener("Event.localEventIdReplaced", eventIdListener); event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
event.removeListener("Event.status", statusListener); event.removeListener(MatrixEventEvent.Status, statusListener);
} }
} }
let request = requestsMap.getRequest(event); 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.invalid && // check it has enough events to pass the UNSENT stage
!request.observeOnly; !request.observeOnly;
if (shouldEmit) { 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; 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 // 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 // about, but we don't currently have a record of that, so we just pass
// everything through. // everything through.
this.emit("crypto.roomKeyRequestCancellation", cancellation); this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation);
} }
/** /**

View File

@@ -20,8 +20,6 @@ limitations under the License.
* @module crypto/verification/Base * @module crypto/verification/Base
*/ */
import { EventEmitter } from 'events';
import { MatrixEvent } from '../../models/event'; import { MatrixEvent } from '../../models/event';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { DeviceInfo } from '../deviceinfo'; import { DeviceInfo } from '../deviceinfo';
@@ -30,6 +28,7 @@ import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossS
import { IVerificationChannel } from "./request/Channel"; import { IVerificationChannel } from "./request/Channel";
import { MatrixClient } from "../../client"; import { MatrixClient } from "../../client";
import { VerificationRequest } from "./request/VerificationRequest"; import { VerificationRequest } from "./request/VerificationRequest";
import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter";
const timeoutException = new Error("Verification timed out"); 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 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<Events | VerificationEvent>,
> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
private cancelled = false; private cancelled = false;
private _done = false; private _done = false;
private promise: Promise<void> = null; private promise: Promise<void> = null;
@@ -261,7 +271,7 @@ export class VerificationBase extends EventEmitter {
} }
// Also emit a 'cancel' event that the app can listen for to detect cancellation // Also emit a 'cancel' event that the app can listen for to detect cancellation
// before calling verify() // before calling verify()
this.emit('cancel', e); this.emit(VerificationEvent.Cancel, e);
} }
} }

View File

@@ -20,7 +20,7 @@ limitations under the License.
* @module crypto/verification/IllegalMethod * @module crypto/verification/IllegalMethod
*/ */
import { VerificationBase as Base } from "./Base"; import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base";
import { IVerificationChannel } from "./request/Channel"; import { IVerificationChannel } from "./request/Channel";
import { MatrixClient } from "../../client"; import { MatrixClient } from "../../client";
import { MatrixEvent } from "../../models/event"; import { MatrixEvent } from "../../models/event";
@@ -30,7 +30,7 @@ import { VerificationRequest } from "./request/VerificationRequest";
* @class crypto/verification/IllegalMethod/IllegalMethod * @class crypto/verification/IllegalMethod/IllegalMethod
* @extends {module:crypto/verification/Base} * @extends {module:crypto/verification/Base}
*/ */
export class IllegalMethod extends Base { export class IllegalMethod extends Base<VerificationEvent, VerificationEventHandlerMap> {
public static factory( public static factory(
channel: IVerificationChannel, channel: IVerificationChannel,
baseApis: MatrixClient, baseApis: MatrixClient,

View File

@@ -19,7 +19,7 @@ limitations under the License.
* @module crypto/verification/QRCode * @module crypto/verification/QRCode
*/ */
import { VerificationBase as Base } from "./Base"; import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base";
import { newKeyMismatchError, newUserCancelledError } from './Error'; import { newKeyMismatchError, newUserCancelledError } from './Error';
import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib"; import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
import { logger } from '../../logger'; 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 SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.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 * @class crypto/verification/QRCode/ReciprocateQRCode
* @extends {module:crypto/verification/Base} * @extends {module:crypto/verification/Base}
*/ */
export class ReciprocateQRCode extends Base { export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
public reciprocateQREvent: { public reciprocateQREvent: IReciprocateQr;
confirm(): void;
cancel(): void;
};
public static factory( public static factory(
channel: IVerificationChannel, channel: IVerificationChannel,
@@ -76,7 +86,7 @@ export class ReciprocateQRCode extends Base {
confirm: resolve, confirm: resolve,
cancel: () => reject(newUserCancelledError()), cancel: () => reject(newUserCancelledError()),
}; };
this.emit("show_reciprocate_qr", this.reciprocateQREvent); this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
}); });
// 3. determine key to sign / mark as trusted // 3. determine key to sign / mark as trusted

View File

@@ -22,7 +22,7 @@ limitations under the License.
import anotherjson from 'another-json'; import anotherjson from 'another-json';
import { Utility, SAS as OlmSAS } from "@matrix-org/olm"; 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 { import {
errorFactory, errorFactory,
newInvalidMessageError, newInvalidMessageError,
@@ -232,11 +232,19 @@ function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : []; 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 * @alias module:crypto/verification/SAS
* @extends {module:crypto/verification/Base} * @extends {module:crypto/verification/Base}
*/ */
export class SAS extends Base { export class SAS extends Base<SasEvent, EventHandlerMap> {
private waitingForAccept: boolean; private waitingForAccept: boolean;
public ourSASPubKey: string; public ourSASPubKey: string;
public theirSASPubKey: string; public theirSASPubKey: string;
@@ -371,7 +379,7 @@ export class SAS extends Base {
cancel: () => reject(newUserCancelledError()), cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()), mismatch: () => reject(newMismatchedSASError()),
}; };
this.emit("show_sas", this.sasEvent); this.emit(SasEvent.ShowSas, this.sasEvent);
}); });
[e] = await Promise.all([ [e] = await Promise.all([
@@ -447,7 +455,7 @@ export class SAS extends Base {
cancel: () => reject(newUserCancelledError()), cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()), mismatch: () => reject(newMismatchedSASError()),
}; };
this.emit("show_sas", this.sasEvent); this.emit(SasEvent.ShowSas, this.sasEvent);
}); });
[e] = await Promise.all([ [e] = await Promise.all([

View File

@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EventEmitter } from 'events';
import { logger } from '../../../logger'; import { logger } from '../../../logger';
import { import {
errorFactory, errorFactory,
@@ -29,6 +27,7 @@ import { MatrixClient } from "../../../client";
import { MatrixEvent } from "../../../models/event"; import { MatrixEvent } from "../../../models/event";
import { VerificationBase } from "../Base"; import { VerificationBase } from "../Base";
import { VerificationMethod } from "../../index"; import { VerificationMethod } from "../../index";
import { TypedEventEmitter } from "../../../models/typed-event-emitter";
// How long after the event's timestamp that the request times out // How long after the event's timestamp that the request times out
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
@@ -76,13 +75,23 @@ interface ITransition {
event?: MatrixEvent; event?: MatrixEvent;
} }
export enum VerificationRequestEvent {
Change = "change",
}
type EventHandlerMap = {
[VerificationRequestEvent.Change]: () => void;
};
/** /**
* State machine for verification requests. * State machine for verification requests.
* Things that differ based on what channel is used to * Things that differ based on what channel is used to
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`. * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
* @event "change" whenever the state of the request object has changed. * @event "change" whenever the state of the request object has changed.
*/ */
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends EventEmitter { export class VerificationRequest<
C extends IVerificationChannel = IVerificationChannel,
> extends TypedEventEmitter<VerificationRequestEvent, EventHandlerMap> {
private eventsByUs = new Map<string, MatrixEvent>(); private eventsByUs = new Map<string, MatrixEvent>();
private eventsByThem = new Map<string, MatrixEvent>(); private eventsByThem = new Map<string, MatrixEvent>();
private _observeOnly = false; private _observeOnly = false;
@@ -104,7 +113,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
private commonMethods: VerificationMethod[] = []; private commonMethods: VerificationMethod[] = [];
private _phase: Phase; private _phase: Phase;
private _cancellingUserId: string; private _cancellingUserId: string;
private _verifier: VerificationBase; private _verifier: VerificationBase<any, any>;
constructor( constructor(
public readonly channel: C, public readonly channel: C,
@@ -236,7 +245,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
} }
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */ /** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
public get verifier(): VerificationBase { public get verifier(): VerificationBase<any, any> {
return this._verifier; return this._verifier;
} }
@@ -410,7 +419,10 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
* @param {string?} targetDevice.deviceId the id of the device to direct this request to * @param {string?} targetDevice.deviceId the id of the device to direct this request to
* @returns {VerifierBase} the verifier of the given method * @returns {VerifierBase} the verifier of the given method
*/ */
public beginKeyVerification(method: VerificationMethod, targetDevice: ITargetDevice = null): VerificationBase { public beginKeyVerification(
method: VerificationMethod,
targetDevice: ITargetDevice = null,
): VerificationBase<any, any> {
// need to allow also when unsent in case of to_device // need to allow also when unsent in case of to_device
if (!this.observeOnly && !this._verifier) { if (!this.observeOnly && !this._verifier) {
const validStartPhase = const validStartPhase =
@@ -453,7 +465,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> { public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) { if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
this._declining = true; this._declining = true;
this.emit("change"); this.emit(VerificationRequestEvent.Change);
if (this._verifier) { if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason)()); return this._verifier.cancel(errorFactory(code, reason)());
} else { } else {
@@ -471,7 +483,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) { if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
const methods = [...this.verificationMethods.keys()]; const methods = [...this.verificationMethods.keys()];
this._accepting = true; this._accepting = true;
this.emit("change"); this.emit(VerificationRequestEvent.Change);
await this.channel.send(READY_TYPE, { methods }); await this.channel.send(READY_TYPE, { methods });
} }
} }
@@ -495,12 +507,12 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
handled = true; handled = true;
} }
if (handled) { if (handled) {
this.off("change", check); this.off(VerificationRequestEvent.Change, check);
} }
return handled; return handled;
}; };
if (!check()) { if (!check()) {
this.on("change", check); this.on(VerificationRequestEvent.Change, check);
} }
}); });
} }
@@ -508,7 +520,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
private setPhase(phase: Phase, notify = true): void { private setPhase(phase: Phase, notify = true): void {
this._phase = phase; this._phase = phase;
if (notify) { if (notify) {
this.emit("change"); this.emit(VerificationRequestEvent.Change);
} }
} }
@@ -768,7 +780,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
// set phase as last thing as this emits the "change" event // set phase as last thing as this emits the "change" event
this.setPhase(phase); this.setPhase(phase);
} else if (this._observeOnly !== wasObserveOnly) { } else if (this._observeOnly !== wasObserveOnly) {
this.emit("change"); this.emit(VerificationRequestEvent.Change);
} }
} finally { } finally {
// log events we processed so we can see from rageshakes what events were added to a request // log events we processed so we can see from rageshakes what events were added to a request
@@ -880,7 +892,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
method: VerificationMethod, method: VerificationMethod,
startEvent: MatrixEvent = null, startEvent: MatrixEvent = null,
targetDevice: ITargetDevice = null, targetDevice: ITargetDevice = null,
): VerificationBase { ): VerificationBase<any, any> {
if (!targetDevice) { if (!targetDevice) {
targetDevice = this.targetDevice; targetDevice = this.targetDevice;
} }

View File

@@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { MatrixClient } from "./client"; import { MatrixClient } from "./client";
import { IEvent, MatrixEvent } from "./models/event"; import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event";
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent; export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
@@ -33,7 +33,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
if (event.isEncrypted()) { if (event.isEncrypted()) {
if (!preventReEmit) { if (!preventReEmit) {
client.reEmitter.reEmit(event, [ client.reEmitter.reEmit(event, [
"Event.decrypted", MatrixEventEvent.Decrypted,
]); ]);
} }
if (decrypt) { if (decrypt) {
@@ -41,7 +41,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
} }
} }
if (!preventReEmit) { if (!preventReEmit) {
client.reEmitter.reEmit(event, ["Event.replaced", "Event.visibilityChange"]); client.reEmitter.reEmit(event, [
MatrixEventEvent.Replaced,
MatrixEventEvent.VisibilityChange,
]);
} }
return event; return event;
} }

View File

@@ -21,7 +21,6 @@ limitations under the License.
*/ */
import { parse as parseContentType, ParsedMediaType } from "content-type"; import { parse as parseContentType, ParsedMediaType } from "content-type";
import EventEmitter from "events";
import type { IncomingHttpHeaders, IncomingMessage } from "http"; import type { IncomingHttpHeaders, IncomingMessage } from "http";
import type { Request as _Request, CoreOptions } from "request"; import type { Request as _Request, CoreOptions } from "request";
@@ -35,6 +34,7 @@ import { IDeferred } from "./utils";
import { Callback } from "./client"; import { Callback } from "./client";
import * as utils from "./utils"; import * as utils from "./utils";
import { logger } from './logger'; import { logger } from './logger';
import { TypedEventEmitter } from "./models/typed-event-emitter";
/* /*
TODO: TODO:
@@ -164,6 +164,16 @@ export enum Method {
export type FileType = Document | XMLHttpRequestBodyInit; 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. * Construct a MatrixHttpApi.
* @constructor * @constructor
@@ -192,7 +202,10 @@ export type FileType = Document | XMLHttpRequestBodyInit;
export class MatrixHttpApi { export class MatrixHttpApi {
private uploads: IUpload[] = []; private uploads: IUpload[] = [];
constructor(private eventEmitter: EventEmitter, public readonly opts: IHttpOpts) { constructor(
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
public readonly opts: IHttpOpts,
) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]); utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = !!opts.onlyData; opts.onlyData = !!opts.onlyData;
opts.useAuthorizationHeader = !!opts.useAuthorizationHeader; opts.useAuthorizationHeader = !!opts.useAuthorizationHeader;
@@ -603,13 +616,9 @@ export class MatrixHttpApi {
requestPromise.catch((err: MatrixError) => { requestPromise.catch((err: MatrixError) => {
if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) { 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') { } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
this.eventEmitter.emit( this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
"no_consent",
err.message,
err.data.consent_uri,
);
} }
}); });

View File

@@ -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",
}

View File

@@ -18,16 +18,15 @@ limitations under the License.
* @module models/event-timeline-set * @module models/event-timeline-set
*/ */
import { EventEmitter } from "events";
import { EventTimeline } from "./event-timeline"; import { EventTimeline } from "./event-timeline";
import { EventStatus, MatrixEvent } from "./event"; import { EventStatus, MatrixEvent, MatrixEventEvent } from "./event";
import { logger } from '../logger'; import { logger } from '../logger';
import { Relations } from './relations'; import { Relations } from './relations';
import { Room } from "./room"; import { Room, RoomEvent } from "./room";
import { Filter } from "../filter"; import { Filter } from "../filter";
import { EventType, RelationType } from "../@types/event"; import { EventType, RelationType } from "../@types/event";
import { RoomState } from "./room-state"; import { RoomState } from "./room-state";
import { TypedEventEmitter } from "./typed-event-emitter";
// var DEBUG = false; // var DEBUG = false;
const DEBUG = true; const DEBUG = true;
@@ -57,7 +56,15 @@ export interface IRoomTimelineData {
liveEvent?: boolean; 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<EmittedEvents, EventTimelineSetHandlerMap> {
private readonly timelineSupport: boolean; private readonly timelineSupport: boolean;
private unstableClientRelationAggregation: boolean; private unstableClientRelationAggregation: boolean;
private displayPendingEvents: boolean; private displayPendingEvents: boolean;
@@ -247,7 +254,7 @@ export class EventTimelineSet extends EventEmitter {
// Now we can swap the live timeline to the new one. // Now we can swap the live timeline to the new one.
this.liveTimeline = newTimeline; 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, timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache, liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
}; };
this.emit("Room.timeline", event, this.room, this.emit(RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data);
Boolean(toStartOfTimeline), false, data);
} }
/** /**
@@ -652,7 +658,7 @@ export class EventTimelineSet extends EventEmitter {
const data = { const data = {
timeline: timeline, timeline: timeline,
}; };
this.emit("Room.timeline", removed, this.room, undefined, true, data); this.emit(RoomEvent.Timeline, removed, this.room, undefined, true, data);
} }
return removed; return removed;
} }
@@ -819,7 +825,7 @@ export class EventTimelineSet extends EventEmitter {
// If the event is currently encrypted, wait until it has been decrypted. // If the event is currently encrypted, wait until it has been decrypted.
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
event.once("Event.decrypted", () => { event.once(MatrixEventEvent.Decrypted, () => {
this.aggregateRelations(event); this.aggregateRelations(event);
}); });
return; return;

View File

@@ -20,49 +20,22 @@ limitations under the License.
* @module models/event * @module models/event
*/ */
import { EventEmitter } from 'events';
import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk"; import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
import { logger } from '../logger'; import { logger } from '../logger';
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
import { import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
EventType,
MsgType,
RelationType,
EVENT_VISIBILITY_CHANGE_TYPE,
} from "../@types/event";
import { Crypto, IEventDecryptionResult } from "../crypto"; import { Crypto, IEventDecryptionResult } from "../crypto";
import { deepSortedObjectEntries } from "../utils"; import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member"; import { RoomMember } from "./room-member";
import { Thread, ThreadEvent } from "./thread"; import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap } from "./thread";
import { IActionsObject } from '../pushprocessor'; import { IActionsObject } from '../pushprocessor';
import { ReEmitter } from '../ReEmitter'; import { TypedReEmitter } from '../ReEmitter';
import { MatrixError } from "../http-api"; import { MatrixError } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter";
import { EventStatus } from "./event-status";
/** export { 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",
}
const interns: Record<string, string> = {}; const interns: Record<string, string> = {};
function intern(str: string): string { function intern(str: string): string {
@@ -209,7 +182,29 @@ export interface IMessageVisibilityHidden {
// A singleton implementing `IMessageVisibilityVisible`. // A singleton implementing `IMessageVisibilityVisible`.
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true }); 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<EmittedEvents, MatrixEventHandlerMap> {
private pushActions: IActionsObject = null; private pushActions: IActionsObject = null;
private _replacingEvent: MatrixEvent = null; private _replacingEvent: MatrixEvent = null;
private _localRedactionEvent: MatrixEvent = null; private _localRedactionEvent: MatrixEvent = null;
@@ -292,7 +287,7 @@ export class MatrixEvent extends EventEmitter {
*/ */
public verificationRequest: VerificationRequest = null; public verificationRequest: VerificationRequest = null;
private readonly reEmitter: ReEmitter; private readonly reEmitter: TypedReEmitter<EmittedEvents, MatrixEventHandlerMap>;
/** /**
* Construct a Matrix Event object * Construct a Matrix Event object
@@ -343,7 +338,7 @@ export class MatrixEvent extends EventEmitter {
this.txnId = event.txn_id || null; this.txnId = event.txn_id || null;
this.localTimestamp = Date.now() - (this.getAge() ?? 0); 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); this.setPushActions(null);
if (options.emit !== false) { if (options.emit !== false) {
this.emit("Event.decrypted", this, err); this.emit(MatrixEventEvent.Decrypted, this, err);
} }
return; return;
@@ -1030,7 +1025,7 @@ export class MatrixEvent extends EventEmitter {
public markLocallyRedacted(redactionEvent: MatrixEvent): void { public markLocallyRedacted(redactionEvent: MatrixEvent): void {
if (this._localRedactionEvent) return; if (this._localRedactionEvent) return;
this.emit("Event.beforeRedaction", this, redactionEvent); this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
this._localRedactionEvent = redactionEvent; this._localRedactionEvent = redactionEvent;
if (!this.event.unsigned) { if (!this.event.unsigned) {
this.event.unsigned = {}; this.event.unsigned = {};
@@ -1068,7 +1063,7 @@ export class MatrixEvent extends EventEmitter {
}); });
} }
if (change) { 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._localRedactionEvent = null;
this.emit("Event.beforeRedaction", this, redactionEvent); this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
this._replacingEvent = null; this._replacingEvent = null;
// we attempt to replicate what we would see from the server if // we attempt to replicate what we would see from the server if
@@ -1263,7 +1258,7 @@ export class MatrixEvent extends EventEmitter {
this.setStatus(null); this.setStatus(null);
if (this.getId() !== oldId) { if (this.getId() !== oldId) {
// emit the event if it changed // emit the event if it changed
this.emit("Event.localEventIdReplaced", this); this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
} }
this.localTimestamp = Date.now() - this.getAge(); this.localTimestamp = Date.now() - this.getAge();
@@ -1286,12 +1281,12 @@ export class MatrixEvent extends EventEmitter {
*/ */
public setStatus(status: EventStatus): void { public setStatus(status: EventStatus): void {
this.status = status; this.status = status;
this.emit("Event.status", this, status); this.emit(MatrixEventEvent.Status, this, status);
} }
public replaceLocalEventId(eventId: string): void { public replaceLocalEventId(eventId: string): void {
this.event.event_id = eventId; 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) { if (this._replacingEvent !== newEvent) {
this._replacingEvent = newEvent; this._replacingEvent = newEvent;
this.emit("Event.replaced", this); this.emit(MatrixEventEvent.Replaced, this);
this.invalidateExtensibleEvent(); this.invalidateExtensibleEvent();
} }
} }
@@ -1559,7 +1554,7 @@ export class MatrixEvent extends EventEmitter {
public setThread(thread: Thread): void { public setThread(thread: Thread): void {
this.thread = thread; this.thread = thread;
this.setThreadId(thread.id); this.setThreadId(thread.id);
this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]); this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
} }
/** /**

View File

@@ -20,6 +20,7 @@ limitations under the License.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued. * @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 { EventEmitter } from "events";
import * as utils from "../utils"; import * as utils from "../utils";

View File

@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Relations } from "./relations"; import { Relations, RelationsEvent, EventHandlerMap } from "./relations";
import { MatrixEvent } from "./event"; import { MatrixEvent } from "./event";
import { Listener } from "./typed-event-emitter";
export class RelatedRelations { export class RelatedRelations {
private relations: Relations[]; private relations: Relations[];
@@ -28,11 +29,11 @@ export class RelatedRelations {
return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []); return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []);
} }
public on(ev: string, fn: (...params) => void) { public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
this.relations.forEach(r => r.on(ev, fn)); this.relations.forEach(r => r.on(ev, fn));
} }
public off(ev: string, fn: (...params) => void) { public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
this.relations.forEach(r => r.off(ev, fn)); this.relations.forEach(r => r.off(ev, fn));
} }
} }

View File

@@ -14,12 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EventEmitter } from 'events'; import { EventStatus, IAggregatedRelation, MatrixEvent, MatrixEventEvent } from './event';
import { EventStatus, MatrixEvent, IAggregatedRelation } from './event';
import { Room } from './room'; import { Room } from './room';
import { logger } from '../logger'; import { logger } from '../logger';
import { RelationType } from "../@types/event"; 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 * 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 * The typical way to get one of these containers is via
* EventTimelineSet#getRelationsForEvent. * EventTimelineSet#getRelationsForEvent.
*/ */
export class Relations extends EventEmitter { export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap> {
private relationEventIds = new Set<string>(); private relationEventIds = new Set<string>();
private relations = new Set<MatrixEvent>(); private relations = new Set<MatrixEvent>();
private annotationsByKey: Record<string, Set<MatrixEvent>> = {}; private annotationsByKey: Record<string, Set<MatrixEvent>> = {};
@@ -84,7 +95,7 @@ export class Relations extends EventEmitter {
// If the event is in the process of being sent, listen for cancellation // If the event is in the process of being sent, listen for cancellation
// so we can remove the event from the collection. // so we can remove the event from the collection.
if (event.isSending()) { if (event.isSending()) {
event.on("Event.status", this.onEventStatus); event.on(MatrixEventEvent.Status, this.onEventStatus);
} }
this.relations.add(event); this.relations.add(event);
@@ -97,9 +108,9 @@ export class Relations extends EventEmitter {
this.targetEvent.makeReplaced(lastReplacement); 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(); this.maybeEmitCreated();
} }
@@ -138,7 +149,7 @@ export class Relations extends EventEmitter {
this.targetEvent.makeReplaced(lastReplacement); 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) => { private onEventStatus = (event: MatrixEvent, status: EventStatus) => {
if (!event.isSending()) { if (!event.isSending()) {
// Sending is done, so we don't need to listen anymore // Sending is done, so we don't need to listen anymore
event.removeListener("Event.status", this.onEventStatus); event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
return; return;
} }
if (status !== EventStatus.CANCELLED) { if (status !== EventStatus.CANCELLED) {
return; return;
} }
// Event was cancelled, remove from the collection // Event was cancelled, remove from the collection
event.removeListener("Event.status", this.onEventStatus); event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
this.removeEvent(event); this.removeEvent(event);
}; };
@@ -255,9 +266,9 @@ export class Relations extends EventEmitter {
this.targetEvent.makeReplaced(lastReplacement); 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; return;
} }
this.creationEmitted = true; this.creationEmitted = true;
this.targetEvent.emit("Event.relationsCreated", this.relationType, this.eventType); this.targetEvent.emit(MatrixEventEvent.RelationsCreated, this.relationType, this.eventType);
} }
} }

View File

@@ -18,16 +18,30 @@ limitations under the License.
* @module models/room-member * @module models/room-member
*/ */
import { EventEmitter } from "events";
import { getHttpUriForMxc } from "../content-repo"; import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils"; import * as utils from "../utils";
import { User } from "./user"; import { User } from "./user";
import { MatrixEvent } from "./event"; import { MatrixEvent } from "./event";
import { RoomState } from "./room-state"; import { RoomState } from "./room-state";
import { logger } from "../logger"; import { logger } from "../logger";
import { TypedEventEmitter } from "./typed-event-emitter";
import { EventType } from "../@types/event";
export class RoomMember extends EventEmitter { 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 TypedEventEmitter<RoomMemberEvent, RoomMemberEventHandlerMap> {
private _isOutOfBand = false; private _isOutOfBand = false;
private _modified: number; private _modified: number;
public _requestedProfileInfo: boolean; // used by sync.ts public _requestedProfileInfo: boolean; // used by sync.ts
@@ -107,7 +121,7 @@ export class RoomMember extends EventEmitter {
public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void { public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void {
const displayName = event.getDirectionalContent().displayname; const displayName = event.getDirectionalContent().displayname;
if (event.getType() !== "m.room.member") { if (event.getType() !== EventType.RoomMember) {
return; return;
} }
@@ -150,11 +164,11 @@ export class RoomMember extends EventEmitter {
if (oldMembership !== this.membership) { if (oldMembership !== this.membership) {
this.updateModifiedTime(); this.updateModifiedTime();
this.emit("RoomMember.membership", event, this, oldMembership); this.emit(RoomMemberEvent.Membership, event, this, oldMembership);
} }
if (oldName !== this.name) { if (oldName !== this.name) {
this.updateModifiedTime(); 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) // redraw everyone's level if the max has changed)
if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) { if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
this.updateModifiedTime(); 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) { if (oldTyping !== this.typing) {
this.updateModifiedTime(); this.updateModifiedTime();
this.emit("RoomMember.typing", event, this); this.emit(RoomMemberEvent.Typing, event, this);
} }
} }

View File

@@ -18,8 +18,6 @@ limitations under the License.
* @module models/room-state * @module models/room-state
*/ */
import { EventEmitter } from "events";
import { RoomMember } from "./room-member"; import { RoomMember } from "./room-member";
import { logger } from '../logger'; import { logger } from '../logger';
import * as utils from "../utils"; import * as utils from "../utils";
@@ -27,6 +25,7 @@ import { EventType } from "../@types/event";
import { MatrixEvent } from "./event"; import { MatrixEvent } from "./event";
import { MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials"; import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
import { TypedEventEmitter } from "./typed-event-emitter";
// possible statuses for out-of-band member loading // possible statuses for out-of-band member loading
enum OobStatus { enum OobStatus {
@@ -35,7 +34,19 @@ enum OobStatus {
Finished, 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<RoomStateEvent, RoomStateEventHandlerMap> {
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys) // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
private displayNameToUserIds: Record<string, string[]> = {}; private displayNameToUserIds: Record<string, string[]> = {};
@@ -307,7 +318,7 @@ export class RoomState extends EventEmitter {
this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname); this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname);
this.updateThirdPartyTokenCache(event); 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 // 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); member.setMembershipEvent(event, this);
this.updateMember(member); this.updateMember(member);
this.emit("RoomState.members", event, this, member); this.emit(RoomStateEvent.Members, event, this, member);
} else if (event.getType() === EventType.RoomPowerLevels) { } else if (event.getType() === EventType.RoomPowerLevels) {
// events with unknown state keys should be ignored // events with unknown state keys should be ignored
// and should not aggregate onto members power levels // and should not aggregate onto members power levels
@@ -357,7 +368,7 @@ export class RoomState extends EventEmitter {
const oldLastModified = member.getLastModifiedTime(); const oldLastModified = member.getLastModifiedTime();
member.setPowerLevelEvent(event); member.setPowerLevelEvent(event);
if (oldLastModified !== member.getLastModifiedTime()) { 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, // add member to members before emitting any events,
// as event handlers often lookup the member // as event handlers often lookup the member
this.members[userId] = member; this.members[userId] = member;
this.emit("RoomState.newMember", event, this, member); this.emit(RoomStateEvent.NewMember, event, this, member);
} }
return member; return member;
} }
@@ -397,8 +408,7 @@ export class RoomState extends EventEmitter {
} }
private getStateEventMatching(event: MatrixEvent): MatrixEvent | null { private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
if (!this.events.has(event.getType())) return null; return this.events.get(event.getType())?.get(event.getStateKey()) ?? null;
return this.events.get(event.getType()).get(event.getStateKey());
} }
private updateMember(member: RoomMember): void { private updateMember(member: RoomMember): void {
@@ -503,7 +513,7 @@ export class RoomState extends EventEmitter {
this.setStateEvent(stateEvent); this.setStateEvent(stateEvent);
this.updateMember(member); this.updateMember(member);
this.emit("RoomState.members", stateEvent, this, member); this.emit(RoomStateEvent.Members, stateEvent, this, member);
} }
/** /**

View File

@@ -18,18 +18,17 @@ limitations under the License.
* @module models/room * @module models/room
*/ */
import { EventEmitter } from "events";
import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set"; import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
import { Direction, EventTimeline } from "./event-timeline"; import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo"; import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils"; import * as utils from "../utils";
import { normalize } 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 { RoomMember } from "./room-member";
import { IRoomSummary, RoomSummary } from "./room-summary"; import { IRoomSummary, RoomSummary } from "./room-summary";
import { logger } from '../logger'; import { logger } from '../logger';
import { ReEmitter } from '../ReEmitter'; import { TypedReEmitter } from '../ReEmitter';
import { import {
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS, EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
EVENT_VISIBILITY_CHANGE_TYPE, EVENT_VISIBILITY_CHANGE_TYPE,
@@ -38,8 +37,9 @@ import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersio
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials"; import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
import { Filter } from "../filter"; import { Filter } from "../filter";
import { RoomState } from "./room-state"; import { RoomState } from "./room-state";
import { Thread, ThreadEvent } from "./thread"; import { Thread, ThreadEvent, EventHandlerMap as ThreadHandlerMap } from "./thread";
import { Method } from "../http-api"; import { Method } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter";
// These constants are used as sane defaults when the homeserver doesn't support // 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 // the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -143,8 +143,44 @@ export interface ICreateFilterOpts {
prepopulateTimeline?: boolean; prepopulateTimeline?: boolean;
} }
export class Room extends EventEmitter { export enum RoomEvent {
private readonly reEmitter: ReEmitter; 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<EmittedEvents, RoomEventHandlerMap> {
private readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent } private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
// receipts should clobber based on receipt_type and user_id pairs hence // 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 // 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 // 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. // common to have quite a few more than the default limit.
this.setMaxListeners(100); this.setMaxListeners(100);
this.reEmitter = new ReEmitter(this); this.reEmitter = new TypedReEmitter(this);
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological; 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. // the subsequent ones are the filtered ones in no particular order.
this.timelineSets = [new EventTimelineSet(this, opts)]; this.timelineSets = [new EventTimelineSet(this, opts)];
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [ this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [
"Room.timeline", "Room.timelineReset", RoomEvent.Timeline,
RoomEvent.TimelineReset,
]); ]);
this.fixUpLegacyTimelineFields(); this.fixUpLegacyTimelineFields();
@@ -712,7 +749,7 @@ export class Room extends EventEmitter {
if (membership === "leave") { if (membership === "leave") {
this.cleanupAfterLeaving(); 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 opts = Object.assign({ filter: filter }, this.opts);
const timelineSet = new EventTimelineSet(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.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet); this.timelineSets.push(timelineSet);
@@ -1418,9 +1458,8 @@ export class Room extends EventEmitter {
this.threads.set(thread.id, thread); this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [ this.reEmitter.reEmit(thread, [
ThreadEvent.Update, ThreadEvent.Update,
ThreadEvent.Ready, RoomEvent.Timeline,
"Room.timeline", RoomEvent.TimelineReset,
"Room.timelineReset",
]); ]);
if (!this.lastThread || this.lastThread.rootEvent.localTimestamp < rootEvent.localTimestamp) { 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 // TODO: we stash user displaynames (among other things) in
// RoomMember objects which are then attached to other events // RoomMember objects which are then attached to other events
@@ -1584,7 +1623,7 @@ export class Room extends EventEmitter {
} }
if (redactedEvent) { if (redactedEvent) {
redactedEvent.markLocallyRedacted(event); redactedEvent.markLocallyRedacted(event);
this.emit("Room.redaction", event, this); this.emit(RoomEvent.Redaction, event, this);
} }
} }
} else { } 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, this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus);
oldEventId, oldStatus);
} }
/** /**
@@ -1815,7 +1853,7 @@ export class Room extends EventEmitter {
} }
this.savePendingEvents(); this.savePendingEvents();
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus); this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus);
} }
private revertRedactionLocalEcho(redactionEvent: MatrixEvent): void { private revertRedactionLocalEcho(redactionEvent: MatrixEvent): void {
@@ -1828,7 +1866,7 @@ export class Room extends EventEmitter {
if (redactedEvent) { if (redactedEvent) {
redactedEvent.unmarkLocallyRedacted(); redactedEvent.unmarkLocallyRedacted();
// re-render after undoing redaction // re-render after undoing redaction
this.emit("Room.redactionCancelled", redactionEvent, this); this.emit(RoomEvent.RedactionCancelled, redactionEvent, this);
// reapply relation now redaction failed // reapply relation now redaction failed
if (redactedEvent.isRelation()) { if (redactedEvent.isRelation()) {
this.aggregateNonLiveRelation(redactedEvent); this.aggregateNonLiveRelation(redactedEvent);
@@ -1968,7 +2006,7 @@ export class Room extends EventEmitter {
}); });
if (oldName !== this.name) { 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); this.addReceiptsToStructure(event, synthetic);
// send events after we've regenerated the structure & cache, otherwise things that // send events after we've regenerated the structure & cache, otherwise things that
// listened for the event would read stale data. // 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 // XXX: we could do a deep-comparison to see if the tags have really
// changed - but do we want to bother? // 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()]; const lastEvent = this.accountData[event.getType()];
this.accountData[event.getType()] = event; this.accountData[event.getType()] = event;
this.emit("Room.accountData", event, this, lastEvent); this.emit(RoomEvent.AccountData, event, this, lastEvent);
} }
} }

View File

@@ -14,25 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient } from "../matrix"; import { MatrixClient, RoomEvent } from "../matrix";
import { ReEmitter } from "../ReEmitter"; import { TypedReEmitter } from "../ReEmitter";
import { RelationType } from "../@types/event"; import { RelationType } from "../@types/event";
import { IRelationsRequestOpts } from "../@types/requests"; import { IRelationsRequestOpts } from "../@types/requests";
import { MatrixEvent, IThreadBundledRelationship } from "./event"; import { IThreadBundledRelationship, MatrixEvent } from "./event";
import { Direction, EventTimeline } from "./event-timeline"; import { Direction, EventTimeline } from "./event-timeline";
import { EventTimelineSet } from './event-timeline-set'; import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
import { Room } from './room'; import { Room } from './room';
import { TypedEventEmitter } from "./typed-event-emitter"; import { TypedEventEmitter } from "./typed-event-emitter";
import { RoomState } from "./room-state"; import { RoomState } from "./room-state";
export enum ThreadEvent { export enum ThreadEvent {
New = "Thread.new", New = "Thread.new",
Ready = "Thread.ready",
Update = "Thread.update", Update = "Thread.update",
NewReply = "Thread.newReply", NewReply = "Thread.newReply",
ViewThread = "Thred.viewThread", ViewThread = "Thread.viewThread",
} }
type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New>
| 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 { interface IThreadOpts {
initialEvents?: MatrixEvent[]; initialEvents?: MatrixEvent[];
room: Room; room: Room;
@@ -42,15 +51,15 @@ interface IThreadOpts {
/** /**
* @experimental * @experimental
*/ */
export class Thread extends TypedEventEmitter<ThreadEvent> { export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
/** /**
* A reference to all the events ID at the bottom of the threads * A reference to all the events ID at the bottom of the threads
*/ */
public readonly timelineSet; public readonly timelineSet: EventTimelineSet;
private _currentUserParticipated = false; private _currentUserParticipated = false;
private reEmitter: ReEmitter; private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
private lastEvent: MatrixEvent; private lastEvent: MatrixEvent;
private replyCount = 0; private replyCount = 0;
@@ -75,11 +84,11 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
timelineSupport: true, timelineSupport: true,
pendingEvents: true, pendingEvents: true,
}); });
this.reEmitter = new ReEmitter(this); this.reEmitter = new TypedReEmitter(this);
this.reEmitter.reEmit(this.timelineSet, [ this.reEmitter.reEmit(this.timelineSet, [
"Room.timeline", RoomEvent.Timeline,
"Room.timelineReset", RoomEvent.TimelineReset,
]); ]);
// If we weren't able to find the root event, it's probably missing // If we weren't able to find the root event, it's probably missing
@@ -94,8 +103,8 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
opts?.initialEvents?.forEach(event => this.addEvent(event)); opts?.initialEvents?.forEach(event => this.addEvent(event));
this.room.on("Room.localEchoUpdated", this.onEcho); this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.room.on("Room.timeline", this.onEcho); this.room.on(RoomEvent.Timeline, this.onEcho);
} }
public get hasServerSideSupport(): boolean { public get hasServerSideSupport(): boolean {
@@ -103,7 +112,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
?.capabilities?.[RelationType.Thread]?.enabled; ?.capabilities?.[RelationType.Thread]?.enabled;
} }
onEcho = (event: MatrixEvent) => { private onEcho = (event: MatrixEvent) => {
if (this.timelineSet.eventIdToTimeline(event.getId())) { if (this.timelineSet.eventIdToTimeline(event.getId())) {
this.emit(ThreadEvent.Update, this); this.emit(ThreadEvent.Update, this);
} }
@@ -139,10 +148,11 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
* the tail/root references if needed * the tail/root references if needed
* Will fire "Thread.update" * Will fire "Thread.update"
* @param event The event to add * @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<void> { public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
// Add all incoming events to the thread's timeline set when there's // Add all incoming events to the thread's timeline set when there's no server support
// no server support
if (!this.hasServerSideSupport) { if (!this.hasServerSideSupport) {
// all the relevant membership info to hydrate events with a sender // all the relevant membership info to hydrate events with a sender
// is held in the main room timeline // is held in the main room timeline

View File

@@ -14,13 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events"; import { EventEmitter } from "events";
enum EventEmitterEvents { export enum EventEmitterEvents {
NewListener = "newListener", NewListener = "newListener",
RemoveListener = "removeListener", RemoveListener = "removeListener",
Error = "error",
} }
type AnyListener = (...args: any) => any;
export type ListenerMap<E extends string> = { [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<E>,
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 * Typed Event Emitter class which can act as a Base Model for all our model
* and communication events. * and communication events.
@@ -28,17 +43,26 @@ enum EventEmitterEvents {
* to properly type this, so that our events are not stringly-based and prone * to properly type this, so that our events are not stringly-based and prone
* to silly typos. * to silly typos.
*/ */
export abstract class TypedEventEmitter<Events extends string> extends EventEmitter { export class TypedEventEmitter<
public addListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { Events extends string,
Arguments extends ListenerMap<Events>,
SuperclassArguments extends ListenerMap<any> = Arguments,
> extends EventEmitter {
public addListener<T extends Events | EventEmitterEvents>(
event: T,
listener: Listener<Events, Arguments, T>,
): this {
return super.addListener(event, listener); return super.addListener(event, listener);
} }
public emit(event: Events | EventEmitterEvents, ...args: any[]): boolean { public emit<T extends Events>(event: T, ...args: Parameters<SuperclassArguments[T]>): boolean;
public emit<T extends Events>(event: T, ...args: Parameters<Arguments[T]>): boolean;
public emit<T extends Events>(event: T, ...args: any[]): boolean {
return super.emit(event, ...args); return super.emit(event, ...args);
} }
public eventNames(): (Events | EventEmitterEvents)[] { public eventNames(): (Events | EventEmitterEvents)[] {
return super.eventNames() as Events[]; return super.eventNames() as Array<Events | EventEmitterEvents>;
} }
public listenerCount(event: Events | EventEmitterEvents): number { public listenerCount(event: Events | EventEmitterEvents): number {
@@ -49,23 +73,38 @@ export abstract class TypedEventEmitter<Events extends string> extends EventEmit
return super.listeners(event); return super.listeners(event);
} }
public off(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { public off<T extends Events | EventEmitterEvents>(
event: T,
listener: Listener<Events, Arguments, T>,
): this {
return super.off(event, listener); return super.off(event, listener);
} }
public on(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { public on<T extends Events | EventEmitterEvents>(
event: T,
listener: Listener<Events, Arguments, T>,
): this {
return super.on(event, listener); return super.on(event, listener);
} }
public once(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { public once<T extends Events | EventEmitterEvents>(
event: T,
listener: Listener<Events, Arguments, T>,
): this {
return super.once(event, listener); return super.once(event, listener);
} }
public prependListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { public prependListener<T extends Events | EventEmitterEvents>(
event: T,
listener: Listener<Events, Arguments, T>,
): this {
return super.prependListener(event, listener); return super.prependListener(event, listener);
} }
public prependOnceListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { public prependOnceListener<T extends Events | EventEmitterEvents>(
event: T,
listener: Listener<Events, Arguments, T>,
): this {
return super.prependOnceListener(event, listener); return super.prependOnceListener(event, listener);
} }
@@ -73,7 +112,10 @@ export abstract class TypedEventEmitter<Events extends string> extends EventEmit
return super.removeAllListeners(event); return super.removeAllListeners(event);
} }
public removeListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this { public removeListener<T extends Events | EventEmitterEvents>(
event: T,
listener: Listener<Events, Arguments, T>,
): this {
return super.removeListener(event, listener); return super.removeListener(event, listener);
} }

View File

@@ -18,12 +18,29 @@ limitations under the License.
* @module models/user * @module models/user
*/ */
import { EventEmitter } from "events";
import { MatrixEvent } from "./event"; import { MatrixEvent } from "./event";
import { TypedEventEmitter } from "./typed-event-emitter";
export class User extends EventEmitter { export enum UserEvent {
// eslint-disable-next-line camelcase 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<UserEvent, UserEventHandlerMap> {
private modified: number; private modified: number;
// XXX these should be read-only // XXX these should be read-only
@@ -94,25 +111,25 @@ export class User extends EventEmitter {
const firstFire = this.events.presence === null; const firstFire = this.events.presence === null;
this.events.presence = event; this.events.presence = event;
const eventsToFire = []; const eventsToFire: UserEvent[] = [];
if (event.getContent().presence !== this.presence || firstFire) { if (event.getContent().presence !== this.presence || firstFire) {
eventsToFire.push("User.presence"); eventsToFire.push(UserEvent.Presence);
} }
if (event.getContent().avatar_url && if (event.getContent().avatar_url &&
event.getContent().avatar_url !== this.avatarUrl) { event.getContent().avatar_url !== this.avatarUrl) {
eventsToFire.push("User.avatarUrl"); eventsToFire.push(UserEvent.AvatarUrl);
} }
if (event.getContent().displayname && if (event.getContent().displayname &&
event.getContent().displayname !== this.displayName) { event.getContent().displayname !== this.displayName) {
eventsToFire.push("User.displayName"); eventsToFire.push(UserEvent.DisplayName);
} }
if (event.getContent().currently_active !== undefined && if (event.getContent().currently_active !== undefined &&
event.getContent().currently_active !== this.currentlyActive) { event.getContent().currently_active !== this.currentlyActive) {
eventsToFire.push("User.currentlyActive"); eventsToFire.push(UserEvent.CurrentlyActive);
} }
this.presence = event.getContent().presence; this.presence = event.getContent().presence;
eventsToFire.push("User.lastPresenceTs"); eventsToFire.push(UserEvent.LastPresenceTs);
if (event.getContent().status_msg) { if (event.getContent().status_msg) {
this.presenceStatusMsg = 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 = ""; if (!event.getContent()) this.unstable_statusMessage = "";
else this.unstable_statusMessage = event.getContent()["status"]; else this.unstable_statusMessage = event.getContent()["status"];
this.updateModifiedTime(); this.updateModifiedTime();
this.emit("User.unstable_statusMessage", this); this.emit(UserEvent._UnstableStatusMessage, this);
} }
} }

View File

@@ -16,8 +16,6 @@ limitations under the License.
/* eslint-disable @babel/no-invalid-this */ /* eslint-disable @babel/no-invalid-this */
import { EventEmitter } from 'events';
import { MemoryStore, IOpts as IBaseOpts } from "./memory"; import { MemoryStore, IOpts as IBaseOpts } from "./memory";
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend"; import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend"; import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend";
@@ -27,6 +25,7 @@ import { logger } from '../logger';
import { ISavedSync } from "./index"; import { ISavedSync } from "./index";
import { IIndexedDBBackend } from "./indexeddb-backend"; import { IIndexedDBBackend } from "./indexeddb-backend";
import { ISyncResponse } from "../sync-accumulator"; import { ISyncResponse } from "../sync-accumulator";
import { TypedEventEmitter } from "../models/typed-event-emitter";
/** /**
* This is an internal module. See {@link IndexedDBStore} for the public class. * This is an internal module. See {@link IndexedDBStore} for the public class.
@@ -46,6 +45,10 @@ interface IOpts extends IBaseOpts {
workerFactory?: () => Worker; workerFactory?: () => Worker;
} }
type EventHandlerMap = {
"degraded": (e: Error) => void;
};
export class IndexedDBStore extends MemoryStore { export class IndexedDBStore extends MemoryStore {
static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> { static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName); 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 // the database, such that we can derive the set if users that have been
// modified since we last saved. // modified since we last saved.
private userModifiedMap: Record<string, number> = {}; // user_id : timestamp private userModifiedMap: Record<string, number> = {}; // user_id : timestamp
private emitter = new EventEmitter(); private emitter = new TypedEventEmitter<keyof EventHandlerMap, EventHandlerMap>();
/** /**
* Construct a new Indexed Database store, which extends MemoryStore. * Construct a new Indexed Database store, which extends MemoryStore.

View File

@@ -25,6 +25,15 @@ export enum LocalStorageErrors {
QuotaExceededError = 'QuotaExceededError' 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 * 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. * 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. * 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 * See: https://github.com/vector-im/element-web/issues/18423
*/ */
class LocalStorageErrorsEventsEmitter extends TypedEventEmitter<LocalStorageErrors> {} class LocalStorageErrorsEventsEmitter extends TypedEventEmitter<LocalStorageErrors, EventHandlerMap> {}
export const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter(); export const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter();

View File

@@ -24,7 +24,7 @@ import { Group } from "../models/group";
import { Room } from "../models/room"; import { Room } from "../models/room";
import { User } from "../models/user"; import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event"; 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 { RoomMember } from "../models/room-member";
import { Filter } from "../filter"; import { Filter } from "../filter";
import { ISavedSync, IStore } from "./index"; import { ISavedSync, IStore } from "./index";
@@ -126,7 +126,7 @@ export class MemoryStore implements IStore {
this.rooms[room.roomId] = room; this.rooms[room.roomId] = room;
// add listeners for room member changes so we can keep the room member // add listeners for room member changes so we can keep the room member
// map up-to-date. // map up-to-date.
room.currentState.on("RoomState.members", this.onRoomMember); room.currentState.on(RoomStateEvent.Members, this.onRoomMember);
// add existing members // add existing members
room.currentState.getMembers().forEach((m) => { room.currentState.getMembers().forEach((m) => {
this.onRoomMember(null, room.currentState, m); this.onRoomMember(null, room.currentState, m);
@@ -185,7 +185,7 @@ export class MemoryStore implements IStore {
*/ */
public removeRoom(roomId: string): void { public removeRoom(roomId: string): void {
if (this.rooms[roomId]) { 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]; delete this.rooms[roomId];
} }

View File

@@ -23,8 +23,8 @@ limitations under the License.
* for HTTP and WS at some point. * for HTTP and WS at some point.
*/ */
import { User } from "./models/user"; import { User, UserEvent } from "./models/user";
import { NotificationCountType, Room } from "./models/room"; import { NotificationCountType, Room, RoomEvent } from "./models/room";
import { Group } from "./models/group"; import { Group } from "./models/group";
import * as utils from "./utils"; import * as utils from "./utils";
import { IDeferred } from "./utils"; import { IDeferred } from "./utils";
@@ -33,7 +33,7 @@ import { EventTimeline } from "./models/event-timeline";
import { PushProcessor } from "./pushprocessor"; import { PushProcessor } from "./pushprocessor";
import { logger } from './logger'; import { logger } from './logger';
import { InvalidStoreError } from './errors'; import { InvalidStoreError } from './errors';
import { IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client"; import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
import { import {
Category, Category,
IEphemeral, IEphemeral,
@@ -53,6 +53,8 @@ import { MatrixError, Method } from "./http-api";
import { ISavedSync } from "./store"; import { ISavedSync } from "./store";
import { EventType } from "./@types/event"; import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules"; import { IPushRules } from "./@types/PushRules";
import { RoomStateEvent } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
const DEBUG = true; const DEBUG = true;
@@ -171,8 +173,10 @@ export class SyncApi {
} }
if (client.getNotifTimelineSet()) { if (client.getNotifTimelineSet()) {
client.reEmitter.reEmit(client.getNotifTimelineSet(), client.reEmitter.reEmit(client.getNotifTimelineSet(), [
["Room.timeline", "Room.timelineReset"]); RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
} }
} }
@@ -192,16 +196,17 @@ export class SyncApi {
timelineSupport, timelineSupport,
unstableClientRelationAggregation, unstableClientRelationAggregation,
}); });
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", client.reEmitter.reEmit(room, [
"Room.redaction", RoomEvent.Name,
"Room.redactionCancelled", RoomEvent.Redaction,
"Room.receipt", "Room.tags", RoomEvent.RedactionCancelled,
"Room.timelineReset", RoomEvent.Receipt,
"Room.localEchoUpdated", RoomEvent.Tags,
"Room.accountData", RoomEvent.LocalEchoUpdated,
"Room.myMembership", RoomEvent.AccountData,
"Room.replaceEvent", RoomEvent.MyMembership,
"Room.visibilityChange", RoomEvent.Timeline,
RoomEvent.TimelineReset,
]); ]);
this.registerStateListeners(room); this.registerStateListeners(room);
return room; return room;
@@ -214,7 +219,10 @@ export class SyncApi {
public createGroup(groupId: string): Group { public createGroup(groupId: string): Group {
const client = this.client; const client = this.client;
const group = new Group(groupId); 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); client.store.storeGroup(group);
return group; return group;
} }
@@ -229,17 +237,18 @@ export class SyncApi {
// to the client now. We need to add a listener for RoomState.members in // to the client now. We need to add a listener for RoomState.members in
// order to hook them correctly. (TODO: find a better way?) // order to hook them correctly. (TODO: find a better way?)
client.reEmitter.reEmit(room.currentState, [ 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); member.user = client.getUser(member.userId);
client.reEmitter.reEmit( client.reEmitter.reEmit(member, [
member, RoomMemberEvent.Name,
[ RoomMemberEvent.Typing,
"RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel", RoomMemberEvent.PowerLevel,
"RoomMember.membership", RoomMemberEvent.Membership,
], ]);
);
}); });
} }
@@ -249,9 +258,9 @@ export class SyncApi {
*/ */
private deregisterStateListeners(room: Room): void { private deregisterStateListeners(room: Room): void {
// could do with a better way of achieving this. // could do with a better way of achieving this.
room.currentState.removeAllListeners("RoomState.events"); room.currentState.removeAllListeners(RoomStateEvent.Events);
room.currentState.removeAllListeners("RoomState.members"); room.currentState.removeAllListeners(RoomStateEvent.Members);
room.currentState.removeAllListeners("RoomState.newMember"); room.currentState.removeAllListeners(RoomStateEvent.NewMember);
} }
/** /**
@@ -314,7 +323,7 @@ export class SyncApi {
room.recalculate(); room.recalculate();
client.store.storeRoom(room); client.store.storeRoom(room);
client.emit("Room", room); client.emit(ClientEvent.Room, room);
this.processEventsForNotifs(room, events); this.processEventsForNotifs(room, events);
}); });
@@ -362,7 +371,7 @@ export class SyncApi {
user.setPresenceEvent(presenceEvent); user.setPresenceEvent(presenceEvent);
client.store.storeUser(user); client.store.storeUser(user);
} }
client.emit("event", presenceEvent); client.emit(ClientEvent.Event, presenceEvent);
}); });
} }
@@ -388,7 +397,7 @@ export class SyncApi {
response.messages.start); response.messages.start);
client.store.storeRoom(this._peekRoom); client.store.storeRoom(this._peekRoom);
client.emit("Room", this._peekRoom); client.emit(ClientEvent.Room, this._peekRoom);
this.peekPoll(this._peekRoom); this.peekPoll(this._peekRoom);
return this._peekRoom; return this._peekRoom;
@@ -445,7 +454,7 @@ export class SyncApi {
user.setPresenceEvent(presenceEvent); user.setPresenceEvent(presenceEvent);
this.client.store.storeUser(user); 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) // 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); logger.error("Caught /sync error", e.stack || e);
// Emit the exception for client handling // 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 // update this as it may have changed
@@ -1073,7 +1082,7 @@ export class SyncApi {
user.setPresenceEvent(presenceEvent); user.setPresenceEvent(presenceEvent);
client.store.storeUser(user); 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); client.pushRules = PushProcessor.rewriteDefaultRules(rules);
} }
const prevEvent = prevEventsMap[accountDataEvent.getId()]; const prevEvent = prevEventsMap[accountDataEvent.getId()];
client.emit("accountData", accountDataEvent, prevEvent); client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
return accountDataEvent; return accountDataEvent;
}, },
); );
@@ -1149,7 +1158,7 @@ export class SyncApi {
} }
} }
client.emit("toDeviceEvent", toDeviceEvent); client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
}, },
); );
} else { } else {
@@ -1201,10 +1210,10 @@ export class SyncApi {
if (inviteObj.isBrandNewRoom) { if (inviteObj.isBrandNewRoom) {
room.recalculate(); room.recalculate();
client.store.storeRoom(room); client.store.storeRoom(room);
client.emit("Room", room); client.emit(ClientEvent.Room, room);
} }
stateEvents.forEach(function(e) { stateEvents.forEach(function(e) {
client.emit("event", e); client.emit(ClientEvent.Event, e);
}); });
room.updateMyMembership("invite"); room.updateMyMembership("invite");
}); });
@@ -1325,13 +1334,13 @@ export class SyncApi {
room.recalculate(); room.recalculate();
if (joinObj.isBrandNewRoom) { if (joinObj.isBrandNewRoom) {
client.store.storeRoom(room); client.store.storeRoom(room);
client.emit("Room", room); client.emit(ClientEvent.Room, room);
} }
this.processEventsForNotifs(room, events); this.processEventsForNotifs(room, events);
const processRoomEvent = async (e) => { const processRoomEvent = async (e) => {
client.emit("event", e); client.emit(ClientEvent.Event, e);
if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) { if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) {
await this.opts.crypto.onCryptoEvent(e); await this.opts.crypto.onCryptoEvent(e);
} }
@@ -1351,10 +1360,10 @@ export class SyncApi {
await utils.promiseMapSeries(timelineEvents, processRoomEvent); await utils.promiseMapSeries(timelineEvents, processRoomEvent);
await utils.promiseMapSeries(threadedEvents, processRoomEvent); await utils.promiseMapSeries(threadedEvents, processRoomEvent);
ephemeralEvents.forEach(function(e) { ephemeralEvents.forEach(function(e) {
client.emit("event", e); client.emit(ClientEvent.Event, e);
}); });
accountDataEvents.forEach(function(e) { accountDataEvents.forEach(function(e) {
client.emit("event", e); client.emit(ClientEvent.Event, e);
}); });
room.updateMyMembership("join"); room.updateMyMembership("join");
@@ -1381,22 +1390,22 @@ export class SyncApi {
room.recalculate(); room.recalculate();
if (leaveObj.isBrandNewRoom) { if (leaveObj.isBrandNewRoom) {
client.store.storeRoom(room); client.store.storeRoom(room);
client.emit("Room", room); client.emit(ClientEvent.Room, room);
} }
this.processEventsForNotifs(room, events); this.processEventsForNotifs(room, events);
stateEvents.forEach(function(e) { stateEvents.forEach(function(e) {
client.emit("event", e); client.emit(ClientEvent.Event, e);
}); });
timelineEvents.forEach(function(e) { timelineEvents.forEach(function(e) {
client.emit("event", e); client.emit(ClientEvent.Event, e);
}); });
threadedEvents.forEach(function(e) { threadedEvents.forEach(function(e) {
client.emit("event", e); client.emit(ClientEvent.Event, e);
}); });
accountDataEvents.forEach(function(e) { accountDataEvents.forEach(function(e) {
client.emit("event", e); client.emit(ClientEvent.Event, e);
}); });
room.updateMyMembership("leave"); room.updateMyMembership("leave");
@@ -1551,7 +1560,7 @@ export class SyncApi {
group.setMyMembership(sectionName); group.setMyMembership(sectionName);
if (isBrandNew) { if (isBrandNew) {
// Now we've filled in all the fields, emit the Group event // 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; const old = this.syncState;
this.syncState = newState; this.syncState = newState;
this.syncStateData = data; 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 { function createNewUser(client: MatrixClient, userId: string): User {
const user = new User(userId); const user = new User(userId);
client.reEmitter.reEmit(user, [ client.reEmitter.reEmit(user, [
"User.avatarUrl", "User.displayName", "User.presence", UserEvent.AvatarUrl,
"User.currentlyActive", "User.lastPresenceTs", UserEvent.DisplayName,
UserEvent.Presence,
UserEvent.CurrentlyActive,
UserEvent.LastPresenceTs,
]); ]);
return user; return user;
} }

View File

@@ -21,8 +21,6 @@ limitations under the License.
* @module webrtc/call * @module webrtc/call
*/ */
import { EventEmitter } from 'events';
import { logger } from '../logger'; import { logger } from '../logger';
import * as utils from '../utils'; import * as utils from '../utils';
import { MatrixEvent } from '../models/event'; import { MatrixEvent } from '../models/event';
@@ -47,6 +45,7 @@ import {
import { CallFeed } from './callFeed'; import { CallFeed } from './callFeed';
import { MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { ISendEventResponse } from "../@types/requests"; import { ISendEventResponse } from "../@types/requests";
import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter";
// events: hangup, error(err), replaced(call), state(state, oldState) // events: hangup, error(err), replaced(call), state(state, oldState)
@@ -241,6 +240,21 @@ function genCallID(): string {
return Date.now().toString() + randomString(16); 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. * Construct a new Matrix Call.
* @constructor * @constructor
@@ -252,7 +266,7 @@ function genCallID(): string {
* @param {Array<Object>} opts.turnServers Optional. A list of TURN servers. * @param {Array<Object>} opts.turnServers Optional. A list of TURN servers.
* @param {MatrixClient} opts.client The Matrix Client instance to send events to. * @param {MatrixClient} opts.client The Matrix Client instance to send events to.
*/ */
export class MatrixCall extends EventEmitter { export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
public roomId: string; public roomId: string;
public callId: string; public callId: string;
public state = CallState.Fledgling; public state = CallState.Fledgling;
@@ -1973,7 +1987,7 @@ export class MatrixCall extends EventEmitter {
this.peerConn.close(); this.peerConn.close();
} }
if (shouldEmit) { if (shouldEmit) {
this.emit(CallEvent.Hangup, this); this.emit(CallEvent.Hangup);
} }
} }
@@ -1995,7 +2009,7 @@ export class MatrixCall extends EventEmitter {
} }
private checkForErrorListener(): void { private checkForErrorListener(): void {
if (this.listeners("error").length === 0) { if (this.listeners(EventEmitterEvents.Error).length === 0) {
throw new Error( throw new Error(
"You MUST attach an error listener using call.on('error', function() {})", "You MUST attach an error listener using call.on('error', function() {})",
); );

View File

@@ -14,17 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixEvent } from '../models/event'; import { MatrixEvent, MatrixEventEvent } from '../models/event';
import { logger } from '../logger'; 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 { EventType } from '../@types/event';
import { MatrixClient } from '../client'; import { ClientEvent, MatrixClient } from '../client';
import { MCallAnswer, MCallHangupReject } from "./callEventTypes"; 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 // Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
// time to press the 'accept' button // time to press the 'accept' button
const RING_GRACE_PERIOD = 3000; const RING_GRACE_PERIOD = 3000;
export enum CallEventHandlerEvent {
Incoming = "Call.incoming",
}
export type CallEventHandlerEventHandlerMap = {
[CallEventHandlerEvent.Incoming]: (call: MatrixCall) => void;
};
export class CallEventHandler { export class CallEventHandler {
client: MatrixClient; client: MatrixClient;
calls: Map<string, MatrixCall>; calls: Map<string, MatrixCall>;
@@ -47,17 +57,17 @@ export class CallEventHandler {
} }
public start() { public start() {
this.client.on("sync", this.evaluateEventBuffer); this.client.on(ClientEvent.Sync, this.evaluateEventBuffer);
this.client.on("Room.timeline", this.onRoomTimeline); this.client.on(RoomEvent.Timeline, this.onRoomTimeline);
} }
public stop() { public stop() {
this.client.removeListener("sync", this.evaluateEventBuffer); this.client.removeListener(ClientEvent.Sync, this.evaluateEventBuffer);
this.client.removeListener("Room.timeline", this.onRoomTimeline); this.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
} }
private evaluateEventBuffer = async () => { private evaluateEventBuffer = async () => {
if (this.client.getSyncState() === "SYNCING") { if (this.client.getSyncState() === SyncState.Syncing) {
await Promise.all(this.callEventBuffer.map(event => { await Promise.all(this.callEventBuffer.map(event => {
this.client.decryptEventIfNeeded(event); this.client.decryptEventIfNeeded(event);
})); }));
@@ -101,7 +111,7 @@ export class CallEventHandler {
if (event.isBeingDecrypted() || event.isDecryptionFailure()) { if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
// add an event listener for once the event is decrypted. // 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.eventIsACall(event)) return;
if (this.callEventBuffer.includes(event)) { if (this.callEventBuffer.includes(event)) {
@@ -221,7 +231,7 @@ export class CallEventHandler {
call.hangup(CallErrorCode.Replaced, true); call.hangup(CallErrorCode.Replaced, true);
} }
} else { } else {
this.client.emit("Call.incoming", call); this.client.emit(CallEventHandlerEvent.Incoming, call);
} }
return; return;
} else if (type === EventType.CallCandidates) { } else if (type === EventType.CallCandidates) {

View File

@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import EventEmitter from "events";
import { SDPStreamMetadataPurpose } from "./callEventTypes"; import { SDPStreamMetadataPurpose } from "./callEventTypes";
import { MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { RoomMember } from "../models/room-member"; import { RoomMember } from "../models/room-member";
import { TypedEventEmitter } from "../models/typed-event-emitter";
const POLLING_INTERVAL = 200; // ms const POLLING_INTERVAL = 200; // ms
export const SPEAKING_THRESHOLD = -60; // dB export const SPEAKING_THRESHOLD = -60; // dB
@@ -47,7 +46,14 @@ export enum CallFeedEvent {
Speaking = "speaking", 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<CallFeedEvent, EventHandlerMap> {
public stream: MediaStream; public stream: MediaStream;
public userId: string; public userId: string;
public purpose: SDPStreamMetadataPurpose; public purpose: SDPStreamMetadataPurpose;