matrix-js-sdk 43.0.0 → 44.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/lib/matrixrtc/CallMembership.d.ts +30 -25
- package/lib/matrixrtc/CallMembership.d.ts.map +1 -1
- package/lib/matrixrtc/CallMembership.js +91 -100
- package/lib/matrixrtc/CallMembership.js.map +1 -1
- package/lib/matrixrtc/EncryptionManager.d.ts +6 -4
- package/lib/matrixrtc/EncryptionManager.d.ts.map +1 -1
- package/lib/matrixrtc/EncryptionManager.js.map +1 -1
- package/lib/matrixrtc/IMembershipManager.d.ts +2 -15
- package/lib/matrixrtc/IMembershipManager.d.ts.map +1 -1
- package/lib/matrixrtc/IMembershipManager.js.map +1 -1
- package/lib/matrixrtc/LivekitTransport.d.ts +36 -7
- package/lib/matrixrtc/LivekitTransport.d.ts.map +1 -1
- package/lib/matrixrtc/LivekitTransport.js +29 -2
- package/lib/matrixrtc/LivekitTransport.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.d.ts +12 -51
- package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.js +10 -66
- package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
- package/lib/matrixrtc/MembershipManager.d.ts +5 -10
- package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
- package/lib/matrixrtc/MembershipManager.js +25 -44
- package/lib/matrixrtc/MembershipManager.js.map +1 -1
- package/lib/matrixrtc/RTCEncryptionManager.d.ts +8 -8
- package/lib/matrixrtc/RTCEncryptionManager.d.ts.map +1 -1
- package/lib/matrixrtc/RTCEncryptionManager.js +30 -22
- package/lib/matrixrtc/RTCEncryptionManager.js.map +1 -1
- package/lib/matrixrtc/ToDeviceKeyTransport.js +7 -8
- package/lib/matrixrtc/ToDeviceKeyTransport.js.map +1 -1
- package/lib/matrixrtc/membershipData/rtc.d.ts +8 -13
- package/lib/matrixrtc/membershipData/rtc.d.ts.map +1 -1
- package/lib/matrixrtc/membershipData/rtc.js +2 -11
- package/lib/matrixrtc/membershipData/rtc.js.map +1 -1
- package/lib/matrixrtc/membershipData/session.d.ts +6 -2
- package/lib/matrixrtc/membershipData/session.d.ts.map +1 -1
- package/lib/matrixrtc/membershipData/session.js +3 -0
- package/lib/matrixrtc/membershipData/session.js.map +1 -1
- package/lib/matrixrtc/types.d.ts +5 -0
- package/lib/matrixrtc/types.d.ts.map +1 -1
- package/lib/matrixrtc/types.js +5 -0
- package/lib/matrixrtc/types.js.map +1 -1
- package/lib/models/event-timeline-set.d.ts +9 -2
- package/lib/models/event-timeline-set.d.ts.map +1 -1
- package/lib/models/event-timeline-set.js +8 -5
- package/lib/models/event-timeline-set.js.map +1 -1
- package/lib/models/room-sticky-events.d.ts +17 -2
- package/lib/models/room-sticky-events.d.ts.map +1 -1
- package/lib/models/room-sticky-events.js +88 -47
- package/lib/models/room-sticky-events.js.map +1 -1
- package/lib/models/room.d.ts +1 -1
- package/lib/models/room.d.ts.map +1 -1
- package/lib/models/room.js +8 -2
- package/lib/models/room.js.map +1 -1
- package/lib/models/thread.d.ts.map +1 -1
- package/lib/models/thread.js +23 -2
- package/lib/models/thread.js.map +1 -1
- package/lib/rendezvous/MSC4108SignInWithQR.d.ts +3 -3
- package/lib/rendezvous/MSC4108SignInWithQR.d.ts.map +1 -1
- package/lib/rendezvous/MSC4108SignInWithQR.js.map +1 -1
- package/lib/rendezvous/SignInWithQRFlow.d.ts +95 -0
- package/lib/rendezvous/SignInWithQRFlow.d.ts.map +1 -0
- package/lib/rendezvous/SignInWithQRFlow.js +3 -0
- package/lib/rendezvous/SignInWithQRFlow.js.map +1 -0
- package/lib/rendezvous/index.d.ts +1 -0
- package/lib/rendezvous/index.d.ts.map +1 -1
- package/lib/rendezvous/index.js.map +1 -1
- package/lib/rust-crypto/index.js +5 -1
- package/lib/rust-crypto/index.js.map +1 -1
- package/package.json +3 -3
- package/src/matrixrtc/CallMembership.ts +85 -95
- package/src/matrixrtc/EncryptionManager.ts +6 -4
- package/src/matrixrtc/IMembershipManager.ts +2 -15
- package/src/matrixrtc/LivekitTransport.ts +58 -9
- package/src/matrixrtc/MatrixRTCSession.ts +21 -80
- package/src/matrixrtc/MembershipManager.ts +23 -40
- package/src/matrixrtc/RTCEncryptionManager.ts +51 -39
- package/src/matrixrtc/ToDeviceKeyTransport.ts +7 -7
- package/src/matrixrtc/membershipData/rtc.ts +9 -23
- package/src/matrixrtc/membershipData/session.ts +14 -2
- package/src/matrixrtc/types.ts +6 -0
- package/src/models/event-timeline-set.ts +19 -4
- package/src/models/room-sticky-events.ts +92 -58
- package/src/models/room.ts +8 -2
- package/src/models/thread.ts +25 -3
- package/src/rendezvous/MSC4108SignInWithQR.ts +3 -3
- package/src/rendezvous/SignInWithQRFlow.ts +117 -0
- package/src/rendezvous/index.ts +1 -0
- package/src/rust-crypto/index.ts +5 -1
|
@@ -19,13 +19,14 @@ import {
|
|
|
19
19
|
getEncryptionKeyMapKey,
|
|
20
20
|
type IEncryptionManager,
|
|
21
21
|
} from "./EncryptionManager.ts";
|
|
22
|
-
import { type EncryptionConfig
|
|
22
|
+
import { type EncryptionConfig } from "./MatrixRTCSession.ts";
|
|
23
23
|
import type { CallMembership } from "./CallMembership.ts";
|
|
24
24
|
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
|
25
25
|
import { type IKeyTransport, type KeyTransportEventListener, KeyTransportEvents } from "./IKeyTransport.ts";
|
|
26
26
|
import { type Logger } from "../logger.ts";
|
|
27
27
|
import { sleep } from "../utils.ts";
|
|
28
28
|
import {
|
|
29
|
+
type Transport,
|
|
29
30
|
type EncryptionKeyMapKey,
|
|
30
31
|
type InboundEncryptionSession,
|
|
31
32
|
type OutboundEncryptionSession,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
} from "./types.ts";
|
|
34
35
|
import { OutdatedKeyFilter } from "./utils.ts";
|
|
35
36
|
import { computeRtcIdentityRaw } from "./membershipData/rtc.ts";
|
|
37
|
+
import { isUnstableLivekitTransport } from "./LivekitTransport.ts";
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Default for {@link EncryptionConfig.keyRotationParticipantLimit}.
|
|
@@ -61,8 +63,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
|
|
|
61
63
|
// This will be done when removing the legacy EncryptionManager.
|
|
62
64
|
private manageMediaKeys = false;
|
|
63
65
|
|
|
64
|
-
private
|
|
65
|
-
private ownRtcBackendIdentityCache: string | undefined;
|
|
66
|
+
private publishedTransports: Transport[] = [];
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
69
|
* Store the key rings for each participant.
|
|
@@ -75,7 +76,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
|
|
|
75
76
|
key: Uint8Array<ArrayBuffer>;
|
|
76
77
|
keyIndex: number;
|
|
77
78
|
membership: CallMembershipIdentityParts;
|
|
78
|
-
|
|
79
|
+
backendIdentity: string;
|
|
79
80
|
}>
|
|
80
81
|
>();
|
|
81
82
|
|
|
@@ -130,7 +131,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
|
|
|
130
131
|
|
|
131
132
|
private logger: Logger | undefined = undefined;
|
|
132
133
|
|
|
133
|
-
private readonly rtcIdentityProvider: (userId: string,
|
|
134
|
+
private readonly rtcIdentityProvider: (userId: string, memberId: string) => Promise<string>;
|
|
134
135
|
|
|
135
136
|
/**
|
|
136
137
|
*
|
|
@@ -151,10 +152,10 @@ export class RTCEncryptionManager implements IEncryptionManager {
|
|
|
151
152
|
keyBin: Uint8Array<ArrayBuffer>,
|
|
152
153
|
encryptionKeyIndex: number,
|
|
153
154
|
membership: CallMembershipIdentityParts,
|
|
154
|
-
|
|
155
|
+
backendIdentity: string,
|
|
155
156
|
) => void,
|
|
156
157
|
parentLogger?: Logger,
|
|
157
|
-
rtcBackendIdProvider?: (userId: string,
|
|
158
|
+
rtcBackendIdProvider?: (userId: string, memberId: string) => Promise<string>,
|
|
158
159
|
) {
|
|
159
160
|
this.logger = parentLogger?.getChild(`[EncryptionManager]`);
|
|
160
161
|
this.rtcIdentityProvider = rtcBackendIdProvider ?? computeRtcIdentityRaw;
|
|
@@ -175,20 +176,25 @@ export class RTCEncryptionManager implements IEncryptionManager {
|
|
|
175
176
|
return this.getMemberships().length >= this.keyRotationParticipantLimit;
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
private
|
|
179
|
-
if (this.ownRtcBackendIdentityCache) return this.ownRtcBackendIdentityCache;
|
|
179
|
+
private ownBackendIdentitiesPromise: Promise<string[]> | undefined;
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
private getOwnBackendIdentities(): Promise<string[]> {
|
|
182
|
+
this.ownBackendIdentitiesPromise ??= (async (): Promise<string[]> => {
|
|
182
183
|
const { userId, deviceId, memberId } = this.ownMembership;
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
184
|
+
const livekitTransports = this.publishedTransports.filter(isUnstableLivekitTransport);
|
|
185
|
+
const backendIdentities: string[] = [];
|
|
186
|
+
|
|
187
|
+
if (livekitTransports.some((t) => "url" in t)) {
|
|
188
|
+
this.logger?.info(`Computing RTC backend identity for ${userId}:${deviceId}:${memberId}`);
|
|
189
|
+
backendIdentities.push(await this.rtcIdentityProvider(userId, memberId));
|
|
190
|
+
}
|
|
191
|
+
if (livekitTransports.some((t) => "livekit_service_url" in t)) {
|
|
192
|
+
backendIdentities.push(`${userId}:${deviceId}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return backendIdentities;
|
|
196
|
+
})();
|
|
197
|
+
return this.ownBackendIdentitiesPromise;
|
|
192
198
|
}
|
|
193
199
|
|
|
194
200
|
public getEncryptionKeys(): ReadonlyMap<
|
|
@@ -197,7 +203,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
|
|
|
197
203
|
key: Uint8Array<ArrayBuffer>;
|
|
198
204
|
keyIndex: number;
|
|
199
205
|
membership: CallMembershipIdentityParts;
|
|
200
|
-
|
|
206
|
+
backendIdentity: string;
|
|
201
207
|
}>
|
|
202
208
|
> {
|
|
203
209
|
return new Map(this.participantKeyRings);
|
|
@@ -233,33 +239,36 @@ export class RTCEncryptionManager implements IEncryptionManager {
|
|
|
233
239
|
this.keysWithoutMatchingRTCMembership.push({ key, keyIndex, membership });
|
|
234
240
|
return;
|
|
235
241
|
}
|
|
236
|
-
|
|
242
|
+
for (const backendIdentity of fullMembership.backendIdentities) {
|
|
243
|
+
this.addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, backendIdentity);
|
|
244
|
+
}
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
private addKeyToParticipantWithBackendIdentity(
|
|
240
248
|
key: Uint8Array<ArrayBuffer>,
|
|
241
249
|
keyIndex: number,
|
|
242
250
|
membership: CallMembershipIdentityParts,
|
|
243
|
-
|
|
251
|
+
backendIdentity: string,
|
|
244
252
|
): void {
|
|
245
253
|
const mapKey = getEncryptionKeyMapKey(membership);
|
|
246
254
|
if (!this.participantKeyRings.has(mapKey)) {
|
|
247
255
|
this.participantKeyRings.set(mapKey, []);
|
|
248
256
|
}
|
|
249
|
-
this.participantKeyRings.get(mapKey)!.push({ key, keyIndex, membership,
|
|
250
|
-
this.onEncryptionKeysChanged(key, keyIndex, membership,
|
|
257
|
+
this.participantKeyRings.get(mapKey)!.push({ key, keyIndex, membership, backendIdentity });
|
|
258
|
+
this.onEncryptionKeysChanged(key, keyIndex, membership, backendIdentity);
|
|
251
259
|
}
|
|
252
260
|
|
|
253
|
-
public join(joinConfig:
|
|
261
|
+
public join(joinConfig: EncryptionConfig | undefined, publishedTransports: Transport[]): void {
|
|
254
262
|
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? true; // default to true
|
|
255
|
-
this.
|
|
263
|
+
this.publishedTransports = publishedTransports;
|
|
264
|
+
this.ownBackendIdentitiesPromise = undefined;
|
|
256
265
|
this.useKeyDelay = joinConfig?.useKeyDelay ?? 1000;
|
|
257
266
|
this.keyRotationGracePeriodMs = joinConfig?.keyRotationGracePeriodMs ?? 10_000;
|
|
258
267
|
this.keyRotationParticipantLimit =
|
|
259
268
|
joinConfig?.keyRotationParticipantLimit ?? DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT;
|
|
260
269
|
|
|
261
270
|
this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
|
|
262
|
-
void this.
|
|
271
|
+
void this.getOwnBackendIdentities(); // precompute own identity
|
|
263
272
|
|
|
264
273
|
this.logger?.info(`Joining room`);
|
|
265
274
|
this.transport.start();
|
|
@@ -364,12 +373,14 @@ export class RTCEncryptionManager implements IEncryptionManager {
|
|
|
364
373
|
keyId: 0,
|
|
365
374
|
};
|
|
366
375
|
this.outboundSession = firstKey;
|
|
367
|
-
this.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
376
|
+
const ids = await this.getOwnBackendIdentities();
|
|
377
|
+
for (const backendIdentity of ids)
|
|
378
|
+
this.addKeyToParticipantWithBackendIdentity(
|
|
379
|
+
firstKey.key,
|
|
380
|
+
firstKey.keyId,
|
|
381
|
+
this.ownMembership,
|
|
382
|
+
backendIdentity,
|
|
383
|
+
);
|
|
373
384
|
}
|
|
374
385
|
// get current memberships
|
|
375
386
|
const toShareWith: ParticipantDeviceInfo[] = this.getMemberships()
|
|
@@ -467,12 +478,13 @@ export class RTCEncryptionManager implements IEncryptionManager {
|
|
|
467
478
|
this.logger?.trace(`Delay Rollout for key:${newOutboundEncryptionSession.keyId}...`);
|
|
468
479
|
await sleep(this.useKeyDelay);
|
|
469
480
|
this.logger?.trace(`...Delayed rollout of index:${newOutboundEncryptionSession.keyId} `);
|
|
470
|
-
this.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
481
|
+
for (const backendIdentity of await this.getOwnBackendIdentities())
|
|
482
|
+
this.addKeyToParticipantWithBackendIdentity(
|
|
483
|
+
newOutboundEncryptionSession.key,
|
|
484
|
+
newOutboundEncryptionSession.keyId,
|
|
485
|
+
this.ownMembership,
|
|
486
|
+
backendIdentity,
|
|
487
|
+
);
|
|
476
488
|
}
|
|
477
489
|
} catch (err) {
|
|
478
490
|
this.logger?.error(`Failed to rollout key`, err);
|
|
@@ -152,13 +152,13 @@ export class ToDeviceKeyTransport
|
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
//
|
|
156
|
-
// see
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
// NB: When received via the widget driver, the to-device events
|
|
156
|
+
// are properly reconstructed as if they are encrypted (see MatrixEvent#makeEncrypted).
|
|
157
|
+
if (event.getWireType() != EventType.RoomMessageEncrypted) {
|
|
158
|
+
// WARN: The call keys were sent in clear. Ignore them
|
|
159
|
+
this.logger.warn(`Call encryption keys sent in clear from: ${event.getSender()}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
162
|
|
|
163
163
|
const content = this.getValidEventContent(event);
|
|
164
164
|
if (!content) return;
|
|
@@ -16,7 +16,6 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
import { MXID_PATTERN } from "../../models/room-member.ts";
|
|
18
18
|
import type { IContent } from "../../models/event.ts";
|
|
19
|
-
import type { RelationType } from "../../types.ts";
|
|
20
19
|
import { type RtcSlotApplicationContent, type Transport } from "../types.ts";
|
|
21
20
|
import { MatrixRTCMembershipParseError } from "./common.ts";
|
|
22
21
|
import { sha256 } from "../../digest.ts";
|
|
@@ -27,24 +26,20 @@ import { slotIdToDescription } from "../utils.ts";
|
|
|
27
26
|
* Represents the current form of MSC4143, which uses sticky events to store membership.
|
|
28
27
|
*/
|
|
29
28
|
export interface RtcMembershipData {
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
slot_id: string;
|
|
30
|
+
member: {
|
|
32
31
|
user_id: string;
|
|
33
32
|
device_id: string;
|
|
34
33
|
id: string;
|
|
35
34
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
rel_type: RelationType.Reference;
|
|
39
|
-
};
|
|
40
|
-
"application": RtcSlotApplicationContent;
|
|
41
|
-
"transports": {
|
|
35
|
+
application: RtcSlotApplicationContent;
|
|
36
|
+
transports: {
|
|
42
37
|
published: Transport[];
|
|
43
38
|
can_subscribe: string[];
|
|
44
39
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
versions: string[];
|
|
41
|
+
msc4354_sticky_key?: string;
|
|
42
|
+
sticky_key?: string;
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
/**
|
|
@@ -142,15 +137,6 @@ export const checkRtcMembershipData = (data: IContent, sender: string): data is
|
|
|
142
137
|
) {
|
|
143
138
|
errors.push(prefix + "sticky_key and msc4354_sticky_key must be equal if both are defined");
|
|
144
139
|
}
|
|
145
|
-
if (data["m.relates_to"] !== undefined) {
|
|
146
|
-
const rel = data["m.relates_to"] as RtcMembershipData["m.relates_to"];
|
|
147
|
-
if (typeof rel !== "object" || rel === null) {
|
|
148
|
-
errors.push(prefix + "m.relates_to must be an object if provided");
|
|
149
|
-
} else {
|
|
150
|
-
if (typeof rel.event_id !== "string") errors.push(prefix + "m.relates_to.event_id must be a string");
|
|
151
|
-
if (rel.rel_type !== "m.reference") errors.push(prefix + "m.relates_to.rel_type must be m.reference");
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
140
|
|
|
155
141
|
if (errors.length) {
|
|
156
142
|
throw new MatrixRTCMembershipParseError("RtcMembership", errors);
|
|
@@ -159,9 +145,9 @@ export const checkRtcMembershipData = (data: IContent, sender: string): data is
|
|
|
159
145
|
return true;
|
|
160
146
|
};
|
|
161
147
|
|
|
162
|
-
export async function computeRtcIdentityRaw(userId: string,
|
|
148
|
+
export async function computeRtcIdentityRaw(userId: string, memberId: string): Promise<string> {
|
|
163
149
|
// canonical JSON serialization (Matrix canonical JSON for arrays)
|
|
164
|
-
const jsonStr = JSON.stringify([userId,
|
|
150
|
+
const jsonStr = JSON.stringify([userId, memberId]);
|
|
165
151
|
const hashBuffer = await sha256(jsonStr);
|
|
166
152
|
const hashedString = encodeUnpaddedBase64(hashBuffer);
|
|
167
153
|
return hashedString;
|
|
@@ -15,7 +15,7 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { type IContent } from "../../matrix.ts";
|
|
18
|
-
import { type RTCCallIntent, type Transport } from "../types.ts";
|
|
18
|
+
import { type RTCCallCapability, type RTCCallIntent, type Transport } from "../types.ts";
|
|
19
19
|
import { MatrixRTCMembershipParseError } from "./common.ts";
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -48,7 +48,7 @@ export type SessionMembershipData = {
|
|
|
48
48
|
*/
|
|
49
49
|
"focus_active": {
|
|
50
50
|
type: "livekit" | string;
|
|
51
|
-
focus_selection: "
|
|
51
|
+
focus_selection: "multi_sfu" | string;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
/**
|
|
@@ -87,6 +87,11 @@ export type SessionMembershipData = {
|
|
|
87
87
|
*/
|
|
88
88
|
"m.call.intent"?: RTCCallIntent;
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The kinds of media this member's device is capable of handling, e.g. audio and/or video.
|
|
92
|
+
*/
|
|
93
|
+
"capabilities"?: RTCCallCapability[];
|
|
94
|
+
|
|
90
95
|
/**
|
|
91
96
|
* The id used on the media backend.
|
|
92
97
|
* (With livekit this is the participant identity on the LK SFU)
|
|
@@ -138,6 +143,13 @@ export const checkSessionsMembershipData = (data: IContent): data is SessionMemb
|
|
|
138
143
|
errors.push(prefix + "m.call.intent must be a string");
|
|
139
144
|
}
|
|
140
145
|
|
|
146
|
+
if (
|
|
147
|
+
data.capabilities !== undefined &&
|
|
148
|
+
!(Array.isArray(data.capabilities) && data.capabilities.every((c: unknown) => typeof c === "string"))
|
|
149
|
+
) {
|
|
150
|
+
errors.push(prefix + "capabilities must be an array of strings");
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
if (errors.length) {
|
|
142
154
|
throw new MatrixRTCMembershipParseError("SessionMembership", errors);
|
|
143
155
|
}
|
package/src/matrixrtc/types.ts
CHANGED
|
@@ -111,6 +111,12 @@ export type RTCNotificationType = "ring" | "notification";
|
|
|
111
111
|
*/
|
|
112
112
|
export type RTCCallIntent = "audio" | "video" | string;
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Represents one kind of media a member's device is capable of handling.
|
|
116
|
+
* May be any string, although `"m.render_audio"` and `"m.render_video"` are commonly accepted values.
|
|
117
|
+
*/
|
|
118
|
+
export type RTCCallCapability = "m.render_audio" | "m.render_video" | string;
|
|
119
|
+
|
|
114
120
|
/**
|
|
115
121
|
* The maximum `lifetime` of an RTC notification as per MSC4075. Larger values are capped to this.
|
|
116
122
|
*
|
|
@@ -63,6 +63,12 @@ export interface IAddEventToTimelineOptions extends Pick<
|
|
|
63
63
|
> {
|
|
64
64
|
/** Whether the sync response came from cache */
|
|
65
65
|
fromCache?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The other events being added in the same batch. A relation whose parent is in the batch but not yet
|
|
68
|
+
* in a timeline (back-pagination adds newest first) is placed as its parent will be, rather than dropped
|
|
69
|
+
* as a relation to an unknown event.
|
|
70
|
+
*/
|
|
71
|
+
batch?: MatrixEvent[];
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
export interface IAddLiveEventOptions extends Pick<
|
|
@@ -501,6 +507,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|
|
501
507
|
this.addEventToTimeline(event, timeline, {
|
|
502
508
|
toStartOfTimeline,
|
|
503
509
|
addToState,
|
|
510
|
+
batch: events,
|
|
504
511
|
});
|
|
505
512
|
lastEventWasNew = true;
|
|
506
513
|
didUpdate = true;
|
|
@@ -656,7 +663,14 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|
|
656
663
|
public addEventToTimeline(
|
|
657
664
|
event: MatrixEvent,
|
|
658
665
|
timeline: EventTimeline,
|
|
659
|
-
{
|
|
666
|
+
{
|
|
667
|
+
toStartOfTimeline,
|
|
668
|
+
fromCache = false,
|
|
669
|
+
roomState,
|
|
670
|
+
timelineWasEmpty,
|
|
671
|
+
addToState,
|
|
672
|
+
batch,
|
|
673
|
+
}: IAddEventToTimelineOptions,
|
|
660
674
|
): void {
|
|
661
675
|
if (timeline.getTimelineSet() !== this) {
|
|
662
676
|
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
|
|
@@ -672,7 +686,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|
|
672
686
|
//
|
|
673
687
|
// We can only run this check for timelines with a `room` because `canContain`
|
|
674
688
|
// requires it
|
|
675
|
-
if (this.room && !this.canContain(event)) {
|
|
689
|
+
if (this.room && !this.canContain(event, batch)) {
|
|
676
690
|
let eventDebugString = `event=${eventId}`;
|
|
677
691
|
if (event.threadRootId) {
|
|
678
692
|
eventDebugString += `(belongs to thread=${event.threadRootId})`;
|
|
@@ -932,10 +946,11 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|
|
932
946
|
* Requires the `room` property to have been set at EventTimelineSet construction time.
|
|
933
947
|
*
|
|
934
948
|
* @param event - the event to check whether it belongs to this timeline set.
|
|
949
|
+
* @param batch - other events being added alongside, in which the event's parent may be found
|
|
935
950
|
* @throws Error if `room` was not set when constructing this timeline set.
|
|
936
951
|
* @returns whether the event belongs to this timeline set.
|
|
937
952
|
*/
|
|
938
|
-
public canContain(event: MatrixEvent): boolean {
|
|
953
|
+
public canContain(event: MatrixEvent, batch?: MatrixEvent[]): boolean {
|
|
939
954
|
if (!this.room) {
|
|
940
955
|
throw new Error(
|
|
941
956
|
"Cannot call `EventTimelineSet::canContain without a `room` set. " +
|
|
@@ -943,7 +958,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|
|
943
958
|
);
|
|
944
959
|
}
|
|
945
960
|
|
|
946
|
-
const { threadId, shouldLiveInRoom, shouldLiveInThread } = this.room.eventShouldLiveIn(event);
|
|
961
|
+
const { threadId, shouldLiveInRoom, shouldLiveInThread } = this.room.eventShouldLiveIn(event, batch);
|
|
947
962
|
|
|
948
963
|
if (this.thread) {
|
|
949
964
|
return this.thread.id === threadId;
|
|
@@ -40,7 +40,7 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
40
40
|
* Sticky event map is a nested map of:
|
|
41
41
|
* eventType -> `content.sticky_key sender` -> StickyMatrixEvent[]
|
|
42
42
|
*
|
|
43
|
-
* The events are ordered
|
|
43
|
+
* The events are ordered descendingly by expiry, tie breaking on the event ID, so that the first event
|
|
44
44
|
* in the array will always be the "current" one.
|
|
45
45
|
*/
|
|
46
46
|
private readonly stickyEventsMap = new Map<string, Map<string, StickyMatrixEvent[]>>();
|
|
@@ -54,25 +54,41 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
54
54
|
private nextStickyEventExpiryTs: number = Number.MAX_SAFE_INTEGER;
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* Sort two sticky events by
|
|
57
|
+
* Sort two sticky events by expiry, tie breaking on the event ID. This assumes the sticky events have the same
|
|
58
58
|
* `type`, `sticky_key` and `sender`.
|
|
59
59
|
* @returns A positive value if event A will expire sooner, or a negative value if event B will expire sooner.
|
|
60
60
|
*/
|
|
61
61
|
private static sortStickyEvent(eventA: StickyMatrixEvent, eventB: StickyMatrixEvent): number {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
// First, compare by expiry (`origin_server_ts + sticky.duration_ms`).
|
|
63
|
+
const expiryA = eventA.getTs() + eventA.unstableStickyInfo!.duration_ms;
|
|
64
|
+
const expiryB = eventB.getTs() + eventB.unstableStickyInfo!.duration_ms;
|
|
65
|
+
if (expiryB !== expiryA) {
|
|
66
|
+
return expiryB - expiryA;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
// Tie break on the highest lexicographical event ID.
|
|
70
|
+
const idA = eventA.getId() ?? "";
|
|
71
|
+
const idB = eventB.getId() ?? "";
|
|
72
|
+
if (idB > idA) {
|
|
69
73
|
return 1;
|
|
74
|
+
} else if (idB < idA) {
|
|
75
|
+
return -1;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
// This should fail as we've got corruption in our sticky array.
|
|
73
79
|
throw Error("Comparing two sticky events with the same event ID is not allowed.");
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Check whether a sticky event has expired.
|
|
84
|
+
* @param event The sticky event to check.
|
|
85
|
+
* @param now The current time in milliseconds since the epoch.
|
|
86
|
+
* @returns True if the event has expired, false otherwise.
|
|
87
|
+
*/
|
|
88
|
+
private static isExpired(event: StickyMatrixEvent, now: number): boolean {
|
|
89
|
+
return event.unstableStickyExpiresAt <= now;
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
/**
|
|
77
93
|
* Generate the correct key for an event to be found in the inner maps of `stickyEventsMap`.
|
|
78
94
|
* @param stickyKey The sticky key of an event.
|
|
@@ -133,7 +149,7 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
133
149
|
private addStickyEvent(event: MatrixEvent): { added: true; prevEvent?: StickyMatrixEvent } | { added: false } {
|
|
134
150
|
// The map is keyed on the event's type and sticky key, both of which are encrypted. Wait for the
|
|
135
151
|
// event to be decrypted, otherwise it would be filed under `m.room.encrypted` with no sticky key.
|
|
136
|
-
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
|
|
152
|
+
if (event.isBeingDecrypted() || event.shouldAttemptDecryption() || event.isDecryptionFailure()) {
|
|
137
153
|
this.addStickyEventOnceDecrypted(event);
|
|
138
154
|
return { added: false };
|
|
139
155
|
}
|
|
@@ -147,10 +163,12 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
147
163
|
if (event.unstableStickyExpiresAt === undefined) {
|
|
148
164
|
throw new Error(`${event.getId()} is missing msc4354_sticky.duration_ms`);
|
|
149
165
|
}
|
|
166
|
+
const stickyEvent = event as StickyMatrixEvent;
|
|
167
|
+
|
|
150
168
|
const sender = event.getSender();
|
|
151
169
|
const type = event.getType();
|
|
152
170
|
assertIsUserId(sender);
|
|
153
|
-
if (
|
|
171
|
+
if (RoomStickyEventsStore.isExpired(stickyEvent, Date.now())) {
|
|
154
172
|
logger.info("ignored sticky event with older expiration time than current time", stickyKey);
|
|
155
173
|
return { added: false };
|
|
156
174
|
}
|
|
@@ -161,8 +179,6 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
161
179
|
throw new Error("Expected sender to start with @");
|
|
162
180
|
}
|
|
163
181
|
|
|
164
|
-
const stickyEvent = event as StickyMatrixEvent;
|
|
165
|
-
|
|
166
182
|
if (stickyKey === undefined) {
|
|
167
183
|
this.unkeyedStickyEvents.add(stickyEvent);
|
|
168
184
|
// Recalculate the next expiry time.
|
|
@@ -269,7 +285,7 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
269
285
|
for (const [eventType, innerEvents] of this.stickyEventsMap.entries()) {
|
|
270
286
|
for (const [innerMapKey, [currentEvent, ...previousEvents]] of innerEvents) {
|
|
271
287
|
// we only added items with `sticky` into this map so we can assert non-null here
|
|
272
|
-
if (now
|
|
288
|
+
if (RoomStickyEventsStore.isExpired(currentEvent, now)) {
|
|
273
289
|
logger.debug("Expiring sticky event", currentEvent.getId());
|
|
274
290
|
removedEvents.push(currentEvent);
|
|
275
291
|
this.stickyEventsMap.get(eventType)!.delete(innerMapKey);
|
|
@@ -279,7 +295,7 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
279
295
|
.get(eventType)!
|
|
280
296
|
.set(innerMapKey, [
|
|
281
297
|
currentEvent,
|
|
282
|
-
...previousEvents.filter((e) => e
|
|
298
|
+
...previousEvents.filter((e) => !RoomStickyEventsStore.isExpired(e, now)),
|
|
283
299
|
]);
|
|
284
300
|
// If not removing the event, check to see if it's the next lowest expiry.
|
|
285
301
|
this.nextStickyEventExpiryTs = Math.min(
|
|
@@ -294,7 +310,7 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
294
310
|
}
|
|
295
311
|
}
|
|
296
312
|
for (const event of this.unkeyedStickyEvents) {
|
|
297
|
-
if (now
|
|
313
|
+
if (RoomStickyEventsStore.isExpired(event, now)) {
|
|
298
314
|
logger.debug("Expiring sticky event", event.getId());
|
|
299
315
|
this.unkeyedStickyEvents.delete(event);
|
|
300
316
|
removedEvents.push(event);
|
|
@@ -328,6 +344,9 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
328
344
|
|
|
329
345
|
// Handle unkeyedStickyEvents first since it's *quick*.
|
|
330
346
|
const redactEventId = typeof redactedEvent === "string" ? redactedEvent : redactedEvent.getId();
|
|
347
|
+
if (!redactEventId) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
331
350
|
for (const event of this.unkeyedStickyEvents) {
|
|
332
351
|
if (event.getId() === redactEventId) {
|
|
333
352
|
this.unkeyedStickyEvents.delete(event);
|
|
@@ -339,60 +358,75 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
339
358
|
// Faster method of finding the event since we have the event cached.
|
|
340
359
|
if (typeof redactedEvent !== "string" && !redactedEvent.isRedacted()) {
|
|
341
360
|
const stickyKey = redactedEvent.getContent().msc4354_sticky_key;
|
|
342
|
-
if (typeof stickyKey !== "string"
|
|
361
|
+
if (typeof stickyKey !== "string") {
|
|
343
362
|
return; // Not a sticky event.
|
|
344
363
|
}
|
|
345
|
-
const eventType = redactedEvent.getType();
|
|
346
364
|
const sender = redactedEvent.getSender();
|
|
347
365
|
assertIsUserId(sender);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const [currentEvent, ...previousEvents] = innerMap.get(mapKey) ?? [];
|
|
354
|
-
if (!currentEvent) {
|
|
355
|
-
// No event current in the map so ignore.
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
logger.debug(`Redaction for ${redactEventId} under sticky key ${stickyKey}`);
|
|
359
|
-
// Revert to previous state, taking care to skip any other redacted events.
|
|
360
|
-
const newEvents = previousEvents.filter((e) => !e.isRedacted()).sort(RoomStickyEventsStore.sortStickyEvent);
|
|
361
|
-
this.stickyEventsMap.get(eventType)?.set(mapKey, newEvents);
|
|
362
|
-
if (newEvents.length) {
|
|
363
|
-
this.emit(
|
|
364
|
-
RoomStickyEventsEvent.Update,
|
|
365
|
-
[],
|
|
366
|
-
[
|
|
367
|
-
{
|
|
368
|
-
// This looks confusing. This emits that the newer event
|
|
369
|
-
// has been redacted and the previous event has taken it's place.
|
|
370
|
-
previous: currentEvent,
|
|
371
|
-
current: newEvents[0],
|
|
372
|
-
},
|
|
373
|
-
],
|
|
374
|
-
[],
|
|
375
|
-
);
|
|
376
|
-
} else {
|
|
377
|
-
// We did not find a previous event, so just expire.
|
|
378
|
-
innerMap.delete(mapKey);
|
|
379
|
-
if (innerMap.size === 0) {
|
|
380
|
-
this.stickyEventsMap.delete(eventType);
|
|
381
|
-
}
|
|
382
|
-
this.emit(RoomStickyEventsEvent.Update, [], [], [currentEvent]);
|
|
383
|
-
}
|
|
366
|
+
this.removeKeyedStickyEvent(
|
|
367
|
+
redactedEvent.getType(),
|
|
368
|
+
RoomStickyEventsStore.stickyMapKey(stickyKey, sender),
|
|
369
|
+
redactEventId,
|
|
370
|
+
);
|
|
384
371
|
return;
|
|
385
372
|
}
|
|
386
373
|
|
|
387
374
|
// We only know the event ID of the redacted event, so we need to
|
|
388
375
|
// traverse the map to find our event.
|
|
389
|
-
for (const innerMap of this.stickyEventsMap
|
|
390
|
-
for (const [
|
|
391
|
-
if (
|
|
392
|
-
|
|
376
|
+
for (const [eventType, innerMap] of this.stickyEventsMap) {
|
|
377
|
+
for (const [mapKey, events] of innerMap) {
|
|
378
|
+
if (events.some((e) => e.getId() === redactEventId)) {
|
|
379
|
+
// Found the event.
|
|
380
|
+
this.removeKeyedStickyEvent(eventType, mapKey, redactEventId);
|
|
381
|
+
return;
|
|
393
382
|
}
|
|
394
|
-
|
|
395
|
-
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Remove a redacted event from an entry of the keyed sticky map, emitting
|
|
389
|
+
* `RoomEvent.StickyEvents` if this changes the "current" event of that entry.
|
|
390
|
+
* @param eventType The event `type` of the redacted event.
|
|
391
|
+
* @param mapKey The inner map key of the redacted event, see {@link stickyMapKey}.
|
|
392
|
+
* @param redactEventId The ID of the redacted event.
|
|
393
|
+
*/
|
|
394
|
+
private removeKeyedStickyEvent(eventType: string, mapKey: string, redactEventId: string): void {
|
|
395
|
+
const innerMap = this.stickyEventsMap.get(eventType);
|
|
396
|
+
const events = innerMap?.get(mapKey);
|
|
397
|
+
if (!innerMap || !events) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const index = events.findIndex((e) => e.getId() === redactEventId);
|
|
401
|
+
if (index === -1) {
|
|
402
|
+
// The event is not in the map so ignore.
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
logger.debug(`Redaction for ${redactEventId} under sticky map key ${mapKey}`);
|
|
406
|
+
|
|
407
|
+
// The current event always sits at index 0.
|
|
408
|
+
const currentEvent = events[0];
|
|
409
|
+
const currentEventRedacted = index === 0;
|
|
410
|
+
|
|
411
|
+
// Drop the redacted event and any other events that may have already been redacted before.
|
|
412
|
+
const newEvents = events.filter((e) => e.getId() !== redactEventId && !e.isRedacted());
|
|
413
|
+
if (newEvents.length) {
|
|
414
|
+
innerMap.set(mapKey, newEvents);
|
|
415
|
+
} else {
|
|
416
|
+
innerMap.delete(mapKey);
|
|
417
|
+
if (innerMap.size === 0) {
|
|
418
|
+
this.stickyEventsMap.delete(eventType);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Emit an update if the current event was redacted.
|
|
423
|
+
if (currentEventRedacted) {
|
|
424
|
+
if (newEvents.length) {
|
|
425
|
+
// The current event was redacted and most recent superseded event takes its place.
|
|
426
|
+
this.emit(RoomStickyEventsEvent.Update, [], [{ previous: currentEvent, current: newEvents[0] }], []);
|
|
427
|
+
} else {
|
|
428
|
+
// The current event was redacted and no superseded events remain to replace it.
|
|
429
|
+
this.emit(RoomStickyEventsEvent.Update, [], [], [currentEvent]);
|
|
396
430
|
}
|
|
397
431
|
}
|
|
398
432
|
}
|