matrix-js-sdk 42.4.0 → 43.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 +23 -0
- package/lib/@types/PushRules.d.ts +3 -1
- package/lib/@types/PushRules.d.ts.map +1 -1
- package/lib/@types/PushRules.js +2 -0
- package/lib/@types/PushRules.js.map +1 -1
- package/lib/@types/event.d.ts +1 -0
- package/lib/@types/event.d.ts.map +1 -1
- package/lib/@types/event.js.map +1 -1
- package/lib/@types/matrix-sdk-crypto-wasm.d.js.map +1 -1
- package/lib/client.d.ts +88 -37
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +72 -47
- package/lib/client.js.map +1 -1
- package/lib/crypto-api/index.d.ts +3 -3
- package/lib/crypto-api/index.d.ts.map +1 -1
- package/lib/crypto-api/index.js.map +1 -1
- package/lib/embedded.d.ts +17 -1
- package/lib/embedded.d.ts.map +1 -1
- package/lib/embedded.js +46 -1
- package/lib/embedded.js.map +1 -1
- package/lib/errors.d.ts +1 -1
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +2 -2
- package/lib/errors.js.map +1 -1
- package/lib/http-api/fetch.d.ts +7 -1
- package/lib/http-api/fetch.d.ts.map +1 -1
- package/lib/http-api/fetch.js +14 -5
- package/lib/http-api/fetch.js.map +1 -1
- package/lib/http-api/interface.d.ts +34 -8
- package/lib/http-api/interface.d.ts.map +1 -1
- package/lib/http-api/interface.js +10 -3
- package/lib/http-api/interface.js.map +1 -1
- package/lib/http-api/refresh.d.ts +30 -11
- package/lib/http-api/refresh.d.ts.map +1 -1
- package/lib/http-api/refresh.js +72 -5
- package/lib/http-api/refresh.js.map +1 -1
- package/lib/matrix.d.ts +1 -1
- package/lib/matrix.d.ts.map +1 -1
- package/lib/matrix.js.map +1 -1
- package/lib/matrixrtc/CallMembership.d.ts.map +1 -1
- package/lib/matrixrtc/CallMembership.js +20 -6
- package/lib/matrixrtc/CallMembership.js.map +1 -1
- package/lib/matrixrtc/IMembershipManager.d.ts +5 -0
- package/lib/matrixrtc/IMembershipManager.d.ts.map +1 -1
- package/lib/matrixrtc/IMembershipManager.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.d.ts +27 -1
- package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.js +58 -36
- package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.js +1 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.js.map +1 -1
- package/lib/matrixrtc/MembershipManager.d.ts +76 -0
- package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
- package/lib/matrixrtc/MembershipManager.js +264 -47
- package/lib/matrixrtc/MembershipManager.js.map +1 -1
- package/lib/matrixrtc/index.d.ts +1 -1
- package/lib/matrixrtc/index.d.ts.map +1 -1
- package/lib/matrixrtc/index.js +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 -1
- package/lib/matrixrtc/membershipData/common.js +10 -0
- package/lib/matrixrtc/membershipData/common.js.map +1 -1
- package/lib/matrixrtc/membershipData/index.d.ts +1 -1
- package/lib/matrixrtc/membershipData/index.d.ts.map +1 -1
- package/lib/matrixrtc/membershipData/index.js +1 -1
- package/lib/matrixrtc/membershipData/index.js.map +1 -1
- package/lib/matrixrtc/types.d.ts +104 -11
- package/lib/matrixrtc/types.d.ts.map +1 -1
- package/lib/matrixrtc/types.js +263 -19
- package/lib/matrixrtc/types.js.map +1 -1
- package/lib/matrixrtc/utils.d.ts +22 -1
- package/lib/matrixrtc/utils.d.ts.map +1 -1
- package/lib/matrixrtc/utils.js +38 -0
- package/lib/matrixrtc/utils.js.map +1 -1
- package/lib/models/room-sticky-events.d.ts +7 -0
- package/lib/models/room-sticky-events.d.ts.map +1 -1
- package/lib/models/room-sticky-events.js +27 -0
- package/lib/models/room-sticky-events.js.map +1 -1
- package/lib/models/thread.d.ts.map +1 -1
- package/lib/models/thread.js +8 -0
- package/lib/models/thread.js.map +1 -1
- package/lib/oauth/authorize.js +1 -1
- package/lib/oauth/authorize.js.map +1 -1
- package/lib/oauth/index.d.ts +3 -5
- package/lib/oauth/index.d.ts.map +1 -1
- package/lib/oauth/index.js +7 -7
- package/lib/oauth/index.js.map +1 -1
- package/lib/oauth/tokenRefresher.d.ts +10 -4
- package/lib/oauth/tokenRefresher.d.ts.map +1 -1
- package/lib/oauth/tokenRefresher.js +34 -24
- package/lib/oauth/tokenRefresher.js.map +1 -1
- package/lib/receipt-accumulator.d.ts +10 -11
- package/lib/receipt-accumulator.d.ts.map +1 -1
- package/lib/receipt-accumulator.js +26 -25
- package/lib/receipt-accumulator.js.map +1 -1
- package/lib/room-summary-api.d.ts +71 -0
- package/lib/room-summary-api.d.ts.map +1 -0
- package/lib/room-summary-api.js +82 -0
- package/lib/room-summary-api.js.map +1 -0
- package/lib/rust-crypto/backup.d.ts +7 -0
- package/lib/rust-crypto/backup.d.ts.map +1 -1
- package/lib/rust-crypto/backup.js +41 -25
- package/lib/rust-crypto/backup.js.map +1 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +6 -1
- package/lib/sync.js.map +1 -1
- package/package.json +6 -6
- package/src/@types/PushRules.ts +2 -0
- package/src/@types/event.ts +1 -1
- package/src/@types/matrix-sdk-crypto-wasm.d.ts +15 -12
- package/src/client.ts +105 -62
- package/src/crypto-api/index.ts +3 -3
- package/src/embedded.ts +61 -2
- package/src/errors.ts +2 -1
- package/src/http-api/fetch.ts +14 -5
- package/src/http-api/interface.ts +36 -8
- package/src/http-api/refresh.ts +101 -17
- package/src/matrix.ts +1 -1
- package/src/matrixrtc/CallMembership.ts +22 -4
- package/src/matrixrtc/IMembershipManager.ts +6 -0
- package/src/matrixrtc/MatrixRTCSession.ts +72 -49
- package/src/matrixrtc/MatrixRTCSessionManager.ts +1 -1
- package/src/matrixrtc/MembershipManager.ts +284 -61
- package/src/matrixrtc/index.ts +7 -1
- package/src/matrixrtc/membershipData/common.ts +12 -0
- package/src/matrixrtc/membershipData/index.ts +1 -1
- package/src/matrixrtc/types.ts +351 -23
- package/src/matrixrtc/utils.ts +54 -1
- package/src/models/room-sticky-events.ts +26 -1
- package/src/models/thread.ts +9 -0
- package/src/oauth/authorize.ts +1 -1
- package/src/oauth/index.ts +7 -8
- package/src/oauth/tokenRefresher.ts +15 -5
- package/src/receipt-accumulator.ts +30 -26
- package/src/room-summary-api.ts +114 -0
- package/src/rust-crypto/backup.ts +46 -33
- package/src/sync.ts +9 -1
package/src/matrixrtc/types.ts
CHANGED
|
@@ -15,8 +15,14 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
import type { IContent, IMentions } from "../matrix.ts";
|
|
17
17
|
import type { RelationEvent } from "../types.ts";
|
|
18
|
-
import type {
|
|
18
|
+
import type { MatrixEvent } from "../models/event.ts";
|
|
19
|
+
import type { Room } from "../models/room.ts";
|
|
20
|
+
import { EventTimeline } from "../models/event-timeline.ts";
|
|
21
|
+
import { EventType, RelationType } from "../@types/event.ts";
|
|
22
|
+
import { CallMembership } from "./CallMembership.ts";
|
|
19
23
|
import { type CallMembershipIdentityParts } from "./EncryptionManager.ts";
|
|
24
|
+
import { isLeftMembershipContent } from "./membershipData/index.ts";
|
|
25
|
+
import { isSlotOpen, slotIdToDescription } from "./utils.ts";
|
|
20
26
|
|
|
21
27
|
export type EncryptionKeyMapKey = string;
|
|
22
28
|
|
|
@@ -106,35 +112,323 @@ export type RTCNotificationType = "ring" | "notification";
|
|
|
106
112
|
export type RTCCallIntent = "audio" | "video" | string;
|
|
107
113
|
|
|
108
114
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* @
|
|
113
|
-
* @returns a parsed IRTCNotificationContent
|
|
115
|
+
* The maximum `lifetime` of an RTC notification as per MSC4075. Larger values are capped to this.
|
|
116
|
+
*
|
|
117
|
+
* Exported for use within the js-sdk only. Not re-exported from `matrixrtc/index.ts`.
|
|
118
|
+
* @internal
|
|
114
119
|
*/
|
|
115
|
-
export
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
export const RTC_NOTIFICATION_MAX_LIFETIME_MS = 2 * 60 * 1000; // 2 minutes
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The maximum amount by which a notification's `sender_ts` may lie ahead of its `origin_server_ts`
|
|
124
|
+
* before the `lifetime` is measured from `origin_server_ts` instead, as per MSC4075.
|
|
125
|
+
*
|
|
126
|
+
* Exported for use within the js-sdk only. Not re-exported from `matrixrtc/index.ts`.
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
129
|
+
export const RTC_NOTIFICATION_MAX_SENDER_TS_AHEAD_MS = 20 * 1000; // 20 seconds
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The context that is needed to evaluate the MSC4075 rules for receiving notifications.
|
|
133
|
+
*/
|
|
134
|
+
export interface RTCNotificationValidationContext {
|
|
135
|
+
/** The room in which the notification was received. */
|
|
136
|
+
room: Pick<
|
|
137
|
+
Room,
|
|
138
|
+
"getLiveTimeline" | "getUnfilteredTimelineSet" | "_unstable_getStickyEvents" | "_unstable_getKeyedStickyEvent"
|
|
139
|
+
>;
|
|
140
|
+
/** The user ID of the receiving (local) user. */
|
|
141
|
+
userId: string;
|
|
142
|
+
/** The current time in milliseconds since the epoch. Defaults to `Date.now()`. */
|
|
143
|
+
now?: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Computes the absolute timestamp at which an RTC notification stops being valid, as per MSC4075.
|
|
148
|
+
*
|
|
149
|
+
* The `lifetime` is measured from `sender_ts` unless `sender_ts` lies more than 20 seconds ahead of
|
|
150
|
+
* `origin_server_ts`, in which case it is measured from `origin_server_ts` instead. Regardless of the basis,
|
|
151
|
+
* the remaining lifetime is capped at 2 minutes.
|
|
152
|
+
*
|
|
153
|
+
* @param content The notification content.
|
|
154
|
+
* @param originServerTs The `origin_server_ts` of the notification event.
|
|
155
|
+
* @param now The current time in milliseconds since the epoch. Defaults to `Date.now()`.
|
|
156
|
+
* @returns The timestamp (in milliseconds since the epoch) at which the notification expires.
|
|
157
|
+
*/
|
|
158
|
+
export function getCallNotificationExpiry(
|
|
159
|
+
content: Pick<IRTCNotificationContent, "sender_ts" | "lifetime">,
|
|
160
|
+
originServerTs: number,
|
|
161
|
+
now: number = Date.now(),
|
|
162
|
+
): number {
|
|
163
|
+
const basis =
|
|
164
|
+
content.sender_ts - originServerTs > RTC_NOTIFICATION_MAX_SENDER_TS_AHEAD_MS
|
|
165
|
+
? originServerTs
|
|
166
|
+
: content.sender_ts;
|
|
167
|
+
return Math.min(basis + content.lifetime, now + RTC_NOTIFICATION_MAX_LIFETIME_MS);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The subset of a notification event that {@link parseCallNotificationContent} needs.
|
|
172
|
+
*/
|
|
173
|
+
type RTCNotificationEvent = Pick<
|
|
174
|
+
MatrixEvent,
|
|
175
|
+
"getContent" | "getId" | "getSender" | "getTs" | "getType" | "unstableStickyInfo"
|
|
176
|
+
>;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Whether the content marks a withdrawn sticky event (MSC4354), i.e. it is empty apart from the sticky key.
|
|
180
|
+
*/
|
|
181
|
+
function isWithdrawal(content: IContent): boolean {
|
|
182
|
+
return Object.keys(content).every((key) => key === "msc4354_sticky_key");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isNonNegativeInteger(value: unknown): value is number {
|
|
186
|
+
return Number.isInteger(value) && (value as number) >= 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Looks up a legacy `m.call.member` state event by ID in the room's current state.
|
|
191
|
+
*
|
|
192
|
+
* @returns The event, or `undefined` if no current membership state event has that ID.
|
|
193
|
+
*/
|
|
194
|
+
function findMembershipStateEvent(
|
|
195
|
+
room: RTCNotificationValidationContext["room"],
|
|
196
|
+
eventId: string,
|
|
197
|
+
): MatrixEvent | undefined {
|
|
198
|
+
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
|
199
|
+
return roomState?.getStateEvents(EventType.GroupCallMemberPrefix).find((event) => event.getId() === eventId);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Determines the ID of the slot that a notification invites to.
|
|
204
|
+
*
|
|
205
|
+
* Notifications predating the `slot_id` property instead carry an `m.reference` relation to the sender's
|
|
206
|
+
* membership event.
|
|
207
|
+
*
|
|
208
|
+
* @throws if the slot can't be determined.
|
|
209
|
+
* @returns A promise resolving to the slot ID.
|
|
210
|
+
*/
|
|
211
|
+
async function resolveSlotId(content: IContent, room: RTCNotificationValidationContext["room"]): Promise<string> {
|
|
212
|
+
if (typeof content["slot_id"] === "string") {
|
|
213
|
+
// Throws if the ID is malformed, e.g. because it contains more than one "#".
|
|
214
|
+
slotIdToDescription(content["slot_id"]);
|
|
215
|
+
return content["slot_id"];
|
|
216
|
+
}
|
|
217
|
+
if (content["slot_id"] !== undefined) throw new Error("Invalid slot_id");
|
|
218
|
+
|
|
219
|
+
const relation = content["m.relates_to"];
|
|
220
|
+
const membershipEventId = relation?.rel_type === RelationType.Reference ? relation.event_id : undefined;
|
|
221
|
+
if (typeof membershipEventId !== "string") {
|
|
222
|
+
throw new Error("Missing slot_id and no m.reference relation to derive it from");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const membershipEvent = findMembershipStateEvent(room, membershipEventId);
|
|
226
|
+
if (!membershipEvent) {
|
|
227
|
+
throw new Error(`Missing slot_id and referenced event ${membershipEventId} is not a current membership`);
|
|
118
228
|
}
|
|
119
|
-
|
|
120
|
-
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
return (await CallMembership.parseFromEvent(membershipEvent)).slotId;
|
|
232
|
+
} catch (cause) {
|
|
233
|
+
throw new Error(`Missing slot_id and referenced event ${membershipEventId} is not a valid membership`, {
|
|
234
|
+
cause,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Parses the content of a received `org.matrix.msc4075.rtc.notification` event.
|
|
241
|
+
*
|
|
242
|
+
* This checks that the content matches the schema of MSC4075, caps `lifetime` to 2 minutes and applies
|
|
243
|
+
* the rules for receiving invites from MSC4075.
|
|
244
|
+
* The notification is only considered valid if all of the following hold:
|
|
245
|
+
* - It wasn't sent by the receiving user.
|
|
246
|
+
* - It is the current entry in the room's sticky event map for its sender and slot and isn't a withdrawal.
|
|
247
|
+
* - The slot it refers to exists and is open.
|
|
248
|
+
* - Its lifetime hasn't elapsed (see {@link getCallNotificationExpiry}).
|
|
249
|
+
* - Its `m.mentions` target the receiving user, either directly or via a room mention that the sender is
|
|
250
|
+
* allowed to trigger.
|
|
251
|
+
* - The receiving user has no joined `m.rtc.member` event for the slot in the sticky event map.
|
|
252
|
+
* - Any left `m.rtc.member` event of the receiving user for the slot predates the notification.
|
|
253
|
+
* - The receiving user has no sticky decline event referencing the notification.
|
|
254
|
+
*
|
|
255
|
+
* @param event The notification event.
|
|
256
|
+
* @param context The context needed to evaluate the receiving rules.
|
|
257
|
+
* @throws if the content is malformed or the notification isn't valid.
|
|
258
|
+
* @returns A promise resolving to the parsed content.
|
|
259
|
+
*/
|
|
260
|
+
export async function parseCallNotificationContent(
|
|
261
|
+
event: RTCNotificationEvent,
|
|
262
|
+
context: RTCNotificationValidationContext,
|
|
263
|
+
): Promise<IRTCNotificationContent> {
|
|
264
|
+
if (event.getType() !== EventType.RTCNotification) {
|
|
265
|
+
throw new Error(`Event ${event.getId()} is not an ${EventType.RTCNotification} event`);
|
|
121
266
|
}
|
|
122
|
-
|
|
267
|
+
|
|
268
|
+
const content = event.getContent();
|
|
269
|
+
if (isWithdrawal(content)) {
|
|
270
|
+
throw new Error("Notification was withdrawn");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const slotId = await resolveSlotId(content, context.room);
|
|
274
|
+
// The sticky key is only validated when present. A notification predating it can still be valid, and one
|
|
275
|
+
// carrying the wrong key is filed under that key in the sticky event map, so the lookup below rejects it.
|
|
276
|
+
if (content["msc4354_sticky_key"] !== undefined && content["msc4354_sticky_key"] !== slotId) {
|
|
277
|
+
throw new Error("msc4354_sticky_key must be equal to slot_id");
|
|
278
|
+
}
|
|
279
|
+
if (!isNonNegativeInteger(content["sender_ts"])) {
|
|
123
280
|
throw new Error("Missing or invalid sender_ts");
|
|
124
281
|
}
|
|
125
|
-
if (
|
|
282
|
+
if (!isNonNegativeInteger(content["lifetime"])) {
|
|
126
283
|
throw new Error("Missing or invalid lifetime");
|
|
127
284
|
}
|
|
128
285
|
|
|
129
|
-
|
|
130
|
-
|
|
286
|
+
const mentions = content["m.mentions"];
|
|
287
|
+
if (typeof mentions !== "object" || mentions === null) {
|
|
288
|
+
throw new Error("Missing or malformed m.mentions");
|
|
289
|
+
}
|
|
290
|
+
if (mentions.room !== undefined && typeof mentions.room !== "boolean") {
|
|
291
|
+
throw new Error("malformed m.mentions.room");
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
mentions.user_ids !== undefined &&
|
|
295
|
+
(!Array.isArray(mentions.user_ids) || !mentions.user_ids.every((userId) => typeof userId === "string"))
|
|
296
|
+
) {
|
|
297
|
+
throw new Error("malformed m.mentions.user_ids");
|
|
131
298
|
}
|
|
132
|
-
if (
|
|
133
|
-
throw new Error("
|
|
299
|
+
if (mentions.room !== true && !mentions.user_ids?.length) {
|
|
300
|
+
throw new Error("m.mentions must target either the room or at least one user");
|
|
134
301
|
}
|
|
135
302
|
|
|
136
|
-
const
|
|
137
|
-
|
|
303
|
+
const parsed = {
|
|
304
|
+
...content,
|
|
305
|
+
slot_id: slotId,
|
|
306
|
+
lifetime: Math.min(content["lifetime"], RTC_NOTIFICATION_MAX_LIFETIME_MS),
|
|
307
|
+
} as IRTCNotificationContent;
|
|
308
|
+
|
|
309
|
+
await checkCallNotificationReceivingRules(parsed, event, context);
|
|
310
|
+
|
|
311
|
+
return parsed;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Applies the MSC4075 rules for receiving invites to an already schema-validated notification.
|
|
316
|
+
* @throws if any of the rules is violated.
|
|
317
|
+
*/
|
|
318
|
+
async function checkCallNotificationReceivingRules(
|
|
319
|
+
content: IRTCNotificationContent,
|
|
320
|
+
event: RTCNotificationEvent,
|
|
321
|
+
{ room, userId, now = Date.now() }: RTCNotificationValidationContext,
|
|
322
|
+
): Promise<void> {
|
|
323
|
+
const eventId = event.getId();
|
|
324
|
+
const sender = event.getSender();
|
|
325
|
+
if (eventId === undefined || sender === undefined) {
|
|
326
|
+
throw new Error("Notification event is missing its event ID or sender");
|
|
327
|
+
}
|
|
328
|
+
const originServerTs = event.getTs();
|
|
329
|
+
const slotId = content.slot_id;
|
|
330
|
+
|
|
331
|
+
if (sender === userId) {
|
|
332
|
+
throw new Error("Notification was sent by the receiving user");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// A notification that wasn't delivered as a sticky event can't be superseded or withdrawn, so there is
|
|
336
|
+
// nothing to check. It isn't in the sticky event map either, which is why this can't be checked blindly.
|
|
337
|
+
if (event.unstableStickyInfo !== undefined) {
|
|
338
|
+
if (room._unstable_getKeyedStickyEvent(sender, event.getType(), slotId)?.getId() !== eventId) {
|
|
339
|
+
throw new Error("Notification is not the current sticky event for its sender and slot");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// `isSlotOpen` returns undefined when no slot event exists at all, which is just as disqualifying.
|
|
344
|
+
if (!isSlotOpen(room, slotIdToDescription(slotId))) {
|
|
345
|
+
throw new Error(`Slot ${slotId} does not exist or is closed`);
|
|
346
|
+
}
|
|
347
|
+
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
|
348
|
+
|
|
349
|
+
if (now >= getCallNotificationExpiry(content, originServerTs, now)) {
|
|
350
|
+
throw new Error("Notification has expired");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const mentions = content["m.mentions"];
|
|
354
|
+
const mentionsUser = mentions.user_ids?.includes(userId) ?? false;
|
|
355
|
+
const mentionsRoom = mentions.room === true && (roomState?.mayTriggerNotifOfType("room", sender) ?? false);
|
|
356
|
+
if (!mentionsUser && !mentionsRoom) {
|
|
357
|
+
throw new Error("Notification does not mention the receiving user");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const membershipEvent of ownMembershipEvents(room, userId)) {
|
|
361
|
+
const membershipContent = membershipEvent.getContent();
|
|
362
|
+
if (isLeftMembershipContent(membershipContent)) {
|
|
363
|
+
if (membershipContent.slot_id === slotId && membershipEvent.getTs() >= originServerTs) {
|
|
364
|
+
throw new Error(`Receiving user left slot ${slotId} after the notification was sent`);
|
|
365
|
+
}
|
|
366
|
+
} else if (await isJoinedToSlot(membershipEvent, slotId, now)) {
|
|
367
|
+
throw new Error(`Receiving user is already joined to slot ${slotId}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (hasDeclined(room, userId, eventId)) {
|
|
372
|
+
throw new Error("Receiving user declined the notification");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Yields the receiving user's own MatrixRTC membership events, from both the sticky event map and the legacy
|
|
378
|
+
* member state events. Sessions that don't use sticky events keep membership in state, so both have to be
|
|
379
|
+
* consulted to tell whether the user is in a session.
|
|
380
|
+
*
|
|
381
|
+
* @yields the user's own `m.rtc.member` and `m.call.member` events.
|
|
382
|
+
*/
|
|
383
|
+
function* ownMembershipEvents(room: RTCNotificationValidationContext["room"], userId: string): Iterable<MatrixEvent> {
|
|
384
|
+
for (const stickyEvent of room._unstable_getStickyEvents()) {
|
|
385
|
+
if (stickyEvent.getType() === EventType.RTCMembership && stickyEvent.getSender() === userId) {
|
|
386
|
+
yield stickyEvent;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
|
391
|
+
for (const stateEvent of roomState?.getStateEvents(EventType.GroupCallMemberPrefix) ?? []) {
|
|
392
|
+
if (stateEvent.getSender() === userId) yield stateEvent;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Whether the given membership event places its sender in the given slot, i.e. it is a valid, unexpired
|
|
398
|
+
* membership for that slot.
|
|
399
|
+
*/
|
|
400
|
+
async function isJoinedToSlot(membershipEvent: MatrixEvent, slotId: string, now: number): Promise<boolean> {
|
|
401
|
+
let membership: CallMembership;
|
|
402
|
+
try {
|
|
403
|
+
membership = await CallMembership.parseFromEvent(membershipEvent);
|
|
404
|
+
} catch {
|
|
405
|
+
// Not a membership we understand, so it doesn't place the user anywhere.
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
if (membership.slotId !== slotId) return false;
|
|
409
|
+
|
|
410
|
+
// Legacy memberships go stale when the client that sent them disappears without leaving. Honouring an
|
|
411
|
+
// expired one would suppress notifications for hours.
|
|
412
|
+
const expiry = membership.getAbsoluteExpiry();
|
|
413
|
+
return expiry === undefined || expiry > now;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Whether the receiving user has declined the given notification.
|
|
418
|
+
*
|
|
419
|
+
* Declines are looked up both through their `m.reference` relation and in the sticky event map. Relations
|
|
420
|
+
* cover declines that couldn't be sent as sticky events, but only see events that reached the timeline. A
|
|
421
|
+
* sticky decline delivered after a gappy or initial sync arrives via the sticky section instead, so it is
|
|
422
|
+
* only in the map.
|
|
423
|
+
*/
|
|
424
|
+
function hasDeclined(room: RTCNotificationValidationContext["room"], userId: string, eventId: string): boolean {
|
|
425
|
+
const declines = room
|
|
426
|
+
.getUnfilteredTimelineSet()
|
|
427
|
+
.relations.getChildEventsForEvent(eventId, RelationType.Reference, EventType.RTCDecline);
|
|
428
|
+
if (declines?.getRelations().some((decline) => decline.getSender() === userId)) return true;
|
|
429
|
+
|
|
430
|
+
// Declines are keyed on the event ID of the notification they decline.
|
|
431
|
+
return room._unstable_getKeyedStickyEvent(userId, EventType.RTCDecline, eventId) !== undefined;
|
|
138
432
|
}
|
|
139
433
|
|
|
140
434
|
/**
|
|
@@ -142,21 +436,55 @@ export function parseCallNotificationContent(content: IContent): IRTCNotificatio
|
|
|
142
436
|
* Don't cast event content to this directly. Use `parseCallNotificationContent` instead to validate the content first.
|
|
143
437
|
*/
|
|
144
438
|
export interface IRTCNotificationContent extends RelationEvent {
|
|
145
|
-
"m.mentions"?: IMentions;
|
|
146
|
-
"notification_type": RTCNotificationType;
|
|
147
439
|
/**
|
|
148
|
-
* The
|
|
440
|
+
* The `state_key` of the `m.rtc.slot` event that the notification refers to.
|
|
441
|
+
*
|
|
442
|
+
* Notifications predating this property omit it. `parseCallNotificationContent` fills it in from the
|
|
443
|
+
* referenced membership event, so the parsed content always carries it.
|
|
444
|
+
*/
|
|
445
|
+
"slot_id": string;
|
|
446
|
+
/**
|
|
447
|
+
* The users targeted by the notification. Must either mention the room or at least one user.
|
|
448
|
+
*/
|
|
449
|
+
"m.mentions": IMentions;
|
|
450
|
+
/**
|
|
451
|
+
* @deprecated Not part of MSC4075 anymore. How to present a notification is up to the receiving client.
|
|
452
|
+
*/
|
|
453
|
+
"notification_type"?: RTCNotificationType;
|
|
454
|
+
/**
|
|
455
|
+
* @deprecated Not part of MSC4075 anymore. The initial intent of the calling user.
|
|
149
456
|
*/
|
|
150
457
|
"m.call.intent"?: RTCCallIntent;
|
|
458
|
+
/**
|
|
459
|
+
* The timestamp (in milliseconds since the epoch) at which the sending client created the event.
|
|
460
|
+
*/
|
|
151
461
|
"sender_ts": number;
|
|
462
|
+
/**
|
|
463
|
+
* The time (in milliseconds) that the notification is valid for, measured from `sender_ts`.
|
|
464
|
+
*/
|
|
152
465
|
"lifetime": number;
|
|
466
|
+
/**
|
|
467
|
+
* The sticky key as per MSC4354. Must be equal to `slot_id`.
|
|
468
|
+
*
|
|
469
|
+
* Optional because notifications predating MSC4354 omit it.
|
|
470
|
+
*/
|
|
471
|
+
"msc4354_sticky_key"?: string;
|
|
153
472
|
}
|
|
154
473
|
|
|
155
474
|
/**
|
|
156
475
|
* MSC4310 decline event content for `org.matrix.msc4310.rtc.decline`.
|
|
157
476
|
* Sent as a standard m.reference relation to an `org.matrix.msc4075.rtc.notification` event.
|
|
158
477
|
*/
|
|
159
|
-
export interface IRTCDeclineContent extends RelationEvent {
|
|
478
|
+
export interface IRTCDeclineContent extends RelationEvent {
|
|
479
|
+
/**
|
|
480
|
+
* The sticky key as per MSC4354. Must be equal to the event ID of the notification being declined.
|
|
481
|
+
*
|
|
482
|
+
* Optional because servers without MSC4354 support can't deliver the decline as a sticky event. Such a
|
|
483
|
+
* decline still counts, but only for devices that receive it directly, since it isn't re-delivered after
|
|
484
|
+
* a gappy or initial sync.
|
|
485
|
+
*/
|
|
486
|
+
msc4354_sticky_key?: string;
|
|
487
|
+
}
|
|
160
488
|
|
|
161
489
|
export enum Status {
|
|
162
490
|
Disconnected = "Disconnected",
|
package/src/matrixrtc/utils.ts
CHANGED
|
@@ -15,7 +15,10 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { getEncryptionKeyMapKey, type CallMembershipIdentityParts } from "./EncryptionManager.ts";
|
|
18
|
-
import type { InboundEncryptionSession, EncryptionKeyMapKey, SlotDescription } from "./types.ts";
|
|
18
|
+
import type { InboundEncryptionSession, EncryptionKeyMapKey, RtcSlotEventContent, SlotDescription } from "./types.ts";
|
|
19
|
+
import type { Room } from "../models/room.ts";
|
|
20
|
+
import { EventTimeline } from "../models/event-timeline.ts";
|
|
21
|
+
import { EventType } from "../@types/event.ts";
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Detects when a key for a given index is outdated.
|
|
@@ -67,3 +70,53 @@ export function slotIdToDescription(slotId: string): SlotDescription {
|
|
|
67
70
|
export function computeSlotId(slotDescription: SlotDescription): string {
|
|
68
71
|
return `${slotDescription.application}#${slotDescription.id}`;
|
|
69
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reads the slot state event's content for the given slot description.
|
|
76
|
+
*
|
|
77
|
+
* @returns The slot event's content, or `undefined` if no slot event exists for the given description.
|
|
78
|
+
*/
|
|
79
|
+
export function getSlotEventContent(
|
|
80
|
+
room: Pick<Room, "getLiveTimeline">,
|
|
81
|
+
slotDescription: SlotDescription,
|
|
82
|
+
): RtcSlotEventContent | undefined {
|
|
83
|
+
const slotId = computeSlotId(slotDescription);
|
|
84
|
+
const slotEvent = room
|
|
85
|
+
.getLiveTimeline()
|
|
86
|
+
.getState(EventTimeline.FORWARDS)
|
|
87
|
+
?.getStateEvents(EventType.RTCSlot, slotId);
|
|
88
|
+
if (!slotEvent) return undefined;
|
|
89
|
+
|
|
90
|
+
return slotEvent.getContent<RtcSlotEventContent>();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether the given slot is closed.
|
|
95
|
+
*
|
|
96
|
+
* @returns `true` if the slot is closed, `false` if the slot is open or `undefined`
|
|
97
|
+
* if no slot exists.
|
|
98
|
+
*/
|
|
99
|
+
export function isSlotClosed(
|
|
100
|
+
room: Pick<Room, "getLiveTimeline">,
|
|
101
|
+
slotDescription: SlotDescription,
|
|
102
|
+
): boolean | undefined {
|
|
103
|
+
const content = getSlotEventContent(room, slotDescription) as Partial<RtcSlotEventContent> | undefined;
|
|
104
|
+
if (content === undefined) return undefined;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
content.status !== "open" ||
|
|
108
|
+
typeof content.application !== "object" ||
|
|
109
|
+
content.application?.type !== slotDescription.application
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether the given slot is open.
|
|
115
|
+
*
|
|
116
|
+
* @returns `true` if the slot is open, `false` if the slot is closed or `undefined`
|
|
117
|
+
* if no slot exists.
|
|
118
|
+
*/
|
|
119
|
+
export function isSlotOpen(room: Pick<Room, "getLiveTimeline">, slotDescription: SlotDescription): boolean | undefined {
|
|
120
|
+
const closed = isSlotClosed(room, slotDescription);
|
|
121
|
+
return closed === undefined ? undefined : !closed;
|
|
122
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger as loggerInstance } from "../logger.ts";
|
|
2
|
-
import { type MatrixEvent } from "./event.ts";
|
|
2
|
+
import { type MatrixEvent, MatrixEventEvent } from "./event.ts";
|
|
3
3
|
import { TypedEventEmitter } from "./typed-event-emitter.ts";
|
|
4
4
|
|
|
5
5
|
const logger = loggerInstance.getChild("RoomStickyEvents");
|
|
@@ -131,6 +131,13 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
131
131
|
* and the previous event it may have replaced.
|
|
132
132
|
*/
|
|
133
133
|
private addStickyEvent(event: MatrixEvent): { added: true; prevEvent?: StickyMatrixEvent } | { added: false } {
|
|
134
|
+
// The map is keyed on the event's type and sticky key, both of which are encrypted. Wait for the
|
|
135
|
+
// event to be decrypted, otherwise it would be filed under `m.room.encrypted` with no sticky key.
|
|
136
|
+
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
|
|
137
|
+
this.addStickyEventOnceDecrypted(event);
|
|
138
|
+
return { added: false };
|
|
139
|
+
}
|
|
140
|
+
|
|
134
141
|
const stickyKey = event.getContent().msc4354_sticky_key;
|
|
135
142
|
if (typeof stickyKey !== "string" && stickyKey !== undefined) {
|
|
136
143
|
throw new Error(`${event.getId()} is missing msc4354_sticky_key`);
|
|
@@ -189,6 +196,24 @@ export class RoomStickyEventsStore extends TypedEventEmitter<RoomStickyEventsEve
|
|
|
189
196
|
};
|
|
190
197
|
}
|
|
191
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Adds the event to the map once it has been decrypted, emitting `RoomEvent.StickyEvents` at that point.
|
|
201
|
+
*
|
|
202
|
+
* Decryption may fail because the keys haven't arrived yet, in which case we keep waiting. An event that
|
|
203
|
+
* is never decrypted simply never enters the map.
|
|
204
|
+
*/
|
|
205
|
+
private addStickyEventOnceDecrypted(event: MatrixEvent): void {
|
|
206
|
+
const onEventDecrypted = (): void => {
|
|
207
|
+
if (event.isDecryptionFailure()) {
|
|
208
|
+
// The event may still be decrypted later, e.g. once the keys arrive. Keep listening.
|
|
209
|
+
event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.addStickyEvents([event]); // Safe to call because it sorts events by timestamp when updating a keyed entry.
|
|
213
|
+
};
|
|
214
|
+
event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
|
|
215
|
+
}
|
|
216
|
+
|
|
192
217
|
/**
|
|
193
218
|
* Add a series of sticky events, emitting `RoomEvent.StickyEvents` if any
|
|
194
219
|
* changes were made.
|
package/src/models/thread.ts
CHANGED
|
@@ -369,6 +369,15 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
|
|
369
369
|
// Modify this event to point at our room's state, and mark its thread
|
|
370
370
|
// as this.
|
|
371
371
|
this.setEventMetadata(event);
|
|
372
|
+
|
|
373
|
+
// The bundled `current_user_participated` flag from the server lags behind local activity,
|
|
374
|
+
// so relying on it alone drops just-sent replies out of "participated" views until the next
|
|
375
|
+
// root event bundle refresh. Latch it as soon as the current user authors a thread reply,
|
|
376
|
+
// matching the server's participated semantics: an m.thread relation, not a reaction or edit.
|
|
377
|
+
if (event.getSender() === this.client.getUserId() && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
|
378
|
+
this._currentUserParticipated = true;
|
|
379
|
+
}
|
|
380
|
+
|
|
372
381
|
const eventId = event.getId();
|
|
373
382
|
if (
|
|
374
383
|
!this.initialEventsFetched &&
|
package/src/oauth/authorize.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
hasRequiredStringProperty,
|
|
25
25
|
isRecord,
|
|
26
26
|
} from "../@types/type-guards.ts";
|
|
27
|
-
import { Method } from "../http-api/
|
|
27
|
+
import { Method } from "../http-api/method.ts";
|
|
28
28
|
import { OAuthGrantType } from "./register.ts";
|
|
29
29
|
import { sleep } from "../utils.ts";
|
|
30
30
|
import { type Logger, logger as rootLogger } from "../logger.ts";
|
package/src/oauth/index.ts
CHANGED
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
} from "./register.ts";
|
|
35
35
|
import { encodeUnpaddedBase64Url } from "../base64.ts";
|
|
36
36
|
import { sha256 } from "../digest.ts";
|
|
37
|
-
import { HTTPError, isMatrixErrorResponse, MatrixError
|
|
37
|
+
import { HTTPError, isMatrixErrorResponse, MatrixError } from "../http-api/errors.ts";
|
|
38
|
+
import { Method } from "../http-api/method.ts";
|
|
38
39
|
import { type Logger, logger as rootLogger } from "../logger.ts";
|
|
39
40
|
import { fetchWithLogging } from "./fetch.ts";
|
|
40
41
|
import { isOAuth2ErrorResponse, OAuth2Error, OAuth2HTTPError } from "./error.ts";
|
|
@@ -44,7 +45,6 @@ import { type NonEmptyArray } from "../@types/common.ts";
|
|
|
44
45
|
export * from "./authorize.ts";
|
|
45
46
|
export * from "./error.ts";
|
|
46
47
|
export * from "./register.ts";
|
|
47
|
-
export * from "./tokenRefresher.ts";
|
|
48
48
|
export * from "./discover.ts";
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -57,8 +57,6 @@ type Context = {
|
|
|
57
57
|
deviceId?: string;
|
|
58
58
|
/** The seed used to generate the challenge code */
|
|
59
59
|
codeVerifier?: string;
|
|
60
|
-
/** The URI to redirect the user to with credentials after auth */
|
|
61
|
-
redirectUri: string;
|
|
62
60
|
};
|
|
63
61
|
|
|
64
62
|
export class OAuth2 {
|
|
@@ -149,7 +147,6 @@ export class OAuth2 {
|
|
|
149
147
|
) {
|
|
150
148
|
this.context = {
|
|
151
149
|
clientId: context.clientId,
|
|
152
|
-
redirectUri: context.redirectUri,
|
|
153
150
|
deviceId: context.deviceId ?? secureRandomString(10),
|
|
154
151
|
codeVerifier: context.codeVerifier ?? secureRandomString(96),
|
|
155
152
|
};
|
|
@@ -162,12 +159,14 @@ export class OAuth2 {
|
|
|
162
159
|
* that will allow the client to maintain state between the authorization request and the callback.
|
|
163
160
|
* The app should use this to key the storage for where the rest of the auth context is saved.
|
|
164
161
|
* @param responseMode - The manner in which the IdP should send the secrets back to the app. Defaults to `fragment` for privacy.
|
|
162
|
+
* @param redirectUri - The redirect URI this application is registered with the delegated auth server under.
|
|
165
163
|
* @param prompt - Optional prompt parameter to pass to the IdP to signal intent, e.g. `create` for User registration.
|
|
166
164
|
* @param scope - The OAuth2 scope to request, will be generated based on the device ID if omitted.
|
|
167
165
|
* @returns a Promise with the url as a string
|
|
168
166
|
*/
|
|
169
167
|
public async generateAuthorizationCodeGrantUrl(
|
|
170
168
|
state: string,
|
|
169
|
+
redirectUri: string,
|
|
171
170
|
responseMode: "fragment" | "query" = "fragment",
|
|
172
171
|
prompt?: string,
|
|
173
172
|
scope?: string,
|
|
@@ -178,7 +177,7 @@ export class OAuth2 {
|
|
|
178
177
|
url.searchParams.set("response_type", "code");
|
|
179
178
|
url.searchParams.set("response_mode", responseMode);
|
|
180
179
|
url.searchParams.set("client_id", this.context.clientId);
|
|
181
|
-
url.searchParams.set("redirect_uri",
|
|
180
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
182
181
|
url.searchParams.set("scope", scope ?? generateScope(this.context.deviceId));
|
|
183
182
|
url.searchParams.set("state", state);
|
|
184
183
|
url.searchParams.set("code_challenge_method", "S256");
|
|
@@ -202,12 +201,12 @@ export class OAuth2 {
|
|
|
202
201
|
* @throws An `Error` with `message` set to an entry in {@link OAuth2Error},
|
|
203
202
|
* when the request fails, or the returned token response is invalid.
|
|
204
203
|
*/
|
|
205
|
-
public async completeAuthorizationCodeGrant(code: string): Promise<BearerTokenResponse> {
|
|
204
|
+
public async completeAuthorizationCodeGrant(code: string, redirectUri: string): Promise<BearerTokenResponse> {
|
|
206
205
|
const params = new URLSearchParams();
|
|
207
206
|
params.append("grant_type", "authorization_code");
|
|
208
207
|
params.append("client_id", this.context.clientId);
|
|
209
208
|
params.append("code_verifier", this.context.codeVerifier);
|
|
210
|
-
params.append("redirect_uri",
|
|
209
|
+
params.append("redirect_uri", redirectUri);
|
|
211
210
|
params.append("code", code);
|
|
212
211
|
|
|
213
212
|
const res = await this.fetch("token", params, OAuth2Error.CodeExchangeFailed);
|