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.
Files changed (138) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/lib/@types/PushRules.d.ts +3 -1
  3. package/lib/@types/PushRules.d.ts.map +1 -1
  4. package/lib/@types/PushRules.js +2 -0
  5. package/lib/@types/PushRules.js.map +1 -1
  6. package/lib/@types/event.d.ts +1 -0
  7. package/lib/@types/event.d.ts.map +1 -1
  8. package/lib/@types/event.js.map +1 -1
  9. package/lib/@types/matrix-sdk-crypto-wasm.d.js.map +1 -1
  10. package/lib/client.d.ts +88 -37
  11. package/lib/client.d.ts.map +1 -1
  12. package/lib/client.js +72 -47
  13. package/lib/client.js.map +1 -1
  14. package/lib/crypto-api/index.d.ts +3 -3
  15. package/lib/crypto-api/index.d.ts.map +1 -1
  16. package/lib/crypto-api/index.js.map +1 -1
  17. package/lib/embedded.d.ts +17 -1
  18. package/lib/embedded.d.ts.map +1 -1
  19. package/lib/embedded.js +46 -1
  20. package/lib/embedded.js.map +1 -1
  21. package/lib/errors.d.ts +1 -1
  22. package/lib/errors.d.ts.map +1 -1
  23. package/lib/errors.js +2 -2
  24. package/lib/errors.js.map +1 -1
  25. package/lib/http-api/fetch.d.ts +7 -1
  26. package/lib/http-api/fetch.d.ts.map +1 -1
  27. package/lib/http-api/fetch.js +14 -5
  28. package/lib/http-api/fetch.js.map +1 -1
  29. package/lib/http-api/interface.d.ts +34 -8
  30. package/lib/http-api/interface.d.ts.map +1 -1
  31. package/lib/http-api/interface.js +10 -3
  32. package/lib/http-api/interface.js.map +1 -1
  33. package/lib/http-api/refresh.d.ts +30 -11
  34. package/lib/http-api/refresh.d.ts.map +1 -1
  35. package/lib/http-api/refresh.js +72 -5
  36. package/lib/http-api/refresh.js.map +1 -1
  37. package/lib/matrix.d.ts +1 -1
  38. package/lib/matrix.d.ts.map +1 -1
  39. package/lib/matrix.js.map +1 -1
  40. package/lib/matrixrtc/CallMembership.d.ts.map +1 -1
  41. package/lib/matrixrtc/CallMembership.js +20 -6
  42. package/lib/matrixrtc/CallMembership.js.map +1 -1
  43. package/lib/matrixrtc/IMembershipManager.d.ts +5 -0
  44. package/lib/matrixrtc/IMembershipManager.d.ts.map +1 -1
  45. package/lib/matrixrtc/IMembershipManager.js.map +1 -1
  46. package/lib/matrixrtc/MatrixRTCSession.d.ts +27 -1
  47. package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
  48. package/lib/matrixrtc/MatrixRTCSession.js +58 -36
  49. package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
  50. package/lib/matrixrtc/MatrixRTCSessionManager.js +1 -1
  51. package/lib/matrixrtc/MatrixRTCSessionManager.js.map +1 -1
  52. package/lib/matrixrtc/MembershipManager.d.ts +76 -0
  53. package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
  54. package/lib/matrixrtc/MembershipManager.js +264 -47
  55. package/lib/matrixrtc/MembershipManager.js.map +1 -1
  56. package/lib/matrixrtc/index.d.ts +1 -1
  57. package/lib/matrixrtc/index.d.ts.map +1 -1
  58. package/lib/matrixrtc/index.js +1 -1
  59. package/lib/matrixrtc/index.js.map +1 -1
  60. package/lib/matrixrtc/membershipData/common.d.ts +8 -0
  61. package/lib/matrixrtc/membershipData/common.d.ts.map +1 -1
  62. package/lib/matrixrtc/membershipData/common.js +10 -0
  63. package/lib/matrixrtc/membershipData/common.js.map +1 -1
  64. package/lib/matrixrtc/membershipData/index.d.ts +1 -1
  65. package/lib/matrixrtc/membershipData/index.d.ts.map +1 -1
  66. package/lib/matrixrtc/membershipData/index.js +1 -1
  67. package/lib/matrixrtc/membershipData/index.js.map +1 -1
  68. package/lib/matrixrtc/types.d.ts +104 -11
  69. package/lib/matrixrtc/types.d.ts.map +1 -1
  70. package/lib/matrixrtc/types.js +263 -19
  71. package/lib/matrixrtc/types.js.map +1 -1
  72. package/lib/matrixrtc/utils.d.ts +22 -1
  73. package/lib/matrixrtc/utils.d.ts.map +1 -1
  74. package/lib/matrixrtc/utils.js +38 -0
  75. package/lib/matrixrtc/utils.js.map +1 -1
  76. package/lib/models/room-sticky-events.d.ts +7 -0
  77. package/lib/models/room-sticky-events.d.ts.map +1 -1
  78. package/lib/models/room-sticky-events.js +27 -0
  79. package/lib/models/room-sticky-events.js.map +1 -1
  80. package/lib/models/thread.d.ts.map +1 -1
  81. package/lib/models/thread.js +8 -0
  82. package/lib/models/thread.js.map +1 -1
  83. package/lib/oauth/authorize.js +1 -1
  84. package/lib/oauth/authorize.js.map +1 -1
  85. package/lib/oauth/index.d.ts +3 -5
  86. package/lib/oauth/index.d.ts.map +1 -1
  87. package/lib/oauth/index.js +7 -7
  88. package/lib/oauth/index.js.map +1 -1
  89. package/lib/oauth/tokenRefresher.d.ts +10 -4
  90. package/lib/oauth/tokenRefresher.d.ts.map +1 -1
  91. package/lib/oauth/tokenRefresher.js +34 -24
  92. package/lib/oauth/tokenRefresher.js.map +1 -1
  93. package/lib/receipt-accumulator.d.ts +10 -11
  94. package/lib/receipt-accumulator.d.ts.map +1 -1
  95. package/lib/receipt-accumulator.js +26 -25
  96. package/lib/receipt-accumulator.js.map +1 -1
  97. package/lib/room-summary-api.d.ts +71 -0
  98. package/lib/room-summary-api.d.ts.map +1 -0
  99. package/lib/room-summary-api.js +82 -0
  100. package/lib/room-summary-api.js.map +1 -0
  101. package/lib/rust-crypto/backup.d.ts +7 -0
  102. package/lib/rust-crypto/backup.d.ts.map +1 -1
  103. package/lib/rust-crypto/backup.js +41 -25
  104. package/lib/rust-crypto/backup.js.map +1 -1
  105. package/lib/sync.d.ts.map +1 -1
  106. package/lib/sync.js +6 -1
  107. package/lib/sync.js.map +1 -1
  108. package/package.json +6 -6
  109. package/src/@types/PushRules.ts +2 -0
  110. package/src/@types/event.ts +1 -1
  111. package/src/@types/matrix-sdk-crypto-wasm.d.ts +15 -12
  112. package/src/client.ts +105 -62
  113. package/src/crypto-api/index.ts +3 -3
  114. package/src/embedded.ts +61 -2
  115. package/src/errors.ts +2 -1
  116. package/src/http-api/fetch.ts +14 -5
  117. package/src/http-api/interface.ts +36 -8
  118. package/src/http-api/refresh.ts +101 -17
  119. package/src/matrix.ts +1 -1
  120. package/src/matrixrtc/CallMembership.ts +22 -4
  121. package/src/matrixrtc/IMembershipManager.ts +6 -0
  122. package/src/matrixrtc/MatrixRTCSession.ts +72 -49
  123. package/src/matrixrtc/MatrixRTCSessionManager.ts +1 -1
  124. package/src/matrixrtc/MembershipManager.ts +284 -61
  125. package/src/matrixrtc/index.ts +7 -1
  126. package/src/matrixrtc/membershipData/common.ts +12 -0
  127. package/src/matrixrtc/membershipData/index.ts +1 -1
  128. package/src/matrixrtc/types.ts +351 -23
  129. package/src/matrixrtc/utils.ts +54 -1
  130. package/src/models/room-sticky-events.ts +26 -1
  131. package/src/models/thread.ts +9 -0
  132. package/src/oauth/authorize.ts +1 -1
  133. package/src/oauth/index.ts +7 -8
  134. package/src/oauth/tokenRefresher.ts +15 -5
  135. package/src/receipt-accumulator.ts +30 -26
  136. package/src/room-summary-api.ts +114 -0
  137. package/src/rust-crypto/backup.ts +46 -33
  138. package/src/sync.ts +9 -1
@@ -15,8 +15,10 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  import { MatrixError, TokenRefreshLogoutError } from "./errors.ts";
18
- import { type IHttpOpts } from "./interface.ts";
18
+ import { type AccessTokens, type IHttpOpts, type OAuth2ClientConfig } from "./interface.ts";
19
19
  import { sleep } from "../utils.ts";
20
+ import { TokenRefresher } from "../oauth/tokenRefresher.ts";
21
+ import { OAuth2, type ValidatedAuthMetadata } from "../oauth/index.ts";
20
22
 
21
23
  /**
22
24
  * This is an internal module. See {@link MatrixHttpApi} for the public class.
@@ -28,25 +30,25 @@ export const enum TokenRefreshOutcome {
28
30
  Logout = "logout",
29
31
  }
30
32
 
31
- interface Snapshot {
32
- accessToken: string;
33
- refreshToken?: string;
34
- expiry?: Date;
35
- }
36
-
37
33
  // If the token expires in less than this time amount of time, we will eagerly refresh it before making the intended request.
38
34
  const REFRESH_IF_TOKEN_EXPIRES_WITHIN_MS = 500;
39
35
  // If we get an unknown token error and the token expires in less than this time amount of time, we will refresh it before making the intended request.
40
36
  // Otherwise, we will error as the token should not have expired yet and we need to avoid retrying indefinitely.
41
37
  const REFRESH_ON_ERROR_IF_TOKEN_EXPIRES_WITHIN_MS = 60 * 1000;
42
38
 
43
- type Opts = Pick<IHttpOpts, "tokenRefreshFunction" | "logger" | "refreshToken" | "accessToken">;
39
+ type Opts = Pick<
40
+ IHttpOpts,
41
+ "onTokenRefresh" | "logger" | "refreshToken" | "accessToken" | "oauth2ClientConfig" | "authMetadataCallback"
42
+ >;
44
43
 
45
44
  /**
46
45
  * This class is responsible for managing the access token and refresh token for authenticated requests.
47
- * It will automatically refresh the access token when it is about to expire, and will handle unknown token errors.
46
+ * It will automatically refresh the tokens when the access token is about to expire and can handle
47
+ * Unknown Token errors (via {@link TokenManager.handleUnknownToken})
48
+ *
49
+ * It will update the {@link Opts} object with new tokens as they are refreshed, and also call the {@link IHttpOpts.onTokenRefresh} callback if provided.
48
50
  */
49
- export class TokenRefresher {
51
+ export class TokenManager {
50
52
  public constructor(private readonly opts: Opts) {}
51
53
 
52
54
  /**
@@ -57,12 +59,22 @@ export class TokenRefresher {
57
59
 
58
60
  private latestTokenRefreshExpiry?: Date;
59
61
 
62
+ // The object at the OAuth API layer that actually does the refreshing
63
+ // This needs OAuth params to be fetched before it can be constructed so it starts
64
+ // off as undefined and is constructed once we are able to do so.
65
+ private tokenRefresher?: TokenRefresher;
66
+
67
+ // Tracks an in-progress attempt to discover the OAuth2 auth server metadata and construct
68
+ // `tokenRefresher`, so that concurrent callers wait for (and share the result of) the same attempt
69
+ // rather than racing to discover it independently.
70
+ private tokenRefresherPromise?: Promise<TokenRefresher>;
71
+
60
72
  /**
61
73
  * This function is called before every request to ensure that the access token is valid.
62
74
  * @returns a snapshot containing the access token and other properties which must be passed to the handleUnknownToken
63
75
  * handler if an M_UNKNOWN_TOKEN error is encountered.
64
76
  */
65
- public async prepareForRequest(): Promise<Snapshot> {
77
+ public async prepareForRequest(): Promise<AccessTokens> {
66
78
  // Ensure our token is refreshed before we build the headers/params
67
79
  await this.refreshIfNeeded();
68
80
 
@@ -93,13 +105,13 @@ export class TokenRefresher {
93
105
  * @param attempt - the number of attempts made for this request so far
94
106
  * @returns a TokenRefreshOutcome indicating the result of the refresh attempt
95
107
  */
96
- public async handleUnknownToken(snapshot: Snapshot, attempt: number): Promise<TokenRefreshOutcome> {
108
+ public async handleUnknownToken(snapshot: AccessTokens, attempt: number): Promise<TokenRefreshOutcome> {
97
109
  return this._handleUnknownToken(snapshot, attempt);
98
110
  }
99
111
 
100
112
  private async _handleUnknownToken(): Promise<TokenRefreshOutcome>;
101
- private async _handleUnknownToken(snapshot: Snapshot, attempt: number): Promise<TokenRefreshOutcome>;
102
- private async _handleUnknownToken(snapshot?: Snapshot, attempt?: number): Promise<TokenRefreshOutcome> {
113
+ private async _handleUnknownToken(snapshot: AccessTokens, attempt: number): Promise<TokenRefreshOutcome>;
114
+ private async _handleUnknownToken(snapshot?: AccessTokens, attempt?: number): Promise<TokenRefreshOutcome> {
103
115
  if (snapshot?.expiry) {
104
116
  // If our token is unknown, but it should not have expired yet, then we should not refresh
105
117
  const expiresIn = snapshot.expiry.getTime() - Date.now();
@@ -132,8 +144,8 @@ export class TokenRefresher {
132
144
  * @returns Promise that resolves to a boolean - true when token was refreshed successfully
133
145
  */
134
146
  private async doTokenRefresh(attempt?: number): Promise<TokenRefreshOutcome> {
135
- if (!this.opts.refreshToken || !this.opts.tokenRefreshFunction) {
136
- this.opts.logger?.error("Unable to refresh token - no refresh token or refresh function");
147
+ if (!this.opts.refreshToken || !this.opts.oauth2ClientConfig) {
148
+ this.opts.logger?.error("Unable to refresh token - no refresh token or OAuth2 client config");
137
149
  return TokenRefreshOutcome.Logout;
138
150
  }
139
151
 
@@ -144,7 +156,13 @@ export class TokenRefresher {
144
156
 
145
157
  try {
146
158
  this.opts.logger?.debug("Attempting to refresh token");
147
- const { accessToken, refreshToken, expiry } = await this.opts.tokenRefreshFunction(this.opts.refreshToken);
159
+ const tokenRefresher = await this.ensureTokenRefresher();
160
+ if (!tokenRefresher) {
161
+ // We already checked `oauth2ClientConfig` is set above, so this should be unreachable.
162
+ return TokenRefreshOutcome.Logout;
163
+ }
164
+
165
+ const { accessToken, refreshToken, expiry } = await tokenRefresher.refreshTokens(this.opts.refreshToken);
148
166
  this.opts.accessToken = accessToken;
149
167
  this.opts.refreshToken = refreshToken;
150
168
  this.latestTokenRefreshExpiry = expiry;
@@ -163,4 +181,70 @@ export class TokenRefresher {
163
181
  return TokenRefreshOutcome.Failure;
164
182
  }
165
183
  }
184
+
185
+ /**
186
+ * Attempt to revoke the current access and refresh tokens with the OAuth2 authorization server, e.g. as
187
+ * part of logging out. Does nothing if this is not an OAuth2-native session (ie. no `oauth2ClientConfig`
188
+ * was supplied).
189
+ * @throws when discovery of the auth server metadata, or revocation of either token, fails
190
+ */
191
+ public async revokeTokens(): Promise<void> {
192
+ const tokenRefresher = await this.ensureTokenRefresher();
193
+ if (!tokenRefresher) return;
194
+
195
+ await Promise.all(
196
+ [
197
+ this.opts.accessToken ? tokenRefresher.revokeToken(this.opts.accessToken, "access_token") : undefined,
198
+ this.opts.refreshToken
199
+ ? tokenRefresher.revokeToken(this.opts.refreshToken, "refresh_token")
200
+ : undefined,
201
+ ].filter((p): p is Promise<void> => p !== undefined),
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Lazily constructs (and caches) the {@link TokenRefresher} used to talk to the OAuth2 authorization
207
+ * server, discovering its metadata first if this is the first time it's needed.
208
+ * @returns the token refresher, or undefined if no `oauth2ClientConfig` was supplied
209
+ * @throws when discovery of the auth server metadata fails in a way that isn't worth retrying
210
+ */
211
+ private async ensureTokenRefresher(): Promise<TokenRefresher | undefined> {
212
+ if (this.tokenRefresher) return this.tokenRefresher;
213
+ if (!this.opts.oauth2ClientConfig) return undefined;
214
+ if (!this.opts.authMetadataCallback) return undefined;
215
+
216
+ if (!this.tokenRefresherPromise) {
217
+ this.tokenRefresherPromise = this.discoverTokenRefresher(
218
+ this.opts.oauth2ClientConfig,
219
+ this.opts.authMetadataCallback,
220
+ );
221
+ }
222
+
223
+ try {
224
+ this.tokenRefresher = await this.tokenRefresherPromise;
225
+ return this.tokenRefresher;
226
+ } finally {
227
+ // Either we now have `tokenRefresher` cached and won't need this again, or discovery
228
+ // failed and a future call should be free to retry from scratch.
229
+ this.tokenRefresherPromise = undefined;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Discovers the OAuth2 auth server metadata and constructs the OAuth2 client and token refresher from it.
235
+ * Throws if the request fails.
236
+ */
237
+ private async discoverTokenRefresher(
238
+ config: OAuth2ClientConfig,
239
+ authMetaDataCallback: () => Promise<ValidatedAuthMetadata>,
240
+ ): Promise<TokenRefresher> {
241
+ const metadata = await authMetaDataCallback();
242
+
243
+ const oauth2 = new OAuth2(metadata, {
244
+ clientId: config.clientId,
245
+ deviceId: config.deviceId,
246
+ });
247
+
248
+ return new TokenRefresher(oauth2, (tokens) => this.opts.onTokenRefresh?.(tokens));
249
+ }
166
250
  }
package/src/matrix.ts CHANGED
@@ -84,7 +84,7 @@ export * from "./models/event-status.ts";
84
84
  export * from "./models/profile-keys.ts";
85
85
  export * from "./models/related-relations.ts";
86
86
  export { type StickyMatrixEvent, RoomStickyEventsEvent } from "./models/room-sticky-events.ts";
87
- export type { RoomSummary } from "./client.ts";
87
+ export type { RoomSummary } from "./room-summary-api.ts";
88
88
  export * as ContentHelpers from "./content-helpers.ts";
89
89
  export * as SecretStorage from "./secret-storage.ts";
90
90
  export { createNewMatrixCall, CallEvent } from "./webrtc/call.ts";
@@ -36,6 +36,20 @@ import { EventType } from "../@types/event.ts";
36
36
  */
37
37
  export const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4;
38
38
 
39
+ /** The fields of a legacy session membership that belong to the session, not the application */
40
+ const sessionFields: (keyof SessionMembershipData)[] = [
41
+ "application",
42
+ "call_id",
43
+ "device_id",
44
+ "focus_active",
45
+ "foci_preferred",
46
+ "created_ts",
47
+ "scope",
48
+ "expires",
49
+ "m.call.intent",
50
+ "membershipID",
51
+ ];
52
+
39
53
  /**
40
54
  * Describes the source event type that provided the membership data.
41
55
  */
@@ -261,10 +275,14 @@ export class CallMembership {
261
275
  case MembershipKind.RTC:
262
276
  return data.application;
263
277
  case MembershipKind.Session:
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.
267
- return { "type": data.application, "m.call.intent": data["m.call.intent"] };
278
+ default: {
279
+ // SessionData has no application object: the application's own
280
+ // data sits at the top level beside the session fields, so
281
+ // everything but those is the application's
282
+ const { application, ...rest } = data;
283
+ for (const key of sessionFields) delete rest[key as keyof typeof rest];
284
+ return { ...rest, "type": application, "m.call.intent": data["m.call.intent"] };
285
+ }
268
286
  }
269
287
  }
270
288
 
@@ -117,4 +117,10 @@ export interface IMembershipManager extends TypedEventEmitter<
117
117
  * @param callIntent The new intent to set.
118
118
  */
119
119
  updateCallIntent(callIntent: RTCCallIntent): Promise<void>;
120
+
121
+ /**
122
+ * Replace the application-specific data of our membership (see
123
+ * `SessionConfig.applicationData`), re-sending the membership if it changed.
124
+ */
125
+ updateApplicationData(applicationData: Record<string, unknown>): Promise<void>;
120
126
  }
@@ -38,6 +38,9 @@ import type {
38
38
  RtcSlotEventContent,
39
39
  RtcSlotEncryptionContent,
40
40
  } from "./types.ts";
41
+ import { RTC_NOTIFICATION_MAX_SENDER_TS_AHEAD_MS } from "./types.ts";
42
+ import { isLeftMembershipContent } from "./membershipData/index.ts";
43
+ import { UnsupportedStickyEventsEndpointError } from "../errors.ts";
41
44
  import {
42
45
  MembershipManagerEvent,
43
46
  type MembershipManagerEventHandlerMap,
@@ -48,7 +51,7 @@ import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
48
51
  import { TypedReEmitter } from "../ReEmitter.ts";
49
52
  import { type IContent, type MatrixEvent } from "../models/event.ts";
50
53
  import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../models/room-sticky-events.ts";
51
- import { computeSlotId } from "./utils.ts";
54
+ import { computeSlotId, getSlotEventContent, isSlotClosed, isSlotOpen } from "./utils.ts";
52
55
 
53
56
  /**
54
57
  * Events emitted by MatrixRTCSession
@@ -101,6 +104,15 @@ export interface SessionConfig {
101
104
  */
102
105
  callIntent?: RTCCallIntent;
103
106
 
107
+ /**
108
+ * Application-specific data to publish in our membership alongside the
109
+ * application `type` and `m.call.intent`: in the `application` object of
110
+ * an `m.rtc.member` event, or at the top level of a legacy `m.call.member`
111
+ * one. Keys should be namespaced. Read back through
112
+ * {@link CallMembership.applicationData}.
113
+ */
114
+ applicationData?: Record<string, unknown>;
115
+
104
116
  /**
105
117
  * How long (in milliseconds) the callee's client should keep ringing/waiting for an
106
118
  * answer before the sender gives up and the call notification is considered timed out.
@@ -358,6 +370,16 @@ export class MatrixRTCSession extends TypedEventEmitter<
358
370
  return isSlotClosed(this.roomSubset, this.slotDescription);
359
371
  }
360
372
 
373
+ /**
374
+ * Whether this session's slot is open.
375
+ *
376
+ * @returns `true` if the slot is open, `false` if the slot is closed or `undefined`
377
+ * if no slot exists.
378
+ */
379
+ public isSlotOpen(): boolean | undefined {
380
+ return isSlotOpen(this.roomSubset, this.slotDescription);
381
+ }
382
+
361
383
  /**
362
384
  * Ensures this session's slot is open, sending a slot state event to open (or create) it if needed.
363
385
  *
@@ -721,6 +743,18 @@ export class MatrixRTCSession extends TypedEventEmitter<
721
743
  await this.membershipManager?.updateCallIntent(callIntent);
722
744
  }
723
745
 
746
+ /**
747
+ * Replace the application-specific data in our membership (see
748
+ * {@link SessionConfig.applicationData}), re-sending it if it changed.
749
+ */
750
+ public async updateApplicationData(applicationData: Record<string, unknown>): Promise<void> {
751
+ const myMembership = this.membershipManager?.ownMembership;
752
+ if (!myMembership) {
753
+ throw Error("Not connected yet");
754
+ }
755
+ await this.membershipManager?.updateApplicationData(applicationData);
756
+ }
757
+
724
758
  /**
725
759
  * Re-emit an EncryptionKeyChanged event for each tracked encryption key. This can be used to export
726
760
  * the keys.
@@ -776,11 +810,13 @@ export class MatrixRTCSession extends TypedEventEmitter<
776
810
  callIntent?: RTCCallIntent,
777
811
  ): void {
778
812
  const lifetime = this.joinConfig?.notificationLifetimeMs ?? 90_000;
813
+ const slotId = computeSlotId(this.slotDescription);
779
814
  const sendNotificationEvent = async (): Promise<{
780
815
  response: ISendEventResponse;
781
816
  content: IRTCNotificationContent;
782
817
  }> => {
783
818
  const content: IRTCNotificationContent = {
819
+ "slot_id": slotId,
784
820
  "m.mentions": { user_ids: [], room: true },
785
821
  "notification_type": notificationType,
786
822
  "m.relates_to": {
@@ -789,12 +825,12 @@ export class MatrixRTCSession extends TypedEventEmitter<
789
825
  },
790
826
  "sender_ts": Date.now(),
791
827
  "lifetime": lifetime,
828
+ "msc4354_sticky_key": slotId,
792
829
  };
793
830
  if (callIntent) {
794
831
  content["m.call.intent"] = callIntent;
795
832
  }
796
- const response = await this.client.sendEvent(this.roomSubset.roomId, EventType.RTCNotification, content);
797
- return { response, content };
833
+ return { response: await this.sendNotificationEvent(content), content };
798
834
  };
799
835
 
800
836
  void sendNotificationEvent()
@@ -803,9 +839,30 @@ export class MatrixRTCSession extends TypedEventEmitter<
803
839
  const newResult = { ...notification.response, ...notification.content };
804
840
  this.emit(MatrixRTCSessionEvent.DidSendCallNotification, newResult);
805
841
  })
806
- .catch(([errorLegacy, errorNew]) =>
807
- this.logger.error("Failed to send call notification", errorLegacy, errorNew),
842
+ .catch((error) => this.logger.error("Failed to send call notification", error));
843
+ }
844
+
845
+ /**
846
+ * Sends a notification event, as a sticky event (MSC4354) where the server supports it.
847
+ */
848
+ private async sendNotificationEvent(content: IRTCNotificationContent): Promise<ISendEventResponse> {
849
+ const roomId = this.roomSubset.roomId;
850
+ try {
851
+ // Stay sticky slightly longer than the lifetime, since the server measures the sticky duration
852
+ // from `origin_server_ts` while receivers measure the lifetime from `sender_ts`.
853
+ const stickyDurationMs = content.lifetime + RTC_NOTIFICATION_MAX_SENDER_TS_AHEAD_MS;
854
+ return await this.client._unstable_sendStickyEvent(
855
+ roomId,
856
+ stickyDurationMs,
857
+ null,
858
+ EventType.RTCNotification,
859
+ content,
808
860
  );
861
+ } catch (error) {
862
+ if (!(error instanceof UnsupportedStickyEventsEndpointError)) throw error;
863
+ this.logger.debug("Server does not support sticky events, sending notification as a regular event");
864
+ return await this.client.sendEvent(roomId, EventType.RTCNotification, content);
865
+ }
809
866
  }
810
867
 
811
868
  /**
@@ -843,13 +900,14 @@ export class MatrixRTCSession extends TypedEventEmitter<
843
900
 
844
901
  /**
845
902
  * Call this when something changed that may impacts the current MatrixRTC members in this session.
903
+ *
904
+ * @deprecated use {@link ensureRecalculateSessionMembers} instead.
846
905
  */
847
- // We allow this name schema since this function should only be used for testing purposes.
848
906
  public _onRTCSessionMemberUpdate = async (): Promise<void> => {
849
- await this.recalculateSessionMembers();
907
+ await this.ensureRecalculateSessionMembers();
850
908
  };
851
909
 
852
- // helper variables to make sure we do not have parallel running recalculations.
910
+ // Recalculations are chained onto this promise, so they never run in parallel.
853
911
  private recalculateSessionMembersPromise: Promise<void> = Promise.resolve();
854
912
 
855
913
  /**
@@ -857,7 +915,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
857
915
  * Also ensures that only one recalculation is made at a time.
858
916
  * @returns A promise resolving when the state has been recalculated.
859
917
  */
860
- private ensureRecalculateSessionMembers(): Promise<void> {
918
+ public ensureRecalculateSessionMembers(): Promise<void> {
861
919
  if (this.membershipNeedsRecalculation) {
862
920
  // We have already requested recalcuation, don't attempt a new one.
863
921
  return this.recalculateSessionMembersPromise;
@@ -952,7 +1010,7 @@ async function computeBackendIdentityAndVerifyMemberEvents(
952
1010
  const content = memberEvent.getContent();
953
1011
 
954
1012
  // Quick filter to avoid unneeded processing of invalid events or left events.
955
- if (!quickFilterNonRelevantContents(content, logger)) {
1013
+ if (!quickFilterNonRelevantContents(content)) {
956
1014
  continue;
957
1015
  }
958
1016
 
@@ -970,11 +1028,12 @@ async function computeBackendIdentityAndVerifyMemberEvents(
970
1028
  return callMemberships;
971
1029
  }
972
1030
 
973
- function quickFilterNonRelevantContents(content: IContent, logger: Logger): boolean {
1031
+ function quickFilterNonRelevantContents(content: IContent): boolean {
1032
+ // Don't even bother about left memberships (saves us from costly type/"key in" checks in bigger rooms)
1033
+ if (isLeftMembershipContent(content)) return false;
1034
+
974
1035
  // Ignore sticky keys for the count
975
1036
  const eventKeysCount = Object.keys(content).filter((k) => k !== "msc4354_sticky_key").length;
976
- // Don't even bother about empty events (saves us from costly type/"key in" checks in bigger rooms)
977
- if (eventKeysCount === 0) return false;
978
1037
 
979
1038
  // We first decide if it's a MSC4143 event (per device state key)
980
1039
  if (eventKeysCount > 1 && "application" in content) {
@@ -1016,42 +1075,6 @@ function isValidMembership(
1016
1075
  return true;
1017
1076
  }
1018
1077
 
1019
- /**
1020
- * Reads the slot state event's content for the given slot description.
1021
- *
1022
- * @returns The slot event's content, or `undefined` if no slot event exists for the given description.
1023
- */
1024
- function getSlotEventContent(
1025
- room: Pick<Room, "getLiveTimeline">,
1026
- slotDescription: SlotDescription,
1027
- ): RtcSlotEventContent | undefined {
1028
- const slotId = computeSlotId(slotDescription);
1029
- const slotEvent = room
1030
- .getLiveTimeline()
1031
- .getState(EventTimeline.FORWARDS)
1032
- ?.getStateEvents(EventType.RTCSlot, slotId);
1033
- if (!slotEvent) return undefined;
1034
-
1035
- return slotEvent.getContent<RtcSlotEventContent>();
1036
- }
1037
-
1038
- /**
1039
- * Whether the given slot is closed.
1040
- *
1041
- * @returns `true` if the slot is closed, `false` if the slot is open or `undefined`
1042
- * if no slot exists.
1043
- */
1044
- function isSlotClosed(room: Pick<Room, "getLiveTimeline">, slotDescription: SlotDescription): boolean | undefined {
1045
- const content = getSlotEventContent(room, slotDescription) as Partial<RtcSlotEventContent> | undefined;
1046
- if (content === undefined) return undefined;
1047
-
1048
- return (
1049
- content.status !== "open" ||
1050
- typeof content.application !== "object" ||
1051
- content.application?.type !== slotDescription.application
1052
- );
1053
- }
1054
-
1055
1078
  /**
1056
1079
  * Collects the raw member events from room state and sticky store.
1057
1080
  */
@@ -172,7 +172,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
172
172
  // Alternatively we would need to setup some event emission when the RTC session ended.
173
173
  // TODO we want to add the emission en session end. This makes the responsibility of the session manager more clear.
174
174
 
175
- await session._onRTCSessionMemberUpdate().catch((error) => {
175
+ await session.ensureRecalculateSessionMembers().catch((error) => {
176
176
  this.logger.error(`Error updating RTC session members for ${room.roomId}: ${error}`);
177
177
  });
178
178