You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership (#4583)
* remove all legacy call related code and adjust tests. We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests. * dont adjust tests but remove legacy tests * Remove deprecated CallMembership.getLocalExpiry() * Remove references to legacy in test case names * Clean up SessionMembershipData tsdoc * Remove CallMembership.expires * Use correct expire duration. * make expiration methods not return optional values and update docstring * add docs to `SessionMembershipData` * Use `MSC4143` (instaed of `non-legacy`) wording in comment Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com> * Incorporate feedback from review * Fix test name --------- Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org> Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
This commit is contained in:
@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent } from "../../../src";
|
import { MatrixEvent } from "../../../src";
|
||||||
import { CallMembership, CallMembershipDataLegacy, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
import { CallMembership, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||||
|
|
||||||
function makeMockEvent(originTs = 0): MatrixEvent {
|
function makeMockEvent(originTs = 0): MatrixEvent {
|
||||||
return {
|
return {
|
||||||
@ -25,91 +25,15 @@ function makeMockEvent(originTs = 0): MatrixEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("CallMembership", () => {
|
describe("CallMembership", () => {
|
||||||
describe("CallMembershipDataLegacy", () => {
|
|
||||||
const membershipTemplate: CallMembershipDataLegacy = {
|
|
||||||
call_id: "",
|
|
||||||
scope: "m.room",
|
|
||||||
application: "m.call",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
expires: 5000,
|
|
||||||
membershipID: "bloop",
|
|
||||||
foci_active: [{ type: "livekit" }],
|
|
||||||
};
|
|
||||||
it("rejects membership with no expiry and no expires_ts", () => {
|
|
||||||
expect(() => {
|
|
||||||
new CallMembership(
|
|
||||||
makeMockEvent(),
|
|
||||||
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }),
|
|
||||||
);
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects membership with no device_id", () => {
|
|
||||||
expect(() => {
|
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects membership with no call_id", () => {
|
|
||||||
expect(() => {
|
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allow membership with no scope", () => {
|
|
||||||
expect(() => {
|
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
it("rejects with malformatted expires_ts", () => {
|
|
||||||
expect(() => {
|
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" }));
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
it("rejects with malformatted expires", () => {
|
|
||||||
expect(() => {
|
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" }));
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses event timestamp if no created_ts", () => {
|
|
||||||
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
|
|
||||||
expect(membership.createdTs()).toEqual(12345);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses created_ts if present", () => {
|
|
||||||
const membership = new CallMembership(
|
|
||||||
makeMockEvent(12345),
|
|
||||||
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
|
|
||||||
);
|
|
||||||
expect(membership.createdTs()).toEqual(67890);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("computes absolute expiry time based on expires", () => {
|
|
||||||
const membership = new CallMembership(makeMockEvent(1000), membershipTemplate);
|
|
||||||
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("computes absolute expiry time based on expires_ts", () => {
|
|
||||||
const membership = new CallMembership(
|
|
||||||
makeMockEvent(1000),
|
|
||||||
Object.assign({}, membershipTemplate, { expires_ts: 6000 }),
|
|
||||||
);
|
|
||||||
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns preferred foci", () => {
|
|
||||||
const fakeEvent = makeMockEvent();
|
|
||||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
|
||||||
const membership = new CallMembership(
|
|
||||||
fakeEvent,
|
|
||||||
Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }),
|
|
||||||
);
|
|
||||||
expect(membership.getPreferredFoci()).toEqual([mockFocus]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("SessionMembershipData", () => {
|
describe("SessionMembershipData", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
const membershipTemplate: SessionMembershipData = {
|
const membershipTemplate: SessionMembershipData = {
|
||||||
call_id: "",
|
call_id: "",
|
||||||
scope: "m.room",
|
scope: "m.room",
|
||||||
@ -150,13 +74,6 @@ describe("CallMembership", () => {
|
|||||||
expect(membership.createdTs()).toEqual(67890);
|
expect(membership.createdTs()).toEqual(67890);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("considers memberships unexpired if local age low enough", () => {
|
|
||||||
const fakeEvent = makeMockEvent(1000);
|
|
||||||
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
|
|
||||||
const membership = new CallMembership(fakeEvent, membershipTemplate);
|
|
||||||
expect(membership.isExpired()).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns preferred foci", () => {
|
it("returns preferred foci", () => {
|
||||||
const fakeEvent = makeMockEvent();
|
const fakeEvent = makeMockEvent();
|
||||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||||
@ -168,49 +85,29 @@ describe("CallMembership", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("expiry calculation", () => {
|
// TODO: re-enable this test when expiry is implemented
|
||||||
let fakeEvent: MatrixEvent;
|
// eslint-disable-next-line jest/no-commented-out-tests
|
||||||
let membership: CallMembership;
|
// describe("expiry calculation", () => {
|
||||||
const membershipTemplate: CallMembershipDataLegacy = {
|
// let fakeEvent: MatrixEvent;
|
||||||
call_id: "",
|
// let membership: CallMembership;
|
||||||
scope: "m.room",
|
|
||||||
application: "m.call",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
expires: 5000,
|
|
||||||
membershipID: "bloop",
|
|
||||||
foci_active: [{ type: "livekit" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
// beforeEach(() => {
|
||||||
// server origin timestamp for this event is 1000
|
// // server origin timestamp for this event is 1000
|
||||||
fakeEvent = makeMockEvent(1000);
|
// fakeEvent = makeMockEvent(1000);
|
||||||
membership = new CallMembership(fakeEvent!, membershipTemplate);
|
// membership = new CallMembership(fakeEvent!, membershipTemplate);
|
||||||
|
|
||||||
jest.useFakeTimers();
|
// jest.useFakeTimers();
|
||||||
});
|
// });
|
||||||
|
|
||||||
afterEach(() => {
|
// afterEach(() => {
|
||||||
jest.useRealTimers();
|
// jest.useRealTimers();
|
||||||
});
|
// });
|
||||||
|
|
||||||
it("converts expiry time into local clock", () => {
|
// eslint-disable-next-line jest/no-commented-out-tests
|
||||||
// our clock would have been at 2000 at the creation time (our clock at event receive time - age)
|
// it("calculates time until expiry", () => {
|
||||||
// (ie. the local clock is 1 second ahead of the servers' clocks)
|
// jest.setSystemTime(2000);
|
||||||
fakeEvent.localTimestamp = 2000;
|
// // should be using absolute expiry time
|
||||||
|
// expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
||||||
// for simplicity's sake, we say that the event's age is zero
|
// });
|
||||||
fakeEvent.getLocalAge = jest.fn().mockReturnValue(0);
|
// });
|
||||||
|
|
||||||
// for sanity's sake, make sure the server-relative expiry time is what we expect
|
|
||||||
expect(membership.getAbsoluteExpiry()).toEqual(6000);
|
|
||||||
// therefore the expiry time converted to our clock should be 1 second later
|
|
||||||
expect(membership.getLocalExpiry()).toEqual(7000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calculates time until expiry", () => {
|
|
||||||
jest.setSystemTime(2000);
|
|
||||||
// should be using absolute expiry time
|
|
||||||
expect(membership.getMsUntilExpiry()).toEqual(4000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -14,27 +14,13 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { encodeBase64, EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
|
import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
|
||||||
import { KnownMembership } from "../../../src/@types/membership";
|
import { KnownMembership } from "../../../src/@types/membership";
|
||||||
import {
|
import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||||
CallMembershipData,
|
|
||||||
CallMembershipDataLegacy,
|
|
||||||
SessionMembershipData,
|
|
||||||
} from "../../../src/matrixrtc/CallMembership";
|
|
||||||
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||||
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||||
import { randomString } from "../../../src/randomstring";
|
import { randomString } from "../../../src/randomstring";
|
||||||
import { makeMockRoom, makeMockRoomState, mockRTCEvent } from "./mocks";
|
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
|
||||||
|
|
||||||
const membershipTemplate: CallMembershipData = {
|
|
||||||
call_id: "",
|
|
||||||
scope: "m.room",
|
|
||||||
application: "m.call",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
expires: 60 * 60 * 1000,
|
|
||||||
membershipID: "bloop",
|
|
||||||
foci_active: [{ type: "livekit", livekit_service_url: "https://lk.url" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockFocus = { type: "mock" };
|
const mockFocus = { type: "mock" };
|
||||||
|
|
||||||
@ -59,7 +45,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
|
|
||||||
describe("roomSessionForRoom", () => {
|
describe("roomSessionForRoom", () => {
|
||||||
it("creates a room-scoped session from room state", () => {
|
it("creates a room-scoped session from room state", () => {
|
||||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
const mockRoom = makeMockRoom(membershipTemplate);
|
||||||
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
expect(sess?.memberships.length).toEqual(1);
|
expect(sess?.memberships.length).toEqual(1);
|
||||||
@ -67,43 +53,46 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(sess?.memberships[0].scope).toEqual("m.room");
|
expect(sess?.memberships[0].scope).toEqual("m.room");
|
||||||
expect(sess?.memberships[0].application).toEqual("m.call");
|
expect(sess?.memberships[0].application).toEqual("m.call");
|
||||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||||
expect(sess?.memberships[0].membershipID).toEqual("bloop");
|
|
||||||
expect(sess?.memberships[0].isExpired()).toEqual(false);
|
expect(sess?.memberships[0].isExpired()).toEqual(false);
|
||||||
expect(sess?.callId).toEqual("");
|
expect(sess?.callId).toEqual("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores expired memberships events", () => {
|
// TODO: re-enable this test when expiry is implemented
|
||||||
jest.useFakeTimers();
|
// eslint-disable-next-line jest/no-commented-out-tests
|
||||||
const expiredMembership = Object.assign({}, membershipTemplate);
|
// it("ignores expired memberships events", () => {
|
||||||
expiredMembership.expires = 1000;
|
// jest.useFakeTimers();
|
||||||
expiredMembership.device_id = "EXPIRED";
|
// const expiredMembership = Object.assign({}, membershipTemplate);
|
||||||
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
|
// expiredMembership.expires = 1000;
|
||||||
|
// expiredMembership.device_id = "EXPIRED";
|
||||||
|
// const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
|
||||||
|
|
||||||
jest.advanceTimersByTime(2000);
|
// jest.advanceTimersByTime(2000);
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
// sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
expect(sess?.memberships.length).toEqual(1);
|
// expect(sess?.memberships.length).toEqual(1);
|
||||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
// expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||||
jest.useRealTimers();
|
// jest.useRealTimers();
|
||||||
});
|
// });
|
||||||
|
|
||||||
it("ignores memberships events of members not in the room", () => {
|
it("ignores memberships events of members not in the room", () => {
|
||||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
const mockRoom = makeMockRoom(membershipTemplate);
|
||||||
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
|
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
expect(sess?.memberships.length).toEqual(0);
|
expect(sess?.memberships.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("honours created_ts", () => {
|
// TODO: re-enable this test when expiry is implemented
|
||||||
jest.useFakeTimers();
|
// eslint-disable-next-line jest/no-commented-out-tests
|
||||||
jest.setSystemTime(500);
|
// it("honours created_ts", () => {
|
||||||
const expiredMembership = Object.assign({}, membershipTemplate);
|
// jest.useFakeTimers();
|
||||||
expiredMembership.created_ts = 500;
|
// jest.setSystemTime(500);
|
||||||
expiredMembership.expires = 1000;
|
// const expiredMembership = Object.assign({}, membershipTemplate);
|
||||||
const mockRoom = makeMockRoom([expiredMembership]);
|
// expiredMembership.created_ts = 500;
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
// expiredMembership.expires = 1000;
|
||||||
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
|
// const mockRoom = makeMockRoom([expiredMembership]);
|
||||||
jest.useRealTimers();
|
// sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
});
|
// expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
|
||||||
|
// jest.useRealTimers();
|
||||||
|
// });
|
||||||
|
|
||||||
it("returns empty session if no membership events are present", () => {
|
it("returns empty session if no membership events are present", () => {
|
||||||
const mockRoom = makeMockRoom([]);
|
const mockRoom = makeMockRoom([]);
|
||||||
@ -181,14 +170,6 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(sess.memberships).toHaveLength(0);
|
expect(sess.memberships).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores memberships with no expires_ts", () => {
|
|
||||||
const expiredMembership = Object.assign({}, membershipTemplate);
|
|
||||||
(expiredMembership.expires as number | undefined) = undefined;
|
|
||||||
const mockRoom = makeMockRoom([expiredMembership]);
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
|
||||||
expect(sess.memberships).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores memberships with no device_id", () => {
|
it("ignores memberships with no device_id", () => {
|
||||||
const testMembership = Object.assign({}, membershipTemplate);
|
const testMembership = Object.assign({}, membershipTemplate);
|
||||||
(testMembership.device_id as string | undefined) = undefined;
|
(testMembership.device_id as string | undefined) = undefined;
|
||||||
@ -224,23 +205,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
|
|
||||||
describe("updateCallMembershipEvent", () => {
|
describe("updateCallMembershipEvent", () => {
|
||||||
const mockFocus = { type: "livekit", livekit_service_url: "https://test.org" };
|
const mockFocus = { type: "livekit", livekit_service_url: "https://test.org" };
|
||||||
const joinSessionConfig = { useLegacyMemberEvents: false };
|
const joinSessionConfig = {};
|
||||||
|
|
||||||
const legacyMembershipData: CallMembershipDataLegacy = {
|
|
||||||
call_id: "",
|
|
||||||
scope: "m.room",
|
|
||||||
application: "m.call",
|
|
||||||
device_id: "AAAAAAA_legacy",
|
|
||||||
expires: 60 * 60 * 1000,
|
|
||||||
membershipID: "bloop",
|
|
||||||
foci_active: [mockFocus],
|
|
||||||
};
|
|
||||||
|
|
||||||
const expiredLegacyMembershipData: CallMembershipDataLegacy = {
|
|
||||||
...legacyMembershipData,
|
|
||||||
device_id: "AAAAAAA_legacy_expired",
|
|
||||||
expires: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const sessionMembershipData: SessionMembershipData = {
|
const sessionMembershipData: SessionMembershipData = {
|
||||||
call_id: "",
|
call_id: "",
|
||||||
@ -273,39 +238,22 @@ describe("MatrixRTCSession", () => {
|
|||||||
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
|
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function testSession(
|
async function testSession(membershipData: SessionMembershipData): Promise<void> {
|
||||||
membershipData: CallMembershipData[] | SessionMembershipData,
|
|
||||||
shouldUseLegacy: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
|
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
|
||||||
|
|
||||||
const makeNewLegacyMembershipsMock = jest.spyOn(sess as any, "makeNewLegacyMemberships");
|
|
||||||
const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership");
|
const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership");
|
||||||
|
|
||||||
sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
|
sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
|
||||||
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]);
|
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]);
|
||||||
|
|
||||||
expect(makeNewLegacyMembershipsMock).toHaveBeenCalledTimes(shouldUseLegacy ? 1 : 0);
|
expect(makeNewMembershipMock).toHaveBeenCalledTimes(1);
|
||||||
expect(makeNewMembershipMock).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
|
|
||||||
|
|
||||||
await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]);
|
await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]);
|
||||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
|
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
it("uses legacy events if there are any active legacy calls", async () => {
|
it("sends events", async () => {
|
||||||
await testSession([expiredLegacyMembershipData, legacyMembershipData, sessionMembershipData], true);
|
await testSession(sessionMembershipData);
|
||||||
});
|
|
||||||
|
|
||||||
it('uses legacy events if a non-legacy call is in a "memberships" array', async () => {
|
|
||||||
await testSession([sessionMembershipData], true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses non-legacy events if all legacy calls are expired", async () => {
|
|
||||||
await testSession([expiredLegacyMembershipData], false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses non-legacy events if there are only non-legacy calls", async () => {
|
|
||||||
await testSession(sessionMembershipData, false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -325,70 +273,6 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getsActiveFocus", () => {
|
|
||||||
const activeFociConfig = { type: "livekit", livekit_service_url: "https://active.url" };
|
|
||||||
it("gets the correct active focus with oldest_membership", () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
jest.setSystemTime(3000);
|
|
||||||
const mockRoom = makeMockRoom([
|
|
||||||
Object.assign({}, membershipTemplate, {
|
|
||||||
device_id: "foo",
|
|
||||||
created_ts: 500,
|
|
||||||
foci_active: [activeFociConfig],
|
|
||||||
}),
|
|
||||||
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
|
|
||||||
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
|
||||||
|
|
||||||
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
|
|
||||||
type: "livekit",
|
|
||||||
focus_selection: "oldest_membership",
|
|
||||||
});
|
|
||||||
expect(sess.getActiveFocus()).toBe(activeFociConfig);
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
it("does not provide focus if the selction method is unknown", () => {
|
|
||||||
const mockRoom = makeMockRoom([
|
|
||||||
Object.assign({}, membershipTemplate, {
|
|
||||||
device_id: "foo",
|
|
||||||
created_ts: 500,
|
|
||||||
foci_active: [activeFociConfig],
|
|
||||||
}),
|
|
||||||
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
|
|
||||||
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
|
||||||
|
|
||||||
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
|
|
||||||
type: "livekit",
|
|
||||||
focus_selection: "unknown",
|
|
||||||
});
|
|
||||||
expect(sess.getActiveFocus()).toBe(undefined);
|
|
||||||
});
|
|
||||||
it("gets the correct active focus legacy", () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
jest.setSystemTime(3000);
|
|
||||||
const mockRoom = makeMockRoom([
|
|
||||||
Object.assign({}, membershipTemplate, {
|
|
||||||
device_id: "foo",
|
|
||||||
created_ts: 500,
|
|
||||||
foci_active: [activeFociConfig],
|
|
||||||
}),
|
|
||||||
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
|
|
||||||
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
|
||||||
|
|
||||||
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }]);
|
|
||||||
expect(sess.getActiveFocus()).toBe(activeFociConfig);
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("joining", () => {
|
describe("joining", () => {
|
||||||
let mockRoom: Room;
|
let mockRoom: Room;
|
||||||
let sendStateEventMock: jest.Mock;
|
let sendStateEventMock: jest.Mock;
|
||||||
@ -439,67 +323,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(sess!.isJoined()).toEqual(true);
|
expect(sess!.isJoined()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends a membership event when joining a call", async () => {
|
describe("calls", () => {
|
||||||
const realSetTimeout = setTimeout;
|
|
||||||
jest.useFakeTimers();
|
|
||||||
sess!.joinRoomSession([mockFocus], mockFocus);
|
|
||||||
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
|
||||||
mockRoom!.roomId,
|
|
||||||
EventType.GroupCallMemberPrefix,
|
|
||||||
{
|
|
||||||
memberships: [
|
|
||||||
{
|
|
||||||
application: "m.call",
|
|
||||||
scope: "m.room",
|
|
||||||
call_id: "",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
expires: 3600000,
|
|
||||||
expires_ts: Date.now() + 3600000,
|
|
||||||
foci_active: [mockFocus],
|
|
||||||
|
|
||||||
membershipID: expect.stringMatching(".*"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"@alice:example.org",
|
|
||||||
);
|
|
||||||
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
|
||||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses membershipExpiryTimeout from join config", async () => {
|
|
||||||
const realSetTimeout = setTimeout;
|
|
||||||
jest.useFakeTimers();
|
|
||||||
sess!.joinRoomSession([mockFocus], mockFocus, { membershipExpiryTimeout: 60000 });
|
|
||||||
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
|
||||||
mockRoom!.roomId,
|
|
||||||
EventType.GroupCallMemberPrefix,
|
|
||||||
{
|
|
||||||
memberships: [
|
|
||||||
{
|
|
||||||
application: "m.call",
|
|
||||||
scope: "m.room",
|
|
||||||
call_id: "",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
expires: 60000,
|
|
||||||
expires_ts: Date.now() + 60000,
|
|
||||||
foci_active: [mockFocus],
|
|
||||||
|
|
||||||
membershipID: expect.stringMatching(".*"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"@alice:example.org",
|
|
||||||
);
|
|
||||||
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
|
||||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("non-legacy calls", () => {
|
|
||||||
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
|
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
|
||||||
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
|
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
|
||||||
|
|
||||||
@ -557,7 +381,6 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sess!.joinRoomSession([activeFocusConfig], activeFocus, {
|
sess!.joinRoomSession([activeFocusConfig], activeFocus, {
|
||||||
useLegacyMemberEvents: false,
|
|
||||||
membershipServerSideExpiryTimeout: 9000,
|
membershipServerSideExpiryTimeout: 9000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -579,6 +402,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
application: "m.call",
|
application: "m.call",
|
||||||
scope: "m.room",
|
scope: "m.room",
|
||||||
call_id: "",
|
call_id: "",
|
||||||
|
expires: 14400000,
|
||||||
device_id: "AAAAAAA",
|
device_id: "AAAAAAA",
|
||||||
foci_preferred: [activeFocusConfig],
|
foci_preferred: [activeFocusConfig],
|
||||||
focus_active: activeFocus,
|
focus_active: activeFocus,
|
||||||
@ -598,7 +422,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
}
|
}
|
||||||
|
|
||||||
it("sends a membership event with session payload when joining a non-legacy call", async () => {
|
it("sends a membership event with session payload when joining a call", async () => {
|
||||||
await testJoin(false);
|
await testJoin(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -607,91 +431,19 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if join called when already joined", () => {
|
it("does nothing if join called when already joined", async () => {
|
||||||
sess!.joinRoomSession([mockFocus], mockFocus);
|
sess!.joinRoomSession([mockFocus], mockFocus);
|
||||||
|
await sentStateEvent;
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
sess!.joinRoomSession([mockFocus], mockFocus);
|
sess!.joinRoomSession([mockFocus], mockFocus);
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renews membership event before expiry time", async () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
let resolveFn: ((_roomId: string, _type: string, val: Record<string, any>) => void) | undefined;
|
|
||||||
|
|
||||||
const eventSentPromise = new Promise<Record<string, any>>((r) => {
|
|
||||||
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
|
|
||||||
r(val);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
const sendStateEventMock = jest.fn().mockImplementation(resolveFn);
|
|
||||||
client.sendStateEvent = sendStateEventMock;
|
|
||||||
|
|
||||||
sess!.joinRoomSession([mockFocus], mockFocus);
|
|
||||||
|
|
||||||
const eventContent = await eventSentPromise;
|
|
||||||
|
|
||||||
jest.setSystemTime(1000);
|
|
||||||
const event = mockRTCEvent(eventContent.memberships, mockRoom.roomId);
|
|
||||||
const getState = mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
|
||||||
getState.getStateEvents = jest.fn().mockReturnValue(event);
|
|
||||||
getState.events = new Map([
|
|
||||||
[
|
|
||||||
event.getType(),
|
|
||||||
{
|
|
||||||
size: () => true,
|
|
||||||
has: (_stateKey: string) => true,
|
|
||||||
get: (_stateKey: string) => event,
|
|
||||||
values: () => [event],
|
|
||||||
} as unknown as Map<string, MatrixEvent>,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const eventReSentPromise = new Promise<Record<string, any>>((r) => {
|
|
||||||
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
|
|
||||||
r(val);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
sendStateEventMock.mockReset().mockImplementation(resolveFn);
|
|
||||||
|
|
||||||
// definitely should have renewed by 1 second before the expiry!
|
|
||||||
const timeElapsed = 60 * 60 * 1000 - 1000;
|
|
||||||
jest.setSystemTime(Date.now() + timeElapsed);
|
|
||||||
jest.advanceTimersByTime(timeElapsed);
|
|
||||||
await eventReSentPromise;
|
|
||||||
|
|
||||||
expect(sendStateEventMock).toHaveBeenCalledWith(
|
|
||||||
mockRoom.roomId,
|
|
||||||
EventType.GroupCallMemberPrefix,
|
|
||||||
{
|
|
||||||
memberships: [
|
|
||||||
{
|
|
||||||
application: "m.call",
|
|
||||||
scope: "m.room",
|
|
||||||
call_id: "",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
expires: 3600000 * 2,
|
|
||||||
expires_ts: 1000 + 3600000 * 2,
|
|
||||||
foci_active: [mockFocus],
|
|
||||||
created_ts: 1000,
|
|
||||||
membershipID: expect.stringMatching(".*"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"@alice:example.org",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
jest.useRealTimers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("onMembershipsChanged", () => {
|
describe("onMembershipsChanged", () => {
|
||||||
it("does not emit if no membership changes", () => {
|
it("does not emit if no membership changes", () => {
|
||||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
const mockRoom = makeMockRoom(membershipTemplate);
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
|
|
||||||
const onMembershipsChanged = jest.fn();
|
const onMembershipsChanged = jest.fn();
|
||||||
@ -702,7 +454,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("emits on membership changes", () => {
|
it("emits on membership changes", () => {
|
||||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
const mockRoom = makeMockRoom(membershipTemplate);
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
|
|
||||||
const onMembershipsChanged = jest.fn();
|
const onMembershipsChanged = jest.fn();
|
||||||
@ -714,26 +466,28 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(onMembershipsChanged).toHaveBeenCalled();
|
expect(onMembershipsChanged).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits an event at the time a membership event expires", () => {
|
// TODO: re-enable this test when expiry is implemented
|
||||||
jest.useFakeTimers();
|
// eslint-disable-next-line jest/no-commented-out-tests
|
||||||
try {
|
// it("emits an event at the time a membership event expires", () => {
|
||||||
const membership = Object.assign({}, membershipTemplate);
|
// jest.useFakeTimers();
|
||||||
const mockRoom = makeMockRoom([membership]);
|
// try {
|
||||||
|
// const membership = Object.assign({}, membershipTemplate);
|
||||||
|
// const mockRoom = makeMockRoom([membership]);
|
||||||
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
// sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
const membershipObject = sess.memberships[0];
|
// const membershipObject = sess.memberships[0];
|
||||||
|
|
||||||
const onMembershipsChanged = jest.fn();
|
// const onMembershipsChanged = jest.fn();
|
||||||
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
|
// sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
|
||||||
|
|
||||||
jest.advanceTimersByTime(61 * 1000 * 1000);
|
// jest.advanceTimersByTime(61 * 1000 * 1000);
|
||||||
|
|
||||||
expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []);
|
// expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []);
|
||||||
expect(sess?.memberships.length).toEqual(0);
|
// expect(sess?.memberships.length).toEqual(0);
|
||||||
} finally {
|
// } finally {
|
||||||
jest.useRealTimers();
|
// jest.useRealTimers();
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("key management", () => {
|
describe("key management", () => {
|
||||||
@ -805,9 +559,13 @@ describe("MatrixRTCSession", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not send key if join called when already joined", () => {
|
it("does not send key if join called when already joined", async () => {
|
||||||
|
const sentStateEvent = new Promise((resolve) => {
|
||||||
|
sendStateEventMock = jest.fn(resolve);
|
||||||
|
});
|
||||||
|
client.sendStateEvent = sendStateEventMock;
|
||||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
|
await sentStateEvent;
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||||
expect(client.sendEvent).toHaveBeenCalledTimes(1);
|
expect(client.sendEvent).toHaveBeenCalledTimes(1);
|
||||||
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
|
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
|
||||||
@ -1016,89 +774,6 @@ describe("MatrixRTCSession", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("re-sends key if a member changes membership ID", async () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
try {
|
|
||||||
const keysSentPromise1 = new Promise((resolve) => {
|
|
||||||
sendEventMock.mockImplementation(resolve);
|
|
||||||
});
|
|
||||||
|
|
||||||
const member1 = membershipTemplate;
|
|
||||||
const member2 = {
|
|
||||||
...membershipTemplate,
|
|
||||||
device_id: "BBBBBBB",
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockRoom = makeMockRoom([member1, member2]);
|
|
||||||
mockRoom.getLiveTimeline().getState = jest
|
|
||||||
.fn()
|
|
||||||
.mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId));
|
|
||||||
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
|
||||||
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
|
||||||
|
|
||||||
await keysSentPromise1;
|
|
||||||
|
|
||||||
// make sure an encryption key was sent
|
|
||||||
expect(sendEventMock).toHaveBeenCalledWith(
|
|
||||||
expect.stringMatching(".*"),
|
|
||||||
"io.element.call.encryption_keys",
|
|
||||||
{
|
|
||||||
call_id: "",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
keys: [
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
key: expect.stringMatching(".*"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
sent_ts: Date.now(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
|
|
||||||
|
|
||||||
sendEventMock.mockClear();
|
|
||||||
|
|
||||||
// this should be a no-op:
|
|
||||||
sess.onMembershipUpdate();
|
|
||||||
expect(sendEventMock).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
// advance time to avoid key throttling
|
|
||||||
jest.advanceTimersByTime(10000);
|
|
||||||
|
|
||||||
// update membership ID
|
|
||||||
member2.membershipID = "newID";
|
|
||||||
|
|
||||||
const keysSentPromise2 = new Promise((resolve) => {
|
|
||||||
sendEventMock.mockImplementation(resolve);
|
|
||||||
});
|
|
||||||
|
|
||||||
// this should re-send the key
|
|
||||||
sess.onMembershipUpdate();
|
|
||||||
|
|
||||||
await keysSentPromise2;
|
|
||||||
|
|
||||||
expect(sendEventMock).toHaveBeenCalledWith(
|
|
||||||
expect.stringMatching(".*"),
|
|
||||||
"io.element.call.encryption_keys",
|
|
||||||
{
|
|
||||||
call_id: "",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
keys: [
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
key: expect.stringMatching(".*"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
sent_ts: Date.now(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
|
|
||||||
} finally {
|
|
||||||
jest.useRealTimers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("re-sends key if a member changes created_ts", async () => {
|
it("re-sends key if a member changes created_ts", async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
jest.setSystemTime(1000);
|
jest.setSystemTime(1000);
|
||||||
@ -1240,7 +915,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
it("wraps key index around to 0 when it reaches the maximum", async () => {
|
it("wraps key index around to 0 when it reaches the maximum", async () => {
|
||||||
// this should give us keys with index [0...255, 0, 1]
|
// this should give us keys with index [0...255, 0, 1]
|
||||||
const membersToTest = 258;
|
const membersToTest = 258;
|
||||||
const members: CallMembershipData[] = [];
|
const members: SessionMembershipData[] = [];
|
||||||
for (let i = 0; i < membersToTest; i++) {
|
for (let i = 0; i < membersToTest; i++) {
|
||||||
members.push(Object.assign({}, membershipTemplate, { device_id: `DEVICE${i}` }));
|
members.push(Object.assign({}, membershipTemplate, { device_id: `DEVICE${i}` }));
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Mock } from "jest-mock";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
@ -24,19 +26,8 @@ import {
|
|||||||
RoomEvent,
|
RoomEvent,
|
||||||
} from "../../../src";
|
} from "../../../src";
|
||||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
import { RoomStateEvent } from "../../../src/models/room-state";
|
||||||
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
|
|
||||||
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
||||||
import { makeMockRoom } from "./mocks";
|
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
|
||||||
|
|
||||||
const membershipTemplate: CallMembershipData = {
|
|
||||||
call_id: "",
|
|
||||||
scope: "m.room",
|
|
||||||
application: "m.call",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
expires: 60 * 60 * 1000,
|
|
||||||
membershipID: "bloop",
|
|
||||||
foci_active: [{ type: "test" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("MatrixRTCSessionManager", () => {
|
describe("MatrixRTCSessionManager", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
@ -69,16 +60,15 @@ describe("MatrixRTCSessionManager", () => {
|
|||||||
it("Fires event when session ends", () => {
|
it("Fires event when session ends", () => {
|
||||||
const onEnded = jest.fn();
|
const onEnded = jest.fn();
|
||||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||||
|
const room1 = makeMockRoom(membershipTemplate);
|
||||||
const memberships = [membershipTemplate];
|
|
||||||
|
|
||||||
const room1 = makeMockRoom(memberships);
|
|
||||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||||
|
|
||||||
client.emit(ClientEvent.Room, room1);
|
client.emit(ClientEvent.Room, room1);
|
||||||
|
|
||||||
memberships.splice(0, 1);
|
(room1.getLiveTimeline as Mock).mockReturnValue({
|
||||||
|
getState: jest.fn().mockReturnValue(makeMockRoomState([{}], room1.roomId)),
|
||||||
|
});
|
||||||
|
|
||||||
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||||
const membEvent = roomState.getStateEvents("")[0];
|
const membEvent = roomState.getStateEvents("")[0];
|
||||||
|
@ -15,16 +15,36 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventType, MatrixEvent, Room } from "../../../src";
|
import { EventType, MatrixEvent, Room } from "../../../src";
|
||||||
import { CallMembershipData, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||||
import { randomString } from "../../../src/randomstring";
|
import { randomString } from "../../../src/randomstring";
|
||||||
|
|
||||||
type MembershipData = CallMembershipData[] | SessionMembershipData;
|
type MembershipData = SessionMembershipData[] | SessionMembershipData | {};
|
||||||
|
|
||||||
|
export const membershipTemplate: SessionMembershipData = {
|
||||||
|
application: "m.call",
|
||||||
|
call_id: "",
|
||||||
|
device_id: "AAAAAAA",
|
||||||
|
scope: "m.room",
|
||||||
|
focus_active: { type: "livekit", livekit_service_url: "https://lk.url" },
|
||||||
|
foci_preferred: [
|
||||||
|
{
|
||||||
|
livekit_alias: "!alias:something.org",
|
||||||
|
livekit_service_url: "https://livekit-jwt.something.io",
|
||||||
|
type: "livekit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
livekit_alias: "!alias:something.org",
|
||||||
|
livekit_service_url: "https://livekit-jwt.something.dev",
|
||||||
|
type: "livekit",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export function makeMockRoom(membershipData: MembershipData): Room {
|
export function makeMockRoom(membershipData: MembershipData): Room {
|
||||||
const roomId = randomString(8);
|
const roomId = randomString(8);
|
||||||
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
||||||
const roomState = makeMockRoomState(membershipData, roomId);
|
const roomState = makeMockRoomState(membershipData, roomId);
|
||||||
return {
|
const room = {
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
hasMembershipState: jest.fn().mockReturnValue(true),
|
hasMembershipState: jest.fn().mockReturnValue(true),
|
||||||
getLiveTimeline: jest.fn().mockReturnValue({
|
getLiveTimeline: jest.fn().mockReturnValue({
|
||||||
@ -32,41 +52,46 @@ export function makeMockRoom(membershipData: MembershipData): Room {
|
|||||||
}),
|
}),
|
||||||
getVersion: jest.fn().mockReturnValue("default"),
|
getVersion: jest.fn().mockReturnValue("default"),
|
||||||
} as unknown as Room;
|
} as unknown as Room;
|
||||||
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeMockRoomState(membershipData: MembershipData, roomId: string) {
|
export function makeMockRoomState(membershipData: MembershipData, roomId: string) {
|
||||||
const event = mockRTCEvent(membershipData, roomId);
|
const events = Array.isArray(membershipData)
|
||||||
|
? membershipData.map((m) => mockRTCEvent(m, roomId))
|
||||||
|
: [mockRTCEvent(membershipData, roomId)];
|
||||||
|
const keysAndEvents = events.map((e) => {
|
||||||
|
const data = e.getContent() as SessionMembershipData;
|
||||||
|
return [`_${e.sender?.userId}_${data.device_id}`];
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
off: jest.fn(),
|
off: jest.fn(),
|
||||||
getStateEvents: (_: string, stateKey: string) => {
|
getStateEvents: (_: string, stateKey: string) => {
|
||||||
if (stateKey !== undefined) return event;
|
if (stateKey !== undefined) return keysAndEvents.find(([k]) => k === stateKey)?.[1];
|
||||||
return [event];
|
return events;
|
||||||
},
|
},
|
||||||
events: new Map([
|
events:
|
||||||
[
|
events.length === 0
|
||||||
event.getType(),
|
? new Map()
|
||||||
{
|
: new Map([
|
||||||
size: () => true,
|
[
|
||||||
has: (_stateKey: string) => true,
|
EventType.GroupCallMemberPrefix,
|
||||||
get: (_stateKey: string) => event,
|
{
|
||||||
values: () => [event],
|
size: () => true,
|
||||||
},
|
has: (stateKey: string) => keysAndEvents.find(([k]) => k === stateKey),
|
||||||
],
|
get: (stateKey: string) => keysAndEvents.find(([k]) => k === stateKey)?.[1],
|
||||||
]),
|
values: () => events,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockRTCEvent(membershipData: MembershipData, roomId: string): MatrixEvent {
|
export function mockRTCEvent(membershipData: MembershipData, roomId: string): MatrixEvent {
|
||||||
return {
|
return {
|
||||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||||
getContent: jest.fn().mockReturnValue(
|
getContent: jest.fn().mockReturnValue(membershipData),
|
||||||
!Array.isArray(membershipData)
|
|
||||||
? membershipData
|
|
||||||
: {
|
|
||||||
memberships: membershipData,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
||||||
getTs: jest.fn().mockReturnValue(Date.now()),
|
getTs: jest.fn().mockReturnValue(Date.now()),
|
||||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||||
|
@ -35,11 +35,7 @@ import {
|
|||||||
SpaceChildEventContent,
|
SpaceChildEventContent,
|
||||||
SpaceParentEventContent,
|
SpaceParentEventContent,
|
||||||
} from "./state_events.ts";
|
} from "./state_events.ts";
|
||||||
import {
|
import { IGroupCallRoomMemberState, IGroupCallRoomState } from "../webrtc/groupCall.ts";
|
||||||
ExperimentalGroupCallRoomMemberState,
|
|
||||||
IGroupCallRoomMemberState,
|
|
||||||
IGroupCallRoomState,
|
|
||||||
} from "../webrtc/groupCall.ts";
|
|
||||||
import { MSC3089EventContent } from "../models/MSC3089Branch.ts";
|
import { MSC3089EventContent } from "../models/MSC3089Branch.ts";
|
||||||
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon.ts";
|
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon.ts";
|
||||||
import { XOR } from "./common.ts";
|
import { XOR } from "./common.ts";
|
||||||
@ -361,10 +357,7 @@ export interface StateEvents {
|
|||||||
|
|
||||||
// MSC3401
|
// MSC3401
|
||||||
[EventType.GroupCallPrefix]: IGroupCallRoomState;
|
[EventType.GroupCallPrefix]: IGroupCallRoomState;
|
||||||
[EventType.GroupCallMemberPrefix]: XOR<
|
[EventType.GroupCallMemberPrefix]: XOR<IGroupCallRoomMemberState, XOR<SessionMembershipData, {}>>;
|
||||||
XOR<IGroupCallRoomMemberState, ExperimentalGroupCallRoomMemberState>,
|
|
||||||
XOR<SessionMembershipData, {}>
|
|
||||||
>;
|
|
||||||
|
|
||||||
// MSC3089
|
// MSC3089
|
||||||
[UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent;
|
[UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent;
|
||||||
|
@ -14,35 +14,71 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EitherAnd } from "matrix-events-sdk/lib/types";
|
|
||||||
|
|
||||||
import { MatrixEvent } from "../matrix.ts";
|
import { MatrixEvent } from "../matrix.ts";
|
||||||
import { deepCompare } from "../utils.ts";
|
import { deepCompare } from "../utils.ts";
|
||||||
import { Focus } from "./focus.ts";
|
import { Focus } from "./focus.ts";
|
||||||
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
||||||
|
|
||||||
type CallScope = "m.room" | "m.user";
|
type CallScope = "m.room" | "m.user";
|
||||||
// Represents an entry in the memberships section of an m.call.member event as it is on the wire
|
|
||||||
|
|
||||||
// There are two different data interfaces. One for the Legacy types and one compliant with MSC4143
|
|
||||||
|
|
||||||
// MSC4143 (MatrixRTC) session membership data
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MSC4143 (MatrixRTC) session membership data.
|
||||||
|
* Represents an entry in the memberships section of an m.call.member event as it is on the wire.
|
||||||
|
**/
|
||||||
export type SessionMembershipData = {
|
export type SessionMembershipData = {
|
||||||
|
/**
|
||||||
|
* The RTC application defines the type of the RTC session.
|
||||||
|
*/
|
||||||
application: string;
|
application: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of this session.
|
||||||
|
* A session can never span over multiple rooms so this id is to distinguish between
|
||||||
|
* multiple session in one room. A room wide session that is not associated with a user,
|
||||||
|
* and therefore immune to creation race conflicts, uses the `call_id: ""`.
|
||||||
|
*/
|
||||||
call_id: string;
|
call_id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Matrix device ID of this session. A single user can have multiple sessions on different devices.
|
||||||
|
*/
|
||||||
device_id: string;
|
device_id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The focus selection system this user/membership is using.
|
||||||
|
*/
|
||||||
focus_active: Focus;
|
focus_active: Focus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of possible foci this uses knows about. One of them might be used based on the focus_active
|
||||||
|
* selection system.
|
||||||
|
*/
|
||||||
foci_preferred: Focus[];
|
foci_preferred: Focus[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional field that contains the creation of the session. If it is undefined the creation
|
||||||
|
* is the `origin_server_ts` of the event itself. For updates to the event this property tracks
|
||||||
|
* the `origin_server_ts` of the initial join event.
|
||||||
|
* - If it is undefined it can be interpreted as a "Join".
|
||||||
|
* - If it is defined it can be interpreted as an "Update"
|
||||||
|
*/
|
||||||
created_ts?: number;
|
created_ts?: number;
|
||||||
|
|
||||||
// Application specific data
|
// Application specific data
|
||||||
scope?: CallScope;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isSessionMembershipData = (data: CallMembershipData): data is SessionMembershipData =>
|
/**
|
||||||
"focus_active" in data;
|
* If the `application` = `"m.call"` this defines if it is a room or user owned call.
|
||||||
|
* There can always be one room scroped call but multiple user owned calls (breakout sessions)
|
||||||
|
*/
|
||||||
|
scope?: CallScope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally we allow to define a delta to the `created_ts` that defines when the event is expired/invalid.
|
||||||
|
* This should be set to multiple hours. The only reason it exist is to deal with failed delayed events.
|
||||||
|
* (for example caused by a homeserver crashes)
|
||||||
|
**/
|
||||||
|
expires?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const checkSessionsMembershipData = (data: any, errors: string[]): data is SessionMembershipData => {
|
const checkSessionsMembershipData = (data: any, errors: string[]): data is SessionMembershipData => {
|
||||||
const prefix = "Malformed session membership event: ";
|
const prefix = "Malformed session membership event: ";
|
||||||
@ -59,65 +95,20 @@ const checkSessionsMembershipData = (data: any, errors: string[]): data is Sessi
|
|||||||
return errors.length === 0;
|
return errors.length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Legacy session membership data
|
|
||||||
|
|
||||||
export type CallMembershipDataLegacy = {
|
|
||||||
application: string;
|
|
||||||
call_id: string;
|
|
||||||
scope: CallScope;
|
|
||||||
device_id: string;
|
|
||||||
membershipID: string;
|
|
||||||
created_ts?: number;
|
|
||||||
foci_active?: Focus[];
|
|
||||||
} & EitherAnd<{ expires: number }, { expires_ts: number }>;
|
|
||||||
|
|
||||||
export const isLegacyCallMembershipData = (data: CallMembershipData): data is CallMembershipDataLegacy =>
|
|
||||||
"membershipID" in data;
|
|
||||||
|
|
||||||
const checkCallMembershipDataLegacy = (data: any, errors: string[]): data is CallMembershipDataLegacy => {
|
|
||||||
const prefix = "Malformed legacy rtc membership event: ";
|
|
||||||
if (!("expires" in data || "expires_ts" in data)) {
|
|
||||||
errors.push(prefix + "expires_ts or expires must be present");
|
|
||||||
}
|
|
||||||
if ("expires" in data) {
|
|
||||||
if (typeof data.expires !== "number") {
|
|
||||||
errors.push(prefix + "expires must be numeric");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ("expires_ts" in data) {
|
|
||||||
if (typeof data.expires_ts !== "number") {
|
|
||||||
errors.push(prefix + "expires_ts must be numeric");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.device_id !== "string") errors.push(prefix + "device_id must be string");
|
|
||||||
if (typeof data.call_id !== "string") errors.push(prefix + "call_id must be string");
|
|
||||||
if (typeof data.application !== "string") errors.push(prefix + "application must be a string");
|
|
||||||
if (typeof data.membershipID !== "string") errors.push(prefix + "membershipID must be a string");
|
|
||||||
// optional elements
|
|
||||||
if (data.created_ts && typeof data.created_ts !== "number") errors.push(prefix + "created_ts must be number");
|
|
||||||
// application specific data (we first need to check if they exist)
|
|
||||||
if (data.scope && typeof data.scope !== "string") errors.push(prefix + "scope must be string");
|
|
||||||
return errors.length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CallMembershipData = CallMembershipDataLegacy | SessionMembershipData;
|
|
||||||
|
|
||||||
export class CallMembership {
|
export class CallMembership {
|
||||||
public static equal(a: CallMembership, b: CallMembership): boolean {
|
public static equal(a: CallMembership, b: CallMembership): boolean {
|
||||||
return deepCompare(a.membershipData, b.membershipData);
|
return deepCompare(a.membershipData, b.membershipData);
|
||||||
}
|
}
|
||||||
private membershipData: CallMembershipData;
|
private membershipData: SessionMembershipData;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private parentEvent: MatrixEvent,
|
private parentEvent: MatrixEvent,
|
||||||
data: any,
|
data: any,
|
||||||
) {
|
) {
|
||||||
const sessionErrors: string[] = [];
|
const sessionErrors: string[] = [];
|
||||||
const legacyErrors: string[] = [];
|
if (!checkSessionsMembershipData(data, sessionErrors)) {
|
||||||
if (!checkSessionsMembershipData(data, sessionErrors) && !checkCallMembershipDataLegacy(data, legacyErrors)) {
|
|
||||||
throw Error(
|
throw Error(
|
||||||
`unknown CallMembership data. Does not match legacy call.member (${legacyErrors.join(" & ")}) events nor MSC4143 (${sessionErrors.join(" & ")})`,
|
`unknown CallMembership data. Does not match MSC4143 call.member (${sessionErrors.join(" & ")}) events this could be a legacy membership event: (${data})`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.membershipData = data;
|
this.membershipData = data;
|
||||||
@ -149,11 +140,10 @@ export class CallMembership {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get membershipID(): string {
|
public get membershipID(): string {
|
||||||
if (isLegacyCallMembershipData(this.membershipData)) return this.membershipData.membershipID;
|
|
||||||
// the createdTs behaves equivalent to the membershipID.
|
// the createdTs behaves equivalent to the membershipID.
|
||||||
// we only need the field for the legacy member envents where we needed to update them
|
// we only need the field for the legacy member envents where we needed to update them
|
||||||
// synapse ignores sending state events if they have the same content.
|
// synapse ignores sending state events if they have the same content.
|
||||||
else return this.createdTs().toString();
|
return this.createdTs().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public createdTs(): number {
|
public createdTs(): number {
|
||||||
@ -161,57 +151,24 @@ export class CallMembership {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the absolute expiry time of the membership if applicable to this membership type.
|
* Gets the absolute expiry timestamp of the membership.
|
||||||
* @returns The absolute expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable
|
* @returns The absolute expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable
|
||||||
*/
|
*/
|
||||||
public getAbsoluteExpiry(): number | undefined {
|
public getAbsoluteExpiry(): number | undefined {
|
||||||
// if the membership is not a legacy membership, we assume it is MSC4143
|
// TODO: implement this in a future PR. Something like:
|
||||||
if (!isLegacyCallMembershipData(this.membershipData)) return undefined;
|
// TODO: calculate this from the MatrixRTCSession join configuration directly
|
||||||
|
// return this.createdTs() + (this.membershipData.expires ?? DEFAULT_EXPIRE_DURATION);
|
||||||
|
|
||||||
if ("expires" in this.membershipData) {
|
return undefined;
|
||||||
// we know createdTs exists since we already do the isLegacyCallMembershipData check
|
|
||||||
return this.createdTs() + this.membershipData.expires;
|
|
||||||
} else {
|
|
||||||
// We know it exists because we checked for this in the constructor.
|
|
||||||
return this.membershipData.expires_ts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the expiry time of the event, converted into the device's local time.
|
|
||||||
* @deprecated This function has been observed returning bad data and is no longer used by MatrixRTC.
|
|
||||||
* @returns The local expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable
|
|
||||||
*/
|
|
||||||
public getLocalExpiry(): number | undefined {
|
|
||||||
// if the membership is not a legacy membership, we assume it is MSC4143
|
|
||||||
if (!isLegacyCallMembershipData(this.membershipData)) return undefined;
|
|
||||||
|
|
||||||
if ("expires" in this.membershipData) {
|
|
||||||
// we know createdTs exists since we already do the isLegacyCallMembershipData check
|
|
||||||
const relativeCreationTime = this.parentEvent.getTs() - this.createdTs();
|
|
||||||
|
|
||||||
const localCreationTs = this.parentEvent.localTimestamp - relativeCreationTime;
|
|
||||||
|
|
||||||
return localCreationTs + this.membershipData.expires;
|
|
||||||
} else {
|
|
||||||
// With expires_ts we cannot convert to local time.
|
|
||||||
// TODO: Check the server timestamp and compute a diff to local time.
|
|
||||||
return this.membershipData.expires_ts;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns The number of milliseconds until the membership expires or undefined if applicable
|
* @returns The number of milliseconds until the membership expires or undefined if applicable
|
||||||
*/
|
*/
|
||||||
public getMsUntilExpiry(): number | undefined {
|
public getMsUntilExpiry(): number | undefined {
|
||||||
if (isLegacyCallMembershipData(this.membershipData)) {
|
// TODO: implement this in a future PR. Something like:
|
||||||
// Assume that local clock is sufficiently in sync with other clocks in the distributed system.
|
// return this.getAbsoluteExpiry() - Date.now();
|
||||||
// We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate.
|
|
||||||
// The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2
|
|
||||||
return this.getAbsoluteExpiry()! - Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumed to be MSC4143
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,29 +176,20 @@ export class CallMembership {
|
|||||||
* @returns true if the membership has expired, otherwise false
|
* @returns true if the membership has expired, otherwise false
|
||||||
*/
|
*/
|
||||||
public isExpired(): boolean {
|
public isExpired(): boolean {
|
||||||
if (isLegacyCallMembershipData(this.membershipData)) return this.getMsUntilExpiry()! <= 0;
|
// TODO: implement this in a future PR. Something like:
|
||||||
|
// return this.getMsUntilExpiry() <= 0;
|
||||||
|
|
||||||
// MSC4143 events expire by being updated. So if the event exists, its not expired.
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPreferredFoci(): Focus[] {
|
public getPreferredFoci(): Focus[] {
|
||||||
// To support both, the new and the old MatrixRTC memberships have two cases based
|
|
||||||
// on the availablitiy of `foci_preferred`
|
|
||||||
if (isLegacyCallMembershipData(this.membershipData)) return this.membershipData.foci_active ?? [];
|
|
||||||
|
|
||||||
// MSC4143 style membership
|
|
||||||
return this.membershipData.foci_preferred;
|
return this.membershipData.foci_preferred;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFocusSelection(): string | undefined {
|
public getFocusSelection(): string | undefined {
|
||||||
if (isLegacyCallMembershipData(this.membershipData)) {
|
const focusActive = this.membershipData.focus_active;
|
||||||
return "oldest_membership";
|
if (isLivekitFocusActive(focusActive)) {
|
||||||
} else {
|
return focusActive.focus_selection;
|
||||||
const focusActive = this.membershipData.focus_active;
|
|
||||||
if (isLivekitFocusActive(focusActive)) {
|
|
||||||
return focusActive.focus_selection;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,25 +21,20 @@ import { Room } from "../models/room.ts";
|
|||||||
import { MatrixClient } from "../client.ts";
|
import { MatrixClient } from "../client.ts";
|
||||||
import { EventType } from "../@types/event.ts";
|
import { EventType } from "../@types/event.ts";
|
||||||
import { UpdateDelayedEventAction } from "../@types/requests.ts";
|
import { UpdateDelayedEventAction } from "../@types/requests.ts";
|
||||||
import {
|
import { CallMembership, SessionMembershipData } from "./CallMembership.ts";
|
||||||
CallMembership,
|
|
||||||
CallMembershipData,
|
|
||||||
CallMembershipDataLegacy,
|
|
||||||
SessionMembershipData,
|
|
||||||
isLegacyCallMembershipData,
|
|
||||||
} from "./CallMembership.ts";
|
|
||||||
import { RoomStateEvent } from "../models/room-state.ts";
|
import { RoomStateEvent } from "../models/room-state.ts";
|
||||||
import { Focus } from "./focus.ts";
|
import { Focus } from "./focus.ts";
|
||||||
import { randomString, secureRandomBase64Url } from "../randomstring.ts";
|
import { secureRandomBase64Url } from "../randomstring.ts";
|
||||||
import { EncryptionKeysEventContent } from "./types.ts";
|
import { EncryptionKeysEventContent } from "./types.ts";
|
||||||
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
|
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
|
||||||
import { KnownMembership } from "../@types/membership.ts";
|
import { KnownMembership } from "../@types/membership.ts";
|
||||||
import { HTTPError, MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts";
|
import { HTTPError, MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts";
|
||||||
import { MatrixEvent } from "../models/event.ts";
|
import { MatrixEvent } from "../models/event.ts";
|
||||||
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
||||||
import { ExperimentalGroupCallRoomMemberState } from "../webrtc/groupCall.ts";
|
|
||||||
import { sleep } from "../utils.ts";
|
import { sleep } from "../utils.ts";
|
||||||
|
|
||||||
|
const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4; // 4 hours
|
||||||
|
|
||||||
const logger = rootLogger.getChild("MatrixRTCSession");
|
const logger = rootLogger.getChild("MatrixRTCSession");
|
||||||
|
|
||||||
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
|
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
|
||||||
@ -82,14 +77,6 @@ export interface JoinSessionConfig {
|
|||||||
*/
|
*/
|
||||||
manageMediaKeys?: boolean;
|
manageMediaKeys?: boolean;
|
||||||
|
|
||||||
/** Lets you configure how the events for the session are formatted.
|
|
||||||
* - legacy: use one event with a membership array.
|
|
||||||
* - MSC4143: use one event per membership (with only one membership per event)
|
|
||||||
* More details can be found in MSC4143 and by checking the types:
|
|
||||||
* `CallMembershipDataLegacy` and `SessionMembershipData`
|
|
||||||
*/
|
|
||||||
useLegacyMemberEvents?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The timeout (in milliseconds) after we joined the call, that our membership should expire
|
* The timeout (in milliseconds) after we joined the call, that our membership should expire
|
||||||
* unless we have explicitly updated it.
|
* unless we have explicitly updated it.
|
||||||
@ -161,11 +148,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
private joinConfig?: JoinSessionConfig;
|
private joinConfig?: JoinSessionConfig;
|
||||||
|
|
||||||
private get membershipExpiryTimeout(): number {
|
private get membershipExpiryTimeout(): number {
|
||||||
return this.joinConfig?.membershipExpiryTimeout ?? 60 * 60 * 1000;
|
return this.joinConfig?.membershipExpiryTimeout ?? DEFAULT_EXPIRE_DURATION;
|
||||||
}
|
|
||||||
|
|
||||||
private get memberEventCheckPeriod(): number {
|
|
||||||
return this.joinConfig?.memberEventCheckPeriod ?? 2 * 60 * 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get callMemberEventRetryDelayMinimum(): number {
|
private get callMemberEventRetryDelayMinimum(): number {
|
||||||
@ -206,14 +189,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
return this.joinConfig?.callMemberEventRetryJitter ?? 2_000;
|
return this.joinConfig?.callMemberEventRetryJitter ?? 2_000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// An identifier for our membership of the call. This will allow us to easily recognise
|
|
||||||
// whether a membership was sent by this session or is stale from some other time.
|
|
||||||
// It also forces our membership events to be unique, because otherwise we could try
|
|
||||||
// to overwrite a membership from a previous session but it would do nothing because the
|
|
||||||
// event content would be identical. We need the origin_server_ts to update though, so
|
|
||||||
// forcing unique content fixes this.
|
|
||||||
private membershipId: string | undefined;
|
|
||||||
|
|
||||||
private memberEventTimeout?: ReturnType<typeof setTimeout>;
|
private memberEventTimeout?: ReturnType<typeof setTimeout>;
|
||||||
private expiryTimeout?: ReturnType<typeof setTimeout>;
|
private expiryTimeout?: ReturnType<typeof setTimeout>;
|
||||||
private keysEventUpdateTimeout?: ReturnType<typeof setTimeout>;
|
private keysEventUpdateTimeout?: ReturnType<typeof setTimeout>;
|
||||||
@ -229,7 +204,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
private needCallMembershipUpdate = false;
|
private needCallMembershipUpdate = false;
|
||||||
|
|
||||||
private manageMediaKeys = false;
|
private manageMediaKeys = false;
|
||||||
private useLegacyMemberEvents = true;
|
|
||||||
// userId:deviceId => array of (key, timestamp)
|
// userId:deviceId => array of (key, timestamp)
|
||||||
private encryptionKeys = new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
|
private encryptionKeys = new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
|
||||||
private lastEncryptionKeyUpdateRequest?: number;
|
private lastEncryptionKeyUpdateRequest?: number;
|
||||||
@ -292,19 +266,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
// Dont even bother about empty events (saves us from costly type/"key in" checks in bigger rooms)
|
// Dont even bother about empty events (saves us from costly type/"key in" checks in bigger rooms)
|
||||||
if (eventKeysCount === 0) continue;
|
if (eventKeysCount === 0) continue;
|
||||||
|
|
||||||
let membershipContents: any[] = [];
|
const membershipContents: any[] = [];
|
||||||
|
|
||||||
// We first decide if its a MSC4143 event (per device state key)
|
// We first decide if its a MSC4143 event (per device state key)
|
||||||
if (eventKeysCount > 1 && "focus_active" in content) {
|
if (eventKeysCount > 1 && "focus_active" in content) {
|
||||||
// We have a MSC4143 event membership event
|
// We have a MSC4143 event membership event
|
||||||
membershipContents.push(content);
|
membershipContents.push(content);
|
||||||
} else if (eventKeysCount === 1 && "memberships" in content) {
|
} else if (eventKeysCount === 1 && "memberships" in content) {
|
||||||
// we have a legacy (one event for all devices) event
|
logger.warn(`Legacy event found. Those are ignored, they do not contribute to the MatrixRTC session`);
|
||||||
if (!Array.isArray(content["memberships"])) {
|
|
||||||
logger.warn(`Malformed member event from ${memberEvent.getSender()}: memberships is not an array`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
membershipContents = content["memberships"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membershipContents.length === 0) continue;
|
if (membershipContents.length === 0) continue;
|
||||||
@ -416,8 +385,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
this.joinConfig = joinConfig;
|
this.joinConfig = joinConfig;
|
||||||
this.relativeExpiry = this.membershipExpiryTimeout;
|
this.relativeExpiry = this.membershipExpiryTimeout;
|
||||||
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
|
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
|
||||||
this.useLegacyMemberEvents = joinConfig?.useLegacyMemberEvents ?? this.useLegacyMemberEvents;
|
|
||||||
this.membershipId = randomString(5);
|
|
||||||
|
|
||||||
logger.info(`Joining call session in room ${this.room.roomId} with manageMediaKeys=${this.manageMediaKeys}`);
|
logger.info(`Joining call session in room ${this.room.roomId} with manageMediaKeys=${this.manageMediaKeys}`);
|
||||||
if (joinConfig?.manageMediaKeys) {
|
if (joinConfig?.manageMediaKeys) {
|
||||||
@ -471,7 +438,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
this.relativeExpiry = undefined;
|
this.relativeExpiry = undefined;
|
||||||
this.ownFocusActive = undefined;
|
this.ownFocusActive = undefined;
|
||||||
this.manageMediaKeys = false;
|
this.manageMediaKeys = false;
|
||||||
this.membershipId = undefined;
|
|
||||||
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
|
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
|
||||||
|
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
@ -492,9 +458,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
const oldestMembership = this.getOldestMembership();
|
const oldestMembership = this.getOldestMembership();
|
||||||
return oldestMembership?.getPreferredFoci()[0];
|
return oldestMembership?.getPreferredFoci()[0];
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
if (!this.ownFocusActive) {
|
// We do not understand the membership format (could be legacy). We default to oldestMembership
|
||||||
// we use the legacy call.member events so default to oldest member
|
// Once there are other methods this is a hard error!
|
||||||
const oldestMembership = this.getOldestMembership();
|
const oldestMembership = this.getOldestMembership();
|
||||||
return oldestMembership?.getPreferredFoci()[0];
|
return oldestMembership?.getPreferredFoci()[0];
|
||||||
}
|
}
|
||||||
@ -928,37 +894,10 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
this.lastMembershipFingerprints = new Set(
|
this.lastMembershipFingerprints = new Set(
|
||||||
this.memberships
|
this.memberships
|
||||||
.filter((m) => !this.isMyMembership(m))
|
.filter((m) => !this.isMyMembership(m))
|
||||||
.map((m) => `${getParticipantIdFromMembership(m)}:${m.membershipID}:${m.createdTs()}`),
|
.map((m) => `${getParticipantIdFromMembership(m)}:${m.createdTs()}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs our own membership
|
|
||||||
* @param prevMembership - The previous value of our call membership, if any
|
|
||||||
*/
|
|
||||||
private makeMyMembershipLegacy(deviceId: string, prevMembership?: CallMembership): CallMembershipDataLegacy {
|
|
||||||
if (this.relativeExpiry === undefined) {
|
|
||||||
throw new Error("Tried to create our own membership event when we're not joined!");
|
|
||||||
}
|
|
||||||
if (this.membershipId === undefined) {
|
|
||||||
throw new Error("Tried to create our own membership event when we have no membership ID!");
|
|
||||||
}
|
|
||||||
const createdTs = prevMembership?.createdTs();
|
|
||||||
return {
|
|
||||||
call_id: "",
|
|
||||||
scope: "m.room",
|
|
||||||
application: "m.call",
|
|
||||||
device_id: deviceId,
|
|
||||||
expires: this.relativeExpiry,
|
|
||||||
// TODO: Date.now() should be the origin_server_ts (now).
|
|
||||||
expires_ts: this.relativeExpiry + (createdTs ?? Date.now()),
|
|
||||||
// we use the fociPreferred since this is the list of foci.
|
|
||||||
// it is named wrong in the Legacy events.
|
|
||||||
foci_active: this.ownFociPreferred,
|
|
||||||
membershipID: this.membershipId,
|
|
||||||
...(createdTs ? { created_ts: createdTs } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Constructs our own membership
|
* Constructs our own membership
|
||||||
*/
|
*/
|
||||||
@ -968,36 +907,12 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
scope: "m.room",
|
scope: "m.room",
|
||||||
application: "m.call",
|
application: "m.call",
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
|
expires: this.relativeExpiry,
|
||||||
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
||||||
foci_preferred: this.ownFociPreferred ?? [],
|
foci_preferred: this.ownFociPreferred ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if our membership event needs to be updated
|
|
||||||
*/
|
|
||||||
private membershipEventNeedsUpdate(
|
|
||||||
myPrevMembershipData?: CallMembershipData,
|
|
||||||
myPrevMembership?: CallMembership,
|
|
||||||
): boolean {
|
|
||||||
if (myPrevMembership && myPrevMembership.getMsUntilExpiry() === undefined) return false;
|
|
||||||
|
|
||||||
// Need to update if there's a membership for us but we're not joined (valid or otherwise)
|
|
||||||
if (!this.isJoined()) return !!myPrevMembershipData;
|
|
||||||
|
|
||||||
// ...or if we are joined, but there's no valid membership event
|
|
||||||
if (!myPrevMembership) return true;
|
|
||||||
|
|
||||||
const expiryTime = myPrevMembership.getMsUntilExpiry();
|
|
||||||
if (expiryTime !== undefined && expiryTime < this.membershipExpiryTimeout / 2) {
|
|
||||||
// ...or if the expiry time needs bumping
|
|
||||||
this.relativeExpiry! += this.membershipExpiryTimeout;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private makeNewMembership(deviceId: string): SessionMembershipData | {} {
|
private makeNewMembership(deviceId: string): SessionMembershipData | {} {
|
||||||
// If we're joined, add our own
|
// If we're joined, add our own
|
||||||
if (this.isJoined()) {
|
if (this.isJoined()) {
|
||||||
@ -1005,49 +920,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Makes a new membership list given the old list along with this user's previous membership event
|
|
||||||
* (if any) and this device's previous membership (if any)
|
|
||||||
*/
|
|
||||||
private makeNewLegacyMemberships(
|
|
||||||
oldMemberships: CallMembershipData[],
|
|
||||||
localDeviceId: string,
|
|
||||||
myCallMemberEvent?: MatrixEvent,
|
|
||||||
myPrevMembership?: CallMembership,
|
|
||||||
): ExperimentalGroupCallRoomMemberState {
|
|
||||||
const filterExpired = (m: CallMembershipData): boolean => {
|
|
||||||
let membershipObj;
|
|
||||||
try {
|
|
||||||
membershipObj = new CallMembership(myCallMemberEvent!, m);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !membershipObj.isExpired();
|
|
||||||
};
|
|
||||||
|
|
||||||
const transformMemberships = (m: CallMembershipData): CallMembershipData => {
|
|
||||||
if (m.created_ts === undefined) {
|
|
||||||
// we need to fill this in with the origin_server_ts from its original event
|
|
||||||
m.created_ts = myCallMemberEvent!.getTs();
|
|
||||||
}
|
|
||||||
|
|
||||||
return m;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter our any invalid or expired memberships, and also our own - we'll add that back in next
|
|
||||||
let newMemberships = oldMemberships.filter(filterExpired).filter((m) => m.device_id !== localDeviceId);
|
|
||||||
|
|
||||||
// Fix up any memberships that need their created_ts adding
|
|
||||||
newMemberships = newMemberships.map(transformMemberships);
|
|
||||||
|
|
||||||
// If we're joined, add our own
|
|
||||||
if (this.isJoined()) {
|
|
||||||
newMemberships.push(this.makeMyMembershipLegacy(localDeviceId, myPrevMembership));
|
|
||||||
}
|
|
||||||
|
|
||||||
return { memberships: newMemberships };
|
|
||||||
}
|
|
||||||
|
|
||||||
private triggerCallMembershipEventUpdate = async (): Promise<void> => {
|
private triggerCallMembershipEventUpdate = async (): Promise<void> => {
|
||||||
// TODO: Should this await on a shared promise?
|
// TODO: Should this await on a shared promise?
|
||||||
@ -1081,64 +953,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
const localDeviceId = this.client.getDeviceId();
|
const localDeviceId = this.client.getDeviceId();
|
||||||
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
|
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
|
||||||
|
|
||||||
const callMemberEvents = roomState.events.get(EventType.GroupCallMemberPrefix);
|
let newContent: {} | SessionMembershipData = {};
|
||||||
const legacy = this.stateEventsContainOngoingLegacySession(callMemberEvents);
|
// TODO: implement expiry logic to MSC4143 events
|
||||||
let newContent: {} | ExperimentalGroupCallRoomMemberState | SessionMembershipData = {};
|
// previously we checked here if the event is timed out and scheduled a check if not.
|
||||||
if (legacy) {
|
// maybe there is a better way.
|
||||||
const myCallMemberEvent = callMemberEvents?.get(localUserId);
|
newContent = this.makeNewMembership(localDeviceId);
|
||||||
const content = myCallMemberEvent?.getContent() ?? {};
|
|
||||||
let myPrevMembership: CallMembership | undefined;
|
|
||||||
// We know its CallMembershipDataLegacy
|
|
||||||
const memberships: CallMembershipDataLegacy[] = Array.isArray(content["memberships"])
|
|
||||||
? content["memberships"]
|
|
||||||
: [];
|
|
||||||
const myPrevMembershipData = memberships.find((m) => m.device_id === localDeviceId);
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
myCallMemberEvent &&
|
|
||||||
myPrevMembershipData &&
|
|
||||||
isLegacyCallMembershipData(myPrevMembershipData) &&
|
|
||||||
myPrevMembershipData.membershipID === this.membershipId
|
|
||||||
) {
|
|
||||||
myPrevMembership = new CallMembership(myCallMemberEvent, myPrevMembershipData);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// This would indicate a bug or something weird if our own call membership
|
|
||||||
// wasn't valid
|
|
||||||
logger.warn("Our previous call membership was invalid - this shouldn't happen.", e);
|
|
||||||
}
|
|
||||||
if (myPrevMembership) {
|
|
||||||
logger.debug(`${myPrevMembership.getMsUntilExpiry()} until our membership expires`);
|
|
||||||
}
|
|
||||||
if (!this.membershipEventNeedsUpdate(myPrevMembershipData, myPrevMembership)) {
|
|
||||||
// nothing to do - reschedule the check again
|
|
||||||
this.memberEventTimeout = setTimeout(
|
|
||||||
this.triggerCallMembershipEventUpdate,
|
|
||||||
this.memberEventCheckPeriod,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
newContent = this.makeNewLegacyMemberships(memberships, localDeviceId, myCallMemberEvent, myPrevMembership);
|
|
||||||
} else {
|
|
||||||
newContent = this.makeNewMembership(localDeviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (legacy) {
|
if (this.isJoined()) {
|
||||||
await this.client.sendStateEvent(
|
|
||||||
this.room.roomId,
|
|
||||||
EventType.GroupCallMemberPrefix,
|
|
||||||
newContent,
|
|
||||||
localUserId,
|
|
||||||
);
|
|
||||||
if (this.isJoined()) {
|
|
||||||
// check periodically to see if we need to refresh our member event
|
|
||||||
this.memberEventTimeout = setTimeout(
|
|
||||||
this.triggerCallMembershipEventUpdate,
|
|
||||||
this.memberEventCheckPeriod,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (this.isJoined()) {
|
|
||||||
const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId);
|
const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId);
|
||||||
const prepareDelayedDisconnection = async (): Promise<void> => {
|
const prepareDelayedDisconnection = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@ -1203,6 +1025,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
this.scheduleDelayDisconnection();
|
this.scheduleDelayDisconnection();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Not joined
|
||||||
let sentDelayedDisconnect = false;
|
let sentDelayedDisconnect = false;
|
||||||
if (this.disconnectDelayId !== undefined) {
|
if (this.disconnectDelayId !== undefined) {
|
||||||
try {
|
try {
|
||||||
@ -1255,29 +1078,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private stateEventsContainOngoingLegacySession(callMemberEvents: Map<string, MatrixEvent> | undefined): boolean {
|
|
||||||
if (!callMemberEvents?.size) {
|
|
||||||
return this.useLegacyMemberEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
let containsAnyOngoingSession = false;
|
|
||||||
let containsUnknownOngoingSession = false;
|
|
||||||
for (const callMemberEvent of callMemberEvents.values()) {
|
|
||||||
const content = callMemberEvent.getContent();
|
|
||||||
if (Array.isArray(content["memberships"])) {
|
|
||||||
for (const membership of content.memberships) {
|
|
||||||
if (!new CallMembership(callMemberEvent, membership).isExpired()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (Object.keys(content).length > 0) {
|
|
||||||
containsAnyOngoingSession ||= true;
|
|
||||||
containsUnknownOngoingSession ||= !("focus_active" in content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return containsAnyOngoingSession && !containsUnknownOngoingSession ? false : this.useLegacyMemberEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
|
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
|
||||||
const stateKey = `${localUserId}_${localDeviceId}`;
|
const stateKey = `${localUserId}_${localDeviceId}`;
|
||||||
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
|
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
|
||||||
|
@ -35,7 +35,6 @@ import {
|
|||||||
import { SummaryStatsReportGatherer } from "./stats/summaryStatsReportGatherer.ts";
|
import { SummaryStatsReportGatherer } from "./stats/summaryStatsReportGatherer.ts";
|
||||||
import { CallFeedStatsReporter } from "./stats/callFeedStatsReporter.ts";
|
import { CallFeedStatsReporter } from "./stats/callFeedStatsReporter.ts";
|
||||||
import { KnownMembership } from "../@types/membership.ts";
|
import { KnownMembership } from "../@types/membership.ts";
|
||||||
import { CallMembershipData } from "../matrixrtc/CallMembership.ts";
|
|
||||||
|
|
||||||
export enum GroupCallIntent {
|
export enum GroupCallIntent {
|
||||||
Ring = "m.ring",
|
Ring = "m.ring",
|
||||||
@ -198,11 +197,6 @@ export interface IGroupCallRoomMemberState {
|
|||||||
"m.calls": IGroupCallRoomMemberCallState[];
|
"m.calls": IGroupCallRoomMemberCallState[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: this hasn't made it into the MSC yet
|
|
||||||
export interface ExperimentalGroupCallRoomMemberState {
|
|
||||||
memberships: CallMembershipData[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum GroupCallState {
|
export enum GroupCallState {
|
||||||
LocalCallFeedUninitialized = "local_call_feed_uninitialized",
|
LocalCallFeedUninitialized = "local_call_feed_uninitialized",
|
||||||
InitializingLocalCallFeed = "initializing_local_call_feed",
|
InitializingLocalCallFeed = "initializing_local_call_feed",
|
||||||
|
Reference in New Issue
Block a user