From ab934a2a0831c68808c9d478c0c4f3029cc9de13 Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 18 Mar 2022 14:38:41 +0100 Subject: [PATCH] kill beacons on expiry (#8075) Signed-off-by: Kerry Archibald --- src/stores/OwnBeaconStore.ts | 39 ++++++++++- test/stores/OwnBeaconStore-test.ts | 104 ++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index e42bdc9b04..6bd256be2c 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -20,6 +20,9 @@ import { MatrixEvent, Room, } from "matrix-js-sdk/src/matrix"; +import { + BeaconInfoState, makeBeaconInfoContent, +} from "matrix-js-sdk/src/content-helpers"; import defaultDispatcher from "../dispatcher/dispatcher"; import { ActionPayload } from "../dispatcher/payloads"; @@ -83,6 +86,17 @@ export class OwnBeaconStore extends AsyncStoreWithClient { return this.liveBeaconIds.filter(beaconId => this.beaconsByRoomId.get(roomId)?.has(beaconId)); } + public stopBeacon = async (beaconInfoId: string): Promise => { + const beacon = this.beacons.get(beaconInfoId); + // if no beacon, or beacon is already explicitly set isLive: false + // do nothing + if (!beacon?.beaconInfo?.live) { + return; + } + + return await this.updateBeaconEvent(beacon, { live: false }); + }; + private onNewBeacon = (_event: MatrixEvent, beacon: Beacon): void => { if (!isOwnBeacon(beacon, this.matrixClient.getUserId())) { return; @@ -106,9 +120,14 @@ export class OwnBeaconStore extends AsyncStoreWithClient { this.liveBeaconIds.push(beacon.beaconInfoId); } + // beacon expired, update beacon to un-alive state + if (!isLive) { + this.stopBeacon(beacon.beaconInfoId); + } + + // TODO start location polling here + this.emit(OwnBeaconStoreEvent.LivenessChange, this.hasLiveBeacons()); - // TODO stop or start polling here - // if not content is live but beacon is not, update state event with live: false }; private initialiseBeaconState = () => { @@ -134,6 +153,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { } this.beaconsByRoomId.get(beacon.roomId).add(beacon.beaconInfoId); + beacon.monitorLiveness(); }; @@ -149,4 +169,19 @@ export class OwnBeaconStore extends AsyncStoreWithClient { this.emit(OwnBeaconStoreEvent.LivenessChange, newLiveness); } }; + + private updateBeaconEvent = async (beacon: Beacon, update: Partial): Promise => { + const { description, timeout, timestamp, live, assetType } = { + ...beacon.beaconInfo, + ...update, + }; + + const updateContent = makeBeaconInfoContent(timeout, + live, + description, + assetType, + timestamp); + + await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent); + }; } diff --git a/test/stores/OwnBeaconStore-test.ts b/test/stores/OwnBeaconStore-test.ts index c334436ee3..fcbd91b1ee 100644 --- a/test/stores/OwnBeaconStore-test.ts +++ b/test/stores/OwnBeaconStore-test.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { Room, Beacon, BeaconEvent } from "matrix-js-sdk/src/matrix"; +import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../src/stores/OwnBeaconStore"; import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../test-utils"; @@ -33,6 +34,7 @@ describe('OwnBeaconStore', () => { const mockClient = getMockClientWithEventEmitter({ getUserId: jest.fn().mockReturnValue(aliceId), getVisibleRooms: jest.fn().mockReturnValue([]), + unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }), }); const room1Id = '$room1:server.org'; const room2Id = '$room2:server.org'; @@ -78,6 +80,7 @@ describe('OwnBeaconStore', () => { beforeEach(() => { mockClient.getVisibleRooms.mockReturnValue([]); + mockClient.unstable_setLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' }); jest.spyOn(global.Date, 'now').mockReturnValue(now); jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore(); }); @@ -335,7 +338,7 @@ describe('OwnBeaconStore', () => { expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds); }); - it('updates state and when beacon liveness changes from true to false', async () => { + it('updates state and emits beacon liveness changes from true to false', async () => { makeRoomsWithStateEvents([ alicesRoom1BeaconInfo, ]); @@ -356,6 +359,35 @@ describe('OwnBeaconStore', () => { expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, false); }); + it('stops beacon when liveness changes from true to false and beacon is expired', async () => { + makeRoomsWithStateEvents([ + alicesRoom1BeaconInfo, + ]); + await makeOwnBeaconStore(); + const alicesBeacon = new Beacon(alicesRoom1BeaconInfo); + const prevEventContent = alicesRoom1BeaconInfo.getContent(); + + // time travel until beacon is expired + advanceDateAndTime(HOUR_MS * 3); + + mockClient.emit(BeaconEvent.LivenessChange, false, alicesBeacon); + + // matches original state of event content + // except for live property + const expectedUpdateContent = { + ...prevEventContent, + [M_BEACON_INFO.name]: { + ...prevEventContent[M_BEACON_INFO.name], + live: false, + }, + }; + expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith( + room1Id, + alicesRoom1BeaconInfo.getType(), + expectedUpdateContent, + ); + }); + it('updates state and when beacon liveness changes from false to true', async () => { makeRoomsWithStateEvents([ alicesOldRoomIdBeaconInfo, @@ -381,9 +413,75 @@ describe('OwnBeaconStore', () => { }); }); - describe('on LivenessChange event', () => { - it('ignores events for irrelevant beacons', async () => { + describe('stopBeacon()', () => { + beforeEach(() => { + makeRoomsWithStateEvents([ + alicesRoom1BeaconInfo, + alicesOldRoomIdBeaconInfo, + ]); + }); + it('does nothing for an unknown beacon id', async () => { + const store = await makeOwnBeaconStore(); + await store.stopBeacon('randomBeaconId'); + expect(mockClient.unstable_setLiveBeacon).not.toHaveBeenCalled(); + }); + + it('does nothing for a beacon that is already not live', async () => { + const store = await makeOwnBeaconStore(); + await store.stopBeacon(alicesOldRoomIdBeaconInfo.getId()); + expect(mockClient.unstable_setLiveBeacon).not.toHaveBeenCalled(); + }); + + it('updates beacon to live:false when it is unexpired', async () => { + const store = await makeOwnBeaconStore(); + + await store.stopBeacon(alicesOldRoomIdBeaconInfo.getId()); + const prevEventContent = alicesRoom1BeaconInfo.getContent(); + + await store.stopBeacon(alicesRoom1BeaconInfo.getId()); + + // matches original state of event content + // except for live property + const expectedUpdateContent = { + ...prevEventContent, + [M_BEACON_INFO.name]: { + ...prevEventContent[M_BEACON_INFO.name], + live: false, + }, + }; + expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith( + room1Id, + alicesRoom1BeaconInfo.getType(), + expectedUpdateContent, + ); + }); + + it('updates beacon to live:false when it is expired but live property is true', async () => { + const store = await makeOwnBeaconStore(); + + await store.stopBeacon(alicesOldRoomIdBeaconInfo.getId()); + const prevEventContent = alicesRoom1BeaconInfo.getContent(); + + // time travel until beacon is expired + advanceDateAndTime(HOUR_MS * 3); + + await store.stopBeacon(alicesRoom1BeaconInfo.getId()); + + // matches original state of event content + // except for live property + const expectedUpdateContent = { + ...prevEventContent, + [M_BEACON_INFO.name]: { + ...prevEventContent[M_BEACON_INFO.name], + live: false, + }, + }; + expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith( + room1Id, + alicesRoom1BeaconInfo.getType(), + expectedUpdateContent, + ); }); }); });