diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts new file mode 100644 index 0000000000..27abc6bc50 --- /dev/null +++ b/src/customisations/Media.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2021 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 {MatrixClientPeg} from "../MatrixClientPeg"; +import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; + +// Populate this class with the details of your customisations when copying it. + +// Implementation note: The Media class must complete the contract as shown here, though +// the constructor can be whatever is relevant to your implementation. The mediaForX +// functions below create an instance of the Media class and are used throughout the +// project. + +/** + * A media object is a representation of a "source media" and an optional + * "thumbnail media", derived from event contents or external sources. + */ +export class Media { + // Per above, this constructor signature can be whatever is helpful for you. + constructor(private prepared: IPreparedMedia) { + } + + /** + * The MXC URI of the source media. + */ + public get srcMxc(): string { + return this.prepared.mxc; + } + + /** + * The MXC URI of the thumbnail media, if a thumbnail is recorded. Null/undefined + * otherwise. + */ + public get thumbnailMxc(): string | undefined | null { + return this.prepared.thumbnail?.mxc; + } + + /** + * Whether or not a thumbnail is recorded for this media. + */ + public get hasThumbnail(): boolean { + return !!this.thumbnailMxc; + } + + /** + * The HTTP URL for the source media. + */ + public get srcHttp(): string { + return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc); + } + + /** + * Gets the HTTP URL for the thumbnail media with the requested characteristics, if a thumbnail + * is recorded for this media. Returns null/undefined otherwise. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {string} The HTTP URL which points to the thumbnail. + */ + public getThumbnailHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string | null | undefined { + if (!this.hasThumbnail) return null; + return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode); + } + + /** + * Gets the HTTP URL for a thumbnail of the source media with the requested characteristics. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {string} The HTTP URL which points to the thumbnail. + */ + public getThumbnailOfSourceHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string { + return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode); + } + + /** + * Downloads the source media. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadSource(): Promise { + return fetch(this.srcHttp); + } + + /** + * Downloads the thumbnail media with the requested characteristics. If no thumbnail media is present, + * this throws an exception. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadThumbnail(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail"); + return fetch(this.getThumbnailHttp(width, height, mode)); + } + + /** + * Downloads a thumbnail of the source media with the requested characteristics. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadThumbnailOfSource(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + return fetch(this.getThumbnailOfSourceHttp(width, height, mode)); + } +} + +/** + * Creates a media object from event content. + * @param {IMediaEventContent} content The event content. + * @returns {Media} The media object. + */ +export function mediaFromContent(content: IMediaEventContent): Media { + return new Media(prepEventContentAsMedia(content)); +} + +/** + * Creates a media object from an MXC URI. + * @param {string} mxc The MXC URI. + * @returns {Media} The media object. + */ +export function mediaFromMxc(mxc: string): Media { + return mediaFromContent({url: mxc}); +} diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts new file mode 100644 index 0000000000..0211a63787 --- /dev/null +++ b/src/customisations/models/IMediaEventContent.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2021 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. + */ + +// TODO: These types should be elsewhere. + +export interface IEncryptedFile { + url: string; + key: { + alg: string; + key_ops: string[]; + kty: string; + k: string; + ext: boolean; + }; + iv: string; + hashes: {[alg: string]: string}; + v: string; +} + +export interface IMediaEventContent { + url?: string; // required on unencrypted media + file?: IEncryptedFile; // required for *encrypted* media + info?: { + thumbnail_url?: string; + thumbnail_file?: IEncryptedFile; + }; +} + +export interface IPreparedMedia extends IMediaObject { + thumbnail?: IMediaObject; +} + +export interface IMediaObject { + mxc: string; + file?: IEncryptedFile; +} + +/** + * Parses an event content body into a prepared media object. This prepared media object + * can be used with other functions to manipulate the media. + * @param {IMediaEventContent} content Unredacted media event content. See interface. + * @returns {IPreparedMedia} A prepared media object. + * @throws Throws if the given content cannot be packaged into a prepared media object. + */ +export function prepEventContentAsMedia(content: IMediaEventContent): IPreparedMedia { + let thumbnail: IMediaObject = null; + if (content?.info?.thumbnail_url) { + thumbnail = { + mxc: content.info.thumbnail_url, + file: content.info.thumbnail_file, + }; + } else if (content?.info?.thumbnail_file?.url) { + thumbnail = { + mxc: content.info.thumbnail_file.url, + file: content.info.thumbnail_file, + }; + } + + if (content?.url) { + return { + thumbnail, + mxc: content.url, + file: content.file, + }; + } else if (content?.file?.url) { + return { + thumbnail, + mxc: content.file.url, + file: content.file, + }; + } + + throw new Error("Invalid file provided: cannot determine MXC URI. Has it been redacted?"); +} diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js index d3625d614a..fb3600cd79 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.js @@ -19,6 +19,7 @@ limitations under the License. import encrypt from 'browser-encrypt-attachment'; // Grab the client so that we can turn mxc:// URLs into https:// URLS. import {MatrixClientPeg} from '../MatrixClientPeg'; +import {mediaFromContent} from "../customisations/Media"; // WARNING: We have to be very careful about what mime-types we allow into blobs, // as for performance reasons these are now rendered via URL.createObjectURL() @@ -87,9 +88,9 @@ const ALLOWED_BLOB_MIMETYPES = { * @returns {Promise} */ export function decryptFile(file) { - const url = MatrixClientPeg.get().mxcUrlToHttp(file.url); + const media = mediaFromContent({file}); // Download the encrypted file as an array buffer. - return Promise.resolve(fetch(url)).then(function(response) { + return media.downloadSource().then(function(response) { return response.arrayBuffer(); }).then(function(responseData) { // Decrypt the array buffer using the information taken from