UNPKG

matrix-js-sdk

Version:
220 lines (189 loc) 8.07 kB
/* 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 { MatrixClient } from "../client"; import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event"; import { IContent, MatrixEvent } from "./event"; import { MSC3089TreeSpace } from "./MSC3089TreeSpace"; import type { ReadStream } from "fs"; /** * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference * to a file (leaf) in the tree. Note that this is UNSTABLE and subject to breaking changes * without notice. */ export class MSC3089Branch { public constructor( private client: MatrixClient, public readonly indexEvent: MatrixEvent, public readonly directory: MSC3089TreeSpace, ) { // Nothing to do } /** * The file ID. */ public get id(): string { return this.indexEvent.getStateKey(); } /** * Whether this branch is active/valid. */ public get isActive(): boolean { return this.indexEvent.getContent()["active"] === true; } /** * Version for the file, one-indexed. */ public get version(): number { return this.indexEvent.getContent()["version"] ?? 1; } private get roomId(): string { return this.indexEvent.getRoomId(); } /** * Deletes the file from the tree, including all prior edits/versions. * @returns {Promise<void>} Resolves when complete. */ public async delete(): Promise<void> { await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {}, this.id); await this.client.redactEvent(this.roomId, this.id); const nextVersion = (await this.getVersionHistory())[1]; // [0] will be us if (nextVersion) await nextVersion.delete(); // implicit recursion } /** * Gets the name for this file. * @returns {string} The name, or "Unnamed File" if unknown. */ public getName(): string { return this.indexEvent.getContent()['name'] || "Unnamed File"; } /** * Sets the name for this file. * @param {string} name The new name for this file. * @returns {Promise<void>} Resolves when complete. */ public async setName(name: string): Promise<void> { await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, { ...this.indexEvent.getContent(), name: name, }, this.id); } /** * Gets whether or not a file is locked. * @returns {boolean} True if locked, false otherwise. */ public isLocked(): boolean { return this.indexEvent.getContent()['locked'] || false; } /** * Sets a file as locked or unlocked. * @param {boolean} locked True to lock the file, false otherwise. * @returns {Promise<void>} Resolves when complete. */ public async setLocked(locked: boolean): Promise<void> { await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, { ...this.indexEvent.getContent(), locked: locked, }, this.id); } /** * Gets information about the file needed to download it. * @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file. */ public async getFileInfo(): Promise<{ info: IEncryptedFile, httpUrl: string }> { const event = await this.getFileEvent(); const file = event.getOriginalContent()['file']; const httpUrl = this.client.mxcUrlToHttp(file['url']); return { info: file, httpUrl: httpUrl }; } /** * Gets the event the file points to. * @returns {Promise<MatrixEvent>} Resolves to the file's event. */ public async getFileEvent(): Promise<MatrixEvent> { const room = this.client.getRoom(this.roomId); if (!room) throw new Error("Unknown room"); const event = room.getUnfilteredTimelineSet().findEventById(this.id); if (!event) throw new Error("Failed to find event"); // Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true` // to ensure that the relations system in the sdk will function. await this.client.decryptEventIfNeeded(event, { emit: true, isRetry: true }); return event; } /** * Creates a new version of this file with contents in a type that is compatible with MatrixClient.uploadContent(). * @param {string} name The name of the file. * @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents. * @param {Partial<IEncryptedFile>} info The encrypted file information. * @param {IContent} additionalContent Optional event content fields to include in the message. * @returns {Promise<void>} Resolves when uploaded. */ public async createNewVersion( name: string, encryptedContents: File | String | Buffer | ReadStream | Blob, info: Partial<IEncryptedFile>, additionalContent?: IContent, ): Promise<void> { const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, { ...(additionalContent ?? {}), "m.new_content": true, "m.relates_to": { "rel_type": RelationType.Replace, "event_id": this.id, }, }); // Update the version of the new event await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, { active: true, name: name, version: this.version + 1, }, fileEventResponse['event_id']); // Deprecate ourselves await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, { ...(this.indexEvent.getContent()), active: false, }, this.id); } /** * Gets the file's version history, starting at this file. * @returns {Promise<MSC3089Branch[]>} Resolves to the file's version history, with the * first element being the current version and the last element being the first version. */ public async getVersionHistory(): Promise<MSC3089Branch[]> { const fileHistory: MSC3089Branch[] = []; fileHistory.push(this); // start with ourselves const room = this.client.getRoom(this.roomId); if (!room) throw new Error("Invalid or unknown room"); // Clone the timeline to reverse it, getting most-recent-first ordering, hopefully // shortening the awful loop below. Without the clone, we can unintentionally mutate // the timeline. const timelineEvents = [...room.getLiveTimeline().getEvents()].reverse(); // XXX: This is a very inefficient search, but it's the best we can do with the // relations structure we have in the SDK. As of writing, it is not worth the // investment in improving the structure. let childEvent: MatrixEvent; let parentEvent = await this.getFileEvent(); do { childEvent = timelineEvents.find(e => e.replacingEventId() === parentEvent.getId()); if (childEvent) { const branch = this.directory.getFile(childEvent.getId()); if (branch) { fileHistory.push(branch); parentEvent = childEvent; } else { break; // prevent infinite loop } } } while (childEvent); return fileHistory; } }