Compare commits
18 Commits
v37.7.0
...
v37.9.0-rc.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 921de6807d | |||
| d4e7b949e7 | |||
| 40bc833bb7 | |||
| a46ad75440 | |||
| 1e80538cfb | |||
| 73cbcfa4ee | |||
| 99972ce0a9 | |||
| 44399f6017 | |||
| c387f30e5c | |||
| 12a9875c46 | |||
| 74f5efc4ef | |||
| 43d47982ed | |||
| 94fb489952 | |||
| 75ae05e5eb | |||
| 9058b79c39 | |||
| 671dd2ca40 | |||
| 34f35393ff | |||
| e206a12902 |
@@ -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
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"] });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user