1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-30 04:23:07 +03:00

Live location sharing - update beacon_info implementation to latest MSC (#2281)

* remove M_BEACON_INFO_VARIABLE

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

* create beacon_info events with non-variable event type

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

* remove isBeaconInfoEventType

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

* refer to msc3673 instead of msc3489

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

* remove event type suffix

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

* update beacon identifier to use state key

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

* fix beacon spec

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

* fix room-state tests

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

* add beacon identifier

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

* dont allow update to older beacon event

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

* lint

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

* unnest beacon_info content

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

* lint

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

* check redaction event id

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry
2022-04-08 10:50:06 +02:00
committed by GitHub
parent dde4285cdf
commit 781fdf4fdc
10 changed files with 108 additions and 117 deletions

View File

@ -42,7 +42,6 @@ export const makeBeaconInfoEvent = (
roomId: string, roomId: string,
contentProps: Partial<InfoContentProps> = {}, contentProps: Partial<InfoContentProps> = {},
eventId?: string, eventId?: string,
eventTypeSuffix?: string,
): MatrixEvent => { ): MatrixEvent => {
const { const {
timeout, isLive, description, assetType, timeout, isLive, description, assetType,
@ -51,12 +50,14 @@ export const makeBeaconInfoEvent = (
...contentProps, ...contentProps,
}; };
const event = new MatrixEvent({ const event = new MatrixEvent({
type: `${M_BEACON_INFO.name}.${sender}.${eventTypeSuffix || Date.now()}`, type: M_BEACON_INFO.name,
room_id: roomId, room_id: roomId,
state_key: sender, state_key: sender,
content: makeBeaconInfoContent(timeout, isLive, description, assetType), content: makeBeaconInfoContent(timeout, isLive, description, assetType),
}); });
event.event.origin_server_ts = Date.now();
// live beacons use the beacon_info event id // live beacons use the beacon_info event id
// set or default this // set or default this
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`); event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);

View File

@ -16,7 +16,6 @@ limitations under the License.
import { REFERENCE_RELATION } from "matrix-events-sdk"; 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 { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers"; import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
@ -36,11 +35,9 @@ describe('Beacon content helpers', () => {
'nice beacon_info', 'nice beacon_info',
LocationAssetType.Pin, LocationAssetType.Pin,
)).toEqual({ )).toEqual({
[M_BEACON_INFO.name]: { description: 'nice beacon_info',
description: 'nice beacon_info', timeout: 1234,
timeout: 1234, live: true,
live: true,
},
[M_TIMESTAMP.name]: mockDateNow, [M_TIMESTAMP.name]: mockDateNow,
[M_ASSET.name]: { [M_ASSET.name]: {
type: LocationAssetType.Pin, type: LocationAssetType.Pin,

View File

@ -999,10 +999,10 @@ describe("MatrixClient", function() {
}); });
it("creates new beacon info", async () => { it("creates new beacon info", async () => {
await client.unstable_createLiveBeacon(roomId, content, '123'); await client.unstable_createLiveBeacon(roomId, content);
// event type combined // event type combined
const expectedEventType = `${M_BEACON_INFO.name}.${userId}.123`; const expectedEventType = M_BEACON_INFO.name;
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy(); expect(callback).toBeFalsy();
expect(method).toBe('PUT'); expect(method).toBe('PUT');
@ -1015,15 +1015,13 @@ describe("MatrixClient", function() {
}); });
it("updates beacon info with specific event type", async () => { it("updates beacon info with specific event type", async () => {
const eventType = `${M_BEACON_INFO.name}.${userId}.456`; await client.unstable_setLiveBeacon(roomId, content);
await client.unstable_setLiveBeacon(roomId, eventType, content);
// event type combined // event type combined
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0]; const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
expect(path).toEqual( expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` + `/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(eventType)}/${encodeURIComponent(userId)}`, `${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
); );
expect(requestContent).toEqual(content); expect(requestContent).toEqual(content);
}); });

View File

@ -14,11 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EventType } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import { import {
isTimestampInDuration, isTimestampInDuration,
isBeaconInfoEventType,
Beacon, Beacon,
BeaconEvent, BeaconEvent,
} from "../../../src/models/beacon"; } from "../../../src/models/beacon";
@ -57,27 +54,9 @@ describe('Beacon', () => {
}); });
}); });
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', () => { describe('Beacon', () => {
const userId = '@user:server.org'; const userId = '@user:server.org';
const userId2 = '@user2:server.org';
const roomId = '$room:server.org'; const roomId = '$room:server.org';
// 14.03.2022 16:15 // 14.03.2022 16:15
const now = 1647270879403; const now = 1647270879403;
@ -88,6 +67,7 @@ describe('Beacon', () => {
// without timeout of 3 hours // without timeout of 3 hours
let liveBeaconEvent; let liveBeaconEvent;
let notLiveBeaconEvent; let notLiveBeaconEvent;
let user2BeaconEvent;
const advanceDateAndTime = (ms: number) => { const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock // bc liveness check uses Date.now we have to advance this mock
@ -107,14 +87,21 @@ describe('Beacon', () => {
isLive: true, isLive: true,
}, },
'$live123', '$live123',
'$live123',
); );
notLiveBeaconEvent = makeBeaconInfoEvent( notLiveBeaconEvent = makeBeaconInfoEvent(
userId, userId,
roomId, roomId,
{ timeout: HOUR_MS * 3, isLive: false }, { timeout: HOUR_MS * 3, isLive: false },
'$dead123', '$dead123',
'$dead123', );
user2BeaconEvent = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
},
'$user2live123',
); );
// back to now // back to now
@ -133,7 +120,7 @@ describe('Beacon', () => {
expect(beacon.isLive).toEqual(true); expect(beacon.isLive).toEqual(true);
expect(beacon.beaconInfoOwner).toEqual(userId); expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType()); expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(liveBeaconEvent.getType()); expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
expect(beacon.beaconInfo).toBeTruthy(); expect(beacon.beaconInfo).toBeTruthy();
}); });
@ -171,8 +158,27 @@ describe('Beacon', () => {
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId()); expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
expect(() => beacon.update(notLiveBeaconEvent)).toThrow(); expect(() => beacon.update(user2BeaconEvent)).toThrow();
expect(beacon.isLive).toEqual(true); // didnt update
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
});
it('does not update with an older event', () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
const oldUpdateEvent = makeBeaconInfoEvent(
userId,
roomId,
);
// less than the original event
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts - 1000;
beacon.update(oldUpdateEvent);
// didnt update
expect(emitSpy).not.toHaveBeenCalled();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
}); });
it('updates event', () => { it('updates event', () => {
@ -182,7 +188,7 @@ describe('Beacon', () => {
expect(beacon.isLive).toEqual(true); expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent( const updatedBeaconEvent = makeBeaconInfoEvent(
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123', '$live123'); userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123');
beacon.update(updatedBeaconEvent); beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false); expect(beacon.isLive).toEqual(false);
@ -200,7 +206,6 @@ describe('Beacon', () => {
roomId, roomId,
{ timeout: HOUR_MS * 3, isLive: false }, { timeout: HOUR_MS * 3, isLive: false },
beacon.beaconInfoId, beacon.beaconInfoId,
'$live123',
); );
beacon.update(updatedBeaconEvent); beacon.update(updatedBeaconEvent);

View File

@ -2,7 +2,7 @@ import * as utils from "../test-utils/test-utils";
import { makeBeaconInfoEvent } from "../test-utils/beacon"; import { makeBeaconInfoEvent } from "../test-utils/beacon";
import { filterEmitCallsByEventType } from "../test-utils/emitter"; import { filterEmitCallsByEventType } from "../test-utils/emitter";
import { RoomState, RoomStateEvent } from "../../src/models/room-state"; import { RoomState, RoomStateEvent } from "../../src/models/room-state";
import { BeaconEvent } from "../../src/models/beacon"; import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
describe("RoomState", function() { describe("RoomState", function() {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
@ -260,7 +260,7 @@ describe("RoomState", function() {
state.setStateEvents([beaconEvent]); state.setStateEvents([beaconEvent]);
expect(state.beacons.size).toEqual(1); expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(beaconEvent.getType()); const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
expect(beaconInstance).toBeTruthy(); expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance); expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
}); });
@ -275,49 +275,49 @@ describe("RoomState", function() {
// no beacon added // no beacon added
expect(state.beacons.size).toEqual(0); expect(state.beacons.size).toEqual(0);
expect(state.beacons.get(redactedBeaconEvent.getType)).toBeFalsy(); expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
// no new beacon emit // no new beacon emit
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy(); expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
}); });
it('updates existing beacon info events in state', () => { it('updates existing beacon info events in state', () => {
const beaconId = '$beacon1'; const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId); const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId, beaconId); const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
state.setStateEvents([beaconEvent]); state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType()); const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
expect(beaconInstance.isLive).toEqual(true); expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([updatedBeaconEvent]); state.setStateEvents([updatedBeaconEvent]);
// same Beacon // same Beacon
expect(state.beacons.get(beaconEvent.getType())).toBe(beaconInstance); expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
// updated liveness // updated liveness
expect(state.beacons.get(beaconEvent.getType()).isLive).toEqual(false); expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent)).isLive).toEqual(false);
}); });
it('destroys and removes redacted beacon events', () => { it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1'; const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId); const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId); const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction' } }; const redactionEvent = { event: { type: 'm.room.redaction', redacts: beaconEvent.getId() } };
redactedBeaconEvent.makeRedacted(redactionEvent); redactedBeaconEvent.makeRedacted(redactionEvent);
state.setStateEvents([beaconEvent]); state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType()); const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
const destroySpy = jest.spyOn(beaconInstance, 'destroy'); const destroySpy = jest.spyOn(beaconInstance, 'destroy');
expect(beaconInstance.isLive).toEqual(true); expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]); state.setStateEvents([redactedBeaconEvent]);
expect(destroySpy).toHaveBeenCalled(); expect(destroySpy).toHaveBeenCalled();
expect(state.beacons.get(beaconEvent.getType())).toBe(undefined); expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
}); });
it('updates live beacon ids once after setting state events', () => { it('updates live beacon ids once after setting state events', () => {
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1', '$beacon1'); const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, '$beacon2', '$beacon2'); const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2');
const emitSpy = jest.spyOn(state, 'emit'); const emitSpy = jest.spyOn(state, 'emit');

View File

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EitherAnd, RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk"; import { RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue"; import { UnstableValue } from "../NamespacedValue";
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location"; import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
/** /**
* Beacon info and beacon event types as described in MSC3489 * Beacon info and beacon event types as described in MSC3672
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489 * https://github.com/matrix-org/matrix-spec-proposals/pull/3672
*/ */
/** /**
@ -60,16 +60,11 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
* } * }
*/ */
/**
* 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 * 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_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3672.beacon_info");
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3489.beacon"); export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3672.beacon");
export type MBeaconInfoContent = { export type MBeaconInfoContent = {
description?: string; description?: string;
@ -80,16 +75,11 @@ export type MBeaconInfoContent = {
live?: boolean; live?: boolean;
}; };
export type MBeaconInfoEvent = EitherAnd<
{ [M_BEACON_INFO.name]: MBeaconInfoContent },
{ [M_BEACON_INFO.altName]: MBeaconInfoContent }
>;
/** /**
* m.beacon_info Event example from the spec * m.beacon_info Event example from the spec
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489 * https://github.com/matrix-org/matrix-spec-proposals/pull/3672
* { * {
"type": "m.beacon_info.@matthew:matrix.org.1", "type": "m.beacon_info",
"state_key": "@matthew:matrix.org", "state_key": "@matthew:matrix.org",
"content": { "content": {
"m.beacon_info": { "m.beacon_info": {
@ -108,7 +98,7 @@ export type MBeaconInfoEvent = EitherAnd<
* m.beacon_info.* event content * m.beacon_info.* event content
*/ */
export type MBeaconInfoEventContent = & export type MBeaconInfoEventContent = &
MBeaconInfoEvent & MBeaconInfoContent &
// creation timestamp of the beacon on the client // creation timestamp of the beacon on the client
MTimestampEvent & MTimestampEvent &
// the type of asset being tracked as per MSC3488 // the type of asset being tracked as per MSC3488
@ -116,7 +106,7 @@ export type MBeaconInfoEventContent = &
/** /**
* m.beacon event example * m.beacon event example
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489 * https://github.com/matrix-org/matrix-spec-proposals/pull/3672
* *
* { * {
"type": "m.beacon", "type": "m.beacon",

View File

@ -180,7 +180,7 @@ import { MediaHandler } from "./webrtc/mediaHandler";
import { IRefreshTokenResponse } from "./@types/auth"; import { IRefreshTokenResponse } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter"; import { TypedEventEmitter } from "./models/typed-event-emitter";
import { Thread, THREAD_RELATION_TYPE } from "./models/thread"; import { Thread, THREAD_RELATION_TYPE } from "./models/thread";
import { MBeaconInfoEventContent, M_BEACON_INFO_VARIABLE } from "./@types/beacon"; import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
export type Store = IStore; export type Store = IStore;
export type SessionStore = WebStorageSessionStore; export type SessionStore = WebStorageSessionStore;
@ -3649,39 +3649,30 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Create an m.beacon_info event * Create an m.beacon_info event
* @param {string} roomId * @param {string} roomId
* @param {MBeaconInfoEventContent} beaconInfoContent * @param {MBeaconInfoEventContent} beaconInfoContent
* @param {string} eventTypeSuffix - string to suffix event type
* to make event type unique.
* See MSC3489 for more context
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* @returns {ISendEventResponse} * @returns {ISendEventResponse}
*/ */
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
public async unstable_createLiveBeacon( public async unstable_createLiveBeacon(
roomId: Room["roomId"], roomId: Room["roomId"],
beaconInfoContent: MBeaconInfoEventContent, beaconInfoContent: MBeaconInfoEventContent,
eventTypeSuffix: string,
) { ) {
const userId = this.getUserId(); return this.unstable_setLiveBeacon(roomId, beaconInfoContent);
const eventType = M_BEACON_INFO_VARIABLE.name.replace('*', `${userId}.${eventTypeSuffix}`);
return this.unstable_setLiveBeacon(roomId, eventType, beaconInfoContent);
} }
/** /**
* Upsert a live beacon event * Upsert a live beacon event
* using a specific m.beacon_info.* event variable type * using a specific m.beacon_info.* event variable type
* @param {string} roomId string * @param {string} roomId string
* @param {string} beaconInfoEventType event type including variable suffix
* @param {MBeaconInfoEventContent} beaconInfoContent * @param {MBeaconInfoEventContent} beaconInfoContent
* @returns {ISendEventResponse} * @returns {ISendEventResponse}
*/ */
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
public async unstable_setLiveBeacon( public async unstable_setLiveBeacon(
roomId: string, roomId: string,
beaconInfoEventType: string,
beaconInfoContent: MBeaconInfoEventContent, beaconInfoContent: MBeaconInfoEventContent,
) { ) {
const userId = this.getUserId(); const userId = this.getUserId();
return this.sendStateEvent(roomId, beaconInfoEventType, beaconInfoContent, userId); return this.sendStateEvent(roomId, M_BEACON_INFO.name, beaconInfoContent, userId);
} }
/** /**

View File

@ -18,7 +18,7 @@ limitations under the License.
import { REFERENCE_RELATION } from "matrix-events-sdk"; import { REFERENCE_RELATION } from "matrix-events-sdk";
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon"; import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
import { MsgType } from "./@types/event"; import { MsgType } from "./@types/event";
import { TEXT_NODE_TYPE } from "./@types/extensible_events"; import { TEXT_NODE_TYPE } from "./@types/extensible_events";
import { import {
@ -208,11 +208,9 @@ export const makeBeaconInfoContent: MakeBeaconInfoContent = (
assetType, assetType,
timestamp, timestamp,
) => ({ ) => ({
[M_BEACON_INFO.name]: { description,
description, timeout,
timeout, live: isLive,
live: isLive,
},
[M_TIMESTAMP.name]: timestamp || Date.now(), [M_TIMESTAMP.name]: timestamp || Date.now(),
[M_ASSET.name]: { [M_ASSET.name]: {
type: assetType ?? LocationAssetType.Self, type: assetType ?? LocationAssetType.Self,
@ -227,7 +225,7 @@ export type BeaconInfoState = MBeaconInfoContent & {
* Flatten beacon info event content * Flatten beacon info event content
*/ */
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => { export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
const { description, timeout, live } = M_BEACON_INFO.findIn<MBeaconInfoContent>(content); const { description, timeout, live } = content;
const { type: assetType } = M_ASSET.findIn<MAssetContent>(content); const { type: assetType } = M_ASSET.findIn<MAssetContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content); const timestamp = M_TIMESTAMP.findIn<number>(content);
@ -243,14 +241,14 @@ export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): Beacon
export type MakeBeaconContent = ( export type MakeBeaconContent = (
uri: string, uri: string,
timestamp: number, timestamp: number,
beaconInfoId: string, beaconInfoEventId: string,
description?: string, description?: string,
) => MBeaconEventContent; ) => MBeaconEventContent;
export const makeBeaconContent: MakeBeaconContent = ( export const makeBeaconContent: MakeBeaconContent = (
uri, uri,
timestamp, timestamp,
beaconInfoId, beaconInfoEventId,
description, description,
) => ({ ) => ({
[M_LOCATION.name]: { [M_LOCATION.name]: {
@ -260,6 +258,6 @@ export const makeBeaconContent: MakeBeaconContent = (
[M_TIMESTAMP.name]: timestamp, [M_TIMESTAMP.name]: timestamp,
"m.relates_to": { "m.relates_to": {
rel_type: REFERENCE_RELATION.name, rel_type: REFERENCE_RELATION.name,
event_id: beaconInfoId, event_id: beaconInfoEventId,
}, },
}); });

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { M_BEACON_INFO } from "../@types/beacon";
import { BeaconInfoState, parseBeaconInfoContent } from "../content-helpers"; import { BeaconInfoState, parseBeaconInfoContent } from "../content-helpers";
import { MatrixEvent } from "../matrix"; import { MatrixEvent } from "../matrix";
import { TypedEventEmitter } from "./typed-event-emitter"; import { TypedEventEmitter } from "./typed-event-emitter";
@ -38,11 +37,13 @@ export const isTimestampInDuration = (
timestamp: number, timestamp: number,
): boolean => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp; ): boolean => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;
export const isBeaconInfoEventType = (type: string) => // beacon info events are uniquely identified by
type.startsWith(M_BEACON_INFO.name) || // `<roomId>_<state_key>`
type.startsWith(M_BEACON_INFO.altName); export type BeaconIdentifier = string;
export const getBeaconInfoIdentifier = (event: MatrixEvent): BeaconIdentifier =>
`${event.getRoomId()}_${event.getStateKey()}`;
// https://github.com/matrix-org/matrix-spec-proposals/pull/3489 // https://github.com/matrix-org/matrix-spec-proposals/pull/3672
export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> { export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
public readonly roomId: string; public readonly roomId: string;
private _beaconInfo: BeaconInfoState; private _beaconInfo: BeaconInfoState;
@ -61,8 +62,8 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
return this._isLive; return this._isLive;
} }
public get identifier(): string { public get identifier(): BeaconIdentifier {
return this.beaconInfoEventType; return getBeaconInfoIdentifier(this.rootEvent);
} }
public get beaconInfoId(): string { public get beaconInfoId(): string {
@ -82,9 +83,13 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
} }
public update(beaconInfoEvent: MatrixEvent): void { public update(beaconInfoEvent: MatrixEvent): void {
if (beaconInfoEvent.getType() !== this.beaconInfoEventType) { if (getBeaconInfoIdentifier(beaconInfoEvent) !== this.identifier) {
throw new Error('Invalid updating event'); throw new Error('Invalid updating event');
} }
// don't update beacon with an older event
if (beaconInfoEvent.event.origin_server_ts < this.rootEvent.event.origin_server_ts) {
return;
}
this.rootEvent = beaconInfoEvent; this.rootEvent = beaconInfoEvent;
this.setBeaconInfo(this.rootEvent); this.setBeaconInfo(this.rootEvent);

View File

@ -26,8 +26,11 @@ import { MatrixEvent } from "./event";
import { MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials"; import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
import { TypedEventEmitter } from "./typed-event-emitter"; import { TypedEventEmitter } from "./typed-event-emitter";
import { Beacon, BeaconEvent, isBeaconInfoEventType, BeaconEventHandlerMap } from "./beacon"; import { Beacon, BeaconEvent, BeaconEventHandlerMap } from "./beacon";
import { TypedReEmitter } from "../ReEmitter"; import { TypedReEmitter } from "../ReEmitter";
import { M_BEACON_INFO } from "../@types/beacon";
import { getBeaconInfoIdentifier } from "./beacon";
import { BeaconIdentifier } from "..";
// possible statuses for out-of-band member loading // possible statuses for out-of-band member loading
enum OobStatus { enum OobStatus {
@ -80,8 +83,8 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>> public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
public paginationToken: string = null; public paginationToken: string = null;
public readonly beacons = new Map<string, Beacon>(); public readonly beacons = new Map<BeaconIdentifier, Beacon>();
private liveBeaconIds: string[] = []; private liveBeaconIds: BeaconIdentifier[] = [];
/** /**
* Construct room state. * Construct room state.
@ -330,7 +333,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
return; return;
} }
if (isBeaconInfoEventType(event.getType())) { if (M_BEACON_INFO.matches(event.getType())) {
this.setBeacon(event); this.setBeacon(event);
} }
@ -437,12 +440,15 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* @experimental * @experimental
*/ */
private setBeacon(event: MatrixEvent): void { private setBeacon(event: MatrixEvent): void {
if (this.beacons.has(event.getType())) { const beaconIdentifier = getBeaconInfoIdentifier(event);
const beacon = this.beacons.get(event.getType()); if (this.beacons.has(beaconIdentifier)) {
const beacon = this.beacons.get(beaconIdentifier);
if (event.isRedacted()) { if (event.isRedacted()) {
beacon.destroy(); if (beacon.beaconInfoId === event.getRedactionEvent()?.['redacts']) {
this.beacons.delete(event.getType()); beacon.destroy();
this.beacons.delete(beaconIdentifier);
}
return; return;
} }
@ -464,7 +470,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
this.emit(BeaconEvent.New, event, beacon); this.emit(BeaconEvent.New, event, beacon);
beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this)); beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this));
this.beacons.set(beacon.beaconInfoEventType, beacon); this.beacons.set(beacon.identifier, beacon);
} }
/** /**
@ -477,7 +483,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
const prevHasLiveBeacons = !!this.liveBeaconIds?.length; const prevHasLiveBeacons = !!this.liveBeaconIds?.length;
this.liveBeaconIds = Array.from(this.beacons.values()) this.liveBeaconIds = Array.from(this.beacons.values())
.filter(beacon => beacon.isLive) .filter(beacon => beacon.isLive)
.map(beacon => beacon.beaconInfoId); .map(beacon => beacon.identifier);
const hasLiveBeacons = !!this.liveBeaconIds.length; const hasLiveBeacons = !!this.liveBeaconIds.length;
if (prevHasLiveBeacons !== hasLiveBeacons) { if (prevHasLiveBeacons !== hasLiveBeacons) {