Compare commits
46 Commits
v34.1.0-rc.2
...
v34.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ae0c2f3ee | |||
| de50129a53 | |||
| 5568dfdd41 | |||
| 39216d44ed | |||
| 8c3b249567 | |||
| b8e40ad2a8 | |||
| 4e2831764d | |||
| 09780672aa | |||
| 0fe53876ec | |||
| dfec3dc33c | |||
| fbdd78b428 | |||
| e10c362ef0 | |||
| 89a9a7fa38 | |||
| 687d08dc9d | |||
| 7f91db83d0 | |||
| 0300d6343f | |||
| dc1cccfecc | |||
| d32f398345 | |||
| 0f08c00c07 | |||
| 6b261b98c9 | |||
| 99f157a0f1 | |||
| f9f6d81346 | |||
| 46604abe7b | |||
| 553758e713 | |||
| 509e64cfc1 | |||
| 60c2e9b3ed | |||
| cfb21fa80a | |||
| c3d7f4e730 | |||
| aa97beae44 | |||
| 5feab37166 | |||
| 1a02835ab2 | |||
| 6f63ff1711 | |||
| 4d90fecb6a | |||
| 30a26813ec | |||
| f17a4fedb9 | |||
| 94e393c9a6 | |||
| 53201688a6 | |||
| 996663bf64 | |||
| d6e4338a37 | |||
| b2665f2128 | |||
| af4b6bc126 | |||
| 565bb0ef7c | |||
| fe0edcd081 | |||
| 6520e0f54f | |||
| ed7b314e6a | |||
| 51544f25a7 |
@@ -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"
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [integ, unit]
|
||||
node: ["lts/*", 21, 22]
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -63,6 +63,16 @@ jobs:
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
# Dummy completion job to simplify branch protections
|
||||
jest-complete:
|
||||
name: Jest tests
|
||||
needs: jest
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
if: github.event_name == 'merge_group'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,7 @@ client.publicRooms(function (err, data) {
|
||||
```
|
||||
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
[the Node.js terminal app](examples/node/README.md) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "34.1.0-rc.2",
|
||||
"version": "34.3.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -53,14 +53,15 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^6.0.0",
|
||||
"@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",
|
||||
@@ -106,7 +106,7 @@
|
||||
"eslint-plugin-tsdoc": "^0.3.0",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "10.0.7",
|
||||
"fetch-mock": "10.1.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
@@ -117,8 +117,8 @@
|
||||
"lint-staged": "^15.0.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.3.2",
|
||||
"rimraf": "^5.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "^6.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.0",
|
||||
"typedoc-plugin-coverage": "^3.0.0",
|
||||
|
||||
@@ -2333,8 +2333,84 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
|
||||
describe("m.room_key.withheld handling", () => {
|
||||
// TODO: there are a bunch more tests for this sort of thing in spec/unit/crypto/algorithms/megolm.spec.ts.
|
||||
// They should be converted to integ tests and moved.
|
||||
describe.each([
|
||||
["m.blacklisted", "The sender has blocked you.", DecryptionFailureCode.MEGOLM_KEY_WITHHELD],
|
||||
[
|
||||
"m.unverified",
|
||||
"The sender has disabled encrypting to unverified devices.",
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
],
|
||||
])(
|
||||
"Decryption fails with withheld error if a withheld notice with code '%s' is received",
|
||||
(withheldCode, expectedMessage, expectedErrorCode) => {
|
||||
it.each(["before", "after"])("%s the event", async (when) => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
|
||||
let awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
|
||||
|
||||
// Send Alice an encrypted room event which looks like it was encrypted with a megolm session
|
||||
async function sendEncryptedEvent() {
|
||||
const event = {
|
||||
...testData.ENCRYPTED_EVENT,
|
||||
origin_server_ts: Date.now(),
|
||||
};
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: { join: { [ROOM_ID]: { timeline: { events: [event] } } } },
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
}
|
||||
|
||||
// Send Alice a withheld notice
|
||||
async function sendWithheldMessage() {
|
||||
const withheldMessage = {
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: ROOM_ID,
|
||||
sender_key: testData.ENCRYPTED_EVENT.content!.sender_key,
|
||||
session_id: testData.ENCRYPTED_EVENT.content!.session_id,
|
||||
code: withheldCode,
|
||||
reason: "zzz",
|
||||
},
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
to_device: { events: [withheldMessage] },
|
||||
});
|
||||
await syncPromise(aliceClient);
|
||||
}
|
||||
|
||||
if (when === "before") {
|
||||
await sendWithheldMessage();
|
||||
await sendEncryptedEvent();
|
||||
} else {
|
||||
await sendEncryptedEvent();
|
||||
// Make sure that the first attempt to decrypt has happened before the withheld arrives
|
||||
await awaitDecryption;
|
||||
awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
|
||||
await sendWithheldMessage();
|
||||
}
|
||||
|
||||
const ev = await awaitDecryption;
|
||||
expect(ev.getContent()).toEqual({
|
||||
body: `** Unable to decrypt: DecryptionError: ${expectedMessage} **`,
|
||||
msgtype: "m.bad.encrypted",
|
||||
});
|
||||
|
||||
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
|
||||
|
||||
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors.
|
||||
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
oldBackendOnly("does not block decryption on an 'm.unavailable' report", async function () {
|
||||
// there may be a key downloads for alice
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -412,7 +412,12 @@ describe("MatrixEvent", () => {
|
||||
const crypto = {
|
||||
decryptEvent: jest
|
||||
.fn()
|
||||
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
|
||||
.mockRejectedValue(
|
||||
new DecryptionError(
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
"The sender has disabled encrypting to unverified devices.",
|
||||
),
|
||||
),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -95,6 +95,7 @@ describe("initRustCrypto", () => {
|
||||
deleteSecretsFromInbox: jest.fn(),
|
||||
registerReceiveSecretCallback: jest.fn(),
|
||||
registerDevicesUpdatedCallback: jest.fn(),
|
||||
registerRoomKeysWithheldCallback: jest.fn(),
|
||||
outgoingRequests: jest.fn(),
|
||||
isBackupEnabled: jest.fn().mockResolvedValue(false),
|
||||
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
|
||||
|
||||
@@ -434,7 +434,7 @@ describe("TimelineWindow", function () {
|
||||
});
|
||||
|
||||
function idsOf(events: Array<MatrixEvent>): Array<string> {
|
||||
return events.map((e) => (e ? e.getId() ?? "MISSING_ID" : "MISSING_EVENT"));
|
||||
return events.map((e) => (e ? (e.getId() ?? "MISSING_ID") : "MISSING_EVENT"));
|
||||
}
|
||||
|
||||
describe("removing events", () => {
|
||||
|
||||
@@ -105,7 +105,7 @@ const mockGetStateEvents =
|
||||
(events: MatrixEvent[] = FAKE_STATE_EVENTS as MatrixEvent[]) =>
|
||||
(type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
if (type === EventType.GroupCallMemberPrefix) {
|
||||
return userId === undefined ? events : events.find((e) => e.getStateKey() === userId) ?? null;
|
||||
return userId === undefined ? events : (events.find((e) => e.getStateKey() === userId) ?? null);
|
||||
} else {
|
||||
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
|
||||
return userId === undefined ? [fakeEvent] : fakeEvent;
|
||||
|
||||
Vendored
-45
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
+237
-46
@@ -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;
|
||||
|
||||
@@ -324,10 +329,13 @@ export interface ICreateClientOpts {
|
||||
localTimeoutMs?: number;
|
||||
|
||||
/**
|
||||
* Set to true to use
|
||||
* Authorization header instead of query param to send the access token to the server.
|
||||
* Set to false to send the access token to the server via a query parameter rather
|
||||
* than the Authorization HTTP header.
|
||||
*
|
||||
* Default false.
|
||||
* Note that as of v1.11 of the Matrix spec, sending the access token via a query
|
||||
* is deprecated.
|
||||
*
|
||||
* Default true.
|
||||
*/
|
||||
useAuthorizationHeader?: boolean;
|
||||
|
||||
@@ -527,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",
|
||||
@@ -4570,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
|
||||
@@ -4583,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) => {
|
||||
@@ -4593,8 +4610,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4608,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();
|
||||
}
|
||||
@@ -4631,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 ~)
|
||||
@@ -4648,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()!);
|
||||
@@ -4821,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();
|
||||
@@ -4853,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5188,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
|
||||
@@ -9299,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) => {
|
||||
@@ -9889,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
|
||||
|
||||
+13
-2
@@ -557,6 +557,12 @@ export enum DecryptionFailureCode {
|
||||
/** Message was encrypted with a Megolm session whose keys have not been shared with us. */
|
||||
MEGOLM_UNKNOWN_INBOUND_SESSION_ID = "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
|
||||
|
||||
/** A special case of {@link MEGOLM_UNKNOWN_INBOUND_SESSION_ID}: the sender has told us it is withholding the key. */
|
||||
MEGOLM_KEY_WITHHELD = "MEGOLM_KEY_WITHHELD",
|
||||
|
||||
/** A special case of {@link MEGOLM_KEY_WITHHELD}: the sender has told us it is withholding the key, because the current device is unverified. */
|
||||
MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE = "MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE",
|
||||
|
||||
/** Message was encrypted with a Megolm session which has been shared with us, but in a later ratchet state. */
|
||||
OLM_UNKNOWN_MESSAGE_INDEX = "OLM_UNKNOWN_MESSAGE_INDEX",
|
||||
|
||||
@@ -848,9 +854,14 @@ export interface CreateSecretStorageOpts {
|
||||
setupNewSecretStorage?: boolean;
|
||||
|
||||
/**
|
||||
* Function called to get the user's
|
||||
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
|
||||
* Function called to get the user's current key backup passphrase.
|
||||
*
|
||||
* Should return a promise that resolves with a Uint8Array
|
||||
* containing the key, or rejects if the key cannot be obtained.
|
||||
*
|
||||
* Only used when the client has existing key backup, but no secret storage.
|
||||
*
|
||||
* @deprecated Not used by the Rust crypto stack.
|
||||
*/
|
||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
+14
-14
@@ -1221,13 +1221,13 @@ export class OlmDevice {
|
||||
this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
|
||||
if (session === null || sessionData === null) {
|
||||
if (withheld) {
|
||||
error = new DecryptionError(
|
||||
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
calculateWithheldMessage(withheld),
|
||||
{
|
||||
session: senderKey + "|" + sessionId,
|
||||
},
|
||||
);
|
||||
const failureCode =
|
||||
withheld.code === "m.unverified"
|
||||
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
|
||||
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
|
||||
error = new DecryptionError(failureCode, calculateWithheldMessage(withheld), {
|
||||
session: senderKey + "|" + sessionId,
|
||||
});
|
||||
}
|
||||
result = null;
|
||||
return;
|
||||
@@ -1237,13 +1237,13 @@ export class OlmDevice {
|
||||
res = session.decrypt(body);
|
||||
} catch (e) {
|
||||
if ((<Error>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) {
|
||||
error = new DecryptionError(
|
||||
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
calculateWithheldMessage(withheld),
|
||||
{
|
||||
session: senderKey + "|" + sessionId,
|
||||
},
|
||||
);
|
||||
const failureCode =
|
||||
withheld.code === "m.unverified"
|
||||
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
|
||||
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
|
||||
error = new DecryptionError(failureCode, calculateWithheldMessage(withheld), {
|
||||
session: senderKey + "|" + sessionId,
|
||||
});
|
||||
} else {
|
||||
error = <Error>e;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+109
-5
@@ -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,8 +311,9 @@ 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 };
|
||||
return { event_id: response.event_id! };
|
||||
}
|
||||
|
||||
public async sendStateEvent(
|
||||
@@ -267,7 +322,56 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
content: any,
|
||||
stateKey = "",
|
||||
): Promise<ISendEventResponse> {
|
||||
return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
|
||||
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<{}> {
|
||||
|
||||
@@ -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,
|
||||
@@ -160,26 +161,27 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
const callMemberships: CallMembership[] = [];
|
||||
for (const memberEvent of callMemberEvents) {
|
||||
const content = memberEvent.getContent();
|
||||
const eventKeysCount = Object.keys(content).length;
|
||||
// Dont even bother about empty events (saves us from costly type/"key in" checks in bigger rooms)
|
||||
if (eventKeysCount === 0) continue;
|
||||
|
||||
let membershipContents: any[] = [];
|
||||
|
||||
// We first decide if its a MSC4143 event (per device state key)
|
||||
if ("memberships" in content) {
|
||||
if (eventKeysCount > 1 && "focus_active" in content) {
|
||||
// We have a MSC4143 event membership event
|
||||
membershipContents.push(content);
|
||||
} else if (eventKeysCount === 1 && "memberships" in content) {
|
||||
// we have a legacy (one event for all devices) event
|
||||
if (!Array.isArray(content["memberships"])) {
|
||||
logger.warn(`Malformed member event from ${memberEvent.getSender()}: memberships is not an array`);
|
||||
continue;
|
||||
}
|
||||
membershipContents = content["memberships"];
|
||||
} else {
|
||||
// We have a MSC4143 event membership event
|
||||
if (Object.keys(content).length !== 0) {
|
||||
// We checked for empty content to not try to construct CallMembership's with {}.
|
||||
membershipContents.push(content);
|
||||
}
|
||||
}
|
||||
if (membershipContents.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (membershipContents.length === 0) continue;
|
||||
|
||||
for (const membershipData of membershipContents) {
|
||||
try {
|
||||
const membership = new CallMembership(memberEvent, membershipData);
|
||||
@@ -864,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();
|
||||
@@ -901,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}`;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
export * from "./CallMembership";
|
||||
export * from "./focus";
|
||||
export * from "./LivekitFocus";
|
||||
export * from "./MatrixRTCSession";
|
||||
export * from "./MatrixRTCSessionManager";
|
||||
export * from "./types";
|
||||
+5
-12
@@ -43,7 +43,6 @@ import { MatrixError } from "../http-api";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { EventStatus } from "./event-status";
|
||||
import { CryptoBackend, DecryptionError } from "../common-crypto/CryptoBackend";
|
||||
import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
|
||||
import { IAnnotatedPushRule } from "../@types/PushRules";
|
||||
import { Room } from "./room";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
@@ -312,12 +311,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
private thread?: Thread;
|
||||
private threadId?: string;
|
||||
|
||||
/*
|
||||
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
|
||||
* the sender has disabled encrypting to unverified devices.
|
||||
*/
|
||||
private encryptedDisabledForUnverifiedDevices = false;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
* the time between the server putting the 'age' field on the event as it sent
|
||||
@@ -411,7 +404,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
// The fallback in these cases will be to use the origin_server_ts.
|
||||
// For EDUs, the origin_server_ts also is not defined so we use Date.now().
|
||||
const age = this.getAge();
|
||||
this.localTimestamp = age !== undefined ? Date.now() - age : this.getTs() ?? Date.now();
|
||||
this.localTimestamp = age !== undefined ? Date.now() - age : (this.getTs() ?? Date.now());
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
}
|
||||
|
||||
@@ -787,12 +780,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
return this._decryptionFailureReason;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
|
||||
* the sender has disabled encrypting to unverified devices.
|
||||
*
|
||||
* @deprecated: Prefer `event.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE`.
|
||||
*/
|
||||
public get isEncryptedDisabledForUnverifiedDevices(): boolean {
|
||||
return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices;
|
||||
return this.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE;
|
||||
}
|
||||
|
||||
public shouldAttemptDecryption(): boolean {
|
||||
@@ -982,7 +977,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null;
|
||||
this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this.untrusted = decryptionResult.untrusted || false;
|
||||
this.encryptedDisabledForUnverifiedDevices = false;
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
@@ -1003,7 +997,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
this.claimedEd25519Key = null;
|
||||
this.forwardingCurve25519KeyChain = [];
|
||||
this.untrusted = false;
|
||||
this.encryptedDisabledForUnverifiedDevices = reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`;
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -24,7 +24,7 @@ import {
|
||||
} from "./event-timeline-set";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import { compare, removeElement } from "../utils";
|
||||
import { removeElement } from "../utils";
|
||||
import { normalize, noUnsafeEventProps } from "../utils";
|
||||
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
|
||||
import { EventStatus } from "./event-status";
|
||||
@@ -3451,7 +3451,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return true;
|
||||
});
|
||||
// make sure members have stable order
|
||||
otherMembers.sort((a, b) => compare(a.userId, b.userId));
|
||||
const collator = new Intl.Collator();
|
||||
otherMembers.sort((a, b) => collator.compare(a.userId, b.userId));
|
||||
// only 5 first members, immitate summaryHeroes
|
||||
otherMembers = otherMembers.slice(0, 5);
|
||||
otherNames = otherMembers.map((m) => m.name);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,16 +14,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import {
|
||||
CollectStrategy,
|
||||
EncryptionAlgorithm,
|
||||
EncryptionSettings,
|
||||
HistoryVisibility as RustHistoryVisibility,
|
||||
OlmMachine,
|
||||
RoomId,
|
||||
UserId,
|
||||
HistoryVisibility as RustHistoryVisibility,
|
||||
ToDeviceRequest,
|
||||
UserId,
|
||||
} from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { EventType } from "../@types/event";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
@@ -142,7 +143,7 @@ export class RoomEncryptor {
|
||||
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
|
||||
*/
|
||||
public encryptEvent(event: MatrixEvent | null, globalBlacklistUnverifiedDevices: boolean): Promise<void> {
|
||||
const logger = new LogSpan(this.prefixedLogger, event ? event.getTxnId() ?? "" : "prepareForEncryption");
|
||||
const logger = new LogSpan(this.prefixedLogger, event ? (event.getTxnId() ?? "") : "prepareForEncryption");
|
||||
// Ensure order of encryption to avoid message ordering issues, as the scheduler only ensures
|
||||
// events order after they have been encrypted.
|
||||
const prom = this.currentEncryptionPromise
|
||||
@@ -252,8 +253,11 @@ export class RoomEncryptor {
|
||||
|
||||
// When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used
|
||||
// See Room#getBlacklistUnverifiedDevices
|
||||
rustEncryptionSettings.onlyAllowTrustedDevices =
|
||||
this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices;
|
||||
if (this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices) {
|
||||
rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyOnlyTrustedDevices;
|
||||
} else {
|
||||
rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyAllDevices;
|
||||
}
|
||||
|
||||
await logDuration(this.prefixedLogger, "shareRoomKey", async () => {
|
||||
const shareMessages: ToDeviceRequest[] = await this.olmMachine.shareRoomKey(
|
||||
|
||||
@@ -174,6 +174,9 @@ async function initOlmMachine(
|
||||
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
|
||||
rustCrypto.onRoomKeysUpdated(sessions),
|
||||
);
|
||||
await olmMachine.registerRoomKeysWithheldCallback((withheld: RustSdkCryptoJs.RoomKeyWithheldInfo[]) =>
|
||||
rustCrypto.onRoomKeysWithheld(withheld),
|
||||
);
|
||||
await olmMachine.registerUserIdentityUpdatedCallback((userId: RustSdkCryptoJs.UserId) =>
|
||||
rustCrypto.onUserIdentityUpdated(userId),
|
||||
);
|
||||
|
||||
@@ -1486,7 +1486,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
this.logger.debug(
|
||||
`Got update for session ${key.senderKey.toBase64()}|${key.sessionId} in ${key.roomId.toString()}`,
|
||||
);
|
||||
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key);
|
||||
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key.roomId.toString(), key.sessionId);
|
||||
if (pendingList.length === 0) return;
|
||||
|
||||
this.logger.debug(
|
||||
@@ -1507,6 +1507,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `OlmMachine.registerRoomKeyWithheldCallback`.
|
||||
*
|
||||
* Called by the rust sdk whenever we are told that a key has been withheld. We see if we had any events that
|
||||
* failed to decrypt for the given session, and update their status if so.
|
||||
*
|
||||
* @param withheld - Details of the withheld sessions.
|
||||
*/
|
||||
public async onRoomKeysWithheld(withheld: RustSdkCryptoJs.RoomKeyWithheldInfo[]): Promise<void> {
|
||||
for (const session of withheld) {
|
||||
this.logger.debug(`Got withheld message for session ${session.sessionId} in ${session.roomId.toString()}`);
|
||||
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(
|
||||
session.roomId.toString(),
|
||||
session.sessionId,
|
||||
);
|
||||
if (pendingList.length === 0) return;
|
||||
|
||||
// The easiest way to update the status of the event is to have another go at decrypting it.
|
||||
this.logger.debug(
|
||||
"Retrying decryption on events:",
|
||||
pendingList.map((e) => `${e.getId()}`),
|
||||
);
|
||||
|
||||
for (const ev of pendingList) {
|
||||
ev.attemptDecryption(this, { isRetry: true }).catch((_e) => {
|
||||
// It's somewhat expected that we still can't decrypt here.
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `OlmMachine.registerUserIdentityUpdatedCallback`
|
||||
*
|
||||
@@ -1683,7 +1714,7 @@ class EventDecryptor {
|
||||
/**
|
||||
* Events which we couldn't decrypt due to unknown sessions / indexes.
|
||||
*
|
||||
* Map from senderKey to sessionId to Set of MatrixEvents
|
||||
* Map from roomId to sessionId to Set of MatrixEvents
|
||||
*/
|
||||
private eventsPendingKey = new MapWithDefault<string, MapWithDefault<string, Set<MatrixEvent>>>(
|
||||
() => new MapWithDefault<string, Set<MatrixEvent>>(() => new Set()),
|
||||
@@ -1787,6 +1818,17 @@ class EventDecryptor {
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a withheld code, expose that.
|
||||
if (err.maybe_withheld) {
|
||||
// Unfortunately the Rust SDK API doesn't let us distinguish between different withheld cases, other than
|
||||
// by string-matching.
|
||||
const failureCode =
|
||||
err.maybe_withheld === "The sender has disabled encrypting to unverified devices."
|
||||
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
|
||||
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
|
||||
throw new DecryptionError(failureCode, err.maybe_withheld, errorDetails);
|
||||
}
|
||||
|
||||
switch (err.code) {
|
||||
case RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey:
|
||||
throw new DecryptionError(
|
||||
@@ -1832,30 +1874,27 @@ class EventDecryptor {
|
||||
* Look for events which are waiting for a given megolm session
|
||||
*
|
||||
* Returns a list of events which were encrypted by `session` and could not be decrypted
|
||||
*
|
||||
* @param session -
|
||||
*/
|
||||
public getEventsPendingRoomKey(session: RustSdkCryptoJs.RoomKeyInfo): MatrixEvent[] {
|
||||
const senderPendingEvents = this.eventsPendingKey.get(session.senderKey.toBase64());
|
||||
if (!senderPendingEvents) return [];
|
||||
public getEventsPendingRoomKey(roomId: string, sessionId: string): MatrixEvent[] {
|
||||
const roomPendingEvents = this.eventsPendingKey.get(roomId);
|
||||
if (!roomPendingEvents) return [];
|
||||
|
||||
const sessionPendingEvents = senderPendingEvents.get(session.sessionId);
|
||||
const sessionPendingEvents = roomPendingEvents.get(sessionId);
|
||||
if (!sessionPendingEvents) return [];
|
||||
|
||||
const roomId = session.roomId.toString();
|
||||
return [...sessionPendingEvents].filter((ev) => ev.getRoomId() === roomId);
|
||||
return [...sessionPendingEvents];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the list of those awaiting their session keys.
|
||||
*/
|
||||
private addEventToPendingList(event: MatrixEvent): void {
|
||||
const content = event.getWireContent();
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
const roomId = event.getRoomId();
|
||||
// We shouldn't have events without a room id here.
|
||||
if (!roomId) return;
|
||||
|
||||
const senderPendingEvents = this.eventsPendingKey.getOrCreate(senderKey);
|
||||
const sessionPendingEvents = senderPendingEvents.getOrCreate(sessionId);
|
||||
const roomPendingEvents = this.eventsPendingKey.getOrCreate(roomId);
|
||||
const sessionPendingEvents = roomPendingEvents.getOrCreate(event.getWireContent().session_id);
|
||||
sessionPendingEvents.add(event);
|
||||
}
|
||||
|
||||
@@ -1863,23 +1902,22 @@ class EventDecryptor {
|
||||
* Remove an event from the list of those awaiting their session keys.
|
||||
*/
|
||||
private removeEventFromPendingList(event: MatrixEvent): void {
|
||||
const content = event.getWireContent();
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
const roomId = event.getRoomId();
|
||||
if (!roomId) return;
|
||||
|
||||
const senderPendingEvents = this.eventsPendingKey.get(senderKey);
|
||||
if (!senderPendingEvents) return;
|
||||
const roomPendingEvents = this.eventsPendingKey.getOrCreate(roomId);
|
||||
if (!roomPendingEvents) return;
|
||||
|
||||
const sessionPendingEvents = senderPendingEvents.get(sessionId);
|
||||
const sessionPendingEvents = roomPendingEvents.get(event.getWireContent().session_id);
|
||||
if (!sessionPendingEvents) return;
|
||||
|
||||
sessionPendingEvents.delete(event);
|
||||
|
||||
// also clean up the higher-level maps if they are now empty
|
||||
if (sessionPendingEvents.size === 0) {
|
||||
senderPendingEvents.delete(sessionId);
|
||||
if (senderPendingEvents.size === 0) {
|
||||
this.eventsPendingKey.delete(senderKey);
|
||||
roomPendingEvents.delete(event.getWireContent().session_id);
|
||||
if (roomPendingEvents.size === 0) {
|
||||
this.eventsPendingKey.delete(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,7 +504,7 @@ export class SyncAccumulator {
|
||||
|
||||
currentData._timeline.push({
|
||||
event: transformedEvent,
|
||||
token: index === 0 ? data.timeline.prev_batch ?? null : null,
|
||||
token: index === 0 ? (data.timeline.prev_batch ?? null) : null,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -1403,7 +1403,9 @@ export class SyncApi {
|
||||
if (limited) {
|
||||
room.resetLiveTimeline(
|
||||
joinObj.timeline.prev_batch,
|
||||
this.syncOpts.canResetEntireTimeline!(room.roomId) ? null : syncEventData.oldSyncToken ?? null,
|
||||
this.syncOpts.canResetEntireTimeline!(room.roomId)
|
||||
? null
|
||||
: (syncEventData.oldSyncToken ?? null),
|
||||
);
|
||||
|
||||
// We have to assume any gap in any timeline is
|
||||
|
||||
@@ -649,16 +649,6 @@ export function lexicographicCompare(a: string, b: string): number {
|
||||
}
|
||||
}
|
||||
|
||||
const collator = new Intl.Collator();
|
||||
/**
|
||||
* Performant language-sensitive string comparison
|
||||
* @param a - the first string to compare
|
||||
* @param b - the second string to compare
|
||||
*/
|
||||
export function compare(a: string, b: string): number {
|
||||
return collator.compare(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is similar to Object.assign() but it assigns recursively and
|
||||
* allows you to ignore nullish values from the source
|
||||
|
||||
Reference in New Issue
Block a user