diff --git a/spec/unit/location.spec.ts b/spec/unit/location.spec.ts index a58b605e6..d7bdf407f 100644 --- a/spec/unit/location.spec.ts +++ b/spec/unit/location.spec.ts @@ -14,43 +14,98 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { makeLocationContent } from "../../src/content-helpers"; +import { makeLocationContent, parseLocationEvent } from "../../src/content-helpers"; import { - ASSET_NODE_TYPE, + M_ASSET, LocationAssetType, - LOCATION_EVENT_TYPE, - TIMESTAMP_NODE_TYPE, + M_LOCATION, + M_TIMESTAMP, + LocationEventWireContent, } from "../../src/@types/location"; import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events"; +import { MsgType } from "../../src/@types/event"; describe("Location", function() { + const defaultContent = { + "body": "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z", + "msgtype": "m.location", + "geo_uri": "geo:-36.24484561954707,175.46884959563613;u=10", + [M_LOCATION.name]: { "uri": "geo:-36.24484561954707,175.46884959563613;u=10", "description": null }, + [M_ASSET.name]: { "type": "m.self" }, + [TEXT_NODE_TYPE.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z", + [M_TIMESTAMP.name]: 1646823712443, + } as any; + + const backwardsCompatibleEventContent = { ...defaultContent }; + + // eslint-disable-next-line camelcase + const { body, msgtype, geo_uri, ...modernProperties } = defaultContent; + const modernEventContent = { ...modernProperties }; + + const legacyEventContent = { + // eslint-disable-next-line camelcase + body, msgtype, geo_uri, + } as LocationEventWireContent; + it("should create a valid location with defaults", function() { - const loc = makeLocationContent("txt", "geo:foo", 134235435); - expect(loc.body).toEqual("txt"); - expect(loc.msgtype).toEqual("m.location"); + const loc = makeLocationContent(undefined, "geo:foo", 134235435); + expect(loc.body).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z'); + expect(loc.msgtype).toEqual(MsgType.Location); expect(loc.geo_uri).toEqual("geo:foo"); - expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({ + expect(M_LOCATION.findIn(loc)).toEqual({ uri: "geo:foo", description: undefined, }); - expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Self }); - expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt"); - expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435); + expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Self }); + expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z'); + expect(M_TIMESTAMP.findIn(loc)).toEqual(134235435); }); it("should create a valid location with explicit properties", function() { const loc = makeLocationContent( - "txxt", "geo:bar", 134235436, "desc", LocationAssetType.Pin); + undefined, "geo:bar", 134235436, "desc", LocationAssetType.Pin); - expect(loc.body).toEqual("txxt"); - expect(loc.msgtype).toEqual("m.location"); + expect(loc.body).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z'); + expect(loc.msgtype).toEqual(MsgType.Location); expect(loc.geo_uri).toEqual("geo:bar"); - expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({ + expect(M_LOCATION.findIn(loc)).toEqual({ uri: "geo:bar", description: "desc", }); - expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Pin }); - expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt"); - expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436); + expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Pin }); + expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z'); + expect(M_TIMESTAMP.findIn(loc)).toEqual(134235436); + }); + + it('parses backwards compatible event correctly', () => { + const eventContent = parseLocationEvent(backwardsCompatibleEventContent); + + expect(eventContent).toEqual(backwardsCompatibleEventContent); + }); + + it('parses modern correctly', () => { + const eventContent = parseLocationEvent(modernEventContent); + + expect(eventContent).toEqual(backwardsCompatibleEventContent); + }); + + it('parses legacy event correctly', () => { + const eventContent = parseLocationEvent(legacyEventContent); + + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [M_TIMESTAMP.name]: timestamp, + ...expectedResult + } = defaultContent; + expect(eventContent).toEqual({ + ...expectedResult, + [M_LOCATION.name]: { + ...expectedResult[M_LOCATION.name], + description: undefined, + }, + }); + + // don't infer timestamp from legacy event + expect(M_TIMESTAMP.findIn(eventContent)).toBeFalsy(); }); }); diff --git a/src/@types/location.ts b/src/@types/location.ts index 09ef31171..9fc37d349 100644 --- a/src/@types/location.ts +++ b/src/@types/location.ts @@ -15,23 +15,44 @@ limitations under the License. */ // Types for MSC3488 - m.location: Extending events with location data +import { EitherAnd } from "matrix-events-sdk"; import { UnstableValue } from "../NamespacedValue"; -import { IContent } from "../models/event"; import { TEXT_NODE_TYPE } from "./extensible_events"; -export const LOCATION_EVENT_TYPE = new UnstableValue( - "m.location", "org.matrix.msc3488.location"); - -export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.asset"); - -export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts"); - export enum LocationAssetType { Self = "m.self", Pin = "m.pin", } +export const M_ASSET = new UnstableValue("m.asset", "org.matrix.msc3488.asset"); +export type MAssetContent = { type: LocationAssetType }; +/** + * The event definition for an m.asset event (in content) + */ +export type MAssetEvent = EitherAnd<{ [M_ASSET.name]: MAssetContent }, { [M_ASSET.altName]: MAssetContent }>; + +export const M_TIMESTAMP = new UnstableValue("m.ts", "org.matrix.msc3488.ts"); +/** + * The event definition for an m.ts event (in content) + */ +export type MTimestampEvent = EitherAnd<{ [M_TIMESTAMP.name]: number }, { [M_TIMESTAMP.altName]: number }>; + +export const M_LOCATION = new UnstableValue( + "m.location", "org.matrix.msc3488.location"); + +export type MLocationContent = { + uri: string; + description?: string | null; +}; + +export type MLocationEvent = EitherAnd< + { [M_LOCATION.name]: MLocationContent }, + { [M_LOCATION.altName]: MLocationContent } +>; + +export type MTextEvent = EitherAnd<{ [TEXT_NODE_TYPE.name]: string }, { [TEXT_NODE_TYPE.altName]: string }>; + /* From the spec at: * https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md { @@ -52,20 +73,25 @@ export enum LocationAssetType { } } */ +type OptionalTimestampEvent = MTimestampEvent | undefined; +/** + * The content for an m.location event +*/ +export type MLocationEventContent = & + MLocationEvent & + MAssetEvent & + MTextEvent & + OptionalTimestampEvent; -/* eslint-disable camelcase */ -export interface ILocationContent extends IContent { +export type LegacyLocationEventContent = { body: string; msgtype: string; geo_uri: string; - [LOCATION_EVENT_TYPE.name]: { - uri: string; - description?: string; - }; - [ASSET_NODE_TYPE.name]: { - type: LocationAssetType; - }; - [TEXT_NODE_TYPE.name]: string; - [TIMESTAMP_NODE_TYPE.name]: number; -} -/* eslint-enable camelcase */ +}; + +/** + * Possible content for location events as sent over the wire + */ +export type LocationEventWireContent = Partial; + +export type ILocationContent = MLocationEventContent & LegacyLocationEventContent; diff --git a/src/content-helpers.ts b/src/content-helpers.ts index 89955bbae..6419b98bd 100644 --- a/src/content-helpers.ts +++ b/src/content-helpers.ts @@ -19,11 +19,15 @@ limitations under the License. import { MsgType } from "./@types/event"; import { TEXT_NODE_TYPE } from "./@types/extensible_events"; import { - ASSET_NODE_TYPE, - ILocationContent, + M_ASSET, LocationAssetType, - LOCATION_EVENT_TYPE, - TIMESTAMP_NODE_TYPE, + M_LOCATION, + M_TIMESTAMP, + LocationEventWireContent, + MLocationEventContent, + MLocationContent, + MAssetContent, + LegacyLocationEventContent, } from "./@types/location"; /** @@ -107,35 +111,78 @@ export function makeEmoteMessage(body: string) { }; } +/** Location content helpers */ + +export const getTextForLocationEvent = ( + uri: string, + assetType: LocationAssetType, + timestamp: number, + description?: string, +): string => { + const date = `at ${new Date(timestamp).toISOString()}`; + const assetName = assetType === LocationAssetType.Self ? 'User' : undefined; + const quotedDescription = description ? `"${description}"` : undefined; + + return [ + assetName, + 'Location', + quotedDescription, + uri, + date, + ].filter(Boolean).join(' '); +}; + /** * Generates the content for a Location event - * @param text a text for of our location * @param uri a geo:// uri for the location * @param ts the timestamp when the location was correct (milliseconds since * the UNIX epoch) * @param description the (optional) label for this location on the map * @param asset_type the (optional) asset type of this location e.g. "m.self" + * @param text optional. A text for the location */ -export function makeLocationContent( - text: string, +export const makeLocationContent = ( + // this is first but optional + // to avoid a breaking change + text: string | undefined, uri: string, - ts: number, + timestamp?: number, description?: string, assetType?: LocationAssetType, -): ILocationContent { +): LegacyLocationEventContent & MLocationEventContent => { + const defaultedText = text ?? + getTextForLocationEvent(uri, assetType || LocationAssetType.Self, timestamp, description); + const timestampEvent = timestamp ? { [M_TIMESTAMP.name]: timestamp } : {}; return { - "body": text, - "msgtype": MsgType.Location, - "geo_uri": uri, - [LOCATION_EVENT_TYPE.name]: { - uri, + msgtype: MsgType.Location, + body: defaultedText, + geo_uri: uri, + [M_LOCATION.name]: { description, + uri, }, - [ASSET_NODE_TYPE.name]: { - type: assetType ?? LocationAssetType.Self, + [M_ASSET.name]: { + type: assetType || LocationAssetType.Self, }, - [TEXT_NODE_TYPE.name]: text, - [TIMESTAMP_NODE_TYPE.name]: ts, - // TODO: MSC1767 fallbacks m.image thumbnail - }; -} + [TEXT_NODE_TYPE.name]: defaultedText, + ...timestampEvent, + } as LegacyLocationEventContent & MLocationEventContent; +}; + +/** + * Parse location event content and transform to + * a backwards compatible modern m.location event format + */ +export const parseLocationEvent = (wireEventContent: LocationEventWireContent): MLocationEventContent => { + const location = M_LOCATION.findIn(wireEventContent); + const asset = M_ASSET.findIn(wireEventContent); + const timestamp = M_TIMESTAMP.findIn(wireEventContent); + const text = TEXT_NODE_TYPE.findIn(wireEventContent); + + const geoUri = location?.uri ?? wireEventContent?.geo_uri; + const description = location?.description; + const assetType = asset?.type ?? LocationAssetType.Self; + const fallbackText = text ?? wireEventContent.body; + + return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType); +};