mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-28 11:28:12 +08:00
59cd5180af
* Absorb the matrix-react-sdk repository (#28192) Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Florian Duros <florian.duros@ormaz.fr> Co-authored-by: Kim Brose <kim.brose@nordeck.net> Co-authored-by: Florian Duros <florianduros@element.io> Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: dbkr <986903+dbkr@users.noreply.github.com> Co-authored-by: ElementRobot <releases@riot.im> Co-authored-by: dbkr <dbkr@users.noreply.github.com> Co-authored-by: David Baker <dbkr@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: David Langley <davidl@element.io> Co-authored-by: Michael Weimann <michaelw@matrix.org> Co-authored-by: Timshel <Timshel@users.noreply.github.com> Co-authored-by: Sahil Silare <32628578+sahil9001@users.noreply.github.com> Co-authored-by: Will Hunt <will@half-shot.uk> Co-authored-by: Hubert Chathi <hubert@uhoreg.ca> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Co-authored-by: Robin <robin@robin.town> Co-authored-by: Tulir Asokan <tulir@maunium.net> * Update dependency @sentry/browser to v8.33.0 [SECURITY] (#28194) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update babel monorepo (#28196) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency @types/react to v17.0.83 (#28138) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency @matrix-org/spec to v1.12.0 (#28200) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency @formatjs/intl-segmenter to v11.5.9 (#28197) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Remove references to `MatrixClient.crypto` (#28204) * Remove `VerificationExplorer` * Remove `remakeolm` slash command * Remove call to `crypto.cancelAndResendAllOutgoingKeyRequests` * Remove crypto mock in `LoginWithQR-test.tsx` * Remove `StopGadWidgetDriver.sendToDevice` * Remove remaining mock * Update dependency typescript to v5.6.3 (#28198) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency eslint-plugin-unicorn to v56 (#28202) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency stylelint to v16.10.0 (#28201) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update browserslist (#28199) * Update browserslist * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> * Add reactions to html export and add test * Add reaction to snapshot test * Update snapshot output * Remove logging * Add reaction to html export screenshot test. * lint * Update reference screenshot. --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Florian Duros <florian.duros@ormaz.fr> Co-authored-by: Kim Brose <kim.brose@nordeck.net> Co-authored-by: Florian Duros <florianduros@element.io> Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: dbkr <986903+dbkr@users.noreply.github.com> Co-authored-by: ElementRobot <releases@riot.im> Co-authored-by: dbkr <dbkr@users.noreply.github.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Michael Weimann <michaelw@matrix.org> Co-authored-by: Timshel <Timshel@users.noreply.github.com> Co-authored-by: Sahil Silare <32628578+sahil9001@users.noreply.github.com> Co-authored-by: Will Hunt <will@half-shot.uk> Co-authored-by: Hubert Chathi <hubert@uhoreg.ca> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Co-authored-by: Robin <robin@robin.town> Co-authored-by: Tulir Asokan <tulir@maunium.net>
646 lines
20 KiB
TypeScript
646 lines
20 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import {
|
|
EventTimeline,
|
|
EventTimelineSet,
|
|
EventType,
|
|
IRoomEvent,
|
|
MatrixClient,
|
|
MatrixEvent,
|
|
MsgType,
|
|
Relations,
|
|
RelationType,
|
|
Room,
|
|
RoomMember,
|
|
RoomState,
|
|
} from "matrix-js-sdk/src/matrix";
|
|
import fetchMock from "fetch-mock-jest";
|
|
import escapeHtml from "escape-html";
|
|
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
|
|
|
import { filterConsole, mkReaction, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../../test-utils";
|
|
import { ExportType, IExportOptions } from "../../../../src/utils/exportUtils/exportUtils";
|
|
import SdkConfig from "../../../../src/SdkConfig";
|
|
import HTMLExporter from "../../../../src/utils/exportUtils/HtmlExport";
|
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
|
import { mediaFromMxc } from "../../../../src/customisations/Media";
|
|
|
|
jest.mock("jszip");
|
|
|
|
const EVENT_MESSAGE: IRoomEvent = {
|
|
event_id: "$1",
|
|
type: EventType.RoomMessage,
|
|
sender: "@bob:example.com",
|
|
origin_server_ts: 0,
|
|
content: {
|
|
msgtype: "m.text",
|
|
body: "Message",
|
|
avatar_url: "mxc://example.org/avatar.bmp",
|
|
},
|
|
};
|
|
|
|
const EVENT_ATTACHMENT: IRoomEvent = {
|
|
event_id: "$2",
|
|
type: EventType.RoomMessage,
|
|
sender: "@alice:example.com",
|
|
origin_server_ts: 1,
|
|
content: {
|
|
msgtype: MsgType.File,
|
|
body: "hello.txt",
|
|
filename: "hello.txt",
|
|
url: "mxc://example.org/test-id",
|
|
},
|
|
};
|
|
|
|
const EVENT_ATTACHMENT_MALFORMED: IRoomEvent = {
|
|
event_id: "$2",
|
|
type: EventType.RoomMessage,
|
|
sender: "@alice:example.com",
|
|
origin_server_ts: 1,
|
|
content: {
|
|
msgtype: MsgType.File,
|
|
body: "hello.txt",
|
|
file: {
|
|
url: undefined,
|
|
},
|
|
},
|
|
};
|
|
|
|
describe("HTMLExport", () => {
|
|
let client: jest.Mocked<MatrixClient>;
|
|
let room: Room;
|
|
|
|
filterConsole(
|
|
"Starting export",
|
|
"events in", // Fetched # events in # seconds
|
|
"events so far",
|
|
"Export successful!",
|
|
"does not have an m.room.create event",
|
|
"Creating HTML",
|
|
"Generating a ZIP",
|
|
"Cleaning up",
|
|
);
|
|
|
|
beforeEach(() => {
|
|
jest.useFakeTimers();
|
|
jest.setSystemTime(REPEATABLE_DATE);
|
|
|
|
client = stubClient() as jest.Mocked<MatrixClient>;
|
|
DMRoomMap.makeShared(client);
|
|
|
|
room = new Room("!myroom:example.org", client, "@me:example.org");
|
|
client.getRoom.mockReturnValue(room);
|
|
});
|
|
|
|
function mockMessages(...events: IRoomEvent[]): void {
|
|
client.createMessagesRequest.mockImplementation((_roomId, fromStr, limit = 30) => {
|
|
const from = fromStr === null ? 0 : parseInt(fromStr);
|
|
const chunk = events.slice(from, limit);
|
|
return Promise.resolve({
|
|
chunk,
|
|
from: from.toString(),
|
|
to: (from + limit).toString(),
|
|
});
|
|
});
|
|
}
|
|
|
|
/** Retrieve a map of files within the zip. */
|
|
function getFiles(exporter: HTMLExporter): { [filename: string]: Blob } {
|
|
//@ts-ignore private access
|
|
const files = exporter.files;
|
|
return files.reduce((d, f) => ({ ...d, [f.name]: f.blob }), {});
|
|
}
|
|
|
|
function getMessageFile(exporter: HTMLExporter): Blob {
|
|
const files = getFiles(exporter);
|
|
return files["messages.html"]!;
|
|
}
|
|
|
|
/** set a mock fetch response for an MXC */
|
|
function mockMxc(mxc: string, body: string) {
|
|
const media = mediaFromMxc(mxc, client);
|
|
fetchMock.get(media.srcHttp!, body);
|
|
}
|
|
|
|
function mockReactionForMessage(message: IRoomEvent): MatrixEvent {
|
|
const firstMessage = new MatrixEvent(message);
|
|
const reaction = mkReaction(firstMessage);
|
|
|
|
const relationsContainer = {
|
|
getRelations: jest.fn(),
|
|
getChildEventsForEvent: jest.fn(),
|
|
} as unknown as RelationsContainer;
|
|
const relations = new Relations(RelationType.Annotation, EventType.Reaction, client);
|
|
relations.addEvent(reaction);
|
|
relationsContainer.getChildEventsForEvent = jest
|
|
.fn()
|
|
.mockImplementation(
|
|
(eventId: string, relationType: RelationType | string, eventType: EventType | string) => {
|
|
if (eventId === firstMessage.getId()) {
|
|
return relations;
|
|
}
|
|
},
|
|
);
|
|
|
|
const timelineSet = {
|
|
relations: relationsContainer,
|
|
getLiveTimeline: () => timeline,
|
|
} as unknown as EventTimelineSet;
|
|
const timeline = new EventTimeline(timelineSet);
|
|
room.getUnfilteredTimelineSet = jest.fn().mockReturnValue(timelineSet);
|
|
return reaction;
|
|
}
|
|
|
|
it("should throw when created with invalid config for LastNMessages", async () => {
|
|
expect(
|
|
() =>
|
|
new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: undefined,
|
|
},
|
|
() => {},
|
|
),
|
|
).toThrow("Invalid export options");
|
|
});
|
|
|
|
it("should have an SDK-branded destination file name", () => {
|
|
const roomName = "My / Test / Room: Welcome";
|
|
const stubOptions: IExportOptions = {
|
|
attachmentsIncluded: false,
|
|
maxSize: 50000000,
|
|
numberOfMessages: 40,
|
|
};
|
|
const stubRoom = mkStubRoom("!myroom:example.org", roomName, client);
|
|
const exporter = new HTMLExporter(stubRoom, ExportType.LastNMessages, stubOptions, () => {});
|
|
|
|
expect(exporter.destinationFileName).toMatchSnapshot();
|
|
|
|
SdkConfig.put({ brand: "BrandedChat/WithSlashes/ForFun" });
|
|
|
|
expect(exporter.destinationFileName).toMatchSnapshot();
|
|
});
|
|
|
|
it("should export", async () => {
|
|
const events = [...Array(50)].map<IRoomEvent>((_, i) => ({
|
|
event_id: `${i}`,
|
|
type: EventType.RoomMessage,
|
|
sender: `@user${i}:example.com`,
|
|
origin_server_ts: 5_000 + i * 1000,
|
|
content: {
|
|
msgtype: "m.text",
|
|
body: `Message #${i}`,
|
|
},
|
|
}));
|
|
mockReactionForMessage(events[0]);
|
|
mockMessages(...events);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: events.length,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
const file = getMessageFile(exporter);
|
|
expect(await file.text()).toMatchSnapshot();
|
|
});
|
|
|
|
it("should include the room's avatar", async () => {
|
|
mockMessages(EVENT_MESSAGE);
|
|
|
|
const mxc = "mxc://www.example.com/avatars/nice-room.jpeg";
|
|
const avatar = "011011000110111101101100";
|
|
jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue(mxc);
|
|
mockMxc(mxc, avatar);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
const files = getFiles(exporter);
|
|
expect(await files["room.png"]!.text()).toBe(avatar);
|
|
});
|
|
|
|
it("should include the creation event", async () => {
|
|
const creator = "@bob:example.com";
|
|
mockMessages(EVENT_MESSAGE);
|
|
room.currentState.setStateEvents([
|
|
new MatrixEvent({
|
|
type: EventType.RoomCreate,
|
|
event_id: "$00001",
|
|
room_id: room.roomId,
|
|
sender: creator,
|
|
origin_server_ts: 0,
|
|
content: {},
|
|
state_key: "",
|
|
}),
|
|
]);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
expect(await getMessageFile(exporter).text()).toContain(`${creator} created this room.`);
|
|
});
|
|
|
|
it("should include the topic", async () => {
|
|
const topic = ":^-) (-^:";
|
|
mockMessages(EVENT_MESSAGE);
|
|
room.currentState.setStateEvents([
|
|
new MatrixEvent({
|
|
type: EventType.RoomTopic,
|
|
event_id: "$00001",
|
|
room_id: room.roomId,
|
|
sender: "@alice:example.com",
|
|
origin_server_ts: 0,
|
|
content: { topic },
|
|
state_key: "",
|
|
}),
|
|
]);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
expect(await getMessageFile(exporter).text()).toContain(`Topic: ${topic}`);
|
|
});
|
|
|
|
it("should include avatars", async () => {
|
|
mockMessages(EVENT_MESSAGE);
|
|
|
|
jest.spyOn(RoomMember.prototype, "getMxcAvatarUrl").mockReturnValue("mxc://example.org/avatar.bmp");
|
|
|
|
const avatarContent = "this is a bitmap all the pixels are red :^-)";
|
|
mockMxc("mxc://example.org/avatar.bmp", avatarContent);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
// Ensure that the avatar is present
|
|
const files = getFiles(exporter);
|
|
const file = files["users/@bob-example.com.png"];
|
|
expect(file).not.toBeUndefined();
|
|
|
|
// Ensure it has the expected content
|
|
expect(await file.text()).toBe(avatarContent);
|
|
});
|
|
|
|
it("should handle when an event has no sender", async () => {
|
|
const EVENT_MESSAGE_NO_SENDER: IRoomEvent = {
|
|
event_id: "$1",
|
|
type: EventType.RoomMessage,
|
|
sender: "",
|
|
origin_server_ts: 0,
|
|
content: {
|
|
msgtype: "m.text",
|
|
body: "Message with no sender",
|
|
},
|
|
};
|
|
mockMessages(EVENT_MESSAGE_NO_SENDER);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
const file = getMessageFile(exporter);
|
|
expect(await file.text()).toContain(EVENT_MESSAGE_NO_SENDER.content.body);
|
|
});
|
|
|
|
it("should handle when events sender cannot be found in room state", async () => {
|
|
mockMessages(EVENT_MESSAGE);
|
|
|
|
jest.spyOn(RoomState.prototype, "getSentinelMember").mockReturnValue(null);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
const file = getMessageFile(exporter);
|
|
expect(await file.text()).toContain(EVENT_MESSAGE.content.body);
|
|
});
|
|
|
|
it("should include attachments", async () => {
|
|
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
|
const attachmentBody = "Lorem ipsum dolor sit amet";
|
|
|
|
mockMxc("mxc://example.org/test-id", attachmentBody);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: true,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
// Ensure that the attachment is present
|
|
const files = getFiles(exporter);
|
|
const file = files[Object.keys(files).find((k) => k.endsWith(".txt"))!];
|
|
expect(file).not.toBeUndefined();
|
|
|
|
// Ensure that the attachment has the expected content
|
|
const text = await file.text();
|
|
expect(text).toBe(attachmentBody);
|
|
});
|
|
|
|
it("should handle when attachment cannot be fetched", async () => {
|
|
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT_MALFORMED, EVENT_ATTACHMENT);
|
|
const attachmentBody = "Lorem ipsum dolor sit amet";
|
|
|
|
mockMxc("mxc://example.org/test-id", attachmentBody);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: true,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
// good attachment present
|
|
const files = getFiles(exporter);
|
|
const file = files[Object.keys(files).find((k) => k.endsWith(".txt"))!];
|
|
expect(file).not.toBeUndefined();
|
|
|
|
// Ensure that the attachment has the expected content
|
|
const text = await file.text();
|
|
expect(text).toBe(attachmentBody);
|
|
|
|
// messages export still successful
|
|
const messagesFile = getMessageFile(exporter);
|
|
expect(await messagesFile.text()).toBeTruthy();
|
|
});
|
|
|
|
it("should handle when attachment srcHttp is falsy", async () => {
|
|
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
|
const attachmentBody = "Lorem ipsum dolor sit amet";
|
|
|
|
mockMxc("mxc://example.org/test-id", attachmentBody);
|
|
|
|
jest.spyOn(client, "mxcUrlToHttp").mockReturnValue(null);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: true,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
// attachment not present
|
|
const files = getFiles(exporter);
|
|
const file = files[Object.keys(files).find((k) => k.endsWith(".txt"))!];
|
|
expect(file).toBeUndefined();
|
|
|
|
// messages export still successful
|
|
const messagesFile = getMessageFile(exporter);
|
|
expect(await messagesFile.text()).toBeTruthy();
|
|
});
|
|
|
|
it("should omit attachments", async () => {
|
|
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
// Ensure that the attachment is present
|
|
const files = getFiles(exporter);
|
|
for (const fileName of Object.keys(files)) {
|
|
expect(fileName).not.toMatch(/^files\/hello/);
|
|
}
|
|
});
|
|
|
|
it("should add link to next and previous file", async () => {
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 5000,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
// test link to the first page
|
|
//@ts-ignore private access
|
|
let result = await exporter.wrapHTML("", 0, 3);
|
|
expect(result).not.toContain("Previous group of messages");
|
|
expect(result).toContain(
|
|
'<div style="text-align:center;margin:10px"><a href="./messages2.html" style="font-weight:bold">Next group of messages</a></div>',
|
|
);
|
|
|
|
// test link for a middle page
|
|
//@ts-ignore private access
|
|
result = await exporter.wrapHTML("", 1, 3);
|
|
expect(result).toContain(
|
|
'<div style="text-align:center"><a href="./messages.html" style="font-weight:bold">Previous group of messages</a></div>',
|
|
);
|
|
expect(result).toContain(
|
|
'<div style="text-align:center;margin:10px"><a href="./messages3.html" style="font-weight:bold">Next group of messages</a></div>',
|
|
);
|
|
|
|
// test link for last page
|
|
//@ts-ignore private access
|
|
result = await exporter.wrapHTML("", 2, 3);
|
|
expect(result).toContain(
|
|
'<div style="text-align:center"><a href="./messages2.html" style="font-weight:bold">Previous group of messages</a></div>',
|
|
);
|
|
expect(result).not.toContain("Next group of messages");
|
|
});
|
|
|
|
it("should not leak javascript from room names or topics", async () => {
|
|
const name = "<svg onload=alert(3)>";
|
|
const topic = "<svg onload=alert(5)>";
|
|
mockMessages(EVENT_MESSAGE);
|
|
room.currentState.setStateEvents([
|
|
new MatrixEvent({
|
|
type: EventType.RoomName,
|
|
event_id: "$00001",
|
|
room_id: room.roomId,
|
|
sender: "@alice:example.com",
|
|
origin_server_ts: 0,
|
|
content: { name },
|
|
state_key: "",
|
|
}),
|
|
new MatrixEvent({
|
|
type: EventType.RoomTopic,
|
|
event_id: "$00002",
|
|
room_id: room.roomId,
|
|
sender: "@alice:example.com",
|
|
origin_server_ts: 1,
|
|
content: { topic },
|
|
state_key: "",
|
|
}),
|
|
]);
|
|
room.recalculate();
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
const html = await getMessageFile(exporter).text();
|
|
|
|
expect(html).not.toContain(`${name}`);
|
|
expect(html).toContain(`${escapeHtml(name)}`);
|
|
expect(html).not.toContain(`${topic}`);
|
|
expect(html).toContain(`Topic: ${escapeHtml(topic)}`);
|
|
});
|
|
|
|
it("should not make /messages requests when exporting 'Current Timeline'", async () => {
|
|
client.createMessagesRequest.mockRejectedValue(new Error("Should never be called"));
|
|
room.addLiveEvents([
|
|
new MatrixEvent({
|
|
event_id: `$eventId`,
|
|
type: EventType.RoomMessage,
|
|
sender: client.getSafeUserId(),
|
|
origin_server_ts: 123456789,
|
|
content: {
|
|
msgtype: "m.text",
|
|
body: `testing testing`,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.Timeline,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
const file = getMessageFile(exporter);
|
|
expect(await file.text()).toContain("testing testing");
|
|
expect(client.createMessagesRequest).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should include reactions", async () => {
|
|
const reaction = mockReactionForMessage(EVENT_MESSAGE);
|
|
mockMessages(EVENT_MESSAGE);
|
|
const exporter = new HTMLExporter(
|
|
room,
|
|
ExportType.LastNMessages,
|
|
{
|
|
attachmentsIncluded: false,
|
|
maxSize: 1_024 * 1_024,
|
|
numberOfMessages: 40,
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
await exporter.export();
|
|
|
|
const file = getMessageFile(exporter);
|
|
expect(await file.text()).toContain(reaction.getContent()["m.relates_to"]?.key);
|
|
});
|
|
});
|