Live location sharing - stop sharing to beacons in rooms you left (#8187)

* remove beacons on membership changes

* add addMembershipToMockedRoom test util

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

* test remove beacons on membership changes

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

* removelistener

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-03-29 18:18:34 +02:00 committed by GitHub
parent e161f0b17b
commit 2adc972eec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 269 additions and 60 deletions

View file

@ -20,6 +20,9 @@ import {
BeaconEvent,
MatrixEvent,
Room,
RoomMember,
RoomState,
RoomStateEvent,
} from "matrix-js-sdk/src/matrix";
import {
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
@ -90,6 +93,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
protected async onNotReady() {
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);
this.beacons.forEach(beacon => beacon.destroy());
@ -102,6 +106,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
protected async onReady(): Promise<void> {
this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness);
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);
this.initialiseBeaconState();
}
@ -136,6 +141,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
return await this.updateBeaconEvent(beacon, { live: false });
};
/**
* Listeners
*/
private onNewBeacon = (_event: MatrixEvent, beacon: Beacon): void => {
if (!isOwnBeacon(beacon, this.matrixClient.getUserId())) {
return;
@ -160,6 +169,33 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.emit(OwnBeaconStoreEvent.LivenessChange, this.getLiveBeaconIds());
};
/**
* Check for changes in membership in rooms with beacons
* and stop monitoring beacons in rooms user is no longer member of
*/
private onRoomStateMembers = (_event: MatrixEvent, roomState: RoomState, member: RoomMember): void => {
// no beacons for this room, ignore
if (
!this.beaconsByRoomId.has(roomState.roomId) ||
member.userId !== this.matrixClient.getUserId()
) {
return;
}
// TODO check powerlevels here
// in PSF-797
// stop watching beacons in rooms where user is no longer a member
if (member.membership === 'leave' || member.membership === 'ban') {
this.beaconsByRoomId.get(roomState.roomId).forEach(this.removeBeacon);
this.beaconsByRoomId.delete(roomState.roomId);
}
};
/**
* State management
*/
private initialiseBeaconState = () => {
const userId = this.matrixClient.getUserId();
const visibleRooms = this.matrixClient.getVisibleRooms();
@ -187,6 +223,21 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
beacon.monitorLiveness();
};
/**
* Remove listeners for a given beacon
* remove from state
* and update liveness if changed
*/
private removeBeacon = (beaconId: string): void => {
if (!this.beacons.has(beaconId)) {
return;
}
this.beacons.get(beaconId).destroy();
this.beacons.delete(beaconId);
this.checkLiveness();
};
private checkLiveness = (): void => {
const prevLiveBeaconIds = this.getLiveBeaconIds();
this.liveBeaconIds = [...this.beacons.values()]
@ -218,20 +269,9 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
}
};
private updateBeaconEvent = async (beacon: Beacon, update: Partial<BeaconInfoState>): Promise<void> => {
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);
};
/**
* Geolocation
*/
private togglePollingLocation = () => {
if (!!this.liveBeaconIds.length) {
@ -270,17 +310,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
};
private onWatchedPosition = (position: GeolocationPosition) => {
const timedGeoPosition = mapGeolocationPositionToTimedGeo(position);
// if this is our first position, publish immediateley
if (!this.lastPublishedPositionTimestamp) {
this.publishLocationToBeacons(timedGeoPosition);
} else {
this.debouncedPublishLocationToBeacons(timedGeoPosition);
}
};
private stopPollingLocation = () => {
clearInterval(this.locationInterval);
this.locationInterval = undefined;
@ -295,6 +324,70 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
};
private onWatchedPosition = (position: GeolocationPosition) => {
const timedGeoPosition = mapGeolocationPositionToTimedGeo(position);
// if this is our first position, publish immediateley
if (!this.lastPublishedPositionTimestamp) {
this.publishLocationToBeacons(timedGeoPosition);
} else {
this.debouncedPublishLocationToBeacons(timedGeoPosition);
}
};
private onGeolocationError = async (error: GeolocationError): Promise<void> => {
this.geolocationError = error;
logger.error('Geolocation failed', this.geolocationError);
// other errors are considered non-fatal
// and self recovering
if (![
GeolocationError.Unavailable,
GeolocationError.PermissionDenied,
].includes(error)) {
return;
}
this.stopPollingLocation();
// kill live beacons when location permissions are revoked
// TODO may need adjustment when PSF-797 is done
await Promise.all(this.liveBeaconIds.map(this.stopBeacon));
};
/**
* Gets the current location
* (as opposed to using watched location)
* and publishes it to all live beacons
*/
private publishCurrentLocationToBeacons = async () => {
try {
const position = await getCurrentPosition();
// TODO error handling
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
} catch (error) {
this.onGeolocationError(error?.message);
}
};
/**
* MatrixClient api
*/
private updateBeaconEvent = async (beacon: Beacon, update: Partial<BeaconInfoState>): Promise<void> => {
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);
};
/**
* Sends m.location events to all live beacons
* Sets last published beacon
@ -316,38 +409,4 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
const content = makeBeaconContent(geoUri, timestamp, beacon.beaconInfoId);
await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content);
};
/**
* Gets the current location
* (as opposed to using watched location)
* and publishes it to all live beacons
*/
private publishCurrentLocationToBeacons = async () => {
try {
const position = await getCurrentPosition();
// TODO error handling
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
} catch (error) {
this.onGeolocationError(error?.message);
}
};
private onGeolocationError = async (error: GeolocationError): Promise<void> => {
this.geolocationError = error;
logger.error('Geolocation failed', this.geolocationError);
// other errors are considered non-fatal
// and self recovering
if (![
GeolocationError.Unavailable,
GeolocationError.PermissionDenied,
].includes(error)) {
return;
}
this.stopPollingLocation();
// kill live beacons when location permissions are revoked
// TODO may need adjustment when PSF-797 is done
await Promise.all(this.liveBeaconIds.map(this.stopBeacon));
};
}