Apply `strictNullChecks` to `src/utils/exportUtils` (#10379)
* Apply `strictNullChecks` to `src/utils/exportUtils` * strict fix * test coverage * lint * test coverage * one more testpull/28788/head^2
parent
1447829543
commit
9a733a6444
|
@ -47,7 +47,8 @@ export class DecryptError extends Error {
|
||||||
* @param {IMediaEventInfo} info The info parameter taken from the matrix event.
|
* @param {IMediaEventInfo} info The info parameter taken from the matrix event.
|
||||||
* @returns {Promise<Blob>} Resolves to a Blob of the file.
|
* @returns {Promise<Blob>} Resolves to a Blob of the file.
|
||||||
*/
|
*/
|
||||||
export async function decryptFile(file: IEncryptedFile, info?: IMediaEventInfo): Promise<Blob> {
|
export async function decryptFile(file?: IEncryptedFile, info?: IMediaEventInfo): Promise<Blob> {
|
||||||
|
// throws if file is falsy
|
||||||
const media = mediaFromContent({ file });
|
const media = mediaFromContent({ file });
|
||||||
|
|
||||||
let responseData: ArrayBuffer;
|
let responseData: ArrayBuffer;
|
||||||
|
@ -64,7 +65,7 @@ export async function decryptFile(file: IEncryptedFile, info?: IMediaEventInfo):
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Decrypt the array buffer using the information taken from the event content.
|
// Decrypt the array buffer using the information taken from the event content.
|
||||||
const dataArray = await encrypt.decryptAttachment(responseData, file);
|
const dataArray = await encrypt.decryptAttachment(responseData, file!);
|
||||||
// Turn the array into a Blob and give it the correct MIME-type.
|
// Turn the array into a Blob and give it the correct MIME-type.
|
||||||
|
|
||||||
// IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
|
// IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
|
||||||
|
|
|
@ -51,7 +51,8 @@ export default abstract class Exporter {
|
||||||
if (
|
if (
|
||||||
exportOptions.maxSize < 1 * 1024 * 1024 || // Less than 1 MB
|
exportOptions.maxSize < 1 * 1024 * 1024 || // Less than 1 MB
|
||||||
exportOptions.maxSize > 8000 * 1024 * 1024 || // More than 8 GB
|
exportOptions.maxSize > 8000 * 1024 * 1024 || // More than 8 GB
|
||||||
exportOptions.numberOfMessages > 10 ** 8
|
(!!exportOptions.numberOfMessages && exportOptions.numberOfMessages > 10 ** 8) ||
|
||||||
|
(exportType === ExportType.LastNMessages && !exportOptions.numberOfMessages)
|
||||||
) {
|
) {
|
||||||
throw new Error("Invalid export options");
|
throw new Error("Invalid export options");
|
||||||
}
|
}
|
||||||
|
@ -123,10 +124,11 @@ export default abstract class Exporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setEventMetadata(event: MatrixEvent): MatrixEvent {
|
protected setEventMetadata(event: MatrixEvent): MatrixEvent {
|
||||||
const roomState = this.client.getRoom(this.room.roomId).currentState;
|
const roomState = this.client.getRoom(this.room.roomId)?.currentState;
|
||||||
event.sender = roomState.getSentinelMember(event.getSender());
|
const sender = event.getSender();
|
||||||
|
event.sender = (!!sender && roomState?.getSentinelMember(sender)) || null;
|
||||||
if (event.getType() === "m.room.member") {
|
if (event.getType() === "m.room.member") {
|
||||||
event.target = roomState.getSentinelMember(event.getStateKey());
|
event.target = roomState?.getSentinelMember(event.getStateKey()!) ?? null;
|
||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
@ -135,6 +137,8 @@ export default abstract class Exporter {
|
||||||
let limit: number;
|
let limit: number;
|
||||||
switch (this.exportType) {
|
switch (this.exportType) {
|
||||||
case ExportType.LastNMessages:
|
case ExportType.LastNMessages:
|
||||||
|
// validated in constructor that numberOfMessages is defined
|
||||||
|
// when export type is LastNMessages
|
||||||
limit = this.exportOptions.numberOfMessages!;
|
limit = this.exportOptions.numberOfMessages!;
|
||||||
break;
|
break;
|
||||||
case ExportType.Timeline:
|
case ExportType.Timeline:
|
||||||
|
@ -221,8 +225,14 @@ export default abstract class Exporter {
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getMediaBlob(event: MatrixEvent): Promise<Blob | undefined> {
|
/**
|
||||||
let blob: Blob | undefined;
|
* Decrypts if necessary, and fetches media from a matrix event
|
||||||
|
* @param event - matrix event with media event content
|
||||||
|
* @resolves when media has been fetched
|
||||||
|
* @throws if media was unable to be fetched
|
||||||
|
*/
|
||||||
|
protected async getMediaBlob(event: MatrixEvent): Promise<Blob> {
|
||||||
|
let blob: Blob | undefined = undefined;
|
||||||
try {
|
try {
|
||||||
const isEncrypted = event.isEncrypted();
|
const isEncrypted = event.isEncrypted();
|
||||||
const content = event.getContent<IMediaEventContent>();
|
const content = event.getContent<IMediaEventContent>();
|
||||||
|
@ -231,12 +241,18 @@ export default abstract class Exporter {
|
||||||
blob = await decryptFile(content.file);
|
blob = await decryptFile(content.file);
|
||||||
} else {
|
} else {
|
||||||
const media = mediaFromContent(content);
|
const media = mediaFromContent(content);
|
||||||
|
if (!media.srcHttp) {
|
||||||
|
throw new Error("Cannot fetch without srcHttp");
|
||||||
|
}
|
||||||
const image = await fetch(media.srcHttp);
|
const image = await fetch(media.srcHttp);
|
||||||
blob = await image.blob();
|
blob = await image.blob();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log("Error decrypting media");
|
logger.log("Error decrypting media");
|
||||||
}
|
}
|
||||||
|
if (!blob) {
|
||||||
|
throw new Error("Unable to fetch file");
|
||||||
|
}
|
||||||
return blob;
|
return blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventType, IRoomEvent, MatrixClient, MatrixEvent, MsgType, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
import {
|
||||||
|
EventType,
|
||||||
|
IRoomEvent,
|
||||||
|
MatrixClient,
|
||||||
|
MatrixEvent,
|
||||||
|
MsgType,
|
||||||
|
Room,
|
||||||
|
RoomMember,
|
||||||
|
RoomState,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
|
||||||
import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../test-utils";
|
import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../test-utils";
|
||||||
|
@ -51,6 +60,20 @@ const EVENT_ATTACHMENT: IRoomEvent = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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", () => {
|
describe("HTMLExport", () => {
|
||||||
let client: jest.Mocked<MatrixClient>;
|
let client: jest.Mocked<MatrixClient>;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
@ -107,6 +130,22 @@ describe("HTMLExport", () => {
|
||||||
fetchMock.get(media.srcHttp!, body);
|
fetchMock.get(media.srcHttp!, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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", () => {
|
it("should have an SDK-branded destination file name", () => {
|
||||||
const roomName = "My / Test / Room: Welcome";
|
const roomName = "My / Test / Room: Welcome";
|
||||||
const stubOptions: IExportOptions = {
|
const stubOptions: IExportOptions = {
|
||||||
|
@ -266,6 +305,56 @@ describe("HTMLExport", () => {
|
||||||
expect(await file.text()).toBe(avatarContent);
|
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.Timeline,
|
||||||
|
{
|
||||||
|
attachmentsIncluded: false,
|
||||||
|
maxSize: 1_024 * 1_024,
|
||||||
|
},
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
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.Timeline,
|
||||||
|
{
|
||||||
|
attachmentsIncluded: false,
|
||||||
|
maxSize: 1_024 * 1_024,
|
||||||
|
},
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
await exporter.export();
|
||||||
|
|
||||||
|
const file = getMessageFile(exporter);
|
||||||
|
expect(await file.text()).toContain(EVENT_MESSAGE.content.body);
|
||||||
|
});
|
||||||
|
|
||||||
it("should include attachments", async () => {
|
it("should include attachments", async () => {
|
||||||
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
||||||
const attachmentBody = "Lorem ipsum dolor sit amet";
|
const attachmentBody = "Lorem ipsum dolor sit amet";
|
||||||
|
@ -294,6 +383,68 @@ describe("HTMLExport", () => {
|
||||||
expect(text).toBe(attachmentBody);
|
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.Timeline,
|
||||||
|
{
|
||||||
|
attachmentsIncluded: true,
|
||||||
|
maxSize: 1_024 * 1_024,
|
||||||
|
},
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
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.Timeline,
|
||||||
|
{
|
||||||
|
attachmentsIncluded: true,
|
||||||
|
maxSize: 1_024 * 1_024,
|
||||||
|
},
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
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 () => {
|
it("should omit attachments", async () => {
|
||||||
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
||||||
|
|
||||||
|
@ -323,6 +474,7 @@ describe("HTMLExport", () => {
|
||||||
{
|
{
|
||||||
attachmentsIncluded: false,
|
attachmentsIncluded: false,
|
||||||
maxSize: 1_024 * 1_024,
|
maxSize: 1_024 * 1_024,
|
||||||
|
numberOfMessages: 5000,
|
||||||
},
|
},
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue