mirror of https://github.com/vector-im/riot-web
Implement GroupCallUtils (#9131)
* Implement GroupCallUtils * Trigger CI * Use UnstableValue for new call event types * Implement PR feedbackpull/28788/head^2
parent
394e181854
commit
9ed5550501
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventTimeline, MatrixClient, MatrixEvent, RoomState } from "matrix-js-sdk/src/matrix";
|
||||
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
|
||||
import { deepCopy } from "matrix-js-sdk/src/utils";
|
||||
|
||||
export const STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
export const CALL_STATE_EVENT_TYPE = new UnstableValue("m.call", "org.matrix.msc3401.call");
|
||||
export const CALL_MEMBER_STATE_EVENT_TYPE = new UnstableValue("m.call.member", "org.matrix.msc3401.call.member");
|
||||
const CALL_STATE_EVENT_TERMINATED = "m.terminated";
|
||||
|
||||
interface MDevice {
|
||||
["m.device_id"]: string;
|
||||
}
|
||||
|
||||
interface MCall {
|
||||
["m.call_id"]: string;
|
||||
["m.devices"]: Array<MDevice>;
|
||||
}
|
||||
|
||||
interface MCallMemberContent {
|
||||
["m.expires_ts"]: number;
|
||||
["m.calls"]: Array<MCall>;
|
||||
}
|
||||
|
||||
const getRoomState = (client: MatrixClient, roomId: string): RoomState => {
|
||||
return client.getRoom(roomId)
|
||||
?.getLiveTimeline()
|
||||
?.getState?.(EventTimeline.FORWARDS);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all room state events for the stable and unstable type value.
|
||||
*/
|
||||
const getRoomStateEvents = (
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
type: UnstableValue<string, string>,
|
||||
): MatrixEvent[] => {
|
||||
const roomState = getRoomState(client, roomId);
|
||||
if (!roomState) return [];
|
||||
|
||||
return [
|
||||
...roomState.getStateEvents(type.name),
|
||||
...roomState.getStateEvents(type.altName),
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the latest, non-terminated call state event.
|
||||
*/
|
||||
export const getGroupCall = (client: MatrixClient, roomId: string): MatrixEvent => {
|
||||
return getRoomStateEvents(client, roomId, CALL_STATE_EVENT_TYPE)
|
||||
.sort((a: MatrixEvent, b: MatrixEvent) => b.getTs() - a.getTs())
|
||||
.find((event: MatrixEvent) => {
|
||||
return !(CALL_STATE_EVENT_TERMINATED in event.getContent());
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the "m.call.member" events for an "m.call" event.
|
||||
*
|
||||
* @returns {MatrixEvent[]} non-expired "m.call.member" events for the call
|
||||
*/
|
||||
export const useConnectedMembers = (client: MatrixClient, callEvent: MatrixEvent): MatrixEvent[] => {
|
||||
if (!CALL_STATE_EVENT_TYPE.matches(callEvent.getType())) return [];
|
||||
|
||||
const callId = callEvent.getStateKey();
|
||||
const now = Date.now();
|
||||
|
||||
return getRoomStateEvents(client, callEvent.getRoomId(), CALL_MEMBER_STATE_EVENT_TYPE)
|
||||
.filter((callMemberEvent: MatrixEvent): boolean => {
|
||||
const {
|
||||
["m.expires_ts"]: expiresTs,
|
||||
["m.calls"]: calls,
|
||||
} = callMemberEvent.getContent<MCallMemberContent>();
|
||||
|
||||
// state event expired
|
||||
if (expiresTs && expiresTs < now) return false;
|
||||
|
||||
return !!calls?.find((call: MCall) => call["m.call_id"] === callId);
|
||||
}) || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a list of devices from a call.
|
||||
* Only works for the current user's devices.
|
||||
*/
|
||||
const removeDevices = async (client: MatrixClient, callEvent: MatrixEvent, deviceIds: string[]): Promise<void> => {
|
||||
if (!CALL_STATE_EVENT_TYPE.matches(callEvent.getType())) return;
|
||||
|
||||
const roomId = callEvent.getRoomId();
|
||||
const roomState = getRoomState(client, roomId);
|
||||
if (!roomState) return;
|
||||
|
||||
const callMemberEvent = roomState.getStateEvents(CALL_MEMBER_STATE_EVENT_TYPE.name, client.getUserId())
|
||||
?? roomState.getStateEvents(CALL_MEMBER_STATE_EVENT_TYPE.altName, client.getUserId());
|
||||
const callMemberEventContent = callMemberEvent?.getContent<MCallMemberContent>();
|
||||
if (
|
||||
!Array.isArray(callMemberEventContent?.["m.calls"])
|
||||
|| callMemberEventContent?.["m.calls"].length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// copy the content to prevent mutations
|
||||
const newContent = deepCopy(callMemberEventContent);
|
||||
const callId = callEvent.getStateKey();
|
||||
let changed = false;
|
||||
|
||||
newContent["m.calls"].forEach((call: MCall) => {
|
||||
// skip other calls
|
||||
if (call["m.call_id"] !== callId) return;
|
||||
|
||||
call["m.devices"] = call["m.devices"]?.filter((device: MDevice) => {
|
||||
if (deviceIds.includes(device["m.device_id"])) {
|
||||
changed = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
// only send a new state event if there has been a change
|
||||
newContent["m.expires_ts"] = Date.now() + STUCK_DEVICE_TIMEOUT_MS;
|
||||
await client.sendStateEvent(
|
||||
roomId,
|
||||
CALL_MEMBER_STATE_EVENT_TYPE.name,
|
||||
newContent,
|
||||
client.getUserId(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the current device from a call.
|
||||
*/
|
||||
export const removeOurDevice = async (client: MatrixClient, callEvent: MatrixEvent) => {
|
||||
return removeDevices(client, callEvent, [client.getDeviceId()]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all devices of the current user that have not been seen within the STUCK_DEVICE_TIMEOUT_MS.
|
||||
* Does per default not remove the current device unless includeCurrentDevice is true.
|
||||
*
|
||||
* @param {boolean} includeCurrentDevice - Whether to include the current device of this session here.
|
||||
*/
|
||||
export const fixStuckDevices = async (client: MatrixClient, callEvent: MatrixEvent, includeCurrentDevice: boolean) => {
|
||||
const now = Date.now();
|
||||
const { devices: myDevices } = await client.getDevices();
|
||||
const currentDeviceId = client.getDeviceId();
|
||||
const devicesToBeRemoved = myDevices.filter(({ last_seen_ts: lastSeenTs, device_id: deviceId }) => {
|
||||
return lastSeenTs
|
||||
&& (deviceId !== currentDeviceId || includeCurrentDevice)
|
||||
&& (now - lastSeenTs) > STUCK_DEVICE_TIMEOUT_MS;
|
||||
}).map(d => d.device_id);
|
||||
return removeDevices(client, callEvent, devicesToBeRemoved);
|
||||
};
|
|
@ -0,0 +1,673 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { IMyDevice, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
CALL_MEMBER_STATE_EVENT_TYPE,
|
||||
CALL_STATE_EVENT_TYPE,
|
||||
fixStuckDevices,
|
||||
getGroupCall,
|
||||
removeOurDevice,
|
||||
STUCK_DEVICE_TIMEOUT_MS,
|
||||
useConnectedMembers,
|
||||
} from "../../src/utils/GroupCallUtils";
|
||||
import { createTestClient, mkEvent } from "../test-utils";
|
||||
|
||||
[
|
||||
{
|
||||
callStateEventType: CALL_STATE_EVENT_TYPE.name,
|
||||
callMemberStateEventType: CALL_MEMBER_STATE_EVENT_TYPE.name,
|
||||
},
|
||||
{
|
||||
callStateEventType: CALL_STATE_EVENT_TYPE.altName,
|
||||
callMemberStateEventType: CALL_MEMBER_STATE_EVENT_TYPE.altName,
|
||||
},
|
||||
].forEach(({ callStateEventType, callMemberStateEventType }) => {
|
||||
describe(`GroupCallUtils (${callStateEventType}, ${callMemberStateEventType})`, () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let callEvent: MatrixEvent;
|
||||
const callId = "test call";
|
||||
const callId2 = "test call 2";
|
||||
const userId1 = "@user1:example.com";
|
||||
const now = 1654616071686;
|
||||
|
||||
const setUpNonCallStateEvent = () => {
|
||||
callEvent = mkEvent({
|
||||
room: roomId,
|
||||
user: userId1,
|
||||
event: true,
|
||||
type: "test",
|
||||
skey: userId1,
|
||||
content: {},
|
||||
});
|
||||
};
|
||||
|
||||
const setUpEmptyStateKeyCallEvent = () => {
|
||||
callEvent = mkEvent({
|
||||
room: roomId,
|
||||
user: userId1,
|
||||
event: true,
|
||||
type: callStateEventType,
|
||||
skey: "",
|
||||
content: {},
|
||||
});
|
||||
};
|
||||
|
||||
const setUpValidCallEvent = () => {
|
||||
callEvent = mkEvent({
|
||||
room: roomId,
|
||||
user: userId1,
|
||||
event: true,
|
||||
type: callStateEventType,
|
||||
skey: callId,
|
||||
content: {},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
});
|
||||
|
||||
describe("getGroupCall", () => {
|
||||
describe("for a non-existing room", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.getRoom).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("should return null", () => {
|
||||
expect(getGroupCall(client, roomId)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an existing room", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room(roomId, client, client.getUserId());
|
||||
mocked(client.getRoom).mockImplementation((rid: string) => {
|
||||
return rid === roomId
|
||||
? room
|
||||
: null;
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null if no 'call' state event exist", () => {
|
||||
expect(getGroupCall(client, roomId)).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("with call state events", () => {
|
||||
let callEvent1: MatrixEvent;
|
||||
let callEvent2: MatrixEvent;
|
||||
let callEvent3: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
callEvent1 = mkEvent({
|
||||
room: roomId,
|
||||
user: client.getUserId(),
|
||||
event: true,
|
||||
type: callStateEventType,
|
||||
content: {},
|
||||
ts: 150,
|
||||
skey: "call1",
|
||||
});
|
||||
room.getLiveTimeline().addEvent(callEvent1, {
|
||||
toStartOfTimeline: false,
|
||||
});
|
||||
|
||||
callEvent2 = mkEvent({
|
||||
room: roomId,
|
||||
user: client.getUserId(),
|
||||
event: true,
|
||||
type: callStateEventType,
|
||||
content: {},
|
||||
ts: 100,
|
||||
skey: "call2",
|
||||
});
|
||||
room.getLiveTimeline().addEvent(callEvent2, {
|
||||
toStartOfTimeline: false,
|
||||
});
|
||||
|
||||
// terminated call - should never be returned
|
||||
callEvent3 = mkEvent({
|
||||
room: roomId,
|
||||
user: client.getUserId(),
|
||||
event: true,
|
||||
type: callStateEventType,
|
||||
content: {
|
||||
["m.terminated"]: "time's up",
|
||||
},
|
||||
ts: 500,
|
||||
skey: "call3",
|
||||
});
|
||||
room.getLiveTimeline().addEvent(callEvent3, {
|
||||
toStartOfTimeline: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the newest call state event (1)", () => {
|
||||
expect(getGroupCall(client, roomId)).toBe(callEvent1);
|
||||
});
|
||||
|
||||
it("should return the newest call state event (2)", () => {
|
||||
callEvent2.getTs = () => 200;
|
||||
expect(getGroupCall(client, roomId)).toBe(callEvent2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("useConnectedMembers", () => {
|
||||
describe("for a non-call event", () => {
|
||||
beforeEach(() => {
|
||||
setUpNonCallStateEvent();
|
||||
});
|
||||
|
||||
it("should return an empty list", () => {
|
||||
expect(useConnectedMembers(client, callEvent)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an empty state key", () => {
|
||||
beforeEach(() => {
|
||||
setUpEmptyStateKeyCallEvent();
|
||||
});
|
||||
|
||||
it("should return an empty list", () => {
|
||||
expect(useConnectedMembers(client, callEvent)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a valid call state event", () => {
|
||||
beforeEach(() => {
|
||||
setUpValidCallEvent();
|
||||
});
|
||||
|
||||
describe("and a non-existing room", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.getRoom).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("should return an empty list", () => {
|
||||
expect(useConnectedMembers(client, callEvent)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and an existing room", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room(roomId, client, client.getUserId());
|
||||
mocked(client.getRoom).mockImplementation((rid: string) => {
|
||||
return rid === roomId
|
||||
? room
|
||||
: null;
|
||||
});
|
||||
});
|
||||
|
||||
it("should return an empty list if no call member state events exist", () => {
|
||||
expect(useConnectedMembers(client, callEvent)).toEqual([]);
|
||||
});
|
||||
|
||||
describe("and some call member state events", () => {
|
||||
const userId2 = "@user2:example.com";
|
||||
const userId3 = "@user3:example.com";
|
||||
const userId4 = "@user4:example.com";
|
||||
let expectedEvent1: MatrixEvent;
|
||||
let expectedEvent2: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
.setSystemTime(now);
|
||||
|
||||
expectedEvent1 = mkEvent({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: userId1,
|
||||
skey: userId1,
|
||||
type: callMemberStateEventType,
|
||||
content: {
|
||||
["m.expires_ts"]: now + 100,
|
||||
["m.calls"]: [
|
||||
{
|
||||
["m.call_id"]: callId2,
|
||||
},
|
||||
{
|
||||
["m.call_id"]: callId,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
room.getLiveTimeline().addEvent(expectedEvent1, { toStartOfTimeline: false });
|
||||
|
||||
expectedEvent2 = mkEvent({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: userId2,
|
||||
skey: userId2,
|
||||
type: callMemberStateEventType,
|
||||
content: {
|
||||
["m.expires_ts"]: now + 100,
|
||||
["m.calls"]: [
|
||||
{
|
||||
["m.call_id"]: callId,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
room.getLiveTimeline().addEvent(expectedEvent2, { toStartOfTimeline: false });
|
||||
|
||||
// expired event
|
||||
const event3 = mkEvent({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: userId3,
|
||||
skey: userId3,
|
||||
type: callMemberStateEventType,
|
||||
content: {
|
||||
["m.expires_ts"]: now - 100,
|
||||
["m.calls"]: [
|
||||
{
|
||||
["m.call_id"]: callId,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
room.getLiveTimeline().addEvent(event3, { toStartOfTimeline: false });
|
||||
|
||||
// other call
|
||||
const event4 = mkEvent({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: userId4,
|
||||
skey: userId4,
|
||||
type: callMemberStateEventType,
|
||||
content: {
|
||||
["m.expires_ts"]: now + 100,
|
||||
["m.calls"]: [
|
||||
{
|
||||
["m.call_id"]: callId2,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
room.getLiveTimeline().addEvent(event4, { toStartOfTimeline: false });
|
||||
|
||||
// empty calls
|
||||
const event5 = mkEvent({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: userId4,
|
||||
skey: userId4,
|
||||
type: callMemberStateEventType,
|
||||
content: {
|
||||
["m.expires_ts"]: now + 100,
|
||||
["m.calls"]: [],
|
||||
},
|
||||
});
|
||||
room.getLiveTimeline().addEvent(event5, { toStartOfTimeline: false });
|
||||
|
||||
// no calls prop
|
||||
const event6 = mkEvent({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: userId4,
|
||||
skey: userId4,
|
||||
type: callMemberStateEventType,
|
||||
content: {
|
||||
["m.expires_ts"]: now + 100,
|
||||
},
|
||||
});
|
||||
room.getLiveTimeline().addEvent(event6, { toStartOfTimeline: false });
|
||||
});
|
||||
|
||||
it("should return the expected call member events", () => {
|
||||
const callMemberEvents = useConnectedMembers(client, callEvent);
|
||||
expect(callMemberEvents).toHaveLength(2);
|
||||
expect(callMemberEvents).toContain(expectedEvent1);
|
||||
expect(callMemberEvents).toContain(expectedEvent2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeOurDevice", () => {
|
||||
describe("for a non-call event", () => {
|
||||
beforeEach(() => {
|
||||
setUpNonCallStateEvent();
|
||||
});
|
||||
|
||||
it("should not update the state", () => {
|
||||
removeOurDevice(client, callEvent);
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an empty state key", () => {
|
||||
beforeEach(() => {
|
||||
setUpEmptyStateKeyCallEvent();
|
||||
});
|
||||
|
||||
it("should not update the state", () => {
|
||||
removeOurDevice(client, callEvent);
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a valid call state event", () => {
|
||||
beforeEach(() => {
|
||||
setUpValidCallEvent();
|
||||
});
|
||||
|
||||
describe("and a non-existing room", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.getRoom).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("should not update the state", () => {
|
||||
removeOurDevice(client, callEvent);
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and an existing room", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room(roomId, client, client.getUserId());
|
||||
room.getLiveTimeline().addEvent(callEvent, { toStartOfTimeline: false });
|
||||
mocked(client.getRoom).mockImplementation((rid: string) => {
|
||||
return rid === roomId
|
||||
? room
|
||||
: null;
|
||||
});
|
||||
});
|
||||
|
||||
it("should not update the state if no call member event exists", () => {
|
||||
removeOurDevice(client, callEvent);
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("and a call member state event", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
.setSystemTime(now);
|
||||
|
||||
const callMemberEvent = mkEvent({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: client.getUserId(),
|
||||
skey: client.getUserId(),
|
||||
type: callMemberStateEventType,
|
||||
content: {
|
||||
["m.expires_ts"]: now - 100,
|
||||
["m.calls"]: [
|
||||
{
|
||||
["m.call_id"]: callId,
|
||||
["m.devices"]: [
|
||||
// device to be removed
|
||||
{ "m.device_id": client.getDeviceId() },
|
||||
{ "m.device_id": "device 2" },
|
||||
],
|
||||
},
|
||||
{
|
||||
// no device list
|
||||
["m.call_id"]: callId,
|
||||
},
|
||||
{
|
||||
// other call
|
||||
["m.call_id"]: callId2,
|
||||
["m.devices"]: [
|
||||
{ "m.device_id": client.getDeviceId() },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
room.getLiveTimeline().addEvent(callMemberEvent, { toStartOfTimeline: false });
|
||||
});
|
||||
|
||||
it("should remove the device from the call", async () => {
|
||||
await removeOurDevice(client, callEvent);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
CALL_MEMBER_STATE_EVENT_TYPE.name,
|
||||
{
|
||||
["m.expires_ts"]: now + STUCK_DEVICE_TIMEOUT_MS,
|
||||
["m.calls"]: [
|
||||
{
|
||||
["m.call_id"]: callId,
|
||||
["m.devices"]: [
|
||||
{ "m.device_id": "device 2" },
|
||||
],
|
||||
},
|
||||
{
|
||||
// no device list
|
||||
["m.call_id"]: callId,
|
||||
},
|
||||
{
|
||||
// other call
|
||||
["m.call_id"]: callId2,
|
||||
["m.devices"]: [
|
||||
{ "m.device_id": client.getDeviceId() },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
client.getUserId(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fixStuckDevices", () => {
|
||||
let thisDevice: IMyDevice;
|
||||
let otherDevice: IMyDevice;
|
||||
let noLastSeenTsDevice: IMyDevice;
|
||||
let stuckDevice: IMyDevice;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
.setSystemTime(now);
|
||||
|
||||
thisDevice = { device_id: "ABCDEFGHI", last_seen_ts: now - STUCK_DEVICE_TIMEOUT_MS - 100 };
|
||||
otherDevice = { device_id: "ABCDEFGHJ", last_seen_ts: now };
|
||||
noLastSeenTsDevice = { device_id: "ABCDEFGHK" };
|
||||
stuckDevice = { device_id: "ABCDEFGHL", last_seen_ts: now - STUCK_DEVICE_TIMEOUT_MS - 100 };
|
||||
|
||||
mocked(client.getDeviceId).mockReturnValue(thisDevice.device_id);
|
||||
mocked(client.getDevices).mockResolvedValue({
|
||||
devices: [
|
||||
thisDevice,
|
||||
otherDevice,
|
||||
noLastSeenTsDevice,
|
||||
stuckDevice,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a non-call event", () => {
|
||||
beforeEach(() => {
|
||||
setUpNonCallStateEvent();
|
||||
});
|
||||
|
||||
it("should not update the state", () => {
|
||||
fixStuckDevices(client, callEvent, true);
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an empty state key", () => {
|
||||
beforeEach(() => {
|
||||
setUpEmptyStateKeyCallEvent();
|
||||
});
|
||||
|
||||
it("should not update the state", () => {
|
||||
fixStuckDevices(client, callEvent, true);
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a valid call state event", () => {
|
||||
beforeEach(() => {
|
||||
setUpValidCallEvent();
|
||||
});
|
||||
|
||||
describe("and a non-existing room", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.getRoom).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("should not update the state", () => {
|
||||
fixStuckDevices(client, callEvent, true);
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and an existing room", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room(roomId, client, client.getUserId());
|
||||
room.getLiveTimeline().addEvent(callEvent, { toStartOfTimeline: false });
|
||||
mocked(client.getRoom).mockImplementation((rid: string) => {
|
||||
return rid === roomId
|
||||
? room
|
||||
: null;
|
||||
});
|
||||
});
|
||||
|
||||
it("should not update the state if no call member event exists", () => {
|
||||
fixStuckDevices(client, callEvent, true);
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("and a call member state event", () => {
|
||||
beforeEach(() => {
|
||||
const callMemberEvent = mkEvent({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: client.getUserId(),
|
||||
skey: client.getUserId(),
|
||||
type: callMemberStateEventType,
|
||||
content: {
|
||||
["m.expires_ts"]: now - 100,
|
||||
["m.calls"]: [
|
||||
{
|
||||
["m.call_id"]: callId,
|
||||
["m.devices"]: [
|
||||
{ "m.device_id": thisDevice.device_id },
|
||||
{ "m.device_id": otherDevice.device_id },
|
||||
{ "m.device_id": noLastSeenTsDevice.device_id },
|
||||
{ "m.device_id": stuckDevice.device_id },
|
||||
],
|
||||
},
|
||||
{
|
||||
// no device list
|
||||
["m.call_id"]: callId,
|
||||
},
|
||||
{
|
||||
// other call
|
||||
["m.call_id"]: callId2,
|
||||
["m.devices"]: [
|
||||
{ "m.device_id": stuckDevice.device_id },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
room.getLiveTimeline().addEvent(callMemberEvent, { toStartOfTimeline: false });
|
||||
});
|
||||
|
||||
it("should remove stuck devices from the call, except this device", async () => {
|
||||
await fixStuckDevices(client, callEvent, false);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
CALL_MEMBER_STATE_EVENT_TYPE.name,
|
||||
{
|
||||
["m.expires_ts"]: now + STUCK_DEVICE_TIMEOUT_MS,
|
||||
["m.calls"]: [
|
||||
{
|
||||
["m.call_id"]: callId,
|
||||
["m.devices"]: [
|
||||
{ "m.device_id": thisDevice.device_id },
|
||||
{ "m.device_id": otherDevice.device_id },
|
||||
{ "m.device_id": noLastSeenTsDevice.device_id },
|
||||
],
|
||||
},
|
||||
{
|
||||
// no device list
|
||||
["m.call_id"]: callId,
|
||||
},
|
||||
{
|
||||
// other call
|
||||
["m.call_id"]: callId2,
|
||||
["m.devices"]: [
|
||||
{ "m.device_id": stuckDevice.device_id },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
client.getUserId(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should remove stuck devices from the call, including this device", async () => {
|
||||
await fixStuckDevices(client, callEvent, true);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
CALL_MEMBER_STATE_EVENT_TYPE.name,
|
||||
{
|
||||
["m.expires_ts"]: now + STUCK_DEVICE_TIMEOUT_MS,
|
||||
["m.calls"]: [
|
||||
{
|
||||
["m.call_id"]: callId,
|
||||
["m.devices"]: [
|
||||
{ "m.device_id": otherDevice.device_id },
|
||||
{ "m.device_id": noLastSeenTsDevice.device_id },
|
||||
],
|
||||
},
|
||||
{
|
||||
// no device list
|
||||
["m.call_id"]: callId,
|
||||
},
|
||||
{
|
||||
// other call
|
||||
["m.call_id"]: callId2,
|
||||
["m.devices"]: [
|
||||
{ "m.device_id": stuckDevice.device_id },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
client.getUserId(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue