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.
- package/CHANGELOG.md +8 -0
- package/lib/@types/event.d.ts +1 -1
- package/lib/@types/event.d.ts.map +1 -1
- package/lib/@types/event.js +1 -1
- package/lib/@types/event.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +250 -246
- package/lib/client.js.map +1 -1
- package/lib/crypto-api/index.d.ts +13 -2
- package/lib/crypto-api/index.d.ts.map +1 -1
- package/lib/crypto-api/index.js +11 -0
- package/lib/crypto-api/index.js.map +1 -1
- package/lib/logger.d.ts +5 -5
- package/lib/logger.d.ts.map +1 -1
- package/lib/logger.js.map +1 -1
- package/lib/matrixrtc/CallMembership.d.ts +49 -145
- package/lib/matrixrtc/CallMembership.d.ts.map +1 -1
- package/lib/matrixrtc/CallMembership.js +157 -265
- package/lib/matrixrtc/CallMembership.js.map +1 -1
- package/lib/matrixrtc/EncryptionManager.d.ts +1 -85
- package/lib/matrixrtc/EncryptionManager.d.ts.map +1 -1
- package/lib/matrixrtc/EncryptionManager.js +0 -317
- package/lib/matrixrtc/EncryptionManager.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.d.ts +18 -22
- package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.js +48 -76
- package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.d.ts +2 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.d.ts.map +1 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.js +3 -2
- package/lib/matrixrtc/MatrixRTCSessionManager.js.map +1 -1
- package/lib/matrixrtc/MembershipManager.d.ts +10 -4
- package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
- package/lib/matrixrtc/MembershipManager.js +10 -4
- package/lib/matrixrtc/MembershipManager.js.map +1 -1
- package/lib/matrixrtc/RTCEncryptionManager.d.ts +6 -7
- package/lib/matrixrtc/RTCEncryptionManager.d.ts.map +1 -1
- package/lib/matrixrtc/RTCEncryptionManager.js +4 -7
- package/lib/matrixrtc/RTCEncryptionManager.js.map +1 -1
- package/lib/matrixrtc/index.d.ts +1 -0
- package/lib/matrixrtc/index.d.ts.map +1 -1
- package/lib/matrixrtc/index.js.map +1 -1
- package/lib/matrixrtc/membershipData/common.d.ts +8 -0
- package/lib/matrixrtc/membershipData/common.d.ts.map +1 -0
- package/lib/matrixrtc/membershipData/common.js +26 -0
- package/lib/matrixrtc/membershipData/common.js.map +1 -0
- package/lib/matrixrtc/membershipData/index.d.ts +4 -0
- package/lib/matrixrtc/membershipData/index.d.ts.map +1 -0
- package/lib/matrixrtc/membershipData/index.js +20 -0
- package/lib/matrixrtc/membershipData/index.js.map +1 -0
- package/lib/matrixrtc/membershipData/rtc.d.ts +33 -0
- package/lib/matrixrtc/membershipData/rtc.d.ts.map +1 -0
- package/lib/matrixrtc/membershipData/rtc.js +137 -0
- package/lib/matrixrtc/membershipData/rtc.js.map +1 -0
- package/lib/matrixrtc/membershipData/session.d.ts +77 -0
- package/lib/matrixrtc/membershipData/session.d.ts.map +1 -0
- package/lib/matrixrtc/membershipData/session.js +62 -0
- package/lib/matrixrtc/membershipData/session.js.map +1 -0
- package/lib/matrixrtc/types.d.ts +23 -0
- package/lib/matrixrtc/types.d.ts.map +1 -1
- package/lib/matrixrtc/types.js +9 -1
- package/lib/matrixrtc/types.js.map +1 -1
- package/lib/matrixrtc/utils.d.ts +11 -1
- package/lib/matrixrtc/utils.d.ts.map +1 -1
- package/lib/matrixrtc/utils.js +24 -1
- package/lib/matrixrtc/utils.js.map +1 -1
- package/lib/rust-crypto/rust-crypto.d.ts.map +1 -1
- package/lib/rust-crypto/rust-crypto.js +2 -2
- package/lib/rust-crypto/rust-crypto.js.map +1 -1
- package/package.json +5 -7
- package/src/@types/event.ts +2 -2
- package/src/client.ts +5 -3
- package/src/crypto-api/index.ts +17 -2
- package/src/logger.ts +5 -5
- package/src/matrixrtc/CallMembership.ts +159 -373
- package/src/matrixrtc/EncryptionManager.ts +1 -417
- package/src/matrixrtc/MatrixRTCSession.ts +82 -122
- package/src/matrixrtc/MatrixRTCSessionManager.ts +5 -3
- package/src/matrixrtc/MembershipManager.ts +14 -17
- package/src/matrixrtc/RTCEncryptionManager.ts +7 -10
- package/src/matrixrtc/index.ts +1 -0
- package/src/matrixrtc/membershipData/common.ts +27 -0
- package/src/matrixrtc/membershipData/index.ts +19 -0
- package/src/matrixrtc/membershipData/rtc.ts +156 -0
- package/src/matrixrtc/membershipData/session.ts +146 -0
- package/src/matrixrtc/types.ts +27 -1
- package/src/matrixrtc/utils.ts +24 -2
- package/src/rust-crypto/rust-crypto.ts +4 -1
- package/lib/matrixrtc/RoomKeyTransport.d.ts +0 -25
- package/lib/matrixrtc/RoomKeyTransport.d.ts.map +0 -1
- package/lib/matrixrtc/RoomKeyTransport.js +0 -152
- package/lib/matrixrtc/RoomKeyTransport.js.map +0 -1
- 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
|
|
20
|
-
import {
|
|
21
|
-
import type
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
*
|
|
148
|
-
|
|
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
|
-
*
|
|
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
|
-
"
|
|
188
|
-
|
|
189
|
-
// Application specific data
|
|
190
|
-
|
|
46
|
+
RTC = "rtc",
|
|
191
47
|
/**
|
|
192
|
-
*
|
|
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
|
-
"
|
|
50
|
+
Session = "session",
|
|
51
|
+
}
|
|
196
52
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
*
|
|
206
|
-
*
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
216
|
-
*
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
|
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
|
|
274
|
-
matrixEvent:
|
|
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
|
|
298
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
|
148
|
+
case MembershipKind.RTC:
|
|
358
149
|
return data.member.user_id;
|
|
359
|
-
case
|
|
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
|
|
168
|
+
case MembershipKind.RTC:
|
|
378
169
|
return data.slot_id;
|
|
379
|
-
case
|
|
170
|
+
case MembershipKind.Session:
|
|
380
171
|
default: {
|
|
381
|
-
const [application, 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
|
|
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
|
|
206
|
+
case MembershipKind.RTC:
|
|
416
207
|
return data.slot_id;
|
|
417
|
-
case
|
|
418
|
-
default:
|
|
419
|
-
|
|
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
|
|
217
|
+
case MembershipKind.RTC:
|
|
429
218
|
return data.member.device_id;
|
|
430
|
-
case
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
|
261
|
+
case MembershipKind.RTC:
|
|
474
262
|
return data.application;
|
|
475
|
-
case
|
|
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():
|
|
272
|
+
public get scope(): SessionMembershipData["scope"] | undefined {
|
|
483
273
|
const { kind, data } = this.membershipData;
|
|
484
274
|
switch (kind) {
|
|
485
|
-
case
|
|
275
|
+
case MembershipKind.RTC:
|
|
486
276
|
return undefined;
|
|
487
|
-
case
|
|
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
|
|
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.
|
|
538
|
-
case
|
|
329
|
+
return this.matrixEvent.getTs();
|
|
330
|
+
case MembershipKind.Session:
|
|
539
331
|
default:
|
|
540
|
-
return data.created_ts ?? this.
|
|
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
|
|
343
|
+
case MembershipKind.RTC:
|
|
552
344
|
return undefined;
|
|
553
|
-
case
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
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
|
|
376
|
+
case MembershipKind.RTC:
|
|
584
377
|
return false;
|
|
585
|
-
case
|
|
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
|
|
404
|
+
case MembershipKind.RTC:
|
|
612
405
|
return data.rtc_transports[0];
|
|
613
|
-
case
|
|
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
|
|
431
|
+
case MembershipKind.RTC:
|
|
643
432
|
return data.rtc_transports;
|
|
644
|
-
case
|
|
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
|
}
|