Compare commits

...

18 Commits

Author SHA1 Message Date
RiotRobot 921de6807d v37.9.0-rc.0 2025-06-10 13:16:01 +00:00
RiotRobot d4e7b949e7 Merge branch 'master' into develop 2025-06-10 12:00:47 +00:00
RiotRobot 40bc833bb7 v37.8.0 2025-06-10 12:00:13 +00:00
ElementRobot a46ad75440 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v14.2.1 (#4868) (#4869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 12:52:35 +01:00
renovate[bot] 1e80538cfb Update dependency @matrix-org/matrix-sdk-crypto-wasm to v14.2.1 (#4868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 11:30:04 +00:00
Michael Telatynski 73cbcfa4ee Ensure we send spec-compliant filter strings by stripping out null values (#4865)
* Ensure we send spec-compliant filter strings by stripping out null values

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

* Add test

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-06-09 08:32:42 +00:00
Richard van der Hoff 99972ce0a9 Factor out common code for processing to-device events (#4863)
`sliding-sync-sdk.ts` and `sync.ts` both have copies of this code, and it's
redundant.
2025-06-05 08:57:50 +00:00
Timo 44399f6017 Fix MatrixRTC membership manager failing to rejoin in a race condition (sync vs not found response) (#4861)
* add test run helper to allow running long tests in vs code

* deprecate IDeferred (as its associated defer method is also deprecated and its just a type rename to PromiseWithResolvers)

* Improve docs and readability of MembershipManager.spec.ts

* Intoduce test for a race condition which results in a state where the state event and the hasMemberStateEvent variable diverge

* fix room state and membership manager state diverging. See:
https://github.com/element-hq/element-call-rageshakes/issues/10609
https://github.com/element-hq/element-call-rageshakes/issues/10594
https://github.com/element-hq/element-call-rageshakes/issues/9902

* logging, docstings and variable name improvements

* review

* review pending timers
2025-06-04 10:44:12 +00:00
RiotRobot c387f30e5c Merge branch 'master' into develop 2025-06-03 14:50:36 +00:00
rsb-tbg 12a9875c46 Include extraParams in all HTTP requests (#4860)
* attaching queryParams from client config in getUrl

Signed-off-by: rsb-tbg <69879226+rsb-tbg@users.noreply.github.com>

* changed client queryParams to QueryDict for consistency and now merging both sets of params in getUrl if one or both exist

Signed-off-by: rsb-tbg <69879226+rsb-tbg@users.noreply.github.com>

* added tests

Signed-off-by: rsb-tbg <69879226+rsb-tbg@users.noreply.github.com>

---------

Signed-off-by: rsb-tbg <69879226+rsb-tbg@users.noreply.github.com>
2025-05-30 09:09:21 +00:00
renovate[bot] 74f5efc4ef Update all non-major dependencies (#4859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-29 08:01:03 +00:00
renovate[bot] 43d47982ed Update dependency @types/node to v18.19.105 (#4858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-29 07:47:37 +00:00
renovate[bot] 94fb489952 Update dependency @stylistic/eslint-plugin to v4.4.0 (#4855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 15:36:54 +00:00
renovate[bot] 75ae05e5eb Update typescript-eslint monorepo to v8.33.0 (#4856)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:45:13 +00:00
renovate[bot] 9058b79c39 Update all non-major dependencies (#4854)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:44:33 +00:00
renovate[bot] 671dd2ca40 Update dependency typedoc to v0.28.5 (#4852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:44:13 +00:00
renovate[bot] 34f35393ff Update dependency @types/node to v18.19.103 (#4851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:43:50 +00:00
renovate[bot] e206a12902 Update babel monorepo to v7.27.3 (#4850)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:43:26 +00:00
17 changed files with 798 additions and 493 deletions
+7
View File
@@ -1,3 +1,10 @@
Changes in [37.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.8.0) (2025-06-10)
==================================================================================================
## 🐛 Bug Fixes
* [Backport staging] Update dependency @matrix-org/matrix-sdk-crypto-wasm to v14.2.1 ([#4869](https://github.com/matrix-org/matrix-js-sdk/pull/4869)). Contributed by @RiotRobot.
Changes in [37.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.7.0) (2025-06-03)
==================================================================================================
## 🦖 Deprecations
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "37.7.0",
"version": "37.9.0-rc.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=22.0.0"
+105
View File
@@ -205,4 +205,109 @@ describe("MatrixClient opts", function () {
expect(res.event_id).toEqual("foo");
});
});
describe("with opts.queryParams", function () {
let client: MatrixClient;
let httpBackend: HttpBackend;
const userId = "@rsb-tbg:localhost";
beforeEach(function () {
httpBackend = new HttpBackend();
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store: new MemoryStore() as IStore,
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
queryParams: { user_id: userId },
});
});
afterEach(function () {
client.stopClient();
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
it("should include queryParams in matrix server requests", async () => {
const eventId = "$test:event";
httpBackend
.when("PUT", "/txn1")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, {
event_id: eventId,
});
const [res] = await Promise.all([
client.sendTextMessage("!foo:bar", "test message", "txn1"),
httpBackend.flush("/txn1", 1),
]);
expect(res.event_id).toEqual(eventId);
});
it("should include queryParams in sync requests", async () => {
httpBackend
.when("GET", "/versions")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, {});
httpBackend
.when("GET", "/pushrules")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, {});
httpBackend
.when("POST", "/filter")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, { filter_id: "foo" });
httpBackend
.when("GET", "/sync")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, syncData);
client.startClient();
await httpBackend.flush("/versions", 1);
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([httpBackend.flush("/sync", 1), utils.syncPromise(client)]);
});
it("should merge queryParams with request-specific params", async () => {
const eventId = "$test:event";
httpBackend
.when("PUT", "/txn1")
.check((req) => {
// Should contain both global queryParams and request-specific params
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, {
event_id: eventId,
});
const [res] = await Promise.all([
client.sendTextMessage("!foo:bar", "test message", "txn1"),
httpBackend.flush("/txn1", 1),
]);
expect(res.event_id).toEqual(eventId);
});
});
});
+5
View File
@@ -19,3 +19,8 @@ jest.mock("../src/http-api/utils", () => ({
// We mock timeoutSignal otherwise it causes tests to leave timers running
timeoutSignal: () => new AbortController().signal,
}));
// Dont make test fail too soon due to timeouts while debugging.
if (process.env.VSCODE_INSPECTOR_OPTIONS) {
jest.setTimeout(60 * 1000 * 5); // 5 minutes
}
+7
View File
@@ -170,4 +170,11 @@ describe("Filter Component", function () {
expect(filter.check(noMatchEvent)).toBe(false);
});
});
describe("toJSON", () => {
it("should omit empty values", () => {
const filter = new FilterComponent({ types: ["m.room.message"], senders: ["@alice:example.com"] });
expect(filter.toJSON()).toEqual({ types: ["m.room.message"], senders: ["@alice:example.com"] });
});
});
});
+77
View File
@@ -521,6 +521,83 @@ describe("FetchHttpApi", () => {
describe("when fetch.opts.baseUrl does have a trailing slash", () => {
runTests(baseUrlWithTrailingSlash);
});
describe("extraParams handling", () => {
const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => {
const fetchFn = jest.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
return new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, extraParams });
};
const userId = "@rsb-tbg:localhost";
const encodedUserId = encodeURIComponent(userId);
it("should include extraParams in URL when no queryParams provided", () => {
const extraParams = { user_id: userId, version: "1.0" };
const api = makeApiWithExtraParams(extraParams);
const result = api.getUrl("/test");
expect(result.toString()).toBe(`${localBaseUrl}${prefix}/test?user_id=${encodedUserId}&version=1.0`);
});
it("should merge extraParams with queryParams", () => {
const extraParams = { user_id: userId, version: "1.0" };
const api = makeApiWithExtraParams(extraParams);
const queryParams = { userId: "123", filter: "active" };
const result = api.getUrl("/test", queryParams);
expect(result.searchParams.get("user_id")!).toBe(userId);
expect(result.searchParams.get("version")!).toBe("1.0");
expect(result.searchParams.get("userId")!).toBe("123");
expect(result.searchParams.get("filter")!).toBe("active");
});
it("should allow queryParams to override extraParams", () => {
const extraParams = { user_id: "@default:localhost", version: "1.0" };
const api = makeApiWithExtraParams(extraParams);
const queryParams = { user_id: "@override:localhost", userId: "123" };
const result = api.getUrl("/test", queryParams);
expect(result.searchParams.get("user_id")).toBe("@override:localhost");
expect(result.searchParams.get("version")!).toBe("1.0");
expect(result.searchParams.get("userId")!).toBe("123");
});
it("should handle empty extraParams", () => {
const extraParams = {};
const api = makeApiWithExtraParams(extraParams);
const queryParams = { userId: "123" };
const result = api.getUrl("/test", queryParams);
expect(result.searchParams.get("userId")!).toBe("123");
expect(result.searchParams.has("user_id")).toBe(false);
});
it("should work when extraParams is undefined", () => {
const fetchFn = jest.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn });
const queryParams = { userId: "123" };
const result = api.getUrl("/test", queryParams);
expect(result.searchParams.get("userId")!).toBe("123");
expect(result.toString()).toBe(`${localBaseUrl}${prefix}/test?userId=123`);
});
it("should work when queryParams is undefined", () => {
const extraParams = { user_id: userId, version: "1.0" };
const api = makeApiWithExtraParams(extraParams);
const result = api.getUrl("/test");
expect(result.searchParams.get("user_id")!).toBe(userId);
expect(result.toString()).toBe(`${localBaseUrl}${prefix}/test?user_id=${encodedUserId}&version=1.0`);
});
});
});
it("should not log query parameters", async () => {
+72 -21
View File
@@ -32,25 +32,38 @@ import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, t
import { MembershipManager } from "../../../src/matrixrtc/NewMembershipManager";
import { logger } from "../../../src/logger.ts";
function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>) {
return new Promise<void>((resolve) => {
method.mockImplementation(() => {
resolve();
return returnVal ?? Promise.resolve();
});
});
}
function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<any>) {
return new Promise<void>((resolve) => {
method.mockImplementationOnce(() => {
resolve();
return returnVal ?? Promise.resolve();
});
/**
* Create a promise that will resolve once a mocked method is called.
* @param method The method to wait for.
* @param returnVal Provide an optional value that the mocked method should return. (use Promise.resolve(val) or Promise.reject(err))
* @returns The promise that resolves once the method is called.
*/
function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>): Promise<void> {
const { promise, resolve } = Promise.withResolvers<void>();
method.mockImplementation(() => {
resolve();
return returnVal ?? Promise.resolve();
});
return promise;
}
function createAsyncHandle(method: MockedFunction<any>) {
const { reject, resolve, promise } = Promise.withResolvers<void>();
/** See waitForMockCall */
function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<any>) {
const { promise, resolve } = Promise.withResolvers<void>();
method.mockImplementationOnce(() => {
resolve();
return returnVal ?? Promise.resolve();
});
return promise;
}
/**
* A handle to control when in the test flow the provided method resolves (or gets rejected).
* @param method The method to control the resolve timing.
* @returns
*/
function createAsyncHandle<T>(method: MockedFunction<any>) {
const { reject, resolve, promise } = Promise.withResolvers<T>();
method.mockImplementation(() => promise);
return { reject, resolve };
}
@@ -110,13 +123,13 @@ describe.each([
it("sends a membership event and schedules delayed leave when joining a call", async () => {
// Spys/Mocks
const updateDelayedEventHandle = createAsyncHandle(client._unstable_updateDelayedEvent as Mock);
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
// Test
const memberManager = new TestMembershipManager(undefined, room, client, () => undefined);
memberManager.join([focus], focusActive);
// expects
await waitForMockCall(client.sendStateEvent);
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
"org.matrix.msc3401.call.member",
@@ -311,6 +324,44 @@ describe.each([
});
});
it("rejoins if delayed event is not found (404) !FailsForLegacy", async () => {
const RESTART_DELAY = 15000;
const manager = new TestMembershipManager(
{ delayedLeaveEventRestartMs: RESTART_DELAY },
room,
client,
() => undefined,
);
// Join with the membership manager
manager.join([focus], focusActive);
expect(manager.status).toBe(Status.Connecting);
// Let the scheduler run one iteration so that we can send the join state event
await jest.runOnlyPendingTimersAsync();
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(manager.status).toBe(Status.Connected);
// Now that we are connected, we set up the mocks.
// We enforce the following scenario where we simulate that the delayed event activated and caused the user to leave:
// - We wait until the delayed event gets sent and then mock its response to be "not found."
// - We enforce a race condition between the sync that informs us that our call membership state event was set to "left"
// and the "not found" response from the delayed event: we receive the sync while we are waiting for the delayed event to be sent.
// - While the delayed leave event is being sent, we inform the manager that our membership state event was set to "left."
// (onRTCSessionMemberUpdate)
// - Only then do we resolve the sending of the delayed event.
// - We test that the manager acknowledges the leave and sends a new membership state event.
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValueOnce(
new MatrixError({ errcode: "M_NOT_FOUND" }),
);
const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent);
await jest.advanceTimersByTimeAsync(RESTART_DELAY);
// first simulate the sync, then resolve sending the delayed event.
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
resolve({ delay_id: "id" });
// Let the scheduler run one iteration so that the new join gets sent
await jest.runOnlyPendingTimersAsync();
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
});
it("uses membershipEventExpiryMs from config", async () => {
const manager = new TestMembershipManager(
{ membershipEventExpiryMs: 1234567 },
@@ -542,8 +593,8 @@ describe.each([
expect(manager.status).toBe(Status.Disconnected);
});
it("emits 'Connection' and 'Connected' after join !FailsForLegacy", async () => {
const handleDelayedEvent = createAsyncHandle(client._unstable_sendDelayedStateEvent);
const handleStateEvent = createAsyncHandle(client.sendStateEvent);
const handleDelayedEvent = createAsyncHandle<void>(client._unstable_sendDelayedStateEvent);
const handleStateEvent = createAsyncHandle<void>(client.sendStateEvent);
const manager = new TestMembershipManager({}, room, client, () => undefined);
expect(manager.status).toBe(Status.Disconnected);
@@ -594,7 +645,7 @@ describe.each([
});
// FailsForLegacy as implementation does not re-check membership before retrying.
it("abandons retry loop and sends new own membership if not present anymore !FailsForLegacy", async () => {
(client._unstable_sendDelayedStateEvent as any).mockRejectedValue(
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
429,
+1 -1
View File
@@ -360,7 +360,7 @@ export interface ICreateClientOpts {
* to all requests with this client. Useful for application services which require
* `?user_id=`.
*/
queryParams?: Record<string, string>;
queryParams?: QueryDict;
/**
* Encryption key used for encrypting sensitive data (such as e2ee keys) in {@link ICreateClientOpts#cryptoStore}.
+13 -11
View File
@@ -99,17 +99,19 @@ export class FilterComponent {
* Converts the filter component into the form expected over the wire
*/
public toJSON(): object {
return {
types: this.filterJson.types || null,
not_types: this.filterJson.not_types || [],
rooms: this.filterJson.rooms || null,
not_rooms: this.filterJson.not_rooms || [],
senders: this.filterJson.senders || null,
not_senders: this.filterJson.not_senders || [],
contains_url: this.filterJson.contains_url || null,
[FILTER_RELATED_BY_SENDERS.name]: this.filterJson[FILTER_RELATED_BY_SENDERS.name] || [],
[FILTER_RELATED_BY_REL_TYPES.name]: this.filterJson[FILTER_RELATED_BY_REL_TYPES.name] || [],
};
return Object.fromEntries(
Object.entries({
types: this.filterJson.types,
not_types: this.filterJson.not_types,
rooms: this.filterJson.rooms,
not_rooms: this.filterJson.not_rooms,
senders: this.filterJson.senders,
not_senders: this.filterJson.not_senders,
contains_url: this.filterJson.contains_url,
[FILTER_RELATED_BY_SENDERS.name]: this.filterJson[FILTER_RELATED_BY_SENDERS.name],
[FILTER_RELATED_BY_REL_TYPES.name]: this.filterJson[FILTER_RELATED_BY_REL_TYPES.name],
}).filter(([_key, value]) => value),
);
}
/**
+5 -2
View File
@@ -379,9 +379,12 @@ export class FetchHttpApi<O extends IHttpOpts> {
? baseUrlWithFallback.slice(0, -1)
: baseUrlWithFallback;
const url = new URL(baseUrlWithoutTrailingSlash + (prefix ?? this.opts.prefix) + path);
if (queryParams) {
encodeParams(queryParams, url.searchParams);
// If there are any params, encode and append them to the URL.
if (this.opts.extraParams || queryParams) {
const mergedParams = { ...this.opts.extraParams, ...queryParams };
encodeParams(mergedParams, url.searchParams);
}
return url;
}
}
+2 -1
View File
@@ -16,6 +16,7 @@ limitations under the License.
import { type MatrixError } from "./errors.ts";
import { type Logger } from "../logger.ts";
import { type QueryDict } from "../utils.ts";
export type Body = Record<string, any> | BodyInit;
@@ -52,7 +53,7 @@ export interface IHttpOpts {
baseUrl: string;
idBaseUrl?: string;
prefix: string;
extraParams?: Record<string, string>;
extraParams?: QueryDict;
accessToken?: string;
/**
+3 -1
View File
@@ -577,7 +577,9 @@ export class MatrixRTCSession extends TypedEventEmitter<
oldMemberships.some((m, i) => !CallMembership.equal(m, this.memberships[i]));
if (changed) {
this.logger.info(`Memberships for call in room ${this.roomSubset.roomId} have changed: emitting`);
this.logger.info(
`Memberships for call in room ${this.roomSubset.roomId} have changed: emitting (${this.memberships.length} members)`,
);
logDurationSync(this.logger, "emit MatrixRTCSessionEvent.MembershipsChanged", () => {
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships);
});
+9 -8
View File
@@ -21,7 +21,6 @@ import { UnsupportedDelayedEventsEndpointError } from "../errors.ts";
import { ConnectionError, HTTPError, MatrixError } from "../http-api/errors.ts";
import { type Logger, logger as rootLogger } from "../logger.ts";
import { type Room } from "../models/room.ts";
import { defer, type IDeferred } from "../utils.ts";
import { type CallMembership, DEFAULT_EXPIRE_DURATION, type SessionMembershipData } from "./CallMembership.ts";
import { type Focus } from "./focus.ts";
import { isMyMembership, Status } from "./types.ts";
@@ -209,14 +208,15 @@ export class MembershipManager
// So we do not check scheduler.actions/scheduler.insertions
if (!this.leavePromiseResolvers) {
// reset scheduled actions so we will not do any new actions.
this.leavePromiseResolvers = defer<boolean>();
this.leavePromiseResolvers = Promise.withResolvers<boolean>();
this.activated = false;
this.scheduler.initiateLeave();
if (timeout) setTimeout(() => this.leavePromiseResolvers?.resolve(false), timeout);
}
return this.leavePromiseResolvers.promise;
}
private leavePromiseResolvers?: IDeferred<boolean>;
private leavePromiseResolvers?: PromiseWithResolvers<boolean>;
public async onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise<void> {
const userId = this.client.getUserId();
@@ -229,14 +229,15 @@ export class MembershipManager
MembershipActionType.SendJoinEvent,
];
this.logger.warn("Missing own membership: force re-join");
this.state.hasMemberStateEvent = false;
if (this.scheduler.actions.find((a) => sendingMembershipActions.includes(a.type as MembershipActionType))) {
this.logger.error(
"NewMembershipManger tried adding another `SendFirstDelayedEvent` actions even though we already have one in the Queue\nActionQueueOnMemberUpdate:",
"NewMembershipManger tried adding another `SendDelayedEvent` actions even though we already have one in the Queue\nActionQueueOnMemberUpdate:",
this.scheduler.actions,
);
} else {
// Only react to our own membership missing if we have not already scheduled sending a new membership DirectMembershipManagerAction.Join
this.state.hasMemberStateEvent = false;
this.scheduler.initiateJoin();
}
}
@@ -382,13 +383,13 @@ export class MembershipManager
return this.sendOrResendDelayedLeaveEvent(); // Normal case without any previous delayed id.
} else {
// This can happen if someone else (or another client) removes our own membership event.
// It will trigger `onRTCSessionMemberUpdate` queue `MembershipActionType.SendFirstDelayedEvent`.
// It will trigger `onRTCSessionMemberUpdate` queue `MembershipActionType.SendDelayedEvent`.
// We might still have our delayed event from the previous participation and dependent on the server this might not
// get removed automatically if the state changes. Hence, it would remove our membership unexpectedly shortly after the rejoin.
//
// In this block we will try to cancel this delayed event before setting up a new one.
return this.cancelKnownDelayIdBeforeSendFirstDelayedEvent(this.state.delayId);
return this.cancelKnownDelayIdBeforeSendDelayedEvent(this.state.delayId);
}
}
case MembershipActionType.RestartDelayedEvent: {
@@ -488,7 +489,7 @@ export class MembershipManager
});
}
private async cancelKnownDelayIdBeforeSendFirstDelayedEvent(delayId: string): Promise<ActionUpdate> {
private async cancelKnownDelayIdBeforeSendDelayedEvent(delayId: string): Promise<ActionUpdate> {
// Remove all running updates and restarts
return await this.client
._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Cancel)
+2 -42
View File
@@ -28,7 +28,7 @@ import {
defaultClientOpts,
defaultSyncApiOpts,
type SetPresence,
mapToDeviceEvent,
processToDeviceMessages,
} from "./sync.ts";
import { type MatrixEvent } from "./models/event.ts";
import {
@@ -150,51 +150,11 @@ class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, Extension
}
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
const cancelledKeyVerificationTxns: string[] = [];
let events = data["events"] || [];
if (events.length > 0 && this.cryptoCallbacks) {
events = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
}
events
.map(mapToDeviceEvent)
.map((toDeviceEvent) => {
// map is a cheap inline forEach
// We want to flag m.key.verification.start events as cancelled
// if there's an accompanying m.key.verification.cancel event, so
// we pull out the transaction IDs from the cancellation events
// so we can flag the verification events as cancelled in the loop
// below.
if (toDeviceEvent.getType() === "m.key.verification.cancel") {
const txnId: string | undefined = toDeviceEvent.getContent()["transaction_id"];
if (txnId) {
cancelledKeyVerificationTxns.push(txnId);
}
}
// as mentioned above, .map is a cheap inline forEach, so return
// the unmodified event.
return toDeviceEvent;
})
.forEach((toDeviceEvent) => {
const content = toDeviceEvent.getContent();
if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") {
// the mapper already logged a warning.
logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender());
return;
}
if (
toDeviceEvent.getType() === "m.key.verification.start" ||
toDeviceEvent.getType() === "m.key.verification.request"
) {
const txnId = content["transaction_id"];
if (cancelledKeyVerificationTxns.includes(txnId)) {
toDeviceEvent.flagCancelled();
}
}
this.client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
});
processToDeviceMessages(events, this.client);
this.nextBatch = data.next_batch;
}
+51 -42
View File
@@ -1150,47 +1150,7 @@ export class SyncApi {
toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
}
const cancelledKeyVerificationTxns: string[] = [];
toDeviceMessages
.map(mapToDeviceEvent)
.map((toDeviceEvent) => {
// map is a cheap inline forEach
// We want to flag m.key.verification.start events as cancelled
// if there's an accompanying m.key.verification.cancel event, so
// we pull out the transaction IDs from the cancellation events
// so we can flag the verification events as cancelled in the loop
// below.
if (toDeviceEvent.getType() === "m.key.verification.cancel") {
const txnId: string = toDeviceEvent.getContent()["transaction_id"];
if (txnId) {
cancelledKeyVerificationTxns.push(txnId);
}
}
// as mentioned above, .map is a cheap inline forEach, so return
// the unmodified event.
return toDeviceEvent;
})
.forEach(function (toDeviceEvent) {
const content = toDeviceEvent.getContent();
if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") {
// the mapper already logged a warning.
logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender());
return;
}
if (
toDeviceEvent.getType() === "m.key.verification.start" ||
toDeviceEvent.getType() === "m.key.verification.request"
) {
const txnId = content["transaction_id"];
if (cancelledKeyVerificationTxns.includes(txnId)) {
toDeviceEvent.flagCancelled();
}
}
client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
});
processToDeviceMessages(toDeviceMessages, client);
} else {
// no more to-device events: we can stop polling with a short timeout.
this.catchingUp = false;
@@ -1946,7 +1906,56 @@ export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts:
return room;
}
export function mapToDeviceEvent(plainOldJsObject: Partial<IEvent>): MatrixEvent {
/**
* Process a list of (decrypted, where possible) received to-device events.
*
* Converts the events into `MatrixEvent`s, and emits appropriate {@link ClientEvent.ToDeviceEvent} events.
* */
export function processToDeviceMessages(toDeviceMessages: IToDeviceEvent[], client: MatrixClient): void {
const cancelledKeyVerificationTxns: string[] = [];
toDeviceMessages
.map(mapToDeviceEvent)
.map((toDeviceEvent) => {
// map is a cheap inline forEach
// We want to flag m.key.verification.start events as cancelled
// if there's an accompanying m.key.verification.cancel event, so
// we pull out the transaction IDs from the cancellation events
// so we can flag the verification events as cancelled in the loop
// below.
if (toDeviceEvent.getType() === "m.key.verification.cancel") {
const txnId: string = toDeviceEvent.getContent()["transaction_id"];
if (txnId) {
cancelledKeyVerificationTxns.push(txnId);
}
}
// as mentioned above, .map is a cheap inline forEach, so return
// the unmodified event.
return toDeviceEvent;
})
.forEach(function (toDeviceEvent) {
const content = toDeviceEvent.getContent();
if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") {
// the mapper already logged a warning.
logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender());
return;
}
if (
toDeviceEvent.getType() === "m.key.verification.start" ||
toDeviceEvent.getType() === "m.key.verification.request"
) {
const txnId = content["transaction_id"];
if (cancelledKeyVerificationTxns.includes(txnId)) {
toDeviceEvent.flagCancelled();
}
}
client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
});
}
function mapToDeviceEvent(plainOldJsObject: Partial<IEvent>): MatrixEvent {
// to-device events should not have a `room_id` property, but let's be sure
delete plainOldJsObject.room_id;
return new MatrixEvent(plainOldJsObject);
+3 -1
View File
@@ -435,7 +435,9 @@ export function immediate(): Promise<void> {
export function isNullOrUndefined(val: any): boolean {
return val === null || val === undefined;
}
/**
* @deprecated use {@link PromiseWithResolvers} instead.
*/
export type IDeferred<T> = PromiseWithResolvers<T>;
/**
+435 -362
View File
File diff suppressed because it is too large Load Diff