matrix-js-sdk 41.4.0 → 41.5.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 (109) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -0
  3. package/lib/@types/json.d.ts +7 -0
  4. package/lib/@types/json.d.ts.map +1 -0
  5. package/lib/@types/json.js +1 -0
  6. package/lib/@types/json.js.map +1 -0
  7. package/lib/@types/requests.d.ts +6 -9
  8. package/lib/@types/requests.d.ts.map +1 -1
  9. package/lib/@types/requests.js.map +1 -1
  10. package/lib/client.d.ts +17 -2
  11. package/lib/client.d.ts.map +1 -1
  12. package/lib/client.js +27 -12
  13. package/lib/client.js.map +1 -1
  14. package/lib/filter.d.ts +20 -5
  15. package/lib/filter.d.ts.map +1 -1
  16. package/lib/filter.js +21 -0
  17. package/lib/filter.js.map +1 -1
  18. package/lib/models/user.d.ts +5 -0
  19. package/lib/models/user.d.ts.map +1 -1
  20. package/lib/models/user.js +5 -0
  21. package/lib/models/user.js.map +1 -1
  22. package/lib/oidc/authorize.d.ts +60 -0
  23. package/lib/oidc/authorize.d.ts.map +1 -1
  24. package/lib/oidc/authorize.js +115 -2
  25. package/lib/oidc/authorize.js.map +1 -1
  26. package/lib/oidc/register.d.ts.map +1 -1
  27. package/lib/oidc/register.js +5 -0
  28. package/lib/oidc/register.js.map +1 -1
  29. package/lib/rendezvous/MSC4108SignInWithQR.d.ts +19 -2
  30. package/lib/rendezvous/MSC4108SignInWithQR.d.ts.map +1 -1
  31. package/lib/rendezvous/MSC4108SignInWithQR.js +126 -36
  32. package/lib/rendezvous/MSC4108SignInWithQR.js.map +1 -1
  33. package/lib/rendezvous/channels/MSC4108SecureChannel.d.ts.map +1 -1
  34. package/lib/rendezvous/channels/MSC4108SecureChannel.js +4 -2
  35. package/lib/rendezvous/channels/MSC4108SecureChannel.js.map +1 -1
  36. package/lib/rendezvous/index.d.ts +36 -0
  37. package/lib/rendezvous/index.d.ts.map +1 -1
  38. package/lib/rendezvous/index.js +115 -0
  39. package/lib/rendezvous/index.js.map +1 -1
  40. package/lib/rendezvous/transports/MSC4108RendezvousSession.d.ts +1 -1
  41. package/lib/rendezvous/transports/MSC4108RendezvousSession.d.ts.map +1 -1
  42. package/lib/rendezvous/transports/MSC4108RendezvousSession.js.map +1 -1
  43. package/lib/rust-crypto/rust-crypto.d.ts.map +1 -1
  44. package/lib/rust-crypto/rust-crypto.js +2 -2
  45. package/lib/rust-crypto/rust-crypto.js.map +1 -1
  46. package/lib/store/index.d.ts +17 -1
  47. package/lib/store/index.d.ts.map +1 -1
  48. package/lib/store/index.js.map +1 -1
  49. package/lib/store/indexeddb-backend.d.ts +4 -0
  50. package/lib/store/indexeddb-backend.d.ts.map +1 -1
  51. package/lib/store/indexeddb-backend.js.map +1 -1
  52. package/lib/store/indexeddb-local-backend.d.ts +4 -1
  53. package/lib/store/indexeddb-local-backend.d.ts.map +1 -1
  54. package/lib/store/indexeddb-local-backend.js +45 -3
  55. package/lib/store/indexeddb-local-backend.js.map +1 -1
  56. package/lib/store/indexeddb-remote-backend.d.ts +4 -0
  57. package/lib/store/indexeddb-remote-backend.d.ts.map +1 -1
  58. package/lib/store/indexeddb-remote-backend.js +21 -3
  59. package/lib/store/indexeddb-remote-backend.js.map +1 -1
  60. package/lib/store/indexeddb-store-worker.d.ts.map +1 -1
  61. package/lib/store/indexeddb-store-worker.js +10 -1
  62. package/lib/store/indexeddb-store-worker.js.map +1 -1
  63. package/lib/store/indexeddb.d.ts +4 -0
  64. package/lib/store/indexeddb.d.ts.map +1 -1
  65. package/lib/store/indexeddb.js +18 -0
  66. package/lib/store/indexeddb.js.map +1 -1
  67. package/lib/store/memory.d.ts +5 -1
  68. package/lib/store/memory.d.ts.map +1 -1
  69. package/lib/store/memory.js +19 -0
  70. package/lib/store/memory.js.map +1 -1
  71. package/lib/store/stub.d.ts +3 -0
  72. package/lib/store/stub.d.ts.map +1 -1
  73. package/lib/store/stub.js +15 -0
  74. package/lib/store/stub.js.map +1 -1
  75. package/lib/sync-accumulator.d.ts +15 -0
  76. package/lib/sync-accumulator.d.ts.map +1 -1
  77. package/lib/sync-accumulator.js +4 -0
  78. package/lib/sync-accumulator.js.map +1 -1
  79. package/lib/sync.d.ts +9 -1
  80. package/lib/sync.d.ts.map +1 -1
  81. package/lib/sync.js +51 -9
  82. package/lib/sync.js.map +1 -1
  83. package/lib/webrtc/call.d.ts.map +1 -1
  84. package/lib/webrtc/call.js +1 -2
  85. package/lib/webrtc/call.js.map +1 -1
  86. package/package.json +7 -7
  87. package/src/@types/json.ts +16 -0
  88. package/src/@types/requests.ts +6 -9
  89. package/src/client.ts +40 -12
  90. package/src/filter.ts +31 -5
  91. package/src/models/user.ts +6 -0
  92. package/src/oidc/authorize.ts +135 -2
  93. package/src/oidc/register.ts +5 -0
  94. package/src/rendezvous/MSC4108SignInWithQR.ts +117 -4
  95. package/src/rendezvous/channels/MSC4108SecureChannel.ts +10 -2
  96. package/src/rendezvous/index.ts +115 -0
  97. package/src/rendezvous/transports/MSC4108RendezvousSession.ts +1 -1
  98. package/src/rust-crypto/rust-crypto.ts +6 -3
  99. package/src/store/index.ts +20 -1
  100. package/src/store/indexeddb-backend.ts +4 -0
  101. package/src/store/indexeddb-local-backend.ts +32 -1
  102. package/src/store/indexeddb-remote-backend.ts +13 -0
  103. package/src/store/indexeddb-store-worker.ts +9 -0
  104. package/src/store/indexeddb.ts +13 -0
  105. package/src/store/memory.ts +14 -1
  106. package/src/store/stub.ts +12 -0
  107. package/src/sync-accumulator.ts +16 -1
  108. package/src/sync.ts +48 -4
  109. package/src/webrtc/call.ts +1 -2
@@ -14,6 +14,14 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { type MatrixClient, OAuthGrantType, type OidcClientConfig } from "../matrix.ts";
18
+ import { MSC4108FailureReason, type RendezvousFailureListener } from "./RendezvousFailureReason.ts";
19
+ import { MSC4108SignInWithQR } from "./MSC4108SignInWithQR.ts";
20
+ import { MSC4108RendezvousSession } from "./transports/MSC4108RendezvousSession.ts";
21
+ import { MSC4108SecureChannel } from "./channels/MSC4108SecureChannel.ts";
22
+ import { RendezvousIntent } from "./RendezvousIntent.ts";
23
+ import { logger } from "../logger.ts";
24
+
17
25
  export * from "./MSC4108SignInWithQR.ts";
18
26
  export type * from "./RendezvousChannel.ts";
19
27
  export type * from "./RendezvousCode.ts";
@@ -23,3 +31,110 @@ export * from "./RendezvousIntent.ts";
23
31
  export type * from "./RendezvousTransport.ts";
24
32
  export * from "./transports/index.ts";
25
33
  export * from "./channels/index.ts";
34
+
35
+ /**
36
+ * Check if the homeserver that the client is connected to supports a variant of sign-in with QR that we can use.
37
+ *
38
+ * @param client the client to check for sign-in with QR support
39
+ * @returns true if the homeserver that the client is connected to supports a variant of sign-in with QR that we can use, false otherwise.
40
+ */
41
+ export async function isSignInWithQRAvailable(client: MatrixClient): Promise<boolean> {
42
+ let metadata: OidcClientConfig;
43
+ try {
44
+ metadata = await client.getAuthMetadata();
45
+ } catch (e) {
46
+ logger.warn("Failed to fetch auth metadata, assuming sign-in with QR is unavailable", e);
47
+ return false;
48
+ }
49
+
50
+ // check for support of device authorization grant
51
+ if (!metadata.grant_types_supported.includes(OAuthGrantType.DeviceAuthorization)) {
52
+ return false;
53
+ }
54
+
55
+ // check for unstable support for MSC4108 2024 version
56
+ return client.doesServerSupportUnstableFeature("org.matrix.msc4108");
57
+ }
58
+
59
+ /**
60
+ * Start a linking flow from an existing authenticated client by generating a QR code that can be scanned by the new device.
61
+ * The new device will then authenticate with the server and link itself to the same account as the existing client and
62
+ * share the end-to-end encryption keys.
63
+ *
64
+ * @param client the existing client
65
+ * @param onFailure callback for when the linking process fails
66
+ * @param abortSignal an AbortSignal that can be used to cancel the linking process,
67
+ * for example when the user cancels out of the flow.
68
+ * This will unbind the {@link onFailure} callback and prevent any further steps in the flow from being executed.
69
+ * @returns a promise that resolves to an instance of the linking flow
70
+ */
71
+ export async function linkNewDeviceByGeneratingQR(
72
+ client: MatrixClient,
73
+ onFailure: RendezvousFailureListener,
74
+ abortSignal: AbortSignal,
75
+ ): Promise<MSC4108SignInWithQR> {
76
+ // we assume rust crypto is already initialised
77
+ return initGenerateQrFlow(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE, client, onFailure, abortSignal);
78
+ }
79
+
80
+ /**
81
+ * Start a sign-in flow by generating a QR code that can be scanned by an existing authenticated client.
82
+ * The existing client will then help complete the authentication of the new device and link it to the same account,
83
+ * sharing the end-to-end encryption keys.
84
+ *
85
+ * @param tempClient temporary client used during the flow for the rendezvous channel
86
+ * @param onFailure callback for when the sign-in process fails
87
+ * @param abortSignal an AbortSignal that can be used to cancel the linking process,
88
+ * for example when the user cancels out of the flow.
89
+ * This will unbind the {@link onFailure} callback and prevent any further steps in the flow from being executed.
90
+ * @returns a promise that resolves to an instance of the sign-in flow
91
+ */
92
+ export async function signInByGeneratingQR(
93
+ tempClient: MatrixClient,
94
+ onFailure: RendezvousFailureListener,
95
+ abortSignal: AbortSignal,
96
+ ): Promise<MSC4108SignInWithQR> {
97
+ // ensure rust crypto is initialized as needed for the secure channel
98
+ const RustSdkCryptoJs = await import("@matrix-org/matrix-sdk-crypto-wasm");
99
+ await RustSdkCryptoJs.initAsync();
100
+
101
+ return initGenerateQrFlow(RendezvousIntent.LOGIN_ON_NEW_DEVICE, tempClient, onFailure, abortSignal);
102
+ }
103
+
104
+ async function initGenerateQrFlow(
105
+ intent: RendezvousIntent,
106
+ client: MatrixClient,
107
+ onFailure: RendezvousFailureListener,
108
+ abortSignal: AbortSignal,
109
+ ): Promise<MSC4108SignInWithQR> {
110
+ const session = new MSC4108RendezvousSession({
111
+ onFailure,
112
+ client,
113
+ });
114
+ const channel = new MSC4108SecureChannel(session, undefined, onFailure);
115
+ const flow = new MSC4108SignInWithQR(
116
+ channel,
117
+ false,
118
+ intent === RendezvousIntent.LOGIN_ON_NEW_DEVICE ? undefined : client,
119
+ onFailure,
120
+ );
121
+
122
+ if (abortSignal.aborted) return flow;
123
+
124
+ abortSignal.onabort = (): void => {
125
+ // Detach failure handlers
126
+ session.onFailure = undefined;
127
+ channel.onFailure = undefined;
128
+ flow.onFailure = undefined;
129
+ // Cancel the session
130
+ flow.cancel(MSC4108FailureReason.UserCancelled);
131
+ };
132
+
133
+ await session.send(""); // open channel
134
+
135
+ if (!abortSignal.aborted) {
136
+ await flow.generateCode();
137
+ }
138
+
139
+ return flow;
140
+ }
@@ -30,7 +30,7 @@ export class MSC4108RendezvousSession {
30
30
  private readonly client?: MatrixClient;
31
31
  private readonly fallbackRzServer?: string;
32
32
  private readonly fetchFn?: typeof globalThis.fetch;
33
- private readonly onFailure?: RendezvousFailureListener;
33
+ public onFailure?: RendezvousFailureListener;
34
34
  private etag?: string;
35
35
  private expiresAt?: Date;
36
36
  private expiresTimer?: ReturnType<typeof setTimeout>;
@@ -2596,7 +2596,7 @@ function rustEncryptionInfoToJsEncryptionInfo(
2596
2596
  }
2597
2597
 
2598
2598
  interface RoomKeyBundleMessage {
2599
- type: "io.element.msc4268.room_key_bundle";
2599
+ type: "m.room_key_bundle" | "io.element.msc4268.room_key_bundle";
2600
2600
  content: {
2601
2601
  room_id: string;
2602
2602
  };
@@ -2606,13 +2606,16 @@ interface RoomKeyBundleMessage {
2606
2606
  * Determines if the given payload is a RoomKeyBundleMessage.
2607
2607
  *
2608
2608
  * A RoomKeyBundleMessage is identified by having a specific message type
2609
- * ("io.element.msc4268.room_key_bundle") and a valid room_id in its content.
2609
+ * ("m.room_key_bundle") and a valid room_id in its content.
2610
2610
  *
2611
2611
  * @param message - The received to-device message to check.
2612
2612
  * @returns True if the payload matches the RoomKeyBundleMessage structure, false otherwise.
2613
2613
  */
2614
2614
  function isRoomKeyBundleMessage(message: IToDeviceEvent): message is IToDeviceEvent & RoomKeyBundleMessage {
2615
- return message.type === "io.element.msc4268.room_key_bundle" && typeof message.content.room_id === "string";
2615
+ return (
2616
+ (message.type === "io.element.msc4268.room_key_bundle" || message.type === "m.room_key_bundle") &&
2617
+ typeof message.content.room_id === "string"
2618
+ );
2616
2619
  }
2617
2620
 
2618
2621
  type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent];
@@ -16,7 +16,7 @@ limitations under the License.
16
16
 
17
17
  import { type EventType } from "../@types/event.ts";
18
18
  import { type Room } from "../models/room.ts";
19
- import { type User } from "../models/user.ts";
19
+ import { type SyncUserProfile, type User } from "../models/user.ts";
20
20
  import { type IEvent, type MatrixEvent } from "../models/event.ts";
21
21
  import { type Filter } from "../filter.ts";
22
22
  import { type RoomSummary } from "../models/room-summary.ts";
@@ -254,6 +254,25 @@ export interface IStore {
254
254
  */
255
255
  removeToDeviceBatch(id: number): Promise<void>;
256
256
 
257
+ /**
258
+ * Store user profile details from a sync. Existing profiles will be overwritten.
259
+ * @param userProfiles - A map of userIds to profiles.
260
+ */
261
+ storeUserProfiles(userProfiles: Map<string, SyncUserProfile>): Promise<void>;
262
+
263
+ /**
264
+ * Delete stored profiles for the given users.
265
+ * @param userIds - The user IDs whose profiles should be deleted.
266
+ */
267
+ removeUserProfiles(userIds: string[]): Promise<void>;
268
+
269
+ /**
270
+ * Retrieve a stored user profile for the given user ID, if it exists.
271
+ * @param userId - The user ID to retrieve the profile for.
272
+ * @returns The stored profile, or undefined if no profile is stored for this user ID.
273
+ */
274
+ getUserProfile(userId: string): Promise<SyncUserProfile | undefined>;
275
+
257
276
  /**
258
277
  * Stop the store and perform any appropriate cleanup
259
278
  */
@@ -17,6 +17,7 @@ limitations under the License.
17
17
  import { type ISavedSync } from "./index.ts";
18
18
  import { type IEvent, type IStateEventWithRoomId, type IStoredClientOpts, type ISyncResponse } from "../matrix.ts";
19
19
  import { type IndexedToDeviceBatch, type ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage.ts";
20
+ import { type SyncUserProfile } from "../models/user.ts";
20
21
 
21
22
  export interface IIndexedDBBackend {
22
23
  connect(onClose?: () => void): Promise<void>;
@@ -35,6 +36,9 @@ export interface IIndexedDBBackend {
35
36
  saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void>;
36
37
  getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null>;
37
38
  removeToDeviceBatch(id: number): Promise<void>;
39
+ getUserProfile(userId: string): Promise<SyncUserProfile | undefined>;
40
+ storeUserProfiles(userProfiles: Map<string, SyncUserProfile>): Promise<void>;
41
+ removeUserProfiles(userIds: string[]): Promise<void>;
38
42
  destroy(): Promise<void>;
39
43
  }
40
44
 
@@ -18,7 +18,7 @@ import { type IMinimalEvent, type ISyncData, type ISyncResponse, SyncAccumulator
18
18
  import { deepCopy, promiseTry } from "../utils.ts";
19
19
  import { exists as idbExists } from "../indexeddb-helpers.ts";
20
20
  import { logger } from "../logger.ts";
21
- import { type IStateEventWithRoomId, type IStoredClientOpts } from "../matrix.ts";
21
+ import type { SyncUserProfile, IStateEventWithRoomId, IStoredClientOpts } from "../matrix.ts";
22
22
  import { type ISavedSync } from "./index.ts";
23
23
  import { type IIndexedDBBackend, type UserTuple } from "./indexeddb-backend.ts";
24
24
  import { type IndexedToDeviceBatch, type ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage.ts";
@@ -48,6 +48,9 @@ const DB_MIGRATIONS: DbMigration[] = [
48
48
  (db): void => {
49
49
  db.createObjectStore("to_device_queue", { autoIncrement: true });
50
50
  },
51
+ (db): void => {
52
+ db.createObjectStore("user_profile", { keyPath: ["userId"] });
53
+ },
51
54
  // Expand as needed.
52
55
  ];
53
56
  const VERSION = DB_MIGRATIONS.length;
@@ -602,6 +605,34 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
602
605
  await txnAsPromise(txn);
603
606
  }
604
607
 
608
+ public async getUserProfile(userId: string): Promise<SyncUserProfile | undefined> {
609
+ return Promise.resolve().then(() => {
610
+ const txn = this.db!.transaction(["user_profile"], "readonly");
611
+ const store = txn.objectStore("user_profile");
612
+ return selectQuery(store, [userId], (cursor) => {
613
+ return cursor.value?.profile;
614
+ }).then((results) => results[0]);
615
+ });
616
+ }
617
+
618
+ public async storeUserProfiles(userProfiles: Map<string, SyncUserProfile>): Promise<void> {
619
+ const txn = this.db!.transaction(["user_profile"], "readwrite");
620
+ const store = txn.objectStore("user_profile");
621
+ for (const [userId, profile] of userProfiles.entries()) {
622
+ store.put({ profile, userId });
623
+ }
624
+ await txnAsPromise(txn);
625
+ }
626
+
627
+ public async removeUserProfiles(userIds: string[]): Promise<void> {
628
+ const txn = this.db!.transaction(["user_profile"], "readwrite");
629
+ const store = txn.objectStore("user_profile");
630
+ for (const userId of userIds) {
631
+ store.delete([userId]);
632
+ }
633
+ await txnAsPromise(txn);
634
+ }
635
+
605
636
  /*
606
637
  * Close the database
607
638
  */
@@ -20,6 +20,7 @@ import { type IStoredClientOpts } from "../client.ts";
20
20
  import { type IStateEventWithRoomId, type ISyncResponse } from "../matrix.ts";
21
21
  import { type IIndexedDBBackend, type UserTuple } from "./indexeddb-backend.ts";
22
22
  import { type IndexedToDeviceBatch, type ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage.ts";
23
+ import { type SyncUserProfile } from "../models/user.ts";
23
24
 
24
25
  export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
25
26
  private worker?: Worker;
@@ -145,6 +146,18 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
145
146
  return this.doCmd("removeToDeviceBatch", [id]);
146
147
  }
147
148
 
149
+ public async getUserProfile(userId: string): Promise<SyncUserProfile | undefined> {
150
+ return this.doCmd("getUserProfile", [userId]);
151
+ }
152
+
153
+ public async storeUserProfiles(userProfiles: Map<string, SyncUserProfile>): Promise<void> {
154
+ await this.doCmd("storeUserProfiles", [userProfiles]);
155
+ }
156
+
157
+ public async removeUserProfiles(userIds: string[]): Promise<void> {
158
+ await this.doCmd("removeUserProfiles", [userIds]);
159
+ }
160
+
148
161
  private ensureStarted(): Promise<void> {
149
162
  if (!this.startPromise) {
150
163
  this.worker = this.workerFactory();
@@ -120,6 +120,15 @@ export class IndexedDBStoreWorker {
120
120
  case "removeToDeviceBatch":
121
121
  prom = this.backend?.removeToDeviceBatch(msg.args[0]);
122
122
  break;
123
+ case "getUserProfile":
124
+ prom = this.backend?.getUserProfile(msg.args[0]);
125
+ break;
126
+ case "storeUserProfiles":
127
+ prom = this.backend?.storeUserProfiles(msg.args[0]);
128
+ break;
129
+ case "removeUserProfiles":
130
+ prom = this.backend?.removeUserProfiles(msg.args[0]);
131
+ break;
123
132
  }
124
133
 
125
134
  if (prom === undefined) {
@@ -28,6 +28,7 @@ import { type EventEmitterEvents, TypedEventEmitter } from "../models/typed-even
28
28
  import { type IStateEventWithRoomId } from "../@types/search.ts";
29
29
  import { type IndexedToDeviceBatch, type ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage.ts";
30
30
  import { type IStoredClientOpts } from "../client.ts";
31
+ import { type SyncUserProfile } from "../models/user.ts";
31
32
 
32
33
  /**
33
34
  * This is an internal module. See {@link IndexedDBStore} for the public class.
@@ -384,6 +385,18 @@ export class IndexedDBStore extends MemoryStore {
384
385
  public removeToDeviceBatch(id: number): Promise<void> {
385
386
  return this.backend.removeToDeviceBatch(id);
386
387
  }
388
+
389
+ public async getUserProfile(userId: string): Promise<SyncUserProfile | undefined> {
390
+ return this.backend.getUserProfile(userId);
391
+ }
392
+
393
+ public async storeUserProfiles(userProfiles: Map<string, SyncUserProfile>): Promise<void> {
394
+ return this.backend.storeUserProfiles(userProfiles);
395
+ }
396
+
397
+ public async removeUserProfiles(userIds: string[]): Promise<void> {
398
+ return this.backend.removeUserProfiles(userIds);
399
+ }
387
400
  }
388
401
 
389
402
  /**
@@ -20,7 +20,7 @@ limitations under the License.
20
20
 
21
21
  import { type EventType } from "../@types/event.ts";
22
22
  import { type Room } from "../models/room.ts";
23
- import { type User } from "../models/user.ts";
23
+ import { type SyncUserProfile, type User } from "../models/user.ts";
24
24
  import { type IEvent, type MatrixEvent } from "../models/event.ts";
25
25
  import { type RoomState, RoomStateEvent } from "../models/room-state.ts";
26
26
  import { type RoomMember } from "../models/room-member.ts";
@@ -65,6 +65,7 @@ export class MemoryStore implements IStore {
65
65
  private pendingToDeviceBatches: IndexedToDeviceBatch[] = [];
66
66
  private nextToDeviceBatchId = 0;
67
67
  protected createUser?: UserCreator;
68
+ public readonly userProfiles = new Map<string, SyncUserProfile>();
68
69
 
69
70
  /**
70
71
  * Construct a new in-memory data store for the Matrix Client.
@@ -442,6 +443,18 @@ export class MemoryStore implements IStore {
442
443
  return Promise.resolve();
443
444
  }
444
445
 
446
+ public async getUserProfile(userId: string): Promise<SyncUserProfile | undefined> {
447
+ return this.userProfiles.get(userId);
448
+ }
449
+
450
+ public async storeUserProfiles(userProfiles: Map<string, SyncUserProfile>): Promise<void> {
451
+ userProfiles.forEach((profile, userId) => this.userProfiles.set(userId, profile));
452
+ }
453
+
454
+ public async removeUserProfiles(userIds: string[]): Promise<void> {
455
+ userIds.forEach((userId) => this.userProfiles.delete(userId));
456
+ }
457
+
445
458
  public async destroy(): Promise<void> {
446
459
  // Nothing to do
447
460
  }
package/src/store/stub.ts CHANGED
@@ -274,6 +274,18 @@ export class StubStore implements IStore {
274
274
  return Promise.resolve();
275
275
  }
276
276
 
277
+ public async getUserProfile(): Promise<undefined> {
278
+ return undefined;
279
+ }
280
+
281
+ public async storeUserProfiles(): Promise<void> {
282
+ return;
283
+ }
284
+
285
+ public async removeUserProfiles(): Promise<void> {
286
+ return;
287
+ }
288
+
277
289
  public async destroy(): Promise<void> {
278
290
  // Nothing to do
279
291
  }
@@ -26,6 +26,7 @@ import { type EventType } from "./@types/event.ts";
26
26
  import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.ts";
27
27
  import { ReceiptAccumulator } from "./receipt-accumulator.ts";
28
28
  import { type OlmEncryptionInfo } from "./crypto-api/index.ts";
29
+ import { type SyncUserProfile } from "./matrix.ts";
29
30
 
30
31
  interface IOpts {
31
32
  /**
@@ -36,6 +37,10 @@ interface IOpts {
36
37
  * Default: 50.
37
38
  */
38
39
  maxTimelineEntries?: number;
40
+ /**
41
+ * Whether to use the stable or unstable fields for user profiles.
42
+ */
43
+ profileFieldsStable?: boolean;
39
44
  }
40
45
 
41
46
  export interface IMinimalEvent {
@@ -183,6 +188,15 @@ export interface IDeviceLists {
183
188
  left?: string[];
184
189
  }
185
190
 
191
+ /**
192
+ * The "users" section of the sync update which contains extended profile updates.
193
+ */
194
+ export interface UsersUpdate {
195
+ [userId: string]: {
196
+ profile_updates?: SyncUserProfile | null;
197
+ };
198
+ }
199
+
186
200
  export interface ISyncResponse {
187
201
  "next_batch": string;
188
202
  "rooms": IRooms;
@@ -191,7 +205,8 @@ export interface ISyncResponse {
191
205
  "to_device"?: IToDevice;
192
206
  "device_lists"?: IDeviceLists;
193
207
  "device_one_time_keys_count"?: Record<string, number>;
194
-
208
+ "users"?: UsersUpdate;
209
+ "org.matrix.msc4429.users"?: UsersUpdate;
195
210
  "device_unused_fallback_key_types"?: string[];
196
211
  "org.matrix.msc2732.device_unused_fallback_key_types"?: string[];
197
212
  }
package/src/sync.ts CHANGED
@@ -24,12 +24,12 @@ limitations under the License.
24
24
  */
25
25
 
26
26
  import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts";
27
- import { User } from "./models/user.ts";
27
+ import { type SyncUserProfile, User } from "./models/user.ts";
28
28
  import { NotificationCountType, Room, RoomEvent } from "./models/room.ts";
29
29
  import { deepCopy, noUnsafeEventProps, unsafeProp } from "./utils.ts";
30
30
  import { Filter } from "./filter.ts";
31
31
  import { EventTimeline } from "./models/event-timeline.ts";
32
- import { type Logger } from "./logger.ts";
32
+ import { logger, type Logger } from "./logger.ts";
33
33
  import {
34
34
  ClientEvent,
35
35
  type IStoredClientOpts,
@@ -622,7 +622,11 @@ export class SyncApi {
622
622
  return filter;
623
623
  };
624
624
 
625
- private prepareLazyLoadingForSync = async (): Promise<void> => {
625
+ /**
626
+ * Sets up the sync filter options for lazy loading if enabled,
627
+ * (or force-disables lazy loading entirely if we're a guest).
628
+ */
629
+ private prepareSyncFilterLazyLoading = (): void => {
626
630
  this.syncOpts.logger.debug("Prepare lazy loading for sync...");
627
631
  if (this.client.isGuest()) {
628
632
  this.opts.lazyLoadMembers = false;
@@ -636,6 +640,22 @@ export class SyncApi {
636
640
  }
637
641
  };
638
642
 
643
+ /**
644
+ * Preare sync filter options for the unstable MSC4429 user profile fields if enabled.
645
+ */
646
+ private prepareSyncFilterUserProfiles = async (): Promise<void> => {
647
+ if (this.opts.unstableMSC4429SyncUserProfileFields?.length) {
648
+ this.syncOpts.logger.debug("Enabling EXPERIMENTAL user profiles on sync filter...");
649
+ if (!this.opts.filter) {
650
+ this.opts.filter = this.buildDefaultFilter();
651
+ }
652
+ this.opts.filter.setUnstableMSC4429SyncUserProfiles(
653
+ this.opts.unstableMSC4429SyncUserProfileFields,
654
+ await this.client.doesServerSupportUnstableFeature("org.matrix.msc4429.stable"),
655
+ );
656
+ }
657
+ };
658
+
639
659
  private storeClientOptions = async (): Promise<void> => {
640
660
  try {
641
661
  this.syncOpts.logger.debug("Storing client options...");
@@ -723,7 +743,8 @@ export class SyncApi {
723
743
  // take a while so if we set it going now, we can wait for it
724
744
  // to finish while we process our saved sync data.
725
745
  await this.getPushRules();
726
- await this.prepareLazyLoadingForSync();
746
+ this.prepareSyncFilterLazyLoading();
747
+ await this.prepareSyncFilterUserProfiles();
727
748
  await this.storeClientOptions();
728
749
 
729
750
  const { filterId, filter } = await this.getFilter();
@@ -1138,6 +1159,29 @@ export class SyncApi {
1138
1159
  });
1139
1160
  }
1140
1161
 
1162
+ // handle user profile updates (MSC4429)
1163
+ const userUpdate = data["users"] ?? data["org.matrix.msc4429.users"];
1164
+ if (typeof userUpdate === "object" && userUpdate !== null) {
1165
+ const usersToRemove: string[] = [];
1166
+ const profilesToAmend: Map<string, SyncUserProfile> = new Map();
1167
+ for (const [userId, userData] of Object.entries(userUpdate)) {
1168
+ logger.info(`Storing user profile ${userId}`, userData);
1169
+ if (userData.profile_updates) {
1170
+ client.emit(ClientEvent.UserProfileUpdate, userId, userData.profile_updates);
1171
+ const existingProfile = await client.store.getUserProfile(userId);
1172
+ profilesToAmend.set(userId, { ...existingProfile, ...userData.profile_updates });
1173
+ } else if (userData.profile_updates === null) {
1174
+ usersToRemove.push(userId);
1175
+ }
1176
+ }
1177
+ if (usersToRemove.length) {
1178
+ await client.store.removeUserProfiles(usersToRemove);
1179
+ }
1180
+ if (profilesToAmend.size) {
1181
+ await client.store.storeUserProfiles(profilesToAmend);
1182
+ }
1183
+ }
1184
+
1141
1185
  // handle to-device events
1142
1186
  if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) {
1143
1187
  const toDeviceMessages: IToDeviceEvent[] = data.to_device.events.filter(noUnsafeEventProps);
@@ -21,7 +21,6 @@ limitations under the License.
21
21
  * This is an internal module. See {@link createNewMatrixCall} for the public API.
22
22
  */
23
23
 
24
- import { v4 as uuidv4 } from "uuid";
25
24
  import { parse as parseSdp, write as writeSdp } from "sdp-transform";
26
25
 
27
26
  import { logger } from "../logger.ts";
@@ -2490,7 +2489,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
2490
2489
  sender_session_id: this.client.getSessionId(),
2491
2490
  dest_session_id: this.opponentSessionId,
2492
2491
  seq: toDeviceSeq,
2493
- [ToDeviceMessageId]: uuidv4(),
2492
+ [ToDeviceMessageId]: globalThis.crypto.randomUUID(),
2494
2493
  };
2495
2494
 
2496
2495
  this.emit(