Compare commits

...

24 Commits

Author SHA1 Message Date
RiotRobot 1ae0c2f3ee v34.3.0 2024-08-13 12:05:45 +00:00
RiotRobot de50129a53 v34.3.0-rc.1 2024-08-06 12:26:55 +00:00
Michael Telatynski 5568dfdd41 Move olm to dependencies as its types are needed downstream
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-08-06 13:25:55 +01:00
RiotRobot 39216d44ed v34.3.0-rc.0 2024-08-06 12:03:46 +00:00
Michael Telatynski 8c3b249567 Re-add olm dependency which is needed for types
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-08-06 12:00:30 +01:00
Michael Telatynski b8e40ad2a8 Resetting package fields for development 2024-08-06 11:46:18 +01:00
Michael Telatynski 4e2831764d Merge branch 'master' into develop 2024-08-06 11:46:05 +01:00
Michael Telatynski 09780672aa Fix release-gitflow.yml node version 2024-08-06 11:42:35 +01:00
Andrew Ferrazzutti 0fe53876ec Bump matrix-widget-api (#4336) 2024-08-02 12:10:24 +00:00
Michael Telatynski dfec3dc33c Make code tsc es2022 compliant (#4335)
* Remove redundant global.d.ts definitions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove roomId overload

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update base.ts

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-08-01 15:16:59 +00:00
Andrew Ferrazzutti fbdd78b428 Also check for MSC3757 for session state keys (#4334)
Do not prefix the non-legacy session membership state keys with an
underscore for rooms using MSC3757 (in addtion to MSC3779).
2024-08-01 14:57:29 +00:00
Andrew Ferrazzutti e10c362ef0 Support MSC4157: delayed events via Widget API (#4311) 2024-08-01 14:17:52 +00:00
David Baker 89a9a7fa38 Fix hashed ID server lookups with no Olm (#4333)
* Fix hashed ID server lookups with no Olm

It used the hash function from Olm (presumably to work cross-platform)
but subtle crypto is available on node nowadays so we can just use
that.

Refactor existing code that did this out to a common function, add
tests.

* Test the code when crypto is available

* Test case of no crypto available

* Move digest file to src to get it out of the way of the olm / e2e stuff

* Fix import

* Fix error string & doc

* subtle crypto, not webcrypto

* Extract the base64 part

* Fix test

* Move test file too

* Add more doc

* Fix imports
2024-08-01 10:55:23 +00:00
Andrew Ferrazzutti 687d08dc9d Support MSC4140: Delayed events (#4294)
and use them for more reliable MatrixRTC session membership events.

Also implement "parent" delayed events, which were in a previous version
of the MSC and may be reintroduced or be part of a new MSC later.

NOTE: Still missing is support for sending encrypted delayed events.
2024-07-30 12:43:25 +00:00
RiotRobot 7f91db83d0 v34.2.0 2024-07-30 12:37:58 +00:00
RiotRobot 5feab37166 v34.2.0-rc.0 2024-07-23 11:58:56 +00:00
Michael Telatynski 1a02835ab2 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into staging 2024-07-23 12:51:48 +01:00
RiotRobot 4d90fecb6a v34.1.0 2024-07-16 12:20:13 +00:00
RiotRobot 6520e0f54f v34.1.0-rc.3 2024-07-09 13:15:09 +00:00
Michael Telatynski ed7b314e6a Promote olm to a real dep given the types refer to it
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 14:13:50 +01:00
RiotRobot 24eff501e4 v34.1.0-rc.2 2024-07-09 12:17:59 +00:00
RiotRobot a0d73dfaca v34.1.0-rc.1 2024-07-09 12:12:53 +00:00
Michael Telatynski 5d2500b7a7 Fix bump-downstreams using incompatible Node version
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 13:11:25 +01:00
RiotRobot eff52b82e8 v34.1.0-rc.0 2024-07-09 12:04:20 +00:00
18 changed files with 1251 additions and 152 deletions
+1
View File
@@ -34,6 +34,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
+40
View File
@@ -1,3 +1,43 @@
Changes in [34.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.3.0) (2024-08-13)
==================================================================================================
## ✨ Features
* Bump matrix-widget-api ([#4336](https://github.com/matrix-org/matrix-js-sdk/pull/4336)). Contributed by @AndrewFerr.
* Also check for MSC3757 for session state keys ([#4334](https://github.com/matrix-org/matrix-js-sdk/pull/4334)). Contributed by @AndrewFerr.
* Support Futures via widgets ([#4311](https://github.com/matrix-org/matrix-js-sdk/pull/4311)). Contributed by @AndrewFerr.
* Support MSC4140: Delayed events (Futures) ([#4294](https://github.com/matrix-org/matrix-js-sdk/pull/4294)). Contributed by @AndrewFerr.
* Handle late-arriving `m.room_key.withheld` messages ([#4310](https://github.com/matrix-org/matrix-js-sdk/pull/4310)). Contributed by @richvdh.
* Be specific about what is considered a MSC4143 call member event. ([#4328](https://github.com/matrix-org/matrix-js-sdk/pull/4328)). Contributed by @toger5.
* Add index.ts for matrixrtc module ([#4314](https://github.com/matrix-org/matrix-js-sdk/pull/4314)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix hashed ID server lookups with no Olm ([#4333](https://github.com/matrix-org/matrix-js-sdk/pull/4333)). Contributed by @dbkr.
Changes in [34.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.2.0) (2024-07-30)
==================================================================================================
## 🐛 Bug Fixes
* Element-R: detect "withheld key" UTD errors, and mark them as such ([#4302](https://github.com/matrix-org/matrix-js-sdk/pull/4302)). Contributed by @richvdh.
Changes in [34.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.1.0) (2024-07-16)
==================================================================================================
## ✨ Features
* Add ability to choose how many timeline events to sync when peeking ([#4300](https://github.com/matrix-org/matrix-js-sdk/pull/4300)). Contributed by @jgarplind.
* Remove redundant hack for using the old pickle key in rust crypto ([#4282](https://github.com/matrix-org/matrix-js-sdk/pull/4282)). Contributed by @richvdh.
* Add fetching the well known in embedded mode. ([#4259](https://github.com/matrix-org/matrix-js-sdk/pull/4259)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix room state being updated with old (now overwritten) state and emitting for those updates. ([#4242](https://github.com/matrix-org/matrix-js-sdk/pull/4242)). Contributed by @toger5.
* Fix incorrect "Olm is not available" errors ([#4301](https://github.com/matrix-org/matrix-js-sdk/pull/4301)). Contributed by @richvdh.
* Fix build for example script ([#4286](https://github.com/matrix-org/matrix-js-sdk/pull/4286)). Contributed by @richvdh.
* Declare matrix-js-sdk as an ES module ([#4285](https://github.com/matrix-org/matrix-js-sdk/pull/4285)). Contributed by @richvdh.
Changes in [34.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.0.0) (2024-07-08)
==================================================================================================
## 🚨 BREAKING CHANGES
+7 -6
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "34.0.0",
"version": "34.3.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
@@ -31,8 +31,8 @@
"keywords": [
"matrix-org"
],
"main": "./src/index.ts",
"browser": "./src/browser-index.ts",
"main": "./lib/index.js",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.ts",
"matrix_lib_main": "./lib/index.js",
@@ -54,13 +54,14 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
"content-type": "^1.0.4",
"jwt-decode": "^4.0.0",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.6.0",
"matrix-widget-api": "^1.8.2",
"oidc-client-ts": "^3.0.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
@@ -82,7 +83,6 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
@@ -130,5 +130,6 @@
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
}
},
"typings": "./lib/index.d.ts"
}
@@ -268,7 +268,8 @@ describe("MSC4108SignInWithQR", () => {
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
return -1;
// TODO: mock timers properly
return -1 as any;
});
jest.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
@@ -320,7 +321,8 @@ describe("MSC4108SignInWithQR", () => {
it("should not send secrets if user cancels", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
return -1;
// TODO: mock timers properly
return -1 as any;
});
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
+40
View File
@@ -0,0 +1,40 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { encodeUnpaddedBase64Url } from "../../src";
import { sha256 } from "../../src/digest";
describe("sha256", () => {
it("should hash a string", async () => {
const hash = await sha256("test");
expect(encodeUnpaddedBase64Url(hash)).toBe("n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg");
});
it("should hash a string with emoji", async () => {
const hash = await sha256("test 🍱");
expect(encodeUnpaddedBase64Url(hash)).toBe("X2aDNrrwfq3nCTOl90R9qg9ynxhHnSzsMqtrdYX-SGw");
});
it("throws if webcrypto is not available", async () => {
const oldCrypto = global.crypto;
try {
global.crypto = {} as any;
await expect(sha256("test")).rejects.toThrow();
} finally {
global.crypto = oldCrypto;
}
});
});
+236 -3
View File
@@ -32,7 +32,7 @@ import {
IOpenIDCredentials,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded";
@@ -59,8 +59,26 @@ class MockWidgetApi extends EventEmitter {
public requestCapabilityToReceiveState = jest.fn();
public requestCapabilityToSendToDevice = jest.fn();
public requestCapabilityToReceiveToDevice = jest.fn();
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
public sendStateEvent = jest.fn();
public sendRoomEvent = jest.fn(
(eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public sendStateEvent = jest.fn(
(
eventType: string,
stateKey: string,
content: unknown,
roomId?: string,
delay?: number,
parentDelayId?: string,
) =>
delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public updateDelayedEvent = jest.fn();
public sendToDevice = jest.fn();
public requestOpenIDConnectToken = jest.fn(() => {
return testOIDCToken;
@@ -125,6 +143,17 @@ describe("RoomWidgetClient", () => {
);
});
it("send handles wrong field in response", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
delay_id: `id-${Math.random}`,
});
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow();
});
it("receives", async () => {
const event = new MatrixEvent({
type: "org.matrix.rageshake_request",
@@ -160,6 +189,199 @@ describe("RoomWidgetClient", () => {
});
});
describe("delayed events", () => {
describe("when supported", () => {
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"),
);
beforeAll(() => {
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
});
afterAll(() => {
doesServerSupportUnstableFeatureMock.mockReset();
});
it("sends delayed message events", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
await client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
);
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
2000,
undefined,
);
});
it("sends child action delayed message events", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
const parentDelayId = `id-${Math.random()}`;
await client._unstable_sendDelayedEvent(
"!1:example.org",
{ parent_delay_id: parentDelayId },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
);
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
undefined,
parentDelayId,
);
});
it("sends delayed state events", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
await client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
);
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
"org.example.foo",
"bar",
{ hello: "world" },
"!1:example.org",
2000,
undefined,
);
});
it("sends child action delayed state events", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
const parentDelayId = `fg-${Math.random()}`;
await client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ parent_delay_id: parentDelayId },
"org.example.foo",
{ hello: "world" },
"bar",
);
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
"org.example.foo",
"bar",
{ hello: "world" },
"!1:example.org",
undefined,
parentDelayId,
);
});
it("send delayed message events handles wrong field in response", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
event_id: `$${Math.random()}`,
});
await expect(
client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
),
).rejects.toThrow();
});
it("send delayed state events handles wrong field in response", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
widgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
event_id: `$${Math.random()}`,
});
await expect(
client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
),
).rejects.toThrow();
});
it("updates delayed events", async () => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await client._unstable_updateDelayedEvent("id", action);
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
}
});
});
describe("when unsupported", () => {
it("fails to send delayed message events", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
await expect(
client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
),
).rejects.toThrow("Server does not support");
});
it("fails to send delayed state events", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
await expect(
client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
),
).rejects.toThrow("Server does not support");
});
it("fails to update delayed state events", async () => {
await makeClient({});
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await expect(client._unstable_updateDelayedEvent("id", action)).rejects.toThrow(
"Server does not support",
);
}
});
});
});
describe("initialization", () => {
it("requests permissions for specific message types", async () => {
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
@@ -211,6 +433,17 @@ describe("RoomWidgetClient", () => {
);
});
it("send handles incorrect response", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
widgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
delay_id: `id-${Math.random}`,
});
await expect(
client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar"),
).rejects.toThrow();
});
it("receives", async () => {
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
+369 -3
View File
@@ -57,6 +57,7 @@ import {
Room,
RuleId,
TweakName,
UpdateDelayedEventAction,
} from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
@@ -97,7 +98,7 @@ type HttpLookup = {
method: string;
path: string;
prefix?: string;
data?: Record<string, any>;
data?: Record<string, any> | Record<string, any>[];
error?: object;
expectBody?: Record<string, any>;
expectQueryParams?: QueryDict;
@@ -298,7 +299,9 @@ describe("MatrixClient", function () {
...(opts || {}),
});
// FIXME: We shouldn't be yanking http like this.
client.http = (["authedRequest", "getContentUri", "request", "uploadContent"] as const).reduce((r, k) => {
client.http = (
["authedRequest", "getContentUri", "request", "uploadContent", "idServerRequest"] as const
).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as MatrixHttpApi<any>);
@@ -704,6 +707,328 @@ describe("MatrixClient", function () {
});
});
describe("_unstable_sendDelayedEvent", () => {
const unstableMSC4140Prefix = `${ClientPrefix.Unstable}/org.matrix.msc4140`;
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent;
const timeoutDelayOpts = { delay: 2000 };
const realTimeoutDelayOpts = { "org.matrix.msc4140.delay": 2000 };
beforeEach(() => {
unstableFeatures["org.matrix.msc4140"] = true;
});
it("throws when unsupported by server", async () => {
unstableFeatures["org.matrix.msc4140"] = false;
const errorMessage = "Server does not support";
await expect(
client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
null,
EventType.RoomMessage,
{ ...content },
client.makeTxnId(),
),
).rejects.toThrow(errorMessage);
await expect(
client._unstable_sendDelayedStateEvent(roomId, timeoutDelayOpts, EventType.RoomTopic, {
topic: "topic",
}),
).rejects.toThrow(errorMessage);
await expect(client._unstable_getDelayedEvents()).rejects.toThrow(errorMessage);
await expect(
client._unstable_updateDelayedEvent("anyDelayId", UpdateDelayedEventAction.Send),
).rejects.toThrow(errorMessage);
});
it("works with null threadId", async () => {
httpLookups = [];
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody: content,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
null,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody: content,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
null,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("works with non-null threadId", async () => {
httpLookups = [];
const threadId = "$threadId:server";
const expectBody = {
...content,
"m.relates_to": {
event_id: threadId,
is_falling_back: true,
rel_type: "m.thread",
},
};
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
threadId,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
threadId,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("should add thread relation if threadId is passed and the relation is missing", async () => {
httpLookups = [];
const threadId = "$threadId:server";
const expectBody = {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: threadId,
},
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
},
};
const room = new Room(roomId, client, userId);
mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
threadId,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
threadId,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
httpLookups = [];
const threadId = "$threadId:server";
const content = {
body,
"msgtype": MsgType.Text,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
},
} satisfies RoomMessageEventContent;
const expectBody = {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
"event_id": threadId,
"is_falling_back": false,
"rel_type": "m.thread",
},
};
const room = new Room(roomId, client, userId);
mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
threadId,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
threadId,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("can send a delayed state event", async () => {
httpLookups = [];
const content = { topic: "The year 2000" };
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody: content,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedStateEvent(
roomId,
timeoutDelayOpts,
EventType.RoomTopic,
{ ...content },
);
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody: content,
});
await client._unstable_sendDelayedStateEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
EventType.RoomTopic,
{ ...content },
);
});
it("can look up delayed events", async () => {
httpLookups = [
{
method: "GET",
prefix: unstableMSC4140Prefix,
path: "/delayed_events",
data: [],
},
];
await client._unstable_getDelayedEvents();
});
it("can update delayed events", async () => {
const delayId = "id";
const action = UpdateDelayedEventAction.Restart;
httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}`,
data: {
action,
},
},
];
await client._unstable_updateDelayedEvent(delayId, action);
});
});
it("should create (unstable) file trees", async () => {
const userId = "@test:example.org";
const roomId = "!room:example.org";
@@ -963,7 +1288,7 @@ describe("MatrixClient", function () {
const filter = new Filter(client.credentials.userId);
const filterId = await client.getOrCreateFilter(filterName, filter);
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
expect(filterId).toEqual(!Array.isArray(FILTER_RESPONSE.data) && FILTER_RESPONSE.data?.filter_id);
});
});
@@ -3035,4 +3360,45 @@ describe("MatrixClient", function () {
expect(httpLookups.length).toEqual(0);
});
});
describe("identityHashedLookup", () => {
it("should return hashed lookup results", async () => {
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";
client.http.idServerRequest = jest.fn().mockImplementation((method, path, params) => {
if (method === "GET" && path === "/hash_details") {
return { algorithms: ["sha256"], lookup_pepper: "carrot" };
} else if (method === "POST" && path === "/lookup") {
return {
mappings: {
"WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU": "@bob:homeserver.dummy",
},
};
}
throw new Error("Test impl doesn't know about this request");
});
const lookupResult = await client.identityHashedLookup([["bob@email.dummy", "email"]], ID_ACCESS_TOKEN);
expect(client.http.idServerRequest).toHaveBeenCalledWith(
"GET",
"/hash_details",
undefined,
"/_matrix/identity/v2",
ID_ACCESS_TOKEN,
);
expect(client.http.idServerRequest).toHaveBeenCalledWith(
"POST",
"/lookup",
{ pepper: "carrot", algorithm: "sha256", addresses: ["WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU"] },
"/_matrix/identity/v2",
ID_ACCESS_TOKEN,
);
expect(lookupResult).toHaveLength(1);
expect(lookupResult[0]).toEqual({ address: "bob@email.dummy", mxid: "@bob:homeserver.dummy" });
});
});
});
+69 -17
View File
@@ -46,6 +46,9 @@ describe("MatrixRTCSession", () => {
client = new MatrixClient({ baseUrl: "base_url" });
client.getUserId = jest.fn().mockReturnValue("@alice:example.org");
client.getDeviceId = jest.fn().mockReturnValue("AAAAAAA");
client.doesServerSupportUnstableFeature = jest.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"),
);
});
afterEach(() => {
@@ -241,35 +244,61 @@ describe("MatrixRTCSession", () => {
foci_preferred: [mockFocus],
};
function testSession(
let sendStateEventMock: jest.Mock;
let sendDelayedStateMock: jest.Mock;
let sentStateEvent: Promise<void>;
let sentDelayedState: Promise<void>;
beforeEach(() => {
sentStateEvent = new Promise((resolve) => {
sendStateEventMock = jest.fn(resolve);
});
sentDelayedState = new Promise((resolve) => {
sendDelayedStateMock = jest.fn(() => {
resolve();
return {
delay_id: "id",
};
});
});
client.sendStateEvent = sendStateEventMock;
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
});
async function testSession(
membershipData: CallMembershipData[] | SessionMembershipData,
shouldUseLegacy: boolean,
): void {
): Promise<void> {
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
const makeNewLegacyMembershipsMock = jest.spyOn(sess as any, "makeNewLegacyMemberships");
const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership");
sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]);
expect(makeNewLegacyMembershipsMock).toHaveBeenCalledTimes(shouldUseLegacy ? 1 : 0);
expect(makeNewMembershipMock).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
}
it("uses legacy events if there are any active legacy calls", () => {
testSession([expiredLegacyMembershipData, legacyMembershipData, sessionMembershipData], true);
it("uses legacy events if there are any active legacy calls", async () => {
await testSession([expiredLegacyMembershipData, legacyMembershipData, sessionMembershipData], true);
});
it('uses legacy events if a non-legacy call is in a "memberships" array', () => {
testSession([sessionMembershipData], true);
it('uses legacy events if a non-legacy call is in a "memberships" array', async () => {
await testSession([sessionMembershipData], true);
});
it("uses non-legacy events if all legacy calls are expired", () => {
testSession([expiredLegacyMembershipData], false);
it("uses non-legacy events if all legacy calls are expired", async () => {
await testSession([expiredLegacyMembershipData], false);
});
it("uses non-legacy events if there are only non-legacy calls", () => {
testSession(sessionMembershipData, false);
it("uses non-legacy events if there are only non-legacy calls", async () => {
await testSession(sessionMembershipData, false);
});
});
@@ -347,12 +376,27 @@ describe("MatrixRTCSession", () => {
describe("joining", () => {
let mockRoom: Room;
let sendStateEventMock: jest.Mock;
let sendDelayedStateMock: jest.Mock;
let sendEventMock: jest.Mock;
let sentStateEvent: Promise<void>;
let sentDelayedState: Promise<void>;
beforeEach(() => {
sendStateEventMock = jest.fn();
sentStateEvent = new Promise((resolve) => {
sendStateEventMock = jest.fn(resolve);
});
sentDelayedState = new Promise((resolve) => {
sendDelayedStateMock = jest.fn(() => {
resolve();
return {
delay_id: "id",
};
});
});
sendEventMock = jest.fn();
client.sendStateEvent = sendStateEventMock;
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
client.sendEvent = sendEventMock;
mockRoom = makeMockRoom([]);
@@ -373,9 +417,11 @@ describe("MatrixRTCSession", () => {
expect(sess!.isJoined()).toEqual(true);
});
it("sends a membership event when joining a call", () => {
it("sends a membership event when joining a call", async () => {
const realSetTimeout = setTimeout;
jest.useFakeTimers();
sess!.joinRoomSession([mockFocus], mockFocus);
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
@@ -396,6 +442,8 @@ describe("MatrixRTCSession", () => {
},
"@alice:example.org",
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
jest.useRealTimers();
});
@@ -403,13 +451,15 @@ describe("MatrixRTCSession", () => {
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
function testJoin(useOwnedStateEvents: boolean): void {
async function testJoin(useOwnedStateEvents: boolean): Promise<void> {
const realSetTimeout = setTimeout;
if (useOwnedStateEvents) {
mockRoom.getVersion = jest.fn().mockReturnValue("org.matrix.msc3779.default");
}
jest.useFakeTimers();
sess!.joinRoomSession([activeFocusConfig], activeFocus, { useLegacyMemberEvents: false });
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
@@ -423,15 +473,17 @@ describe("MatrixRTCSession", () => {
} satisfies SessionMembershipData,
`${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`,
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
jest.useRealTimers();
}
it("sends a membership event with session payload when joining a non-legacy call", () => {
testJoin(false);
it("sends a membership event with session payload when joining a non-legacy call", async () => {
await testJoin(false);
});
it("does not prefix the state key with _ for rooms that support user-owned state events", () => {
testJoin(true);
it("does not prefix the state key with _ for rooms that support user-owned state events", async () => {
await testJoin(true);
});
});
+13 -4
View File
@@ -89,11 +89,8 @@ describe("oidc authorization", () => {
describe("generateAuthorizationUrl()", () => {
it("should generate url with correct parameters", async () => {
// test the no crypto case here
// @ts-ignore mocking
globalThis.crypto.subtle = undefined;
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
authorizationParams.codeVerifier = "test-code-verifier";
const authUrl = new URL(
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
);
@@ -105,6 +102,18 @@ describe("oidc authorization", () => {
expect(authUrl.searchParams.get("scope")).toEqual(authorizationParams.scope);
expect(authUrl.searchParams.get("state")).toEqual(authorizationParams.state);
expect(authUrl.searchParams.get("nonce")).toEqual(authorizationParams.nonce);
expect(authUrl.searchParams.get("code_challenge")).toEqual("0FLIKahrX7kqxncwhV5WD82lu_wi5GA8FsRSLubaOpU");
});
it("should log a warning if crypto is not available", async () => {
// test the no crypto case here
// @ts-ignore mocking
globalThis.crypto.subtle = undefined;
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
const authUrl = new URL(
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
);
// crypto not available, plain text code_challenge is used
expect(authUrl.searchParams.get("code_challenge")).toEqual(authorizationParams.codeVerifier);
-45
View File
@@ -29,20 +29,11 @@ declare global {
namespace NodeJS {
interface Global {
localStorage: Storage;
// marker variable used to detect both the browser & node entrypoints being used at once
__js_sdk_entrypoint: unknown;
}
}
interface Window {
webkitAudioContext: typeof AudioContext;
}
interface Crypto {
webkitSubtle?: Window["crypto"]["subtle"];
}
interface MediaDevices {
// This is experimental and types don't know about it yet
// https://github.com/microsoft/TypeScript/issues/33232
@@ -76,40 +67,4 @@ declare global {
// on webkit: we should check if we still need to do this
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
}
export interface ISettledFulfilled<T> {
status: "fulfilled";
value: T;
}
export interface ISettledRejected {
status: "rejected";
reason: any;
}
interface PromiseConstructor {
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
}
interface RTCRtpTransceiver {
// This has been removed from TS
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
// but we still need this for MatrixCall::getRidOfRTXCodecs()
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
}
interface RequestInit {
/**
* Specifies the priority of the fetch request relative to other requests of the same type.
* Must be one of the following strings:
* high: A high priority fetch request relative to other requests of the same type.
* low: A low priority fetch request relative to other requests of the same type.
* auto: Automatically determine the priority of the fetch request relative to other requests of the same type (default).
*
* @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attribute
* @see https://github.com/microsoft/TypeScript/issues/54472
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#browser_compatibility
* Not yet supported in Safari or Firefox
*/
priority?: "high" | "low" | "auto";
}
}
+50
View File
@@ -76,6 +76,56 @@ export interface ISendEventResponse {
event_id: string;
}
export type TimeoutDelay = {
delay: number;
};
export type ParentDelayId = {
parent_delay_id: string;
};
export type SendTimeoutDelayedEventRequestOpts = TimeoutDelay & Partial<ParentDelayId>;
export type SendActionDelayedEventRequestOpts = ParentDelayId;
export type SendDelayedEventRequestOpts = SendTimeoutDelayedEventRequestOpts | SendActionDelayedEventRequestOpts;
export type SendDelayedEventResponse = {
delay_id: string;
};
export enum UpdateDelayedEventAction {
Cancel = "cancel",
Restart = "restart",
Send = "send",
}
export type UpdateDelayedEventRequestOpts = SendDelayedEventResponse & {
action: UpdateDelayedEventAction;
};
type DelayedPartialTimelineEvent = {
room_id: string;
type: string;
content: IContent;
};
type DelayedPartialStateEvent = DelayedPartialTimelineEvent & {
state_key: string;
transaction_id: string;
};
type DelayedPartialEvent = DelayedPartialTimelineEvent | DelayedPartialStateEvent;
export type DelayedEventInfo = {
delayed_events: DelayedPartialEvent &
SendDelayedEventResponse &
SendDelayedEventRequestOpts &
{
running_since: number;
}[];
next_batch?: string;
};
export interface IPresenceOpts {
// One of "online", "offline" or "unavailable"
presence: "online" | "offline" | "unavailable";
+231 -43
View File
@@ -47,7 +47,7 @@ import { Direction, EventTimeline } from "./models/event-timeline";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./base64";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64Url } from "./base64";
import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice";
import { IOlmDevice } from "./crypto/algorithms/megolm";
import { TypedReEmitter } from "./ReEmitter";
@@ -114,6 +114,7 @@ import { NotificationCountType, Room, RoomEvent, RoomEventHandlerMap, RoomNameSt
import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member";
import { IPowerLevelsContent, RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state";
import {
DelayedEventInfo,
IAddThreePidOnlyBody,
IBindThreePidBody,
IContextResponse,
@@ -134,6 +135,9 @@ import {
IStatusResponse,
ITagsResponse,
KnockRoomOpts,
SendDelayedEventRequestOpts,
SendDelayedEventResponse,
UpdateDelayedEventAction,
} from "./@types/requests";
import {
EventType,
@@ -227,6 +231,7 @@ import { KnownMembership, Membership } from "./@types/membership";
import { RoomMessageEventContent, StickerEventContent } from "./@types/events";
import { ImageInfo } from "./@types/media";
import { Capabilities, ServerCapabilities } from "./serverCapabilities";
import { sha256 } from "./digest";
export type Store = IStore;
@@ -530,6 +535,8 @@ export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666";
export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms";
export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
enum CrossSigningKeyType {
MasterKey = "master_key",
SelfSigningKey = "self_signing_key",
@@ -4573,12 +4580,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
threadId = threadIdOrEventType;
}
// If we expect that an event is part of a thread but is missing the relation
// we need to add it manually, as well as the reply fallback
if (threadId && !content!["m.relates_to"]?.rel_type) {
const isReply = !!content!["m.relates_to"]?.["m.in_reply_to"];
content!["m.relates_to"] = {
...content!["m.relates_to"],
this.addThreadRelationIfNeeded(content, threadId, roomId);
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId);
}
/**
* If we expect that an event is part of a thread but is missing the relation
* we need to add it manually, as well as the reply fallback
*/
private addThreadRelationIfNeeded(content: IContent, threadId: string | null, roomId: string): void {
if (threadId && !content["m.relates_to"]?.rel_type) {
const isReply = !!content["m.relates_to"]?.["m.in_reply_to"];
content["m.relates_to"] = {
...content["m.relates_to"],
rel_type: THREAD_RELATION_TYPE.name,
event_id: threadId,
// Set is_falling_back to true unless this is actually intended to be a reply
@@ -4586,7 +4600,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
};
const thread = this.getRoom(roomId)?.getThread(threadId);
if (thread && !isReply) {
content!["m.relates_to"]["m.in_reply_to"] = {
content["m.relates_to"]["m.in_reply_to"] = {
event_id:
thread
.lastReply((ev: MatrixEvent) => {
@@ -4596,8 +4610,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
};
}
}
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId);
}
/**
@@ -4611,7 +4623,38 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
threadId: string | null,
eventObject: Partial<IEvent>,
txnId?: string,
): Promise<ISendEventResponse> {
): Promise<ISendEventResponse>;
/**
* Sends a delayed event (MSC4140).
* @param eventObject - An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added.
* @param delayOpts - Properties of the delay for this event.
* @param txnId - Optional.
* @returns Promise which resolves: to an empty object `{}`
* @returns Rejects: with an error response.
*/
private sendCompleteEvent(
roomId: string,
threadId: string | null,
eventObject: Partial<IEvent>,
delayOpts: SendDelayedEventRequestOpts,
txnId?: string,
): Promise<SendDelayedEventResponse>;
private sendCompleteEvent(
roomId: string,
threadId: string | null,
eventObject: Partial<IEvent>,
delayOptsOrTxnId?: SendDelayedEventRequestOpts | string,
txnIdOrVoid?: string,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
let delayOpts: SendDelayedEventRequestOpts | undefined;
let txnId: string | undefined;
if (typeof delayOptsOrTxnId === "string") {
txnId = delayOptsOrTxnId;
} else {
delayOpts = delayOptsOrTxnId;
txnId = txnIdOrVoid;
}
if (!txnId) {
txnId = this.makeTxnId();
}
@@ -4634,9 +4677,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
localEvent.setThread(thread);
}
// set up re-emitter for this new event - this is normally the job of EventMapper but we don't use it here
this.reEmitter.reEmit(localEvent, [MatrixEventEvent.Replaced, MatrixEventEvent.VisibilityChange]);
room?.reEmitter.reEmit(localEvent, [MatrixEventEvent.BeforeRedaction]);
if (!delayOpts) {
// set up re-emitter for this new event - this is normally the job of EventMapper but we don't use it here
this.reEmitter.reEmit(localEvent, [MatrixEventEvent.Replaced, MatrixEventEvent.VisibilityChange]);
room?.reEmitter.reEmit(localEvent, [MatrixEventEvent.BeforeRedaction]);
}
// if this is a relation or redaction of an event
// that hasn't been sent yet (e.g. with a local id starting with a ~)
@@ -4651,29 +4696,56 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const type = localEvent.getType();
this.logger.debug(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
this.logger.debug(
`sendEvent of type ${type} in ${roomId} with txnId ${txnId}${delayOpts ? " (delayed event)" : ""}`,
);
localEvent.setTxnId(txnId);
localEvent.setStatus(EventStatus.SENDING);
// add this event immediately to the local store as 'sending'.
room?.addPendingEvent(localEvent, txnId);
// TODO: separate store for delayed events?
if (!delayOpts) {
// add this event immediately to the local store as 'sending'.
room?.addPendingEvent(localEvent, txnId);
// addPendingEvent can change the state to NOT_SENT if it believes
// that there's other events that have failed. We won't bother to
// try sending the event if the state has changed as such.
if (localEvent.status === EventStatus.NOT_SENT) {
return Promise.reject(new Error("Event blocked by other events not yet sent"));
// addPendingEvent can change the state to NOT_SENT if it believes
// that there's other events that have failed. We won't bother to
// try sending the event if the state has changed as such.
if (localEvent.status === EventStatus.NOT_SENT) {
return Promise.reject(new Error("Event blocked by other events not yet sent"));
}
return this.encryptAndSendEvent(room, localEvent);
} else {
return this.encryptAndSendEvent(room, localEvent, delayOpts);
}
return this.encryptAndSendEvent(room, localEvent);
}
/**
* encrypts the event if necessary; adds the event to the queue, or sends it; marks the event as sent/unsent
* @returns returns a promise which resolves with the result of the send request
*/
protected async encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
protected async encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse>;
/**
* Simply sends a delayed event without encrypting it.
* TODO: Allow encrypted delayed events, and encrypt them properly
* @param delayOpts - Properties of the delay for this event.
* @returns returns a promise which resolves with the result of the delayed send request
*/
protected async encryptAndSendEvent(
room: Room | null,
event: MatrixEvent,
delayOpts: SendDelayedEventRequestOpts,
): Promise<SendDelayedEventResponse>;
protected async encryptAndSendEvent(
room: Room | null,
event: MatrixEvent,
delayOpts?: SendDelayedEventRequestOpts,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
if (delayOpts) {
return this.sendEventHttpRequest(event, delayOpts);
}
try {
let cancelled: boolean;
this.eventsBeingEncrypted.add(event.getId()!);
@@ -4824,7 +4896,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
private sendEventHttpRequest(event: MatrixEvent): Promise<ISendEventResponse> {
private sendEventHttpRequest(event: MatrixEvent): Promise<ISendEventResponse>;
private sendEventHttpRequest(
event: MatrixEvent,
delayOpts: SendDelayedEventRequestOpts,
): Promise<SendDelayedEventResponse>;
private sendEventHttpRequest(
event: MatrixEvent,
delayOpts?: SendDelayedEventRequestOpts,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
let txnId = event.getTxnId();
if (!txnId) {
txnId = this.makeTxnId();
@@ -4856,12 +4936,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams);
}
return this.http
.authedRequest<ISendEventResponse>(Method.Put, path, undefined, event.getWireContent())
.then((res) => {
const content = event.getWireContent();
if (!delayOpts) {
return this.http.authedRequest<ISendEventResponse>(Method.Put, path, undefined, content).then((res) => {
this.logger.debug(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`);
return res;
});
} else {
return this.http.authedRequest<SendDelayedEventResponse>(
Method.Put,
path,
getUnstableDelayQueryOpts(delayOpts),
content,
);
}
}
/**
@@ -5191,6 +5279,101 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.sendMessage(roomId, threadId, content);
}
/**
* Send a delayed timeline event.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*/
// eslint-disable-next-line
public async _unstable_sendDelayedEvent<K extends keyof TimelineEvents>(
roomId: string,
delayOpts: SendDelayedEventRequestOpts,
threadId: string | null,
eventType: K,
content: TimelineEvents[K],
txnId?: string,
): Promise<SendDelayedEventResponse> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
this.addThreadRelationIfNeeded(content, threadId, roomId);
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, delayOpts, txnId);
}
/**
* Send a delayed state event.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*/
// eslint-disable-next-line
public async _unstable_sendDelayedStateEvent<K extends keyof StateEvents>(
roomId: string,
delayOpts: SendDelayedEventRequestOpts,
eventType: K,
content: StateEvents[K],
stateKey = "",
opts: IRequestOpts = {},
): Promise<SendDelayedEventResponse> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
const pathParams = {
$roomId: roomId,
$eventType: eventType,
$stateKey: stateKey,
};
let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
if (stateKey !== undefined) {
path = utils.encodeUri(path + "/$stateKey", pathParams);
}
return this.http.authedRequest(Method.Put, path, getUnstableDelayQueryOpts(delayOpts), content as Body, opts);
}
/**
* Get all pending delayed events for the calling user.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*/
// eslint-disable-next-line
public async _unstable_getDelayedEvents(fromToken?: string): Promise<DelayedEventInfo> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
const queryDict = fromToken ? { from: fromToken } : undefined;
return await this.http.authedRequest(Method.Get, "/delayed_events", queryDict, undefined, {
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
});
}
/**
* Manage a delayed event associated with the given delay_id.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*/
// eslint-disable-next-line
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<{}> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
const path = utils.encodeUri("/delayed_events/$delayId", {
$delayId: delayId,
});
const data = {
action,
};
return await this.http.authedRequest(Method.Post, path, undefined, data, {
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
});
}
/**
* Send a receipt.
* @param event - The event being acknowledged
@@ -9302,20 +9485,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// When picking an algorithm, we pick the hashed over no hashes
if (hashes["algorithms"].includes("sha256")) {
// Abuse the olm hashing
const olmutil = new global.Olm.Utility();
params["addresses"] = addressPairs.map((p) => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const hashed = olmutil
.sha256(`${addr} ${med} ${params["pepper"]}`)
.replace(/\+/g, "-")
.replace(/\//g, "_"); // URL-safe base64
// Map the hash to a known (case-sensitive) address. We use the case
// sensitive version because the caller might be expecting that.
localMapping[hashed] = p[0];
return hashed;
});
params["addresses"] = await Promise.all(
addressPairs.map(async (p) => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const hashBuffer = await sha256(`${addr} ${med} ${params["pepper"]}`);
const hashed = encodeUnpaddedBase64Url(hashBuffer);
// Map the hash to a known (case-sensitive) address. We use the case
// sensitive version because the caller might be expecting that.
localMapping[hashed] = p[0];
return hashed;
}),
);
params["algorithm"] = "sha256";
} else if (hashes["algorithms"].includes("none")) {
params["addresses"] = addressPairs.map((p) => {
@@ -9892,6 +10074,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
function getUnstableDelayQueryOpts(delayOpts: SendDelayedEventRequestOpts): QueryDict {
return Object.fromEntries(
Object.entries(delayOpts).map(([k, v]) => [`${UNSTABLE_MSC4140_DELAYED_EVENTS}.${k}`, v]),
);
}
/**
* recalculates an accurate notifications count on event decryption.
* Servers do not have enough knowledge about encrypted events to calculate an
-4
View File
@@ -66,7 +66,6 @@ export abstract class EncryptionAlgorithm {
protected readonly crypto: Crypto;
protected readonly olmDevice: OlmDevice;
protected readonly baseApis: MatrixClient;
protected readonly roomId?: string;
/**
* @param params - parameters
@@ -77,7 +76,6 @@ export abstract class EncryptionAlgorithm {
this.crypto = params.crypto;
this.olmDevice = params.olmDevice;
this.baseApis = params.baseApis;
this.roomId = params.roomId;
}
/**
@@ -127,14 +125,12 @@ export abstract class DecryptionAlgorithm {
protected readonly crypto: Crypto;
protected readonly olmDevice: OlmDevice;
protected readonly baseApis: MatrixClient;
protected readonly roomId?: string;
public constructor(params: DecryptionClassParams) {
this.userId = params.userId;
this.crypto = params.crypto;
this.olmDevice = params.olmDevice;
this.baseApis = params.baseApis;
this.roomId = params.roomId;
}
/**
+34
View File
@@ -0,0 +1,34 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Computes a SHA-256 hash of a string (after utf-8 encoding) and returns it as an ArrayBuffer.
*
* @param plaintext The string to hash
* @returns An ArrayBuffer containing the SHA-256 hash of the input string
* @throws If the subtle crypto API is not available, for example if the code is running
* in a web page with an insecure context (eg. served over plain HTTP).
*/
export async function sha256(plaintext: string): Promise<ArrayBuffer> {
if (!globalThis.crypto.subtle) {
throw new Error("Crypto.subtle is not available: insecure context?");
}
const utf8 = new TextEncoder().encode(plaintext);
const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8);
return digest;
}
+108 -4
View File
@@ -26,8 +26,13 @@ import {
} from "matrix-widget-api";
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event";
import { ISendEventResponse } from "./@types/requests";
import { EventType } from "./@types/event";
import {
ISendEventResponse,
SendDelayedEventRequestOpts,
SendDelayedEventResponse,
UpdateDelayedEventAction,
} from "./@types/requests";
import { EventType, StateEvents } from "./@types/event";
import { logger } from "./logger";
import {
MatrixClient,
@@ -36,6 +41,7 @@ import {
IStartClientOpts,
SendToDeviceContentMap,
IOpenIDToken,
UNSTABLE_MSC4140_DELAYED_EVENTS,
} from "./client";
import { SyncApi, SyncState } from "./sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk";
@@ -95,6 +101,20 @@ export interface ICapabilities {
* @defaultValue false
*/
turnServers?: boolean;
/**
* Whether this client needs to be able to send delayed events.
* @experimental Part of MSC4140 & MSC4157
* @defaultValue false
*/
sendDelayedEvents?: boolean;
/**
* Whether this client needs to be able to update delayed events.
* @experimental Part of MSC4140 & MSC4157
* @defaultValue false
*/
updateDelayedEvents?: boolean;
}
/**
@@ -162,6 +182,18 @@ export class RoomWidgetClient extends MatrixClient {
);
capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
if (
capabilities.sendDelayedEvents &&
(capabilities.sendEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.sendState?.length)
) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
}
if (capabilities.updateDelayedEvents) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
}
if (capabilities.turnServers) {
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
}
@@ -248,7 +280,29 @@ export class RoomWidgetClient extends MatrixClient {
throw new Error(`Unknown room: ${roomIdOrAlias}`);
}
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse>;
protected async encryptAndSendEvent(
room: Room,
event: MatrixEvent,
delayOpts: SendDelayedEventRequestOpts,
): Promise<SendDelayedEventResponse>;
protected async encryptAndSendEvent(
room: Room,
event: MatrixEvent,
delayOpts?: SendDelayedEventRequestOpts,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
if (delayOpts) {
// TODO: updatePendingEvent for delayed events?
const response = await this.widgetApi.sendRoomEvent(
event.getType(),
event.getContent(),
room.roomId,
"delay" in delayOpts ? delayOpts.delay : undefined,
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
);
return this.validateSendDelayedEventResponse(response);
}
let response: ISendEventFromWidgetResponseData;
try {
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
@@ -257,6 +311,7 @@ export class RoomWidgetClient extends MatrixClient {
throw e;
}
// This also checks for an event id on the response
room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
return { event_id: response.event_id! };
}
@@ -267,7 +322,56 @@ export class RoomWidgetClient extends MatrixClient {
content: any,
stateKey = "",
): Promise<ISendEventResponse> {
return (await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId)) as ISendEventResponse;
const response = await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
if (response.event_id === undefined) {
throw new Error("'event_id' absent from response to an event request");
}
return { event_id: response.event_id };
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line
public async _unstable_sendDelayedStateEvent<K extends keyof StateEvents>(
roomId: string,
delayOpts: SendDelayedEventRequestOpts,
eventType: K,
content: StateEvents[K],
stateKey = "",
): Promise<SendDelayedEventResponse> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
const response = await this.widgetApi.sendStateEvent(
eventType,
stateKey,
content,
roomId,
"delay" in delayOpts ? delayOpts.delay : undefined,
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
);
return this.validateSendDelayedEventResponse(response);
}
private validateSendDelayedEventResponse(response: ISendEventFromWidgetResponseData): SendDelayedEventResponse {
if (response.delay_id === undefined) {
throw new Error("'delay_id' absent from response to a delayed event request");
}
return { delay_id: response.delay_id };
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<{}> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
return await this.widgetApi.updateDelayedEvent(delayId, action);
}
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {
+41 -10
View File
@@ -20,6 +20,7 @@ import { EventTimeline } from "../models/event-timeline";
import { Room } from "../models/room";
import { MatrixClient } from "../client";
import { EventType } from "../@types/event";
import { UpdateDelayedEventAction } from "../@types/requests";
import {
CallMembership,
CallMembershipData,
@@ -865,27 +866,57 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
newContent = this.makeNewMembership(localDeviceId);
}
const stateKey = legacy ? localUserId : this.makeMembershipStateKey(localUserId, localDeviceId);
try {
await this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
newContent,
legacy ? localUserId : this.makeMembershipStateKey(localUserId, localDeviceId),
);
await this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, stateKey);
logger.info(`Sent updated call member event.`);
// check periodically to see if we need to refresh our member event
if (this.isJoined() && legacy) {
this.memberEventTimeout = setTimeout(this.triggerCallMembershipEventUpdate, MEMBER_EVENT_CHECK_PERIOD);
if (this.isJoined()) {
if (legacy) {
this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate,
MEMBER_EVENT_CHECK_PERIOD,
);
} else {
try {
// TODO: If delayed event times out, re-join!
const res = await this.client._unstable_sendDelayedStateEvent(
this.room.roomId,
{
delay: 8000,
},
EventType.GroupCallMemberPrefix,
{}, // leave event
stateKey,
);
this.scheduleDelayDisconnection(res.delay_id);
} catch (e) {
logger.error("Failed to send delayed event:", e);
}
}
}
} catch (e) {
const resendDelay = CALL_MEMBER_EVENT_RETRY_DELAY_MIN + Math.random() * 2000;
logger.warn(`Failed to send call member event: retrying in ${resendDelay}`);
logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`);
await new Promise((resolve) => setTimeout(resolve, resendDelay));
await this.triggerCallMembershipEventUpdate();
}
}
private scheduleDelayDisconnection(delayId: string): void {
this.memberEventTimeout = setTimeout(() => this.delayDisconnection(delayId), 5000);
}
private async delayDisconnection(delayId: string): Promise<void> {
try {
await this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart);
this.scheduleDelayDisconnection(delayId);
} catch (e) {
logger.error("Failed to delay our disconnection event", e);
}
}
private stateEventsContainOngoingLegacySession(callMemberEvents: Map<string, MatrixEvent>): boolean {
for (const callMemberEvent of callMemberEvents.values()) {
const content = callMemberEvent.getContent();
@@ -902,7 +933,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
const stateKey = `${localUserId}_${localDeviceId}`;
if (/^org\.matrix\.msc3779\b/.exec(this.room.getVersion())) {
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
return stateKey;
} else {
return `_${stateKey}`;
+4 -7
View File
@@ -27,6 +27,8 @@ import {
validateIdToken,
validateStoredUserState,
} from "./validate";
import { sha256 } from "../digest";
import { encodeUnpaddedBase64Url } from "../base64";
// reexport for backwards compatibility
export type { BearerTokenResponse };
@@ -61,14 +63,9 @@ const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
logger.warn("A secure context is required to generate code challenge. Using plain text code challenge");
return codeVerifier;
}
const utf8 = new TextEncoder().encode(codeVerifier);
const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
const hashBuffer = await sha256(codeVerifier);
return encodeUnpaddedBase64Url(hashBuffer);
};
/**
+4 -4
View File
@@ -4722,10 +4722,10 @@ matrix-mock-request@^2.5.0:
dependencies:
expect "^28.1.0"
matrix-widget-api@^1.6.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.7.0.tgz#ae3b44380f11bb03519d0bf0373dfc3341634667"
integrity sha512-dzSnA5Va6CeIkyWs89xZty/uv38HLyfjOrHGbbEikCa2ZV0HTkUNtrBMKlrn4CRYyDJ6yoO/3ssRwiR0jJvOkQ==
matrix-widget-api@^1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.8.2.tgz#28d344502a85593740f560b0f8120e474a054505"
integrity sha512-kdmks3CvFNPIYN669Y4rO13KrazDvX8KHC7i6jOzJs8uZ8s54FNkuRVVyiQHeVCSZG5ixUqW9UuCj9lf03qxTQ==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"