Compare commits
26 Commits
v40.1.0-rc.0
...
v40.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c8e38009f | |||
| 40165942e3 | |||
| f301251ff5 | |||
| 21cd5e98c1 | |||
| db070dca57 | |||
| 8b6ff0abcb | |||
| f2157f28bb | |||
| 739b8e1f89 | |||
| bc57a3f829 | |||
| f136f6ddf7 | |||
| d428e7119a | |||
| 5532066178 | |||
| 82b51d0d46 | |||
| fb12a5a1d6 | |||
| 61ea5a7dfc | |||
| 4e032317fe | |||
| dbb2ae5c07 | |||
| c8032a214e | |||
| 6e34ca6f2d | |||
| 4a7a699623 | |||
| 774776178e | |||
| ed4078528d | |||
| 7224961e9e | |||
| 35ca07e8fb | |||
| ccffb5df2d | |||
| 0d162f66f3 |
@@ -21,7 +21,7 @@ jobs:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "yarn"
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
scripts/release
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
ref: staging
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@9f6f057c1b30500877460c66d4ae937e53d168ee
|
||||
uses: matrix-org/sonarcloud-workflow-action@ea0cd9dbd5562e79816685972bc0d03c235a900c
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Build Types
|
||||
run: "yarn build:types"
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "npm"
|
||||
node-version-file: "examples/node/package.json"
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
@@ -1,3 +1,47 @@
|
||||
Changes in [40.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.2.0) (2026-02-10)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* [MatrixRTC] Remove sending of deprecated `notify` event (we now use `m.rtc.notification`) ([#5167](https://github.com/matrix-org/matrix-js-sdk/pull/5167)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Use stable /auth\_metadata endpoint where homeserver supports v1.15 ([#5174](https://github.com/matrix-org/matrix-js-sdk/pull/5174)). Contributed by @hughns.
|
||||
* Support additional\_creators in upgradeRoom (MSC4289) ([#5173](https://github.com/matrix-org/matrix-js-sdk/pull/5173)). Contributed by @andybalaam.
|
||||
* [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId ([#5166](https://github.com/matrix-org/matrix-js-sdk/pull/5166)). Contributed by @toger5.
|
||||
* [MatrixRTC] Do not send the `livekit_alias` in sticky events ([#5165](https://github.com/matrix-org/matrix-js-sdk/pull/5165)). Contributed by @toger5.
|
||||
* Improve startup performance by using `promise.all` when processing rooms from sync ([#5095](https://github.com/matrix-org/matrix-js-sdk/pull/5095)). Contributed by @MidhunSureshR.
|
||||
* Add OAuthGrantType enum for OAuth 2.0 API grant types ([#5161](https://github.com/matrix-org/matrix-js-sdk/pull/5161)). Contributed by @hughns.
|
||||
* Add support for stable OAuth2.0 aware feature from MSC3824 ([#5159](https://github.com/matrix-org/matrix-js-sdk/pull/5159)). Contributed by @hughns.
|
||||
* Give RoomWidgetClient the ability to send and receive sticky events ([#5142](https://github.com/matrix-org/matrix-js-sdk/pull/5142)). Contributed by @robintown.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated ([#5168](https://github.com/matrix-org/matrix-js-sdk/pull/5168)). Contributed by @toger5.
|
||||
* [MatrixRTC] Fix delayId not resetting on leave ([#5156](https://github.com/matrix-org/matrix-js-sdk/pull/5156)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [40.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.1.0) (2026-01-27)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate unused `EventShieldReason` reason codes ([#5127](https://github.com/matrix-org/matrix-js-sdk/pull/5127)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add stable m.oauth UIA stage enum ([#5138](https://github.com/matrix-org/matrix-js-sdk/pull/5138)). Contributed by @hughns.
|
||||
* Add `MatrixEvent.getKeyForwardingUser` ([#5128](https://github.com/matrix-org/matrix-js-sdk/pull/5128)). Contributed by @richvdh.
|
||||
* Add types for (unstable) policy servers ([#5116](https://github.com/matrix-org/matrix-js-sdk/pull/5116)). Contributed by @turt2live.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Recalculate room name on loading members ([#5164](https://github.com/matrix-org/matrix-js-sdk/pull/5164)). Contributed by @RiotRobot.
|
||||
* Avoid rapidly retrying failed requests ([#5146](https://github.com/matrix-org/matrix-js-sdk/pull/5146)). Contributed by @andybalaam.
|
||||
* [matrixRTC] MatrixRTCSessions, add missing event reemission. ([#5144](https://github.com/matrix-org/matrix-js-sdk/pull/5144)). Contributed by @toger5.
|
||||
* Use normal base64 encoding for RTC backend identities ([#5129](https://github.com/matrix-org/matrix-js-sdk/pull/5129)). Contributed by @robintown.
|
||||
* export parseCallNotificationContent and isMyMembership from RTC types ([#5132](https://github.com/matrix-org/matrix-js-sdk/pull/5132)). Contributed by @Half-Shot.
|
||||
|
||||
|
||||
Changes in [40.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.0.0) (2026-01-13)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "40.1.0-rc.0",
|
||||
"version": "40.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
@@ -56,7 +56,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "7",
|
||||
"sdp-transform": "^3.0.0",
|
||||
@@ -98,7 +98,7 @@
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^61.0.0",
|
||||
"eslint-plugin-jsdoc": "^62.0.0",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.5.0",
|
||||
@@ -110,7 +110,7 @@
|
||||
"knip": "^5.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "3.7.4",
|
||||
"prettier": "3.8.0",
|
||||
"rimraf": "^6.0.0",
|
||||
"typedoc": "^0.28.1",
|
||||
"typedoc-plugin-coverage": "^4.0.0",
|
||||
|
||||
+171
-1
@@ -32,7 +32,14 @@ import {
|
||||
type IRoomEvent,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import {
|
||||
createRoomWidgetClient,
|
||||
EventType,
|
||||
type IEvent,
|
||||
MatrixError,
|
||||
MsgType,
|
||||
UpdateDelayedEventAction,
|
||||
} from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
|
||||
@@ -42,6 +49,7 @@ import { sleep } from "../../src/utils";
|
||||
import { SlidingSync } from "../../src/sliding-sync";
|
||||
import { logger } from "../../src/logger";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
|
||||
|
||||
const testOIDCToken = {
|
||||
access_token: "12345678",
|
||||
@@ -49,6 +57,7 @@ const testOIDCToken = {
|
||||
matrix_server_name: "homeserver.oabc",
|
||||
token_type: "Bearer",
|
||||
};
|
||||
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = vi.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = vi.fn();
|
||||
@@ -167,6 +176,9 @@ describe("RoomWidgetClient", () => {
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -422,6 +434,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
2000,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -442,6 +455,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
parentDelayId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -855,6 +869,162 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky events", () => {
|
||||
describe("when supported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
|
||||
Promise.resolve(feature === "org.matrix.msc4354"),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests capabilities when set", async () => {
|
||||
await makeClient({ sendSticky: true, receiveSticky: true });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
});
|
||||
|
||||
it("does not request capabilities when unset", async () => {
|
||||
await makeClient({});
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(
|
||||
MatrixCapabilities.MSC4407ReceiveStickyEvent,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith(EventType.RTCMembership);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
await client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
});
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
EventType.RTCMembership,
|
||||
{ msc4354_sticky_key: "test" },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
2000,
|
||||
);
|
||||
});
|
||||
|
||||
it("receives (adds, updates, then removes when redacted)", async () => {
|
||||
await makeClient({ receiveEvent: [EventType.RTCMembership, EventType.RoomRedaction] });
|
||||
const room = client.getRoom("!1:example.org")!;
|
||||
|
||||
function expectStickyEvents(events: IEvent[]) {
|
||||
expect([...room._unstable_getStickyEvents()].map((e) => e.getEffectiveEvent())).toEqual(events);
|
||||
}
|
||||
|
||||
async function sendAndExpectStickyUpdate(
|
||||
eventToSend: IEvent,
|
||||
added: IEvent[],
|
||||
updated: { current: IEvent; previous: IEvent }[],
|
||||
removed: IEvent[],
|
||||
) {
|
||||
const emittedStickyUpdate = new Promise<
|
||||
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
|
||||
>((resolve) => room.once(RoomStickyEventsEvent.Update, (...args) => resolve(args)));
|
||||
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
|
||||
detail: { data: eventToSend },
|
||||
}),
|
||||
);
|
||||
|
||||
const [addedReceived, updatedReceived, removedReceived] = await emittedStickyUpdate;
|
||||
expect(addedReceived.map((e) => e.getEffectiveEvent())).toEqual(added);
|
||||
expect(
|
||||
updatedReceived.map(({ current, previous }) => ({
|
||||
current: current.getEffectiveEvent(),
|
||||
previous: previous.getEffectiveEvent(),
|
||||
})),
|
||||
).toEqual(updated);
|
||||
expect(removedReceived.map((e) => e.getEffectiveEvent())).toEqual(removed);
|
||||
}
|
||||
|
||||
// First, add a new sticky event to the map. The client should emit.
|
||||
const event1 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$pduhfiidph",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event1, [event1], [], []);
|
||||
// It should remain cached in the sticky map
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Next, update the same key in the sticky map
|
||||
const event2 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$zshgyutptfh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event2, [], [{ current: event2, previous: event1 }], []);
|
||||
expectStickyEvents([event2]);
|
||||
|
||||
// Next, redact the second event. Because it has the first as a predecessor, the map should revert to
|
||||
// the first event.
|
||||
const redaction1 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$cimoexnvz",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event2.event_id,
|
||||
content: { redacts: event2.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction1, [], [{ current: event1, previous: event2 }], []);
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Finally, redact the first event. Now everything should be gone from the map.
|
||||
const redaction2 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$drgzmenlh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event1.event_id,
|
||||
content: { redacts: event1.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction2, [], [], [event1]);
|
||||
expectStickyEvents([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsupported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("fails to send", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
await expect(
|
||||
client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
}),
|
||||
).rejects.toThrow("Server does not support");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = new Map([
|
||||
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
||||
|
||||
@@ -51,18 +51,21 @@ describe("SSO login URL", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
|
||||
expect(url.searchParams.has("action")).toBe(false);
|
||||
});
|
||||
|
||||
it("register", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
|
||||
expect(url.searchParams.get("action")).toEqual("register");
|
||||
});
|
||||
|
||||
it("login", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
|
||||
expect(url.searchParams.get("action")).toEqual("login");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";
|
||||
import { type CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { SyncResponder } from "../test-utils/SyncResponder.ts";
|
||||
import { mockInitialApiRequests } from "../test-utils/mockEndpoints.ts";
|
||||
import { type Transport } from "src/matrixrtc/index.ts";
|
||||
import { type Transport } from "../../src/matrixrtc/index.ts";
|
||||
|
||||
vi.useFakeTimers();
|
||||
|
||||
@@ -3877,6 +3877,27 @@ describe("MatrixClient", function () {
|
||||
makeClient();
|
||||
});
|
||||
|
||||
it("should use stable prefix", async () => {
|
||||
const metadata = mockOpenIdConfiguration();
|
||||
client.getVersions = vi.fn().mockResolvedValue({
|
||||
versions: ["v1.15"],
|
||||
});
|
||||
httpLookups = [
|
||||
{
|
||||
method: "GET",
|
||||
path: `/auth_metadata`,
|
||||
data: metadata,
|
||||
prefix: "/_matrix/client/v1",
|
||||
},
|
||||
];
|
||||
|
||||
await expect(client.getAuthMetadata()).resolves.toEqual({
|
||||
...metadata,
|
||||
signingKeys: [],
|
||||
});
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should use unstable prefix", async () => {
|
||||
const metadata = mockOpenIdConfiguration();
|
||||
httpLookups = [
|
||||
|
||||
@@ -158,8 +158,10 @@ describe("CallMembership", () => {
|
||||
expect(membership.eventId).toBe("$eventid");
|
||||
});
|
||||
it("returns correct slot_id", () => {
|
||||
expect(membership.slotId).toBe("m.call#");
|
||||
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
|
||||
// for legacy events we expect the room to be added automagically
|
||||
// See INFO_SLOT_ID_LEGACY_CASE comments
|
||||
expect(membership.slotId).toBe("m.call#ROOM");
|
||||
expect(membership.slotDescription).toStrictEqual({ id: "ROOM", application: "m.call" });
|
||||
});
|
||||
it("returns correct deviceId", () => {
|
||||
expect(membership.deviceId).toBe("AAAAAAA");
|
||||
|
||||
@@ -49,7 +49,7 @@ const mockFocus = { type: "mock" };
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
const callSession = { id: "", application: "m.call" };
|
||||
const callSession = { id: "ROOM", application: "m.call" };
|
||||
|
||||
describe("MatrixRTCSession", () => {
|
||||
let client: MatrixClient;
|
||||
@@ -134,12 +134,12 @@ describe("MatrixRTCSession", () => {
|
||||
);
|
||||
await flushPromises();
|
||||
expect(sess?.memberships.length).toEqual(1);
|
||||
expect(sess?.memberships[0].slotDescription.id).toEqual("");
|
||||
expect(sess?.memberships[0].slotDescription.id).toEqual("ROOM");
|
||||
expect(sess?.memberships[0].scope).toEqual("m.room");
|
||||
expect(sess?.memberships[0].application).toEqual("m.call");
|
||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
expect(sess?.memberships[0].isExpired()).toEqual(false);
|
||||
expect(sess?.slotDescription.id).toEqual("");
|
||||
expect(sess?.slotDescription.id).toEqual("ROOM");
|
||||
});
|
||||
|
||||
it("ignores memberships where application is not m.call", () => {
|
||||
@@ -381,12 +381,12 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
await flushPromises();
|
||||
expect(sess?.memberships.length).toEqual(1);
|
||||
expect(sess?.memberships[0].slotDescription.id).toEqual("");
|
||||
expect(sess?.memberships[0].slotDescription.id).toEqual("ROOM");
|
||||
expect(sess?.memberships[0].scope).toEqual("m.room");
|
||||
expect(sess?.memberships[0].application).toEqual("m.call");
|
||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
expect(sess?.memberships[0].isExpired()).toEqual(false);
|
||||
expect(sess?.slotDescription.id).toEqual("");
|
||||
expect(sess?.slotDescription.id).toEqual("ROOM");
|
||||
});
|
||||
it("combines sticky and membership events when both exist", async () => {
|
||||
// Create a room with identical member state and sticky state for the same user.
|
||||
@@ -415,7 +415,7 @@ describe("MatrixRTCSession", () => {
|
||||
const memberships = sess.memberships;
|
||||
expect(memberships.length).toEqual(2);
|
||||
expect(memberships[0].sender).toEqual(stickyUserId);
|
||||
expect(memberships[0].slotDescription.id).toEqual("");
|
||||
expect(memberships[0].slotDescription.id).toEqual("ROOM");
|
||||
expect(memberships[0].scope).toEqual("m.room");
|
||||
expect(memberships[0].application).toEqual("m.call");
|
||||
expect(memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
@@ -424,7 +424,7 @@ describe("MatrixRTCSession", () => {
|
||||
// Then state
|
||||
expect(memberships[1].sender).toEqual(membershipTemplate.user_id);
|
||||
|
||||
expect(sess?.slotDescription.id).toEqual("");
|
||||
expect(sess?.slotDescription.id).toEqual("ROOM");
|
||||
});
|
||||
it("handles an incoming sticky event to an existing session", async () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
@@ -620,9 +620,7 @@ describe("MatrixRTCSession", () => {
|
||||
it("sends a notification when starting a call and emit DidSendCallNotification", async () => {
|
||||
// Simulate a join, including the update to the room state
|
||||
// Ensure sendEvent returns event IDs so the DidSendCallNotification payload includes them
|
||||
sendEventMock
|
||||
.mockResolvedValueOnce({ event_id: "legacy-evt" })
|
||||
.mockResolvedValueOnce({ event_id: "new-evt" });
|
||||
sendEventMock.mockResolvedValueOnce({ event_id: "new-evt" });
|
||||
const didSendEventFn = vi.fn();
|
||||
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, didSendEventFn);
|
||||
// Create an additional listener to create a promise that resolves after the emission.
|
||||
@@ -647,43 +645,25 @@ describe("MatrixRTCSession", () => {
|
||||
"sender_ts": expect.any(Number),
|
||||
});
|
||||
|
||||
// Check if deprecated notify event is also sent.
|
||||
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
|
||||
"application": "m.call",
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"notify_type": "ring",
|
||||
"call_id": "",
|
||||
});
|
||||
await didSendNotification;
|
||||
// And ensure we emitted the DidSendCallNotification event with both payloads
|
||||
expect(didSendEventFn).toHaveBeenCalledWith(
|
||||
{
|
||||
"event_id": "new-evt",
|
||||
"lifetime": 30000,
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"m.relates_to": {
|
||||
event_id: expect.any(String),
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
"notification_type": "ring",
|
||||
"sender_ts": expect.any(Number),
|
||||
expect(didSendEventFn).toHaveBeenCalledWith({
|
||||
"event_id": "new-evt",
|
||||
"lifetime": 30000,
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"m.relates_to": {
|
||||
event_id: expect.any(String),
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
{
|
||||
"application": "m.call",
|
||||
"call_id": "",
|
||||
"event_id": "legacy-evt",
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"notify_type": "ring",
|
||||
},
|
||||
);
|
||||
"notification_type": "ring",
|
||||
"sender_ts": expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a notification with a intent when starting a call and emits DidSendCallNotification", async () => {
|
||||
// Simulate a join, including the update to the room state
|
||||
// Ensure sendEvent returns event IDs so the DidSendCallNotification payload includes them
|
||||
sendEventMock
|
||||
.mockResolvedValueOnce({ event_id: "legacy-evt" })
|
||||
.mockResolvedValueOnce({ event_id: "new-evt" });
|
||||
sendEventMock.mockResolvedValueOnce({ event_id: "new-evt" });
|
||||
const didSendEventFn = vi.fn();
|
||||
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, didSendEventFn);
|
||||
// Create an additional listener to create a promise that resolves after the emission.
|
||||
@@ -707,7 +687,7 @@ describe("MatrixRTCSession", () => {
|
||||
]);
|
||||
|
||||
await sess!._onRTCSessionMemberUpdate();
|
||||
const ownMembershipId = sess?.memberships[0].eventId;
|
||||
const ownMembershipEventId = sess?.memberships[0].eventId;
|
||||
expect(sess!.getConsensusCallIntent()).toEqual("audio");
|
||||
|
||||
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.RTCNotification, {
|
||||
@@ -715,43 +695,27 @@ describe("MatrixRTCSession", () => {
|
||||
"notification_type": "ring",
|
||||
"m.call.intent": "audio",
|
||||
"m.relates_to": {
|
||||
event_id: ownMembershipId,
|
||||
event_id: ownMembershipEventId,
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
"lifetime": 30000,
|
||||
"sender_ts": expect.any(Number),
|
||||
});
|
||||
|
||||
// Check if deprecated notify event is also sent.
|
||||
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
|
||||
"application": "m.call",
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"notify_type": "ring",
|
||||
"call_id": "",
|
||||
});
|
||||
await didSendNotification;
|
||||
// And ensure we emitted the DidSendCallNotification event with both payloads
|
||||
expect(didSendEventFn).toHaveBeenCalledWith(
|
||||
{
|
||||
"event_id": "new-evt",
|
||||
"lifetime": 30000,
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"m.relates_to": {
|
||||
event_id: expect.any(String),
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
"notification_type": "ring",
|
||||
"m.call.intent": "audio",
|
||||
"sender_ts": expect.any(Number),
|
||||
expect(didSendEventFn).toHaveBeenCalledWith({
|
||||
"event_id": "new-evt",
|
||||
"lifetime": 30000,
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"m.relates_to": {
|
||||
event_id: expect.any(String),
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
{
|
||||
"application": "m.call",
|
||||
"call_id": "",
|
||||
"event_id": "legacy-evt",
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"notify_type": "ring",
|
||||
},
|
||||
);
|
||||
"notification_type": "ring",
|
||||
"m.call.intent": "audio",
|
||||
"sender_ts": expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't send a notification when joining an existing call", async () => {
|
||||
|
||||
@@ -71,7 +71,7 @@ function createAsyncHandle<T>(method: MockedFunction<(...args: any[]) => any>) {
|
||||
return { reject, resolve };
|
||||
}
|
||||
|
||||
const callSession = { id: "", application: "m.call" };
|
||||
const callSession = { id: "ROOM", application: "m.call" };
|
||||
|
||||
describe("MembershipManager", () => {
|
||||
let client: MockClient;
|
||||
@@ -416,8 +416,9 @@ describe("MembershipManager", () => {
|
||||
await manager.leave();
|
||||
expect(client._unstable_sendScheduledDelayedEvent).toHaveBeenLastCalledWith("id");
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
expect(manager.delayId).toBe(undefined);
|
||||
});
|
||||
it("send leave event when leave is called and resolving delayed leave fails", async () => {
|
||||
it("send leave event when leave is called and resolving delayed leave fails unknown error", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
@@ -431,6 +432,27 @@ describe("MembershipManager", () => {
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
// If there is a unknown error, we do not reset the delayId
|
||||
// The delayed event might still be around and we track it.
|
||||
expect(manager.delayId).not.toBe(undefined);
|
||||
});
|
||||
it("send leave event when leave is called and resolving delayed leave fails not found error", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
new MatrixError({ errcode: "M_NOT_FOUND" }, 404),
|
||||
);
|
||||
await manager.leave();
|
||||
|
||||
// We send a normal leave event since we failed using sendScheduledDelayedEvent.
|
||||
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
room.roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
expect(manager.delayId).toBe(undefined);
|
||||
});
|
||||
it("does nothing if not joined", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
@@ -947,8 +969,8 @@ describe("MembershipManager", () => {
|
||||
id: "@alice:example.org:AAAAAAA_m.call",
|
||||
device_id: "AAAAAAA",
|
||||
},
|
||||
slot_id: "m.call#",
|
||||
rtc_transports: [focus],
|
||||
slot_id: "m.call#ROOM",
|
||||
rtc_transports: [{ type: focus.type, livekit_service_url: focus.livekit_service_url }],
|
||||
versions: [],
|
||||
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ import { type Mocked, type MockedObject } from "vitest";
|
||||
import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src";
|
||||
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc";
|
||||
import { secureRandomString } from "../../../src/randomstring";
|
||||
import { type CallMembershipIdentityParts } from "src/matrixrtc/EncryptionManager";
|
||||
import { type CallMembershipIdentityParts } from "../../../src/matrixrtc/EncryptionManager";
|
||||
import { logger } from "../../../src/logger.ts";
|
||||
|
||||
export type MembershipData = (SessionMembershipData | {}) & { user_id: string };
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2026 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 { MatrixClient } from "../../src/client";
|
||||
|
||||
describe("Room upgrades", function () {
|
||||
it("Sends an HTTP request upgrading the room", () => {
|
||||
// Given a client with a fake authedRequest method
|
||||
const { client, authedRequest } = createClient();
|
||||
|
||||
// When we upgrade the room to version 12
|
||||
client.upgradeRoom("!r1", "12");
|
||||
|
||||
// Then we make an HTTP request to the correct endpoint, with the
|
||||
// version provided in the JSON.
|
||||
expect(authedRequest).toHaveBeenCalledWith("POST", "/rooms/!r1/upgrade", undefined, { new_version: "12" });
|
||||
});
|
||||
|
||||
it("Includes additional_creators if provided", () => {
|
||||
// Given a client with a fake authedRequest method
|
||||
const { client, authedRequest } = createClient();
|
||||
|
||||
// When we upgrade the room to version 13 and supply additionalCreators
|
||||
client.upgradeRoom("!r1", "13", ["@u:s.co", "@v:a.b"]);
|
||||
|
||||
// Then we make an HTTP request to the correct endpoint, with the
|
||||
// version and additional creators provided.
|
||||
expect(authedRequest).toHaveBeenCalledWith("POST", "/rooms/!r1/upgrade", undefined, {
|
||||
new_version: "13",
|
||||
additional_creators: ["@u:s.co", "@v:a.b"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
///
|
||||
function createClient(): { client: MatrixClient; authedRequest: any } {
|
||||
const authedRequest = vi.fn();
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
userId: "@u:s.co",
|
||||
});
|
||||
client.http.authedRequest = authedRequest;
|
||||
return { client, authedRequest };
|
||||
}
|
||||
+22
-5
@@ -2103,7 +2103,7 @@ describe("Room", function () {
|
||||
}
|
||||
|
||||
const memberEvent = utils.mkMembership({
|
||||
user: "@user_a:bar",
|
||||
user: userA,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
@@ -2114,7 +2114,7 @@ describe("Room", function () {
|
||||
const client = createClientMock([memberEvent]);
|
||||
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar")!;
|
||||
const memberA = room.getMember(userA)!;
|
||||
expect(memberA.name).toEqual("User A");
|
||||
const storedMembers = client.store.storedMembers!;
|
||||
expect(storedMembers.length).toEqual(1);
|
||||
@@ -2123,7 +2123,7 @@ describe("Room", function () {
|
||||
|
||||
it("should take members from storage if available", async function () {
|
||||
const memberEvent2 = utils.mkMembership({
|
||||
user: "@user_a:bar",
|
||||
user: userA,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
@@ -2134,7 +2134,7 @@ describe("Room", function () {
|
||||
|
||||
await room.loadMembersIfNeeded();
|
||||
|
||||
const memberA = room.getMember("@user_a:bar")!;
|
||||
const memberA = room.getMember(userA)!;
|
||||
expect(memberA.name).toEqual("User A");
|
||||
});
|
||||
|
||||
@@ -2145,9 +2145,26 @@ describe("Room", function () {
|
||||
|
||||
client.members.mockReturnValue({ chunk: [memberEvent] });
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar")!;
|
||||
const memberA = room.getMember(userA)!;
|
||||
expect(memberA.name).toEqual("User A");
|
||||
});
|
||||
|
||||
it("should emit room name change event if loading members causes the room name to change", async function () {
|
||||
const otherMemberEvent = utils.mkMembership({
|
||||
user: userB,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "User B",
|
||||
});
|
||||
|
||||
const client = createClientMock([memberEvent, otherMemberEvent]);
|
||||
const room = new Room(roomId, client as any, userA, { lazyLoadMembers: true });
|
||||
|
||||
expect(room.name).toEqual("!foo:bar");
|
||||
await room.loadMembersIfNeeded();
|
||||
expect(room.name).toEqual("User B");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMyMembership", function () {
|
||||
|
||||
@@ -236,4 +236,105 @@ describe("OutgoingRequestsManager", () => {
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Repeated processing of outgoing requests", () => {
|
||||
it("Processes any requests still in the queue after processing", async () => {
|
||||
// Given that the first time we call outgoingRequests we get back
|
||||
// requests 1 and 2, but the second time, we get request 3
|
||||
const [request1, request2, request3] = setupOutgoingTwoRequestsThenOne();
|
||||
|
||||
// (And requests finish immediately)
|
||||
processor.makeOutgoingRequest.mockImplementation(async () => {
|
||||
return;
|
||||
});
|
||||
|
||||
// When we ask a manager to process the requests
|
||||
await manager.doProcessOutgoingRequests();
|
||||
|
||||
// Then all three are processed because we re-call outgoingRequests
|
||||
// to check for more
|
||||
await vi.waitFor(() => expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(3));
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(3);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request3);
|
||||
});
|
||||
|
||||
it("Does reprocess if any request succeeded, even if some failed", async () => {
|
||||
// Given that the first time we call outgoingRequests we get back
|
||||
// requests 1 and 2, but the second time, we get request 3
|
||||
const [request1, request2, request3] = setupOutgoingTwoRequestsThenOne();
|
||||
|
||||
// And the first request fails, but subsequent ones pass
|
||||
processor.makeOutgoingRequest
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new Error("This request failed!");
|
||||
})
|
||||
.mockImplementation(async () => {
|
||||
return;
|
||||
});
|
||||
|
||||
// When we ask a manager to process the requests
|
||||
await manager.doProcessOutgoingRequests();
|
||||
|
||||
// Then all three are processed because we re-call outgoingRequests
|
||||
// to check for more, even though one failed
|
||||
await vi.waitFor(() => expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(3));
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(3);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request3);
|
||||
});
|
||||
|
||||
it("Does not reprocess if all requests failed", async () => {
|
||||
// Given that the first time we call outgoingRequests we get back
|
||||
// requests 1 and 2, but the second time, we get request 3
|
||||
const [request1, request2, request3] = setupOutgoingTwoRequestsThenOne();
|
||||
|
||||
// And the first two requests fail, but subsequent ones pass
|
||||
processor.makeOutgoingRequest
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new Error("Request 1 failed!");
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new Error("Request 2 failed!");
|
||||
})
|
||||
.mockImplementation(async () => {
|
||||
return;
|
||||
});
|
||||
|
||||
// When we ask a manager to process the requests
|
||||
await manager.doProcessOutgoingRequests();
|
||||
|
||||
// Then only the first two requests are processed, because since
|
||||
// they both failed we stop retrying
|
||||
await vi.waitFor(() => expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1));
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(2);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
|
||||
expect(processor.makeOutgoingRequest).not.toHaveBeenCalledWith(request3);
|
||||
});
|
||||
|
||||
/// Mock calls to olmMachine.outgoingRequests. The first time it is
|
||||
// called, return two requests and the second time return a third one.
|
||||
//
|
||||
// Returns the three returned requests.
|
||||
function setupOutgoingTwoRequestsThenOne(): [OutgoingRequest, OutgoingRequest, OutgoingRequest] {
|
||||
const request1 = new RustSdkCryptoJs.KeysQueryRequest("1", "{}");
|
||||
const request2 = new RustSdkCryptoJs.KeysUploadRequest("2", "{}");
|
||||
const request3 = new RustSdkCryptoJs.KeysUploadRequest("3", "{}");
|
||||
|
||||
// Given that the first time we call outgoingRequests we get back
|
||||
// requests 1 and 2, but the second time, we get request 3
|
||||
olmMachine.outgoingRequests
|
||||
.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
|
||||
return [request1, request2];
|
||||
})
|
||||
.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
|
||||
return [request3];
|
||||
});
|
||||
|
||||
return [request1, request2, request3];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+10
-4
@@ -48,11 +48,17 @@ export interface IPasswordFlow extends ILoginFlow {
|
||||
type: "m.login.password";
|
||||
}
|
||||
|
||||
export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
|
||||
"delegated_oidc_compatibility",
|
||||
export const OAUTH_AWARE_PREFERRED_FLOW_FIELD = new UnstableValue(
|
||||
"oauth_aware_preferred",
|
||||
"org.matrix.msc3824.delegated_oidc_compatibility",
|
||||
);
|
||||
|
||||
/**
|
||||
* @alias
|
||||
* @deprecated use `OAUTH_AWARE_PREFERRED_FLOW_FIELD` instead.
|
||||
*/
|
||||
export const DELEGATED_OIDC_COMPATIBILITY = OAUTH_AWARE_PREFERRED_FLOW_FIELD;
|
||||
|
||||
/**
|
||||
* Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
|
||||
*/
|
||||
@@ -60,8 +66,8 @@ export interface ISSOFlow extends ILoginFlow {
|
||||
type: "m.login.sso" | "m.login.cas";
|
||||
// eslint-disable-next-line camelcase
|
||||
identity_providers?: IIdentityProvider[];
|
||||
[DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
|
||||
[DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
|
||||
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.name]?: boolean;
|
||||
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.altName]?: boolean;
|
||||
}
|
||||
|
||||
export enum IdentityProviderBrand {
|
||||
|
||||
Vendored
-24
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
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 "oidc-client-ts";
|
||||
|
||||
declare module "oidc-client-ts" {
|
||||
interface OidcMetadata {
|
||||
// Add the missing device_authorization_endpoint field to the OidcMetadata interface
|
||||
device_authorization_endpoint?: string;
|
||||
}
|
||||
}
|
||||
+19
-8
@@ -6689,6 +6689,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const params = {
|
||||
redirectUrl,
|
||||
[SSO_ACTION_PARAM.stable!]: action,
|
||||
[SSO_ACTION_PARAM.unstable!]: action,
|
||||
};
|
||||
|
||||
@@ -6936,13 +6937,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* Upgrades a room to a new protocol version
|
||||
* @param newVersion - The target version to upgrade to
|
||||
* @param additionalCreators - an optional list of user IDs of users who
|
||||
* should have the same permissions as the user performing the
|
||||
* upgrade
|
||||
* @returns Promise which resolves: Object with key 'replacement_room'
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public upgradeRoom(roomId: string, newVersion: string): Promise<{ replacement_room: string }> {
|
||||
public upgradeRoom(
|
||||
roomId: string,
|
||||
newVersion: string,
|
||||
additionalCreators?: string[],
|
||||
): Promise<{ replacement_room: string }> {
|
||||
// eslint-disable-line camelcase
|
||||
const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId });
|
||||
return this.http.authedRequest(Method.Post, path, undefined, { new_version: newVersion });
|
||||
return this.http.authedRequest(Method.Post, path, undefined, {
|
||||
new_version: newVersion,
|
||||
additional_creators: additionalCreators,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8826,21 +8837,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover and validate delegated auth configuration
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||
* Discover and validate the auth metadata for the OAuth 2.0 API.
|
||||
*
|
||||
* Fetches /auth_metadata falling back to legacy implementation using /auth_issuer followed by
|
||||
* https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
|
||||
* When successful, validated metadata is returned
|
||||
* When successful, validated metadata is returned.
|
||||
*
|
||||
* @returns validated authentication metadata and optionally signing keys
|
||||
* @throws when delegated auth config is invalid or unreachable
|
||||
* @experimental - part of MSC2965
|
||||
*/
|
||||
public async getAuthMetadata(): Promise<OidcClientConfig> {
|
||||
let authMetadata: unknown | undefined;
|
||||
try {
|
||||
const useStable = await this.isVersionSupported("v1.15");
|
||||
authMetadata = await this.http.request<unknown>(Method.Get, "/auth_metadata", undefined, undefined, {
|
||||
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||
prefix: useStable ? ClientPrefix.V1 : ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
|
||||
|
||||
+110
-54
@@ -38,6 +38,7 @@ import {
|
||||
type SendDelayedEventRequestOpts,
|
||||
type SendDelayedEventResponse,
|
||||
UpdateDelayedEventAction,
|
||||
isSendDelayedEventRequestOpts,
|
||||
} from "./@types/requests.ts";
|
||||
import { EventType, type StateEvents } from "./@types/event.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
@@ -56,7 +57,7 @@ import { ConnectionError, MatrixError } from "./http-api/errors.ts";
|
||||
import { User } from "./models/user.ts";
|
||||
import { type Room } from "./models/room.ts";
|
||||
import { type ToDeviceBatch, type ToDevicePayload } from "./models/ToDeviceMessage.ts";
|
||||
import { MapWithDefault, recursiveMapToObject } from "./utils.ts";
|
||||
import { MapWithDefault, type QueryDict, recursiveMapToObject } from "./utils.ts";
|
||||
import { type EmptyObject, TypedEventEmitter, UnsupportedDelayedEventsEndpointError } from "./matrix.ts";
|
||||
|
||||
interface IStateEventRequest {
|
||||
@@ -122,6 +123,20 @@ export interface ICapabilities {
|
||||
* @defaultValue false
|
||||
*/
|
||||
updateDelayedEvents?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to send sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
sendSticky?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to receive sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
receiveSticky?: boolean;
|
||||
}
|
||||
|
||||
export enum RoomWidgetClientEvent {
|
||||
@@ -197,54 +212,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
)
|
||||
: Promise.resolve();
|
||||
|
||||
// Request capabilities for the functionality this client needs to support
|
||||
if (
|
||||
capabilities.sendEvent?.length ||
|
||||
capabilities.receiveEvent?.length ||
|
||||
capabilities.sendMessage === true ||
|
||||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
|
||||
capabilities.receiveMessage === true ||
|
||||
(Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length) ||
|
||||
capabilities.sendState?.length ||
|
||||
capabilities.receiveState?.length
|
||||
) {
|
||||
widgetApi.requestCapabilityForRoomTimeline(roomId);
|
||||
}
|
||||
capabilities.sendEvent?.forEach((eventType) => widgetApi.requestCapabilityToSendEvent(eventType));
|
||||
capabilities.receiveEvent?.forEach((eventType) => widgetApi.requestCapabilityToReceiveEvent(eventType));
|
||||
if (capabilities.sendMessage === true) {
|
||||
widgetApi.requestCapabilityToSendMessage();
|
||||
} else if (Array.isArray(capabilities.sendMessage)) {
|
||||
capabilities.sendMessage.forEach((msgType) => widgetApi.requestCapabilityToSendMessage(msgType));
|
||||
}
|
||||
if (capabilities.receiveMessage === true) {
|
||||
widgetApi.requestCapabilityToReceiveMessage();
|
||||
} else if (Array.isArray(capabilities.receiveMessage)) {
|
||||
capabilities.receiveMessage.forEach((msgType) => widgetApi.requestCapabilityToReceiveMessage(msgType));
|
||||
}
|
||||
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
|
||||
widgetApi.requestCapabilityToSendState(eventType, stateKey),
|
||||
);
|
||||
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
|
||||
widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
|
||||
);
|
||||
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);
|
||||
}
|
||||
this.requestInitialCapabilities(capabilities, roomId);
|
||||
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||
@@ -260,10 +228,70 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
if (sendContentLoaded) widgetApi.sendContentLoaded();
|
||||
}
|
||||
|
||||
private requestInitialCapabilities(capabilities: ICapabilities, roomId: string): void {
|
||||
// Request capabilities for the functionality this client needs to support
|
||||
if (
|
||||
capabilities.sendEvent?.length ||
|
||||
capabilities.receiveEvent?.length ||
|
||||
capabilities.sendMessage === true ||
|
||||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
|
||||
capabilities.receiveMessage === true ||
|
||||
(Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length) ||
|
||||
capabilities.sendState?.length ||
|
||||
capabilities.receiveState?.length
|
||||
) {
|
||||
this.widgetApi.requestCapabilityForRoomTimeline(roomId);
|
||||
}
|
||||
capabilities.sendEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToSendEvent(eventType));
|
||||
capabilities.receiveEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToReceiveEvent(eventType));
|
||||
if (capabilities.sendMessage === true) {
|
||||
this.widgetApi.requestCapabilityToSendMessage();
|
||||
} else if (Array.isArray(capabilities.sendMessage)) {
|
||||
capabilities.sendMessage.forEach((msgType) => this.widgetApi.requestCapabilityToSendMessage(msgType));
|
||||
}
|
||||
if (capabilities.receiveMessage === true) {
|
||||
this.widgetApi.requestCapabilityToReceiveMessage();
|
||||
} else if (Array.isArray(capabilities.receiveMessage)) {
|
||||
capabilities.receiveMessage.forEach((msgType) => this.widgetApi.requestCapabilityToReceiveMessage(msgType));
|
||||
}
|
||||
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
|
||||
this.widgetApi.requestCapabilityToSendState(eventType, stateKey),
|
||||
);
|
||||
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
|
||||
this.widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
|
||||
);
|
||||
capabilities.sendToDevice?.forEach((eventType) => this.widgetApi.requestCapabilityToSendToDevice(eventType));
|
||||
capabilities.receiveToDevice?.forEach((eventType) =>
|
||||
this.widgetApi.requestCapabilityToReceiveToDevice(eventType),
|
||||
);
|
||||
if (
|
||||
capabilities.sendDelayedEvents &&
|
||||
(capabilities.sendEvent?.length ||
|
||||
capabilities.sendMessage === true ||
|
||||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
|
||||
capabilities.sendState?.length)
|
||||
) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
}
|
||||
if (capabilities.updateDelayedEvents) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
}
|
||||
if (capabilities.sendSticky) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
}
|
||||
if (capabilities.receiveSticky) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
}
|
||||
if (capabilities.turnServers) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
|
||||
}
|
||||
}
|
||||
|
||||
public async supportUpdateState(): Promise<boolean> {
|
||||
return (await this.widgetApi.getClientVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE);
|
||||
}
|
||||
|
||||
private readonly syncApiResolver = Promise.withResolvers<void>();
|
||||
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
|
||||
this.lifecycle = new AbortController();
|
||||
|
||||
@@ -282,6 +310,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
} else {
|
||||
this.syncApi = new SyncApi(this, opts, this.buildSyncApiOptions());
|
||||
}
|
||||
this.syncApiResolver.resolve();
|
||||
|
||||
this.room = this.syncApi.createRoom(this.roomId);
|
||||
this.store.storeRoom(this.room);
|
||||
@@ -346,17 +375,41 @@ 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,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOpts: SendDelayedEventRequestOpts,
|
||||
): Promise<SendDelayedEventResponse>;
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOpts?: SendDelayedEventRequestOpts,
|
||||
delayOptsOrQuery?: SendDelayedEventRequestOpts | QueryDict,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse | SendDelayedEventResponse> {
|
||||
let queryOpts = queryDict;
|
||||
let delayOpts: SendDelayedEventRequestOpts | undefined;
|
||||
if (delayOptsOrQuery && isSendDelayedEventRequestOpts(delayOptsOrQuery)) {
|
||||
delayOpts = delayOptsOrQuery;
|
||||
} else if (!queryOpts) {
|
||||
queryOpts = delayOptsOrQuery;
|
||||
}
|
||||
|
||||
const stickyDurationMs = queryOpts?.["org.matrix.msc4354.sticky_duration_ms"];
|
||||
if (stickyDurationMs !== undefined && typeof stickyDurationMs !== "number") {
|
||||
throw new Error("Sticky duration must be a number when defined");
|
||||
}
|
||||
// This is save since we just checked that above
|
||||
// We need the additional as assertion for the EW linter to be happy.
|
||||
// It is not capable of implying the type based on the throw if `stickyDurationMs !== undefined && typeof stickyDurationMs !== "number"`
|
||||
// above
|
||||
const stickyDurationMsAsNumber: number | undefined = stickyDurationMs as number | undefined;
|
||||
|
||||
// We need to extend the content with the redacts parameter
|
||||
// The js sdk uses event.redacts but the widget api uses event.content.redacts
|
||||
// This will be converted back to event.redacts in the widget driver.
|
||||
@@ -374,6 +427,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
room.roomId,
|
||||
"delay" in delayOpts ? delayOpts.delay : undefined,
|
||||
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
|
||||
stickyDurationMsAsNumber,
|
||||
)
|
||||
.catch(timeoutToConnectionError);
|
||||
return this.validateSendDelayedEventResponse(response);
|
||||
@@ -386,7 +440,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
let response: ISendEventFromWidgetResponseData;
|
||||
try {
|
||||
response = await this.widgetApi
|
||||
.sendRoomEvent(event.getType(), content, room.roomId)
|
||||
.sendRoomEvent(event.getType(), content, room.roomId, undefined, undefined, stickyDurationMsAsNumber)
|
||||
.catch(timeoutToConnectionError);
|
||||
} catch (e) {
|
||||
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
@@ -683,7 +737,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
|
||||
// Only inject once we have update the txId
|
||||
await this.updateTxId(event);
|
||||
|
||||
await this.syncApiResolver.promise;
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
if (await this.supportUpdateState()) {
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
|
||||
@@ -704,6 +758,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
}
|
||||
|
||||
this.emit(ClientEvent.Event, event);
|
||||
if (event.unstableStickyInfo !== undefined) this.room!._unstable_addStickyEvents([event]);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info(`Received event ${event.getId()} ${event.getType()}`);
|
||||
} else {
|
||||
@@ -737,6 +792,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
"received update_state widget action but the widget driver did not claim to support 'org.matrix.msc2762_update_state'",
|
||||
);
|
||||
}
|
||||
await this.syncApiResolver.promise;
|
||||
for (const rawEvent of ev.detail.data.state) {
|
||||
// Verify the room ID matches, since it's possible for the client to
|
||||
// send us state updates from other rooms if this widget is always
|
||||
|
||||
@@ -377,7 +377,23 @@ export class CallMembership {
|
||||
return data.slot_id;
|
||||
case "session":
|
||||
default:
|
||||
return slotDescriptionToId({ application: this.application, id: data.call_id });
|
||||
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
|
||||
// The spec got changed to use `"ROOM"` instead of `""` empyt string for the implicit default call.
|
||||
// State events still are sent with `""` however. To find other events that should end up in the same call,
|
||||
// we use the slotId.
|
||||
// Since the CallMembership is the public representation of a rtc.member event, we just pretend it is a
|
||||
// "ROOM" slotId/call_id.
|
||||
// This makes all the remote members work with just this simple trick.
|
||||
//
|
||||
// We of course now need to be careful when sending legacy events (state events)
|
||||
// They get a slotDescription containing "ROOM" since this is what we use starting at the time this comment
|
||||
// is commited.
|
||||
//
|
||||
// See the Other INFO_SLOT_ID_LEGACY_CASE comments to see where we revert back to "" just before sending the event.
|
||||
return slotDescriptionToId({
|
||||
application: this.application,
|
||||
id: data.call_id === "" ? "ROOM" : data.call_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,13 +26,12 @@ import { CallMembership } from "./CallMembership.ts";
|
||||
import { RoomStateEvent } from "../models/room-state.ts";
|
||||
import { MembershipManager, StickyEventMembershipManager } from "./MembershipManager.ts";
|
||||
import { type CallMembershipIdentityParts, EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
|
||||
import { deepCompare, logDurationSync } from "../utils.ts";
|
||||
import { logDurationSync } from "../utils.ts";
|
||||
import type {
|
||||
Statistics,
|
||||
RTCNotificationType,
|
||||
Status,
|
||||
IRTCNotificationContent,
|
||||
ICallNotifyContent,
|
||||
RTCCallIntent,
|
||||
Transport,
|
||||
} from "./types.ts";
|
||||
@@ -81,7 +80,6 @@ export type MatrixRTCSessionEventHandlerMap = {
|
||||
[MatrixRTCSessionEvent.MembershipManagerError]: (error: unknown) => void;
|
||||
[MatrixRTCSessionEvent.DidSendCallNotification]: (
|
||||
notificationContentNew: { event_id: string } & IRTCNotificationContent,
|
||||
notificationContentLegacy: { event_id: string } & ICallNotifyContent,
|
||||
) => void;
|
||||
};
|
||||
|
||||
@@ -328,7 +326,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
*/
|
||||
public static async sessionMembershipsForSlot(
|
||||
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState" | "_unstable_getStickyEvents">,
|
||||
slotDescription: SlotDescription,
|
||||
slotId: string,
|
||||
// default both true this implied we combine sticky and state events for the final call state
|
||||
// (prefer sticky events in case of a duplicate)
|
||||
options: SessionMembershipsForSlotOpts = DEFAULT_SESSION_MEMBERSHIPS_FOR_SLOT_OPTS,
|
||||
@@ -339,7 +337,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
const callMemberships = await computeBackendIdentityAndVerifyMemberEvents(
|
||||
room,
|
||||
callMemberEvents,
|
||||
slotDescription,
|
||||
slotId,
|
||||
logger,
|
||||
);
|
||||
|
||||
@@ -713,20 +711,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
notificationType: RTCNotificationType,
|
||||
callIntent?: RTCCallIntent,
|
||||
): void {
|
||||
const sendLegacyNotificationEvent = async (): Promise<{
|
||||
response: ISendEventResponse;
|
||||
content: ICallNotifyContent;
|
||||
}> => {
|
||||
const content: ICallNotifyContent = {
|
||||
"application": "m.call",
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"notify_type": notificationType === "notification" ? "notify" : notificationType,
|
||||
"call_id": this.callId!,
|
||||
};
|
||||
const response = await this.client.sendEvent(this.roomSubset.roomId, EventType.CallNotify, content);
|
||||
return { response, content };
|
||||
};
|
||||
const sendNewNotificationEvent = async (): Promise<{
|
||||
const sendNotificationEvent = async (): Promise<{
|
||||
response: ISendEventResponse;
|
||||
content: IRTCNotificationContent;
|
||||
}> => {
|
||||
@@ -747,12 +732,11 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
return { response, content };
|
||||
};
|
||||
|
||||
void Promise.all([sendLegacyNotificationEvent(), sendNewNotificationEvent()])
|
||||
.then(([legacy, newNotification]) => {
|
||||
void sendNotificationEvent()
|
||||
.then((notification) => {
|
||||
// Join event_id and origin event content
|
||||
const legacyResult = { ...legacy.response, ...legacy.content };
|
||||
const newResult = { ...newNotification.response, ...newNotification.content };
|
||||
this.emit(MatrixRTCSessionEvent.DidSendCallNotification, newResult, legacyResult);
|
||||
const newResult = { ...notification.response, ...notification.content };
|
||||
this.emit(MatrixRTCSessionEvent.DidSendCallNotification, newResult);
|
||||
})
|
||||
.catch(([errorLegacy, errorNew]) =>
|
||||
this.logger.error("Failed to send call notification", errorLegacy, errorNew),
|
||||
@@ -823,7 +807,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
|
||||
this.memberships = await MatrixRTCSession.sessionMembershipsForSlot(
|
||||
this.room,
|
||||
this.slotDescription,
|
||||
slotDescriptionToId(this.slotDescription),
|
||||
this.calculateMembershipsOpts,
|
||||
);
|
||||
|
||||
@@ -873,7 +857,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
async function computeBackendIdentityAndVerifyMemberEvents(
|
||||
room: Pick<Room, "hasMembershipState">,
|
||||
callMemberEvents: MatrixEvent[],
|
||||
slotDescription: SlotDescription,
|
||||
slotId: string,
|
||||
logger: Logger,
|
||||
): Promise<CallMembership[]> {
|
||||
const callMemberships: CallMembership[] = [];
|
||||
@@ -897,7 +881,7 @@ async function computeBackendIdentityAndVerifyMemberEvents(
|
||||
logger,
|
||||
);
|
||||
|
||||
if (isValidMembership(membership, room, slotDescription, logger)) {
|
||||
if (isValidMembership(membership, room, slotId, logger)) {
|
||||
callMemberships.push(membership);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -930,12 +914,12 @@ function quickFilterNonRelevantContents(content: IContent, logger: Logger): bool
|
||||
function isValidMembership(
|
||||
membership: CallMembership,
|
||||
room: Pick<Room, "hasMembershipState">,
|
||||
slotDescription: SlotDescription,
|
||||
slotId: string,
|
||||
logger: Logger,
|
||||
): boolean {
|
||||
if (!deepCompare(membership.slotDescription, slotDescription)) {
|
||||
if (membership.slotId !== slotId) {
|
||||
logger.info(
|
||||
`Ignoring membership of user ${membership.userId} for a different slot: ${JSON.stringify(membership.slotDescription)}`,
|
||||
`Ignoring membership of user ${membership.userId} for a different slot: user: ${JSON.stringify(membership.slotDescription)}, slotId: ${slotId})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
||||
public constructor(
|
||||
rootLogger: Logger,
|
||||
private client: MatrixClient,
|
||||
private readonly slotDescription: SlotDescription = { application: "m.call", id: "" }, // Default to the Matrix Call application
|
||||
private readonly slotDescription: SlotDescription = { application: "m.call", id: "ROOM" }, // Default to the Matrix Call application
|
||||
) {
|
||||
super();
|
||||
this.logger = rootLogger.getChild("[MatrixRTCSessionManager]");
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
type IMembershipManager,
|
||||
type MembershipManagerEventHandlerMap,
|
||||
} from "./IMembershipManager.ts";
|
||||
import { isLivekitTransportConfig } from "./LivekitTransport.ts";
|
||||
|
||||
/* MembershipActionTypes:
|
||||
On Join: ───────────────┐ ┌───────────────(1)───────────┐
|
||||
@@ -584,7 +585,7 @@ export class MembershipManager
|
||||
this.emit(MembershipManagerEvent.ProbablyLeft, this.state.probablyLeft);
|
||||
}
|
||||
|
||||
private setAndEmitDelayId(delayId?: string): void {
|
||||
private setAndEmitDelayId(delayId: string | undefined): void {
|
||||
if (this.state.delayId === delayId) return;
|
||||
|
||||
this.state.delayId = delayId;
|
||||
@@ -654,6 +655,7 @@ export class MembershipManager
|
||||
._unstable_sendScheduledDelayedEvent(delayId)
|
||||
.then(() => {
|
||||
this.state.hasMemberStateEvent = false;
|
||||
this.setAndEmitDelayId(undefined);
|
||||
this.resetRateLimitCounter(MembershipActionType.SendScheduledDelayedLeaveEvent);
|
||||
|
||||
return { replace: [] };
|
||||
@@ -772,7 +774,11 @@ export class MembershipManager
|
||||
* which is not compatible with membershipID of session type member events. They have to be `${localUserId}:${localDeviceId}`
|
||||
*/
|
||||
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
|
||||
const stateKey = `${localUserId}_${localDeviceId}_${this.slotDescription.application}${this.slotDescription.id}`;
|
||||
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
|
||||
// Revert back to "" just for the state key (state keys are always legacy. we use sticky events for non legacy events)
|
||||
const application = this.slotDescription.application;
|
||||
const slotId = this.slotDescription.id === "ROOM" ? "" : this.slotDescription.id;
|
||||
const stateKey = `${localUserId}_${localDeviceId}_${application}${slotId}`;
|
||||
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
|
||||
return stateKey;
|
||||
} else {
|
||||
@@ -798,7 +804,9 @@ export class MembershipManager
|
||||
};
|
||||
return {
|
||||
"application": this.slotDescription.application,
|
||||
"call_id": this.slotDescription.id,
|
||||
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
|
||||
// Revert back to "" just for the sending the event.
|
||||
"call_id": this.slotDescription.id === "ROOM" ? "" : this.slotDescription.id,
|
||||
"scope": "m.room",
|
||||
"device_id": this.deviceId,
|
||||
// DO NOT use this.memberId here since that is the state key (using application...)
|
||||
@@ -1091,6 +1099,7 @@ export class StickyEventMembershipManager extends MembershipManager {
|
||||
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
|
||||
const ownMembership = this.ownMembership;
|
||||
|
||||
const livekitTransport = isLivekitTransportConfig(this.rtcTransport) ? this.rtcTransport : undefined;
|
||||
const relationObject = ownMembership?.eventId
|
||||
? { "m.relation": { rel_type: RelationType.Reference, event_id: ownMembership?.eventId } }
|
||||
: {};
|
||||
@@ -1100,7 +1109,11 @@ export class StickyEventMembershipManager extends MembershipManager {
|
||||
...(this.callIntent ? { "m.call.intent": this.callIntent } : {}),
|
||||
},
|
||||
slot_id: slotDescriptionToId(this.slotDescription),
|
||||
rtc_transports: this.rtcTransport ? [this.rtcTransport] : [],
|
||||
// Make sure we do not add the alias to the transport.
|
||||
// It is not needed in matrix2.0. The additional session information will be used to find the right alias on the sfu.
|
||||
rtc_transports: livekitTransport
|
||||
? [{ type: livekitTransport.type, livekit_service_url: livekitTransport.livekit_service_url }]
|
||||
: [],
|
||||
member: { device_id: this.deviceId, user_id: this.userId, id: this.memberId },
|
||||
versions: [],
|
||||
...relationObject,
|
||||
|
||||
@@ -38,7 +38,6 @@ export type BeaconEventHandlerMap = {
|
||||
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
|
||||
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
|
||||
[BeaconEvent.LocationUpdate]: (locationState: BeaconLocationState) => void;
|
||||
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
|
||||
};
|
||||
|
||||
export const isTimestampInDuration = (startTimestamp: number, durationMs: number, timestamp: number): boolean =>
|
||||
|
||||
@@ -1103,6 +1103,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
const inMemoryUpdate = this.loadMembers()
|
||||
.then((result) => {
|
||||
this.currentState.setOutOfBandMembers(result.memberEvents);
|
||||
// recalculate the room name: it may have been based on members, so may have changed
|
||||
this.recalculate();
|
||||
return result.fromServer;
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
+29
-2
@@ -52,7 +52,34 @@ interface OidcRegistrationRequestBody {
|
||||
application_type: "web" | "native";
|
||||
}
|
||||
|
||||
export const DEVICE_CODE_SCOPE = "urn:ietf:params:oauth:grant-type:device_code";
|
||||
/**
|
||||
* The OAuth 2.0 grant types that are defined for Matrix in https://spec.matrix.org/v1.17/client-server-api/#grant-types
|
||||
*/
|
||||
export enum OAuthGrantType {
|
||||
/**
|
||||
* See https://spec.matrix.org/v1.17/client-server-api/#authorization-code-grant
|
||||
*/
|
||||
AuthorizationCode = "authorization_code",
|
||||
/**
|
||||
* https://spec.matrix.org/v1.17/client-server-api/#refresh-token-grant
|
||||
*/
|
||||
RefreshToken = "refresh_token",
|
||||
/**
|
||||
* The OAuth 2.0 Device Authorization Grant type identifier as per
|
||||
* https://www.rfc-editor.org/rfc/rfc8628.html#section-7.2 from
|
||||
* [MSC4341](https://github.com/matrix-org/matrix-spec-proposals/pull/4341).
|
||||
*
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
DeviceAuthorization = "urn:ietf:params:oauth:grant-type:device_code",
|
||||
}
|
||||
|
||||
/**
|
||||
* The name "scope" is a misnomer here as it is actually a "grant type".
|
||||
*
|
||||
* @deprecated use `OAuthGrantType.DeviceAuthorization` instead
|
||||
*/
|
||||
export const DEVICE_CODE_SCOPE: string = OAuthGrantType.DeviceAuthorization;
|
||||
|
||||
// Check that URIs have a common base, as per the MSC2966 definition
|
||||
const urlHasCommonBase = (base: URL, urlStr?: string): boolean => {
|
||||
@@ -79,7 +106,7 @@ export const registerOidcClient = async (
|
||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||
}
|
||||
|
||||
const grantTypes: NonEmptyArray<string> = ["authorization_code", "refresh_token"];
|
||||
const grantTypes: NonEmptyArray<string> = [OAuthGrantType.AuthorizationCode, OAuthGrantType.RefreshToken];
|
||||
if (grantTypes.some((scope) => !delegatedAuthConfig.grant_types_supported.includes(scope))) {
|
||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||
}
|
||||
|
||||
+14
-8
@@ -19,13 +19,17 @@ import { type IdTokenClaims, type OidcMetadata, type SigninResponse } from "oidc
|
||||
|
||||
import { logger } from "../logger.ts";
|
||||
import { OidcError } from "./error.ts";
|
||||
import { OAuthGrantType } from "./index.ts";
|
||||
|
||||
/**
|
||||
* Metadata from OIDC authority discovery
|
||||
* Metadata from OAuth 2.0 client authentication API as per
|
||||
* https://spec.matrix.org/v1.17/client-server-api/#get_matrixclientv1auth_metadata
|
||||
* With validated properties required in type
|
||||
*/
|
||||
export type ValidatedAuthMetadata = Partial<OidcMetadata> &
|
||||
Pick<
|
||||
// These values are from [RFC8414](https://datatracker.ietf.org/doc/html/rfc8414#section-2)
|
||||
// so we can reuse the OidcMetadata definitions from oidc-client-ts
|
||||
OidcMetadata,
|
||||
| "issuer"
|
||||
| "authorization_endpoint"
|
||||
@@ -35,12 +39,14 @@ export type ValidatedAuthMetadata = Partial<OidcMetadata> &
|
||||
| "grant_types_supported"
|
||||
| "code_challenge_methods_supported"
|
||||
> & {
|
||||
// MSC2965 extensions to the OIDC spec
|
||||
// These values aren't part of RFC8414 so we add them here
|
||||
// Account management fields from stable MSC4191:
|
||||
account_management_uri?: string;
|
||||
account_management_actions_supported?: string[];
|
||||
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
|
||||
// even though it is part of the OIDC spec
|
||||
// Value from [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html):
|
||||
prompt_values_supported?: string[];
|
||||
// Experimental MSC4341 value from [RFC8628](https://datatracker.ietf.org/doc/html/rfc8628#section-4):
|
||||
device_authorization_endpoint?: string;
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
@@ -79,9 +85,9 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates issuer `.well-known/openid-configuration`
|
||||
* As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
* validates that OP is compatible with Element's OIDC flow
|
||||
* Validates OAuth 2.0 auth metadata as defined by
|
||||
* https://spec.matrix.org/v1.17/client-server-api/#get_matrixclientv1auth_metadata
|
||||
* is compatible with Element's OAuth/OIDC flow
|
||||
* @param authMetadata - json object
|
||||
* @returns valid issuer config
|
||||
* @throws Error - when issuer config is not found or is invalid
|
||||
@@ -102,7 +108,7 @@ export const validateAuthMetadata = (authMetadata: unknown): ValidatedAuthMetada
|
||||
optionalStringProperty(authMetadata, "device_authorization_endpoint"),
|
||||
optionalStringArrayProperty(authMetadata, "account_management_actions_supported"),
|
||||
requiredArrayValue(authMetadata, "response_types_supported", "code"),
|
||||
requiredArrayValue(authMetadata, "grant_types_supported", "authorization_code"),
|
||||
requiredArrayValue(authMetadata, "grant_types_supported", OAuthGrantType.AuthorizationCode),
|
||||
requiredArrayValue(authMetadata, "code_challenge_methods_supported", "S256"),
|
||||
optionalStringArrayProperty(authMetadata, "prompt_values_supported"),
|
||||
].some((isValid) => !isValid);
|
||||
|
||||
@@ -27,7 +27,7 @@ import { logger } from "../logger.ts";
|
||||
import { type MSC4108SecureChannel } from "./channels/MSC4108SecureChannel.ts";
|
||||
import { MatrixError } from "../http-api/index.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import { DEVICE_CODE_SCOPE, type OidcClientConfig } from "../oidc/index.ts";
|
||||
import { OAuthGrantType, type OidcClientConfig } from "../oidc/index.ts";
|
||||
import { type CryptoApi } from "../crypto-api/index.ts";
|
||||
|
||||
/**
|
||||
@@ -194,7 +194,7 @@ export class MSC4108SignInWithQR {
|
||||
logger.error("Failed to discover OIDC metadata", e);
|
||||
}
|
||||
|
||||
if (oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
|
||||
if (oidcClientConfig?.grant_types_supported.includes(OAuthGrantType.DeviceAuthorization)) {
|
||||
await this.send<ProtocolsPayload>({
|
||||
type: PayloadType.Protocols,
|
||||
protocols: ["device_authorization_grant"],
|
||||
|
||||
+285
-271
@@ -26,7 +26,7 @@ limitations under the License.
|
||||
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts";
|
||||
import { User } from "./models/user.ts";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./models/room.ts";
|
||||
import { deepCopy, noUnsafeEventProps, promiseMapSeries, unsafeProp } from "./utils.ts";
|
||||
import { deepCopy, noUnsafeEventProps, unsafeProp } from "./utils.ts";
|
||||
import { Filter } from "./filter.ts";
|
||||
import { EventTimeline } from "./models/event-timeline.ts";
|
||||
import { type Logger } from "./logger.ts";
|
||||
@@ -1188,312 +1188,326 @@ export class SyncApi {
|
||||
this.notifEvents = [];
|
||||
|
||||
// Handle invites
|
||||
await promiseMapSeries(inviteRooms, async (inviteObj) => {
|
||||
const room = inviteObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||
await Promise.all(
|
||||
inviteRooms.map(async (inviteObj) => {
|
||||
const room = inviteObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||
|
||||
await this.injectRoomEvents(room, stateEvents, undefined);
|
||||
await this.injectRoomEvents(room, stateEvents, undefined);
|
||||
|
||||
if (inviteObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for invite->reject->invite cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
});
|
||||
if (inviteObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for invite->reject->invite cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Handle joins
|
||||
await promiseMapSeries(joinRooms, async (joinObj) => {
|
||||
const room = joinObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
|
||||
const stateAfterEvents = this.mapSyncEventsFormat(joinObj["org.matrix.msc4222.state_after"], room);
|
||||
// Prevent events from being decrypted ahead of time
|
||||
// this helps large account to speed up faster
|
||||
// room::decryptCriticalEvent is in charge of decrypting all the events
|
||||
// required for a client to function properly
|
||||
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
|
||||
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
|
||||
const stickyEvents = this.mapSyncEventsFormat(joinObj.msc4354_sticky);
|
||||
await Promise.all(
|
||||
joinRooms.map(async (joinObj) => {
|
||||
const room = joinObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
|
||||
const stateAfterEvents = this.mapSyncEventsFormat(joinObj["org.matrix.msc4222.state_after"], room);
|
||||
// Prevent events from being decrypted ahead of time
|
||||
// this helps large account to speed up faster
|
||||
// room::decryptCriticalEvent is in charge of decrypting all the events
|
||||
// required for a client to function properly
|
||||
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
|
||||
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
|
||||
const stickyEvents = this.mapSyncEventsFormat(joinObj.msc4354_sticky);
|
||||
|
||||
// If state_after is present, this is the events that form the state at the end of the timeline block and
|
||||
// regular timeline events do *not* count towards state. If it's not present, then the state is formed by
|
||||
// the state events plus the timeline events. Note mapSyncEventsFormat returns an empty array if the field
|
||||
// is absent so we explicitly check the field on the original object.
|
||||
const eventsFormingFinalState = joinObj["org.matrix.msc4222.state_after"]
|
||||
? stateAfterEvents
|
||||
: stateEvents.concat(timelineEvents);
|
||||
// If state_after is present, this is the events that form the state at the end of the timeline block and
|
||||
// regular timeline events do *not* count towards state. If it's not present, then the state is formed by
|
||||
// the state events plus the timeline events. Note mapSyncEventsFormat returns an empty array if the field
|
||||
// is absent so we explicitly check the field on the original object.
|
||||
const eventsFormingFinalState = joinObj["org.matrix.msc4222.state_after"]
|
||||
? stateAfterEvents
|
||||
: stateEvents.concat(timelineEvents);
|
||||
|
||||
const encrypted = this.isRoomEncrypted(room, eventsFormingFinalState);
|
||||
// We store the server-provided value first so it's correct when any of the events fire.
|
||||
if (joinObj.unread_notifications) {
|
||||
/**
|
||||
* We track unread notifications ourselves in encrypted rooms, so don't
|
||||
* bother setting it here. We trust our calculations better than the
|
||||
* server's for this case, and therefore will assume that our non-zero
|
||||
* count is accurate.
|
||||
* XXX: this is known faulty as the push rule for `.m.room.encrypted` may be disabled so server
|
||||
* may issue notification counts of 0 which we wrongly trust.
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/2654 would fix this
|
||||
*
|
||||
* @see import("./client").fixNotificationCountOnDecryption
|
||||
*/
|
||||
if (!encrypted || joinObj.unread_notifications.notification_count === 0) {
|
||||
// In an encrypted room, if the room has notifications enabled then it's typical for
|
||||
// the server to flag all new messages as notifying. However, some push rules calculate
|
||||
// events as ignored based on their event contents (e.g. ignoring msgtype=m.notice messages)
|
||||
// so we want to calculate this figure on the client in all cases.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Total,
|
||||
joinObj.unread_notifications.notification_count ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
|
||||
// If the locally stored highlight count is zero, use the server provided value.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Highlight,
|
||||
joinObj.unread_notifications.highlight_count ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const unreadThreadNotifications =
|
||||
joinObj[UNREAD_THREAD_NOTIFICATIONS.name] ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!];
|
||||
if (unreadThreadNotifications) {
|
||||
// This mirrors the logic above for rooms: take the *total* notification count from
|
||||
// the server for unencrypted rooms or is it's zero. Any threads not present in this
|
||||
// object implicitly have zero notifications, so start by clearing the total counts
|
||||
// for all such threads.
|
||||
room.resetThreadUnreadNotificationCountFromSync(Object.keys(unreadThreadNotifications));
|
||||
for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) {
|
||||
if (!encrypted || unreadNotification.notification_count === 0) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
const encrypted = this.isRoomEncrypted(room, eventsFormingFinalState);
|
||||
// We store the server-provided value first so it's correct when any of the events fire.
|
||||
if (joinObj.unread_notifications) {
|
||||
/**
|
||||
* We track unread notifications ourselves in encrypted rooms, so don't
|
||||
* bother setting it here. We trust our calculations better than the
|
||||
* server's for this case, and therefore will assume that our non-zero
|
||||
* count is accurate.
|
||||
* XXX: this is known faulty as the push rule for `.m.room.encrypted` may be disabled so server
|
||||
* may issue notification counts of 0 which we wrongly trust.
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/2654 would fix this
|
||||
*
|
||||
* @see import("./client").fixNotificationCountOnDecryption
|
||||
*/
|
||||
if (!encrypted || joinObj.unread_notifications.notification_count === 0) {
|
||||
// In an encrypted room, if the room has notifications enabled then it's typical for
|
||||
// the server to flag all new messages as notifying. However, some push rules calculate
|
||||
// events as ignored based on their event contents (e.g. ignoring msgtype=m.notice messages)
|
||||
// so we want to calculate this figure on the client in all cases.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Total,
|
||||
unreadNotification.notification_count ?? 0,
|
||||
joinObj.unread_notifications.notification_count ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
const hasNoNotifications =
|
||||
room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0;
|
||||
if (!encrypted || (encrypted && hasNoNotifications)) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
|
||||
// If the locally stored highlight count is zero, use the server provided value.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Highlight,
|
||||
unreadNotification.highlight_count ?? 0,
|
||||
joinObj.unread_notifications.highlight_count ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
room.resetThreadUnreadNotificationCountFromSync();
|
||||
}
|
||||
|
||||
joinObj.timeline = joinObj.timeline || ({} as ITimeline);
|
||||
const unreadThreadNotifications =
|
||||
joinObj[UNREAD_THREAD_NOTIFICATIONS.name] ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!];
|
||||
if (unreadThreadNotifications) {
|
||||
// This mirrors the logic above for rooms: take the *total* notification count from
|
||||
// the server for unencrypted rooms or is it's zero. Any threads not present in this
|
||||
// object implicitly have zero notifications, so start by clearing the total counts
|
||||
// for all such threads.
|
||||
room.resetThreadUnreadNotificationCountFromSync(Object.keys(unreadThreadNotifications));
|
||||
for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) {
|
||||
if (!encrypted || unreadNotification.notification_count === 0) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
NotificationCountType.Total,
|
||||
unreadNotification.notification_count ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
// set the back-pagination token. Do this *before* adding any
|
||||
// events so that clients can start back-paginating.
|
||||
if (joinObj.timeline.prev_batch !== null) {
|
||||
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||
}
|
||||
} else if (joinObj.timeline.limited) {
|
||||
let limited = true;
|
||||
|
||||
// we've got a limited sync, so we *probably* have a gap in the
|
||||
// timeline, so should reset. But we might have been peeking or
|
||||
// paginating and already have some of the events, in which
|
||||
// case we just want to append any subsequent events to the end
|
||||
// of the existing timeline.
|
||||
//
|
||||
// This is particularly important in the case that we already have
|
||||
// *all* of the events in the timeline - in that case, if we reset
|
||||
// the timeline, we'll end up with an entirely empty timeline,
|
||||
// which we'll try to paginate but not get any new events (which
|
||||
// will stop us linking the empty timeline into the chain).
|
||||
//
|
||||
for (let i = timelineEvents.length - 1; i >= 0; i--) {
|
||||
const eventId = timelineEvents[i].getId()!;
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
this.syncOpts.logger.debug(`Already have event ${eventId} in limited sync - not resetting`);
|
||||
limited = false;
|
||||
|
||||
// we might still be missing some of the events before i;
|
||||
// we don't want to be adding them to the end of the
|
||||
// timeline because that would put them out of order.
|
||||
timelineEvents.splice(0, i);
|
||||
|
||||
// XXX: there's a problem here if the skipped part of the
|
||||
// timeline modifies the state set in stateEvents, because
|
||||
// we'll end up using the state from stateEvents rather
|
||||
// than the later state from timelineEvents. We probably
|
||||
// need to wind stateEvents forward over the events we're
|
||||
// skipping.
|
||||
|
||||
break;
|
||||
const hasNoNotifications =
|
||||
room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0;
|
||||
if (!encrypted || (encrypted && hasNoNotifications)) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
NotificationCountType.Highlight,
|
||||
unreadNotification.highlight_count ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (limited) {
|
||||
room.resetLiveTimeline(
|
||||
joinObj.timeline.prev_batch,
|
||||
this.syncOpts.canResetEntireTimeline!(room.roomId)
|
||||
? null
|
||||
: (syncEventData.oldSyncToken ?? null),
|
||||
);
|
||||
|
||||
// We have to assume any gap in any timeline is
|
||||
// reason to stop incrementally tracking notifications and
|
||||
// reset the timeline.
|
||||
client.resetNotifTimelineSet();
|
||||
}
|
||||
}
|
||||
|
||||
// process any crypto events *before* emitting the RoomStateEvent events. This
|
||||
// avoids a race condition if the application tries to send a message after the
|
||||
// state event is processed, but before crypto is enabled, which then causes the
|
||||
// crypto layer to complain.
|
||||
|
||||
if (this.syncOpts.cryptoCallbacks) {
|
||||
for (const e of eventsFormingFinalState) {
|
||||
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
|
||||
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proactively decrypt state events: normally we decrypt on demand, but for state
|
||||
// events we need them immediately, so we handle them here. Specifically, consumers
|
||||
// (e.g. Element Web) expect state events to be unencrypted upon receipt.
|
||||
for (const ev of timelineEvents.filter((ev) => ev.isState())) {
|
||||
await this.client.decryptEventIfNeeded(ev);
|
||||
}
|
||||
|
||||
try {
|
||||
if ("org.matrix.msc4222.state_after" in joinObj) {
|
||||
await this.injectRoomEvents(
|
||||
room,
|
||||
undefined,
|
||||
stateAfterEvents,
|
||||
timelineEvents,
|
||||
syncEventData.fromCache,
|
||||
);
|
||||
} else {
|
||||
await this.injectRoomEvents(room, stateEvents, undefined, timelineEvents, syncEventData.fromCache);
|
||||
room.resetThreadUnreadNotificationCountFromSync();
|
||||
}
|
||||
} catch (e) {
|
||||
this.syncOpts.logger.error(`Failed to process events on room ${room.roomId}:`, e);
|
||||
}
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
// which needs the room state to be up to date
|
||||
if (joinObj.summary) {
|
||||
room.setSummary(joinObj.summary);
|
||||
}
|
||||
joinObj.timeline = joinObj.timeline || ({} as ITimeline);
|
||||
|
||||
// we deliberately don't add ephemeral events to the timeline
|
||||
room.addEphemeralEvents(ephemeralEvents);
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
// set the back-pagination token. Do this *before* adding any
|
||||
// events so that clients can start back-paginating.
|
||||
if (joinObj.timeline.prev_batch !== null) {
|
||||
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||
}
|
||||
} else if (joinObj.timeline.limited) {
|
||||
let limited = true;
|
||||
|
||||
// we deliberately don't add accountData to the timeline
|
||||
room.addAccountData(accountDataEvents);
|
||||
// we've got a limited sync, so we *probably* have a gap in the
|
||||
// timeline, so should reset. But we might have been peeking or
|
||||
// paginating and already have some of the events, in which
|
||||
// case we just want to append any subsequent events to the end
|
||||
// of the existing timeline.
|
||||
//
|
||||
// This is particularly important in the case that we already have
|
||||
// *all* of the events in the timeline - in that case, if we reset
|
||||
// the timeline, we'll end up with an entirely empty timeline,
|
||||
// which we'll try to paginate but not get any new events (which
|
||||
// will stop us linking the empty timeline into the chain).
|
||||
//
|
||||
for (let i = timelineEvents.length - 1; i >= 0; i--) {
|
||||
const eventId = timelineEvents[i].getId()!;
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
this.syncOpts.logger.debug(`Already have event ${eventId} in limited sync - not resetting`);
|
||||
limited = false;
|
||||
|
||||
// Sticky events primarily come via the `timeline` field, with the
|
||||
// sticky info field marking them as sticky.
|
||||
// If the sync is "gappy" (meaning it is skipping events to catch up) then
|
||||
// sticky events will instead come down the sticky section.
|
||||
// This ensures we collect sticky events from both places.
|
||||
const stickyEventsAndStickyEventsFromTheTimeline = stickyEvents.concat(
|
||||
timelineEvents.filter((e) => e.unstableStickyInfo !== undefined),
|
||||
);
|
||||
// Note: We calculate sticky events before emitting `.Room` as it's nice to have
|
||||
// sticky events calculated and ready to go.
|
||||
room._unstable_addStickyEvents(stickyEventsAndStickyEventsFromTheTimeline);
|
||||
// we might still be missing some of the events before i;
|
||||
// we don't want to be adding them to the end of the
|
||||
// timeline because that would put them out of order.
|
||||
timelineEvents.splice(0, i);
|
||||
|
||||
room.recalculate();
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
// XXX: there's a problem here if the skipped part of the
|
||||
// timeline modifies the state set in stateEvents, because
|
||||
// we'll end up using the state from stateEvents rather
|
||||
// than the later state from timelineEvents. We probably
|
||||
// need to wind stateEvents forward over the events we're
|
||||
// skipping.
|
||||
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e);
|
||||
// this fires a couple of times for some events. (eg state events are in the timeline and the state)
|
||||
// should this get a sync section as an additional event emission param (e, syncSection))?
|
||||
stateEvents.forEach(emitEvent);
|
||||
timelineEvents.forEach(emitEvent);
|
||||
ephemeralEvents.forEach(emitEvent);
|
||||
accountDataEvents.forEach(emitEvent);
|
||||
stickyEvents
|
||||
.filter(
|
||||
(stickyEvent) =>
|
||||
// This is highly unlikey, but in the case where a sticky event
|
||||
// has appeared in the timeline AND the sticky section, we only
|
||||
// want to emit the event once.
|
||||
!timelineEvents.some((timelineEvent) => timelineEvent.getId() === stickyEvent.getId()),
|
||||
)
|
||||
.forEach(emitEvent);
|
||||
// Decrypt only the last message in all rooms to make sure we can generate a preview
|
||||
// And decrypt all events after the recorded read receipt to ensure an accurate
|
||||
// notification count
|
||||
room.decryptCriticalEvents();
|
||||
});
|
||||
if (limited) {
|
||||
room.resetLiveTimeline(
|
||||
joinObj.timeline.prev_batch,
|
||||
this.syncOpts.canResetEntireTimeline!(room.roomId)
|
||||
? null
|
||||
: (syncEventData.oldSyncToken ?? null),
|
||||
);
|
||||
|
||||
// We have to assume any gap in any timeline is
|
||||
// reason to stop incrementally tracking notifications and
|
||||
// reset the timeline.
|
||||
client.resetNotifTimelineSet();
|
||||
}
|
||||
}
|
||||
|
||||
// process any crypto events *before* emitting the RoomStateEvent events. This
|
||||
// avoids a race condition if the application tries to send a message after the
|
||||
// state event is processed, but before crypto is enabled, which then causes the
|
||||
// crypto layer to complain.
|
||||
|
||||
if (this.syncOpts.cryptoCallbacks) {
|
||||
for (const e of eventsFormingFinalState) {
|
||||
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
|
||||
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proactively decrypt state events: normally we decrypt on demand, but for state
|
||||
// events we need them immediately, so we handle them here. Specifically, consumers
|
||||
// (e.g. Element Web) expect state events to be unencrypted upon receipt.
|
||||
for (const ev of timelineEvents.filter((ev) => ev.isState())) {
|
||||
await this.client.decryptEventIfNeeded(ev);
|
||||
}
|
||||
|
||||
try {
|
||||
if ("org.matrix.msc4222.state_after" in joinObj) {
|
||||
await this.injectRoomEvents(
|
||||
room,
|
||||
undefined,
|
||||
stateAfterEvents,
|
||||
timelineEvents,
|
||||
syncEventData.fromCache,
|
||||
);
|
||||
} else {
|
||||
await this.injectRoomEvents(
|
||||
room,
|
||||
stateEvents,
|
||||
undefined,
|
||||
timelineEvents,
|
||||
syncEventData.fromCache,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.syncOpts.logger.error(`Failed to process events on room ${room.roomId}:`, e);
|
||||
}
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
// which needs the room state to be up to date
|
||||
if (joinObj.summary) {
|
||||
room.setSummary(joinObj.summary);
|
||||
}
|
||||
|
||||
// we deliberately don't add ephemeral events to the timeline
|
||||
room.addEphemeralEvents(ephemeralEvents);
|
||||
|
||||
// we deliberately don't add accountData to the timeline
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
// Sticky events primarily come via the `timeline` field, with the
|
||||
// sticky info field marking them as sticky.
|
||||
// If the sync is "gappy" (meaning it is skipping events to catch up) then
|
||||
// sticky events will instead come down the sticky section.
|
||||
// This ensures we collect sticky events from both places.
|
||||
const stickyEventsAndStickyEventsFromTheTimeline = stickyEvents.concat(
|
||||
timelineEvents.filter((e) => e.unstableStickyInfo !== undefined),
|
||||
);
|
||||
// Note: We calculate sticky events before emitting `.Room` as it's nice to have
|
||||
// sticky events calculated and ready to go.
|
||||
room._unstable_addStickyEvents(stickyEventsAndStickyEventsFromTheTimeline);
|
||||
|
||||
room.recalculate();
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
|
||||
const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e);
|
||||
// this fires a couple of times for some events. (eg state events are in the timeline and the state)
|
||||
// should this get a sync section as an additional event emission param (e, syncSection))?
|
||||
stateEvents.forEach(emitEvent);
|
||||
timelineEvents.forEach(emitEvent);
|
||||
ephemeralEvents.forEach(emitEvent);
|
||||
accountDataEvents.forEach(emitEvent);
|
||||
stickyEvents
|
||||
.filter(
|
||||
(stickyEvent) =>
|
||||
// This is highly unlikey, but in the case where a sticky event
|
||||
// has appeared in the timeline AND the sticky section, we only
|
||||
// want to emit the event once.
|
||||
!timelineEvents.some((timelineEvent) => timelineEvent.getId() === stickyEvent.getId()),
|
||||
)
|
||||
.forEach(emitEvent);
|
||||
// Decrypt only the last message in all rooms to make sure we can generate a preview
|
||||
// And decrypt all events after the recorded read receipt to ensure an accurate
|
||||
// notification count
|
||||
room.decryptCriticalEvents();
|
||||
}),
|
||||
);
|
||||
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
await promiseMapSeries(leaveRooms, async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
const { timelineEvents, stateEvents, stateAfterEvents } = await this.mapAndInjectRoomEvents(leaveObj);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
|
||||
await Promise.all(
|
||||
leaveRooms.map(async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
const { timelineEvents, stateEvents, stateAfterEvents } = await this.mapAndInjectRoomEvents(leaveObj);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
|
||||
|
||||
room.addAccountData(accountDataEvents);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
room.recalculate();
|
||||
if (leaveObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
room.recalculate();
|
||||
if (leaveObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
|
||||
stateEvents?.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
stateAfterEvents?.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
timelineEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
accountDataEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
});
|
||||
stateEvents?.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
stateAfterEvents?.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
timelineEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
accountDataEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Handle knocks
|
||||
await promiseMapSeries(knockRooms, async (knockObj) => {
|
||||
const room = knockObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room);
|
||||
await Promise.all(
|
||||
knockRooms.map(async (knockObj) => {
|
||||
const room = knockObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room);
|
||||
|
||||
await this.injectRoomEvents(room, stateEvents, undefined);
|
||||
await this.injectRoomEvents(room, stateEvents, undefined);
|
||||
|
||||
if (knockObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for knock->leave->knock cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
});
|
||||
if (knockObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for knock->leave->knock cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// update the notification timeline, if appropriate.
|
||||
// we only do this for live events, as otherwise we can't order them sanely
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ import { type RoomMember } from "./models/room-member.ts";
|
||||
import { EventType } from "./@types/event.ts";
|
||||
import { type DecryptionFailureCode } from "./crypto-api/index.ts";
|
||||
import { DecryptionError, type EventDecryptionResult } from "./common-crypto/CryptoBackend.ts";
|
||||
import { type OidcClientConfig, type ValidatedAuthMetadata } from "./oidc/index.ts";
|
||||
import { OAuthGrantType, type OidcClientConfig, type ValidatedAuthMetadata } from "./oidc/index.ts";
|
||||
|
||||
/**
|
||||
* Create a {@link MatrixEvent}.
|
||||
@@ -226,6 +226,6 @@ export const mockOpenIdConfiguration = (
|
||||
device_authorization_endpoint: issuer + "device",
|
||||
jwks_uri: issuer + "jwks",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
|
||||
grant_types_supported: [OAuthGrantType.AuthorizationCode, OAuthGrantType.RefreshToken, ...additionalGrantTypes],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"strict": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"lib": ["es2024", "dom", "dom.iterable"],
|
||||
"baseUrl": ".",
|
||||
"types": ["vitest/globals", "node"],
|
||||
"paths": {
|
||||
"#types/*": ["./node_modules/vite/types/*"]
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/en-us/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["typedoc/tsdoc.json"],
|
||||
"noStandardTags": false,
|
||||
"tagDefinitions": [
|
||||
{
|
||||
"tagName": "@alias",
|
||||
"syntaxKind": "modifier"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user