-
-
Notifications
You must be signed in to change notification settings - Fork 582
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Live location sharing - create m.beacon_info events (#2238)
* add content helpers Signed-off-by: Kerry Archibald <kerrya@element.io> * stubbed Beacon class Signed-off-by: Kerry Archibald <kerrya@element.io> * beacon test utils Signed-off-by: Kerry Archibald <kerrya@element.io> * add beacon test utils Signed-off-by: Kerry Archibald <kerrya@element.io> * copyrights Signed-off-by: Kerry Archibald <kerrya@element.io> * add beacons to room state Signed-off-by: Kerry Archibald <kerrya@element.io> * tidy comments Signed-off-by: Kerry Archibald <kerrya@element.io> * unit test RoomState.setBeacon Signed-off-by: Kerry Archibald <kerrya@element.io>
- Loading branch information
Kerry
committed
Mar 15, 2022
1 parent
57d71cc
commit c2fdb44
Showing
8 changed files
with
467 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
/* | ||
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 { MatrixEvent } from "../../src"; | ||
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon"; | ||
import { LocationAssetType } from "../../src/@types/location"; | ||
import { | ||
makeBeaconContent, | ||
makeBeaconInfoContent, | ||
} from "../../src/content-helpers"; | ||
|
||
type InfoContentProps = { | ||
timeout: number; | ||
isLive?: boolean; | ||
assetType?: LocationAssetType; | ||
description?: string; | ||
}; | ||
const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = { | ||
timeout: 3600000, | ||
}; | ||
|
||
/** | ||
* Create an m.beacon_info event | ||
* all required properties are mocked | ||
* override with contentProps | ||
*/ | ||
export const makeBeaconInfoEvent = ( | ||
sender: string, | ||
roomId: string, | ||
contentProps: Partial<InfoContentProps> = {}, | ||
eventId?: string, | ||
): MatrixEvent => { | ||
const { | ||
timeout, isLive, description, assetType, | ||
} = { | ||
...DEFAULT_INFO_CONTENT_PROPS, | ||
...contentProps, | ||
}; | ||
const event = new MatrixEvent({ | ||
type: `${M_BEACON_INFO.name}.${sender}`, | ||
room_id: roomId, | ||
state_key: sender, | ||
content: makeBeaconInfoContent(timeout, isLive, description, assetType), | ||
}); | ||
|
||
// live beacons use the beacon_info event id | ||
// set or default this | ||
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`); | ||
|
||
return event; | ||
}; | ||
|
||
type ContentProps = { | ||
uri: string; | ||
timestamp: number; | ||
beaconInfoId: string; | ||
description?: string; | ||
}; | ||
const DEFAULT_CONTENT_PROPS: ContentProps = { | ||
uri: 'geo:-36.24484561954707,175.46884959563613;u=10', | ||
timestamp: 123, | ||
beaconInfoId: '$123', | ||
}; | ||
|
||
/** | ||
* Create an m.beacon event | ||
* all required properties are mocked | ||
* override with contentProps | ||
*/ | ||
export const makeBeaconEvent = ( | ||
sender: string, | ||
contentProps: Partial<ContentProps> = {}, | ||
): MatrixEvent => { | ||
const { uri, timestamp, beaconInfoId, description } = { | ||
...DEFAULT_CONTENT_PROPS, | ||
...contentProps, | ||
}; | ||
|
||
return new MatrixEvent({ | ||
type: M_BEACON.name, | ||
sender, | ||
content: makeBeaconContent(uri, timestamp, beaconInfoId, description), | ||
}); | ||
}; | ||
|
||
/** | ||
* Create a mock geolocation position | ||
* defaults all required properties | ||
*/ | ||
export const makeGeolocationPosition = ( | ||
{ timestamp, coords }: | ||
{ timestamp?: number, coords: Partial<GeolocationCoordinates> }, | ||
): GeolocationPosition => ({ | ||
timestamp: timestamp ?? 1647256791840, | ||
coords: { | ||
accuracy: 1, | ||
latitude: 54.001927, | ||
longitude: -8.253491, | ||
altitude: null, | ||
altitudeAccuracy: null, | ||
heading: null, | ||
speed: null, | ||
...coords, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
/* | ||
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 { EventType } from "../../../src"; | ||
import { M_BEACON_INFO } from "../../../src/@types/beacon"; | ||
import { | ||
isTimestampInDuration, | ||
isBeaconInfoEventType, | ||
Beacon, | ||
BeaconEvent, | ||
} from "../../../src/models/beacon"; | ||
import { makeBeaconInfoEvent } from "../../test-utils/beacon"; | ||
|
||
describe('Beacon', () => { | ||
describe('isTimestampInDuration()', () => { | ||
const startTs = new Date('2022-03-11T12:07:47.592Z').getTime(); | ||
const HOUR_MS = 3600000; | ||
it('returns false when timestamp is before start time', () => { | ||
// day before | ||
const timestamp = new Date('2022-03-10T12:07:47.592Z').getTime(); | ||
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false); | ||
}); | ||
|
||
it('returns false when timestamp is after start time + duration', () => { | ||
// 1 second later | ||
const timestamp = new Date('2022-03-10T12:07:48.592Z').getTime(); | ||
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false); | ||
}); | ||
|
||
it('returns true when timestamp is exactly start time', () => { | ||
expect(isTimestampInDuration(startTs, HOUR_MS, startTs)).toBe(true); | ||
}); | ||
|
||
it('returns true when timestamp is exactly the end of the duration', () => { | ||
expect(isTimestampInDuration(startTs, HOUR_MS, startTs + HOUR_MS)).toBe(true); | ||
}); | ||
|
||
it('returns true when timestamp is within the duration', () => { | ||
const twoHourDuration = HOUR_MS * 2; | ||
const now = startTs + HOUR_MS; | ||
expect(isTimestampInDuration(startTs, twoHourDuration, now)).toBe(true); | ||
}); | ||
}); | ||
|
||
describe('isBeaconInfoEventType', () => { | ||
it.each([ | ||
EventType.CallAnswer, | ||
`prefix.${M_BEACON_INFO.name}`, | ||
`prefix.${M_BEACON_INFO.altName}`, | ||
])('returns false for %s', (type) => { | ||
expect(isBeaconInfoEventType(type)).toBe(false); | ||
}); | ||
|
||
it.each([ | ||
M_BEACON_INFO.name, | ||
M_BEACON_INFO.altName, | ||
`${M_BEACON_INFO.name}.@test:server.org.12345`, | ||
`${M_BEACON_INFO.altName}.@test:server.org.12345`, | ||
])('returns true for %s', (type) => { | ||
expect(isBeaconInfoEventType(type)).toBe(true); | ||
}); | ||
}); | ||
|
||
describe('Beacon', () => { | ||
const userId = '@user:server.org'; | ||
const roomId = '$room:server.org'; | ||
// 14.03.2022 16:15 | ||
const now = 1647270879403; | ||
const HOUR_MS = 3600000; | ||
|
||
// beacon_info events | ||
// created 'an hour ago' | ||
// without timeout of 3 hours | ||
let liveBeaconEvent; | ||
let notLiveBeaconEvent; | ||
beforeEach(() => { | ||
// go back in time to create the beacon | ||
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS); | ||
liveBeaconEvent = makeBeaconInfoEvent(userId, roomId, { timeout: HOUR_MS * 3, isLive: true }, '$live123'); | ||
notLiveBeaconEvent = makeBeaconInfoEvent( | ||
userId, | ||
roomId, | ||
{ timeout: HOUR_MS * 3, isLive: false }, | ||
'$dead123', | ||
); | ||
|
||
// back to now | ||
jest.spyOn(global.Date, 'now').mockReturnValue(now); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.spyOn(global.Date, 'now').mockRestore(); | ||
}); | ||
|
||
it('creates beacon from event', () => { | ||
const beacon = new Beacon(liveBeaconEvent); | ||
|
||
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId()); | ||
expect(beacon.isLive).toEqual(true); | ||
}); | ||
|
||
describe('isLive()', () => { | ||
it('returns false when beacon is explicitly set to not live', () => { | ||
const beacon = new Beacon(notLiveBeaconEvent); | ||
expect(beacon.isLive).toEqual(false); | ||
}); | ||
|
||
it('returns false when beacon is expired', () => { | ||
// time travel to beacon creation + 3 hours | ||
jest.spyOn(global.Date, 'now').mockReturnValue(now - 3 * HOUR_MS); | ||
const beacon = new Beacon(liveBeaconEvent); | ||
expect(beacon.isLive).toEqual(false); | ||
}); | ||
|
||
it('returns false when beacon timestamp is in future', () => { | ||
// time travel to before beacon events timestamp | ||
// event was created now - 1 hour | ||
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS - HOUR_MS); | ||
const beacon = new Beacon(liveBeaconEvent); | ||
expect(beacon.isLive).toEqual(false); | ||
}); | ||
|
||
it('returns true when beacon was created in past and not yet expired', () => { | ||
// liveBeaconEvent was created 1 hour ago | ||
const beacon = new Beacon(liveBeaconEvent); | ||
expect(beacon.isLive).toEqual(true); | ||
}); | ||
}); | ||
|
||
describe('update()', () => { | ||
it('does not update with different event', () => { | ||
const beacon = new Beacon(liveBeaconEvent); | ||
|
||
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId()); | ||
|
||
expect(() => beacon.update(notLiveBeaconEvent)).toThrow(); | ||
expect(beacon.isLive).toEqual(true); | ||
}); | ||
|
||
it('updates event', () => { | ||
const beacon = new Beacon(liveBeaconEvent); | ||
const emitSpy = jest.spyOn(beacon, 'emit'); | ||
|
||
expect(beacon.isLive).toEqual(true); | ||
|
||
const updatedBeaconEvent = makeBeaconInfoEvent( | ||
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123'); | ||
|
||
beacon.update(updatedBeaconEvent); | ||
expect(beacon.isLive).toEqual(false); | ||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Update, updatedBeaconEvent, beacon); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.