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

Properly re-insert room ID in bundled thread relation messages from sync (#2505)

Events returned by the `/sync` endpoint, including relations bundled with other events, may have their `room_id`s stripped out. This causes decryption errors if the IDs aren't repopulated.

Fixes vector-im/element-web#22094.
This commit is contained in:
Faye Duxovni
2022-07-08 18:43:38 -04:00
committed by GitHub
parent 72f9a51c27
commit 3935152d08
3 changed files with 170 additions and 50 deletions

View File

@ -1,5 +1,21 @@
/*
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 * as utils from "../test-utils/test-utils";
import { EventTimeline, Filter, MatrixEvent } from "../../src/matrix";
import { ClientEvent, EventTimeline, Filter, IEvent, MatrixClient, MatrixEvent, Room } from "../../src/matrix";
import { logger } from "../../src/logger";
import { TestClient } from "../TestClient";
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
@ -10,8 +26,14 @@ const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
const withoutRoomId = (e: Partial<IEvent>): Partial<IEvent> => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { room_id: _, ...copy } = e;
return copy;
};
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
room: roomId, mship: "join", user: userId, name: userName, event: false,
});
const ROOM_NAME_EVENT = utils.mkEvent({
@ -19,34 +41,37 @@ const ROOM_NAME_EVENT = utils.mkEvent({
content: {
name: "Old room name",
},
event: false,
});
const INITIAL_SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
[roomId]: {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello",
user: otherUserId, msg: "hello", event: false,
}),
],
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
withoutRoomId(ROOM_NAME_EVENT),
utils.mkMembership({
room: roomId, mship: "join",
mship: "join",
user: otherUserId, name: "Bob",
event: false,
}),
USER_MEMBERSHIP_EVENT,
withoutRoomId(USER_MEMBERSHIP_EVENT),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
type: "m.room.create", user: userId,
content: {
creator: userId,
},
event: false,
}),
],
},
@ -57,16 +82,16 @@ const INITIAL_SYNC_DATA = {
const EVENTS = [
utils.mkMessage({
room: roomId, user: userId, msg: "we",
room: roomId, user: userId, msg: "we", event: false,
}),
utils.mkMessage({
room: roomId, user: userId, msg: "could",
room: roomId, user: userId, msg: "could", event: false,
}),
utils.mkMessage({
room: roomId, user: userId, msg: "be",
room: roomId, user: userId, msg: "be", event: false,
}),
utils.mkMessage({
room: roomId, user: userId, msg: "heroes",
room: roomId, user: userId, msg: "heroes", event: false,
}),
];
@ -81,12 +106,13 @@ const THREAD_ROOT = utils.mkEvent({
unsigned: {
"m.relations": {
"io.element.thread": {
"latest_event": undefined,
//"latest_event": undefined,
"count": 1,
"current_user_participated": true,
},
},
},
event: false,
});
const THREAD_REPLY = utils.mkEvent({
@ -102,12 +128,25 @@ const THREAD_REPLY = utils.mkEvent({
event_id: THREAD_ROOT.event_id,
},
},
event: false,
});
THREAD_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD_REPLY;
const SYNC_THREAD_ROOT = withoutRoomId(THREAD_ROOT);
const SYNC_THREAD_REPLY = withoutRoomId(THREAD_REPLY);
SYNC_THREAD_ROOT.unsigned = {
"m.relations": {
"io.element.thread": {
"latest_event": SYNC_THREAD_REPLY,
"count": 1,
"current_user_participated": true,
},
},
};
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
function startClient(httpBackend: TestClient["httpBackend"], client: MatrixClient) {
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
@ -116,8 +155,8 @@ function startClient(httpBackend, client) {
client.startClient();
// set up a promise which will resolve once the client is initialised
const prom = new Promise((resolve) => {
client.on("sync", function(state) {
const prom = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, function(state) {
logger.log("sync", state);
if (state != "SYNCING") {
return;
@ -133,8 +172,8 @@ function startClient(httpBackend, client) {
}
describe("getEventTimeline support", function() {
let httpBackend;
let client;
let httpBackend: TestClient["httpBackend"];
let client: MatrixClient;
beforeEach(function() {
const testClient = new TestClient(userId, "DEVICE", accessToken);
@ -177,7 +216,7 @@ describe("getEventTimeline support", function() {
it("scrollback should be able to scroll back to before a gappy /sync", function() {
// need a client with timelineSupport disabled to make this work
let room;
let room: Room;
return startClient(httpBackend, client).then(function() {
room = client.getRoom(roomId);
@ -189,7 +228,7 @@ describe("getEventTimeline support", function() {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
withoutRoomId(EVENTS[0]),
],
prev_batch: "f_1_1",
},
@ -205,7 +244,7 @@ describe("getEventTimeline support", function() {
"!foo:bar": {
timeline: {
events: [
EVENTS[1],
withoutRoomId(EVENTS[1]),
],
limited: true,
prev_batch: "f_1_2",
@ -240,8 +279,8 @@ describe("getEventTimeline support", function() {
});
describe("MatrixClient event timelines", function() {
let client = null;
let httpBackend = null;
let client: MatrixClient;
let httpBackend: TestClient["httpBackend"];
beforeEach(function() {
const testClient = new TestClient(
@ -260,7 +299,7 @@ describe("MatrixClient event timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
Thread.setServerSideSupport(false);
Thread.setServerSideSupport(false, false);
});
describe("getEventTimeline", function() {
@ -308,7 +347,7 @@ describe("MatrixClient event timelines", function() {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
withoutRoomId(EVENTS[0]),
],
prev_batch: "f_1_2",
},
@ -343,7 +382,7 @@ describe("MatrixClient event timelines", function() {
"!foo:bar": {
timeline: {
events: [
EVENTS[3],
withoutRoomId(EVENTS[3]),
],
prev_batch: "f_1_2",
},
@ -366,7 +405,7 @@ describe("MatrixClient event timelines", function() {
});
const prom = new Promise((resolve, reject) => {
client.on("sync", function() {
client.on(ClientEvent.Sync, function() {
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
@ -511,8 +550,9 @@ describe("MatrixClient event timelines", function() {
});
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true);
Thread.setServerSideSupport(true, false);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const thread = room.createThread(THREAD_ROOT.event_id, undefined, [], false);
@ -556,8 +596,9 @@ describe("MatrixClient event timelines", function() {
});
it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true);
Thread.setServerSideSupport(true, false);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const threadRoot = new MatrixEvent(THREAD_ROOT);
@ -587,8 +628,9 @@ describe("MatrixClient event timelines", function() {
});
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true);
Thread.setServerSideSupport(true, false);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const threadRoot = new MatrixEvent(THREAD_ROOT);
@ -614,8 +656,9 @@ describe("MatrixClient event timelines", function() {
});
it("should return undefined when event is within a thread but timelineSet is not", () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true);
Thread.setServerSideSupport(true, false);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@ -639,6 +682,7 @@ describe("MatrixClient event timelines", function() {
});
it("should should add lazy loading filter when requested", async () => {
// @ts-ignore
client.clientOpts.lazyLoadMembers = true;
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
@ -656,7 +700,7 @@ describe("MatrixClient event timelines", function() {
};
});
req.check((request) => {
expect(request.opts.qs.filter).toEqual(JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER));
expect(request.queryParams.filter).toEqual(JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER));
});
await Promise.all([
@ -863,7 +907,7 @@ describe("MatrixClient event timelines", function() {
"!foo:bar": {
timeline: {
events: [
event,
withoutRoomId(event),
],
prev_batch: "f_1_1",
},
@ -941,11 +985,10 @@ describe("MatrixClient event timelines", function() {
// a state event, followed by a redaction thereof
const event = utils.mkMembership({
room: roomId, mship: "join", user: otherUserId,
mship: "join", user: otherUserId,
});
const redaction = utils.mkEvent({
type: "m.room.redaction",
room_id: roomId,
sender: otherUserId,
content: {},
});
@ -987,7 +1030,7 @@ describe("MatrixClient event timelines", function() {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "world",
user: otherUserId, msg: "world",
}),
],
limited: true,
@ -1006,4 +1049,75 @@ describe("MatrixClient event timelines", function() {
expect(tl.getEvents().length).toEqual(1);
});
});
it("should re-insert room IDs for bundled thread relation events", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true, false);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_ROOT,
],
prev_batch: "f_1_1",
},
},
},
},
});
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
const room = client.getRoom(roomId);
const thread = room.getThread(THREAD_ROOT.event_id);
const timelineSet = thread.timelineSet;
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id))
.respond(200, {
start: "start_token",
events_before: [],
event: THREAD_ROOT,
events_after: [],
state: [],
end: "end_token",
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
// no next batch as this is the oldest end of the timeline
};
});
await Promise.all([
client.getEventTimeline(timelineSet, THREAD_ROOT.event_id),
httpBackend.flushAllExpected(),
]);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_REPLY,
],
prev_batch: "f_1_2",
},
},
},
},
});
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
expect(thread.liveTimeline.getEvents()[1].event).toEqual(THREAD_REPLY);
});
});

View File

@ -70,7 +70,7 @@ export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
interface IEventOpts {
type: EventType | string;
room: string;
room?: string;
sender?: string;
skey?: string;
content: IContent;
@ -93,8 +93,8 @@ let testEventIndex = 1; // counter for events, easier for comparison of randomly
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): object;
export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixClient): object | MatrixEvent {
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixClient): Partial<IEvent> | MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
@ -145,8 +145,8 @@ interface IPresenceOpts {
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent;
export function mkPresence(opts: IPresenceOpts & { event?: false }): object;
export function mkPresence(opts: IPresenceOpts & { event?: boolean }): object | MatrixEvent {
export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial<IEvent>;
export function mkPresence(opts: IPresenceOpts & { event?: boolean }): Partial<IEvent> | MatrixEvent {
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
@ -162,7 +162,7 @@ export function mkPresence(opts: IPresenceOpts & { event?: boolean }): object |
}
interface IMembershipOpts {
room: string;
room?: string;
mship: string;
sender?: string;
user?: string;
@ -186,8 +186,8 @@ interface IMembershipOpts {
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent;
export function mkMembership(opts: IMembershipOpts & { event?: false }): object;
export function mkMembership(opts: IMembershipOpts & { event?: boolean }): object | MatrixEvent {
export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial<IEvent>;
export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Partial<IEvent> | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMember,
@ -209,7 +209,7 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): objec
}
interface IMessageOpts {
room: string;
room?: string;
user: string;
msg?: string;
event?: boolean;
@ -226,8 +226,11 @@ interface IMessageOpts {
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): object;
export function mkMessage(opts: IMessageOpts & { event?: boolean }, client?: MatrixClient): object | MatrixEvent {
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
export function mkMessage(
opts: IMessageOpts & { event?: boolean },
client?: MatrixClient,
): Partial<IEvent> | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
@ -260,11 +263,11 @@ interface IReplyMessageOpts extends IMessageOpts {
* @return {Object|MatrixEvent} The event
*/
export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): object;
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
export function mkReplyMessage(
opts: IReplyMessageOpts & { event?: boolean },
client?: MatrixClient,
): object | MatrixEvent {
): Partial<IEvent> | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,

View File

@ -279,7 +279,10 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this.replyCount = bundledRelationship.count;
this._currentUserParticipated = bundledRelationship.current_user_participated;
const event = new MatrixEvent(bundledRelationship.latest_event);
const event = new MatrixEvent({
room_id: this.rootEvent.getRoomId(),
...bundledRelationship.latest_event,
});
this.setEventMetadata(event);
event.setThread(this);
this.lastEvent = event;