diff --git a/spec/unit/content-helpers.spec.ts b/spec/unit/content-helpers.spec.ts new file mode 100644 index 00000000000..d43148dc8d3 --- /dev/null +++ b/spec/unit/content-helpers.spec.ts @@ -0,0 +1,115 @@ +/* +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 { REFERENCE_RELATION } from "matrix-events-sdk"; + +import { M_BEACON_INFO } from "../../src/@types/beacon"; +import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location"; +import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers"; + +describe('Beacon content helpers', () => { + describe('makeBeaconInfoContent()', () => { + const mockDateNow = 123456789; + beforeEach(() => { + jest.spyOn(global.Date, 'now').mockReturnValue(mockDateNow); + }); + afterAll(() => { + jest.spyOn(global.Date, 'now').mockRestore(); + }); + it('create fully defined event content', () => { + expect(makeBeaconInfoContent( + 1234, + true, + 'nice beacon_info', + LocationAssetType.Pin, + )).toEqual({ + [M_BEACON_INFO.name]: { + description: 'nice beacon_info', + timeout: 1234, + live: true, + }, + [M_TIMESTAMP.name]: mockDateNow, + [M_ASSET.name]: { + type: LocationAssetType.Pin, + }, + }); + }); + + it('defaults timestamp to current time', () => { + expect(makeBeaconInfoContent( + 1234, + true, + 'nice beacon_info', + LocationAssetType.Pin, + )).toEqual(expect.objectContaining({ + [M_TIMESTAMP.name]: mockDateNow, + })); + }); + + it('defaults asset type to self when not set', () => { + expect(makeBeaconInfoContent( + 1234, + true, + 'nice beacon_info', + // no assetType passed + )).toEqual(expect.objectContaining({ + [M_ASSET.name]: { + type: LocationAssetType.Self, + }, + })); + }); + }); + + describe('makeBeaconContent()', () => { + it('creates event content without description', () => { + expect(makeBeaconContent( + 'geo:foo', + 123, + '$1234', + // no description + )).toEqual({ + [M_LOCATION.name]: { + description: undefined, + uri: 'geo:foo', + }, + [M_TIMESTAMP.name]: 123, + "m.relates_to": { + rel_type: REFERENCE_RELATION.name, + event_id: '$1234', + }, + }); + }); + + it('creates event content with description', () => { + expect(makeBeaconContent( + 'geo:foo', + 123, + '$1234', + 'test description', + )).toEqual({ + [M_LOCATION.name]: { + description: 'test description', + uri: 'geo:foo', + }, + [M_TIMESTAMP.name]: 123, + "m.relates_to": { + rel_type: REFERENCE_RELATION.name, + event_id: '$1234', + }, + }); + }); + }); +}); diff --git a/src/@types/beacon.ts b/src/@types/beacon.ts new file mode 100644 index 00000000000..ff3cf64d264 --- /dev/null +++ b/src/@types/beacon.ts @@ -0,0 +1,144 @@ +/* +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 { EitherAnd, RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk"; + +import { UnstableValue } from "../NamespacedValue"; +import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location"; + +/** + * Beacon info and beacon event types as described in MSC3489 + * https://github.com/matrix-org/matrix-spec-proposals/pull/3489 + */ + +/** + * Beacon info events are state events. + * We have two requirements for these events: + * 1. they can only be written by their owner + * 2. a user can have an arbitrary number of beacon_info events + * + * 1. is achieved by setting the state_key to the owners mxid. + * Event keys in room state are a combination of `type` + `state_key`. + * To achieve an arbitrary number of only owner-writable state events + * we introduce a variable suffix to the event type + * + * Eg + * { + * "type": "m.beacon_info.@matthew:matrix.org.1", + * "state_key": "@matthew:matrix.org", + * "content": { + * "m.beacon_info": { + * "description": "The Matthew Tracker", + * "timeout": 86400000, + * }, + * // more content as described below + * } + * }, + * { + * "type": "m.beacon_info.@matthew:matrix.org.2", + * "state_key": "@matthew:matrix.org", + * "content": { + * "m.beacon_info": { + * "description": "Another different Matthew tracker", + * "timeout": 400000, + * }, + * // more content as described below + * } + * } + */ + +/** + * Variable event type for m.beacon_info + */ +export const M_BEACON_INFO_VARIABLE = new UnstableValue("m.beacon_info.*", "org.matrix.msc3489.beacon_info.*"); + +/** + * Non-variable type for m.beacon_info event content + */ +export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3489.beacon_info"); +export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3489.beacon"); + +export type MBeaconInfoContent = { + description?: string; + // how long from the last event until we consider the beacon inactive in milliseconds + timeout: number; + // true when this is a live location beacon + // https://github.com/matrix-org/matrix-spec-proposals/pull/3672 + live?: boolean; +}; + +export type MBeaconInfoEvent = EitherAnd< + { [M_BEACON_INFO.name]: MBeaconInfoContent }, + { [M_BEACON_INFO.altName]: MBeaconInfoContent } +>; + +/** + * m.beacon_info Event example from the spec + * https://github.com/matrix-org/matrix-spec-proposals/pull/3489 + * { + "type": "m.beacon_info.@matthew:matrix.org.1", + "state_key": "@matthew:matrix.org", + "content": { + "m.beacon_info": { + "description": "The Matthew Tracker", // same as an `m.location` description + "timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds + }, + "m.ts": 1436829458432, // creation timestamp of the beacon on the client + "m.asset": { + "type": "m.self" // the type of asset being tracked as per MSC3488 + } + } +} + */ + +/** + * m.beacon_info.* event content + */ +export type MBeaconInfoEventContent = & + MBeaconInfoEvent & + // creation timestamp of the beacon on the client + MTimestampEvent & + // the type of asset being tracked as per MSC3488 + MAssetEvent; + +/** + * m.beacon event example + * https://github.com/matrix-org/matrix-spec-proposals/pull/3489 + * + * { + "type": "m.beacon", + "sender": "@matthew:matrix.org", + "content": { + "m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674 + "rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267 + "event_id": "$beacon_info" + }, + "m.location": { + "uri": "geo:51.5008,0.1247;u=35", + "description": "Arbitrary beacon information" + }, + "m.ts": 1636829458432, + } +} +*/ + +export type MBeaconEventContent = & + MLocationEvent & + // timestamp when location was taken + MTimestampEvent & + // relates to a beacon_info event + RELATES_TO_RELATIONSHIP; + diff --git a/src/content-helpers.ts b/src/content-helpers.ts index 6419b98bdce..ce8aebf0af9 100644 --- a/src/content-helpers.ts +++ b/src/content-helpers.ts @@ -16,6 +16,9 @@ limitations under the License. /** @module ContentHelpers */ +import { REFERENCE_RELATION } from "matrix-events-sdk"; + +import { MBeaconEventContent, MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon"; import { MsgType } from "./@types/event"; import { TEXT_NODE_TYPE } from "./@types/extensible_events"; import { @@ -186,3 +189,41 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent): return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType); }; + +/** + * Beacon event helpers + */ + +export const makeBeaconInfoContent = ( + timeout: number, + isLive?: boolean, + description?: string, + assetType?: LocationAssetType, +): MBeaconInfoEventContent => ({ + [M_BEACON_INFO.name]: { + description, + timeout, + live: isLive, + }, + [M_TIMESTAMP.name]: Date.now(), + [M_ASSET.name]: { + type: assetType ?? LocationAssetType.Self, + }, +}); + +export const makeBeaconContent = ( + uri: string, + timestamp: number, + beaconInfoId: string, + description?: string, +): MBeaconEventContent => ({ + [M_LOCATION.name]: { + description, + uri, + }, + [M_TIMESTAMP.name]: timestamp, + "m.relates_to": { + rel_type: REFERENCE_RELATION.name, + event_id: beaconInfoId, + }, +});