Skip to content

Commit 34ee566

Browse files
author
Kerry
authored
Live location sharing: handle encrypted messages in processBeaconEvents (#2327)
* handle encrypted locations Signed-off-by: Kerry Archibald <[email protected]> * fix processBeaconEvents to handle encrypted events Signed-off-by: Kerry Archibald <[email protected]>
1 parent 3649cf4 commit 34ee566

File tree

4 files changed

+255
-51
lines changed

4 files changed

+255
-51
lines changed

spec/unit/matrix-client.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,15 +1047,15 @@ describe("MatrixClient", function() {
10471047
expect(roomStateProcessSpy).not.toHaveBeenCalled();
10481048
});
10491049

1050-
it('calls room states processBeaconEvents with m.beacon events', () => {
1050+
it('calls room states processBeaconEvents with events', () => {
10511051
const room = new Room(roomId, client, userId);
10521052
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
10531053

10541054
const messageEvent = testUtils.mkMessage({ room: roomId, user: userId, event: true });
10551055
const beaconEvent = makeBeaconEvent(userId);
10561056

10571057
client.processBeaconEvents(room, [messageEvent, beaconEvent]);
1058-
expect(roomStateProcessSpy).toHaveBeenCalledWith([beaconEvent]);
1058+
expect(roomStateProcessSpy).toHaveBeenCalledWith([messageEvent, beaconEvent], client);
10591059
});
10601060
});
10611061
});

spec/unit/room-state.spec.js

Lines changed: 216 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
33
import { filterEmitCallsByEventType } from "../test-utils/emitter";
44
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
55
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
6+
import { EventType, RelationType } from "../../src/@types/event";
7+
import {
8+
MatrixEvent,
9+
MatrixEventEvent,
10+
} from "../../src/models/event";
11+
import { M_BEACON } from "../../src/@types/beacon";
612

713
describe("RoomState", function() {
814
const roomId = "!foo:bar";
@@ -717,52 +723,238 @@ describe("RoomState", function() {
717723
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
718724
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
719725

726+
const mockClient = { decryptEventIfNeeded: jest.fn() };
727+
728+
beforeEach(() => {
729+
mockClient.decryptEventIfNeeded.mockClear();
730+
});
731+
720732
it('does nothing when state has no beacons', () => {
721733
const emitSpy = jest.spyOn(state, 'emit');
722-
state.processBeaconEvents([makeBeaconEvent(userA, { beaconInfoId: '$beacon1' })]);
734+
state.processBeaconEvents([makeBeaconEvent(userA, { beaconInfoId: '$beacon1' })], mockClient);
723735
expect(emitSpy).not.toHaveBeenCalled();
736+
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
724737
});
725738

726739
it('does nothing when there are no events', () => {
727740
state.setStateEvents([beacon1, beacon2]);
728741
const emitSpy = jest.spyOn(state, 'emit').mockClear();
729-
state.processBeaconEvents([]);
742+
state.processBeaconEvents([], mockClient);
730743
expect(emitSpy).not.toHaveBeenCalled();
744+
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
731745
});
732746

733-
it('discards events for beacons that are not in state', () => {
734-
const location = makeBeaconEvent(userA, {
735-
beaconInfoId: 'some-other-beacon',
747+
describe('without encryption', () => {
748+
it('discards events for beacons that are not in state', () => {
749+
const location = makeBeaconEvent(userA, {
750+
beaconInfoId: 'some-other-beacon',
751+
});
752+
const otherRelatedEvent = new MatrixEvent({
753+
sender: userA,
754+
type: EventType.RoomMessage,
755+
content: {
756+
['m.relates_to']: {
757+
event_id: 'whatever',
758+
},
759+
},
760+
});
761+
state.setStateEvents([beacon1, beacon2]);
762+
const emitSpy = jest.spyOn(state, 'emit').mockClear();
763+
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
764+
expect(emitSpy).not.toHaveBeenCalled();
765+
});
766+
767+
it('discards events that are not beacon type', () => {
768+
// related to beacon1
769+
const otherRelatedEvent = new MatrixEvent({
770+
sender: userA,
771+
type: EventType.RoomMessage,
772+
content: {
773+
['m.relates_to']: {
774+
rel_type: RelationType.Reference,
775+
event_id: beacon1.getId(),
776+
},
777+
},
778+
});
779+
state.setStateEvents([beacon1, beacon2]);
780+
const emitSpy = jest.spyOn(state, 'emit').mockClear();
781+
state.processBeaconEvents([otherRelatedEvent], mockClient);
782+
expect(emitSpy).not.toHaveBeenCalled();
783+
});
784+
785+
it('adds locations to beacons', () => {
786+
const location1 = makeBeaconEvent(userA, {
787+
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
788+
});
789+
const location2 = makeBeaconEvent(userA, {
790+
beaconInfoId: '$beacon1', timestamp: Date.now() + 2,
791+
});
792+
const location3 = makeBeaconEvent(userB, {
793+
beaconInfoId: 'some-other-beacon',
794+
});
795+
796+
state.setStateEvents([beacon1, beacon2], mockClient);
797+
798+
expect(state.beacons.size).toEqual(2);
799+
800+
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
801+
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
802+
803+
state.processBeaconEvents([location1, location2, location3], mockClient);
804+
805+
expect(addLocationsSpy).toHaveBeenCalledTimes(2);
806+
// only called with locations for beacon1
807+
expect(addLocationsSpy).toHaveBeenCalledWith([location1]);
808+
expect(addLocationsSpy).toHaveBeenCalledWith([location2]);
736809
});
737-
state.setStateEvents([beacon1, beacon2]);
738-
const emitSpy = jest.spyOn(state, 'emit').mockClear();
739-
state.processBeaconEvents([location]);
740-
expect(emitSpy).not.toHaveBeenCalled();
741810
});
742811

743-
it('adds locations to beacons', () => {
744-
const location1 = makeBeaconEvent(userA, {
745-
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
812+
describe('with encryption', () => {
813+
const beacon1RelationContent = { ['m.relates_to']: {
814+
rel_type: RelationType.Reference,
815+
event_id: beacon1.getId(),
816+
} };
817+
const relatedEncryptedEvent = new MatrixEvent({
818+
sender: userA,
819+
type: EventType.RoomMessageEncrypted,
820+
content: beacon1RelationContent,
746821
});
747-
const location2 = makeBeaconEvent(userA, {
748-
beaconInfoId: '$beacon1', timestamp: Date.now() + 2,
822+
const decryptingRelatedEvent = new MatrixEvent({
823+
sender: userA,
824+
type: EventType.RoomMessageEncrypted,
825+
content: beacon1RelationContent,
749826
});
750-
const location3 = makeBeaconEvent(userB, {
751-
beaconInfoId: 'some-other-beacon',
827+
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
828+
829+
const failedDecryptionRelatedEvent = new MatrixEvent({
830+
sender: userA,
831+
type: EventType.RoomMessageEncrypted,
832+
content: beacon1RelationContent,
833+
});
834+
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
835+
836+
it('discards events without relations', () => {
837+
const unrelatedEvent = new MatrixEvent({
838+
sender: userA,
839+
type: EventType.RoomMessageEncrypted,
840+
});
841+
state.setStateEvents([beacon1, beacon2]);
842+
const emitSpy = jest.spyOn(state, 'emit').mockClear();
843+
state.processBeaconEvents([unrelatedEvent], mockClient);
844+
expect(emitSpy).not.toHaveBeenCalled();
845+
// discard unrelated events early
846+
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
752847
});
753848

754-
state.setStateEvents([beacon1, beacon2]);
849+
it('discards events for beacons that are not in state', () => {
850+
const location = makeBeaconEvent(userA, {
851+
beaconInfoId: 'some-other-beacon',
852+
});
853+
const otherRelatedEvent = new MatrixEvent({
854+
sender: userA,
855+
type: EventType.RoomMessageEncrypted,
856+
content: {
857+
['m.relates_to']: {
858+
rel_type: RelationType.Reference,
859+
event_id: 'whatever',
860+
},
861+
},
862+
});
863+
state.setStateEvents([beacon1, beacon2]);
864+
865+
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
866+
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
867+
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
868+
expect(addLocationsSpy).not.toHaveBeenCalled();
869+
// discard unrelated events early
870+
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
871+
});
872+
873+
it('decrypts related events if needed', () => {
874+
const location = makeBeaconEvent(userA, {
875+
beaconInfoId: beacon1.getId(),
876+
});
877+
state.setStateEvents([beacon1, beacon2]);
878+
state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
879+
// discard unrelated events early
880+
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
881+
});
755882

756-
expect(state.beacons.size).toEqual(2);
883+
it('listens for decryption on events that are being decrypted', () => {
884+
const decryptingRelatedEvent = new MatrixEvent({
885+
sender: userA,
886+
type: EventType.RoomMessageEncrypted,
887+
content: beacon1RelationContent,
888+
});
889+
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
890+
// spy on event.once
891+
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
892+
893+
state.setStateEvents([beacon1, beacon2]);
894+
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
895+
896+
// listener was added
897+
expect(eventOnceSpy).toHaveBeenCalled();
898+
});
757899

758-
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
759-
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
900+
it('listens for decryption on events that have decryption failure', () => {
901+
const failedDecryptionRelatedEvent = new MatrixEvent({
902+
sender: userA,
903+
type: EventType.RoomMessageEncrypted,
904+
content: beacon1RelationContent,
905+
});
906+
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
907+
// spy on event.once
908+
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
909+
910+
state.setStateEvents([beacon1, beacon2]);
911+
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
912+
913+
// listener was added
914+
expect(eventOnceSpy).toHaveBeenCalled();
915+
});
760916

761-
state.processBeaconEvents([location1, location2, location3]);
917+
it('discard events that are not m.beacon type after decryption', () => {
918+
const decryptingRelatedEvent = new MatrixEvent({
919+
sender: userA,
920+
type: EventType.RoomMessageEncrypted,
921+
content: beacon1RelationContent,
922+
});
923+
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
924+
state.setStateEvents([beacon1, beacon2]);
925+
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
926+
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
927+
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
928+
929+
// this event is a message after decryption
930+
decryptingRelatedEvent.type = EventType.RoomMessage;
931+
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
932+
933+
expect(addLocationsSpy).not.toHaveBeenCalled();
934+
});
762935

763-
expect(addLocationsSpy).toHaveBeenCalledTimes(1);
764-
// only called with locations for beacon1
765-
expect(addLocationsSpy).toHaveBeenCalledWith([location1, location2]);
936+
it('adds locations to beacons after decryption', () => {
937+
const decryptingRelatedEvent = new MatrixEvent({
938+
sender: userA,
939+
type: EventType.RoomMessageEncrypted,
940+
content: beacon1RelationContent,
941+
});
942+
const locationEvent = makeBeaconEvent(userA, {
943+
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
944+
});
945+
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
946+
state.setStateEvents([beacon1, beacon2]);
947+
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
948+
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
949+
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
950+
951+
// update type after '''decryption'''
952+
decryptingRelatedEvent.event.type = M_BEACON.name;
953+
decryptingRelatedEvent.event.content = locationEvent.content;
954+
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
955+
956+
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
957+
});
766958
});
767959
});
768960
});

src/client.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ import { MediaHandler } from "./webrtc/mediaHandler";
180180
import { IRefreshTokenResponse } from "./@types/auth";
181181
import { TypedEventEmitter } from "./models/typed-event-emitter";
182182
import { Thread, THREAD_RELATION_TYPE } from "./models/thread";
183-
import { MBeaconInfoEventContent, M_BEACON, M_BEACON_INFO } from "./@types/beacon";
183+
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
184184

185185
export type Store = IStore;
186186
export type SessionStore = WebStorageSessionStore;
@@ -5192,7 +5192,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
51925192

51935193
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
51945194

5195-
this.processBeaconEvents(room, matrixEvents);
5195+
this.processBeaconEvents(room, timelineEvents);
51965196
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
51975197
await this.processThreadEvents(room, threadedEvents, true);
51985198

@@ -5335,7 +5335,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
53355335
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
53365336
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
53375337
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
5338-
this.processBeaconEvents(timelineSet.room, events);
5338+
this.processBeaconEvents(timelineSet.room, timelineEvents);
53395339

53405340
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
53415341
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
@@ -5503,7 +5503,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
55035503
const timelineSet = eventTimeline.getTimelineSet();
55045504
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents);
55055505
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
5506-
this.processBeaconEvents(timelineSet.room, matrixEvents);
5506+
this.processBeaconEvents(timelineSet.room, timelineEvents);
55075507
await this.processThreadEvents(room, threadedEvents, backwards);
55085508

55095509
// if we've hit the end of the timeline, we need to stop trying to
@@ -8929,8 +8929,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
89298929
if (!events?.length) {
89308930
return;
89318931
}
8932-
const beaconEvents = events.filter(event => M_BEACON.matches(event.getType()));
8933-
room.currentState.processBeaconEvents(beaconEvents);
8932+
room.currentState.processBeaconEvents(events, this);
89348933
}
89358934

89368935
/**

0 commit comments

Comments
 (0)