Skip to content

Commit

Permalink
Live location sharing - handle redacted beacons (#2269)
Browse files Browse the repository at this point in the history
* emit beacon destroy event on destroy

Signed-off-by: Kerry Archibald <kerrya@element.io>

* handle redacted beacon events in room-state

Signed-off-by: Kerry Archibald <kerrya@element.io>

* empty line

Signed-off-by: Kerry Archibald <kerrya@element.io>
  • Loading branch information
Kerry authored Apr 4, 2022
1 parent 106f7be commit 71b7521
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 40 deletions.
8 changes: 6 additions & 2 deletions spec/unit/models/beacon.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ describe('Beacon', () => {
expect(beacon.livenessWatchInterval).not.toEqual(oldMonitor);
});

it('destroy kills liveness monitor', () => {
it('destroy kills liveness monitor and emits', () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
Expand All @@ -267,10 +267,14 @@ describe('Beacon', () => {

// destroy the beacon
beacon.destroy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Destroy, beacon.identifier);
// live forced to false
expect(beacon.isLive).toBe(false);

advanceDateAndTime(HOUR_MS * 2 + 1);

expect(emitSpy).not.toHaveBeenCalled();
// no additional calls
expect(emitSpy).toHaveBeenCalledTimes(1);
});
});
});
Expand Down
109 changes: 72 additions & 37 deletions spec/unit/room-state.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,56 +252,91 @@ describe("RoomState", function() {
);
});

it('adds new beacon info events to state and emits', () => {
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
const emitSpy = jest.spyOn(state, 'emit');
describe('beacon events', () => {
it('adds new beacon info events to state and emits', () => {
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
const emitSpy = jest.spyOn(state, 'emit');

state.setStateEvents([beaconEvent]);
state.setStateEvents([beaconEvent]);

expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});
expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});

it('updates existing beacon info events in state', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId, beaconId);
it('does not add redacted beacon info events to state', () => {
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
redactedBeaconEvent.makeRedacted(redactionEvent);
const emitSpy = jest.spyOn(state, 'emit');

state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]);

state.setStateEvents([updatedBeaconEvent]);
// no beacon added
expect(state.beacons.size).toEqual(0);
expect(state.beacons.get(redactedBeaconEvent.getType)).toBeFalsy();
// no new beacon emit
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
});

// same Beacon
expect(state.beacons.get(beaconEvent.getType())).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(beaconEvent.getType()).isLive).toEqual(false);
});
it('updates existing beacon info events in state', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId, beaconId);

it('updates live beacon ids once after setting state events', () => {
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1', '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, '$beacon2', '$beacon2');
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance.isLive).toEqual(true);

const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([updatedBeaconEvent]);

state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
// same Beacon
expect(state.beacons.get(beaconEvent.getType())).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(beaconEvent.getType()).isLive).toEqual(false);
});

// called once
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
redactedBeaconEvent.makeRedacted(redactionEvent);

// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
expect(beaconInstance.isLive).toEqual(true);

state.setStateEvents([updatedLiveBeaconEvent]);
state.setStateEvents([redactedBeaconEvent]);

expect(destroySpy).toHaveBeenCalled();
expect(state.beacons.get(beaconEvent.getType())).toBe(undefined);
});

expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(2);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
it('updates live beacon ids once after setting state events', () => {
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1', '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, '$beacon2', '$beacon2');

const emitSpy = jest.spyOn(state, 'emit');

state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);

// called once
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);

// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
);

state.setStateEvents([updatedLiveBeaconEvent]);

expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(2);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
});
});
});

Expand Down
5 changes: 5 additions & 0 deletions src/models/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ export enum BeaconEvent {
New = "Beacon.new",
Update = "Beacon.update",
LivenessChange = "Beacon.LivenessChange",
Destroy = "Destroy",
}

export type BeaconEventHandlerMap = {
[BeaconEvent.Update]: (event: MatrixEvent, beacon: Beacon) => void;
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
};

export const isTimestampInDuration = (
Expand Down Expand Up @@ -93,6 +95,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
if (this.livenessWatchInterval) {
clearInterval(this.livenessWatchInterval);
}

this._isLive = false;
this.emit(BeaconEvent.Destroy, this.identifier);
}

/**
Expand Down
15 changes: 14 additions & 1 deletion src/models/room-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,14 +438,27 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
*/
private setBeacon(event: MatrixEvent): void {
if (this.beacons.has(event.getType())) {
return this.beacons.get(event.getType()).update(event);
const beacon = this.beacons.get(event.getType());

if (event.isRedacted()) {
beacon.destroy();
this.beacons.delete(event.getType());
return;
}

return beacon.update(event);
}

if (event.isRedacted()) {
return;
}

const beacon = new Beacon(event);

this.reEmitter.reEmit<BeaconEvent, BeaconEvent>(beacon, [
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);

Expand Down
1 change: 1 addition & 0 deletions src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export class SyncApi {
RoomStateEvent.Update,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);

Expand Down

0 comments on commit 71b7521

Please sign in to comment.