You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-23 17:02:25 +03:00
[MatrixRTC] Sticky Events support (MSC4354) (#5017)
* Implement Sticky Events MSC * Renames * lint * some review work * Update for support for 4-ples * fix lint * pull through method * Fix the mistake * More tests to appease SC * Cleaner code * Review cleanup * Refactors based on review. * lint * Add sticky event support to the js-sdk Signed-off-by: Timo K <toger5@hotmail.de> * use sticky events for matrixRTC Signed-off-by: Timo K <toger5@hotmail.de> * make sticky events a non breaking change (default to state events. use joinConfig to use sticky events) Signed-off-by: Timo K <toger5@hotmail.de> * review - fix types (`msc4354_sticky:number` -> `msc4354_sticky?: { duration_ms: number };`) - add `MultiKeyMap` Signed-off-by: Timo K <toger5@hotmail.de> * Refactor all of this away to it's own accumulator and class. * Add tests * tidyup * more test cleaning * lint * Updates and tests * fix filter * fix filter with lint * Add timer tests * Add tests for MatrixRTCSessionManager * Listen for sticky events on MatrixRTCSessionManager * fix logic on filtering out state events * lint * more lint * tweaks * Add logging in areas * more debugging * much more logging * remove more logging * Finish supporting new MSC * a line * reconnect the bits to RTC * fixup more bits * fixup testrs * Ensure consistent order * lint * fix log line * remove extra bit of code * revert changes to room-sticky-events.ts * fixup mocks again * lint * fix * cleanup * fix paths * tweak test * fixup * Add more tests for coverage * Small improvements Signed-off-by: Timo K <toger5@hotmail.de> * review Signed-off-by: Timo K <toger5@hotmail.de> * Document better * fix sticky event type Signed-off-by: Timo K <toger5@hotmail.de> * fix demo Signed-off-by: Timo K <toger5@hotmail.de> * fix tests Signed-off-by: Timo K <toger5@hotmail.de> * Update src/matrixrtc/CallMembership.ts Co-authored-by: Robin <robin@robin.town> * cleanup * lint * fix ci Signed-off-by: Timo K <toger5@hotmail.de> --------- Signed-off-by: Timo K <toger5@hotmail.de> Co-authored-by: Half-Shot <will@half-shot.uk> Co-authored-by: Robin <robin@robin.town>
This commit is contained in:
@@ -14,13 +14,30 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { encodeBase64, EventType, MatrixClient, type MatrixError, type MatrixEvent, type Room } from "../../../src";
|
import {
|
||||||
|
encodeBase64,
|
||||||
|
type EventTimeline,
|
||||||
|
EventType,
|
||||||
|
MatrixClient,
|
||||||
|
type MatrixError,
|
||||||
|
type MatrixEvent,
|
||||||
|
type Room,
|
||||||
|
} from "../../../src";
|
||||||
import { KnownMembership } from "../../../src/@types/membership";
|
import { KnownMembership } from "../../../src/@types/membership";
|
||||||
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||||
import { Status, type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
import { Status, type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||||
import { secureRandomString } from "../../../src/randomstring";
|
import {
|
||||||
import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey, type MembershipData, mockRoomState } from "./mocks";
|
makeMockEvent,
|
||||||
|
makeMockRoom,
|
||||||
|
membershipTemplate,
|
||||||
|
makeKey,
|
||||||
|
type MembershipData,
|
||||||
|
mockRoomState,
|
||||||
|
mockRTCEvent,
|
||||||
|
} from "./mocks";
|
||||||
import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts";
|
import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts";
|
||||||
|
import { RoomStickyEventsEvent, type StickyMatrixEvent } from "../../../src/models/room-sticky-events.ts";
|
||||||
|
import { StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
|
||||||
|
|
||||||
const mockFocus = { type: "mock" };
|
const mockFocus = { type: "mock" };
|
||||||
|
|
||||||
@@ -47,11 +64,63 @@ describe("MatrixRTCSession", () => {
|
|||||||
sess = undefined;
|
sess = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("roomSessionForRoom", () => {
|
describe.each([
|
||||||
it("creates a room-scoped session from room state", () => {
|
{
|
||||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
listenForStickyEvents: true,
|
||||||
|
listenForMemberStateEvents: true,
|
||||||
|
testCreateSticky: false,
|
||||||
|
createWithDefaults: true, // Create MatrixRTCSession with defaults
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listenForStickyEvents: true,
|
||||||
|
listenForMemberStateEvents: true,
|
||||||
|
testCreateSticky: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listenForStickyEvents: false,
|
||||||
|
listenForMemberStateEvents: true,
|
||||||
|
testCreateSticky: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listenForStickyEvents: true,
|
||||||
|
listenForMemberStateEvents: true,
|
||||||
|
testCreateSticky: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listenForStickyEvents: true,
|
||||||
|
listenForMemberStateEvents: false,
|
||||||
|
testCreateSticky: true,
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"roomSessionForRoom listenForSticky=$listenForStickyEvents listenForMemberStateEvents=$listenForMemberStateEvents testCreateSticky=$testCreateSticky",
|
||||||
|
(testConfig) => {
|
||||||
|
it(`will ${testConfig.listenForMemberStateEvents ? "" : "NOT"} throw if the room does not have any state stored`, () => {
|
||||||
|
const mockRoom = makeMockRoom([membershipTemplate], testConfig.testCreateSticky);
|
||||||
|
mockRoom.getLiveTimeline.mockReturnValue({
|
||||||
|
getState: jest.fn().mockReturnValue(undefined),
|
||||||
|
} as unknown as EventTimeline);
|
||||||
|
if (testConfig.listenForMemberStateEvents) {
|
||||||
|
// eslint-disable-next-line jest/no-conditional-expect
|
||||||
|
expect(() => {
|
||||||
|
MatrixRTCSession.sessionForSlot(client, mockRoom, callSession, testConfig);
|
||||||
|
}).toThrow();
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line jest/no-conditional-expect
|
||||||
|
expect(() => {
|
||||||
|
MatrixRTCSession.sessionForSlot(client, mockRoom, callSession, testConfig);
|
||||||
|
}).not.toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
it("creates a room-scoped session from room state", () => {
|
||||||
|
const mockRoom = makeMockRoom([membershipTemplate], testConfig.testCreateSticky);
|
||||||
|
|
||||||
|
sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess?.memberships.length).toEqual(1);
|
expect(sess?.memberships.length).toEqual(1);
|
||||||
expect(sess?.memberships[0].slotDescription.id).toEqual("");
|
expect(sess?.memberships[0].slotDescription.id).toEqual("");
|
||||||
expect(sess?.memberships[0].scope).toEqual("m.room");
|
expect(sess?.memberships[0].scope).toEqual("m.room");
|
||||||
@@ -65,8 +134,13 @@ describe("MatrixRTCSession", () => {
|
|||||||
const testMembership = Object.assign({}, membershipTemplate, {
|
const testMembership = Object.assign({}, membershipTemplate, {
|
||||||
application: "not-m.call",
|
application: "not-m.call",
|
||||||
});
|
});
|
||||||
const mockRoom = makeMockRoom([testMembership]);
|
const mockRoom = makeMockRoom([testMembership], testConfig.testCreateSticky);
|
||||||
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
const sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess?.memberships).toHaveLength(0);
|
expect(sess?.memberships).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,8 +149,13 @@ describe("MatrixRTCSession", () => {
|
|||||||
call_id: "not-empty",
|
call_id: "not-empty",
|
||||||
scope: "m.room",
|
scope: "m.room",
|
||||||
});
|
});
|
||||||
const mockRoom = makeMockRoom([testMembership]);
|
const mockRoom = makeMockRoom([testMembership], testConfig.testCreateSticky);
|
||||||
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
const sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess?.memberships).toHaveLength(0);
|
expect(sess?.memberships).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,19 +164,42 @@ describe("MatrixRTCSession", () => {
|
|||||||
const expiredMembership = Object.assign({}, membershipTemplate);
|
const expiredMembership = Object.assign({}, membershipTemplate);
|
||||||
expiredMembership.expires = 1000;
|
expiredMembership.expires = 1000;
|
||||||
expiredMembership.device_id = "EXPIRED";
|
expiredMembership.device_id = "EXPIRED";
|
||||||
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
|
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], testConfig.testCreateSticky);
|
||||||
|
|
||||||
jest.advanceTimersByTime(2000);
|
jest.advanceTimersByTime(2000);
|
||||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
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], testConfig.testCreateSticky);
|
||||||
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
|
mockRoom.hasMembershipState.mockImplementation((state) => state === KnownMembership.Join);
|
||||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
|
expect(sess?.memberships.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores memberships events with no sender", () => {
|
||||||
|
// Force the sender to be undefined.
|
||||||
|
const mockRoom = makeMockRoom([{ ...membershipTemplate, user_id: "" }], testConfig.testCreateSticky);
|
||||||
|
mockRoom.hasMembershipState.mockImplementation((state) => state === KnownMembership.Join);
|
||||||
|
sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess?.memberships.length).toEqual(0);
|
expect(sess?.memberships.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,20 +209,29 @@ describe("MatrixRTCSession", () => {
|
|||||||
const expiredMembership = Object.assign({}, membershipTemplate);
|
const expiredMembership = Object.assign({}, membershipTemplate);
|
||||||
expiredMembership.created_ts = 500;
|
expiredMembership.created_ts = 500;
|
||||||
expiredMembership.expires = 1000;
|
expiredMembership.expires = 1000;
|
||||||
const mockRoom = makeMockRoom([expiredMembership]);
|
const mockRoom = makeMockRoom([expiredMembership], testConfig.testCreateSticky);
|
||||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
|
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
|
||||||
jest.useRealTimers();
|
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([], testConfig.testCreateSticky);
|
||||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess?.memberships).toHaveLength(0);
|
expect(sess?.memberships).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("safely ignores events with no memberships section", () => {
|
it("safely ignores events with no memberships section", () => {
|
||||||
const roomId = secureRandomString(8);
|
|
||||||
const event = {
|
const event = {
|
||||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||||
getContent: jest.fn().mockReturnValue({}),
|
getContent: jest.fn().mockReturnValue({}),
|
||||||
@@ -128,10 +239,8 @@ describe("MatrixRTCSession", () => {
|
|||||||
getTs: jest.fn().mockReturnValue(1000),
|
getTs: jest.fn().mockReturnValue(1000),
|
||||||
getLocalAge: jest.fn().mockReturnValue(0),
|
getLocalAge: jest.fn().mockReturnValue(0),
|
||||||
};
|
};
|
||||||
const mockRoom = {
|
const mockRoom = makeMockRoom([]);
|
||||||
...makeMockRoom([]),
|
mockRoom.getLiveTimeline.mockReturnValue({
|
||||||
roomId,
|
|
||||||
getLiveTimeline: jest.fn().mockReturnValue({
|
|
||||||
getState: jest.fn().mockReturnValue({
|
getState: jest.fn().mockReturnValue({
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
off: jest.fn(),
|
off: jest.fn(),
|
||||||
@@ -148,14 +257,17 @@ describe("MatrixRTCSession", () => {
|
|||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
}),
|
} as unknown as EventTimeline);
|
||||||
};
|
sess = MatrixRTCSession.sessionForSlot(
|
||||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom as unknown as Room, callSession);
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess.memberships).toHaveLength(0);
|
expect(sess.memberships).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("safely ignores events with junk memberships section", () => {
|
it("safely ignores events with junk memberships section", () => {
|
||||||
const roomId = secureRandomString(8);
|
|
||||||
const event = {
|
const event = {
|
||||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||||
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),
|
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),
|
||||||
@@ -163,10 +275,8 @@ describe("MatrixRTCSession", () => {
|
|||||||
getTs: jest.fn().mockReturnValue(1000),
|
getTs: jest.fn().mockReturnValue(1000),
|
||||||
getLocalAge: jest.fn().mockReturnValue(0),
|
getLocalAge: jest.fn().mockReturnValue(0),
|
||||||
};
|
};
|
||||||
const mockRoom = {
|
const mockRoom = makeMockRoom([]);
|
||||||
...makeMockRoom([]),
|
mockRoom.getLiveTimeline.mockReturnValue({
|
||||||
roomId,
|
|
||||||
getLiveTimeline: jest.fn().mockReturnValue({
|
|
||||||
getState: jest.fn().mockReturnValue({
|
getState: jest.fn().mockReturnValue({
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
off: jest.fn(),
|
off: jest.fn(),
|
||||||
@@ -183,9 +293,13 @@ describe("MatrixRTCSession", () => {
|
|||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
}),
|
} as unknown as EventTimeline);
|
||||||
};
|
sess = MatrixRTCSession.sessionForSlot(
|
||||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom as unknown as Room, callSession);
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess.memberships).toHaveLength(0);
|
expect(sess.memberships).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,7 +307,12 @@ describe("MatrixRTCSession", () => {
|
|||||||
const testMembership = Object.assign({}, membershipTemplate);
|
const testMembership = Object.assign({}, membershipTemplate);
|
||||||
(testMembership.device_id as string | undefined) = undefined;
|
(testMembership.device_id as string | undefined) = undefined;
|
||||||
const mockRoom = makeMockRoom([testMembership]);
|
const mockRoom = makeMockRoom([testMembership]);
|
||||||
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
const sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess.memberships).toHaveLength(0);
|
expect(sess.memberships).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,9 +320,107 @@ describe("MatrixRTCSession", () => {
|
|||||||
const testMembership = Object.assign({}, membershipTemplate);
|
const testMembership = Object.assign({}, membershipTemplate);
|
||||||
(testMembership.call_id as string | undefined) = undefined;
|
(testMembership.call_id as string | undefined) = undefined;
|
||||||
const mockRoom = makeMockRoom([testMembership]);
|
const mockRoom = makeMockRoom([testMembership]);
|
||||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
sess = MatrixRTCSession.sessionForSlot(
|
||||||
|
client,
|
||||||
|
mockRoom,
|
||||||
|
callSession,
|
||||||
|
testConfig.createWithDefaults ? undefined : testConfig,
|
||||||
|
);
|
||||||
expect(sess.memberships).toHaveLength(0);
|
expect(sess.memberships).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("roomSessionForRoom combined state", () => {
|
||||||
|
it("perfers sticky events when both membership and sticky events appear for the same user", () => {
|
||||||
|
// Create a room with identical member state and sticky state for the same user.
|
||||||
|
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||||
|
mockRoom._unstable_getStickyEvents.mockImplementation(() => {
|
||||||
|
const ev = mockRTCEvent(
|
||||||
|
{
|
||||||
|
...membershipTemplate,
|
||||||
|
msc4354_sticky_key: `_${membershipTemplate.user_id}_${membershipTemplate.device_id}`,
|
||||||
|
},
|
||||||
|
mockRoom.roomId,
|
||||||
|
);
|
||||||
|
return [ev as StickyMatrixEvent];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expect for there to be one membership as the state has been merged down.
|
||||||
|
sess = MatrixRTCSession.sessionForSlot(client, mockRoom, callSession, {
|
||||||
|
listenForStickyEvents: true,
|
||||||
|
listenForMemberStateEvents: true,
|
||||||
|
});
|
||||||
|
expect(sess?.memberships.length).toEqual(1);
|
||||||
|
expect(sess?.memberships[0].slotDescription.id).toEqual("");
|
||||||
|
expect(sess?.memberships[0].scope).toEqual("m.room");
|
||||||
|
expect(sess?.memberships[0].application).toEqual("m.call");
|
||||||
|
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||||
|
expect(sess?.memberships[0].isExpired()).toEqual(false);
|
||||||
|
expect(sess?.slotDescription.id).toEqual("");
|
||||||
|
});
|
||||||
|
it("combines sticky and membership events when both exist", () => {
|
||||||
|
// Create a room with identical member state and sticky state for the same user.
|
||||||
|
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||||
|
const stickyUserId = "@stickyev:user.example";
|
||||||
|
mockRoom._unstable_getStickyEvents.mockImplementation(() => {
|
||||||
|
const ev = mockRTCEvent(
|
||||||
|
{
|
||||||
|
...membershipTemplate,
|
||||||
|
user_id: stickyUserId,
|
||||||
|
msc4354_sticky_key: `_${stickyUserId}_${membershipTemplate.device_id}`,
|
||||||
|
},
|
||||||
|
mockRoom.roomId,
|
||||||
|
15000,
|
||||||
|
Date.now() - 1000, // Sticky event comes first.
|
||||||
|
);
|
||||||
|
return [ev as StickyMatrixEvent];
|
||||||
|
});
|
||||||
|
|
||||||
|
sess = MatrixRTCSession.sessionForSlot(client, mockRoom, callSession, {
|
||||||
|
listenForStickyEvents: true,
|
||||||
|
listenForMemberStateEvents: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const memberships = sess.memberships;
|
||||||
|
expect(memberships.length).toEqual(2);
|
||||||
|
expect(memberships[0].sender).toEqual(stickyUserId);
|
||||||
|
expect(memberships[0].slotDescription.id).toEqual("");
|
||||||
|
expect(memberships[0].scope).toEqual("m.room");
|
||||||
|
expect(memberships[0].application).toEqual("m.call");
|
||||||
|
expect(memberships[0].deviceId).toEqual("AAAAAAA");
|
||||||
|
expect(memberships[0].isExpired()).toEqual(false);
|
||||||
|
|
||||||
|
// Then state
|
||||||
|
expect(memberships[1].sender).toEqual(membershipTemplate.user_id);
|
||||||
|
|
||||||
|
expect(sess?.slotDescription.id).toEqual("");
|
||||||
|
});
|
||||||
|
it("handles an incoming sticky event to an existing session", () => {
|
||||||
|
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||||
|
const stickyUserId = "@stickyev:user.example";
|
||||||
|
|
||||||
|
sess = MatrixRTCSession.sessionForSlot(client, mockRoom, callSession, {
|
||||||
|
listenForStickyEvents: true,
|
||||||
|
listenForMemberStateEvents: true,
|
||||||
|
});
|
||||||
|
expect(sess.memberships.length).toEqual(1);
|
||||||
|
const stickyEv = mockRTCEvent(
|
||||||
|
{
|
||||||
|
...membershipTemplate,
|
||||||
|
user_id: stickyUserId,
|
||||||
|
msc4354_sticky_key: `_${stickyUserId}_${membershipTemplate.device_id}`,
|
||||||
|
},
|
||||||
|
mockRoom.roomId,
|
||||||
|
15000,
|
||||||
|
Date.now() - 1000, // Sticky event comes first.
|
||||||
|
) as StickyMatrixEvent;
|
||||||
|
mockRoom._unstable_getStickyEvents.mockImplementation(() => {
|
||||||
|
return [stickyEv];
|
||||||
|
});
|
||||||
|
mockRoom.emit(RoomStickyEventsEvent.Update, [stickyEv], [], []);
|
||||||
|
expect(sess.memberships.length).toEqual(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getOldestMembership", () => {
|
describe("getOldestMembership", () => {
|
||||||
@@ -329,6 +546,12 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(sess!.isJoined()).toEqual(true);
|
expect(sess!.isJoined()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the sticky events membership manager implementation", () => {
|
||||||
|
sess!.joinRoomSession([mockFocus], mockFocus, { unstableSendStickyEvents: true });
|
||||||
|
expect(sess!.isJoined()).toEqual(true);
|
||||||
|
expect(sess!["membershipManager"] instanceof StickyEventMembershipManager).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("sends a notification when starting a call and emit DidSendCallNotification", async () => {
|
it("sends a notification when starting a call and emit DidSendCallNotification", async () => {
|
||||||
// Simulate a join, including the update to the room state
|
// Simulate a join, including the update to the room state
|
||||||
// Ensure sendEvent returns event IDs so the DidSendCallNotification payload includes them
|
// Ensure sendEvent returns event IDs so the DidSendCallNotification payload includes them
|
||||||
|
|||||||
@@ -14,15 +14,29 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
|
import { ClientEvent, EventTimeline, MatrixClient, type Room } from "../../../src";
|
||||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
import { RoomStateEvent } from "../../../src/models/room-state";
|
||||||
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
||||||
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks";
|
import { makeMockRoom, type MembershipData, membershipTemplate, mockRoomState, mockRTCEvent } from "./mocks";
|
||||||
import { logger } from "../../../src/logger";
|
import { logger } from "../../../src/logger";
|
||||||
|
|
||||||
describe("MatrixRTCSessionManager", () => {
|
describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
|
||||||
|
"MatrixRTCSessionManager ($eventKind)",
|
||||||
|
({ eventKind }) => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
|
|
||||||
|
function sendLeaveMembership(room: Room, membershipData: MembershipData[]): void {
|
||||||
|
if (eventKind === "memberState") {
|
||||||
|
mockRoomState(room, [{ user_id: membershipTemplate.user_id }]);
|
||||||
|
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||||
|
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||||
|
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
||||||
|
} else {
|
||||||
|
membershipData.splice(0, 1, { user_id: membershipTemplate.user_id });
|
||||||
|
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client = new MatrixClient({ baseUrl: "base_url" });
|
client = new MatrixClient({ baseUrl: "base_url" });
|
||||||
client.matrixRTC.start();
|
client.matrixRTC.start();
|
||||||
@@ -38,7 +52,7 @@ describe("MatrixRTCSessionManager", () => {
|
|||||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const room1 = makeMockRoom([membershipTemplate]);
|
const room1 = makeMockRoom([membershipTemplate], eventKind === "sticky");
|
||||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
|
|
||||||
client.emit(ClientEvent.Room, room1);
|
client.emit(ClientEvent.Room, room1);
|
||||||
@@ -53,7 +67,7 @@ describe("MatrixRTCSessionManager", () => {
|
|||||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
|
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }], eventKind === "sticky");
|
||||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
|
|
||||||
client.emit(ClientEvent.Room, room1);
|
client.emit(ClientEvent.Room, room1);
|
||||||
@@ -66,26 +80,25 @@ 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 membershipData: MembershipData[] = [membershipTemplate];
|
||||||
|
const room1 = makeMockRoom(membershipData, eventKind === "sticky");
|
||||||
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);
|
||||||
|
|
||||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
sendLeaveMembership(room1, membershipData);
|
||||||
|
|
||||||
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
|
||||||
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
|
||||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
|
||||||
|
|
||||||
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Fires correctly with for with custom sessionDescription", () => {
|
it("Fires correctly with custom sessionDescription", () => {
|
||||||
const onStarted = jest.fn();
|
const onStarted = jest.fn();
|
||||||
const onEnded = jest.fn();
|
const onEnded = jest.fn();
|
||||||
// create a session manager with a custom session description
|
// create a session manager with a custom session description
|
||||||
const sessionManager = new MatrixRTCSessionManager(logger, client, { id: "test", application: "m.notCall" });
|
const sessionManager = new MatrixRTCSessionManager(logger, client, {
|
||||||
|
id: "test",
|
||||||
|
application: "m.notCall",
|
||||||
|
});
|
||||||
|
|
||||||
// manually start the session manager (its not the default one started by the client)
|
// manually start the session manager (its not the default one started by the client)
|
||||||
sessionManager.start();
|
sessionManager.start();
|
||||||
@@ -93,35 +106,33 @@ describe("MatrixRTCSessionManager", () => {
|
|||||||
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
|
// Create a session for applicaation m.other, we ignore this session ecause it lacks a call_id
|
||||||
|
const room1MembershipData: MembershipData[] = [{ ...membershipTemplate, application: "m.other" }];
|
||||||
|
const room1 = makeMockRoom(room1MembershipData, eventKind === "sticky");
|
||||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
|
|
||||||
client.emit(ClientEvent.Room, room1);
|
client.emit(ClientEvent.Room, room1);
|
||||||
expect(onStarted).not.toHaveBeenCalled();
|
expect(onStarted).not.toHaveBeenCalled();
|
||||||
onStarted.mockClear();
|
onStarted.mockClear();
|
||||||
|
|
||||||
const room2 = makeMockRoom([{ ...membershipTemplate, application: "m.notCall", call_id: "test" }]);
|
// Create a session for applicaation m.notCall. We expect this call to be tracked because it has a call_id
|
||||||
|
const room2MembershipData: MembershipData[] = [
|
||||||
|
{ ...membershipTemplate, application: "m.notCall", call_id: "test" },
|
||||||
|
];
|
||||||
|
const room2 = makeMockRoom(room2MembershipData, eventKind === "sticky");
|
||||||
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
|
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
|
||||||
|
|
||||||
client.emit(ClientEvent.Room, room2);
|
client.emit(ClientEvent.Room, room2);
|
||||||
expect(onStarted).toHaveBeenCalled();
|
expect(onStarted).toHaveBeenCalled();
|
||||||
onStarted.mockClear();
|
onStarted.mockClear();
|
||||||
|
|
||||||
mockRoomState(room2, [{ user_id: membershipTemplate.user_id }]);
|
// Stop room1's RTC session. Tracked.
|
||||||
jest.spyOn(client, "getRoom").mockReturnValue(room2);
|
jest.spyOn(client, "getRoom").mockReturnValue(room2);
|
||||||
|
sendLeaveMembership(room2, room2MembershipData);
|
||||||
const roomState = room2.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
|
||||||
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
|
||||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
|
||||||
expect(onEnded).toHaveBeenCalled();
|
expect(onEnded).toHaveBeenCalled();
|
||||||
onEnded.mockClear();
|
onEnded.mockClear();
|
||||||
|
|
||||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
// Stop room1's RTC session. Not tracked.
|
||||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||||
|
sendLeaveMembership(room1, room1MembershipData);
|
||||||
const roomStateOther = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
|
||||||
const membEventOther = roomStateOther.getStateEvents("org.matrix.msc3401.call.member")[0];
|
|
||||||
client.emit(RoomStateEvent.Events, membEventOther, roomStateOther, null);
|
|
||||||
expect(onEnded).not.toHaveBeenCalled();
|
expect(onEnded).not.toHaveBeenCalled();
|
||||||
} finally {
|
} finally {
|
||||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||||
@@ -132,18 +143,16 @@ describe("MatrixRTCSessionManager", () => {
|
|||||||
it("Doesn't fire event if unrelated sessions ends", () => {
|
it("Doesn't fire event if unrelated sessions 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, application: "m.other_app" }]);
|
const membership: MembershipData[] = [{ ...membershipTemplate, application: "m.other_app" }];
|
||||||
|
const room1 = makeMockRoom(membership, eventKind === "sticky");
|
||||||
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);
|
||||||
|
|
||||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
sendLeaveMembership(room1, membership);
|
||||||
|
|
||||||
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
|
||||||
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
|
||||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
|
||||||
|
|
||||||
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
MatrixError,
|
MatrixError,
|
||||||
UnsupportedDelayedEventsEndpointError,
|
UnsupportedDelayedEventsEndpointError,
|
||||||
type Room,
|
type Room,
|
||||||
|
MAX_STICKY_DURATION_MS,
|
||||||
} from "../../../src";
|
} from "../../../src";
|
||||||
import {
|
import {
|
||||||
MembershipManagerEvent,
|
MembershipManagerEvent,
|
||||||
@@ -32,7 +33,7 @@ import {
|
|||||||
type LivekitFocusSelection,
|
type LivekitFocusSelection,
|
||||||
} from "../../../src/matrixrtc";
|
} from "../../../src/matrixrtc";
|
||||||
import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
|
import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
|
||||||
import { MembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
|
import { MembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a promise that will resolve once a mocked method is called.
|
* Create a promise that will resolve once a mocked method is called.
|
||||||
@@ -93,7 +94,9 @@ describe("MembershipManager", () => {
|
|||||||
// Provide a default mock that is like the default "non error" server behaviour.
|
// Provide a default mock that is like the default "non error" server behaviour.
|
||||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||||
(client.sendStateEvent as Mock<any>).mockResolvedValue(undefined);
|
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
|
||||||
|
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||||
|
(client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -151,43 +154,6 @@ describe("MembershipManager", () => {
|
|||||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends a rtc membership event when using `useRtcMemberFormat`", async () => {
|
|
||||||
// Spys/Mocks
|
|
||||||
|
|
||||||
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
|
|
||||||
|
|
||||||
// Test
|
|
||||||
const memberManager = new MembershipManager({ useRtcMemberFormat: true }, room, client, callSession);
|
|
||||||
memberManager.join([], focus);
|
|
||||||
// expects
|
|
||||||
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
|
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
|
||||||
room.roomId,
|
|
||||||
"org.matrix.msc4143.rtc.member",
|
|
||||||
{
|
|
||||||
application: { type: "m.call" },
|
|
||||||
member: {
|
|
||||||
user_id: "@alice:example.org",
|
|
||||||
id: "_@alice:example.org_AAAAAAA_m.call",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
},
|
|
||||||
slot_id: "m.call#",
|
|
||||||
rtc_transports: [focus],
|
|
||||||
versions: [],
|
|
||||||
},
|
|
||||||
"_@alice:example.org_AAAAAAA_m.call",
|
|
||||||
);
|
|
||||||
updateDelayedEventHandle.resolve?.();
|
|
||||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
|
||||||
room.roomId,
|
|
||||||
{ delay: 8000 },
|
|
||||||
"org.matrix.msc4143.rtc.member",
|
|
||||||
{},
|
|
||||||
"_@alice:example.org_AAAAAAA_m.call",
|
|
||||||
);
|
|
||||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reschedules delayed leave event if sending state cancels it", async () => {
|
it("reschedules delayed leave event if sending state cancels it", async () => {
|
||||||
const memberManager = new MembershipManager(undefined, room, client, callSession);
|
const memberManager = new MembershipManager(undefined, room, client, callSession);
|
||||||
const waitForSendState = waitForMockCall(client.sendStateEvent);
|
const waitForSendState = waitForMockCall(client.sendStateEvent);
|
||||||
@@ -921,6 +887,63 @@ describe("MembershipManager", () => {
|
|||||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(0);
|
expect(client.sendStateEvent).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("StickyEventMembershipManager", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Provide a default mock that is like the default "non error" server behaviour.
|
||||||
|
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||||
|
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("join()", () => {
|
||||||
|
describe("sends an rtc membership event", () => {
|
||||||
|
it("sends a membership event and schedules delayed leave when joining a call", async () => {
|
||||||
|
const updateDelayedEventHandle = createAsyncHandle<void>(
|
||||||
|
client._unstable_updateDelayedEvent as Mock,
|
||||||
|
);
|
||||||
|
const memberManager = new StickyEventMembershipManager(undefined, room, client, callSession);
|
||||||
|
|
||||||
|
memberManager.join([], focus);
|
||||||
|
|
||||||
|
await waitForMockCall(client._unstable_sendStickyEvent, Promise.resolve({ event_id: "id" }));
|
||||||
|
// Test we sent the initial join
|
||||||
|
expect(client._unstable_sendStickyEvent).toHaveBeenCalledWith(
|
||||||
|
room.roomId,
|
||||||
|
3600000,
|
||||||
|
null,
|
||||||
|
"org.matrix.msc4143.rtc.member",
|
||||||
|
{
|
||||||
|
application: { type: "m.call" },
|
||||||
|
member: {
|
||||||
|
user_id: "@alice:example.org",
|
||||||
|
id: "_@alice:example.org_AAAAAAA_m.call",
|
||||||
|
device_id: "AAAAAAA",
|
||||||
|
},
|
||||||
|
slot_id: "m.call#",
|
||||||
|
rtc_transports: [focus],
|
||||||
|
versions: [],
|
||||||
|
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
updateDelayedEventHandle.resolve?.();
|
||||||
|
|
||||||
|
// Ensure we have sent the delayed disconnect event.
|
||||||
|
expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledWith(
|
||||||
|
room.roomId,
|
||||||
|
MAX_STICKY_DURATION_MS,
|
||||||
|
{ delay: 8000 },
|
||||||
|
null,
|
||||||
|
"org.matrix.msc4143.rtc.member",
|
||||||
|
{
|
||||||
|
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// ..once
|
||||||
|
expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should prefix log with MembershipManager used", () => {
|
it("Should prefix log with MembershipManager used", () => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "stream";
|
import { EventEmitter } from "stream";
|
||||||
|
import { type Mocked } from "jest-mock";
|
||||||
|
|
||||||
import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src";
|
import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src";
|
||||||
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||||
@@ -51,6 +52,8 @@ export type MockClient = Pick<
|
|||||||
| "sendStateEvent"
|
| "sendStateEvent"
|
||||||
| "_unstable_sendDelayedStateEvent"
|
| "_unstable_sendDelayedStateEvent"
|
||||||
| "_unstable_updateDelayedEvent"
|
| "_unstable_updateDelayedEvent"
|
||||||
|
| "_unstable_sendStickyEvent"
|
||||||
|
| "_unstable_sendStickyDelayedEvent"
|
||||||
| "cancelPendingEvent"
|
| "cancelPendingEvent"
|
||||||
>;
|
>;
|
||||||
/**
|
/**
|
||||||
@@ -65,15 +68,19 @@ export function makeMockClient(userId: string, deviceId: string): MockClient {
|
|||||||
cancelPendingEvent: jest.fn(),
|
cancelPendingEvent: jest.fn(),
|
||||||
_unstable_updateDelayedEvent: jest.fn(),
|
_unstable_updateDelayedEvent: jest.fn(),
|
||||||
_unstable_sendDelayedStateEvent: jest.fn(),
|
_unstable_sendDelayedStateEvent: jest.fn(),
|
||||||
|
_unstable_sendStickyEvent: jest.fn(),
|
||||||
|
_unstable_sendStickyDelayedEvent: jest.fn(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeMockRoom(
|
export function makeMockRoom(
|
||||||
membershipData: MembershipData[],
|
membershipData: MembershipData[],
|
||||||
): Room & { emitTimelineEvent: (event: MatrixEvent) => void } {
|
useStickyEvents = false,
|
||||||
|
): Mocked<Room & { emitTimelineEvent: (event: MatrixEvent) => void }> {
|
||||||
const roomId = secureRandomString(8);
|
const roomId = secureRandomString(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(useStickyEvents ? [] : membershipData, roomId);
|
||||||
|
const ts = Date.now();
|
||||||
const room = Object.assign(new EventEmitter(), {
|
const room = Object.assign(new EventEmitter(), {
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
hasMembershipState: jest.fn().mockReturnValue(true),
|
hasMembershipState: jest.fn().mockReturnValue(true),
|
||||||
@@ -81,11 +88,16 @@ export function makeMockRoom(
|
|||||||
getState: jest.fn().mockReturnValue(roomState),
|
getState: jest.fn().mockReturnValue(roomState),
|
||||||
}),
|
}),
|
||||||
getVersion: jest.fn().mockReturnValue("default"),
|
getVersion: jest.fn().mockReturnValue("default"),
|
||||||
}) as unknown as Room;
|
_unstable_getStickyEvents: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() =>
|
||||||
|
useStickyEvents ? membershipData.map((m) => mockRTCEvent(m, roomId, 10000, ts)) : [],
|
||||||
|
) as any,
|
||||||
|
});
|
||||||
return Object.assign(room, {
|
return Object.assign(room, {
|
||||||
emitTimelineEvent: (event: MatrixEvent) =>
|
emitTimelineEvent: (event: MatrixEvent) =>
|
||||||
room.emit(RoomEvent.Timeline, event, room, undefined, false, {} as any),
|
room.emit(RoomEvent.Timeline, event, room, undefined, false, {} as any),
|
||||||
});
|
}) as unknown as Mocked<Room & { emitTimelineEvent: (event: MatrixEvent) => void }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
|
function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
|
||||||
@@ -129,6 +141,7 @@ export function makeMockEvent(
|
|||||||
roomId: string | undefined,
|
roomId: string | undefined,
|
||||||
content: any,
|
content: any,
|
||||||
timestamp?: number,
|
timestamp?: number,
|
||||||
|
stateKey?: string,
|
||||||
): MatrixEvent {
|
): MatrixEvent {
|
||||||
return {
|
return {
|
||||||
getType: jest.fn().mockReturnValue(type),
|
getType: jest.fn().mockReturnValue(type),
|
||||||
@@ -137,12 +150,28 @@ export function makeMockEvent(
|
|||||||
getTs: jest.fn().mockReturnValue(timestamp ?? Date.now()),
|
getTs: jest.fn().mockReturnValue(timestamp ?? Date.now()),
|
||||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||||
getId: jest.fn().mockReturnValue(secureRandomString(8)),
|
getId: jest.fn().mockReturnValue(secureRandomString(8)),
|
||||||
|
getStateKey: jest.fn().mockReturnValue(stateKey),
|
||||||
isDecryptionFailure: jest.fn().mockReturnValue(false),
|
isDecryptionFailure: jest.fn().mockReturnValue(false),
|
||||||
} as unknown as MatrixEvent;
|
} as unknown as MatrixEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockRTCEvent({ user_id: sender, ...membershipData }: MembershipData, roomId: string): MatrixEvent {
|
export function mockRTCEvent(
|
||||||
return makeMockEvent(EventType.GroupCallMemberPrefix, sender, roomId, membershipData);
|
{ user_id: sender, ...membershipData }: MembershipData,
|
||||||
|
roomId: string,
|
||||||
|
stickyDuration?: number,
|
||||||
|
timestamp?: number,
|
||||||
|
): MatrixEvent {
|
||||||
|
return {
|
||||||
|
...makeMockEvent(
|
||||||
|
stickyDuration !== undefined ? EventType.RTCMembership : EventType.GroupCallMemberPrefix,
|
||||||
|
sender,
|
||||||
|
roomId,
|
||||||
|
membershipData,
|
||||||
|
timestamp,
|
||||||
|
!stickyDuration && "device_id" in membershipData ? `_${sender}_${membershipData.device_id}` : "",
|
||||||
|
),
|
||||||
|
unstableStickyExpiresAt: stickyDuration,
|
||||||
|
} as unknown as MatrixEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockCallMembership(membershipData: MembershipData, roomId: string): CallMembership {
|
export function mockCallMembership(membershipData: MembershipData, roomId: string): CallMembership {
|
||||||
|
|||||||
@@ -338,6 +338,7 @@ export interface TimelineEvents {
|
|||||||
[M_BEACON.name]: MBeaconEventContent;
|
[M_BEACON.name]: MBeaconEventContent;
|
||||||
[M_POLL_START.name]: PollStartEventContent;
|
[M_POLL_START.name]: PollStartEventContent;
|
||||||
[M_POLL_END.name]: PollEndEventContent;
|
[M_POLL_END.name]: PollEndEventContent;
|
||||||
|
[EventType.RTCMembership]: RtcMembershipData | { msc4354_sticky_key: string }; // An object containing just the sticky key is empty.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ export type SessionMembershipData = {
|
|||||||
* something else.
|
* something else.
|
||||||
*/
|
*/
|
||||||
"m.call.intent"?: RTCCallIntent;
|
"m.call.intent"?: RTCCallIntent;
|
||||||
|
/**
|
||||||
|
* The sticky key in case of a sticky event. This string encodes the application + device_id indicating the used slot + device.
|
||||||
|
*/
|
||||||
|
"msc4354_sticky_key"?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkSessionsMembershipData = (data: IContent, errors: string[]): data is SessionMembershipData => {
|
const checkSessionsMembershipData = (data: IContent, errors: string[]): data is SessionMembershipData => {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { KnownMembership } from "../@types/membership.ts";
|
|||||||
import { type ISendEventResponse } from "../@types/requests.ts";
|
import { type ISendEventResponse } from "../@types/requests.ts";
|
||||||
import { CallMembership } from "./CallMembership.ts";
|
import { CallMembership } from "./CallMembership.ts";
|
||||||
import { RoomStateEvent } from "../models/room-state.ts";
|
import { RoomStateEvent } from "../models/room-state.ts";
|
||||||
import { MembershipManager } from "./MembershipManager.ts";
|
import { MembershipManager, StickyEventMembershipManager } from "./MembershipManager.ts";
|
||||||
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
|
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
|
||||||
import { deepCompare, logDurationSync } from "../utils.ts";
|
import { deepCompare, logDurationSync } from "../utils.ts";
|
||||||
import type {
|
import type {
|
||||||
@@ -50,6 +50,8 @@ import {
|
|||||||
} from "./RoomAndToDeviceKeyTransport.ts";
|
} from "./RoomAndToDeviceKeyTransport.ts";
|
||||||
import { TypedReEmitter } from "../ReEmitter.ts";
|
import { TypedReEmitter } from "../ReEmitter.ts";
|
||||||
import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
|
import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
|
||||||
|
import { type MatrixEvent } from "../models/event.ts";
|
||||||
|
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../models/room-sticky-events.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events emitted by MatrixRTCSession
|
* Events emitted by MatrixRTCSession
|
||||||
@@ -123,14 +125,6 @@ export function slotDescriptionToId(slotDescription: SlotDescription): string {
|
|||||||
// - we use a `Ms` postfix if the option is a duration to avoid using words like:
|
// - we use a `Ms` postfix if the option is a duration to avoid using words like:
|
||||||
// `time`, `duration`, `delay`, `timeout`... that might be mistaken/confused with technical terms.
|
// `time`, `duration`, `delay`, `timeout`... that might be mistaken/confused with technical terms.
|
||||||
export interface MembershipConfig {
|
export interface MembershipConfig {
|
||||||
/**
|
|
||||||
* Use the new Manager.
|
|
||||||
*
|
|
||||||
* Default: `false`.
|
|
||||||
* @deprecated does nothing anymore we always default to the new membership manager.
|
|
||||||
*/
|
|
||||||
useNewMembershipManager?: 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.
|
||||||
@@ -192,7 +186,14 @@ export interface MembershipConfig {
|
|||||||
* but only applies to calls to the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.)
|
* but only applies to calls to the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.)
|
||||||
*/
|
*/
|
||||||
delayedLeaveEventRestartLocalTimeoutMs?: number;
|
delayedLeaveEventRestartLocalTimeoutMs?: number;
|
||||||
useRtcMemberFormat?: boolean;
|
|
||||||
|
/**
|
||||||
|
* Send membership using sticky events rather than state events.
|
||||||
|
* This also make the client use the new m.rtc.member MSC4354 event format. (instead of m.call.member)
|
||||||
|
*
|
||||||
|
* **WARNING**: This is an unstable feature and not all clients will support it.
|
||||||
|
*/
|
||||||
|
unstableSendStickyEvents?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EncryptionConfig {
|
export interface EncryptionConfig {
|
||||||
@@ -238,6 +239,19 @@ export interface EncryptionConfig {
|
|||||||
}
|
}
|
||||||
export type JoinSessionConfig = SessionConfig & MembershipConfig & EncryptionConfig;
|
export type JoinSessionConfig = SessionConfig & MembershipConfig & EncryptionConfig;
|
||||||
|
|
||||||
|
interface SessionMembershipsForRoomOpts {
|
||||||
|
/**
|
||||||
|
* Listen for incoming sticky member events. If disabled, this session will
|
||||||
|
* ignore any incoming sticky events.
|
||||||
|
*/
|
||||||
|
listenForStickyEvents: boolean;
|
||||||
|
/**
|
||||||
|
* Listen for incoming member state events (legacy). If disabled, this session will
|
||||||
|
* ignore any incoming state events.
|
||||||
|
*/
|
||||||
|
listenForMemberStateEvents: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
|
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
|
||||||
* This class doesn't deal with media at all, just membership & properties of a session.
|
* This class doesn't deal with media at all, just membership & properties of a session.
|
||||||
@@ -307,7 +321,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
* @deprecated Use `MatrixRTCSession.sessionMembershipsForSlot` instead.
|
* @deprecated Use `MatrixRTCSession.sessionMembershipsForSlot` instead.
|
||||||
*/
|
*/
|
||||||
public static callMembershipsForRoom(
|
public static callMembershipsForRoom(
|
||||||
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">,
|
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState" | "_unstable_getStickyEvents">,
|
||||||
): CallMembership[] {
|
): CallMembership[] {
|
||||||
return MatrixRTCSession.sessionMembershipsForSlot(room, {
|
return MatrixRTCSession.sessionMembershipsForSlot(room, {
|
||||||
id: "",
|
id: "",
|
||||||
@@ -319,7 +333,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
* @deprecated use `MatrixRTCSession.slotMembershipsForRoom` instead.
|
* @deprecated use `MatrixRTCSession.slotMembershipsForRoom` instead.
|
||||||
*/
|
*/
|
||||||
public static sessionMembershipsForRoom(
|
public static sessionMembershipsForRoom(
|
||||||
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">,
|
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState" | "_unstable_getStickyEvents">,
|
||||||
sessionDescription: SlotDescription,
|
sessionDescription: SlotDescription,
|
||||||
): CallMembership[] {
|
): CallMembership[] {
|
||||||
return this.sessionMembershipsForSlot(room, sessionDescription);
|
return this.sessionMembershipsForSlot(room, sessionDescription);
|
||||||
@@ -328,30 +342,58 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
/**
|
/**
|
||||||
* Returns all the call memberships for a room that match the provided `sessionDescription`,
|
* Returns all the call memberships for a room that match the provided `sessionDescription`,
|
||||||
* oldest first.
|
* oldest first.
|
||||||
|
*
|
||||||
|
* By default, this will return *both* sticky and member state events.
|
||||||
*/
|
*/
|
||||||
public static sessionMembershipsForSlot(
|
public static sessionMembershipsForSlot(
|
||||||
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">,
|
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState" | "_unstable_getStickyEvents">,
|
||||||
slotDescription: SlotDescription,
|
slotDescription: SlotDescription,
|
||||||
|
// default both true this implied we combine sticky and state events for the final call state
|
||||||
|
// (prefer sticky events in case of a duplicate)
|
||||||
|
{ listenForStickyEvents, listenForMemberStateEvents }: SessionMembershipsForRoomOpts = {
|
||||||
|
listenForStickyEvents: true,
|
||||||
|
listenForMemberStateEvents: true,
|
||||||
|
},
|
||||||
): CallMembership[] {
|
): CallMembership[] {
|
||||||
const logger = rootLogger.getChild(`[MatrixRTCSession ${room.roomId}]`);
|
const logger = rootLogger.getChild(`[MatrixRTCSession ${room.roomId}]`);
|
||||||
|
let callMemberEvents = [] as MatrixEvent[];
|
||||||
|
if (listenForStickyEvents) {
|
||||||
|
// prefill with sticky events
|
||||||
|
callMemberEvents = [...room._unstable_getStickyEvents()].filter(
|
||||||
|
(e) => e.getType() === EventType.RTCMembership,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (listenForMemberStateEvents) {
|
||||||
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||||
if (!roomState) {
|
if (!roomState) {
|
||||||
logger.warn("Couldn't get state for room " + room.roomId);
|
logger.warn("Couldn't get state for room " + room.roomId);
|
||||||
throw new Error("Could't get state for room " + room.roomId);
|
throw new Error("Could't get state for room " + room.roomId);
|
||||||
}
|
}
|
||||||
const callMemberEvents = roomState.getStateEvents(EventType.GroupCallMemberPrefix);
|
const callMemberStateEvents = roomState.getStateEvents(EventType.GroupCallMemberPrefix);
|
||||||
|
callMemberEvents = callMemberEvents.concat(
|
||||||
|
callMemberStateEvents.filter(
|
||||||
|
(callMemberStateEvent) =>
|
||||||
|
!callMemberEvents.some(
|
||||||
|
// only care about state events which have keys which we have not yet seen in the sticky events.
|
||||||
|
(stickyEvent) =>
|
||||||
|
stickyEvent.getContent().msc4354_sticky_key === callMemberStateEvent.getStateKey(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const callMemberships: CallMembership[] = [];
|
const callMemberships: CallMembership[] = [];
|
||||||
for (const memberEvent of callMemberEvents) {
|
for (const memberEvent of callMemberEvents) {
|
||||||
const content = memberEvent.getContent();
|
const content = memberEvent.getContent();
|
||||||
const eventKeysCount = Object.keys(content).length;
|
// Ignore sticky keys for the count
|
||||||
|
const eventKeysCount = Object.keys(content).filter((k) => k !== "msc4354_sticky_key").length;
|
||||||
// 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;
|
||||||
|
|
||||||
const 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 && "application" 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) {
|
||||||
@@ -411,8 +453,16 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
*
|
*
|
||||||
* @deprecated Use `MatrixRTCSession.sessionForSlot` with sessionDescription `{ id: "", application: "m.call" }` instead.
|
* @deprecated Use `MatrixRTCSession.sessionForSlot` with sessionDescription `{ id: "", application: "m.call" }` instead.
|
||||||
*/
|
*/
|
||||||
public static roomSessionForRoom(client: MatrixClient, room: Room): MatrixRTCSession {
|
public static roomSessionForRoom(
|
||||||
const callMemberships = MatrixRTCSession.sessionMembershipsForSlot(room, { id: "", application: "m.call" });
|
client: MatrixClient,
|
||||||
|
room: Room,
|
||||||
|
opts?: SessionMembershipsForRoomOpts,
|
||||||
|
): MatrixRTCSession {
|
||||||
|
const callMemberships = MatrixRTCSession.sessionMembershipsForSlot(
|
||||||
|
room,
|
||||||
|
{ id: "", application: "m.call" },
|
||||||
|
opts,
|
||||||
|
);
|
||||||
return new MatrixRTCSession(client, room, callMemberships, { id: "", application: "m.call" });
|
return new MatrixRTCSession(client, room, callMemberships, { id: "", application: "m.call" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,9 +478,13 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
* This returned session can be used to find out if there are active sessions
|
* This returned session can be used to find out if there are active sessions
|
||||||
* for the requested room and `slotDescription`.
|
* for the requested room and `slotDescription`.
|
||||||
*/
|
*/
|
||||||
public static sessionForSlot(client: MatrixClient, room: Room, slotDescription: SlotDescription): MatrixRTCSession {
|
public static sessionForSlot(
|
||||||
const callMemberships = MatrixRTCSession.sessionMembershipsForSlot(room, slotDescription);
|
client: MatrixClient,
|
||||||
|
room: Room,
|
||||||
|
slotDescription: SlotDescription,
|
||||||
|
opts?: SessionMembershipsForRoomOpts,
|
||||||
|
): MatrixRTCSession {
|
||||||
|
const callMemberships = MatrixRTCSession.sessionMembershipsForSlot(room, slotDescription, opts);
|
||||||
return new MatrixRTCSession(client, room, callMemberships, slotDescription);
|
return new MatrixRTCSession(client, room, callMemberships, slotDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,10 +515,12 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
MatrixClient,
|
MatrixClient,
|
||||||
| "getUserId"
|
| "getUserId"
|
||||||
| "getDeviceId"
|
| "getDeviceId"
|
||||||
|
| "sendEvent"
|
||||||
| "sendStateEvent"
|
| "sendStateEvent"
|
||||||
| "_unstable_sendDelayedStateEvent"
|
| "_unstable_sendDelayedStateEvent"
|
||||||
| "_unstable_updateDelayedEvent"
|
| "_unstable_updateDelayedEvent"
|
||||||
| "sendEvent"
|
| "_unstable_sendStickyEvent"
|
||||||
|
| "_unstable_sendStickyDelayedEvent"
|
||||||
| "cancelPendingEvent"
|
| "cancelPendingEvent"
|
||||||
| "encryptAndSendToDevice"
|
| "encryptAndSendToDevice"
|
||||||
| "off"
|
| "off"
|
||||||
@@ -488,9 +544,10 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
const roomState = this.roomSubset.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
const roomState = this.roomSubset.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||||
// TODO: double check if this is actually needed. Should be covered by refreshRoom in MatrixRTCSessionManager
|
// TODO: double check if this is actually needed. Should be covered by refreshRoom in MatrixRTCSessionManager
|
||||||
roomState?.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
roomState?.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
||||||
|
this.roomSubset.on(RoomStickyEventsEvent.Update, this.onStickyEventUpdate);
|
||||||
|
|
||||||
this.setExpiryTimer();
|
this.setExpiryTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns true if we intend to be participating in the MatrixRTC session.
|
* Returns true if we intend to be participating in the MatrixRTC session.
|
||||||
* This is determined by checking if the relativeExpiry has been set.
|
* This is determined by checking if the relativeExpiry has been set.
|
||||||
@@ -510,7 +567,9 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
}
|
}
|
||||||
const roomState = this.roomSubset.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
const roomState = this.roomSubset.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||||
roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
||||||
|
this.roomSubset.off(RoomStickyEventsEvent.Update, this.onStickyEventUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private reEmitter = new TypedReEmitter<
|
private reEmitter = new TypedReEmitter<
|
||||||
MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent,
|
MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent,
|
||||||
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap
|
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap
|
||||||
@@ -540,14 +599,15 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Create MembershipManager and pass the RTCSession logger (with room id info)
|
// Create MembershipManager and pass the RTCSession logger (with room id info)
|
||||||
|
this.membershipManager = joinConfig?.unstableSendStickyEvents
|
||||||
this.membershipManager = new MembershipManager(
|
? new StickyEventMembershipManager(
|
||||||
joinConfig,
|
joinConfig,
|
||||||
this.roomSubset,
|
this.roomSubset,
|
||||||
this.client,
|
this.client,
|
||||||
this.slotDescription,
|
this.slotDescription,
|
||||||
this.logger,
|
this.logger,
|
||||||
);
|
)
|
||||||
|
: new MembershipManager(joinConfig, this.roomSubset, this.client, this.slotDescription, this.logger);
|
||||||
|
|
||||||
this.reEmitter.reEmit(this.membershipManager!, [
|
this.reEmitter.reEmit(this.membershipManager!, [
|
||||||
MembershipManagerEvent.ProbablyLeft,
|
MembershipManagerEvent.ProbablyLeft,
|
||||||
@@ -786,10 +846,27 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
/**
|
/**
|
||||||
* Call this when the Matrix room members have changed.
|
* Call this when the Matrix room members have changed.
|
||||||
*/
|
*/
|
||||||
public onRoomMemberUpdate = (): void => {
|
private readonly onRoomMemberUpdate = (): void => {
|
||||||
this.recalculateSessionMembers();
|
this.recalculateSessionMembers();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this when a sticky event update has occured.
|
||||||
|
*/
|
||||||
|
private readonly onStickyEventUpdate: RoomStickyEventsMap[RoomStickyEventsEvent.Update] = (
|
||||||
|
added,
|
||||||
|
updated,
|
||||||
|
removed,
|
||||||
|
): void => {
|
||||||
|
if (
|
||||||
|
[...added, ...removed, ...updated.flatMap((v) => [v.current, v.previous])].some(
|
||||||
|
(e) => e.getType() === EventType.RTCMembership,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.recalculateSessionMembers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call this when something changed that may impacts the current MatrixRTC members in this session.
|
* Call this when something changed that may impacts the current MatrixRTC members in this session.
|
||||||
*/
|
*/
|
||||||
@@ -839,6 +916,8 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
// If anyone else joins the session it is no longer our responsibility to send the notification.
|
// If anyone else joins the session it is no longer our responsibility to send the notification.
|
||||||
// (If we were the joiner we already did sent the notification in the block above.)
|
// (If we were the joiner we already did sent the notification in the block above.)
|
||||||
if (this.memberships.length > 0) this.pendingNotificationToSend = undefined;
|
if (this.memberships.length > 0) this.pendingNotificationToSend = undefined;
|
||||||
|
} else {
|
||||||
|
this.logger.debug(`No membership changes detected for room ${this.roomSubset.roomId}`);
|
||||||
}
|
}
|
||||||
// This also needs to be done if `changed` = false
|
// This also needs to be done if `changed` = false
|
||||||
// A member might have updated their fingerprint (created_ts)
|
// A member might have updated their fingerprint (created_ts)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { type Logger } from "../logger.ts";
|
|||||||
import { type MatrixClient, ClientEvent } from "../client.ts";
|
import { type MatrixClient, ClientEvent } from "../client.ts";
|
||||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||||
import { type Room } from "../models/room.ts";
|
import { type Room } from "../models/room.ts";
|
||||||
import { type RoomState, RoomStateEvent } from "../models/room-state.ts";
|
import { RoomStateEvent } from "../models/room-state.ts";
|
||||||
import { type MatrixEvent } from "../models/event.ts";
|
import { type MatrixEvent } from "../models/event.ts";
|
||||||
import { MatrixRTCSession, type SlotDescription } from "./MatrixRTCSession.ts";
|
import { MatrixRTCSession, type SlotDescription } from "./MatrixRTCSession.ts";
|
||||||
import { EventType } from "../@types/event.ts";
|
import { EventType } from "../@types/event.ts";
|
||||||
@@ -73,6 +73,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.client.on(ClientEvent.Room, this.onRoom);
|
this.client.on(ClientEvent.Room, this.onRoom);
|
||||||
|
this.client.on(ClientEvent.Event, this.onEvent);
|
||||||
this.client.on(RoomStateEvent.Events, this.onRoomState);
|
this.client.on(RoomStateEvent.Events, this.onRoomState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +84,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
|||||||
this.roomSessions.clear();
|
this.roomSessions.clear();
|
||||||
|
|
||||||
this.client.off(ClientEvent.Room, this.onRoom);
|
this.client.off(ClientEvent.Room, this.onRoom);
|
||||||
|
this.client.off(ClientEvent.Event, this.onEvent);
|
||||||
this.client.off(RoomStateEvent.Events, this.onRoomState);
|
this.client.off(RoomStateEvent.Events, this.onRoomState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,16 +115,28 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
|||||||
this.refreshRoom(room);
|
this.refreshRoom(room);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomState = (event: MatrixEvent, _state: RoomState): void => {
|
private readonly onEvent = (event: MatrixEvent): void => {
|
||||||
|
if (!event.unstableStickyExpiresAt) return; // Not sticky, not interested.
|
||||||
|
|
||||||
|
if (event.getType() !== EventType.RTCMembership) return;
|
||||||
|
|
||||||
|
const room = this.client.getRoom(event.getRoomId());
|
||||||
|
if (!room) return;
|
||||||
|
|
||||||
|
this.refreshRoom(room);
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly onRoomState = (event: MatrixEvent): void => {
|
||||||
|
if (event.getType() !== EventType.GroupCallMemberPrefix) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const room = this.client.getRoom(event.getRoomId());
|
const room = this.client.getRoom(event.getRoomId());
|
||||||
if (!room) {
|
if (!room) {
|
||||||
this.logger.error(`Got room state event for unknown room ${event.getRoomId()}!`);
|
this.logger.error(`Got room state event for unknown room ${event.getRoomId()}!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getType() == EventType.GroupCallMemberPrefix) {
|
|
||||||
this.refreshRoom(room);
|
this.refreshRoom(room);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private refreshRoom(room: Room): void {
|
private refreshRoom(room: Room): void {
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ limitations under the License.
|
|||||||
import { AbortError } from "p-retry";
|
import { AbortError } from "p-retry";
|
||||||
|
|
||||||
import { EventType, RelationType } from "../@types/event.ts";
|
import { EventType, RelationType } from "../@types/event.ts";
|
||||||
import { UpdateDelayedEventAction } from "../@types/requests.ts";
|
import {
|
||||||
|
type ISendEventResponse,
|
||||||
|
type SendDelayedEventResponse,
|
||||||
|
UpdateDelayedEventAction,
|
||||||
|
} from "../@types/requests.ts";
|
||||||
|
import { type EmptyObject } from "../@types/common.ts";
|
||||||
import type { MatrixClient } from "../client.ts";
|
import type { MatrixClient } from "../client.ts";
|
||||||
import { ConnectionError, HTTPError, MatrixError } from "../http-api/errors.ts";
|
import { ConnectionError, HTTPError, MatrixError } from "../http-api/errors.ts";
|
||||||
import { type Logger, logger as rootLogger } from "../logger.ts";
|
import { type Logger, logger as rootLogger } from "../logger.ts";
|
||||||
@@ -84,6 +89,12 @@ On Leave: ───────── STOP ALL ABOVE
|
|||||||
(s) Successful restart/resend
|
(s) Successful restart/resend
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call membership should always remain sticky for this amount
|
||||||
|
* of time.
|
||||||
|
*/
|
||||||
|
const MEMBERSHIP_STICKY_DURATION_MS = 60 * 60 * 1000; // 60 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The different types of actions the MembershipManager can take.
|
* The different types of actions the MembershipManager can take.
|
||||||
* @internal
|
* @internal
|
||||||
@@ -144,6 +155,23 @@ export interface MembershipManagerState {
|
|||||||
probablyLeft: boolean;
|
probablyLeft: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createInsertActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate {
|
||||||
|
return {
|
||||||
|
insert: [{ ts: Date.now() + (offset ?? 0), type }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createReplaceActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate {
|
||||||
|
return {
|
||||||
|
replace: [{ ts: Date.now() + (offset ?? 0), type }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type MembershipManagerClient = Pick<
|
||||||
|
MatrixClient,
|
||||||
|
"getUserId" | "getDeviceId" | "sendStateEvent" | "_unstable_sendDelayedStateEvent" | "_unstable_updateDelayedEvent"
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is responsible for sending all events relating to the own membership of a matrixRTC call.
|
* This class is responsible for sending all events relating to the own membership of a matrixRTC call.
|
||||||
* It has the following tasks:
|
* It has the following tasks:
|
||||||
@@ -162,8 +190,8 @@ export class MembershipManager
|
|||||||
implements IMembershipManager
|
implements IMembershipManager
|
||||||
{
|
{
|
||||||
private activated = false;
|
private activated = false;
|
||||||
private logger: Logger;
|
private readonly logger: Logger;
|
||||||
private callIntent: RTCCallIntent | undefined;
|
protected callIntent: RTCCallIntent | undefined;
|
||||||
|
|
||||||
public isActivated(): boolean {
|
public isActivated(): boolean {
|
||||||
return this.activated;
|
return this.activated;
|
||||||
@@ -295,16 +323,9 @@ export class MembershipManager
|
|||||||
* @param client
|
* @param client
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private joinConfig: (SessionConfig & MembershipConfig) | undefined,
|
private readonly joinConfig: (SessionConfig & MembershipConfig) | undefined,
|
||||||
private room: Pick<Room, "getLiveTimeline" | "roomId" | "getVersion">,
|
protected readonly room: Pick<Room, "roomId" | "getVersion">,
|
||||||
private client: Pick<
|
protected readonly client: MembershipManagerClient,
|
||||||
MatrixClient,
|
|
||||||
| "getUserId"
|
|
||||||
| "getDeviceId"
|
|
||||||
| "sendStateEvent"
|
|
||||||
| "_unstable_sendDelayedStateEvent"
|
|
||||||
| "_unstable_updateDelayedEvent"
|
|
||||||
>,
|
|
||||||
public readonly slotDescription: SlotDescription,
|
public readonly slotDescription: SlotDescription,
|
||||||
parentLogger?: Logger,
|
parentLogger?: Logger,
|
||||||
) {
|
) {
|
||||||
@@ -361,11 +382,11 @@ export class MembershipManager
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Membership Event static parameters:
|
// Membership Event static parameters:
|
||||||
private deviceId: string;
|
protected deviceId: string;
|
||||||
private memberId: string;
|
protected memberId: string;
|
||||||
|
protected rtcTransport?: Transport;
|
||||||
/** @deprecated This will be removed in favor or rtcTransport becoming a list of actively used transports */
|
/** @deprecated This will be removed in favor or rtcTransport becoming a list of actively used transports */
|
||||||
private fociPreferred?: Transport[];
|
private fociPreferred?: Transport[];
|
||||||
private rtcTransport?: Transport;
|
|
||||||
|
|
||||||
// Config:
|
// Config:
|
||||||
private delayedLeaveEventDelayMsOverride?: number;
|
private delayedLeaveEventDelayMsOverride?: number;
|
||||||
@@ -380,9 +401,13 @@ export class MembershipManager
|
|||||||
return this.joinConfig?.membershipEventExpiryHeadroomMs ?? 5_000;
|
return this.joinConfig?.membershipEventExpiryHeadroomMs ?? 5_000;
|
||||||
}
|
}
|
||||||
private computeNextExpiryActionTs(iteration: number): number {
|
private computeNextExpiryActionTs(iteration: number): number {
|
||||||
return this.state.startTime + this.membershipEventExpiryMs * iteration - this.membershipEventExpiryHeadroomMs;
|
return (
|
||||||
|
this.state.startTime +
|
||||||
|
Math.min(this.membershipEventExpiryMs, MEMBERSHIP_STICKY_DURATION_MS) * iteration -
|
||||||
|
this.membershipEventExpiryHeadroomMs
|
||||||
|
);
|
||||||
}
|
}
|
||||||
private get delayedLeaveEventDelayMs(): number {
|
protected get delayedLeaveEventDelayMs(): number {
|
||||||
return this.delayedLeaveEventDelayMsOverride ?? this.joinConfig?.delayedLeaveEventDelayMs ?? 8_000;
|
return this.delayedLeaveEventDelayMsOverride ?? this.joinConfig?.delayedLeaveEventDelayMs ?? 8_000;
|
||||||
}
|
}
|
||||||
private get delayedLeaveEventRestartMs(): number {
|
private get delayedLeaveEventRestartMs(): number {
|
||||||
@@ -394,13 +419,10 @@ export class MembershipManager
|
|||||||
private get maximumNetworkErrorRetryCount(): number {
|
private get maximumNetworkErrorRetryCount(): number {
|
||||||
return this.joinConfig?.maximumNetworkErrorRetryCount ?? 10;
|
return this.joinConfig?.maximumNetworkErrorRetryCount ?? 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get delayedLeaveEventRestartLocalTimeoutMs(): number {
|
private get delayedLeaveEventRestartLocalTimeoutMs(): number {
|
||||||
return this.joinConfig?.delayedLeaveEventRestartLocalTimeoutMs ?? 2000;
|
return this.joinConfig?.delayedLeaveEventRestartLocalTimeoutMs ?? 2000;
|
||||||
}
|
}
|
||||||
private get useRtcMemberFormat(): boolean {
|
|
||||||
return this.joinConfig?.useRtcMemberFormat ?? false;
|
|
||||||
}
|
|
||||||
// LOOP HANDLER:
|
// LOOP HANDLER:
|
||||||
private async membershipLoopHandler(type: MembershipActionType): Promise<ActionUpdate> {
|
private async membershipLoopHandler(type: MembershipActionType): Promise<ActionUpdate> {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -455,22 +477,23 @@ export class MembershipManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// an abstraction to switch between sending state or a sticky event
|
||||||
|
protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () =>
|
||||||
|
this.client._unstable_sendDelayedStateEvent(
|
||||||
|
this.room.roomId,
|
||||||
|
{ delay: this.delayedLeaveEventDelayMs },
|
||||||
|
EventType.GroupCallMemberPrefix,
|
||||||
|
{},
|
||||||
|
this.memberId,
|
||||||
|
);
|
||||||
|
|
||||||
// HANDLERS (used in the membershipLoopHandler)
|
// HANDLERS (used in the membershipLoopHandler)
|
||||||
private async sendOrResendDelayedLeaveEvent(): Promise<ActionUpdate> {
|
private async sendOrResendDelayedLeaveEvent(): Promise<ActionUpdate> {
|
||||||
// We can reach this at the start of a call (where we do not yet have a membership: state.hasMemberStateEvent=false)
|
// We can reach this at the start of a call (where we do not yet have a membership: state.hasMemberStateEvent=false)
|
||||||
// or during a call if the state event canceled our delayed event or caused by an unexpected error that removed our delayed event.
|
// or during a call if the state event canceled our delayed event or caused by an unexpected error that removed our delayed event.
|
||||||
// (Another client could have canceled it, the homeserver might have removed/lost it due to a restart, ...)
|
// (Another client could have canceled it, the homeserver might have removed/lost it due to a restart, ...)
|
||||||
// In the `then` and `catch` block we treat both cases differently. "if (this.state.hasMemberStateEvent) {} else {}"
|
// In the `then` and `catch` block we treat both cases differently. "if (this.state.hasMemberStateEvent) {} else {}"
|
||||||
return await this.client
|
return await this.clientSendDelayedDisconnectMembership()
|
||||||
._unstable_sendDelayedStateEvent(
|
|
||||||
this.room.roomId,
|
|
||||||
{
|
|
||||||
delay: this.delayedLeaveEventDelayMs,
|
|
||||||
},
|
|
||||||
this.useRtcMemberFormat ? EventType.RTCMembership : EventType.GroupCallMemberPrefix,
|
|
||||||
{}, // leave event
|
|
||||||
this.memberId,
|
|
||||||
)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
|
this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
|
||||||
this.setAndEmitProbablyLeft(false);
|
this.setAndEmitProbablyLeft(false);
|
||||||
@@ -494,7 +517,7 @@ export class MembershipManager
|
|||||||
if (this.manageMaxDelayExceededSituation(e)) {
|
if (this.manageMaxDelayExceededSituation(e)) {
|
||||||
return createInsertActionUpdate(repeatActionType);
|
return createInsertActionUpdate(repeatActionType);
|
||||||
}
|
}
|
||||||
const update = this.actionUpdateFromErrors(e, repeatActionType, "sendDelayedStateEvent");
|
const update = this.actionUpdateFromErrors(e, repeatActionType, "_unstable_sendDelayedStateEvent");
|
||||||
if (update) return update;
|
if (update) return update;
|
||||||
|
|
||||||
if (this.state.hasMemberStateEvent) {
|
if (this.state.hasMemberStateEvent) {
|
||||||
@@ -650,14 +673,19 @@ export class MembershipManager
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendJoinEvent(): Promise<ActionUpdate> {
|
protected clientSendMembership: (
|
||||||
return await this.client
|
myMembership: RtcMembershipData | SessionMembershipData | EmptyObject,
|
||||||
.sendStateEvent(
|
) => Promise<ISendEventResponse> = (myMembership) => {
|
||||||
|
return this.client.sendStateEvent(
|
||||||
this.room.roomId,
|
this.room.roomId,
|
||||||
this.useRtcMemberFormat ? EventType.RTCMembership : EventType.GroupCallMemberPrefix,
|
EventType.GroupCallMemberPrefix,
|
||||||
this.makeMyMembership(this.membershipEventExpiryMs),
|
myMembership as EmptyObject | SessionMembershipData,
|
||||||
this.memberId,
|
this.memberId,
|
||||||
)
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private async sendJoinEvent(): Promise<ActionUpdate> {
|
||||||
|
return await this.clientSendMembership(this.makeMyMembership(this.membershipEventExpiryMs))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setAndEmitProbablyLeft(false);
|
this.setAndEmitProbablyLeft(false);
|
||||||
this.state.startTime = Date.now();
|
this.state.startTime = Date.now();
|
||||||
@@ -697,12 +725,8 @@ export class MembershipManager
|
|||||||
|
|
||||||
private async updateExpiryOnJoinedEvent(): Promise<ActionUpdate> {
|
private async updateExpiryOnJoinedEvent(): Promise<ActionUpdate> {
|
||||||
const nextExpireUpdateIteration = this.state.expireUpdateIterations + 1;
|
const nextExpireUpdateIteration = this.state.expireUpdateIterations + 1;
|
||||||
return await this.client
|
return await this.clientSendMembership(
|
||||||
.sendStateEvent(
|
|
||||||
this.room.roomId,
|
|
||||||
this.useRtcMemberFormat ? EventType.RTCMembership : EventType.GroupCallMemberPrefix,
|
|
||||||
this.makeMyMembership(this.membershipEventExpiryMs * nextExpireUpdateIteration),
|
this.makeMyMembership(this.membershipEventExpiryMs * nextExpireUpdateIteration),
|
||||||
this.memberId,
|
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Success, we reset retries and schedule update.
|
// Success, we reset retries and schedule update.
|
||||||
@@ -725,13 +749,7 @@ export class MembershipManager
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
private async sendFallbackLeaveEvent(): Promise<ActionUpdate> {
|
private async sendFallbackLeaveEvent(): Promise<ActionUpdate> {
|
||||||
return await this.client
|
return await this.clientSendMembership({})
|
||||||
.sendStateEvent(
|
|
||||||
this.room.roomId,
|
|
||||||
this.useRtcMemberFormat ? EventType.RTCMembership : EventType.GroupCallMemberPrefix,
|
|
||||||
{},
|
|
||||||
this.memberId,
|
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.resetRateLimitCounter(MembershipActionType.SendLeaveEvent);
|
this.resetRateLimitCounter(MembershipActionType.SendLeaveEvent);
|
||||||
this.state.hasMemberStateEvent = false;
|
this.state.hasMemberStateEvent = false;
|
||||||
@@ -757,24 +775,9 @@ export class MembershipManager
|
|||||||
/**
|
/**
|
||||||
* Constructs our own membership
|
* Constructs our own membership
|
||||||
*/
|
*/
|
||||||
private makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
|
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
|
||||||
const ownMembership = this.ownMembership;
|
const ownMembership = this.ownMembership;
|
||||||
if (this.useRtcMemberFormat) {
|
|
||||||
const relationObject = ownMembership?.eventId
|
|
||||||
? { "m.relation": { rel_type: RelationType.Reference, event_id: ownMembership?.eventId } }
|
|
||||||
: {};
|
|
||||||
return {
|
|
||||||
application: {
|
|
||||||
type: this.slotDescription.application,
|
|
||||||
...(this.callIntent ? { "m.call.intent": this.callIntent } : {}),
|
|
||||||
},
|
|
||||||
slot_id: slotDescriptionToId(this.slotDescription),
|
|
||||||
rtc_transports: this.rtcTransport ? [this.rtcTransport] : [],
|
|
||||||
member: { device_id: this.deviceId, user_id: this.client.getUserId()!, id: this.memberId },
|
|
||||||
versions: [],
|
|
||||||
...relationObject,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const focusObjects =
|
const focusObjects =
|
||||||
this.rtcTransport === undefined
|
this.rtcTransport === undefined
|
||||||
? {
|
? {
|
||||||
@@ -796,7 +799,6 @@ export class MembershipManager
|
|||||||
...(ownMembership !== undefined ? { created_ts: ownMembership.createdTs() } : undefined),
|
...(ownMembership !== undefined ? { created_ts: ownMembership.createdTs() } : undefined),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Error checks and handlers
|
// Error checks and handlers
|
||||||
|
|
||||||
@@ -830,7 +832,7 @@ export class MembershipManager
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private actionUpdateFromErrors(
|
protected actionUpdateFromErrors(
|
||||||
error: unknown,
|
error: unknown,
|
||||||
type: MembershipActionType,
|
type: MembershipActionType,
|
||||||
method: string,
|
method: string,
|
||||||
@@ -878,7 +880,7 @@ export class MembershipManager
|
|||||||
return createInsertActionUpdate(type, resendDelay);
|
return createInsertActionUpdate(type, resendDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Error("Exceeded maximum retries for " + type + " attempts (client." + method + "): " + (error as Error));
|
throw Error("Exceeded maximum retries for " + type + " attempts (client." + method + ")", { cause: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1022,14 +1024,68 @@ export class MembershipManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createInsertActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate {
|
/**
|
||||||
return {
|
* Implementation of the Membership manager that uses sticky events
|
||||||
insert: [{ ts: Date.now() + (offset ?? 0), type }],
|
* rather than state events.
|
||||||
};
|
*/
|
||||||
}
|
export class StickyEventMembershipManager extends MembershipManager {
|
||||||
|
public constructor(
|
||||||
|
joinConfig: (SessionConfig & MembershipConfig) | undefined,
|
||||||
|
room: Pick<Room, "getLiveTimeline" | "roomId" | "getVersion">,
|
||||||
|
private readonly clientWithSticky: MembershipManagerClient &
|
||||||
|
Pick<MatrixClient, "_unstable_sendStickyEvent" | "_unstable_sendStickyDelayedEvent">,
|
||||||
|
sessionDescription: SlotDescription,
|
||||||
|
parentLogger?: Logger,
|
||||||
|
) {
|
||||||
|
super(joinConfig, room, clientWithSticky, sessionDescription, parentLogger);
|
||||||
|
}
|
||||||
|
|
||||||
function createReplaceActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate {
|
protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () =>
|
||||||
return {
|
this.clientWithSticky._unstable_sendStickyDelayedEvent(
|
||||||
replace: [{ ts: Date.now() + (offset ?? 0), type }],
|
this.room.roomId,
|
||||||
|
MEMBERSHIP_STICKY_DURATION_MS,
|
||||||
|
{ delay: this.delayedLeaveEventDelayMs },
|
||||||
|
null,
|
||||||
|
EventType.RTCMembership,
|
||||||
|
{ msc4354_sticky_key: this.memberId },
|
||||||
|
);
|
||||||
|
|
||||||
|
protected clientSendMembership: (
|
||||||
|
myMembership: RtcMembershipData | SessionMembershipData | EmptyObject,
|
||||||
|
) => Promise<ISendEventResponse> = (myMembership) => {
|
||||||
|
return this.clientWithSticky._unstable_sendStickyEvent(
|
||||||
|
this.room.roomId,
|
||||||
|
MEMBERSHIP_STICKY_DURATION_MS,
|
||||||
|
null,
|
||||||
|
EventType.RTCMembership,
|
||||||
|
{ ...myMembership, msc4354_sticky_key: this.memberId },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static nameMap = new Map([
|
||||||
|
["sendStateEvent", "_unstable_sendStickyEvent"],
|
||||||
|
["sendDelayedStateEvent", "_unstable_sendStickyDelayedEvent"],
|
||||||
|
]);
|
||||||
|
protected actionUpdateFromErrors(e: unknown, t: MembershipActionType, m: string): ActionUpdate | undefined {
|
||||||
|
return super.actionUpdateFromErrors(e, t, StickyEventMembershipManager.nameMap.get(m) ?? "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
|
||||||
|
const ownMembership = this.ownMembership;
|
||||||
|
|
||||||
|
const relationObject = ownMembership?.eventId
|
||||||
|
? { "m.relation": { rel_type: RelationType.Reference, event_id: ownMembership?.eventId } }
|
||||||
|
: {};
|
||||||
|
return {
|
||||||
|
application: {
|
||||||
|
type: this.slotDescription.application,
|
||||||
|
...(this.callIntent ? { "m.call.intent": this.callIntent } : {}),
|
||||||
|
},
|
||||||
|
slot_id: slotDescriptionToId(this.slotDescription),
|
||||||
|
rtc_transports: this.rtcTransport ? [this.rtcTransport] : [],
|
||||||
|
member: { device_id: this.deviceId, user_id: this.client.getUserId()!, id: this.memberId },
|
||||||
|
versions: [],
|
||||||
|
...relationObject,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export enum RoomStickyEventsEvent {
|
|||||||
Update = "RoomStickyEvents.Update",
|
Update = "RoomStickyEvents.Update",
|
||||||
}
|
}
|
||||||
|
|
||||||
type StickyMatrixEvent = MatrixEvent & { unstableStickyExpiresAt: number };
|
export type StickyMatrixEvent = MatrixEvent & { unstableStickyExpiresAt: number };
|
||||||
|
|
||||||
export type RoomStickyEventsMap = {
|
export type RoomStickyEventsMap = {
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user