matrix-js-sdk 41.0.0 → 41.1.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/@types/event.d.ts +1 -1
  3. package/lib/@types/event.d.ts.map +1 -1
  4. package/lib/@types/event.js +1 -1
  5. package/lib/@types/event.js.map +1 -1
  6. package/lib/client.d.ts.map +1 -1
  7. package/lib/client.js +250 -246
  8. package/lib/client.js.map +1 -1
  9. package/lib/crypto-api/index.d.ts +13 -2
  10. package/lib/crypto-api/index.d.ts.map +1 -1
  11. package/lib/crypto-api/index.js +11 -0
  12. package/lib/crypto-api/index.js.map +1 -1
  13. package/lib/logger.d.ts +5 -5
  14. package/lib/logger.d.ts.map +1 -1
  15. package/lib/logger.js.map +1 -1
  16. package/lib/matrixrtc/CallMembership.d.ts +49 -145
  17. package/lib/matrixrtc/CallMembership.d.ts.map +1 -1
  18. package/lib/matrixrtc/CallMembership.js +157 -265
  19. package/lib/matrixrtc/CallMembership.js.map +1 -1
  20. package/lib/matrixrtc/EncryptionManager.d.ts +1 -85
  21. package/lib/matrixrtc/EncryptionManager.d.ts.map +1 -1
  22. package/lib/matrixrtc/EncryptionManager.js +0 -317
  23. package/lib/matrixrtc/EncryptionManager.js.map +1 -1
  24. package/lib/matrixrtc/MatrixRTCSession.d.ts +18 -22
  25. package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
  26. package/lib/matrixrtc/MatrixRTCSession.js +48 -76
  27. package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
  28. package/lib/matrixrtc/MatrixRTCSessionManager.d.ts +2 -1
  29. package/lib/matrixrtc/MatrixRTCSessionManager.d.ts.map +1 -1
  30. package/lib/matrixrtc/MatrixRTCSessionManager.js +3 -2
  31. package/lib/matrixrtc/MatrixRTCSessionManager.js.map +1 -1
  32. package/lib/matrixrtc/MembershipManager.d.ts +10 -4
  33. package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
  34. package/lib/matrixrtc/MembershipManager.js +10 -4
  35. package/lib/matrixrtc/MembershipManager.js.map +1 -1
  36. package/lib/matrixrtc/RTCEncryptionManager.d.ts +6 -7
  37. package/lib/matrixrtc/RTCEncryptionManager.d.ts.map +1 -1
  38. package/lib/matrixrtc/RTCEncryptionManager.js +4 -7
  39. package/lib/matrixrtc/RTCEncryptionManager.js.map +1 -1
  40. package/lib/matrixrtc/index.d.ts +1 -0
  41. package/lib/matrixrtc/index.d.ts.map +1 -1
  42. package/lib/matrixrtc/index.js.map +1 -1
  43. package/lib/matrixrtc/membershipData/common.d.ts +8 -0
  44. package/lib/matrixrtc/membershipData/common.d.ts.map +1 -0
  45. package/lib/matrixrtc/membershipData/common.js +26 -0
  46. package/lib/matrixrtc/membershipData/common.js.map +1 -0
  47. package/lib/matrixrtc/membershipData/index.d.ts +4 -0
  48. package/lib/matrixrtc/membershipData/index.d.ts.map +1 -0
  49. package/lib/matrixrtc/membershipData/index.js +20 -0
  50. package/lib/matrixrtc/membershipData/index.js.map +1 -0
  51. package/lib/matrixrtc/membershipData/rtc.d.ts +33 -0
  52. package/lib/matrixrtc/membershipData/rtc.d.ts.map +1 -0
  53. package/lib/matrixrtc/membershipData/rtc.js +137 -0
  54. package/lib/matrixrtc/membershipData/rtc.js.map +1 -0
  55. package/lib/matrixrtc/membershipData/session.d.ts +77 -0
  56. package/lib/matrixrtc/membershipData/session.d.ts.map +1 -0
  57. package/lib/matrixrtc/membershipData/session.js +62 -0
  58. package/lib/matrixrtc/membershipData/session.js.map +1 -0
  59. package/lib/matrixrtc/types.d.ts +23 -0
  60. package/lib/matrixrtc/types.d.ts.map +1 -1
  61. package/lib/matrixrtc/types.js +9 -1
  62. package/lib/matrixrtc/types.js.map +1 -1
  63. package/lib/matrixrtc/utils.d.ts +11 -1
  64. package/lib/matrixrtc/utils.d.ts.map +1 -1
  65. package/lib/matrixrtc/utils.js +24 -1
  66. package/lib/matrixrtc/utils.js.map +1 -1
  67. package/lib/rust-crypto/rust-crypto.d.ts.map +1 -1
  68. package/lib/rust-crypto/rust-crypto.js +2 -2
  69. package/lib/rust-crypto/rust-crypto.js.map +1 -1
  70. package/package.json +5 -7
  71. package/src/@types/event.ts +2 -2
  72. package/src/client.ts +5 -3
  73. package/src/crypto-api/index.ts +17 -2
  74. package/src/logger.ts +5 -5
  75. package/src/matrixrtc/CallMembership.ts +159 -373
  76. package/src/matrixrtc/EncryptionManager.ts +1 -417
  77. package/src/matrixrtc/MatrixRTCSession.ts +82 -122
  78. package/src/matrixrtc/MatrixRTCSessionManager.ts +5 -3
  79. package/src/matrixrtc/MembershipManager.ts +14 -17
  80. package/src/matrixrtc/RTCEncryptionManager.ts +7 -10
  81. package/src/matrixrtc/index.ts +1 -0
  82. package/src/matrixrtc/membershipData/common.ts +27 -0
  83. package/src/matrixrtc/membershipData/index.ts +19 -0
  84. package/src/matrixrtc/membershipData/rtc.ts +156 -0
  85. package/src/matrixrtc/membershipData/session.ts +146 -0
  86. package/src/matrixrtc/types.ts +27 -1
  87. package/src/matrixrtc/utils.ts +24 -2
  88. package/src/rust-crypto/rust-crypto.ts +4 -1
  89. package/lib/matrixrtc/RoomKeyTransport.d.ts +0 -25
  90. package/lib/matrixrtc/RoomKeyTransport.d.ts.map +0 -1
  91. package/lib/matrixrtc/RoomKeyTransport.js +0 -152
  92. package/lib/matrixrtc/RoomKeyTransport.js.map +0 -1
  93. package/src/matrixrtc/RoomKeyTransport.ts +0 -189
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2023 The Matrix.org Foundation C.I.C.
2
+ Copyright 2023-2026 The Matrix.org Foundation C.I.C.
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -14,16 +14,20 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import { MXID_PATTERN } from "../models/room-member.ts";
18
17
  import { deepCompare } from "../utils.ts";
19
- import { type LivekitFocusSelection } from "./LivekitTransport.ts";
20
- import { slotDescriptionToId, slotIdToDescription, type SlotDescription } from "./MatrixRTCSession.ts";
21
- import type { RTCCallIntent, Transport } from "./types.ts";
22
- import { type MatrixEvent, type IContent } from "../models/event.ts";
23
- import { type RelationType } from "../@types/event.ts";
24
- import { sha256 } from "../digest.ts";
25
- import { encodeUnpaddedBase64 } from "../base64.ts";
26
- import { type Logger } from "../logger.ts";
18
+ import { type RTCCallIntent, type Transport, type SlotDescription } from "./types.ts";
19
+ import { type MatrixEvent } from "../models/event.ts";
20
+ import { type Logger, logger } from "../logger.ts";
21
+ import { computeSlotId, slotIdToDescription } from "./utils.ts";
22
+ import {
23
+ checkRtcMembershipData,
24
+ computeRtcIdentityRaw,
25
+ type RtcMembershipData,
26
+ checkSessionsMembershipData,
27
+ type SessionMembershipData,
28
+ MatrixRTCMembershipParseError,
29
+ } from "./membershipData/index.ts";
30
+ import { EventType } from "../@types/event.ts";
27
31
 
28
32
  /**
29
33
  * The default duration in milliseconds that a membership is considered valid for.
@@ -32,331 +36,118 @@ import { type Logger } from "../logger.ts";
32
36
  */
33
37
  export const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4;
34
38
 
35
- type CallScope = "m.room" | "m.user";
36
- type Member = {
37
- user_id: string;
38
- device_id: string;
39
- /**
40
- * The id used on the media backend.
41
- * (With livekit this is the participant identity on the LK SFU)
42
- * This can be a UUID but right now it is `${this.matrixEventData.sender}:${data.device_id}`.
43
- */
44
- id: string;
45
- };
46
-
47
- export interface RtcMembershipData {
48
- "slot_id": string;
49
- "member": Member;
50
- "m.relates_to"?: {
51
- event_id: string;
52
- rel_type: RelationType.Reference;
53
- };
54
- "application": {
55
- type: string;
56
- // other application specific keys
57
- [key: string]: unknown;
58
- };
59
- "rtc_transports": Transport[];
60
- "versions": string[];
61
- "msc4354_sticky_key"?: string;
62
- "sticky_key"?: string;
63
- }
64
-
65
- const checkRtcMembershipData = (
66
- data: IContent,
67
- errors: string[],
68
- referenceUserId: string,
69
- ): data is RtcMembershipData => {
70
- const prefix = " - ";
71
-
72
- // required fields
73
- if (typeof data.slot_id !== "string") {
74
- errors.push(prefix + "slot_id must be string");
75
- } else {
76
- if (data.slot_id.split("#").length !== 2) errors.push(prefix + 'slot_id must include exactly one "#"');
77
- }
78
- if (typeof data.member !== "object" || data.member === null) {
79
- errors.push(prefix + "member must be an object");
80
- } else {
81
- if (typeof data.member.user_id !== "string") errors.push(prefix + "member.user_id must be string");
82
- else if (!MXID_PATTERN.test(data.member.user_id)) errors.push(prefix + "member.user_id must be a valid mxid");
83
- // This is not what the spec enforces but there currently are no rules what power levels are required to
84
- // send a m.rtc.member event for a other user. So we add this check for simplicity and to avoid possible attacks until there
85
- // is a proper definition when this is allowed.
86
- else if (data.member.user_id !== referenceUserId) errors.push(prefix + "member.user_id must match the sender");
87
- if (typeof data.member.device_id !== "string") errors.push(prefix + "member.device_id must be string");
88
- if (typeof data.member.id !== "string") errors.push(prefix + "member.id must be string");
89
- }
90
- if (typeof data.application !== "object" || data.application === null) {
91
- errors.push(prefix + "application must be an object");
92
- } else {
93
- if (typeof data.application.type !== "string") {
94
- errors.push(prefix + "application.type must be a string");
95
- } else {
96
- if (data.application.type.includes("#")) errors.push(prefix + 'application.type must not include "#"');
97
- }
98
- }
99
- if (data.rtc_transports === undefined || !Array.isArray(data.rtc_transports)) {
100
- errors.push(prefix + "rtc_transports must be an array");
101
- } else {
102
- // validate that each transport has at least a string 'type'
103
- for (const t of data.rtc_transports) {
104
- if (typeof t !== "object" || t === null || typeof (t as any).type !== "string") {
105
- errors.push(prefix + "rtc_transports entries must be objects with a string type");
106
- break;
107
- }
108
- }
109
- }
110
- if (data.versions === undefined || !Array.isArray(data.versions)) {
111
- errors.push(prefix + "versions must be an array");
112
- } else if (!data.versions.every((v) => typeof v === "string")) {
113
- errors.push(prefix + "versions must be an array of strings");
114
- }
115
-
116
- // optional fields
117
- if ((data.sticky_key ?? data.msc4354_sticky_key) === undefined) {
118
- errors.push(prefix + "sticky_key or msc4354_sticky_key must be a defined");
119
- }
120
- if (data.sticky_key !== undefined && typeof data.sticky_key !== "string") {
121
- errors.push(prefix + "sticky_key must be a string");
122
- }
123
- if (data.msc4354_sticky_key !== undefined && typeof data.msc4354_sticky_key !== "string") {
124
- errors.push(prefix + "msc4354_sticky_key must be a string");
125
- }
126
- if (
127
- data.sticky_key !== undefined &&
128
- data.msc4354_sticky_key !== undefined &&
129
- data.sticky_key !== data.msc4354_sticky_key
130
- ) {
131
- errors.push(prefix + "sticky_key and msc4354_sticky_key must be equal if both are defined");
132
- }
133
- if (data["m.relates_to"] !== undefined) {
134
- const rel = data["m.relates_to"] as RtcMembershipData["m.relates_to"];
135
- if (typeof rel !== "object" || rel === null) {
136
- errors.push(prefix + "m.relates_to must be an object if provided");
137
- } else {
138
- if (typeof rel.event_id !== "string") errors.push(prefix + "m.relates_to.event_id must be a string");
139
- if (rel.rel_type !== "m.reference") errors.push(prefix + "m.relates_to.rel_type must be m.reference");
140
- }
141
- }
142
-
143
- return errors.length === 0;
144
- };
145
-
146
39
  /**
147
- * MSC4143 (MatrixRTC) session membership data.
148
- * Represents the `session` in the memberships section of an m.call.member event as it is on the wire.
149
- **/
150
- export type SessionMembershipData = {
151
- /**
152
- * The RTC application defines the type of the RTC session.
153
- */
154
- "application": string;
155
-
156
- /**
157
- * The id of this session.
158
- * A session can never span over multiple rooms so this id is to distinguish between
159
- * multiple session in one room. A room wide session that is not associated with a user,
160
- * and therefore immune to creation race conflicts, uses the `call_id: ""`.
161
- */
162
- "call_id": string;
163
-
164
- /**
165
- * The Matrix device ID of this session. A single user can have multiple sessions on different devices.
166
- */
167
- "device_id": string;
168
-
169
- /**
170
- * The focus selection system this user/membership is using.
171
- */
172
- "focus_active": LivekitFocusSelection;
173
-
174
- /**
175
- * A list of possible foci this user knows about. One of them might be used based on the focus_active
176
- * selection system.
177
- */
178
- "foci_preferred": Transport[];
179
-
40
+ * Describes the source event type that provided the membership data.
41
+ */
42
+ enum MembershipKind {
180
43
  /**
181
- * Optional field that contains the creation of the session. If it is undefined the creation
182
- * is the `origin_server_ts` of the event itself. For updates to the event this property tracks
183
- * the `origin_server_ts` of the initial join event.
184
- * - If it is undefined it can be interpreted as a "Join".
185
- * - If it is defined it can be interpreted as an "Update"
44
+ * The modern MSC4143 format event.
186
45
  */
187
- "created_ts"?: number;
188
-
189
- // Application specific data
190
-
46
+ RTC = "rtc",
191
47
  /**
192
- * If the `application` = `"m.call"` this defines if it is a room or user owned call.
193
- * There can always be one room scoped call but multiple user owned calls (breakout sessions)
48
+ * The legacy call event type.
194
49
  */
195
- "scope"?: CallScope;
50
+ Session = "session",
51
+ }
196
52
 
197
- /**
198
- * Optionally we allow to define a delta to the `created_ts` that defines when the event is expired/invalid.
199
- * This should be set to multiple hours. The only reason it exist is to deal with failed delayed events.
200
- * (for example caused by a homeserver crashes)
201
- **/
202
- "expires"?: number;
53
+ type MembershipData =
54
+ | { kind: MembershipKind.RTC; data: RtcMembershipData }
55
+ | { kind: MembershipKind.Session; data: SessionMembershipData };
203
56
 
57
+ type LimitedEvent = Pick<MatrixEvent, "getId" | "getSender" | "getTs" | "getType" | "getContent">;
58
+ // TODO: Rename to RtcMembership once we removed the legacy SessionMembership is removed, to avoid confusion.
59
+ export class CallMembership {
204
60
  /**
205
- * The intent of the call from the perspective of this user. This may be an audio call, video call or
206
- * something else.
207
- */
208
- "m.call.intent"?: RTCCallIntent;
209
- /**
210
- * The sticky key in case of a sticky event. This string encodes the application + device_id indicating the used slot + device.
61
+ * Parse the membershipdata from a call membership event.
62
+ * @param matrixEvent The Matrix event to read.
63
+ * @returns MembershipData in either MembershipKind.RTC or MembershipKind.Session format.
64
+ * @throws If the content is neither format.
211
65
  */
212
- "msc4354_sticky_key"?: string;
66
+ public static membershipDataFromMatrixEvent(matrixEvent: LimitedEvent): MembershipData {
67
+ const sender = matrixEvent.getSender();
68
+ const evType = matrixEvent.getType();
69
+ const data = matrixEvent.getContent();
70
+ if (sender === undefined) throw new Error("matrixEvent is missing sender field");
71
+ try {
72
+ // Event types are strictly checked here.
73
+ if (evType === EventType.RTCMembership && checkRtcMembershipData(data, sender)) {
74
+ return { kind: MembershipKind.RTC, data };
75
+ } else if (evType === EventType.GroupCallMemberPrefix && checkSessionsMembershipData(data)) {
76
+ return { kind: MembershipKind.Session, data };
77
+ } else {
78
+ throw Error(`'${evType} is not a known call membership type`);
79
+ }
80
+ } catch (ex) {
81
+ if (ex instanceof MatrixRTCMembershipParseError) {
82
+ logger.debug("CallMembership.MatrixRTCMembershipParseError provided invalid data", data);
83
+ }
84
+ throw ex;
85
+ }
86
+ }
213
87
 
214
88
  /**
215
- * The id used on the media backend.
216
- * (With livekit this is the participant identity on the LK SFU)
217
- * This can be a UUID but right now it is `${this.matrixEventData.sender}:${data.device_id}`.
218
- *
219
- * It is compleatly valid to not set this field. Other clients will treat `undefined` as `${this.matrixEventData.sender}:${data.device_id}`
89
+ * Parse the contents of a MatrixEvent and create a CallMembership instance.
90
+ * @param matrixEvent The Matrix event to read.
220
91
  */
221
- "membershipID"?: string;
222
- };
223
-
224
- const checkSessionsMembershipData = (data: IContent, errors: string[]): data is SessionMembershipData => {
225
- const prefix = " - ";
226
- if (typeof data.device_id !== "string") errors.push(prefix + "device_id must be string");
227
- if (typeof data.call_id !== "string") errors.push(prefix + "call_id must be string");
228
- if (typeof data.application !== "string") errors.push(prefix + "application must be a string");
229
- if (typeof data.focus_active?.type !== "string") errors.push(prefix + "focus_active.type must be a string");
230
- if (data.focus_active === undefined) {
231
- errors.push(prefix + "focus_active has an invalid type");
92
+ public static async parseFromEvent(matrixEvent: LimitedEvent): Promise<CallMembership> {
93
+ const membershipData: MembershipData = this.membershipDataFromMatrixEvent(matrixEvent);
94
+ const rtcBackendIdentity =
95
+ membershipData.kind === MembershipKind.RTC
96
+ ? await computeRtcIdentityRaw(
97
+ membershipData.data.member.user_id,
98
+ membershipData.data.member.device_id,
99
+ membershipData.data.member.id,
100
+ )
101
+ : `${matrixEvent.getSender()}:${membershipData.data.device_id}`;
102
+ return new CallMembership(matrixEvent, membershipData, rtcBackendIdentity);
232
103
  }
233
- if (
234
- data.foci_preferred !== undefined &&
235
- !(
236
- Array.isArray(data.foci_preferred) &&
237
- data.foci_preferred.every(
238
- (f: Transport) => typeof f === "object" && f !== null && typeof f.type === "string",
239
- )
240
- )
241
- ) {
242
- errors.push(prefix + "foci_preferred must be an array of transport objects");
243
- }
244
- // optional parameters
245
- if (data.created_ts !== undefined && typeof data.created_ts !== "number") {
246
- errors.push(prefix + "created_ts must be number");
247
- }
248
-
249
- // application specific data (we first need to check if they exist)
250
- if (data.scope !== undefined && typeof data.scope !== "string") errors.push(prefix + "scope must be string");
251
-
252
- if (data["m.call.intent"] !== undefined && typeof data["m.call.intent"] !== "string") {
253
- errors.push(prefix + "m.call.intent must be a string");
254
- }
255
-
256
- return errors.length === 0;
257
- };
258
104
 
259
- type MembershipData = { kind: "rtc"; data: RtcMembershipData } | { kind: "session"; data: SessionMembershipData };
260
- // TODO: Rename to RtcMembership once we removed the legacy SessionMembership from this file.
261
- export class CallMembership {
262
105
  public static equal(a?: CallMembership, b?: CallMembership): boolean {
263
106
  return deepCompare(a?.membershipData, b?.membershipData);
264
107
  }
265
108
 
266
- private logger?: Logger;
109
+ private logger: Logger;
110
+
267
111
  /** The parsed data from the Matrix event.
268
112
  * To access checked eventId and sender from the matrixEvent.
269
113
  * Class construction will fail if these values cannot get obtained. */
270
- private readonly matrixEventData: { eventId: string; sender: string; ts: number };
114
+ private readonly matrixEventData: { eventId: string; sender: string };
271
115
 
116
+ /**
117
+ * Use `parseFromEvent`.
118
+ * Constructor should only be used by tests.
119
+ * @private
120
+ * @param matrixEvent
121
+ * @param membershipData
122
+ * @param rtcBackendIdentity
123
+ */
272
124
  public constructor(
273
- /** The required parts of the Matrix event that this membership is based on */
274
- matrixEvent: Pick<MatrixEvent, "getId" | "getSender" | "getTs">,
275
-
276
- /**
277
- * The type checked membership data {data: (content of the matrix event), kind: (type hint)}
278
- *
279
- */
125
+ /** The Matrix event that this membership is based on */
126
+ private readonly matrixEvent: LimitedEvent,
280
127
  private readonly membershipData: MembershipData,
281
-
282
- /**
283
- *
284
- * Anonymized identity to use with the RTC backend.
285
- *
286
- * The rtcBackendIdentity is a hashed version of all the identity parts:
287
- * `sha256(${this.userId}|${this.deviceId}|${this.memberId})`
288
- *
289
- * It is used to anonymize the identity of the user in the RTC backend.
290
- */
291
128
  public readonly rtcBackendIdentity: string,
292
- /**
293
- * The constructor will automatically create a properly tagged child logger instance.
294
- */
295
- logger?: Logger,
296
129
  ) {
297
- const [eventId, sender, ts] = [matrixEvent.getId(), matrixEvent.getSender(), matrixEvent.getTs()];
298
- if (eventId === undefined) throw new Error("parentEvent is missing eventId field");
299
- if (sender === undefined) throw new Error("parentEvent is missing sender field");
300
-
301
- this.matrixEventData = { eventId, sender, ts };
302
-
303
- this.logger = logger?.getChild(`[CallMembership ${sender}:${this.deviceId}]`);
304
- }
305
-
306
- /**
307
- * sha256(`${this.userId}|${this.deviceId}|${this.memberId}`) for sticky events (kind = rtc)
308
- * `${this.userId}:${this.deviceId}` for state events (kind = session)
309
- */
310
- public static async computeRtcBackendIdentity(
311
- matrixEvent: Pick<MatrixEvent, "getSender">,
312
- membershipData: MembershipData,
313
- ): Promise<string> {
314
- const { kind, data } = membershipData;
315
- switch (kind) {
316
- case "rtc": {
317
- return CallMembership.computeRtcIdentityRaw(data.member.user_id, data.member.device_id, data.member.id);
318
- }
319
- case "session":
320
- return `${matrixEvent.getSender()}:${data.device_id}`;
321
- }
322
- }
323
-
324
- public static async computeRtcIdentityRaw(userId: string, deviceId: string, memberId: string): Promise<string> {
325
- return encodeUnpaddedBase64(await sha256(`${userId}|${deviceId}|${memberId}`));
326
- }
327
-
328
- public static membershipDataFromMatrixEvent(matrixEvent: MatrixEvent): MembershipData {
329
- const [eventId, sender, content] = [matrixEvent.getId(), matrixEvent.getSender(), matrixEvent.getContent()];
130
+ const eventId = matrixEvent.getId();
131
+ const sender = matrixEvent.getSender();
330
132
 
331
133
  if (eventId === undefined) throw new Error("parentEvent is missing eventId field");
332
134
  if (sender === undefined) throw new Error("parentEvent is missing sender field");
333
135
 
334
- const sessionErrors: string[] = [];
335
- const rtcErrors: string[] = [];
336
- if (checkSessionsMembershipData(content, sessionErrors)) {
337
- return { kind: "session", data: content };
338
- } else if (checkRtcMembershipData(content, rtcErrors, sender)) {
339
- return { kind: "rtc", data: content };
340
- } else {
341
- const details =
342
- sessionErrors.length < rtcErrors.length
343
- ? `Does not match MSC4143 m.call.member:\n${sessionErrors.join("\n")}\n\n`
344
- : `Does not match MSC4143 m.rtc.member:\n${rtcErrors.join("\n")}\n\n`;
345
- const json = "\nevent:\n" + JSON.stringify(content).replaceAll('"', "'");
346
- throw Error(`unknown CallMembership data.\n` + details + json);
347
- }
136
+ this.logger = logger.getChild(`[CallMembership ${sender}:${this.deviceId}]`);
137
+ this.matrixEventData = { eventId, sender };
348
138
  }
349
139
 
350
140
  /** @deprecated use userId instead */
351
141
  public get sender(): string {
352
142
  return this.userId;
353
143
  }
144
+
354
145
  public get userId(): string {
355
146
  const { kind, data } = this.membershipData;
356
147
  switch (kind) {
357
- case "rtc":
148
+ case MembershipKind.RTC:
358
149
  return data.member.user_id;
359
- case "session":
150
+ case MembershipKind.Session:
360
151
  default:
361
152
  return this.matrixEventData.sender;
362
153
  }
@@ -374,11 +165,11 @@ export class CallMembership {
374
165
  const { kind, data } = this.membershipData;
375
166
  if (data.application === "m.call") {
376
167
  switch (kind) {
377
- case "rtc":
168
+ case MembershipKind.RTC:
378
169
  return data.slot_id;
379
- case "session":
170
+ case MembershipKind.Session:
380
171
  default: {
381
- const [application, id] = [this.application, data.call_id];
172
+ const [application, id] = [data.application, data.call_id];
382
173
 
383
174
  // INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
384
175
  // The spec got changed to use `"ROOM"` instead of `""` empyt string for the implicit default call.
@@ -400,7 +191,7 @@ export class CallMembership {
400
191
  } else {
401
192
  compatibilityAdaptedId = id;
402
193
  }
403
- return slotDescriptionToId({
194
+ return computeSlotId({
404
195
  application,
405
196
  id: compatibilityAdaptedId,
406
197
  });
@@ -412,89 +203,82 @@ export class CallMembership {
412
203
  // This is what the function should look like for any other application that did not
413
204
  // go through a `""`=> `"ROOM"` rename
414
205
  switch (kind) {
415
- case "rtc":
206
+ case MembershipKind.RTC:
416
207
  return data.slot_id;
417
- case "session":
418
- default: {
419
- const [application, id] = [this.application, data.call_id];
420
- return slotDescriptionToId({ application, id });
421
- }
208
+ case MembershipKind.Session:
209
+ default:
210
+ return computeSlotId({ application: data.application, id: data.call_id });
422
211
  }
423
212
  }
424
213
 
425
214
  public get deviceId(): string {
426
215
  const { kind, data } = this.membershipData;
427
216
  switch (kind) {
428
- case "rtc":
217
+ case MembershipKind.RTC:
429
218
  return data.member.device_id;
430
- case "session":
219
+ case MembershipKind.Session:
431
220
  default:
432
221
  return data.device_id;
433
222
  }
434
223
  }
435
224
 
436
225
  public get callIntent(): RTCCallIntent | undefined {
437
- const { kind, data } = this.membershipData;
438
- switch (kind) {
439
- case "rtc": {
440
- const intent = data.application["m.call.intent"];
441
- if (typeof intent === "string") {
442
- return intent;
443
- }
444
- this.logger?.warn("RTC membership has invalid m.call.intent");
445
- return undefined;
446
- }
447
- case "session":
448
- default:
449
- return data["m.call.intent"];
226
+ const intent = this.applicationData["m.call.intent"];
227
+ if (typeof intent === "string") {
228
+ return intent;
450
229
  }
230
+ this.logger.warn("RTC membership has invalid m.call.intent");
231
+ return undefined;
451
232
  }
452
233
 
453
234
  /**
454
235
  * Parsed `slot_id` (format `{application}#{id}`) into its components (application and id).
455
236
  */
456
237
  public get slotDescription(): SlotDescription {
238
+ const { kind, data } = this.membershipData;
239
+ if (kind === MembershipKind.RTC) {
240
+ const id = data.slot_id.slice(`${data.application.type}#`.length);
241
+ return { application: data.application.type, id };
242
+ }
457
243
  return slotIdToDescription(this.slotId);
458
244
  }
459
245
 
246
+ /**
247
+ * The application `type`.
248
+ * @deprecated Use @see applicationData
249
+ */
460
250
  public get application(): string {
461
- const { kind, data } = this.membershipData;
462
- switch (kind) {
463
- case "rtc":
464
- return data.application.type;
465
- case "session":
466
- default:
467
- return data.application;
468
- }
251
+ return this.applicationData.type;
469
252
  }
253
+
254
+ /**
255
+ * Information about the application being used for the RTC session.
256
+ * May contain extra keys specific to the application.
257
+ */
470
258
  public get applicationData(): { type: string; [key: string]: unknown } {
471
259
  const { kind, data } = this.membershipData;
472
260
  switch (kind) {
473
- case "rtc":
261
+ case MembershipKind.RTC:
474
262
  return data.application;
475
- case "session":
263
+ case MembershipKind.Session:
476
264
  default:
265
+ // SessionData does not have application data as such. We return specific
266
+ // properties in use by other getters in this class, for compatibility.
477
267
  return { "type": data.application, "m.call.intent": data["m.call.intent"] };
478
268
  }
479
269
  }
480
270
 
481
271
  /** @deprecated scope is not used and will be removed in future versions. replaced by application specific types.*/
482
- public get scope(): CallScope | undefined {
272
+ public get scope(): SessionMembershipData["scope"] | undefined {
483
273
  const { kind, data } = this.membershipData;
484
274
  switch (kind) {
485
- case "rtc":
275
+ case MembershipKind.RTC:
486
276
  return undefined;
487
- case "session":
277
+ case MembershipKind.Session:
488
278
  default:
489
279
  return data.scope;
490
280
  }
491
281
  }
492
- /**
493
- * @deprecated renamed to `memberId`
494
- */
495
- public get membershipID(): string {
496
- return this.memberId;
497
- }
498
282
 
499
283
  /**
500
284
  * This computes the membership ID for the membership.
@@ -519,25 +303,33 @@ export class CallMembership {
519
303
  case "rtc":
520
304
  return data.member.id;
521
305
  case "session":
522
- default:
523
306
  return (
524
307
  // best case we have a client already publishing the right custom membershipId
525
308
  data.membershipID ??
526
309
  // alternativly we use the hard coded jwt id defuatl value (used until version 0.16.0)
527
310
  `${this.matrixEventData.sender}:${data.device_id}`
528
311
  );
312
+ default:
313
+ throw Error("Not possible to get memberID without knowing the membership event kind");
529
314
  }
530
315
  }
531
316
 
317
+ /**
318
+ * @deprecated renamed to `memberId`
319
+ */
320
+ public get membershipID(): string {
321
+ return this.memberId;
322
+ }
323
+
532
324
  public createdTs(): number {
533
325
  const { kind, data } = this.membershipData;
534
326
  switch (kind) {
535
- case "rtc":
327
+ case MembershipKind.RTC:
536
328
  // TODO we need to read the referenced (relation) event if available to get the real created_ts
537
- return this.matrixEventData.ts;
538
- case "session":
329
+ return this.matrixEvent.getTs();
330
+ case MembershipKind.Session:
539
331
  default:
540
- return data.created_ts ?? this.matrixEventData.ts;
332
+ return data.created_ts ?? this.matrixEvent.getTs();
541
333
  }
542
334
  }
543
335
 
@@ -548,9 +340,9 @@ export class CallMembership {
548
340
  public getAbsoluteExpiry(): number | undefined {
549
341
  const { kind, data } = this.membershipData;
550
342
  switch (kind) {
551
- case "rtc":
343
+ case MembershipKind.RTC:
552
344
  return undefined;
553
- case "session":
345
+ case MembershipKind.Session:
554
346
  default:
555
347
  // TODO: calculate this from the MatrixRTCSession join configuration directly
556
348
  return this.createdTs() + (data.expires ?? DEFAULT_EXPIRE_DURATION);
@@ -559,19 +351,20 @@ export class CallMembership {
559
351
 
560
352
  /**
561
353
  * @returns The number of milliseconds until the membership expires or undefined if applicable
354
+ * @deprecated Not used by RTC events.
562
355
  */
563
356
  public getMsUntilExpiry(): number | undefined {
564
357
  const { kind } = this.membershipData;
565
- switch (kind) {
566
- case "rtc":
567
- return undefined;
568
- case "session":
569
- default:
358
+ if (kind === MembershipKind.Session) {
359
+ const absExpiry = this.getAbsoluteExpiry();
360
+ if (absExpiry) {
570
361
  // Assume that local clock is sufficiently in sync with other clocks in the distributed system.
571
362
  // We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate.
572
363
  // The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2
573
- return this.getAbsoluteExpiry()! - Date.now();
364
+ return absExpiry - Date.now();
365
+ }
574
366
  }
367
+ return undefined;
575
368
  }
576
369
 
577
370
  /**
@@ -580,9 +373,9 @@ export class CallMembership {
580
373
  public isExpired(): boolean {
581
374
  const { kind } = this.membershipData;
582
375
  switch (kind) {
583
- case "rtc":
376
+ case MembershipKind.RTC:
584
377
  return false;
585
- case "session":
378
+ case MembershipKind.Session:
586
379
  default:
587
380
  return this.getMsUntilExpiry()! <= 0;
588
381
  }
@@ -608,30 +401,26 @@ export class CallMembership {
608
401
  public getTransport(oldestMembership: CallMembership): Transport | undefined {
609
402
  const { kind, data } = this.membershipData;
610
403
  switch (kind) {
611
- case "rtc":
404
+ case MembershipKind.RTC:
612
405
  return data.rtc_transports[0];
613
- case "session":
406
+ case MembershipKind.Session:
614
407
  switch (data.focus_active.focus_selection) {
615
- case "multi_sfu":
616
- return data.foci_preferred[0];
617
408
  case "oldest_membership":
618
409
  if (CallMembership.equal(this, oldestMembership)) return data.foci_preferred[0];
619
410
  if (oldestMembership !== undefined) return oldestMembership.getTransport(oldestMembership);
620
411
  break;
412
+ case "multi_sfu":
413
+ return data.foci_preferred[0];
414
+ default:
415
+ // `focus_selection` not understood.
416
+ return undefined;
621
417
  }
418
+ break;
419
+ default:
420
+ return undefined;
622
421
  }
623
- return undefined;
624
422
  }
625
423
 
626
- /**
627
- * The focus_active filed of the session membership (m.call.member).
628
- * @deprecated focus_active is not used and will be removed in future versions.
629
- */
630
- public getFocusActive(): LivekitFocusSelection | undefined {
631
- const { kind, data } = this.membershipData;
632
- if (kind === "session") return data.focus_active;
633
- return undefined;
634
- }
635
424
  /**
636
425
  * The value of the `rtc_transports` field for RTC memberships (m.rtc.member).
637
426
  * Or the value of the `foci_preferred` field for legacy session memberships (m.call.member).
@@ -639,14 +428,11 @@ export class CallMembership {
639
428
  public get transports(): Transport[] {
640
429
  const { kind, data } = this.membershipData;
641
430
  switch (kind) {
642
- case "rtc":
431
+ case MembershipKind.RTC:
643
432
  return data.rtc_transports;
644
- case "session":
433
+ case MembershipKind.Session:
645
434
  default:
646
435
  return data.foci_preferred;
647
436
  }
648
437
  }
649
- public get kind(): MembershipData["kind"] {
650
- return this.membershipData.kind;
651
- }
652
438
  }