diff options
author | RaindropsSys <raindrops@equestria.dev> | 2023-11-17 23:25:29 +0100 |
---|---|---|
committer | RaindropsSys <raindrops@equestria.dev> | 2023-11-17 23:25:29 +0100 |
commit | 953ddd82e48dd206cef5ac94456549aed13b3ad5 (patch) | |
tree | 8f003106ee2e7f422e5a22d2ee04d0db302e66c0 /includes/external/matrix/node_modules/matrix-js-sdk/src/models | |
parent | 62a9199846b0c07c03218703b33e8385764f42d9 (diff) | |
download | pluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.tar.gz pluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.tar.bz2 pluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.zip |
Updated 30 files and deleted 2976 files (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/models')
23 files changed, 0 insertions, 11899 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts deleted file mode 100644 index 27be4b8..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts +++ /dev/null @@ -1,258 +0,0 @@ -/* -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 { EventTimeline } from "./event-timeline"; -import { FileType } from "../http-api"; -import type { ISendEventResponse } from "../@types/requests"; - -/** - * 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 { - const stateKey = this.indexEvent.getStateKey(); - if (!stateKey) { - throw new Error("State key not found for branch"); - } - return stateKey; - } - - /** - * 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 which 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 The name, or "Unnamed File" if unknown. - */ - public getName(): string { - return this.indexEvent.getContent()["name"] || "Unnamed File"; - } - - /** - * Sets the name for this file. - * @param name - The new name for this file. - * @returns Promise which 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 True if locked, false otherwise. - */ - public isLocked(): boolean { - return this.indexEvent.getContent()["locked"] || false; - } - - /** - * Sets a file as locked or unlocked. - * @param locked - True to lock the file, false otherwise. - * @returns Promise which 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 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"]); - - if (!httpUrl) { - throw new Error(`No HTTP URL available for ${file["url"]}`); - } - - return { info: file, httpUrl: httpUrl }; - } - - /** - * Gets the event the file points to. - * @returns Promise which 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"); - - let event: MatrixEvent | undefined = room.getUnfilteredTimelineSet().findEventById(this.id); - - // keep scrolling back if needed until we find the event or reach the start of the room: - while (!event && room.getLiveTimeline().getState(EventTimeline.BACKWARDS)!.paginationToken) { - await this.client.scrollback(room, 100); - 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 name - The name of the file. - * @param encryptedContents - The encrypted contents. - * @param info - The encrypted file information. - * @param additionalContent - Optional event content fields to include in the message. - * @returns Promise which resolves to the file event's sent response. - */ - public async createNewVersion( - name: string, - encryptedContents: FileType, - info: Partial<IEncryptedFile>, - additionalContent?: IContent, - ): Promise<ISendEventResponse> { - 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, - ); - - return fileEventResponse; - } - - /** - * Gets the file's version history, starting at this file. - * @returns Promise which 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 | undefined; - 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; - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts deleted file mode 100644 index b0e71d9..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts +++ /dev/null @@ -1,566 +0,0 @@ -/* -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 promiseRetry from "p-retry"; - -import { MatrixClient } from "../client"; -import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event"; -import { Room } from "./room"; -import { logger } from "../logger"; -import { IContent, MatrixEvent } from "./event"; -import { - averageBetweenStrings, - DEFAULT_ALPHABET, - lexicographicCompare, - nextString, - prevString, - simpleRetryOperation, -} from "../utils"; -import { MSC3089Branch } from "./MSC3089Branch"; -import { isRoomSharedHistory } from "../crypto/algorithms/megolm"; -import { ISendEventResponse } from "../@types/requests"; -import { FileType } from "../http-api"; - -/** - * The recommended defaults for a tree space's power levels. Note that this - * is UNSTABLE and subject to breaking changes without notice. - */ -export const DEFAULT_TREE_POWER_LEVELS_TEMPLATE = { - // Owner - invite: 100, - kick: 100, - ban: 100, - - // Editor - redact: 50, - state_default: 50, - events_default: 50, - - // Viewer - users_default: 0, - - // Mixed - events: { - [EventType.RoomPowerLevels]: 100, - [EventType.RoomHistoryVisibility]: 100, - [EventType.RoomTombstone]: 100, - [EventType.RoomEncryption]: 100, - [EventType.RoomName]: 50, - [EventType.RoomMessage]: 50, - [EventType.RoomMessageEncrypted]: 50, - [EventType.Sticker]: 50, - }, - - users: {}, // defined by calling code -}; - -/** - * Ease-of-use representation for power levels represented as simple roles. - * Note that this is UNSTABLE and subject to breaking changes without notice. - */ -export enum TreePermissions { - Viewer = "viewer", // Default - Editor = "editor", // "Moderator" or ~PL50 - Owner = "owner", // "Admin" or PL100 -} - -/** - * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) - * file tree Space. Note that this is UNSTABLE and subject to breaking changes - * without notice. - */ -export class MSC3089TreeSpace { - public readonly room: Room; - - public constructor(private client: MatrixClient, public readonly roomId: string) { - this.room = this.client.getRoom(this.roomId)!; - - if (!this.room) throw new Error("Unknown room"); - } - - /** - * Syntactic sugar for room ID of the Space. - */ - public get id(): string { - return this.roomId; - } - - /** - * Whether or not this is a top level space. - */ - public get isTopLevel(): boolean { - // XXX: This is absolutely not how you find out if the space is top level - // but is safe for a managed usecase like we offer in the SDK. - const parentEvents = this.room.currentState.getStateEvents(EventType.SpaceParent); - if (!parentEvents?.length) return true; - return parentEvents.every((e) => !e.getContent()?.["via"]); - } - - /** - * Sets the name of the tree space. - * @param name - The new name for the space. - * @returns Promise which resolves when complete. - */ - public async setName(name: string): Promise<void> { - await this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, ""); - } - - /** - * Invites a user to the tree space. They will be given the default Viewer - * permission level unless specified elsewhere. - * @param userId - The user ID to invite. - * @param andSubspaces - True (default) to invite the user to all - * directories/subspaces too, recursively. - * @param shareHistoryKeys - True (default) to share encryption keys - * with the invited user. This will allow them to decrypt the events (files) - * in the tree. Keys will not be shared if the room is lacking appropriate - * history visibility (by default, history visibility is "shared" in trees, - * which is an appropriate visibility for these purposes). - * @returns Promise which resolves when complete. - */ - public async invite(userId: string, andSubspaces = true, shareHistoryKeys = true): Promise<void> { - const promises: Promise<void>[] = [this.retryInvite(userId)]; - if (andSubspaces) { - promises.push(...this.getDirectories().map((d) => d.invite(userId, andSubspaces, shareHistoryKeys))); - } - return Promise.all(promises).then(() => { - // Note: key sharing is default on because for file trees it is relatively important that the invite - // target can actually decrypt the files. The implied use case is that by inviting a user to the tree - // it means the sender would like the receiver to view/download the files contained within, much like - // sharing a folder in other circles. - if (shareHistoryKeys && isRoomSharedHistory(this.room)) { - // noinspection JSIgnoredPromiseFromCall - we aren't concerned as much if this fails. - this.client.sendSharedHistoryKeys(this.roomId, [userId]); - } - }); - } - - private retryInvite(userId: string): Promise<void> { - return simpleRetryOperation(async () => { - await this.client.invite(this.roomId, userId).catch((e) => { - // We don't want to retry permission errors forever... - if (e?.errcode === "M_FORBIDDEN") { - throw new promiseRetry.AbortError(e); - } - throw e; - }); - }); - } - - /** - * Sets the permissions of a user to the given role. Note that if setting a user - * to Owner then they will NOT be able to be demoted. If the user does not have - * permission to change the power level of the target, an error will be thrown. - * @param userId - The user ID to change the role of. - * @param role - The role to assign. - * @returns Promise which resolves when complete. - */ - public async setPermissions(userId: string, role: TreePermissions): Promise<void> { - const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); - if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels"); - - const pls = currentPls?.getContent() || {}; - const viewLevel = pls["users_default"] || 0; - const editLevel = pls["events_default"] || 50; - const adminLevel = pls["events"]?.[EventType.RoomPowerLevels] || 100; - - const users = pls["users"] || {}; - switch (role) { - case TreePermissions.Viewer: - users[userId] = viewLevel; - break; - case TreePermissions.Editor: - users[userId] = editLevel; - break; - case TreePermissions.Owner: - users[userId] = adminLevel; - break; - default: - throw new Error("Invalid role: " + role); - } - pls["users"] = users; - - await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, ""); - } - - /** - * Gets the current permissions of a user. Note that any users missing explicit permissions (or not - * in the space) will be considered Viewers. Appropriate membership checks need to be performed - * elsewhere. - * @param userId - The user ID to check permissions of. - * @returns The permissions for the user, defaulting to Viewer. - */ - public getPermissions(userId: string): TreePermissions { - const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); - if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels"); - - const pls = currentPls?.getContent() || {}; - const viewLevel = pls["users_default"] || 0; - const editLevel = pls["events_default"] || 50; - const adminLevel = pls["events"]?.[EventType.RoomPowerLevels] || 100; - - const userLevel = pls["users"]?.[userId] || viewLevel; - if (userLevel >= adminLevel) return TreePermissions.Owner; - if (userLevel >= editLevel) return TreePermissions.Editor; - return TreePermissions.Viewer; - } - - /** - * Creates a directory under this tree space, represented as another tree space. - * @param name - The name for the directory. - * @returns Promise which resolves to the created directory. - */ - public async createDirectory(name: string): Promise<MSC3089TreeSpace> { - const directory = await this.client.unstableCreateFileTree(name); - - await this.client.sendStateEvent( - this.roomId, - EventType.SpaceChild, - { - via: [this.client.getDomain()], - }, - directory.roomId, - ); - - await this.client.sendStateEvent( - directory.roomId, - EventType.SpaceParent, - { - via: [this.client.getDomain()], - }, - this.roomId, - ); - - return directory; - } - - /** - * Gets a list of all known immediate subdirectories to this tree space. - * @returns The tree spaces (directories). May be empty, but not null. - */ - public getDirectories(): MSC3089TreeSpace[] { - const trees: MSC3089TreeSpace[] = []; - const children = this.room.currentState.getStateEvents(EventType.SpaceChild); - for (const child of children) { - try { - const stateKey = child.getStateKey(); - if (stateKey) { - const tree = this.client.unstableGetFileTreeSpace(stateKey); - if (tree) trees.push(tree); - } - } catch (e) { - logger.warn("Unable to create tree space instance for listing. Are we joined?", e); - } - } - return trees; - } - - /** - * Gets a subdirectory of a given ID under this tree space. Note that this will not recurse - * into children and instead only look one level deep. - * @param roomId - The room ID (directory ID) to find. - * @returns The directory, or undefined if not found. - */ - public getDirectory(roomId: string): MSC3089TreeSpace | undefined { - return this.getDirectories().find((r) => r.roomId === roomId); - } - - /** - * Deletes the tree, kicking all members and deleting **all subdirectories**. - * @returns Promise which resolves when complete. - */ - public async delete(): Promise<void> { - const subdirectories = this.getDirectories(); - for (const dir of subdirectories) { - await dir.delete(); - } - - const kickMemberships = ["invite", "knock", "join"]; - const members = this.room.currentState.getStateEvents(EventType.RoomMember); - for (const member of members) { - const isNotUs = member.getStateKey() !== this.client.getUserId(); - if (isNotUs && kickMemberships.includes(member.getContent().membership!)) { - const stateKey = member.getStateKey(); - if (!stateKey) { - throw new Error("State key not found for branch"); - } - await this.client.kick(this.roomId, stateKey, "Room deleted"); - } - } - - await this.client.leave(this.roomId); - } - - private getOrderedChildren(children: MatrixEvent[]): { roomId: string; order: string }[] { - const ordered: { roomId: string; order: string }[] = children - .map((c) => ({ roomId: c.getStateKey(), order: c.getContent()["order"] })) - .filter((c) => c.roomId) as { roomId: string; order: string }[]; - ordered.sort((a, b) => { - if (a.order && !b.order) { - return -1; - } else if (!a.order && b.order) { - return 1; - } else if (!a.order && !b.order) { - const roomA = this.client.getRoom(a.roomId); - const roomB = this.client.getRoom(b.roomId); - if (!roomA || !roomB) { - // just don't bother trying to do more partial sorting - return lexicographicCompare(a.roomId, b.roomId); - } - - const createTsA = roomA.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0; - const createTsB = roomB.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0; - if (createTsA === createTsB) { - return lexicographicCompare(a.roomId, b.roomId); - } - return createTsA - createTsB; - } else { - // both not-null orders - return lexicographicCompare(a.order, b.order); - } - }); - return ordered; - } - - private getParentRoom(): Room { - const parents = this.room.currentState.getStateEvents(EventType.SpaceParent); - const parent = parents[0]; // XXX: Wild assumption - if (!parent) throw new Error("Expected to have a parent in a non-top level space"); - - // XXX: We are assuming the parent is a valid tree space. - // We probably don't need to validate the parent room state for this usecase though. - const stateKey = parent.getStateKey(); - if (!stateKey) throw new Error("No state key found for parent"); - const parentRoom = this.client.getRoom(stateKey); - if (!parentRoom) throw new Error("Unable to locate room for parent"); - - return parentRoom; - } - - /** - * Gets the current order index for this directory. Note that if this is the top level space - * then -1 will be returned. - * @returns The order index of this space. - */ - public getOrder(): number { - if (this.isTopLevel) return -1; - - const parentRoom = this.getParentRoom(); - const children = parentRoom.currentState.getStateEvents(EventType.SpaceChild); - const ordered = this.getOrderedChildren(children); - - return ordered.findIndex((c) => c.roomId === this.roomId); - } - - /** - * Sets the order index for this directory within its parent. Note that if this is a top level - * space then an error will be thrown. -1 can be used to move the child to the start, and numbers - * larger than the number of children can be used to move the child to the end. - * @param index - The new order index for this space. - * @returns Promise which resolves when complete. - * @throws Throws if this is a top level space. - */ - public async setOrder(index: number): Promise<void> { - if (this.isTopLevel) throw new Error("Cannot set order of top level spaces currently"); - - const parentRoom = this.getParentRoom(); - const children = parentRoom.currentState.getStateEvents(EventType.SpaceChild); - const ordered = this.getOrderedChildren(children); - index = Math.max(Math.min(index, ordered.length - 1), 0); - - const currentIndex = this.getOrder(); - const movingUp = currentIndex < index; - if (movingUp && index === ordered.length - 1) { - index--; - } else if (!movingUp && index === 0) { - index++; - } - - const prev = ordered[movingUp ? index : index - 1]; - const next = ordered[movingUp ? index + 1 : index]; - - let newOrder = DEFAULT_ALPHABET[0]; - let ensureBeforeIsSane = false; - if (!prev) { - // Move to front - if (next?.order) { - newOrder = prevString(next.order); - } - } else if (index === ordered.length - 1) { - // Move to back - if (next?.order) { - newOrder = nextString(next.order); - } - } else { - // Move somewhere in the middle - const startOrder = prev?.order; - const endOrder = next?.order; - if (startOrder && endOrder) { - if (startOrder === endOrder) { - // Error case: just move +1 to break out of awful math - newOrder = nextString(startOrder); - } else { - newOrder = averageBetweenStrings(startOrder, endOrder); - } - } else { - if (startOrder) { - // We're at the end (endOrder is null, so no explicit order) - newOrder = nextString(startOrder); - } else if (endOrder) { - // We're at the start (startOrder is null, so nothing before us) - newOrder = prevString(endOrder); - } else { - // Both points are unknown. We're likely in a range where all the children - // don't have particular order values, so we may need to update them too. - // The other possibility is there's only us as a child, but we should have - // shown up in the other states. - ensureBeforeIsSane = true; - } - } - } - - if (ensureBeforeIsSane) { - // We were asked by the order algorithm to prepare the moving space for a landing - // in the undefined order part of the order array, which means we need to update the - // spaces that come before it with a stable order value. - let lastOrder: string | undefined; - for (let i = 0; i <= index; i++) { - const target = ordered[i]; - if (i === 0) { - lastOrder = target.order; - } - if (!target.order) { - // XXX: We should be creating gaps to avoid conflicts - lastOrder = lastOrder ? nextString(lastOrder) : DEFAULT_ALPHABET[0]; - const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, target.roomId); - const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] }; - await this.client.sendStateEvent( - parentRoom.roomId, - EventType.SpaceChild, - { - ...content, - order: lastOrder, - }, - target.roomId, - ); - } else { - lastOrder = target.order; - } - } - if (lastOrder) { - newOrder = nextString(lastOrder); - } - } - - // TODO: Deal with order conflicts by reordering - - // Now we can finally update our own order state - const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, this.roomId); - const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] }; - await this.client.sendStateEvent( - parentRoom.roomId, - EventType.SpaceChild, - { - ...content, - - // TODO: Safely constrain to 50 character limit required by spaces. - order: newOrder, - }, - this.roomId, - ); - } - - /** - * Creates (uploads) a new file to this tree. The file must have already been encrypted for the room. - * The file contents are in a type that is compatible with MatrixClient.uploadContent(). - * @param name - The name of the file. - * @param encryptedContents - The encrypted contents. - * @param info - The encrypted file information. - * @param additionalContent - Optional event content fields to include in the message. - * @returns Promise which resolves to the file event's sent response. - */ - public async createFile( - name: string, - encryptedContents: FileType, - info: Partial<IEncryptedFile>, - additionalContent?: IContent, - ): Promise<ISendEventResponse> { - const { content_uri: mxc } = await this.client.uploadContent(encryptedContents, { - includeFilename: false, - }); - info.url = mxc; - - const fileContent = { - msgtype: MsgType.File, - body: name, - url: mxc, - file: info, - }; - - additionalContent = additionalContent ?? {}; - if (additionalContent["m.new_content"]) { - // We do the right thing according to the spec, but due to how relations are - // handled we also end up duplicating this information to the regular `content` - // as well. - additionalContent["m.new_content"] = fileContent; - } - - const res = await this.client.sendMessage(this.roomId, { - ...additionalContent, - ...fileContent, - [UNSTABLE_MSC3089_LEAF.name]: {}, - }); - - await this.client.sendStateEvent( - this.roomId, - UNSTABLE_MSC3089_BRANCH.name, - { - active: true, - name: name, - }, - res["event_id"], - ); - - return res; - } - - /** - * Retrieves a file from the tree. - * @param fileEventId - The event ID of the file. - * @returns The file, or null if not found. - */ - public getFile(fileEventId: string): MSC3089Branch | null { - const branch = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name, fileEventId); - return branch ? new MSC3089Branch(this.client, branch, this) : null; - } - - /** - * Gets an array of all known files for the tree. - * @returns The known files. May be empty, but not null. - */ - public listFiles(): MSC3089Branch[] { - return this.listAllFiles().filter((b) => b.isActive); - } - - /** - * Gets an array of all known files for the tree, including inactive/invalid ones. - * @returns The known files. May be empty, but not null. - */ - public listAllFiles(): MSC3089Branch[] { - const branches = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name) ?? []; - return branches.map((e) => new MSC3089Branch(this.client, e, this)); - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts deleted file mode 100644 index 8efc3ed..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* -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. -*/ - -export type ToDevicePayload = Record<string, any>; - -export interface ToDeviceMessage { - userId: string; - deviceId: string; - payload: ToDevicePayload; -} - -export interface ToDeviceBatch { - eventType: string; - batch: ToDeviceMessage[]; -} - -// Only used internally -export interface ToDeviceBatchWithTxnId extends ToDeviceBatch { - txnId: string; -} - -// Only used internally -export interface IndexedToDeviceBatch extends ToDeviceBatchWithTxnId { - id: number; -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts deleted file mode 100644 index 3801831..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts +++ /dev/null @@ -1,209 +0,0 @@ -/* -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 { MBeaconEventContent } from "../@types/beacon"; -import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers"; -import { MatrixEvent } from "./event"; -import { sortEventsByLatestContentTimestamp } from "../utils"; -import { TypedEventEmitter } from "./typed-event-emitter"; - -export enum BeaconEvent { - New = "Beacon.new", - Update = "Beacon.update", - LivenessChange = "Beacon.LivenessChange", - Destroy = "Beacon.Destroy", - LocationUpdate = "Beacon.LocationUpdate", -} - -export type BeaconEventHandlerMap = { - [BeaconEvent.Update]: (event: MatrixEvent, beacon: Beacon) => void; - [BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void; - [BeaconEvent.Destroy]: (beaconIdentifier: string) => void; - [BeaconEvent.LocationUpdate]: (locationState: BeaconLocationState) => void; - [BeaconEvent.Destroy]: (beaconIdentifier: string) => void; -}; - -export const isTimestampInDuration = (startTimestamp: number, durationMs: number, timestamp: number): boolean => - timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp; - -// beacon info events are uniquely identified by -// `<roomId>_<state_key>` -export type BeaconIdentifier = string; -export const getBeaconInfoIdentifier = (event: MatrixEvent): BeaconIdentifier => - `${event.getRoomId()}_${event.getStateKey()}`; - -// https://github.com/matrix-org/matrix-spec-proposals/pull/3672 -export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> { - public readonly roomId: string; - // beaconInfo is assigned by setBeaconInfo in the constructor - // ! to make tsc believe it is definitely assigned - private _beaconInfo!: BeaconInfoState; - private _isLive?: boolean; - private livenessWatchTimeout?: ReturnType<typeof setTimeout>; - private _latestLocationEvent?: MatrixEvent; - - public constructor(private rootEvent: MatrixEvent) { - super(); - this.roomId = this.rootEvent.getRoomId()!; - this.setBeaconInfo(this.rootEvent); - } - - public get isLive(): boolean { - return !!this._isLive; - } - - public get identifier(): BeaconIdentifier { - return getBeaconInfoIdentifier(this.rootEvent); - } - - public get beaconInfoId(): string { - return this.rootEvent.getId()!; - } - - public get beaconInfoOwner(): string { - return this.rootEvent.getStateKey()!; - } - - public get beaconInfoEventType(): string { - return this.rootEvent.getType(); - } - - public get beaconInfo(): BeaconInfoState { - return this._beaconInfo; - } - - public get latestLocationState(): BeaconLocationState | undefined { - return this._latestLocationEvent && parseBeaconContent(this._latestLocationEvent.getContent()); - } - - public get latestLocationEvent(): MatrixEvent | undefined { - return this._latestLocationEvent; - } - - public update(beaconInfoEvent: MatrixEvent): void { - if (getBeaconInfoIdentifier(beaconInfoEvent) !== this.identifier) { - throw new Error("Invalid updating event"); - } - // don't update beacon with an older event - if (beaconInfoEvent.getTs() < this.rootEvent.getTs()) { - return; - } - this.rootEvent = beaconInfoEvent; - this.setBeaconInfo(this.rootEvent); - - this.emit(BeaconEvent.Update, beaconInfoEvent, this); - this.clearLatestLocation(); - } - - public destroy(): void { - if (this.livenessWatchTimeout) { - clearTimeout(this.livenessWatchTimeout); - } - - this._isLive = false; - this.emit(BeaconEvent.Destroy, this.identifier); - } - - /** - * Monitor liveness of a beacon - * Emits BeaconEvent.LivenessChange when beacon expires - */ - public monitorLiveness(): void { - if (this.livenessWatchTimeout) { - clearTimeout(this.livenessWatchTimeout); - } - - this.checkLiveness(); - if (!this.beaconInfo) return; - if (this.isLive) { - const expiryInMs = this.beaconInfo.timestamp! + this.beaconInfo.timeout - Date.now(); - if (expiryInMs > 1) { - this.livenessWatchTimeout = setTimeout(() => { - this.monitorLiveness(); - }, expiryInMs); - } - } else if (this.beaconInfo.timestamp! > Date.now()) { - // beacon start timestamp is in the future - // check liveness again then - this.livenessWatchTimeout = setTimeout(() => { - this.monitorLiveness(); - }, this.beaconInfo.timestamp! - Date.now()); - } - } - - /** - * Process Beacon locations - * Emits BeaconEvent.LocationUpdate - */ - public addLocations(beaconLocationEvents: MatrixEvent[]): void { - // discard locations for beacons that are not live - if (!this.isLive) { - return; - } - - const validLocationEvents = beaconLocationEvents.filter((event) => { - const content = event.getContent<MBeaconEventContent>(); - const parsed = parseBeaconContent(content); - if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these - const { timestamp } = parsed; - return ( - this._beaconInfo.timestamp && - // only include positions that were taken inside the beacon's live period - isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) && - // ignore positions older than our current latest location - (!this.latestLocationState || timestamp > this.latestLocationState.timestamp!) - ); - }); - const latestLocationEvent = validLocationEvents.sort(sortEventsByLatestContentTimestamp)?.[0]; - - if (latestLocationEvent) { - this._latestLocationEvent = latestLocationEvent; - this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!); - } - } - - private clearLatestLocation = (): void => { - this._latestLocationEvent = undefined; - this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!); - }; - - private setBeaconInfo(event: MatrixEvent): void { - this._beaconInfo = parseBeaconInfoContent(event.getContent()); - this.checkLiveness(); - } - - private checkLiveness(): void { - const prevLiveness = this.isLive; - - // element web sets a beacon's start timestamp to the senders local current time - // when Alice's system clock deviates slightly from Bob's a beacon Alice intended to be live - // may have a start timestamp in the future from Bob's POV - // handle this by adding 6min of leniency to the start timestamp when it is in the future - if (!this.beaconInfo) return; - const startTimestamp = - this.beaconInfo.timestamp! > Date.now() - ? this.beaconInfo.timestamp! - 360000 /* 6min */ - : this.beaconInfo.timestamp; - this._isLive = - !!this._beaconInfo.live && - !!startTimestamp && - isTimestampInDuration(startTimestamp, this._beaconInfo.timeout, Date.now()); - - if (prevLiveness !== this.isLive) { - this.emit(BeaconEvent.LivenessChange, this.isLive, this); - } - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts deleted file mode 100644 index 0401cd5..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* -Copyright 2015 - 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 { MatrixEvent } from "./event"; -import { Direction } from "./event-timeline"; - -export class EventContext { - private timeline: MatrixEvent[]; - private ourEventIndex = 0; - private paginateTokens: Record<Direction, string | null> = { - [Direction.Backward]: null, - [Direction.Forward]: null, - }; - - /** - * Construct a new EventContext - * - * An eventcontext is used for circumstances such as search results, when we - * have a particular event of interest, and a bunch of events before and after - * it. - * - * It also stores pagination tokens for going backwards and forwards in the - * timeline. - * - * @param ourEvent - the event at the centre of this context - */ - public constructor(public readonly ourEvent: MatrixEvent) { - this.timeline = [ourEvent]; - } - - /** - * Get the main event of interest - * - * This is a convenience function for getTimeline()[getOurEventIndex()]. - * - * @returns The event at the centre of this context. - */ - public getEvent(): MatrixEvent { - return this.timeline[this.ourEventIndex]; - } - - /** - * Get the list of events in this context - * - * @returns An array of MatrixEvents - */ - public getTimeline(): MatrixEvent[] { - return this.timeline; - } - - /** - * Get the index in the timeline of our event - */ - public getOurEventIndex(): number { - return this.ourEventIndex; - } - - /** - * Get a pagination token. - * - * @param backwards - true to get the pagination token for going - */ - public getPaginateToken(backwards = false): string | null { - return this.paginateTokens[backwards ? Direction.Backward : Direction.Forward]; - } - - /** - * Set a pagination token. - * - * Generally this will be used only by the matrix js sdk. - * - * @param token - pagination token - * @param backwards - true to set the pagination token for going - * backwards in time - */ - public setPaginateToken(token?: string, backwards = false): void { - this.paginateTokens[backwards ? Direction.Backward : Direction.Forward] = token ?? null; - } - - /** - * Add more events to the timeline - * - * @param events - new events, in timeline order - * @param atStart - true to insert new events at the start - */ - public addEvents(events: MatrixEvent[], atStart = false): void { - // TODO: should we share logic with Room.addEventsToTimeline? - // Should Room even use EventContext? - - if (atStart) { - this.timeline = events.concat(this.timeline); - this.ourEventIndex += events.length; - } else { - this.timeline = this.timeline.concat(events); - } - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts deleted file mode 100644 index a5113e0..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2015 - 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. -*/ - -/** - * Enum for event statuses. - * @readonly - */ -export enum EventStatus { - /** The event was not sent and will no longer be retried. */ - NOT_SENT = "not_sent", - - /** The message is being encrypted */ - ENCRYPTING = "encrypting", - - /** The event is in the process of being sent. */ - SENDING = "sending", - - /** The event is in a queue waiting to be sent. */ - QUEUED = "queued", - - /** The event has been sent to the server, but we have not yet received the echo. */ - SENT = "sent", - - /** The event was cancelled before it was successfully sent. */ - CANCELLED = "cancelled", -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts deleted file mode 100644 index 5cb0499..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts +++ /dev/null @@ -1,906 +0,0 @@ -/* -Copyright 2016 - 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 { EventTimeline, IAddEventOptions } from "./event-timeline"; -import { MatrixEvent } from "./event"; -import { logger } from "../logger"; -import { Room, RoomEvent } from "./room"; -import { Filter } from "../filter"; -import { RoomState } from "./room-state"; -import { TypedEventEmitter } from "./typed-event-emitter"; -import { RelationsContainer } from "./relations-container"; -import { MatrixClient } from "../client"; -import { Thread, ThreadFilterType } from "./thread"; - -const DEBUG = true; - -/* istanbul ignore next */ -let debuglog: (...args: any[]) => void; -if (DEBUG) { - // using bind means that we get to keep useful line numbers in the console - debuglog = logger.log.bind(logger); -} else { - /* istanbul ignore next */ - debuglog = function (): void {}; -} - -interface IOpts { - // Set to true to enable improved timeline support. - timelineSupport?: boolean; - // The filter object, if any, for this timelineSet. - filter?: Filter; - pendingEvents?: boolean; -} - -export enum DuplicateStrategy { - Ignore = "ignore", - Replace = "replace", -} - -export interface IRoomTimelineData { - // the timeline the event was added to/removed from - timeline: EventTimeline; - // true if the event was a real-time event added to the end of the live timeline - liveEvent?: boolean; -} - -export interface IAddEventToTimelineOptions - extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty"> { - /** Whether the sync response came from cache */ - fromCache?: boolean; -} - -export interface IAddLiveEventOptions - extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty"> { - /** Applies to events in the timeline only. If this is 'replace' then if a - * duplicate is encountered, the event passed to this function will replace - * the existing event in the timeline. If this is not specified, or is - * 'ignore', then the event passed to this function will be ignored - * entirely, preserving the existing event in the timeline. Events are - * identical based on their event ID <b>only</b>. */ - duplicateStrategy?: DuplicateStrategy; -} - -type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset; - -export type EventTimelineSetHandlerMap = { - /** - * Fires whenever the timeline in a room is updated. - * @param event - The matrix event which caused this event to fire. - * @param room - The room, if any, whose timeline was updated. - * @param toStartOfTimeline - True if this event was added to the start - * @param removed - True if this event has just been removed from the timeline - * (beginning; oldest) of the timeline e.g. due to pagination. - * - * @param data - more data about the event - * - * @example - * ``` - * matrixClient.on("Room.timeline", - * function(event, room, toStartOfTimeline, removed, data) { - * if (!toStartOfTimeline && data.liveEvent) { - * var messageToAppend = room.timeline.[room.timeline.length - 1]; - * } - * }); - * ``` - */ - [RoomEvent.Timeline]: ( - event: MatrixEvent, - room: Room | undefined, - toStartOfTimeline: boolean | undefined, - removed: boolean, - data: IRoomTimelineData, - ) => void; - /** - * Fires whenever the live timeline in a room is reset. - * - * When we get a 'limited' sync (for example, after a network outage), we reset - * the live timeline to be empty before adding the recent events to the new - * timeline. This event is fired after the timeline is reset, and before the - * new events are added. - * - * @param room - The room whose live timeline was reset, if any - * @param timelineSet - timelineSet room whose live timeline was reset - * @param resetAllTimelines - True if all timelines were reset. - */ - [RoomEvent.TimelineReset]: ( - room: Room | undefined, - eventTimelineSet: EventTimelineSet, - resetAllTimelines: boolean, - ) => void; -}; - -export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTimelineSetHandlerMap> { - public readonly relations: RelationsContainer; - private readonly timelineSupport: boolean; - private readonly displayPendingEvents: boolean; - private liveTimeline: EventTimeline; - private timelines: EventTimeline[]; - private _eventIdToTimeline = new Map<string, EventTimeline>(); - private filter?: Filter; - - /** - * Construct a set of EventTimeline objects, typically on behalf of a given - * room. A room may have multiple EventTimelineSets for different levels - * of filtering. The global notification list is also an EventTimelineSet, but - * lacks a room. - * - * <p>This is an ordered sequence of timelines, which may or may not - * be continuous. Each timeline lists a series of events, as well as tracking - * the room state at the start and the end of the timeline (if appropriate). - * It also tracks forward and backward pagination tokens, as well as containing - * links to the next timeline in the sequence. - * - * <p>There is one special timeline - the 'live' timeline, which represents the - * timeline to which events are being added in real-time as they are received - * from the /sync API. Note that you should not retain references to this - * timeline - even if it is the current timeline right now, it may not remain - * so if the server gives us a timeline gap in /sync. - * - * <p>In order that we can find events from their ids later, we also maintain a - * map from event_id to timeline and index. - * - * @param room - Room for this timelineSet. May be null for non-room cases, such as the - * notification timeline. - * @param opts - Options inherited from Room. - * @param client - the Matrix client which owns this EventTimelineSet, - * can be omitted if room is specified. - * @param thread - the thread to which this timeline set relates. - * @param isThreadTimeline - Whether this timeline set relates to a thread list timeline - * (e.g., All threads or My threads) - */ - public constructor( - public readonly room: Room | undefined, - opts: IOpts = {}, - client?: MatrixClient, - public readonly thread?: Thread, - public readonly threadListType: ThreadFilterType | null = null, - ) { - super(); - - this.timelineSupport = Boolean(opts.timelineSupport); - this.liveTimeline = new EventTimeline(this); - this.displayPendingEvents = opts.pendingEvents !== false; - - // just a list - *not* ordered. - this.timelines = [this.liveTimeline]; - this._eventIdToTimeline = new Map<string, EventTimeline>(); - - this.filter = opts.filter; - - this.relations = this.room?.relations ?? new RelationsContainer(room?.client ?? client!); - } - - /** - * Get all the timelines in this set - * @returns the timelines in this set - */ - public getTimelines(): EventTimeline[] { - return this.timelines; - } - - /** - * Get the filter object this timeline set is filtered on, if any - * @returns the optional filter for this timelineSet - */ - public getFilter(): Filter | undefined { - return this.filter; - } - - /** - * Set the filter object this timeline set is filtered on - * (passed to the server when paginating via /messages). - * @param filter - the filter for this timelineSet - */ - public setFilter(filter?: Filter): void { - this.filter = filter; - } - - /** - * Get the list of pending sent events for this timelineSet's room, filtered - * by the timelineSet's filter if appropriate. - * - * @returns A list of the sent events - * waiting for remote echo. - * - * @throws If `opts.pendingEventOrdering` was not 'detached' - */ - public getPendingEvents(): MatrixEvent[] { - if (!this.room || !this.displayPendingEvents) { - return []; - } - - return this.room.getPendingEvents(); - } - /** - * Get the live timeline for this room. - * - * @returns live timeline - */ - public getLiveTimeline(): EventTimeline { - return this.liveTimeline; - } - - /** - * Set the live timeline for this room. - * - * @returns live timeline - */ - public setLiveTimeline(timeline: EventTimeline): void { - this.liveTimeline = timeline; - } - - /** - * Return the timeline (if any) this event is in. - * @param eventId - the eventId being sought - * @returns timeline - */ - public eventIdToTimeline(eventId: string): EventTimeline | undefined { - return this._eventIdToTimeline.get(eventId); - } - - /** - * Track a new event as if it were in the same timeline as an old event, - * replacing it. - * @param oldEventId - event ID of the original event - * @param newEventId - event ID of the replacement event - */ - public replaceEventId(oldEventId: string, newEventId: string): void { - const existingTimeline = this._eventIdToTimeline.get(oldEventId); - if (existingTimeline) { - this._eventIdToTimeline.delete(oldEventId); - this._eventIdToTimeline.set(newEventId, existingTimeline); - } - } - - /** - * Reset the live timeline, and start a new one. - * - * <p>This is used when /sync returns a 'limited' timeline. - * - * @param backPaginationToken - token for back-paginating the new timeline - * @param forwardPaginationToken - token for forward-paginating the old live timeline, - * if absent or null, all timelines are reset. - * - * @remarks - * Fires {@link RoomEvent.TimelineReset} - */ - public resetLiveTimeline(backPaginationToken?: string, forwardPaginationToken?: string): void { - // Each EventTimeline has RoomState objects tracking the state at the start - // and end of that timeline. The copies at the end of the live timeline are - // special because they will have listeners attached to monitor changes to - // the current room state, so we move this RoomState from the end of the - // current live timeline to the end of the new one and, if necessary, - // replace it with a newly created one. We also make a copy for the start - // of the new timeline. - - // if timeline support is disabled, forget about the old timelines - const resetAllTimelines = !this.timelineSupport || !forwardPaginationToken; - - const oldTimeline = this.liveTimeline; - const newTimeline = resetAllTimelines - ? oldTimeline.forkLive(EventTimeline.FORWARDS) - : oldTimeline.fork(EventTimeline.FORWARDS); - - if (resetAllTimelines) { - this.timelines = [newTimeline]; - this._eventIdToTimeline = new Map<string, EventTimeline>(); - } else { - this.timelines.push(newTimeline); - } - - if (forwardPaginationToken) { - // Now set the forward pagination token on the old live timeline - // so it can be forward-paginated. - oldTimeline.setPaginationToken(forwardPaginationToken, EventTimeline.FORWARDS); - } - - // make sure we set the pagination token before firing timelineReset, - // otherwise clients which start back-paginating will fail, and then get - // stuck without realising that they *can* back-paginate. - newTimeline.setPaginationToken(backPaginationToken ?? null, EventTimeline.BACKWARDS); - - // Now we can swap the live timeline to the new one. - this.liveTimeline = newTimeline; - this.emit(RoomEvent.TimelineReset, this.room, this, resetAllTimelines); - } - - /** - * Get the timeline which contains the given event, if any - * - * @param eventId - event ID to look for - * @returns timeline containing - * the given event, or null if unknown - */ - public getTimelineForEvent(eventId?: string): EventTimeline | null { - if (eventId === null || eventId === undefined) { - return null; - } - const res = this._eventIdToTimeline.get(eventId); - return res === undefined ? null : res; - } - - /** - * Get an event which is stored in our timelines - * - * @param eventId - event ID to look for - * @returns the given event, or undefined if unknown - */ - public findEventById(eventId: string): MatrixEvent | undefined { - const tl = this.getTimelineForEvent(eventId); - if (!tl) { - return undefined; - } - return tl.getEvents().find(function (ev) { - return ev.getId() == eventId; - }); - } - - /** - * Add a new timeline to this timeline list - * - * @returns newly-created timeline - */ - public addTimeline(): EventTimeline { - if (!this.timelineSupport) { - throw new Error( - "timeline support is disabled. Set the 'timelineSupport'" + - " parameter to true when creating MatrixClient to enable" + - " it.", - ); - } - - const timeline = new EventTimeline(this); - this.timelines.push(timeline); - return timeline; - } - - /** - * Add events to a timeline - * - * <p>Will fire "Room.timeline" for each event added. - * - * @param events - A list of events to add. - * - * @param toStartOfTimeline - True to add these events to the start - * (oldest) instead of the end (newest) of the timeline. If true, the oldest - * event will be the <b>last</b> element of 'events'. - * - * @param timeline - timeline to - * add events to. - * - * @param paginationToken - token for the next batch of events - * - * @remarks - * Fires {@link RoomEvent.Timeline} - * - */ - public addEventsToTimeline( - events: MatrixEvent[], - toStartOfTimeline: boolean, - timeline: EventTimeline, - paginationToken?: string | null, - ): void { - if (!timeline) { - throw new Error("'timeline' not specified for EventTimelineSet.addEventsToTimeline"); - } - - if (!toStartOfTimeline && timeline == this.liveTimeline) { - throw new Error( - "EventTimelineSet.addEventsToTimeline cannot be used for adding events to " + - "the live timeline - use Room.addLiveEvents instead", - ); - } - - if (this.filter) { - events = this.filter.filterRoomTimeline(events); - if (!events.length) { - return; - } - } - - const direction = toStartOfTimeline ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; - const inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS; - - // Adding events to timelines can be quite complicated. The following - // illustrates some of the corner-cases. - // - // Let's say we start by knowing about four timelines. timeline3 and - // timeline4 are neighbours: - // - // timeline1 timeline2 timeline3 timeline4 - // [M] [P] [S] <------> [T] - // - // Now we paginate timeline1, and get the following events from the server: - // [M, N, P, R, S, T, U]. - // - // 1. First, we ignore event M, since we already know about it. - // - // 2. Next, we append N to timeline 1. - // - // 3. Next, we don't add event P, since we already know about it, - // but we do link together the timelines. We now have: - // - // timeline1 timeline2 timeline3 timeline4 - // [M, N] <---> [P] [S] <------> [T] - // - // 4. Now we add event R to timeline2: - // - // timeline1 timeline2 timeline3 timeline4 - // [M, N] <---> [P, R] [S] <------> [T] - // - // Note that we have switched the timeline we are working on from - // timeline1 to timeline2. - // - // 5. We ignore event S, but again join the timelines: - // - // timeline1 timeline2 timeline3 timeline4 - // [M, N] <---> [P, R] <---> [S] <------> [T] - // - // 6. We ignore event T, and the timelines are already joined, so there - // is nothing to do. - // - // 7. Finally, we add event U to timeline4: - // - // timeline1 timeline2 timeline3 timeline4 - // [M, N] <---> [P, R] <---> [S] <------> [T, U] - // - // The important thing to note in the above is what happened when we - // already knew about a given event: - // - // - if it was appropriate, we joined up the timelines (steps 3, 5). - // - in any case, we started adding further events to the timeline which - // contained the event we knew about (steps 3, 5, 6). - // - // - // So much for adding events to the timeline. But what do we want to do - // with the pagination token? - // - // In the case above, we will be given a pagination token which tells us how to - // get events beyond 'U' - in this case, it makes sense to store this - // against timeline4. But what if timeline4 already had 'U' and beyond? in - // that case, our best bet is to throw away the pagination token we were - // given and stick with whatever token timeline4 had previously. In short, - // we want to only store the pagination token if the last event we receive - // is one we didn't previously know about. - // - // We make an exception for this if it turns out that we already knew about - // *all* of the events, and we weren't able to join up any timelines. When - // that happens, it means our existing pagination token is faulty, since it - // is only telling us what we already know. Rather than repeatedly - // paginating with the same token, we might as well use the new pagination - // token in the hope that we eventually work our way out of the mess. - - let didUpdate = false; - let lastEventWasNew = false; - for (const event of events) { - const eventId = event.getId()!; - - const existingTimeline = this._eventIdToTimeline.get(eventId); - - if (!existingTimeline) { - // we don't know about this event yet. Just add it to the timeline. - this.addEventToTimeline(event, timeline, { - toStartOfTimeline, - }); - lastEventWasNew = true; - didUpdate = true; - continue; - } - - lastEventWasNew = false; - - if (existingTimeline == timeline) { - debuglog("Event " + eventId + " already in timeline " + timeline); - continue; - } - - const neighbour = timeline.getNeighbouringTimeline(direction); - if (neighbour) { - // this timeline already has a neighbour in the relevant direction; - // let's assume the timelines are already correctly linked up, and - // skip over to it. - // - // there's probably some edge-case here where we end up with an - // event which is in a timeline a way down the chain, and there is - // a break in the chain somewhere. But I can't really imagine how - // that would happen, so I'm going to ignore it for now. - // - if (existingTimeline == neighbour) { - debuglog("Event " + eventId + " in neighbouring timeline - " + "switching to " + existingTimeline); - } else { - debuglog("Event " + eventId + " already in a different " + "timeline " + existingTimeline); - } - timeline = existingTimeline; - continue; - } - - // time to join the timelines. - logger.info( - "Already have timeline for " + eventId + " - joining timeline " + timeline + " to " + existingTimeline, - ); - - // Variables to keep the line length limited below. - const existingIsLive = existingTimeline === this.liveTimeline; - const timelineIsLive = timeline === this.liveTimeline; - - const backwardsIsLive = direction === EventTimeline.BACKWARDS && existingIsLive; - const forwardsIsLive = direction === EventTimeline.FORWARDS && timelineIsLive; - - if (backwardsIsLive || forwardsIsLive) { - // The live timeline should never be spliced into a non-live position. - // We use independent logging to better discover the problem at a glance. - if (backwardsIsLive) { - logger.warn( - "Refusing to set a preceding existingTimeLine on our " + - "timeline as the existingTimeLine is live (" + - existingTimeline + - ")", - ); - } - if (forwardsIsLive) { - logger.warn( - "Refusing to set our preceding timeline on a existingTimeLine " + - "as our timeline is live (" + - timeline + - ")", - ); - } - continue; // abort splicing - try next event - } - - timeline.setNeighbouringTimeline(existingTimeline, direction); - existingTimeline.setNeighbouringTimeline(timeline, inverseDirection); - - timeline = existingTimeline; - didUpdate = true; - } - - // see above - if the last event was new to us, or if we didn't find any - // new information, we update the pagination token for whatever - // timeline we ended up on. - if (lastEventWasNew || !didUpdate) { - if (direction === EventTimeline.FORWARDS && timeline === this.liveTimeline) { - logger.warn({ lastEventWasNew, didUpdate }); // for debugging - logger.warn( - `Refusing to set forwards pagination token of live timeline ` + `${timeline} to ${paginationToken}`, - ); - return; - } - timeline.setPaginationToken(paginationToken ?? null, direction); - } - } - - /** - * Add an event to the end of this live timeline. - * - * @param event - Event to be added - * @param options - addLiveEvent options - */ - public addLiveEvent( - event: MatrixEvent, - { duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions, - ): void; - /** - * @deprecated In favor of the overload with `IAddLiveEventOptions` - */ - public addLiveEvent( - event: MatrixEvent, - duplicateStrategy?: DuplicateStrategy, - fromCache?: boolean, - roomState?: RoomState, - ): void; - public addLiveEvent( - event: MatrixEvent, - duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions, - fromCache = false, - roomState?: RoomState, - ): void { - let duplicateStrategy = (duplicateStrategyOrOpts as DuplicateStrategy) || DuplicateStrategy.Ignore; - let timelineWasEmpty: boolean | undefined; - if (typeof duplicateStrategyOrOpts === "object") { - ({ - duplicateStrategy = DuplicateStrategy.Ignore, - fromCache = false, - roomState, - timelineWasEmpty, - } = duplicateStrategyOrOpts); - } else if (duplicateStrategyOrOpts !== undefined) { - // Deprecation warning - // FIXME: Remove after 2023-06-01 (technical debt) - logger.warn( - "Overload deprecated: " + - "`EventTimelineSet.addLiveEvent(event, duplicateStrategy?, fromCache?, roomState?)` " + - "is deprecated in favor of the overload with " + - "`EventTimelineSet.addLiveEvent(event, IAddLiveEventOptions)`", - ); - } - - if (this.filter) { - const events = this.filter.filterRoomTimeline([event]); - if (!events.length) { - return; - } - } - - const timeline = this._eventIdToTimeline.get(event.getId()!); - if (timeline) { - if (duplicateStrategy === DuplicateStrategy.Replace) { - debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId()); - const tlEvents = timeline.getEvents(); - for (let j = 0; j < tlEvents.length; j++) { - if (tlEvents[j].getId() === event.getId()) { - // still need to set the right metadata on this event - if (!roomState) { - roomState = timeline.getState(EventTimeline.FORWARDS); - } - EventTimeline.setEventMetadata(event, roomState!, false); - tlEvents[j] = event; - - // XXX: we need to fire an event when this happens. - break; - } - } - } else { - debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " + event.getId()); - } - return; - } - - this.addEventToTimeline(event, this.liveTimeline, { - toStartOfTimeline: false, - fromCache, - roomState, - timelineWasEmpty, - }); - } - - /** - * Add event to the given timeline, and emit Room.timeline. Assumes - * we have already checked we don't know about this event. - * - * Will fire "Room.timeline" for each event added. - * - * @param options - addEventToTimeline options - * - * @remarks - * Fires {@link RoomEvent.Timeline} - */ - public addEventToTimeline( - event: MatrixEvent, - timeline: EventTimeline, - { toStartOfTimeline, fromCache, roomState, timelineWasEmpty }: IAddEventToTimelineOptions, - ): void; - /** - * @deprecated In favor of the overload with `IAddEventToTimelineOptions` - */ - public addEventToTimeline( - event: MatrixEvent, - timeline: EventTimeline, - toStartOfTimeline: boolean, - fromCache?: boolean, - roomState?: RoomState, - ): void; - public addEventToTimeline( - event: MatrixEvent, - timeline: EventTimeline, - toStartOfTimelineOrOpts: boolean | IAddEventToTimelineOptions, - fromCache = false, - roomState?: RoomState, - ): void { - let toStartOfTimeline = !!toStartOfTimelineOrOpts; - let timelineWasEmpty: boolean | undefined; - if (typeof toStartOfTimelineOrOpts === "object") { - ({ toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts); - } else if (toStartOfTimelineOrOpts !== undefined) { - // Deprecation warning - // FIXME: Remove after 2023-06-01 (technical debt) - logger.warn( - "Overload deprecated: " + - "`EventTimelineSet.addEventToTimeline(event, timeline, toStartOfTimeline, fromCache?, roomState?)` " + - "is deprecated in favor of the overload with " + - "`EventTimelineSet.addEventToTimeline(event, timeline, IAddEventToTimelineOptions)`", - ); - } - - if (timeline.getTimelineSet() !== this) { - throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " + - "in timelineSet(threadId=${this.thread?.id})`); - } - - // Make sure events don't get mixed in timelines they shouldn't be in (e.g. a - // threaded message should not be in the main timeline). - // - // We can only run this check for timelines with a `room` because `canContain` - // requires it - if (this.room && !this.canContain(event)) { - let eventDebugString = `event=${event.getId()}`; - if (event.threadRootId) { - eventDebugString += `(belongs to thread=${event.threadRootId})`; - } - logger.warn( - `EventTimelineSet.addEventToTimeline: Ignoring ${eventDebugString} that does not belong ` + - `in timeline=${timeline.toString()} timelineSet(threadId=${this.thread?.id})`, - ); - return; - } - - const eventId = event.getId()!; - timeline.addEvent(event, { - toStartOfTimeline, - roomState, - timelineWasEmpty, - }); - this._eventIdToTimeline.set(eventId, timeline); - - this.relations.aggregateParentEvent(event); - this.relations.aggregateChildEvent(event, this); - - const data: IRoomTimelineData = { - timeline: timeline, - liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache, - }; - this.emit(RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data); - } - - /** - * Replaces event with ID oldEventId with one with newEventId, if oldEventId is - * recognised. Otherwise, add to the live timeline. Used to handle remote echos. - * - * @param localEvent - the new event to be added to the timeline - * @param oldEventId - the ID of the original event - * @param newEventId - the ID of the replacement event - * - * @remarks - * Fires {@link RoomEvent.Timeline} - */ - public handleRemoteEcho(localEvent: MatrixEvent, oldEventId: string, newEventId: string): void { - // XXX: why don't we infer newEventId from localEvent? - const existingTimeline = this._eventIdToTimeline.get(oldEventId); - if (existingTimeline) { - this._eventIdToTimeline.delete(oldEventId); - this._eventIdToTimeline.set(newEventId, existingTimeline); - } else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) { - this.addEventToTimeline(localEvent, this.liveTimeline, { - toStartOfTimeline: false, - }); - } - } - - /** - * Removes a single event from this room. - * - * @param eventId - The id of the event to remove - * - * @returns the removed event, or null if the event was not found - * in this room. - */ - public removeEvent(eventId: string): MatrixEvent | null { - const timeline = this._eventIdToTimeline.get(eventId); - if (!timeline) { - return null; - } - - const removed = timeline.removeEvent(eventId); - if (removed) { - this._eventIdToTimeline.delete(eventId); - const data = { - timeline: timeline, - }; - this.emit(RoomEvent.Timeline, removed, this.room, undefined, true, data); - } - return removed; - } - - /** - * Determine where two events appear in the timeline relative to one another - * - * @param eventId1 - The id of the first event - * @param eventId2 - The id of the second event - - * @returns a number less than zero if eventId1 precedes eventId2, and - * greater than zero if eventId1 succeeds eventId2. zero if they are the - * same event; null if we can't tell (either because we don't know about one - * of the events, or because they are in separate timelines which don't join - * up). - */ - public compareEventOrdering(eventId1: string, eventId2: string): number | null { - if (eventId1 == eventId2) { - // optimise this case - return 0; - } - - const timeline1 = this._eventIdToTimeline.get(eventId1); - const timeline2 = this._eventIdToTimeline.get(eventId2); - - if (timeline1 === undefined) { - return null; - } - if (timeline2 === undefined) { - return null; - } - - if (timeline1 === timeline2) { - // both events are in the same timeline - figure out their relative indices - let idx1: number | undefined = undefined; - let idx2: number | undefined = undefined; - const events = timeline1.getEvents(); - for (let idx = 0; idx < events.length && (idx1 === undefined || idx2 === undefined); idx++) { - const evId = events[idx].getId(); - if (evId == eventId1) { - idx1 = idx; - } - if (evId == eventId2) { - idx2 = idx; - } - } - return idx1! - idx2!; - } - - // the events are in different timelines. Iterate through the - // linkedlist to see which comes first. - - // first work forwards from timeline1 - let tl: EventTimeline | null = timeline1; - while (tl) { - if (tl === timeline2) { - // timeline1 is before timeline2 - return -1; - } - tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS); - } - - // now try backwards from timeline1 - tl = timeline1; - while (tl) { - if (tl === timeline2) { - // timeline2 is before timeline1 - return 1; - } - tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS); - } - - // the timelines are not contiguous. - return null; - } - - /** - * Determine whether a given event can sanely be added to this event timeline set, - * for timeline sets relating to a thread, only return true for events in the same - * thread timeline, for timeline sets not relating to a thread only return true - * for events which should be shown in the main room timeline. - * Requires the `room` property to have been set at EventTimelineSet construction time. - * - * @param event - the event to check whether it belongs to this timeline set. - * @throws Error if `room` was not set when constructing this timeline set. - * @returns whether the event belongs to this timeline set. - */ - public canContain(event: MatrixEvent): boolean { - if (!this.room) { - throw new Error( - "Cannot call `EventTimelineSet::canContain without a `room` set. " + - "Set the room when creating the EventTimelineSet to call this method.", - ); - } - - const { threadId, shouldLiveInRoom } = this.room.eventShouldLiveIn(event); - - if (this.thread) { - return this.thread.id === threadId; - } - return shouldLiveInRoom; - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts deleted file mode 100644 index d1ba321..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts +++ /dev/null @@ -1,458 +0,0 @@ -/* -Copyright 2016 - 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 { logger } from "../logger"; -import { IMarkerFoundOptions, RoomState } from "./room-state"; -import { EventTimelineSet } from "./event-timeline-set"; -import { MatrixEvent } from "./event"; -import { Filter } from "../filter"; -import { EventType } from "../@types/event"; - -export interface IInitialiseStateOptions extends Pick<IMarkerFoundOptions, "timelineWasEmpty"> { - // This is a separate interface without any extra stuff currently added on - // top of `IMarkerFoundOptions` just because it feels like they have - // different concerns. One shouldn't necessarily look to add to - // `IMarkerFoundOptions` just because they want to add an extra option to - // `initialiseState`. -} - -export interface IAddEventOptions extends Pick<IMarkerFoundOptions, "timelineWasEmpty"> { - /** Whether to insert the new event at the start of the timeline where the - * oldest events are (timeline is in chronological order, oldest to most - * recent) */ - toStartOfTimeline: boolean; - /** The state events to reconcile metadata from */ - roomState?: RoomState; -} - -export enum Direction { - Backward = "b", - Forward = "f", -} - -export class EventTimeline { - /** - * Symbolic constant for methods which take a 'direction' argument: - * refers to the start of the timeline, or backwards in time. - */ - public static readonly BACKWARDS = Direction.Backward; - - /** - * Symbolic constant for methods which take a 'direction' argument: - * refers to the end of the timeline, or forwards in time. - */ - public static readonly FORWARDS = Direction.Forward; - - /** - * Static helper method to set sender and target properties - * - * @param event - the event whose metadata is to be set - * @param stateContext - the room state to be queried - * @param toStartOfTimeline - if true the event's forwardLooking flag is set false - */ - public static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void { - // When we try to generate a sentinel member before we have that member - // in the members object, we still generate a sentinel but it doesn't - // have a membership event, so test to see if events.member is set. We - // check this to avoid overriding non-sentinel members by sentinel ones - // when adding the event to a filtered timeline - if (!event.sender?.events?.member) { - event.sender = stateContext.getSentinelMember(event.getSender()!); - } - if (!event.target?.events?.member && event.getType() === EventType.RoomMember) { - event.target = stateContext.getSentinelMember(event.getStateKey()!); - } - - if (event.isState()) { - // room state has no concept of 'old' or 'current', but we want the - // room state to regress back to previous values if toStartOfTimeline - // is set, which means inspecting prev_content if it exists. This - // is done by toggling the forwardLooking flag. - if (toStartOfTimeline) { - event.forwardLooking = false; - } - } - } - - private readonly roomId: string | null; - private readonly name: string; - private events: MatrixEvent[] = []; - private baseIndex = 0; - - private startState?: RoomState; - private endState?: RoomState; - // If we have a roomId then we delegate pagination token storage to the room state objects `startState` and - // `endState`, but for things like the notification timeline which mix multiple rooms we store the tokens ourselves. - private startToken: string | null = null; - private endToken: string | null = null; - - private prevTimeline: EventTimeline | null = null; - private nextTimeline: EventTimeline | null = null; - public paginationRequests: Record<Direction, Promise<boolean> | null> = { - [Direction.Backward]: null, - [Direction.Forward]: null, - }; - - /** - * Construct a new EventTimeline - * - * <p>An EventTimeline represents a contiguous sequence of events in a room. - * - * <p>As well as keeping track of the events themselves, it stores the state of - * the room at the beginning and end of the timeline, and pagination tokens for - * going backwards and forwards in the timeline. - * - * <p>In order that clients can meaningfully maintain an index into a timeline, - * the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is - * incremented when events are prepended to the timeline. The index of an event - * relative to baseIndex therefore remains constant. - * - * <p>Once a timeline joins up with its neighbour, they are linked together into a - * doubly-linked list. - * - * @param eventTimelineSet - the set of timelines this is part of - */ - public constructor(private readonly eventTimelineSet: EventTimelineSet) { - this.roomId = eventTimelineSet.room?.roomId ?? null; - if (this.roomId) { - this.startState = new RoomState(this.roomId); - this.endState = new RoomState(this.roomId); - } - - // this is used by client.js - this.paginationRequests = { b: null, f: null }; - - this.name = this.roomId + ":" + new Date().toISOString(); - } - - /** - * Initialise the start and end state with the given events - * - * <p>This can only be called before any events are added. - * - * @param stateEvents - list of state events to initialise the - * state with. - * @throws Error if an attempt is made to call this after addEvent is called. - */ - public initialiseState(stateEvents: MatrixEvent[], { timelineWasEmpty }: IInitialiseStateOptions = {}): void { - if (this.events.length > 0) { - throw new Error("Cannot initialise state after events are added"); - } - - this.startState?.setStateEvents(stateEvents, { timelineWasEmpty }); - this.endState?.setStateEvents(stateEvents, { timelineWasEmpty }); - } - - /** - * Forks the (live) timeline, taking ownership of the existing directional state of this timeline. - * All attached listeners will keep receiving state updates from the new live timeline state. - * The end state of this timeline gets replaced with an independent copy of the current RoomState, - * and will need a new pagination token if it ever needs to paginate forwards. - - * @param direction - EventTimeline.BACKWARDS to get the state at the - * start of the timeline; EventTimeline.FORWARDS to get the state at the end - * of the timeline. - * - * @returns the new timeline - */ - public forkLive(direction: Direction): EventTimeline { - const forkState = this.getState(direction); - const timeline = new EventTimeline(this.eventTimelineSet); - timeline.startState = forkState?.clone(); - // Now clobber the end state of the new live timeline with that from the - // previous live timeline. It will be identical except that we'll keep - // using the same RoomMember objects for the 'live' set of members with any - // listeners still attached - timeline.endState = forkState; - // Firstly, we just stole the current timeline's end state, so it needs a new one. - // Make an immutable copy of the state so back pagination will get the correct sentinels. - this.endState = forkState?.clone(); - return timeline; - } - - /** - * Creates an independent timeline, inheriting the directional state from this timeline. - * - * @param direction - EventTimeline.BACKWARDS to get the state at the - * start of the timeline; EventTimeline.FORWARDS to get the state at the end - * of the timeline. - * - * @returns the new timeline - */ - public fork(direction: Direction): EventTimeline { - const forkState = this.getState(direction); - const timeline = new EventTimeline(this.eventTimelineSet); - timeline.startState = forkState?.clone(); - timeline.endState = forkState?.clone(); - return timeline; - } - - /** - * Get the ID of the room for this timeline - * @returns room ID - */ - public getRoomId(): string | null { - return this.roomId; - } - - /** - * Get the filter for this timeline's timelineSet (if any) - * @returns filter - */ - public getFilter(): Filter | undefined { - return this.eventTimelineSet.getFilter(); - } - - /** - * Get the timelineSet for this timeline - * @returns timelineSet - */ - public getTimelineSet(): EventTimelineSet { - return this.eventTimelineSet; - } - - /** - * Get the base index. - * - * <p>This is an index which is incremented when events are prepended to the - * timeline. An individual event therefore stays at the same index in the array - * relative to the base index (although note that a given event's index may - * well be less than the base index, thus giving that event a negative relative - * index). - */ - public getBaseIndex(): number { - return this.baseIndex; - } - - /** - * Get the list of events in this context - * - * @returns An array of MatrixEvents - */ - public getEvents(): MatrixEvent[] { - return this.events; - } - - /** - * Get the room state at the start/end of the timeline - * - * @param direction - EventTimeline.BACKWARDS to get the state at the - * start of the timeline; EventTimeline.FORWARDS to get the state at the end - * of the timeline. - * - * @returns state at the start/end of the timeline - */ - public getState(direction: Direction): RoomState | undefined { - if (direction == EventTimeline.BACKWARDS) { - return this.startState; - } else if (direction == EventTimeline.FORWARDS) { - return this.endState; - } else { - throw new Error("Invalid direction '" + direction + "'"); - } - } - - /** - * Get a pagination token - * - * @param direction - EventTimeline.BACKWARDS to get the pagination - * token for going backwards in time; EventTimeline.FORWARDS to get the - * pagination token for going forwards in time. - * - * @returns pagination token - */ - public getPaginationToken(direction: Direction): string | null { - if (this.roomId) { - return this.getState(direction)!.paginationToken; - } else if (direction === Direction.Backward) { - return this.startToken; - } else { - return this.endToken; - } - } - - /** - * Set a pagination token - * - * @param token - pagination token - * - * @param direction - EventTimeline.BACKWARDS to set the pagination - * token for going backwards in time; EventTimeline.FORWARDS to set the - * pagination token for going forwards in time. - */ - public setPaginationToken(token: string | null, direction: Direction): void { - if (this.roomId) { - this.getState(direction)!.paginationToken = token; - } else if (direction === Direction.Backward) { - this.startToken = token; - } else { - this.endToken = token; - } - } - - /** - * Get the next timeline in the series - * - * @param direction - EventTimeline.BACKWARDS to get the previous - * timeline; EventTimeline.FORWARDS to get the next timeline. - * - * @returns previous or following timeline, if they have been - * joined up. - */ - public getNeighbouringTimeline(direction: Direction): EventTimeline | null { - if (direction == EventTimeline.BACKWARDS) { - return this.prevTimeline; - } else if (direction == EventTimeline.FORWARDS) { - return this.nextTimeline; - } else { - throw new Error("Invalid direction '" + direction + "'"); - } - } - - /** - * Set the next timeline in the series - * - * @param neighbour - previous/following timeline - * - * @param direction - EventTimeline.BACKWARDS to set the previous - * timeline; EventTimeline.FORWARDS to set the next timeline. - * - * @throws Error if an attempt is made to set the neighbouring timeline when - * it is already set. - */ - public setNeighbouringTimeline(neighbour: EventTimeline, direction: Direction): void { - if (this.getNeighbouringTimeline(direction)) { - throw new Error( - "timeline already has a neighbouring timeline - " + - "cannot reset neighbour (direction: " + - direction + - ")", - ); - } - - if (direction == EventTimeline.BACKWARDS) { - this.prevTimeline = neighbour; - } else if (direction == EventTimeline.FORWARDS) { - this.nextTimeline = neighbour; - } else { - throw new Error("Invalid direction '" + direction + "'"); - } - - // make sure we don't try to paginate this timeline - this.setPaginationToken(null, direction); - } - - /** - * Add a new event to the timeline, and update the state - * - * @param event - new event - * @param options - addEvent options - */ - public addEvent(event: MatrixEvent, { toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions): void; - /** - * @deprecated In favor of the overload with `IAddEventOptions` - */ - public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, roomState?: RoomState): void; - public addEvent( - event: MatrixEvent, - toStartOfTimelineOrOpts: boolean | IAddEventOptions, - roomState?: RoomState, - ): void { - let toStartOfTimeline = !!toStartOfTimelineOrOpts; - let timelineWasEmpty: boolean | undefined; - if (typeof toStartOfTimelineOrOpts === "object") { - ({ toStartOfTimeline, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts); - } else if (toStartOfTimelineOrOpts !== undefined) { - // Deprecation warning - // FIXME: Remove after 2023-06-01 (technical debt) - logger.warn( - "Overload deprecated: " + - "`EventTimeline.addEvent(event, toStartOfTimeline, roomState?)` " + - "is deprecated in favor of the overload with `EventTimeline.addEvent(event, IAddEventOptions)`", - ); - } - - if (!roomState) { - roomState = toStartOfTimeline ? this.startState : this.endState; - } - - const timelineSet = this.getTimelineSet(); - - if (timelineSet.room) { - EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline); - - // modify state but only on unfiltered timelineSets - if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) { - roomState?.setStateEvents([event], { timelineWasEmpty }); - // it is possible that the act of setting the state event means we - // can set more metadata (specifically sender/target props), so try - // it again if the prop wasn't previously set. It may also mean that - // the sender/target is updated (if the event set was a room member event) - // so we want to use the *updated* member (new avatar/name) instead. - // - // However, we do NOT want to do this on member events if we're going - // back in time, else we'll set the .sender value for BEFORE the given - // member event, whereas we want to set the .sender value for the ACTUAL - // member event itself. - if (!event.sender || (event.getType() === EventType.RoomMember && !toStartOfTimeline)) { - EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline); - } - } - } - - let insertIndex: number; - - if (toStartOfTimeline) { - insertIndex = 0; - } else { - insertIndex = this.events.length; - } - - this.events.splice(insertIndex, 0, event); // insert element - if (toStartOfTimeline) { - this.baseIndex++; - } - } - - /** - * Remove an event from the timeline - * - * @param eventId - ID of event to be removed - * @returns removed event, or null if not found - */ - public removeEvent(eventId: string): MatrixEvent | null { - for (let i = this.events.length - 1; i >= 0; i--) { - const ev = this.events[i]; - if (ev.getId() == eventId) { - this.events.splice(i, 1); - if (i < this.baseIndex) { - this.baseIndex--; - } - return ev; - } - } - return null; - } - - /** - * Return a string to identify this timeline, for debugging - * - * @returns name for this timeline - */ - public toString(): string { - return this.name; - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts deleted file mode 100644 index 2db3479..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts +++ /dev/null @@ -1,1631 +0,0 @@ -/* -Copyright 2015 - 2023 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. -*/ - -/** - * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for - * the public classes. - */ - -import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk"; - -import type { IEventDecryptionResult } from "../@types/crypto"; -import { logger } from "../logger"; -import { VerificationRequest } from "../crypto/verification/request/VerificationRequest"; -import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event"; -import { Crypto } from "../crypto"; -import { deepSortedObjectEntries, internaliseString } from "../utils"; -import { RoomMember } from "./room-member"; -import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread"; -import { IActionsObject } from "../pushprocessor"; -import { TypedReEmitter } from "../ReEmitter"; -import { MatrixError } from "../http-api"; -import { TypedEventEmitter } from "./typed-event-emitter"; -import { EventStatus } from "./event-status"; -import { DecryptionError } from "../crypto/algorithms"; -import { CryptoBackend } from "../common-crypto/CryptoBackend"; -import { WITHHELD_MESSAGES } from "../crypto/OlmDevice"; - -export { EventStatus } from "./event-status"; - -/* eslint-disable camelcase */ -export interface IContent { - [key: string]: any; - "msgtype"?: MsgType | string; - "membership"?: string; - "avatar_url"?: string; - "displayname"?: string; - "m.relates_to"?: IEventRelation; - - "org.matrix.msc3952.mentions"?: IMentions; -} - -type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>; - -export interface IUnsigned { - "age"?: number; - "prev_sender"?: string; - "prev_content"?: IContent; - "redacted_because"?: IEvent; - "transaction_id"?: string; - "invite_room_state"?: StrippedState[]; - "m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations -} - -export interface IThreadBundledRelationship { - latest_event: IEvent; - count: number; - current_user_participated?: boolean; -} - -export interface IEvent { - event_id: string; - type: string; - content: IContent; - sender: string; - room_id?: string; - origin_server_ts: number; - txn_id?: string; - state_key?: string; - membership?: string; - unsigned: IUnsigned; - redacts?: string; - - /** - * @deprecated in favour of `sender` - */ - user_id?: string; - /** - * @deprecated in favour of `unsigned.prev_content` - */ - prev_content?: IContent; - /** - * @deprecated in favour of `origin_server_ts` - */ - age?: number; -} - -export interface IAggregatedRelation { - origin_server_ts: number; - event_id?: string; - sender?: string; - type?: string; - count?: number; - key?: string; -} - -export interface IEventRelation { - "rel_type"?: RelationType | string; - "event_id"?: string; - "is_falling_back"?: boolean; - "m.in_reply_to"?: { - event_id?: string; - }; - "key"?: string; -} - -export interface IMentions { - user_ids?: string[]; - room?: boolean; -} - -/** - * When an event is a visibility change event, as per MSC3531, - * the visibility change implied by the event. - */ -export interface IVisibilityChange { - /** - * If `true`, the target event should be made visible. - * Otherwise, it should be hidden. - */ - visible: boolean; - - /** - * The event id affected. - */ - eventId: string; - - /** - * Optionally, a human-readable reason explaining why - * the event was hidden. Ignored if the event was made - * visible. - */ - reason: string | null; -} - -export interface IClearEvent { - room_id?: string; - type: string; - content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">; - unsigned?: IUnsigned; -} -/* eslint-enable camelcase */ - -interface IKeyRequestRecipient { - userId: string; - deviceId: "*" | string; -} - -export interface IDecryptOptions { - // Emits "event.decrypted" if set to true - emit?: boolean; - // True if this is a retry (enables more logging) - isRetry?: boolean; - // whether the message should be re-decrypted if it was previously successfully decrypted with an untrusted key - forceRedecryptIfUntrusted?: boolean; -} - -/** - * Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531. - */ -export type MessageVisibility = IMessageVisibilityHidden | IMessageVisibilityVisible; -/** - * Variant of `MessageVisibility` for the case in which the message should be displayed. - */ -export interface IMessageVisibilityVisible { - readonly visible: true; -} -/** - * Variant of `MessageVisibility` for the case in which the message should be hidden. - */ -export interface IMessageVisibilityHidden { - readonly visible: false; - /** - * Optionally, a human-readable reason to show to the user indicating why the - * message has been hidden (e.g. "Message Pending Moderation"). - */ - readonly reason: string | null; -} -// A singleton implementing `IMessageVisibilityVisible`. -const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true }); - -export enum MatrixEventEvent { - Decrypted = "Event.decrypted", - BeforeRedaction = "Event.beforeRedaction", - VisibilityChange = "Event.visibilityChange", - LocalEventIdReplaced = "Event.localEventIdReplaced", - Status = "Event.status", - Replaced = "Event.replaced", - RelationsCreated = "Event.relationsCreated", -} - -export type MatrixEventEmittedEvents = MatrixEventEvent | ThreadEvent.Update; - -export type MatrixEventHandlerMap = { - /** - * Fires when an event is decrypted - * - * @param event - The matrix event which has been decrypted - * @param err - The error that occurred during decryption, or `undefined` if no error occurred. - */ - [MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void; - [MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void; - [MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void; - [MatrixEventEvent.LocalEventIdReplaced]: (event: MatrixEvent) => void; - [MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus | null) => void; - [MatrixEventEvent.Replaced]: (event: MatrixEvent) => void; - [MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void; -} & Pick<ThreadEventHandlerMap, ThreadEvent.Update>; - -export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, MatrixEventHandlerMap> { - private pushActions: IActionsObject | null = null; - private _replacingEvent: MatrixEvent | null = null; - private _localRedactionEvent: MatrixEvent | null = null; - private _isCancelled = false; - private clearEvent?: IClearEvent; - - /* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531. - - Note: We're returning this object, so any value stored here MUST be frozen. - */ - private visibility: MessageVisibility = MESSAGE_VISIBLE; - - // Not all events will be extensible-event compatible, so cache a flag in - // addition to a falsy cached event value. We check the flag later on in - // a public getter to decide if the cache is valid. - private _hasCachedExtEv = false; - private _cachedExtEv: Optional<ExtensibleEvent> = undefined; - - /* curve25519 key which we believe belongs to the sender of the event. See - * getSenderKey() - */ - private senderCurve25519Key: string | null = null; - - /* ed25519 key which the sender of this event (for olm) or the creator of - * the megolm session (for megolm) claims to own. See getClaimedEd25519Key() - */ - private claimedEd25519Key: string | null = null; - - /* curve25519 keys of devices involved in telling us about the - * senderCurve25519Key and claimedEd25519Key. - * See getForwardingCurve25519KeyChain(). - */ - private forwardingCurve25519KeyChain: string[] = []; - - /* where the decryption key is untrusted - */ - private untrusted: boolean | null = null; - - /* if we have a process decrypting this event, a Promise which resolves - * when it is finished. Normally null. - */ - private decryptionPromise: Promise<void> | null = null; - - /* flag to indicate if we should retry decrypting this event after the - * first attempt (eg, we have received new data which means that a second - * attempt may succeed) - */ - private retryDecryption = false; - - /* The txnId with which this event was sent if it was during this session, - * allows for a unique ID which does not change when the event comes back down sync. - */ - private txnId?: string; - - /** - * A reference to the thread this event belongs to - */ - private thread?: Thread; - private threadId?: string; - - /* - * True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and - * the sender has disabled encrypting to unverified devices. - */ - private encryptedDisabledForUnverifiedDevices = false; - - /* Set an approximate timestamp for the event relative the local clock. - * This will inherently be approximate because it doesn't take into account - * the time between the server putting the 'age' field on the event as it sent - * it to us and the time we're now constructing this event, but that's better - * than assuming the local clock is in sync with the origin HS's clock. - */ - public localTimestamp: number; - - /** - * The room member who sent this event, or null e.g. - * this is a presence event. This is only guaranteed to be set for events that - * appear in a timeline, ie. do not guarantee that it will be set on state - * events. - * @privateRemarks - * Should be read-only - */ - public sender: RoomMember | null = null; - /** - * The room member who is the target of this event, e.g. - * the invitee, the person being banned, etc. - * @privateRemarks - * Should be read-only - */ - public target: RoomMember | null = null; - /** - * The sending status of the event. - * @privateRemarks - * Should be read-only - */ - public status: EventStatus | null = null; - /** - * most recent error associated with sending the event, if any - * @privateRemarks - * Should be read-only - */ - public error: MatrixError | null = null; - /** - * True if this event is 'forward looking', meaning - * that getDirectionalContent() will return event.content and not event.prev_content. - * Only state events may be backwards looking - * Default: true. <strong>This property is experimental and may change.</strong> - * @privateRemarks - * Should be read-only - */ - public forwardLooking = true; - - /* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event, - * `Crypto` will set this the `VerificationRequest` for the event - * so it can be easily accessed from the timeline. - */ - public verificationRequest?: VerificationRequest; - - private readonly reEmitter: TypedReEmitter<MatrixEventEmittedEvents, MatrixEventHandlerMap>; - - /** - * Construct a Matrix Event object - * - * @param event - The raw (possibly encrypted) event. <b>Do not access - * this property</b> directly unless you absolutely have to. Prefer the getter - * methods defined on this class. Using the getter methods shields your app - * from changes to event JSON between Matrix versions. - */ - public constructor(public event: Partial<IEvent> = {}) { - super(); - - // intern the values of matrix events to force share strings and reduce the - // amount of needless string duplication. This can save moderate amounts of - // memory (~10% on a 350MB heap). - // 'membership' at the event level (rather than the content level) is a legacy - // field that Element never otherwise looks at, but it will still take up a lot - // of space if we don't intern it. - (["state_key", "type", "sender", "room_id", "membership"] as const).forEach((prop) => { - if (typeof event[prop] !== "string") return; - event[prop] = internaliseString(event[prop]!); - }); - - (["membership", "avatar_url", "displayname"] as const).forEach((prop) => { - if (typeof event.content?.[prop] !== "string") return; - event.content[prop] = internaliseString(event.content[prop]!); - }); - - (["rel_type"] as const).forEach((prop) => { - if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return; - event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]!); - }); - - this.txnId = event.txn_id; - this.localTimestamp = Date.now() - (this.getAge() ?? 0); - this.reEmitter = new TypedReEmitter(this); - } - - /** - * Unstable getter to try and get an extensible event. Note that this might - * return a falsy value if the event could not be parsed as an extensible - * event. - * - * @deprecated Use stable functions where possible. - */ - public get unstableExtensibleEvent(): Optional<ExtensibleEvent> { - if (!this._hasCachedExtEv) { - this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent()); - } - return this._cachedExtEv; - } - - private invalidateExtensibleEvent(): void { - // just reset the flag - that'll trick the getter into parsing a new event - this._hasCachedExtEv = false; - } - - /** - * Gets the event as though it would appear unencrypted. If the event is already not - * encrypted, it is simply returned as-is. - * @returns The event in wire format. - */ - public getEffectiveEvent(): IEvent { - const content = Object.assign({}, this.getContent()); // clone for mutation - - if (this.getWireType() === EventType.RoomMessageEncrypted) { - // Encrypted events sometimes aren't symmetrical on the `content` so we'll copy - // that over too, but only for missing properties. We don't copy over mismatches - // between the plain and decrypted copies of `content` because we assume that the - // app is relying on the decrypted version, so we want to expose that as a source - // of truth here too. - for (const [key, value] of Object.entries(this.getWireContent())) { - // Skip fields from the encrypted event schema though - we don't want to leak - // these. - if (["algorithm", "ciphertext", "device_id", "sender_key", "session_id"].includes(key)) { - continue; - } - - if (content[key] === undefined) content[key] = value; - } - } - - // clearEvent doesn't have all the fields, so we'll copy what we can from this.event. - // We also copy over our "fixed" content key. - return Object.assign({}, this.event, this.clearEvent, { content }) as IEvent; - } - - /** - * Get the event_id for this event. - * @returns The event ID, e.g. <code>$143350589368169JsLZx:localhost - * </code> - */ - public getId(): string | undefined { - return this.event.event_id; - } - - /** - * Get the user_id for this event. - * @returns The user ID, e.g. `@alice:matrix.org` - */ - public getSender(): string | undefined { - return this.event.sender || this.event.user_id; // v2 / v1 - } - - /** - * Get the (decrypted, if necessary) type of event. - * - * @returns The event type, e.g. `m.room.message` - */ - public getType(): EventType | string { - if (this.clearEvent) { - return this.clearEvent.type; - } - return this.event.type!; - } - - /** - * Get the (possibly encrypted) type of the event that will be sent to the - * homeserver. - * - * @returns The event type. - */ - public getWireType(): EventType | string { - return this.event.type!; - } - - /** - * Get the room_id for this event. This will return `undefined` - * for `m.presence` events. - * @returns The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org - * </code> - */ - public getRoomId(): string | undefined { - return this.event.room_id; - } - - /** - * Get the timestamp of this event. - * @returns The event timestamp, e.g. `1433502692297` - */ - public getTs(): number { - return this.event.origin_server_ts!; - } - - /** - * Get the timestamp of this event, as a Date object. - * @returns The event date, e.g. `new Date(1433502692297)` - */ - public getDate(): Date | null { - return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null; - } - - /** - * Get a string containing details of this event - * - * This is intended for logging, to help trace errors. Example output: - * - * @example - * ``` - * id=$HjnOHV646n0SjLDAqFrgIjim7RCpB7cdMXFrekWYAn type=m.room.encrypted - * sender=@user:example.com room=!room:example.com ts=2022-10-25T17:30:28.404Z - * ``` - */ - public getDetails(): string { - let details = `id=${this.getId()} type=${this.getWireType()} sender=${this.getSender()}`; - const room = this.getRoomId(); - if (room) { - details += ` room=${room}`; - } - const date = this.getDate(); - if (date) { - details += ` ts=${date.toISOString()}`; - } - return details; - } - - /** - * Get the (decrypted, if necessary) event content JSON, even if the event - * was replaced by another event. - * - * @returns The event content JSON, or an empty object. - */ - public getOriginalContent<T = IContent>(): T { - if (this._localRedactionEvent) { - return {} as T; - } - if (this.clearEvent) { - return (this.clearEvent.content || {}) as T; - } - return (this.event.content || {}) as T; - } - - /** - * Get the (decrypted, if necessary) event content JSON, - * or the content from the replacing event, if any. - * See `makeReplaced`. - * - * @returns The event content JSON, or an empty object. - */ - public getContent<T extends IContent = IContent>(): T { - if (this._localRedactionEvent) { - return {} as T; - } else if (this._replacingEvent) { - return this._replacingEvent.getContent()["m.new_content"] || {}; - } else { - return this.getOriginalContent(); - } - } - - /** - * Get the (possibly encrypted) event content JSON that will be sent to the - * homeserver. - * - * @returns The event content JSON, or an empty object. - */ - public getWireContent(): IContent { - return this.event.content || {}; - } - - /** - * Get the event ID of the thread head - */ - public get threadRootId(): string | undefined { - const relatesTo = this.getWireContent()?.["m.relates_to"]; - if (relatesTo?.rel_type === THREAD_RELATION_TYPE.name) { - return relatesTo.event_id; - } else { - return this.getThread()?.id || this.threadId; - } - } - - /** - * A helper to check if an event is a thread's head or not - */ - public get isThreadRoot(): boolean { - const threadDetails = this.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name); - - // Bundled relationships only returned when the sync response is limited - // hence us having to check both bundled relation and inspect the thread - // model - return !!threadDetails || this.getThread()?.id === this.getId(); - } - - public get replyEventId(): string | undefined { - return this.getWireContent()["m.relates_to"]?.["m.in_reply_to"]?.event_id; - } - - public get relationEventId(): string | undefined { - return this.getWireContent()?.["m.relates_to"]?.event_id; - } - - /** - * Get the previous event content JSON. This will only return something for - * state events which exist in the timeline. - * @returns The previous event content JSON, or an empty object. - */ - public getPrevContent(): IContent { - // v2 then v1 then default - return this.getUnsigned().prev_content || this.event.prev_content || {}; - } - - /** - * Get either 'content' or 'prev_content' depending on if this event is - * 'forward-looking' or not. This can be modified via event.forwardLooking. - * In practice, this means we get the chronologically earlier content value - * for this event (this method should surely be called getEarlierContent) - * <strong>This method is experimental and may change.</strong> - * @returns event.content if this event is forward-looking, else - * event.prev_content. - */ - public getDirectionalContent(): IContent { - return this.forwardLooking ? this.getContent() : this.getPrevContent(); - } - - /** - * Get the age of this event. This represents the age of the event when the - * event arrived at the device, and not the age of the event when this - * function was called. - * Can only be returned once the server has echo'ed back - * @returns The age of this event in milliseconds. - */ - public getAge(): number | undefined { - return this.getUnsigned().age || this.event.age; // v2 / v1 - } - - /** - * Get the age of the event when this function was called. - * This is the 'age' field adjusted according to how long this client has - * had the event. - * @returns The age of this event in milliseconds. - */ - public getLocalAge(): number { - return Date.now() - this.localTimestamp; - } - - /** - * Get the event state_key if it has one. This will return <code>undefined - * </code> for message events. - * @returns The event's `state_key`. - */ - public getStateKey(): string | undefined { - return this.event.state_key; - } - - /** - * Check if this event is a state event. - * @returns True if this is a state event. - */ - public isState(): boolean { - return this.event.state_key !== undefined; - } - - /** - * Replace the content of this event with encrypted versions. - * (This is used when sending an event; it should not be used by applications). - * - * @internal - * - * @param cryptoType - type of the encrypted event - typically - * <tt>"m.room.encrypted"</tt> - * - * @param cryptoContent - raw 'content' for the encrypted event. - * - * @param senderCurve25519Key - curve25519 key to record for the - * sender of this event. - * See {@link MatrixEvent#getSenderKey}. - * - * @param claimedEd25519Key - claimed ed25519 key to record for the - * sender if this event. - * See {@link MatrixEvent#getClaimedEd25519Key} - */ - public makeEncrypted( - cryptoType: string, - cryptoContent: object, - senderCurve25519Key: string, - claimedEd25519Key: string, - ): void { - // keep the plain-text data for 'view source' - this.clearEvent = { - type: this.event.type!, - content: this.event.content!, - }; - this.event.type = cryptoType; - this.event.content = cryptoContent; - this.senderCurve25519Key = senderCurve25519Key; - this.claimedEd25519Key = claimedEd25519Key; - } - - /** - * Check if this event is currently being decrypted. - * - * @returns True if this event is currently being decrypted, else false. - */ - public isBeingDecrypted(): boolean { - return this.decryptionPromise != null; - } - - public getDecryptionPromise(): Promise<void> | null { - return this.decryptionPromise; - } - - /** - * Check if this event is an encrypted event which we failed to decrypt - * - * (This implies that we might retry decryption at some point in the future) - * - * @returns True if this event is an encrypted event which we - * couldn't decrypt. - */ - public isDecryptionFailure(): boolean { - return this.clearEvent?.content?.msgtype === "m.bad.encrypted"; - } - - /* - * True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and - * the sender has disabled encrypting to unverified devices. - */ - public get isEncryptedDisabledForUnverifiedDevices(): boolean { - return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices; - } - - public shouldAttemptDecryption(): boolean { - if (this.isRedacted()) return false; - if (this.isBeingDecrypted()) return false; - if (this.clearEvent) return false; - if (!this.isEncrypted()) return false; - - return true; - } - - /** - * Start the process of trying to decrypt this event. - * - * (This is used within the SDK: it isn't intended for use by applications) - * - * @internal - * - * @param crypto - crypto module - * - * @returns promise which resolves (to undefined) when the decryption - * attempt is completed. - */ - public async attemptDecryption(crypto: CryptoBackend, options: IDecryptOptions = {}): Promise<void> { - // start with a couple of sanity checks. - if (!this.isEncrypted()) { - throw new Error("Attempt to decrypt event which isn't encrypted"); - } - - const alreadyDecrypted = this.clearEvent && !this.isDecryptionFailure(); - const forceRedecrypt = options.forceRedecryptIfUntrusted && this.isKeySourceUntrusted(); - if (alreadyDecrypted && !forceRedecrypt) { - // we may want to just ignore this? let's start with rejecting it. - throw new Error("Attempt to decrypt event which has already been decrypted"); - } - - // if we already have a decryption attempt in progress, then it may - // fail because it was using outdated info. We now have reason to - // succeed where it failed before, but we don't want to have multiple - // attempts going at the same time, so just set a flag that says we have - // new info. - // - if (this.decryptionPromise) { - logger.log(`Event ${this.getId()} already being decrypted; queueing a retry`); - this.retryDecryption = true; - return this.decryptionPromise; - } - - this.decryptionPromise = this.decryptionLoop(crypto, options); - return this.decryptionPromise; - } - - /** - * Cancel any room key request for this event and resend another. - * - * @param crypto - crypto module - * @param userId - the user who received this event - * - * @returns a promise that resolves when the request is queued - */ - public cancelAndResendKeyRequest(crypto: Crypto, userId: string): Promise<void> { - const wireContent = this.getWireContent(); - return crypto.requestRoomKey( - { - algorithm: wireContent.algorithm, - room_id: this.getRoomId()!, - session_id: wireContent.session_id, - sender_key: wireContent.sender_key, - }, - this.getKeyRequestRecipients(userId), - true, - ); - } - - /** - * Calculate the recipients for keyshare requests. - * - * @param userId - the user who received this event. - * - * @returns array of recipients - */ - public getKeyRequestRecipients(userId: string): IKeyRequestRecipient[] { - // send the request to all of our own devices - const recipients = [ - { - userId, - deviceId: "*", - }, - ]; - - return recipients; - } - - private async decryptionLoop(crypto: CryptoBackend, options: IDecryptOptions = {}): Promise<void> { - // make sure that this method never runs completely synchronously. - // (doing so would mean that we would clear decryptionPromise *before* - // it is set in attemptDecryption - and hence end up with a stuck - // `decryptionPromise`). - await Promise.resolve(); - - // eslint-disable-next-line no-constant-condition - while (true) { - this.retryDecryption = false; - - let res: IEventDecryptionResult; - let err: Error | undefined = undefined; - try { - if (!crypto) { - res = this.badEncryptedMessage("Encryption not enabled"); - } else { - res = await crypto.decryptEvent(this); - if (options.isRetry === true) { - logger.info(`Decrypted event on retry (${this.getDetails()})`); - } - } - } catch (e) { - const detailedError = e instanceof DecryptionError ? (<DecryptionError>e).detailedString : String(e); - - err = e as Error; - - // see if we have a retry queued. - // - // NB: make sure to keep this check in the same tick of the - // event loop as `decryptionPromise = null` below - otherwise we - // risk a race: - // - // * A: we check retryDecryption here and see that it is - // false - // * B: we get a second call to attemptDecryption, which sees - // that decryptionPromise is set so sets - // retryDecryption - // * A: we continue below, clear decryptionPromise, and - // never do the retry. - // - if (this.retryDecryption) { - // decryption error, but we have a retry queued. - logger.log(`Error decrypting event (${this.getDetails()}), but retrying: ${detailedError}`); - continue; - } - - // decryption error, no retries queued. Warn about the error and - // set it to m.bad.encrypted. - // - // the detailedString already includes the name and message of the error, and the stack isn't much use, - // so we don't bother to log `e` separately. - logger.warn(`Error decrypting event (${this.getDetails()}): ${detailedError}`); - - res = this.badEncryptedMessage(String(e)); - } - - // at this point, we've either successfully decrypted the event, or have given up - // (and set res to a 'badEncryptedMessage'). Either way, we can now set the - // cleartext of the event and raise Event.decrypted. - // - // make sure we clear 'decryptionPromise' before sending the 'Event.decrypted' event, - // otherwise the app will be confused to see `isBeingDecrypted` still set when - // there isn't an `Event.decrypted` on the way. - // - // see also notes on retryDecryption above. - // - this.decryptionPromise = null; - this.retryDecryption = false; - this.setClearData(res); - - // Before we emit the event, clear the push actions so that they can be recalculated - // by relevant code. We do this because the clear event has now changed, making it - // so that existing rules can be re-run over the applicable properties. Stuff like - // highlighting when the user's name is mentioned rely on this happening. We also want - // to set the push actions before emitting so that any notification listeners don't - // pick up the wrong contents. - this.setPushActions(null); - - if (options.emit !== false) { - this.emit(MatrixEventEvent.Decrypted, this, err); - } - - return; - } - } - - private badEncryptedMessage(reason: string): IEventDecryptionResult { - return { - clearEvent: { - type: EventType.RoomMessage, - content: { - msgtype: "m.bad.encrypted", - body: "** Unable to decrypt: " + reason + " **", - }, - }, - encryptedDisabledForUnverifiedDevices: reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`, - }; - } - - /** - * Update the cleartext data on this event. - * - * (This is used after decrypting an event; it should not be used by applications). - * - * @internal - * - * @param decryptionResult - the decryption result, including the plaintext and some key info - * - * @remarks - * Fires {@link MatrixEventEvent.Decrypted} - */ - private setClearData(decryptionResult: IEventDecryptionResult): void { - this.clearEvent = decryptionResult.clearEvent; - this.senderCurve25519Key = decryptionResult.senderCurve25519Key ?? null; - this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null; - this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || []; - this.untrusted = decryptionResult.untrusted || false; - this.encryptedDisabledForUnverifiedDevices = decryptionResult.encryptedDisabledForUnverifiedDevices || false; - this.invalidateExtensibleEvent(); - } - - /** - * Gets the cleartext content for this event. If the event is not encrypted, - * or encryption has not been completed, this will return null. - * - * @returns The cleartext (decrypted) content for the event - */ - public getClearContent(): IContent | null { - return this.clearEvent ? this.clearEvent.content : null; - } - - /** - * Check if the event is encrypted. - * @returns True if this event is encrypted. - */ - public isEncrypted(): boolean { - return !this.isState() && this.event.type === EventType.RoomMessageEncrypted; - } - - /** - * The curve25519 key for the device that we think sent this event - * - * For an Olm-encrypted event, this is inferred directly from the DH - * exchange at the start of the session: the curve25519 key is involved in - * the DH exchange, so only a device which holds the private part of that - * key can establish such a session. - * - * For a megolm-encrypted event, it is inferred from the Olm message which - * established the megolm session - */ - public getSenderKey(): string | null { - return this.senderCurve25519Key; - } - - /** - * The additional keys the sender of this encrypted event claims to possess. - * - * Just a wrapper for #getClaimedEd25519Key (q.v.) - */ - public getKeysClaimed(): Partial<Record<"ed25519", string>> { - if (!this.claimedEd25519Key) return {}; - - return { - ed25519: this.claimedEd25519Key, - }; - } - - /** - * Get the ed25519 the sender of this event claims to own. - * - * For Olm messages, this claim is encoded directly in the plaintext of the - * event itself. For megolm messages, it is implied by the m.room_key event - * which established the megolm session. - * - * Until we download the device list of the sender, it's just a claim: the - * device list gives a proof that the owner of the curve25519 key used for - * this event (and returned by #getSenderKey) also owns the ed25519 key by - * signing the public curve25519 key with the ed25519 key. - * - * In general, applications should not use this method directly, but should - * instead use MatrixClient.getEventSenderDeviceInfo. - */ - public getClaimedEd25519Key(): string | null { - return this.claimedEd25519Key; - } - - /** - * Get the curve25519 keys of the devices which were involved in telling us - * about the claimedEd25519Key and sender curve25519 key. - * - * Normally this will be empty, but in the case of a forwarded megolm - * session, the sender keys are sent to us by another device (the forwarding - * device), which we need to trust to do this. In that case, the result will - * be a list consisting of one entry. - * - * If the device that sent us the key (A) got it from another device which - * it wasn't prepared to vouch for (B), the result will be [A, B]. And so on. - * - * @returns base64-encoded curve25519 keys, from oldest to newest. - */ - public getForwardingCurve25519KeyChain(): string[] { - return this.forwardingCurve25519KeyChain; - } - - /** - * Whether the decryption key was obtained from an untrusted source. If so, - * we cannot verify the authenticity of the message. - */ - public isKeySourceUntrusted(): boolean | undefined { - return !!this.untrusted; - } - - public getUnsigned(): IUnsigned { - return this.event.unsigned || {}; - } - - public setUnsigned(unsigned: IUnsigned): void { - this.event.unsigned = unsigned; - } - - public unmarkLocallyRedacted(): boolean { - const value = this._localRedactionEvent; - this._localRedactionEvent = null; - if (this.event.unsigned) { - this.event.unsigned.redacted_because = undefined; - } - return !!value; - } - - public markLocallyRedacted(redactionEvent: MatrixEvent): void { - if (this._localRedactionEvent) return; - this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent); - this._localRedactionEvent = redactionEvent; - if (!this.event.unsigned) { - this.event.unsigned = {}; - } - this.event.unsigned.redacted_because = redactionEvent.event as IEvent; - } - - /** - * Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 . - * - * @param visibilityChange - event holding a hide/unhide payload, or nothing - * if the event is being reset to its original visibility (presumably - * by a visibility event being redacted). - * - * @remarks - * Fires {@link MatrixEventEvent.VisibilityChange} if `visibilityEvent` - * caused a change in the actual visibility of this event, either by making it - * visible (if it was hidden), by making it hidden (if it was visible) or by - * changing the reason (if it was hidden). - */ - public applyVisibilityEvent(visibilityChange?: IVisibilityChange): void { - const visible = visibilityChange?.visible ?? true; - const reason = visibilityChange?.reason ?? null; - let change = false; - if (this.visibility.visible !== visible) { - change = true; - } else if (!this.visibility.visible && this.visibility["reason"] !== reason) { - change = true; - } - if (change) { - if (visible) { - this.visibility = MESSAGE_VISIBLE; - } else { - this.visibility = Object.freeze({ - visible: false, - reason, - }); - } - this.emit(MatrixEventEvent.VisibilityChange, this, visible); - } - } - - /** - * Return instructions to display or hide the message. - * - * @returns Instructions determining whether the message - * should be displayed. - */ - public messageVisibility(): MessageVisibility { - // Note: We may return `this.visibility` without fear, as - // this is a shallow frozen object. - return this.visibility; - } - - /** - * Update the content of an event in the same way it would be by the server - * if it were redacted before it was sent to us - * - * @param redactionEvent - event causing the redaction - */ - public makeRedacted(redactionEvent: MatrixEvent): void { - // quick sanity-check - if (!redactionEvent.event) { - throw new Error("invalid redactionEvent in makeRedacted"); - } - - this._localRedactionEvent = null; - - this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent); - - this._replacingEvent = null; - // we attempt to replicate what we would see from the server if - // the event had been redacted before we saw it. - // - // The server removes (most of) the content of the event, and adds a - // "redacted_because" key to the unsigned section containing the - // redacted event. - if (!this.event.unsigned) { - this.event.unsigned = {}; - } - this.event.unsigned.redacted_because = redactionEvent.event as IEvent; - - for (const key in this.event) { - if (this.event.hasOwnProperty(key) && !REDACT_KEEP_KEYS.has(key)) { - delete this.event[key as keyof IEvent]; - } - } - - // If the event is encrypted prune the decrypted bits - if (this.isEncrypted()) { - this.clearEvent = undefined; - } - - const keeps = - this.getType() in REDACT_KEEP_CONTENT_MAP - ? REDACT_KEEP_CONTENT_MAP[this.getType() as keyof typeof REDACT_KEEP_CONTENT_MAP] - : {}; - const content = this.getContent(); - for (const key in content) { - if (content.hasOwnProperty(key) && !keeps[key]) { - delete content[key]; - } - } - - this.invalidateExtensibleEvent(); - } - - /** - * Check if this event has been redacted - * - * @returns True if this event has been redacted - */ - public isRedacted(): boolean { - return Boolean(this.getUnsigned().redacted_because); - } - - /** - * Check if this event is a redaction of another event - * - * @returns True if this event is a redaction - */ - public isRedaction(): boolean { - return this.getType() === EventType.RoomRedaction; - } - - /** - * Return the visibility change caused by this event, - * as per https://github.com/matrix-org/matrix-doc/pull/3531. - * - * @returns If the event is a well-formed visibility change event, - * an instance of `IVisibilityChange`, otherwise `null`. - */ - public asVisibilityChange(): IVisibilityChange | null { - if (!EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) { - // Not a visibility change event. - return null; - } - const relation = this.getRelation(); - if (!relation || relation.rel_type != "m.reference") { - // Ill-formed, ignore this event. - return null; - } - const eventId = relation.event_id; - if (!eventId) { - // Ill-formed, ignore this event. - return null; - } - const content = this.getWireContent(); - const visible = !!content.visible; - const reason = content.reason; - if (reason && typeof reason != "string") { - // Ill-formed, ignore this event. - return null; - } - // Well-formed visibility change event. - return { - visible, - reason, - eventId, - }; - } - - /** - * Check if this event alters the visibility of another event, - * as per https://github.com/matrix-org/matrix-doc/pull/3531. - * - * @returns True if this event alters the visibility - * of another event. - */ - public isVisibilityEvent(): boolean { - return EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType()); - } - - /** - * Get the (decrypted, if necessary) redaction event JSON - * if event was redacted - * - * @returns The redaction event JSON, or an empty object - */ - public getRedactionEvent(): IEvent | {} | null { - if (!this.isRedacted()) return null; - - if (this.clearEvent?.unsigned) { - return this.clearEvent?.unsigned.redacted_because ?? null; - } else if (this.event.unsigned?.redacted_because) { - return this.event.unsigned.redacted_because; - } else { - return {}; - } - } - - /** - * Get the push actions, if known, for this event - * - * @returns push actions - */ - public getPushActions(): IActionsObject | null { - return this.pushActions; - } - - /** - * Set the push actions for this event. - * - * @param pushActions - push actions - */ - public setPushActions(pushActions: IActionsObject | null): void { - this.pushActions = pushActions; - } - - /** - * Replace the `event` property and recalculate any properties based on it. - * @param event - the object to assign to the `event` property - */ - public handleRemoteEcho(event: object): void { - const oldUnsigned = this.getUnsigned(); - const oldId = this.getId(); - this.event = event; - // if this event was redacted before it was sent, it's locally marked as redacted. - // At this point, we've received the remote echo for the event, but not yet for - // the redaction that we are sending ourselves. Preserve the locally redacted - // state by copying over redacted_because so we don't get a flash of - // redacted, not-redacted, redacted as remote echos come in - if (oldUnsigned.redacted_because) { - if (!this.event.unsigned) { - this.event.unsigned = {}; - } - this.event.unsigned.redacted_because = oldUnsigned.redacted_because; - } - // successfully sent. - this.setStatus(null); - if (this.getId() !== oldId) { - // emit the event if it changed - this.emit(MatrixEventEvent.LocalEventIdReplaced, this); - } - - this.localTimestamp = Date.now() - this.getAge()!; - } - - /** - * Whether the event is in any phase of sending, send failure, waiting for - * remote echo, etc. - */ - public isSending(): boolean { - return !!this.status; - } - - /** - * Update the event's sending status and emit an event as well. - * - * @param status - The new status - */ - public setStatus(status: EventStatus | null): void { - this.status = status; - this.emit(MatrixEventEvent.Status, this, status); - } - - public replaceLocalEventId(eventId: string): void { - this.event.event_id = eventId; - this.emit(MatrixEventEvent.LocalEventIdReplaced, this); - } - - /** - * Get whether the event is a relation event, and of a given type if - * `relType` is passed in. State events cannot be relation events - * - * @param relType - if given, checks that the relation is of the - * given type - */ - public isRelation(relType?: string): boolean { - // Relation info is lifted out of the encrypted content when sent to - // encrypted rooms, so we have to check `getWireContent` for this. - const relation = this.getWireContent()?.["m.relates_to"]; - if (this.isState() && relation?.rel_type === RelationType.Replace) { - // State events cannot be m.replace relations - return false; - } - return !!(relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true)); - } - - /** - * Get relation info for the event, if any. - */ - public getRelation(): IEventRelation | null { - if (!this.isRelation()) { - return null; - } - return this.getWireContent()["m.relates_to"] ?? null; - } - - /** - * Set an event that replaces the content of this event, through an m.replace relation. - * - * @param newEvent - the event with the replacing content, if any. - * - * @remarks - * Fires {@link MatrixEventEvent.Replaced} - */ - public makeReplaced(newEvent?: MatrixEvent): void { - // don't allow redacted events to be replaced. - // if newEvent is null we allow to go through though, - // as with local redaction, the replacing event might get - // cancelled, which should be reflected on the target event. - if (this.isRedacted() && newEvent) { - return; - } - // don't allow state events to be replaced using this mechanism as per MSC2676 - if (this.isState()) { - return; - } - if (this._replacingEvent !== newEvent) { - this._replacingEvent = newEvent ?? null; - this.emit(MatrixEventEvent.Replaced, this); - this.invalidateExtensibleEvent(); - } - } - - /** - * Returns the status of any associated edit or redaction - * (not for reactions/annotations as their local echo doesn't affect the original event), - * or else the status of the event. - */ - public getAssociatedStatus(): EventStatus | null { - if (this._replacingEvent) { - return this._replacingEvent.status; - } else if (this._localRedactionEvent) { - return this._localRedactionEvent.status; - } - return this.status; - } - - public getServerAggregatedRelation<T>(relType: RelationType | string): T | undefined { - return this.getUnsigned()["m.relations"]?.[relType]; - } - - /** - * Returns the event ID of the event replacing the content of this event, if any. - */ - public replacingEventId(): string | undefined { - const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace); - if (replaceRelation) { - return replaceRelation.event_id; - } else if (this._replacingEvent) { - return this._replacingEvent.getId(); - } - } - - /** - * Returns the event replacing the content of this event, if any. - * Replacements are aggregated on the server, so this would only - * return an event in case it came down the sync, or for local echo of edits. - */ - public replacingEvent(): MatrixEvent | null { - return this._replacingEvent; - } - - /** - * Returns the origin_server_ts of the event replacing the content of this event, if any. - */ - public replacingEventDate(): Date | undefined { - const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace); - if (replaceRelation) { - const ts = replaceRelation.origin_server_ts; - if (Number.isFinite(ts)) { - return new Date(ts); - } - } else if (this._replacingEvent) { - return this._replacingEvent.getDate() ?? undefined; - } - } - - /** - * Returns the event that wants to redact this event, but hasn't been sent yet. - * @returns the event - */ - public localRedactionEvent(): MatrixEvent | null { - return this._localRedactionEvent; - } - - /** - * For relations and redactions, returns the event_id this event is referring to. - */ - public getAssociatedId(): string | undefined { - const relation = this.getRelation(); - if (this.replyEventId) { - return this.replyEventId; - } else if (relation) { - return relation.event_id; - } else if (this.isRedaction()) { - return this.event.redacts; - } - } - - /** - * Checks if this event is associated with another event. See `getAssociatedId`. - * @deprecated use hasAssociation instead. - */ - public hasAssocation(): boolean { - return !!this.getAssociatedId(); - } - - /** - * Checks if this event is associated with another event. See `getAssociatedId`. - */ - public hasAssociation(): boolean { - return !!this.getAssociatedId(); - } - - /** - * Update the related id with a new one. - * - * Used to replace a local id with remote one before sending - * an event with a related id. - * - * @param eventId - the new event id - */ - public updateAssociatedId(eventId: string): void { - const relation = this.getRelation(); - if (relation) { - relation.event_id = eventId; - } else if (this.isRedaction()) { - this.event.redacts = eventId; - } - } - - /** - * Flags an event as cancelled due to future conditions. For example, a verification - * request event in the same sync transaction may be flagged as cancelled to warn - * listeners that a cancellation event is coming down the same pipe shortly. - * @param cancelled - Whether the event is to be cancelled or not. - */ - public flagCancelled(cancelled = true): void { - this._isCancelled = cancelled; - } - - /** - * Gets whether or not the event is flagged as cancelled. See flagCancelled() for - * more information. - * @returns True if the event is cancelled, false otherwise. - */ - public isCancelled(): boolean { - return this._isCancelled; - } - - /** - * Get a copy/snapshot of this event. The returned copy will be loosely linked - * back to this instance, though will have "frozen" event information. Other - * properties of this MatrixEvent instance will be copied verbatim, which can - * mean they are in reference to this instance despite being on the copy too. - * The reference the snapshot uses does not change, however members aside from - * the underlying event will not be deeply cloned, thus may be mutated internally. - * For example, the sender profile will be copied over at snapshot time, and - * the sender profile internally may mutate without notice to the consumer. - * - * This is meant to be used to snapshot the event details themselves, not the - * features (such as sender) surrounding the event. - * @returns A snapshot of this event. - */ - public toSnapshot(): MatrixEvent { - const ev = new MatrixEvent(JSON.parse(JSON.stringify(this.event))); - for (const [p, v] of Object.entries(this)) { - if (p !== "event") { - // exclude the thing we just cloned - // @ts-ignore - XXX: this is just nasty - ev[p as keyof MatrixEvent] = v; - } - } - return ev; - } - - /** - * Determines if this event is equivalent to the given event. This only checks - * the event object itself, not the other properties of the event. Intended for - * use with toSnapshot() to identify events changing. - * @param otherEvent - The other event to check against. - * @returns True if the events are the same, false otherwise. - */ - public isEquivalentTo(otherEvent: MatrixEvent): boolean { - if (!otherEvent) return false; - if (otherEvent === this) return true; - const myProps = deepSortedObjectEntries(this.event); - const theirProps = deepSortedObjectEntries(otherEvent.event); - return JSON.stringify(myProps) === JSON.stringify(theirProps); - } - - /** - * Summarise the event as JSON. This is currently used by React SDK's view - * event source feature and Seshat's event indexing, so take care when - * adjusting the output here. - * - * If encrypted, include both the decrypted and encrypted view of the event. - * - * This is named `toJSON` for use with `JSON.stringify` which checks objects - * for functions named `toJSON` and will call them to customise the output - * if they are defined. - */ - public toJSON(): object { - const event = this.getEffectiveEvent(); - - if (!this.isEncrypted()) { - return event; - } - - return { - decrypted: event, - encrypted: this.event, - }; - } - - public setVerificationRequest(request: VerificationRequest): void { - this.verificationRequest = request; - } - - public setTxnId(txnId: string): void { - this.txnId = txnId; - } - - public getTxnId(): string | undefined { - return this.txnId; - } - - /** - * Set the instance of a thread associated with the current event - * @param thread - the thread - */ - public setThread(thread?: Thread): void { - if (this.thread) { - this.reEmitter.stopReEmitting(this.thread, [ThreadEvent.Update]); - } - this.thread = thread; - this.setThreadId(thread?.id); - if (thread) { - this.reEmitter.reEmit(thread, [ThreadEvent.Update]); - } - } - - /** - * Get the instance of the thread associated with the current event - */ - public getThread(): Thread | undefined { - return this.thread; - } - - public setThreadId(threadId?: string): void { - this.threadId = threadId; - } -} - -/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted - * - * This is specified here: - * http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions - * - * Also: - * - We keep 'unsigned' since that is created by the local server - * - We keep user_id for backwards-compat with v1 - */ -const REDACT_KEEP_KEYS = new Set([ - "event_id", - "type", - "room_id", - "user_id", - "sender", - "state_key", - "prev_state", - "content", - "unsigned", - "origin_server_ts", -]); - -// a map from state event type to the .content keys we keep when an event is redacted -const REDACT_KEEP_CONTENT_MAP: Record<string, Record<string, 1>> = { - [EventType.RoomMember]: { membership: 1 }, - [EventType.RoomCreate]: { creator: 1 }, - [EventType.RoomJoinRules]: { join_rule: 1 }, - [EventType.RoomPowerLevels]: { - ban: 1, - events: 1, - events_default: 1, - kick: 1, - redact: 1, - state_default: 1, - users: 1, - users_default: 1, - }, -} as const; diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts deleted file mode 100644 index 173ba62..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts +++ /dev/null @@ -1,368 +0,0 @@ -/* -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 { UnstableValue } from "matrix-events-sdk"; - -import { MatrixClient } from "../client"; -import { IContent, MatrixEvent } from "./event"; -import { EventTimeline } from "./event-timeline"; -import { Preset } from "../@types/partials"; -import { globToRegexp } from "../utils"; -import { Room } from "./room"; - -/// The event type storing the user's individual policies. -/// -/// Exported for testing purposes. -export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies"); - -/// The key within the user's individual policies storing the user's ignored invites. -/// -/// Exported for testing purposes. -export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue( - "m.ignore.invites", - "org.matrix.msc3847.ignore.invites", -); - -/// The types of recommendations understood. -enum PolicyRecommendation { - Ban = "m.ban", -} - -/** - * The various scopes for policies. - */ -export enum PolicyScope { - /** - * The policy deals with an individual user, e.g. reject invites - * from this user. - */ - User = "m.policy.user", - - /** - * The policy deals with a room, e.g. reject invites towards - * a specific room. - */ - Room = "m.policy.room", - - /** - * The policy deals with a server, e.g. reject invites from - * this server. - */ - Server = "m.policy.server", -} - -/** - * A container for ignored invites. - * - * # Performance - * - * This implementation is extremely naive. It expects that we are dealing - * with a very short list of sources (e.g. only one). If real-world - * applications turn out to require longer lists, we may need to rework - * our data structures. - */ -export class IgnoredInvites { - public constructor(private readonly client: MatrixClient) {} - - /** - * Add a new rule. - * - * @param scope - The scope for this rule. - * @param entity - The entity covered by this rule. Globs are supported. - * @param reason - A human-readable reason for introducing this new rule. - * @returns The event id for the new rule. - */ - public async addRule(scope: PolicyScope, entity: string, reason: string): Promise<string> { - const target = await this.getOrCreateTargetRoom(); - const response = await this.client.sendStateEvent(target.roomId, scope, { - entity, - reason, - recommendation: PolicyRecommendation.Ban, - }); - return response.event_id; - } - - /** - * Remove a rule. - */ - public async removeRule(event: MatrixEvent): Promise<void> { - await this.client.redactEvent(event.getRoomId()!, event.getId()!); - } - - /** - * Add a new room to the list of sources. If the user isn't a member of the - * room, attempt to join it. - * - * @param roomId - A valid room id. If this room is already in the list - * of sources, it will not be duplicated. - * @returns `true` if the source was added, `false` if it was already present. - * @throws If `roomId` isn't the id of a room that the current user is already - * member of or can join. - * - * # Safety - * - * This method will rewrite the `Policies` object in the user's account data. - * This rewrite is inherently racy and could overwrite or be overwritten by - * other concurrent rewrites of the same object. - */ - public async addSource(roomId: string): Promise<boolean> { - // We attempt to join the room *before* calling - // `await this.getOrCreateSourceRooms()` to decrease the duration - // of the racy section. - await this.client.joinRoom(roomId); - // Race starts. - const sources = (await this.getOrCreateSourceRooms()).map((room) => room.roomId); - if (sources.includes(roomId)) { - return false; - } - sources.push(roomId); - await this.withIgnoreInvitesPolicies((ignoreInvitesPolicies) => { - ignoreInvitesPolicies.sources = sources; - }); - - // Race ends. - return true; - } - - /** - * Find out whether an invite should be ignored. - * - * @param sender - The user id for the user who issued the invite. - * @param roomId - The room to which the user is invited. - * @returns A rule matching the entity, if any was found, `null` otherwise. - */ - public async getRuleForInvite({ - sender, - roomId, - }: { - sender: string; - roomId: string; - }): Promise<Readonly<MatrixEvent | null>> { - // In this implementation, we perform a very naive lookup: - // - search in each policy room; - // - turn each (potentially glob) rule entity into a regexp. - // - // Real-world testing will tell us whether this is performant enough. - // In the (unfortunately likely) case it isn't, there are several manners - // in which we could optimize this: - // - match several entities per go; - // - pre-compile each rule entity into a regexp; - // - pre-compile entire rooms into a single regexp. - const policyRooms = await this.getOrCreateSourceRooms(); - const senderServer = sender.split(":")[1]; - const roomServer = roomId.split(":")[1]; - for (const room of policyRooms) { - const state = room.getUnfilteredTimelineSet().getLiveTimeline().getState(EventTimeline.FORWARDS)!; - - for (const { scope, entities } of [ - { scope: PolicyScope.Room, entities: [roomId] }, - { scope: PolicyScope.User, entities: [sender] }, - { scope: PolicyScope.Server, entities: [senderServer, roomServer] }, - ]) { - const events = state.getStateEvents(scope); - for (const event of events) { - const content = event.getContent(); - if (content?.recommendation != PolicyRecommendation.Ban) { - // Ignoring invites only looks at `m.ban` recommendations. - continue; - } - const glob = content?.entity; - if (!glob) { - // Invalid event. - continue; - } - let regexp: RegExp; - try { - regexp = new RegExp(globToRegexp(glob, false)); - } catch (ex) { - // Assume invalid event. - continue; - } - for (const entity of entities) { - if (entity && regexp.test(entity)) { - return event; - } - } - // No match. - } - } - } - return null; - } - - /** - * Get the target room, i.e. the room in which any new rule should be written. - * - * If there is no target room setup, a target room is created. - * - * Note: This method is public for testing reasons. Most clients should not need - * to call it directly. - * - * # Safety - * - * This method will rewrite the `Policies` object in the user's account data. - * This rewrite is inherently racy and could overwrite or be overwritten by - * other concurrent rewrites of the same object. - */ - public async getOrCreateTargetRoom(): Promise<Room> { - const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies(); - let target = ignoreInvitesPolicies.target; - // Validate `target`. If it is invalid, trash out the current `target` - // and create a new room. - if (typeof target !== "string") { - target = null; - } - if (target) { - // Check that the room exists and is valid. - const room = this.client.getRoom(target); - if (room) { - return room; - } else { - target = null; - } - } - // We need to create our own policy room for ignoring invites. - target = ( - await this.client.createRoom({ - name: "Individual Policy Room", - preset: Preset.PrivateChat, - }) - ).room_id; - await this.withIgnoreInvitesPolicies((ignoreInvitesPolicies) => { - ignoreInvitesPolicies.target = target; - }); - - // Since we have just called `createRoom`, `getRoom` should not be `null`. - return this.client.getRoom(target)!; - } - - /** - * Get the list of source rooms, i.e. the rooms from which rules need to be read. - * - * If no source rooms are setup, the target room is used as sole source room. - * - * Note: This method is public for testing reasons. Most clients should not need - * to call it directly. - * - * # Safety - * - * This method will rewrite the `Policies` object in the user's account data. - * This rewrite is inherently racy and could overwrite or be overwritten by - * other concurrent rewrites of the same object. - */ - public async getOrCreateSourceRooms(): Promise<Room[]> { - const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies(); - let sources: string[] = ignoreInvitesPolicies.sources; - - // Validate `sources`. If it is invalid, trash out the current `sources` - // and create a new list of sources from `target`. - let hasChanges = false; - if (!Array.isArray(sources)) { - // `sources` could not be an array. - hasChanges = true; - sources = []; - } - let sourceRooms = sources - // `sources` could contain non-string / invalid room ids - .filter((roomId) => typeof roomId === "string") - .map((roomId) => this.client.getRoom(roomId)) - .filter((room) => !!room) as Room[]; - if (sourceRooms.length != sources.length) { - hasChanges = true; - } - if (sourceRooms.length == 0) { - // `sources` could be empty (possibly because we've removed - // invalid content) - const target = await this.getOrCreateTargetRoom(); - hasChanges = true; - sourceRooms = [target]; - } - if (hasChanges) { - // Reload `policies`/`ignoreInvitesPolicies` in case it has been changed - // during or by our call to `this.getTargetRoom()`. - await this.withIgnoreInvitesPolicies((ignoreInvitesPolicies) => { - ignoreInvitesPolicies.sources = sources; - }); - } - return sourceRooms; - } - - /** - * Fetch the `IGNORE_INVITES_POLICIES` object from account data. - * - * If both an unstable prefix version and a stable prefix version are available, - * it will return the stable prefix version preferentially. - * - * The result is *not* validated but is guaranteed to be a non-null object. - * - * @returns A non-null object. - */ - private getIgnoreInvitesPolicies(): { [key: string]: any } { - return this.getPoliciesAndIgnoreInvitesPolicies().ignoreInvitesPolicies; - } - - /** - * Modify in place the `IGNORE_INVITES_POLICIES` object from account data. - */ - private async withIgnoreInvitesPolicies( - cb: (ignoreInvitesPolicies: { [key: string]: any }) => void, - ): Promise<void> { - const { policies, ignoreInvitesPolicies } = this.getPoliciesAndIgnoreInvitesPolicies(); - cb(ignoreInvitesPolicies); - policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies; - await this.client.setAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name, policies); - } - - /** - * As `getIgnoreInvitesPolicies` but also return the `POLICIES_ACCOUNT_EVENT_TYPE` - * object. - */ - private getPoliciesAndIgnoreInvitesPolicies(): { - policies: { [key: string]: any }; - ignoreInvitesPolicies: { [key: string]: any }; - } { - let policies: IContent = {}; - for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) { - if (!key) { - continue; - } - const value = this.client.getAccountData(key)?.getContent(); - if (value) { - policies = value; - break; - } - } - - let ignoreInvitesPolicies = {}; - let hasIgnoreInvitesPolicies = false; - for (const key of [IGNORE_INVITES_ACCOUNT_EVENT_KEY.name, IGNORE_INVITES_ACCOUNT_EVENT_KEY.altName]) { - if (!key) { - continue; - } - const value = policies[key]; - if (value && typeof value == "object") { - ignoreInvitesPolicies = value; - hasIgnoreInvitesPolicies = true; - break; - } - } - if (!hasIgnoreInvitesPolicies) { - policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies; - } - - return { policies, ignoreInvitesPolicies }; - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts deleted file mode 100644 index 1d4344a..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts +++ /dev/null @@ -1,268 +0,0 @@ -/* -Copyright 2023 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 { M_POLL_END, M_POLL_RESPONSE } from "../@types/polls"; -import { MatrixClient } from "../client"; -import { PollStartEvent } from "../extensible_events_v1/PollStartEvent"; -import { MatrixEvent } from "./event"; -import { Relations } from "./relations"; -import { Room } from "./room"; -import { TypedEventEmitter } from "./typed-event-emitter"; - -export enum PollEvent { - New = "Poll.new", - End = "Poll.end", - Update = "Poll.update", - Responses = "Poll.Responses", - Destroy = "Poll.Destroy", - UndecryptableRelations = "Poll.UndecryptableRelations", -} - -export type PollEventHandlerMap = { - [PollEvent.Update]: (event: MatrixEvent, poll: Poll) => void; - [PollEvent.Destroy]: (pollIdentifier: string) => void; - [PollEvent.End]: () => void; - [PollEvent.Responses]: (responses: Relations) => void; - [PollEvent.UndecryptableRelations]: (count: number) => void; -}; - -const filterResponseRelations = ( - relationEvents: MatrixEvent[], - pollEndTimestamp: number, -): { - responseEvents: MatrixEvent[]; -} => { - const responseEvents = relationEvents.filter((event) => { - if (event.isDecryptionFailure()) { - return; - } - return ( - M_POLL_RESPONSE.matches(event.getType()) && - // From MSC3381: - // "Votes sent on or before the end event's timestamp are valid votes" - event.getTs() <= pollEndTimestamp - ); - }); - - return { responseEvents }; -}; - -export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, PollEventHandlerMap> { - public readonly roomId: string; - public readonly pollEvent: PollStartEvent; - private _isFetchingResponses = false; - private relationsNextBatch: string | undefined; - private responses: null | Relations = null; - private endEvent: MatrixEvent | undefined; - /** - * Keep track of undecryptable relations - * As incomplete result sets affect poll results - */ - private undecryptableRelationEventIds = new Set<string>(); - - public constructor(public readonly rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) { - super(); - if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) { - throw new Error("Invalid poll start event."); - } - this.roomId = this.rootEvent.getRoomId()!; - this.pollEvent = this.rootEvent.unstableExtensibleEvent as unknown as PollStartEvent; - } - - public get pollId(): string { - return this.rootEvent.getId()!; - } - - public get endEventId(): string | undefined { - return this.endEvent?.getId(); - } - - public get isEnded(): boolean { - return !!this.endEvent; - } - - public get isFetchingResponses(): boolean { - return this._isFetchingResponses; - } - - public get undecryptableRelationsCount(): number { - return this.undecryptableRelationEventIds.size; - } - - public async getResponses(): Promise<Relations> { - // if we have already fetched some responses - // just return them - if (this.responses) { - return this.responses; - } - - // if there is no fetching in progress - // start fetching - if (!this.isFetchingResponses) { - await this.fetchResponses(); - } - // return whatever responses we got from the first page - return this.responses!; - } - - /** - * - * @param event - event with a relation to the rootEvent - * @returns void - */ - public onNewRelation(event: MatrixEvent): void { - if (M_POLL_END.matches(event.getType()) && this.validateEndEvent(event)) { - this.endEvent = event; - this.refilterResponsesOnEnd(); - this.emit(PollEvent.End); - } - - // wait for poll responses to be initialised - if (!this.responses) { - return; - } - - const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; - const { responseEvents } = filterResponseRelations([event], pollEndTimestamp); - - this.countUndecryptableEvents([event]); - - if (responseEvents.length) { - responseEvents.forEach((event) => { - this.responses!.addEvent(event); - }); - - this.emit(PollEvent.Responses, this.responses); - } - } - - private async fetchResponses(): Promise<void> { - this._isFetchingResponses = true; - - // we want: - // - stable and unstable M_POLL_RESPONSE - // - stable and unstable M_POLL_END - // so make one api call and filter by event type client side - const allRelations = await this.matrixClient.relations( - this.roomId, - this.rootEvent.getId()!, - "m.reference", - undefined, - { - from: this.relationsNextBatch || undefined, - }, - ); - - await Promise.all(allRelations.events.map((event) => this.matrixClient.decryptEventIfNeeded(event))); - - const responses = - this.responses || - new Relations("m.reference", M_POLL_RESPONSE.name, this.matrixClient, [M_POLL_RESPONSE.altName!]); - - const pollEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType())); - - if (this.validateEndEvent(pollEndEvent)) { - this.endEvent = pollEndEvent; - this.refilterResponsesOnEnd(); - this.emit(PollEvent.End); - } - - const pollCloseTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; - - const { responseEvents } = filterResponseRelations(allRelations.events, pollCloseTimestamp); - - responseEvents.forEach((event) => { - responses.addEvent(event); - }); - - this.relationsNextBatch = allRelations.nextBatch ?? undefined; - this.responses = responses; - this.countUndecryptableEvents(allRelations.events); - - // while there are more pages of relations - // fetch them - if (this.relationsNextBatch) { - // don't await - // we want to return the first page as soon as possible - this.fetchResponses(); - } else { - // no more pages - this._isFetchingResponses = false; - } - - // emit after updating _isFetchingResponses state - this.emit(PollEvent.Responses, this.responses); - } - - /** - * Only responses made before the poll ended are valid - * Refilter after an end event is recieved - * To ensure responses are valid - */ - private refilterResponsesOnEnd(): void { - if (!this.responses) { - return; - } - - const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; - this.responses.getRelations().forEach((event) => { - if (event.getTs() > pollEndTimestamp) { - this.responses?.removeEvent(event); - } - }); - - this.emit(PollEvent.Responses, this.responses); - } - - private countUndecryptableEvents = (events: MatrixEvent[]): void => { - const undecryptableEventIds = events - .filter((event) => event.isDecryptionFailure()) - .map((event) => event.getId()!); - - const previousCount = this.undecryptableRelationsCount; - this.undecryptableRelationEventIds = new Set([...this.undecryptableRelationEventIds, ...undecryptableEventIds]); - - if (this.undecryptableRelationsCount !== previousCount) { - this.emit(PollEvent.UndecryptableRelations, this.undecryptableRelationsCount); - } - }; - - private validateEndEvent(endEvent?: MatrixEvent): boolean { - if (!endEvent) { - return false; - } - /** - * Repeated end events are ignored - - * only the first (valid) closure event by origin_server_ts is counted. - */ - if (this.endEvent && this.endEvent.getTs() < endEvent.getTs()) { - return false; - } - - /** - * MSC3381 - * If a m.poll.end event is received from someone other than the poll creator or user with permission to redact - * others' messages in the room, the event must be ignored by clients due to being invalid. - */ - const roomCurrentState = this.room.currentState; - const endEventSender = endEvent.getSender(); - return ( - !!endEventSender && - (endEventSender === this.rootEvent.getSender() || - roomCurrentState.maySendRedactionForEvent(this.rootEvent, endEventSender)) - ); - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts deleted file mode 100644 index 5858fe5..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts +++ /dev/null @@ -1,312 +0,0 @@ -/* -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 { - CachedReceipt, - MAIN_ROOM_TIMELINE, - Receipt, - ReceiptCache, - ReceiptType, - WrappedReceipt, -} from "../@types/read_receipts"; -import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter"; -import * as utils from "../utils"; -import { MatrixEvent } from "./event"; -import { EventType } from "../@types/event"; -import { EventTimelineSet } from "./event-timeline-set"; -import { MapWithDefault } from "../utils"; -import { NotificationCountType } from "./room"; - -export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent { - return new MatrixEvent({ - content: { - [event.getId()!]: { - [receiptType]: { - [userId]: { - ts: event.getTs(), - thread_id: event.threadRootId ?? MAIN_ROOM_TIMELINE, - }, - }, - }, - }, - type: EventType.Receipt, - room_id: event.getRoomId(), - }); -} - -const ReceiptPairRealIndex = 0; -const ReceiptPairSyntheticIndex = 1; - -export abstract class ReadReceipt< - Events extends string, - Arguments extends ListenerMap<Events>, - SuperclassArguments extends ListenerMap<any> = Arguments, -> extends TypedEventEmitter<Events, Arguments, SuperclassArguments> { - // receipts should clobber based on receipt_type and user_id pairs hence - // the form of this structure. This is sub-optimal for the exposed APIs - // which pass in an event ID and get back some receipts, so we also store - // a pre-cached list for this purpose. - // Map: receipt type → user Id → receipt - private receipts = new MapWithDefault<string, Map<string, [WrappedReceipt | null, WrappedReceipt | null]>>( - () => new Map(), - ); - private receiptCacheByEventId: ReceiptCache = new Map(); - - public abstract getUnfilteredTimelineSet(): EventTimelineSet; - public abstract timeline: MatrixEvent[]; - - /** - * Gets the latest receipt for a given user in the room - * @param userId - The id of the user for which we want the receipt - * @param ignoreSynthesized - Whether to ignore synthesized receipts or not - * @param receiptType - Optional. The type of the receipt we want to get - * @returns the latest receipts of the chosen type for the chosen user - */ - public getReadReceiptForUserId( - userId: string, - ignoreSynthesized = false, - receiptType = ReceiptType.Read, - ): WrappedReceipt | null { - const [realReceipt, syntheticReceipt] = this.receipts.get(receiptType)?.get(userId) ?? [null, null]; - if (ignoreSynthesized) { - return realReceipt; - } - - return syntheticReceipt ?? realReceipt; - } - - /** - * Get the ID of the event that a given user has read up to, or null if we - * have received no read receipts from them. - * @param userId - The user ID to get read receipt event ID for - * @param ignoreSynthesized - If true, return only receipts that have been - * sent by the server, not implicit ones generated - * by the JS SDK. - * @returns ID of the latest event that the given user has read, or null. - */ - public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null { - // XXX: This is very very ugly and I hope I won't have to ever add a new - // receipt type here again. IMHO this should be done by the server in - // some more intelligent manner or the client should just use timestamps - - const timelineSet = this.getUnfilteredTimelineSet(); - const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read); - const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate); - - // If we have both, compare them - let comparison: number | null | undefined; - if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) { - comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId); - } - - // If we didn't get a comparison try to compare the ts of the receipts - if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) { - comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts; - } - - // The public receipt is more likely to drift out of date so the private - // one has precedence - if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null; - - // If public read receipt is older, return the private one - return (comparison < 0 ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null; - } - - public addReceiptToStructure( - eventId: string, - receiptType: ReceiptType, - userId: string, - receipt: Receipt, - synthetic: boolean, - ): void { - const receiptTypesMap = this.receipts.getOrCreate(receiptType); - let pair = receiptTypesMap.get(userId); - - if (!pair) { - pair = [null, null]; - receiptTypesMap.set(userId, pair); - } - - let existingReceipt = pair[ReceiptPairRealIndex]; - if (synthetic) { - existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; - } - - if (existingReceipt) { - // we only want to add this receipt if we think it is later than the one we already have. - // This is managed server-side, but because we synthesize RRs locally we have to do it here too. - const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId); - if (ordering !== null && ordering >= 0) { - return; - } - } - - const wrappedReceipt: WrappedReceipt = { - eventId, - data: receipt, - }; - - const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt; - const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex]; - - let ordering: number | null = null; - if (realReceipt && syntheticReceipt) { - ordering = this.getUnfilteredTimelineSet().compareEventOrdering( - realReceipt.eventId, - syntheticReceipt.eventId, - ); - } - - const preferSynthetic = ordering === null || ordering < 0; - - // we don't bother caching just real receipts by event ID as there's nothing that would read it. - // Take the current cached receipt before we overwrite the pair elements. - const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; - - if (synthetic && preferSynthetic) { - pair[ReceiptPairSyntheticIndex] = wrappedReceipt; - } else if (!synthetic) { - pair[ReceiptPairRealIndex] = wrappedReceipt; - - if (!preferSynthetic) { - pair[ReceiptPairSyntheticIndex] = null; - } - } - - const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; - if (cachedReceipt === newCachedReceipt) return; - - // clean up any previous cache entry - if (cachedReceipt && this.receiptCacheByEventId.get(cachedReceipt.eventId)) { - const previousEventId = cachedReceipt.eventId; - // Remove the receipt we're about to clobber out of existence from the cache - this.receiptCacheByEventId.set( - previousEventId, - this.receiptCacheByEventId.get(previousEventId)!.filter((r) => { - return r.type !== receiptType || r.userId !== userId; - }), - ); - - if (this.receiptCacheByEventId.get(previousEventId)!.length < 1) { - this.receiptCacheByEventId.delete(previousEventId); // clean up the cache keys - } - } - - // cache the new one - if (!this.receiptCacheByEventId.get(eventId)) { - this.receiptCacheByEventId.set(eventId, []); - } - this.receiptCacheByEventId.get(eventId)!.push({ - userId: userId, - type: receiptType as ReceiptType, - data: receipt, - }); - } - - /** - * Get a list of receipts for the given event. - * @param event - the event to get receipts for - * @returns A list of receipts with a userId, type and data keys or - * an empty list. - */ - public getReceiptsForEvent(event: MatrixEvent): CachedReceipt[] { - return this.receiptCacheByEventId.get(event.getId()!) || []; - } - - public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void; - - public abstract setUnread(type: NotificationCountType, count: number): void; - - /** - * This issue should also be addressed on synapse's side and is tracked as part - * of https://github.com/matrix-org/synapse/issues/14837 - * - * Retrieves the read receipt for the logged in user and checks if it matches - * the last event in the room and whether that event originated from the logged - * in user. - * Under those conditions we can consider the context as read. This is useful - * because we never send read receipts against our own events - * @param userId - the logged in user - */ - public fixupNotifications(userId: string): void { - const receipt = this.getReadReceiptForUserId(userId, false); - - const lastEvent = this.timeline[this.timeline.length - 1]; - if (lastEvent && receipt?.eventId === lastEvent.getId() && userId === lastEvent.getSender()) { - this.setUnread(NotificationCountType.Total, 0); - this.setUnread(NotificationCountType.Highlight, 0); - } - } - - /** - * Add a temporary local-echo receipt to the room to reflect in the - * client the fact that we've sent one. - * @param userId - The user ID if the receipt sender - * @param e - The event that is to be acknowledged - * @param receiptType - The type of receipt - */ - public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void { - this.addReceipt(synthesizeReceipt(userId, e, receiptType), true); - } - - /** - * Get a list of user IDs who have <b>read up to</b> the given event. - * @param event - the event to get read receipts for. - * @returns A list of user IDs. - */ - public getUsersReadUpTo(event: MatrixEvent): string[] { - return this.getReceiptsForEvent(event) - .filter(function (receipt) { - return utils.isSupportedReceiptType(receipt.type); - }) - .map(function (receipt) { - return receipt.userId; - }); - } - - /** - * Determines if the given user has read a particular event ID with the known - * history of the room. This is not a definitive check as it relies only on - * what is available to the room at the time of execution. - * @param userId - The user ID to check the read state of. - * @param eventId - The event ID to check if the user read. - * @returns True if the user has read the event, false otherwise. - */ - public hasUserReadEvent(userId: string, eventId: string): boolean { - const readUpToId = this.getEventReadUpTo(userId, false); - if (readUpToId === eventId) return true; - - if ( - this.timeline?.length && - this.timeline[this.timeline.length - 1].getSender() && - this.timeline[this.timeline.length - 1].getSender() === userId - ) { - // It doesn't matter where the event is in the timeline, the user has read - // it because they've sent the latest event. - return true; - } - - for (let i = this.timeline?.length - 1; i >= 0; --i) { - const ev = this.timeline[i]; - - // If we encounter the target event first, the user hasn't read it - // however if we encounter the readUpToId first then the user has read - // it. These rules apply because we're iterating bottom-up. - if (ev.getId() === eventId) return false; - if (ev.getId() === readUpToId) return true; - } - - // We don't know if the user has read it, so assume not. - return false; - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts deleted file mode 100644 index a005169..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* -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 { Relations, RelationsEvent, EventHandlerMap } from "./relations"; -import { MatrixEvent } from "./event"; -import { Listener } from "./typed-event-emitter"; - -export class RelatedRelations { - private relations: Relations[]; - - public constructor(relations: Relations[]) { - this.relations = relations.filter((r) => !!r); - } - - public getRelations(): MatrixEvent[] { - return this.relations.reduce<MatrixEvent[]>((c, p) => [...c, ...p.getRelations()], []); - } - - public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>): void { - this.relations.forEach((r) => r.on(ev, fn)); - } - - public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>): void { - this.relations.forEach((r) => r.off(ev, fn)); - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts deleted file mode 100644 index d328b1c..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* -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 { Relations } from "./relations"; -import { EventType, RelationType } from "../@types/event"; -import { EventStatus, MatrixEvent, MatrixEventEvent } from "./event"; -import { EventTimelineSet } from "./event-timeline-set"; -import { MatrixClient } from "../client"; -import { Room } from "./room"; - -export class RelationsContainer { - // A tree of objects to access a set of related children for an event, as in: - // this.relations.get(parentEventId).get(relationType).get(relationEventType) - private relations = new Map<string, Map<RelationType | string, Map<EventType | string, Relations>>>(); - - public constructor(private readonly client: MatrixClient, private readonly room?: Room) {} - - /** - * Get a collection of child events to a given event in this timeline set. - * - * @param eventId - The ID of the event that you'd like to access child events for. - * For example, with annotations, this would be the ID of the event being annotated. - * @param relationType - The type of relationship involved, such as "m.annotation", "m.reference", "m.replace", etc. - * @param eventType - The relation event's type, such as "m.reaction", etc. - * @throws If `eventId</code>, <code>relationType</code> or <code>eventType` - * are not valid. - * - * @returns - * A container for relation events or undefined if there are no relation events for - * the relationType. - */ - public getChildEventsForEvent( - eventId: string, - relationType: RelationType | string, - eventType: EventType | string, - ): Relations | undefined { - return this.relations.get(eventId)?.get(relationType)?.get(eventType); - } - - public getAllChildEventsForEvent(parentEventId: string): MatrixEvent[] { - const relationsForEvent = - this.relations.get(parentEventId) ?? new Map<RelationType | string, Map<EventType | string, Relations>>(); - const events: MatrixEvent[] = []; - for (const relationsRecord of relationsForEvent.values()) { - for (const relations of relationsRecord.values()) { - events.push(...relations.getRelations()); - } - } - return events; - } - - /** - * Set an event as the target event if any Relations exist for it already. - * Child events can point to other child events as their parent, so this method may be - * called for events which are also logically child events. - * - * @param event - The event to check as relation target. - */ - public aggregateParentEvent(event: MatrixEvent): void { - const relationsForEvent = this.relations.get(event.getId()!); - if (!relationsForEvent) return; - - for (const relationsWithRelType of relationsForEvent.values()) { - for (const relationsWithEventType of relationsWithRelType.values()) { - relationsWithEventType.setTargetEvent(event); - } - } - } - - /** - * Add relation events to the relevant relation collection. - * - * @param event - The new child event to be aggregated. - * @param timelineSet - The event timeline set within which to search for the related event if any. - */ - public aggregateChildEvent(event: MatrixEvent, timelineSet?: EventTimelineSet): void { - if (event.isRedacted() || event.status === EventStatus.CANCELLED) { - return; - } - - const relation = event.getRelation(); - if (!relation) return; - - const onEventDecrypted = (): void => { - if (event.isDecryptionFailure()) { - // This could for example happen if the encryption keys are not yet available. - // The event may still be decrypted later. Register the listener again. - event.once(MatrixEventEvent.Decrypted, onEventDecrypted); - return; - } - - this.aggregateChildEvent(event, timelineSet); - }; - - // If the event is currently encrypted, wait until it has been decrypted. - if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { - event.once(MatrixEventEvent.Decrypted, onEventDecrypted); - return; - } - - const { event_id: relatesToEventId, rel_type: relationType } = relation; - const eventType = event.getType(); - - let relationsForEvent = this.relations.get(relatesToEventId!); - if (!relationsForEvent) { - relationsForEvent = new Map<RelationType | string, Map<EventType | string, Relations>>(); - this.relations.set(relatesToEventId!, relationsForEvent); - } - - let relationsWithRelType = relationsForEvent.get(relationType!); - if (!relationsWithRelType) { - relationsWithRelType = new Map<EventType | string, Relations>(); - relationsForEvent.set(relationType!, relationsWithRelType); - } - - let relationsWithEventType = relationsWithRelType.get(eventType); - if (!relationsWithEventType) { - relationsWithEventType = new Relations(relationType!, eventType, this.client); - relationsWithRelType.set(eventType, relationsWithEventType); - - const room = this.room ?? timelineSet?.room; - const relatesToEvent = - timelineSet?.findEventById(relatesToEventId!) ?? - room?.findEventById(relatesToEventId!) ?? - room?.getPendingEvent(relatesToEventId!); - if (relatesToEvent) { - relationsWithEventType.setTargetEvent(relatesToEvent); - } - } - - relationsWithEventType.addEvent(event); - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts deleted file mode 100644 index d2b637c..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts +++ /dev/null @@ -1,368 +0,0 @@ -/* -Copyright 2019, 2021, 2023 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 { EventStatus, IAggregatedRelation, MatrixEvent, MatrixEventEvent } from "./event"; -import { logger } from "../logger"; -import { RelationType } from "../@types/event"; -import { TypedEventEmitter } from "./typed-event-emitter"; -import { MatrixClient } from "../client"; -import { Room } from "./room"; - -export enum RelationsEvent { - Add = "Relations.add", - Remove = "Relations.remove", - Redaction = "Relations.redaction", -} - -export type EventHandlerMap = { - [RelationsEvent.Add]: (event: MatrixEvent) => void; - [RelationsEvent.Remove]: (event: MatrixEvent) => void; - [RelationsEvent.Redaction]: (event: MatrixEvent) => void; -}; - -const matchesEventType = (eventType: string, targetEventType: string, altTargetEventTypes: string[] = []): boolean => - [targetEventType, ...altTargetEventTypes].includes(eventType); - -/** - * A container for relation events that supports easy access to common ways of - * aggregating such events. Each instance holds events that of a single relation - * type and event type. All of the events also relate to the same original event. - * - * The typical way to get one of these containers is via - * EventTimelineSet#getRelationsForEvent. - */ -export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap> { - private relationEventIds = new Set<string>(); - private relations = new Set<MatrixEvent>(); - private annotationsByKey: Record<string, Set<MatrixEvent>> = {}; - private annotationsBySender: Record<string, Set<MatrixEvent>> = {}; - private sortedAnnotationsByKey: [string, Set<MatrixEvent>][] = []; - private targetEvent: MatrixEvent | null = null; - private creationEmitted = false; - private readonly client: MatrixClient; - - /** - * @param relationType - The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc. - * @param eventType - The relation event's type, such as "m.reaction", etc. - * @param client - The client which created this instance. For backwards compatibility also accepts a Room. - * @param altEventTypes - alt event types for relation events, for example to support unstable prefixed event types - */ - public constructor( - public readonly relationType: RelationType | string, - public readonly eventType: string, - client: MatrixClient | Room, - public readonly altEventTypes?: string[], - ) { - super(); - this.client = client instanceof Room ? client.client : client; - } - - /** - * Add relation events to this collection. - * - * @param event - The new relation event to be added. - */ - public async addEvent(event: MatrixEvent): Promise<void> { - if (this.relationEventIds.has(event.getId()!)) { - return; - } - - const relation = event.getRelation(); - if (!relation) { - logger.error("Event must have relation info"); - return; - } - - const relationType = relation.rel_type; - const eventType = event.getType(); - - if (this.relationType !== relationType || !matchesEventType(eventType, this.eventType, this.altEventTypes)) { - logger.error("Event relation info doesn't match this container"); - return; - } - - // If the event is in the process of being sent, listen for cancellation - // so we can remove the event from the collection. - if (event.isSending()) { - event.on(MatrixEventEvent.Status, this.onEventStatus); - } - - this.relations.add(event); - this.relationEventIds.add(event.getId()!); - - if (this.relationType === RelationType.Annotation) { - this.addAnnotationToAggregation(event); - } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) { - const lastReplacement = await this.getLastReplacement(); - this.targetEvent.makeReplaced(lastReplacement!); - } - - event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - - this.emit(RelationsEvent.Add, event); - - this.maybeEmitCreated(); - } - - /** - * Remove relation event from this collection. - * - * @param event - The relation event to remove. - */ - public async removeEvent(event: MatrixEvent): Promise<void> { - if (!this.relations.has(event)) { - return; - } - - this.relations.delete(event); - - if (this.relationType === RelationType.Annotation) { - this.removeAnnotationFromAggregation(event); - } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) { - const lastReplacement = await this.getLastReplacement(); - this.targetEvent.makeReplaced(lastReplacement!); - } - - this.emit(RelationsEvent.Remove, event); - } - - /** - * Listens for event status changes to remove cancelled events. - * - * @param event - The event whose status has changed - * @param status - The new status - */ - private onEventStatus = (event: MatrixEvent, status: EventStatus | null): void => { - if (!event.isSending()) { - // Sending is done, so we don't need to listen anymore - event.removeListener(MatrixEventEvent.Status, this.onEventStatus); - return; - } - if (status !== EventStatus.CANCELLED) { - return; - } - // Event was cancelled, remove from the collection - event.removeListener(MatrixEventEvent.Status, this.onEventStatus); - this.removeEvent(event); - }; - - /** - * Get all relation events in this collection. - * - * These are currently in the order of insertion to this collection, which - * won't match timeline order in the case of scrollback. - * TODO: Tweak `addEvent` to insert correctly for scrollback. - * - * Relation events in insertion order. - */ - public getRelations(): MatrixEvent[] { - return [...this.relations]; - } - - private addAnnotationToAggregation(event: MatrixEvent): void { - const { key } = event.getRelation() ?? {}; - if (!key) return; - - let eventsForKey = this.annotationsByKey[key]; - if (!eventsForKey) { - eventsForKey = this.annotationsByKey[key] = new Set(); - this.sortedAnnotationsByKey.push([key, eventsForKey]); - } - // Add the new event to the set for this key - eventsForKey.add(event); - // Re-sort the [key, events] pairs in descending order of event count - this.sortedAnnotationsByKey.sort((a, b) => { - const aEvents = a[1]; - const bEvents = b[1]; - return bEvents.size - aEvents.size; - }); - - const sender = event.getSender()!; - let eventsFromSender = this.annotationsBySender[sender]; - if (!eventsFromSender) { - eventsFromSender = this.annotationsBySender[sender] = new Set(); - } - // Add the new event to the set for this sender - eventsFromSender.add(event); - } - - private removeAnnotationFromAggregation(event: MatrixEvent): void { - const { key } = event.getRelation() ?? {}; - if (!key) return; - - const eventsForKey = this.annotationsByKey[key]; - if (eventsForKey) { - eventsForKey.delete(event); - - // Re-sort the [key, events] pairs in descending order of event count - this.sortedAnnotationsByKey.sort((a, b) => { - const aEvents = a[1]; - const bEvents = b[1]; - return bEvents.size - aEvents.size; - }); - } - - const sender = event.getSender()!; - const eventsFromSender = this.annotationsBySender[sender]; - if (eventsFromSender) { - eventsFromSender.delete(event); - } - } - - /** - * For relations that have been redacted, we want to remove them from - * aggregation data sets and emit an update event. - * - * To do so, we listen for `Event.beforeRedaction`, which happens: - * - after the server accepted the redaction and remote echoed back to us - * - before the original event has been marked redacted in the client - * - * @param redactedEvent - The original relation event that is about to be redacted. - */ - private onBeforeRedaction = async (redactedEvent: MatrixEvent): Promise<void> => { - if (!this.relations.has(redactedEvent)) { - return; - } - - this.relations.delete(redactedEvent); - - if (this.relationType === RelationType.Annotation) { - // Remove the redacted annotation from aggregation by key - this.removeAnnotationFromAggregation(redactedEvent); - } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) { - const lastReplacement = await this.getLastReplacement(); - this.targetEvent.makeReplaced(lastReplacement!); - } - - redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - - this.emit(RelationsEvent.Redaction, redactedEvent); - }; - - /** - * Get all events in this collection grouped by key and sorted by descending - * event count in each group. - * - * This is currently only supported for the annotation relation type. - * - * An array of [key, events] pairs sorted by descending event count. - * The events are stored in a Set (which preserves insertion order). - */ - public getSortedAnnotationsByKey(): [string, Set<MatrixEvent>][] | null { - if (this.relationType !== RelationType.Annotation) { - // Other relation types are not grouped currently. - return null; - } - - return this.sortedAnnotationsByKey; - } - - /** - * Get all events in this collection grouped by sender. - * - * This is currently only supported for the annotation relation type. - * - * An object with each relation sender as a key and the matching Set of - * events for that sender as a value. - */ - public getAnnotationsBySender(): Record<string, Set<MatrixEvent>> | null { - if (this.relationType !== RelationType.Annotation) { - // Other relation types are not grouped currently. - return null; - } - - return this.annotationsBySender; - } - - /** - * Returns the most recent (and allowed) m.replace relation, if any. - * - * This is currently only supported for the m.replace relation type, - * once the target event is known, see `addEvent`. - */ - public async getLastReplacement(): Promise<MatrixEvent | null> { - if (this.relationType !== RelationType.Replace) { - // Aggregating on last only makes sense for this relation type - return null; - } - if (!this.targetEvent) { - // Don't know which replacements to accept yet. - // This method shouldn't be called before the original - // event is known anyway. - return null; - } - - // the all-knowning server tells us that the event at some point had - // this timestamp for its replacement, so any following replacement should definitely not be less - const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace); - const minTs = replaceRelation?.origin_server_ts; - - const lastReplacement = this.getRelations().reduce<MatrixEvent | null>((last, event) => { - if (event.getSender() !== this.targetEvent!.getSender()) { - return last; - } - if (minTs && minTs > event.getTs()) { - return last; - } - if (last && last.getTs() > event.getTs()) { - return last; - } - return event; - }, null); - - if (lastReplacement?.shouldAttemptDecryption() && this.client.isCryptoEnabled()) { - await lastReplacement.attemptDecryption(this.client.crypto!); - } else if (lastReplacement?.isBeingDecrypted()) { - await lastReplacement.getDecryptionPromise(); - } - - return lastReplacement; - } - - /* - * @param targetEvent - the event the relations are related to. - */ - public async setTargetEvent(event: MatrixEvent): Promise<void> { - if (this.targetEvent) { - return; - } - this.targetEvent = event; - - if (this.relationType === RelationType.Replace && !this.targetEvent.isState()) { - const replacement = await this.getLastReplacement(); - // this is the initial update, so only call it if we already have something - // to not emit Event.replaced needlessly - if (replacement) { - this.targetEvent.makeReplaced(replacement); - } - } - - this.maybeEmitCreated(); - } - - private maybeEmitCreated(): void { - if (this.creationEmitted) { - return; - } - // Only emit we're "created" once we have a target event instance _and_ - // at least one related event. - if (!this.targetEvent || !this.relations.size) { - return; - } - this.creationEmitted = true; - this.targetEvent.emit(MatrixEventEvent.RelationsCreated, this.relationType, this.eventType); - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts deleted file mode 100644 index 116a93b..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts +++ /dev/null @@ -1,453 +0,0 @@ -/* -Copyright 2015 - 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 { getHttpUriForMxc } from "../content-repo"; -import * as utils from "../utils"; -import { User } from "./user"; -import { MatrixEvent } from "./event"; -import { RoomState } from "./room-state"; -import { logger } from "../logger"; -import { TypedEventEmitter } from "./typed-event-emitter"; -import { EventType } from "../@types/event"; - -export enum RoomMemberEvent { - Membership = "RoomMember.membership", - Name = "RoomMember.name", - PowerLevel = "RoomMember.powerLevel", - Typing = "RoomMember.typing", -} - -export type RoomMemberEventHandlerMap = { - /** - * Fires whenever any room member's membership state changes. - * @param event - The matrix event which caused this event to fire. - * @param member - The member whose RoomMember.membership changed. - * @param oldMembership - The previous membership state. Null if it's a new member. - * @example - * ``` - * matrixClient.on("RoomMember.membership", function(event, member, oldMembership){ - * var newState = member.membership; - * }); - * ``` - */ - [RoomMemberEvent.Membership]: (event: MatrixEvent, member: RoomMember, oldMembership?: string) => void; - /** - * Fires whenever any room member's name changes. - * @param event - The matrix event which caused this event to fire. - * @param member - The member whose RoomMember.name changed. - * @param oldName - The previous name. Null if the member didn't have a name previously. - * @example - * ``` - * matrixClient.on("RoomMember.name", function(event, member){ - * var newName = member.name; - * }); - * ``` - */ - [RoomMemberEvent.Name]: (event: MatrixEvent, member: RoomMember, oldName: string | null) => void; - /** - * Fires whenever any room member's power level changes. - * @param event - The matrix event which caused this event to fire. - * @param member - The member whose RoomMember.powerLevel changed. - * @example - * ``` - * matrixClient.on("RoomMember.powerLevel", function(event, member){ - * var newPowerLevel = member.powerLevel; - * var newNormPowerLevel = member.powerLevelNorm; - * }); - * ``` - */ - [RoomMemberEvent.PowerLevel]: (event: MatrixEvent, member: RoomMember) => void; - /** - * Fires whenever any room member's typing state changes. - * @param event - The matrix event which caused this event to fire. - * @param member - The member whose RoomMember.typing changed. - * @example - * ``` - * matrixClient.on("RoomMember.typing", function(event, member){ - * var isTyping = member.typing; - * }); - * ``` - */ - [RoomMemberEvent.Typing]: (event: MatrixEvent, member: RoomMember) => void; -}; - -export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEventHandlerMap> { - private _isOutOfBand = false; - private modified = -1; - public requestedProfileInfo = false; // used by sync.ts - - // XXX these should be read-only - /** - * True if the room member is currently typing. - */ - public typing = false; - /** - * The human-readable name for this room member. This will be - * disambiguated with a suffix of " (\@user_id:matrix.org)" if another member shares the - * same displayname. - */ - public name: string; - /** - * The ambiguous displayname of this room member. - */ - public rawDisplayName: string; - /** - * The power level for this room member. - */ - public powerLevel = 0; - /** - * The normalised power level (0-100) for this room member. - */ - public powerLevelNorm = 0; - /** - * The User object for this room member, if one exists. - */ - public user?: User; - /** - * The membership state for this room member e.g. 'join'. - */ - public membership?: string; - /** - * True if the member's name is disambiguated. - */ - public disambiguate = false; - /** - * The events describing this RoomMember. - */ - public events: { - /** - * The m.room.member event for this RoomMember. - */ - member?: MatrixEvent; - } = {}; - - /** - * Construct a new room member. - * - * @param roomId - The room ID of the member. - * @param userId - The user ID of the member. - */ - public constructor(public readonly roomId: string, public readonly userId: string) { - super(); - - this.name = userId; - this.rawDisplayName = userId; - this.updateModifiedTime(); - } - - /** - * Mark the member as coming from a channel that is not sync - */ - public markOutOfBand(): void { - this._isOutOfBand = true; - } - - /** - * @returns does the member come from a channel that is not sync? - * This is used to store the member seperately - * from the sync state so it available across browser sessions. - */ - public isOutOfBand(): boolean { - return this._isOutOfBand; - } - - /** - * Update this room member's membership event. May fire "RoomMember.name" if - * this event updates this member's name. - * @param event - The `m.room.member` event - * @param roomState - Optional. The room state to take into account - * when calculating (e.g. for disambiguating users with the same name). - * - * @remarks - * Fires {@link RoomMemberEvent.Name} - * Fires {@link RoomMemberEvent.Membership} - */ - public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void { - const displayName = event.getDirectionalContent().displayname ?? ""; - - if (event.getType() !== EventType.RoomMember) { - return; - } - - this._isOutOfBand = false; - - this.events.member = event; - - const oldMembership = this.membership; - this.membership = event.getDirectionalContent().membership; - if (this.membership === undefined) { - // logging to diagnose https://github.com/vector-im/element-web/issues/20962 - // (logs event content, although only of membership events) - logger.trace( - `membership event with membership undefined (forwardLooking: ${event.forwardLooking})!`, - event.getContent(), - `prevcontent is `, - event.getPrevContent(), - ); - } - - this.disambiguate = shouldDisambiguate(this.userId, displayName, roomState); - - const oldName = this.name; - this.name = calculateDisplayName(this.userId, displayName, this.disambiguate); - - // not quite raw: we strip direction override chars so it can safely be inserted into - // blocks of text without breaking the text direction - this.rawDisplayName = utils.removeDirectionOverrideChars(event.getDirectionalContent().displayname ?? ""); - if (!this.rawDisplayName || !utils.removeHiddenChars(this.rawDisplayName)) { - this.rawDisplayName = this.userId; - } - - if (oldMembership !== this.membership) { - this.updateModifiedTime(); - this.emit(RoomMemberEvent.Membership, event, this, oldMembership); - } - if (oldName !== this.name) { - this.updateModifiedTime(); - this.emit(RoomMemberEvent.Name, event, this, oldName); - } - } - - /** - * Update this room member's power level event. May fire - * "RoomMember.powerLevel" if this event updates this member's power levels. - * @param powerLevelEvent - The `m.room.power_levels` event - * - * @remarks - * Fires {@link RoomMemberEvent.PowerLevel} - */ - public setPowerLevelEvent(powerLevelEvent: MatrixEvent): void { - if (powerLevelEvent.getType() !== EventType.RoomPowerLevels || powerLevelEvent.getStateKey() !== "") { - return; - } - - const evContent = powerLevelEvent.getDirectionalContent(); - - let maxLevel = evContent.users_default || 0; - const users: { [userId: string]: number } = evContent.users || {}; - Object.values(users).forEach((lvl: number) => { - maxLevel = Math.max(maxLevel, lvl); - }); - const oldPowerLevel = this.powerLevel; - const oldPowerLevelNorm = this.powerLevelNorm; - - if (users[this.userId] !== undefined && Number.isInteger(users[this.userId])) { - this.powerLevel = users[this.userId]; - } else if (evContent.users_default !== undefined) { - this.powerLevel = evContent.users_default; - } else { - this.powerLevel = 0; - } - this.powerLevelNorm = 0; - if (maxLevel > 0) { - this.powerLevelNorm = (this.powerLevel * 100) / maxLevel; - } - - // emit for changes in powerLevelNorm as well (since the app will need to - // redraw everyone's level if the max has changed) - if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) { - this.updateModifiedTime(); - this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this); - } - } - - /** - * Update this room member's typing event. May fire "RoomMember.typing" if - * this event changes this member's typing state. - * @param event - The typing event - * - * @remarks - * Fires {@link RoomMemberEvent.Typing} - */ - public setTypingEvent(event: MatrixEvent): void { - if (event.getType() !== "m.typing") { - return; - } - const oldTyping = this.typing; - this.typing = false; - const typingList = event.getContent().user_ids; - if (!Array.isArray(typingList)) { - // malformed event :/ bail early. TODO: whine? - return; - } - if (typingList.indexOf(this.userId) !== -1) { - this.typing = true; - } - if (oldTyping !== this.typing) { - this.updateModifiedTime(); - this.emit(RoomMemberEvent.Typing, event, this); - } - } - - /** - * Update the last modified time to the current time. - */ - private updateModifiedTime(): void { - this.modified = Date.now(); - } - - /** - * Get the timestamp when this RoomMember was last updated. This timestamp is - * updated when properties on this RoomMember are updated. - * It is updated <i>before</i> firing events. - * @returns The timestamp - */ - public getLastModifiedTime(): number { - return this.modified; - } - - public isKicked(): boolean { - return ( - this.membership === "leave" && - this.events.member !== undefined && - this.events.member.getSender() !== this.events.member.getStateKey() - ); - } - - /** - * If this member was invited with the is_direct flag set, return - * the user that invited this member - * @returns user id of the inviter - */ - public getDMInviter(): string | undefined { - // when not available because that room state hasn't been loaded in, - // we don't really know, but more likely to not be a direct chat - if (this.events.member) { - // TODO: persist the is_direct flag on the member as more member events - // come in caused by displayName changes. - - // the is_direct flag is set on the invite member event. - // This is copied on the prev_content section of the join member event - // when the invite is accepted. - - const memberEvent = this.events.member; - let memberContent = memberEvent.getContent(); - let inviteSender: string | undefined = memberEvent.getSender(); - - if (memberContent.membership === "join") { - memberContent = memberEvent.getPrevContent(); - inviteSender = memberEvent.getUnsigned().prev_sender; - } - - if (memberContent.membership === "invite" && memberContent.is_direct) { - return inviteSender; - } - } - } - - /** - * Get the avatar URL for a room member. - * @param baseUrl - The base homeserver URL See - * {@link MatrixClient#getHomeserverUrl}. - * @param width - The desired width of the thumbnail. - * @param height - The desired height of the thumbnail. - * @param resizeMethod - The thumbnail resize method to use, either - * "crop" or "scale". - * @param allowDefault - (optional) Passing false causes this method to - * return null if the user has no avatar image. Otherwise, a default image URL - * will be returned. Default: true. (Deprecated) - * @param allowDirectLinks - (optional) If true, the avatar URL will be - * returned even if it is a direct hyperlink rather than a matrix content URL. - * If false, any non-matrix content URLs will be ignored. Setting this option to - * true will expose URLs that, if fetched, will leak information about the user - * to anyone who they share a room with. - * @returns the avatar URL or null. - */ - public getAvatarUrl( - baseUrl: string, - width: number, - height: number, - resizeMethod: string, - allowDefault = true, - allowDirectLinks: boolean, - ): string | null { - const rawUrl = this.getMxcAvatarUrl(); - - if (!rawUrl && !allowDefault) { - return null; - } - const httpUrl = getHttpUriForMxc(baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks); - if (httpUrl) { - return httpUrl; - } - return null; - } - - /** - * get the mxc avatar url, either from a state event, or from a lazily loaded member - * @returns the mxc avatar url - */ - public getMxcAvatarUrl(): string | undefined { - if (this.events.member) { - return this.events.member.getDirectionalContent().avatar_url; - } else if (this.user) { - return this.user.avatarUrl; - } - } -} - -const MXID_PATTERN = /@.+:.+/; -const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/; - -function shouldDisambiguate(selfUserId: string, displayName?: string, roomState?: RoomState): boolean { - if (!displayName || displayName === selfUserId) return false; - - // First check if the displayname is something we consider truthy - // after stripping it of zero width characters and padding spaces - if (!utils.removeHiddenChars(displayName)) return false; - - if (!roomState) return false; - - // Next check if the name contains something that look like a mxid - // If it does, it may be someone trying to impersonate someone else - // Show full mxid in this case - if (MXID_PATTERN.test(displayName)) return true; - - // Also show mxid if the display name contains any LTR/RTL characters as these - // make it very difficult for us to find similar *looking* display names - // E.g "Mark" could be cloned by writing "kraM" but in RTL. - if (LTR_RTL_PATTERN.test(displayName)) return true; - - // Also show mxid if there are other people with the same or similar - // displayname, after hidden character removal. - const userIds = roomState.getUserIdsWithDisplayName(displayName); - if (userIds.some((u) => u !== selfUserId)) return true; - - return false; -} - -function calculateDisplayName(selfUserId: string, displayName: string | undefined, disambiguate: boolean): string { - if (!displayName || displayName === selfUserId) return selfUserId; - - if (disambiguate) return utils.removeDirectionOverrideChars(displayName) + " (" + selfUserId + ")"; - - // First check if the displayname is something we consider truthy - // after stripping it of zero width characters and padding spaces - if (!utils.removeHiddenChars(displayName)) return selfUserId; - - // We always strip the direction override characters (LRO and RLO). - // These override the text direction for all subsequent characters - // in the paragraph so if display names contained these, they'd - // need to be wrapped in something to prevent this from leaking out - // (which we can do in HTML but not text) or we'd need to add - // control characters to the string to reset any overrides (eg. - // adding PDF characters at the end). As far as we can see, - // there should be no reason these would be necessary - rtl display - // names should flip into the correct direction automatically based on - // the characters, and you can still embed rtl in ltr or vice versa - // with the embed chars or marker chars. - return utils.removeDirectionOverrideChars(displayName); -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts deleted file mode 100644 index f975b9c..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts +++ /dev/null @@ -1,1081 +0,0 @@ -/* -Copyright 2015 - 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 { RoomMember } from "./room-member"; -import { logger } from "../logger"; -import * as utils from "../utils"; -import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event"; -import { IEvent, MatrixEvent, MatrixEventEvent } from "./event"; -import { MatrixClient } from "../client"; -import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials"; -import { TypedEventEmitter } from "./typed-event-emitter"; -import { Beacon, BeaconEvent, BeaconEventHandlerMap, getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon"; -import { TypedReEmitter } from "../ReEmitter"; -import { M_BEACON, M_BEACON_INFO } from "../@types/beacon"; - -export interface IMarkerFoundOptions { - /** Whether the timeline was empty before the marker event arrived in the - * room. This could be happen in a variety of cases: - * 1. From the initial sync - * 2. It's the first state we're seeing after joining the room - * 3. Or whether it's coming from `syncFromCache` - * - * A marker event refers to `UNSTABLE_MSC2716_MARKER` and indicates that - * history was imported somewhere back in time. It specifically points to an - * MSC2716 insertion event where the history was imported at. Marker events - * are sent as state events so they are easily discoverable by clients and - * homeservers and don't get lost in timeline gaps. - */ - timelineWasEmpty?: boolean; -} - -// possible statuses for out-of-band member loading -enum OobStatus { - NotStarted, - InProgress, - Finished, -} - -export interface IPowerLevelsContent { - users?: Record<string, number>; - events?: Record<string, number>; - // eslint-disable-next-line camelcase - users_default?: number; - // eslint-disable-next-line camelcase - events_default?: number; - // eslint-disable-next-line camelcase - state_default?: number; - ban?: number; - kick?: number; - redact?: number; -} - -export enum RoomStateEvent { - Events = "RoomState.events", - Members = "RoomState.members", - NewMember = "RoomState.newMember", - Update = "RoomState.update", // signals batches of updates without specificity - BeaconLiveness = "RoomState.BeaconLiveness", - Marker = "RoomState.Marker", -} - -export type RoomStateEventHandlerMap = { - /** - * Fires whenever the event dictionary in room state is updated. - * @param event - The matrix event which caused this event to fire. - * @param state - The room state whose RoomState.events dictionary - * was updated. - * @param prevEvent - The event being replaced by the new state, if - * known. Note that this can differ from `getPrevContent()` on the new state event - * as this is the store's view of the last state, not the previous state provided - * by the server. - * @example - * ``` - * matrixClient.on("RoomState.events", function(event, state, prevEvent){ - * var newStateEvent = event; - * }); - * ``` - */ - [RoomStateEvent.Events]: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void; - /** - * Fires whenever a member in the members dictionary is updated in any way. - * @param event - The matrix event which caused this event to fire. - * @param state - The room state whose RoomState.members dictionary - * was updated. - * @param member - The room member that was updated. - * @example - * ``` - * matrixClient.on("RoomState.members", function(event, state, member){ - * var newMembershipState = member.membership; - * }); - * ``` - */ - [RoomStateEvent.Members]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void; - /** - * Fires whenever a member is added to the members dictionary. The RoomMember - * will not be fully populated yet (e.g. no membership state) but will already - * be available in the members dictionary. - * @param event - The matrix event which caused this event to fire. - * @param state - The room state whose RoomState.members dictionary - * was updated with a new entry. - * @param member - The room member that was added. - * @example - * ``` - * matrixClient.on("RoomState.newMember", function(event, state, member){ - * // add event listeners on 'member' - * }); - * ``` - */ - [RoomStateEvent.NewMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void; - [RoomStateEvent.Update]: (state: RoomState) => void; - [RoomStateEvent.BeaconLiveness]: (state: RoomState, hasLiveBeacons: boolean) => void; - [RoomStateEvent.Marker]: (event: MatrixEvent, setStateOptions?: IMarkerFoundOptions) => void; - [BeaconEvent.New]: (event: MatrixEvent, beacon: Beacon) => void; -}; - -type EmittedEvents = RoomStateEvent | BeaconEvent; -type EventHandlerMap = RoomStateEventHandlerMap & BeaconEventHandlerMap; - -export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap> { - public readonly reEmitter = new TypedReEmitter<EmittedEvents, EventHandlerMap>(this); - private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember - // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys) - private displayNameToUserIds = new Map<string, string[]>(); - private userIdsToDisplayNames: Record<string, string> = {}; - private tokenToInvite: Record<string, MatrixEvent> = {}; // 3pid invite state_key to m.room.member invite - private joinedMemberCount: number | null = null; // cache of the number of joined members - // joined members count from summary api - // once set, we know the server supports the summary api - // and we should only trust that - // we could also only trust that before OOB members - // are loaded but doesn't seem worth the hassle atm - private summaryJoinedMemberCount: number | null = null; - // same for invited member count - private invitedMemberCount: number | null = null; - private summaryInvitedMemberCount: number | null = null; - private modified = -1; - - // XXX: Should be read-only - // The room member dictionary, keyed on the user's ID. - public members: Record<string, RoomMember> = {}; // userId: RoomMember - // The state events dictionary, keyed on the event type and then the state_key value. - public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>> - // The pagination token for this state. - public paginationToken: string | null = null; - - public readonly beacons = new Map<BeaconIdentifier, Beacon>(); - private _liveBeaconIds: BeaconIdentifier[] = []; - - /** - * Construct room state. - * - * Room State represents the state of the room at a given point. - * It can be mutated by adding state events to it. - * There are two types of room member associated with a state event: - * normal member objects (accessed via getMember/getMembers) which mutate - * with the state to represent the current state of that room/user, e.g. - * the object returned by `getMember('@bob:example.com')` will mutate to - * get a different display name if Bob later changes his display name - * in the room. - * There are also 'sentinel' members (accessed via getSentinelMember). - * These also represent the state of room members at the point in time - * represented by the RoomState object, but unlike objects from getMember, - * sentinel objects will always represent the room state as at the time - * getSentinelMember was called, so if Bob subsequently changes his display - * name, a room member object previously acquired with getSentinelMember - * will still have his old display name. Calling getSentinelMember again - * after the display name change will return a new RoomMember object - * with Bob's new display name. - * - * @param roomId - Optional. The ID of the room which has this state. - * If none is specified it just tracks paginationTokens, useful for notifTimelineSet - * @param oobMemberFlags - Optional. The state of loading out of bound members. - * As the timeline might get reset while they are loading, this state needs to be inherited - * and shared when the room state is cloned for the new timeline. - * This should only be passed from clone. - */ - public constructor(public readonly roomId: string, private oobMemberFlags = { status: OobStatus.NotStarted }) { - super(); - this.updateModifiedTime(); - } - - /** - * Returns the number of joined members in this room - * This method caches the result. - * @returns The number of members in this room whose membership is 'join' - */ - public getJoinedMemberCount(): number { - if (this.summaryJoinedMemberCount !== null) { - return this.summaryJoinedMemberCount; - } - if (this.joinedMemberCount === null) { - this.joinedMemberCount = this.getMembers().reduce((count, m) => { - return m.membership === "join" ? count + 1 : count; - }, 0); - } - return this.joinedMemberCount; - } - - /** - * Set the joined member count explicitly (like from summary part of the sync response) - * @param count - the amount of joined members - */ - public setJoinedMemberCount(count: number): void { - this.summaryJoinedMemberCount = count; - } - - /** - * Returns the number of invited members in this room - * @returns The number of members in this room whose membership is 'invite' - */ - public getInvitedMemberCount(): number { - if (this.summaryInvitedMemberCount !== null) { - return this.summaryInvitedMemberCount; - } - if (this.invitedMemberCount === null) { - this.invitedMemberCount = this.getMembers().reduce((count, m) => { - return m.membership === "invite" ? count + 1 : count; - }, 0); - } - return this.invitedMemberCount; - } - - /** - * Set the amount of invited members in this room - * @param count - the amount of invited members - */ - public setInvitedMemberCount(count: number): void { - this.summaryInvitedMemberCount = count; - } - - /** - * Get all RoomMembers in this room. - * @returns A list of RoomMembers. - */ - public getMembers(): RoomMember[] { - return Object.values(this.members); - } - - /** - * Get all RoomMembers in this room, excluding the user IDs provided. - * @param excludedIds - The user IDs to exclude. - * @returns A list of RoomMembers. - */ - public getMembersExcept(excludedIds: string[]): RoomMember[] { - return this.getMembers().filter((m) => !excludedIds.includes(m.userId)); - } - - /** - * Get a room member by their user ID. - * @param userId - The room member's user ID. - * @returns The member or null if they do not exist. - */ - public getMember(userId: string): RoomMember | null { - return this.members[userId] || null; - } - - /** - * Get a room member whose properties will not change with this room state. You - * typically want this if you want to attach a RoomMember to a MatrixEvent which - * may no longer be represented correctly by Room.currentState or Room.oldState. - * The term 'sentinel' refers to the fact that this RoomMember is an unchanging - * guardian for state at this particular point in time. - * @param userId - The room member's user ID. - * @returns The member or null if they do not exist. - */ - public getSentinelMember(userId: string): RoomMember | null { - if (!userId) return null; - let sentinel = this.sentinels[userId]; - - if (sentinel === undefined) { - sentinel = new RoomMember(this.roomId, userId); - const member = this.members[userId]; - if (member?.events.member) { - sentinel.setMembershipEvent(member.events.member, this); - } - this.sentinels[userId] = sentinel; - } - return sentinel; - } - - /** - * Get state events from the state of the room. - * @param eventType - The event type of the state event. - * @param stateKey - Optional. The state_key of the state event. If - * this is `undefined` then all matching state events will be - * returned. - * @returns A list of events if state_key was - * `undefined`, else a single event (or null if no match found). - */ - public getStateEvents(eventType: EventType | string): MatrixEvent[]; - public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent | null; - public getStateEvents(eventType: EventType | string, stateKey?: string): MatrixEvent[] | MatrixEvent | null { - if (!this.events.has(eventType)) { - // no match - return stateKey === undefined ? [] : null; - } - if (stateKey === undefined) { - // return all values - return Array.from(this.events.get(eventType)!.values()); - } - const event = this.events.get(eventType)!.get(stateKey); - return event ? event : null; - } - - public get hasLiveBeacons(): boolean { - return !!this.liveBeaconIds?.length; - } - - public get liveBeaconIds(): BeaconIdentifier[] { - return this._liveBeaconIds; - } - - /** - * Creates a copy of this room state so that mutations to either won't affect the other. - * @returns the copy of the room state - */ - public clone(): RoomState { - const copy = new RoomState(this.roomId, this.oobMemberFlags); - - // Ugly hack: because setStateEvents will mark - // members as susperseding future out of bound members - // if loading is in progress (through oobMemberFlags) - // since these are not new members, we're merely copying them - // set the status to not started - // after copying, we set back the status - const status = this.oobMemberFlags.status; - this.oobMemberFlags.status = OobStatus.NotStarted; - - Array.from(this.events.values()).forEach((eventsByStateKey) => { - copy.setStateEvents(Array.from(eventsByStateKey.values())); - }); - - // Ugly hack: see above - this.oobMemberFlags.status = status; - - if (this.summaryInvitedMemberCount !== null) { - copy.setInvitedMemberCount(this.getInvitedMemberCount()); - } - if (this.summaryJoinedMemberCount !== null) { - copy.setJoinedMemberCount(this.getJoinedMemberCount()); - } - - // copy out of band flags if needed - if (this.oobMemberFlags.status == OobStatus.Finished) { - // copy markOutOfBand flags - this.getMembers().forEach((member) => { - if (member.isOutOfBand()) { - copy.getMember(member.userId)?.markOutOfBand(); - } - }); - } - - return copy; - } - - /** - * Add previously unknown state events. - * When lazy loading members while back-paginating, - * the relevant room state for the timeline chunk at the end - * of the chunk can be set with this method. - * @param events - state events to prepend - */ - public setUnknownStateEvents(events: MatrixEvent[]): void { - const unknownStateEvents = events.filter((event) => { - return !this.events.has(event.getType()) || !this.events.get(event.getType())!.has(event.getStateKey()!); - }); - - this.setStateEvents(unknownStateEvents); - } - - /** - * Add an array of one or more state MatrixEvents, overwriting any existing - * state with the same `{type, stateKey}` tuple. Will fire "RoomState.events" - * for every event added. May fire "RoomState.members" if there are - * `m.room.member` events. May fire "RoomStateEvent.Marker" if there are - * `UNSTABLE_MSC2716_MARKER` events. - * @param stateEvents - a list of state events for this room. - * - * @remarks - * Fires {@link RoomStateEvent.Members} - * Fires {@link RoomStateEvent.NewMember} - * Fires {@link RoomStateEvent.Events} - * Fires {@link RoomStateEvent.Marker} - */ - public setStateEvents(stateEvents: MatrixEvent[], markerFoundOptions?: IMarkerFoundOptions): void { - this.updateModifiedTime(); - - // update the core event dict - stateEvents.forEach((event) => { - if (event.getRoomId() !== this.roomId || !event.isState()) return; - - if (M_BEACON_INFO.matches(event.getType())) { - this.setBeacon(event); - } - - const lastStateEvent = this.getStateEventMatching(event); - this.setStateEvent(event); - if (event.getType() === EventType.RoomMember) { - this.updateDisplayNameCache(event.getStateKey()!, event.getContent().displayname ?? ""); - this.updateThirdPartyTokenCache(event); - } - this.emit(RoomStateEvent.Events, event, this, lastStateEvent); - }); - - this.onBeaconLivenessChange(); - // update higher level data structures. This needs to be done AFTER the - // core event dict as these structures may depend on other state events in - // the given array (e.g. disambiguating display names in one go to do both - // clashing names rather than progressively which only catches 1 of them). - stateEvents.forEach((event) => { - if (event.getRoomId() !== this.roomId || !event.isState()) return; - - if (event.getType() === EventType.RoomMember) { - const userId = event.getStateKey()!; - - // leave events apparently elide the displayname or avatar_url, - // so let's fake one up so that we don't leak user ids - // into the timeline - if (event.getContent().membership === "leave" || event.getContent().membership === "ban") { - event.getContent().avatar_url = event.getContent().avatar_url || event.getPrevContent().avatar_url; - event.getContent().displayname = - event.getContent().displayname || event.getPrevContent().displayname; - } - - const member = this.getOrCreateMember(userId, event); - member.setMembershipEvent(event, this); - this.updateMember(member); - this.emit(RoomStateEvent.Members, event, this, member); - } else if (event.getType() === EventType.RoomPowerLevels) { - // events with unknown state keys should be ignored - // and should not aggregate onto members power levels - if (event.getStateKey() !== "") { - return; - } - const members = Object.values(this.members); - members.forEach((member) => { - // We only propagate `RoomState.members` event if the - // power levels has been changed - // large room suffer from large re-rendering especially when not needed - const oldLastModified = member.getLastModifiedTime(); - member.setPowerLevelEvent(event); - if (oldLastModified !== member.getLastModifiedTime()) { - this.emit(RoomStateEvent.Members, event, this, member); - } - }); - - // assume all our sentinels are now out-of-date - this.sentinels = {}; - } else if (UNSTABLE_MSC2716_MARKER.matches(event.getType())) { - this.emit(RoomStateEvent.Marker, event, markerFoundOptions); - } - }); - - this.emit(RoomStateEvent.Update, this); - } - - public async processBeaconEvents(events: MatrixEvent[], matrixClient: MatrixClient): Promise<void> { - if ( - !events.length || - // discard locations if we have no beacons - !this.beacons.size - ) { - return; - } - - const beaconByEventIdDict = [...this.beacons.values()].reduce<Record<string, Beacon>>((dict, beacon) => { - dict[beacon.beaconInfoId] = beacon; - return dict; - }, {}); - - const processBeaconRelation = (beaconInfoEventId: string, event: MatrixEvent): void => { - if (!M_BEACON.matches(event.getType())) { - return; - } - - const beacon = beaconByEventIdDict[beaconInfoEventId]; - - if (beacon) { - beacon.addLocations([event]); - } - }; - - for (const event of events) { - const relatedToEventId = event.getRelation()?.event_id; - // not related to a beacon we know about; discard - if (!relatedToEventId || !beaconByEventIdDict[relatedToEventId]) return; - if (!M_BEACON.matches(event.getType()) && !event.isEncrypted()) return; - - try { - await matrixClient.decryptEventIfNeeded(event); - processBeaconRelation(relatedToEventId, event); - } catch { - if (event.isDecryptionFailure()) { - // add an event listener for once the event is decrypted. - event.once(MatrixEventEvent.Decrypted, async () => { - processBeaconRelation(relatedToEventId, event); - }); - } - } - } - } - - /** - * Looks up a member by the given userId, and if it doesn't exist, - * create it and emit the `RoomState.newMember` event. - * This method makes sure the member is added to the members dictionary - * before emitting, as this is done from setStateEvents and setOutOfBandMember. - * @param userId - the id of the user to look up - * @param event - the membership event for the (new) member. Used to emit. - * @returns the member, existing or newly created. - * - * @remarks - * Fires {@link RoomStateEvent.NewMember} - */ - private getOrCreateMember(userId: string, event: MatrixEvent): RoomMember { - let member = this.members[userId]; - if (!member) { - member = new RoomMember(this.roomId, userId); - // add member to members before emitting any events, - // as event handlers often lookup the member - this.members[userId] = member; - this.emit(RoomStateEvent.NewMember, event, this, member); - } - return member; - } - - private setStateEvent(event: MatrixEvent): void { - if (!this.events.has(event.getType())) { - this.events.set(event.getType(), new Map()); - } - this.events.get(event.getType())!.set(event.getStateKey()!, event); - } - - /** - * @experimental - */ - private setBeacon(event: MatrixEvent): void { - const beaconIdentifier = getBeaconInfoIdentifier(event); - - if (this.beacons.has(beaconIdentifier)) { - const beacon = this.beacons.get(beaconIdentifier)!; - - if (event.isRedacted()) { - if (beacon.beaconInfoId === (<IEvent>event.getRedactionEvent())?.redacts) { - beacon.destroy(); - this.beacons.delete(beaconIdentifier); - } - return; - } - - return beacon.update(event); - } - - if (event.isRedacted()) { - return; - } - - const beacon = new Beacon(event); - - this.reEmitter.reEmit<BeaconEvent, BeaconEvent>(beacon, [ - BeaconEvent.New, - BeaconEvent.Update, - BeaconEvent.Destroy, - BeaconEvent.LivenessChange, - ]); - - this.emit(BeaconEvent.New, event, beacon); - beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this)); - beacon.on(BeaconEvent.Destroy, this.onBeaconLivenessChange.bind(this)); - - this.beacons.set(beacon.identifier, beacon); - } - - /** - * @experimental - * Check liveness of room beacons - * emit RoomStateEvent.BeaconLiveness event - */ - private onBeaconLivenessChange(): void { - this._liveBeaconIds = Array.from(this.beacons.values()) - .filter((beacon) => beacon.isLive) - .map((beacon) => beacon.identifier); - - this.emit(RoomStateEvent.BeaconLiveness, this, this.hasLiveBeacons); - } - - private getStateEventMatching(event: MatrixEvent): MatrixEvent | null { - return this.events.get(event.getType())?.get(event.getStateKey()!) ?? null; - } - - private updateMember(member: RoomMember): void { - // this member may have a power level already, so set it. - const pwrLvlEvent = this.getStateEvents(EventType.RoomPowerLevels, ""); - if (pwrLvlEvent) { - member.setPowerLevelEvent(pwrLvlEvent); - } - - // blow away the sentinel which is now outdated - delete this.sentinels[member.userId]; - - this.members[member.userId] = member; - this.joinedMemberCount = null; - this.invitedMemberCount = null; - } - - /** - * Get the out-of-band members loading state, whether loading is needed or not. - * Note that loading might be in progress and hence isn't needed. - * @returns whether or not the members of this room need to be loaded - */ - public needsOutOfBandMembers(): boolean { - return this.oobMemberFlags.status === OobStatus.NotStarted; - } - - /** - * Check if loading of out-of-band-members has completed - * - * @returns true if the full membership list of this room has been loaded. False if it is not started or is in - * progress. - */ - public outOfBandMembersReady(): boolean { - return this.oobMemberFlags.status === OobStatus.Finished; - } - - /** - * Mark this room state as waiting for out-of-band members, - * ensuring it doesn't ask for them to be requested again - * through needsOutOfBandMembers - */ - public markOutOfBandMembersStarted(): void { - if (this.oobMemberFlags.status !== OobStatus.NotStarted) { - return; - } - this.oobMemberFlags.status = OobStatus.InProgress; - } - - /** - * Mark this room state as having failed to fetch out-of-band members - */ - public markOutOfBandMembersFailed(): void { - if (this.oobMemberFlags.status !== OobStatus.InProgress) { - return; - } - this.oobMemberFlags.status = OobStatus.NotStarted; - } - - /** - * Clears the loaded out-of-band members - */ - public clearOutOfBandMembers(): void { - let count = 0; - Object.keys(this.members).forEach((userId) => { - const member = this.members[userId]; - if (member.isOutOfBand()) { - ++count; - delete this.members[userId]; - } - }); - logger.log(`LL: RoomState removed ${count} members...`); - this.oobMemberFlags.status = OobStatus.NotStarted; - } - - /** - * Sets the loaded out-of-band members. - * @param stateEvents - array of membership state events - */ - public setOutOfBandMembers(stateEvents: MatrixEvent[]): void { - logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`); - if (this.oobMemberFlags.status !== OobStatus.InProgress) { - return; - } - logger.log(`LL: RoomState put in finished state ...`); - this.oobMemberFlags.status = OobStatus.Finished; - stateEvents.forEach((e) => this.setOutOfBandMember(e)); - this.emit(RoomStateEvent.Update, this); - } - - /** - * Sets a single out of band member, used by both setOutOfBandMembers and clone - * @param stateEvent - membership state event - */ - private setOutOfBandMember(stateEvent: MatrixEvent): void { - if (stateEvent.getType() !== EventType.RoomMember) { - return; - } - const userId = stateEvent.getStateKey()!; - const existingMember = this.getMember(userId); - // never replace members received as part of the sync - if (existingMember && !existingMember.isOutOfBand()) { - return; - } - - const member = this.getOrCreateMember(userId, stateEvent); - member.setMembershipEvent(stateEvent, this); - // needed to know which members need to be stored seperately - // as they are not part of the sync accumulator - // this is cleared by setMembershipEvent so when it's updated through /sync - member.markOutOfBand(); - - this.updateDisplayNameCache(member.userId, member.name); - - this.setStateEvent(stateEvent); - this.updateMember(member); - this.emit(RoomStateEvent.Members, stateEvent, this, member); - } - - /** - * Set the current typing event for this room. - * @param event - The typing event - */ - public setTypingEvent(event: MatrixEvent): void { - Object.values(this.members).forEach(function (member) { - member.setTypingEvent(event); - }); - } - - /** - * Get the m.room.member event which has the given third party invite token. - * - * @param token - The token - * @returns The m.room.member event or null - */ - public getInviteForThreePidToken(token: string): MatrixEvent | null { - return this.tokenToInvite[token] || null; - } - - /** - * Update the last modified time to the current time. - */ - private updateModifiedTime(): void { - this.modified = Date.now(); - } - - /** - * Get the timestamp when this room state was last updated. This timestamp is - * updated when this object has received new state events. - * @returns The timestamp - */ - public getLastModifiedTime(): number { - return this.modified; - } - - /** - * Get user IDs with the specified or similar display names. - * @param displayName - The display name to get user IDs from. - * @returns An array of user IDs or an empty array. - */ - public getUserIdsWithDisplayName(displayName: string): string[] { - return this.displayNameToUserIds.get(utils.removeHiddenChars(displayName)) ?? []; - } - - /** - * Returns true if userId is in room, event is not redacted and either sender of - * mxEvent or has power level sufficient to redact events other than their own. - * @param mxEvent - The event to test permission for - * @param userId - The user ID of the user to test permission for - * @returns true if the given used ID can redact given event - */ - public maySendRedactionForEvent(mxEvent: MatrixEvent, userId: string): boolean { - const member = this.getMember(userId); - if (!member || member.membership === "leave") return false; - - if (mxEvent.status || mxEvent.isRedacted()) return false; - - // The user may have been the sender, but they can't redact their own message - // if redactions are blocked. - const canRedact = this.maySendEvent(EventType.RoomRedaction, userId); - if (mxEvent.getSender() === userId) return canRedact; - - return this.hasSufficientPowerLevelFor("redact", member.powerLevel); - } - - /** - * Returns true if the given power level is sufficient for action - * @param action - The type of power level to check - * @param powerLevel - The power level of the member - * @returns true if the given power level is sufficient - */ - public hasSufficientPowerLevelFor(action: "ban" | "kick" | "redact", powerLevel: number): boolean { - const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, ""); - - let powerLevels: IPowerLevelsContent = {}; - if (powerLevelsEvent) { - powerLevels = powerLevelsEvent.getContent(); - } - - let requiredLevel = 50; - if (utils.isNumber(powerLevels[action])) { - requiredLevel = powerLevels[action]!; - } - - return powerLevel >= requiredLevel; - } - - /** - * Short-form for maySendEvent('m.room.message', userId) - * @param userId - The user ID of the user to test permission for - * @returns true if the given user ID should be permitted to send - * message events into the given room. - */ - public maySendMessage(userId: string): boolean { - return this.maySendEventOfType(EventType.RoomMessage, userId, false); - } - - /** - * Returns true if the given user ID has permission to send a normal - * event of type `eventType` into this room. - * @param eventType - The type of event to test - * @param userId - The user ID of the user to test permission for - * @returns true if the given user ID should be permitted to send - * the given type of event into this room, - * according to the room's state. - */ - public maySendEvent(eventType: EventType | string, userId: string): boolean { - return this.maySendEventOfType(eventType, userId, false); - } - - /** - * Returns true if the given MatrixClient has permission to send a state - * event of type `stateEventType` into this room. - * @param stateEventType - The type of state events to test - * @param cli - The client to test permission for - * @returns true if the given client should be permitted to send - * the given type of state event into this room, - * according to the room's state. - */ - public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean { - if (cli.isGuest() || !cli.credentials.userId) { - return false; - } - return this.maySendStateEvent(stateEventType, cli.credentials.userId); - } - - /** - * Returns true if the given user ID has permission to send a state - * event of type `stateEventType` into this room. - * @param stateEventType - The type of state events to test - * @param userId - The user ID of the user to test permission for - * @returns true if the given user ID should be permitted to send - * the given type of state event into this room, - * according to the room's state. - */ - public maySendStateEvent(stateEventType: EventType | string, userId: string): boolean { - return this.maySendEventOfType(stateEventType, userId, true); - } - - /** - * Returns true if the given user ID has permission to send a normal or state - * event of type `eventType` into this room. - * @param eventType - The type of event to test - * @param userId - The user ID of the user to test permission for - * @param state - If true, tests if the user may send a state - event of this type. Otherwise tests whether - they may send a regular event. - * @returns true if the given user ID should be permitted to send - * the given type of event into this room, - * according to the room's state. - */ - private maySendEventOfType(eventType: EventType | string, userId: string, state: boolean): boolean { - const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, ""); - - let powerLevels: IPowerLevelsContent; - let eventsLevels: Record<EventType | string, number> = {}; - - let stateDefault = 0; - let eventsDefault = 0; - let powerLevel = 0; - if (powerLevelsEvent) { - powerLevels = powerLevelsEvent.getContent(); - eventsLevels = powerLevels.events || {}; - - if (Number.isSafeInteger(powerLevels.state_default)) { - stateDefault = powerLevels.state_default!; - } else { - stateDefault = 50; - } - - const userPowerLevel = powerLevels.users && powerLevels.users[userId]; - if (Number.isSafeInteger(userPowerLevel)) { - powerLevel = userPowerLevel!; - } else if (Number.isSafeInteger(powerLevels.users_default)) { - powerLevel = powerLevels.users_default!; - } - - if (Number.isSafeInteger(powerLevels.events_default)) { - eventsDefault = powerLevels.events_default!; - } - } - - let requiredLevel = state ? stateDefault : eventsDefault; - if (Number.isSafeInteger(eventsLevels[eventType])) { - requiredLevel = eventsLevels[eventType]; - } - return powerLevel >= requiredLevel; - } - - /** - * Returns true if the given user ID has permission to trigger notification - * of type `notifLevelKey` - * @param notifLevelKey - The level of notification to test (eg. 'room') - * @param userId - The user ID of the user to test permission for - * @returns true if the given user ID has permission to trigger a - * notification of this type. - */ - public mayTriggerNotifOfType(notifLevelKey: string, userId: string): boolean { - const member = this.getMember(userId); - if (!member) { - return false; - } - - const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, ""); - - let notifLevel = 50; - if ( - powerLevelsEvent && - powerLevelsEvent.getContent() && - powerLevelsEvent.getContent().notifications && - utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey]) - ) { - notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey]; - } - - return member.powerLevel >= notifLevel; - } - - /** - * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`. - * @returns the join_rule applied to this room - */ - public getJoinRule(): JoinRule { - const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, ""); - const joinRuleContent: Partial<IJoinRuleEventContent> = joinRuleEvent?.getContent() ?? {}; - return joinRuleContent["join_rule"] || JoinRule.Invite; - } - - /** - * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns the history_visibility applied to this room - */ - public getHistoryVisibility(): HistoryVisibility { - const historyVisibilityEvent = this.getStateEvents(EventType.RoomHistoryVisibility, ""); - const historyVisibilityContent = historyVisibilityEvent?.getContent() ?? {}; - return historyVisibilityContent["history_visibility"] || HistoryVisibility.Shared; - } - - /** - * Returns the guest access based on the m.room.guest_access state event, defaulting to `shared`. - * @returns the guest_access applied to this room - */ - public getGuestAccess(): GuestAccess { - const guestAccessEvent = this.getStateEvents(EventType.RoomGuestAccess, ""); - const guestAccessContent = guestAccessEvent?.getContent() ?? {}; - return guestAccessContent["guest_access"] || GuestAccess.Forbidden; - } - - /** - * Find the predecessor room based on this room state. - * - * @param msc3946ProcessDynamicPredecessor - if true, look for an - * m.room.predecessor state event and use it if found (MSC3946). - * @returns null if this room has no predecessor. Otherwise, returns - * the roomId, last eventId and viaServers of the predecessor room. - * - * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events - * as well as m.room.create events to find predecessors. - * - * Note: if an m.predecessor event is used, eventId may be undefined - * since last_known_event_id is optional. - * - * Note: viaServers may be undefined, and will definitely be undefined if - * this predecessor comes from a RoomCreate event (rather than a - * RoomPredecessor, which has the optional via_servers property). - */ - public findPredecessor( - msc3946ProcessDynamicPredecessor = false, - ): { roomId: string; eventId?: string; viaServers?: string[] } | null { - // Note: the tests for this function are against Room.findPredecessor, - // which just calls through to here. - - if (msc3946ProcessDynamicPredecessor) { - const predecessorEvent = this.getStateEvents(EventType.RoomPredecessor, ""); - if (predecessorEvent) { - const content = predecessorEvent.getContent<{ - predecessor_room_id: string; - last_known_event_id?: string; - via_servers?: string[]; - }>(); - const roomId = content.predecessor_room_id; - let eventId = content.last_known_event_id; - if (typeof eventId !== "string") { - eventId = undefined; - } - let viaServers = content.via_servers; - if (!Array.isArray(viaServers)) { - viaServers = undefined; - } - if (typeof roomId === "string") { - return { roomId, eventId, viaServers }; - } - } - } - - const createEvent = this.getStateEvents(EventType.RoomCreate, ""); - if (createEvent) { - const predecessor = createEvent.getContent<{ - predecessor?: Partial<{ - room_id: string; - event_id: string; - }>; - }>()["predecessor"]; - if (predecessor) { - const roomId = predecessor["room_id"]; - if (typeof roomId === "string") { - let eventId = predecessor["event_id"]; - if (typeof eventId !== "string" || eventId === "") { - eventId = undefined; - } - return { roomId, eventId }; - } - } - } - return null; - } - - private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void { - if (!memberEvent.getContent().third_party_invite) { - return; - } - const token = (memberEvent.getContent().third_party_invite.signed || {}).token; - if (!token) { - return; - } - const threePidInvite = this.getStateEvents(EventType.RoomThirdPartyInvite, token); - if (!threePidInvite) { - return; - } - this.tokenToInvite[token] = memberEvent; - } - - private updateDisplayNameCache(userId: string, displayName: string): void { - const oldName = this.userIdsToDisplayNames[userId]; - delete this.userIdsToDisplayNames[userId]; - if (oldName) { - // Remove the old name from the cache. - // We clobber the user_id > name lookup but the name -> [user_id] lookup - // means we need to remove that user ID from that array rather than nuking - // the lot. - const strippedOldName = utils.removeHiddenChars(oldName); - - const existingUserIds = this.displayNameToUserIds.get(strippedOldName); - if (existingUserIds) { - // remove this user ID from this array - const filteredUserIDs = existingUserIds.filter((id) => id !== userId); - this.displayNameToUserIds.set(strippedOldName, filteredUserIDs); - } - } - - this.userIdsToDisplayNames[userId] = displayName; - - const strippedDisplayname = displayName && utils.removeHiddenChars(displayName); - // an empty stripped displayname (undefined/'') will be set to MXID in room-member.js - if (strippedDisplayname) { - const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? []; - arr.push(userId); - this.displayNameToUserIds.set(strippedDisplayname, arr); - } - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts deleted file mode 100644 index 936ec1d..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2015 - 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. -*/ - -export interface IRoomSummary { - "m.heroes": string[]; - "m.joined_member_count"?: number; - "m.invited_member_count"?: number; -} - -interface IInfo { - /** The title of the room (e.g. `m.room.name`) */ - title: string; - /** The description of the room (e.g. `m.room.topic`) */ - desc?: string; - /** The number of joined users. */ - numMembers?: number; - /** The list of aliases for this room. */ - aliases?: string[]; - /** The timestamp for this room. */ - timestamp?: number; -} - -/** - * Construct a new Room Summary. A summary can be used for display on a recent - * list, without having to load the entire room list into memory. - * @param roomId - Required. The ID of this room. - * @param info - Optional. The summary info. Additional keys are supported. - */ -export class RoomSummary { - public constructor(public readonly roomId: string, info?: IInfo) {} -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts deleted file mode 100644 index 133b210..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts +++ /dev/null @@ -1,3487 +0,0 @@ -/* -Copyright 2015 - 2023 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 { M_POLL_START, Optional } from "matrix-events-sdk"; - -import { - EventTimelineSet, - DuplicateStrategy, - IAddLiveEventOptions, - EventTimelineSetHandlerMap, -} from "./event-timeline-set"; -import { Direction, EventTimeline } from "./event-timeline"; -import { getHttpUriForMxc } from "../content-repo"; -import * as utils from "../utils"; -import { normalize, noUnsafeEventProps } from "../utils"; -import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event"; -import { EventStatus } from "./event-status"; -import { RoomMember } from "./room-member"; -import { IRoomSummary, RoomSummary } from "./room-summary"; -import { logger } from "../logger"; -import { TypedReEmitter } from "../ReEmitter"; -import { - EventType, - RoomCreateTypeField, - RoomType, - UNSTABLE_ELEMENT_FUNCTIONAL_USERS, - EVENT_VISIBILITY_CHANGE_TYPE, - RelationType, -} from "../@types/event"; -import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client"; -import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials"; -import { Filter, IFilterDefinition } from "../filter"; -import { RoomState, RoomStateEvent, RoomStateEventHandlerMap } from "./room-state"; -import { BeaconEvent, BeaconEventHandlerMap } from "./beacon"; -import { - Thread, - ThreadEvent, - EventHandlerMap as ThreadHandlerMap, - FILTER_RELATED_BY_REL_TYPES, - THREAD_RELATION_TYPE, - FILTER_RELATED_BY_SENDERS, - ThreadFilterType, -} from "./thread"; -import { - CachedReceiptStructure, - MAIN_ROOM_TIMELINE, - Receipt, - ReceiptContent, - ReceiptType, -} from "../@types/read_receipts"; -import { IStateEventWithRoomId } from "../@types/search"; -import { RelationsContainer } from "./relations-container"; -import { ReadReceipt, synthesizeReceipt } from "./read-receipt"; -import { Poll, PollEvent } from "./poll"; - -// These constants are used as sane defaults when the homeserver doesn't support -// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be -// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the -// room versions which are considered okay for people to run without being asked -// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers -// return an m.room_versions capability. -export const KNOWN_SAFE_ROOM_VERSION = "9"; -const SAFE_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; - -interface IOpts { - /** - * Controls where pending messages appear in a room's timeline. - * If "<b>chronological</b>", messages will appear in the timeline when the call to `sendEvent` was made. - * If "<b>detached</b>", pending messages will appear in a separate list, - * accessible via {@link Room#getPendingEvents}. - * Default: "chronological". - */ - pendingEventOrdering?: PendingEventOrdering; - /** - * Set to true to enable improved timeline support. - */ - timelineSupport?: boolean; - lazyLoadMembers?: boolean; -} - -export interface IRecommendedVersion { - version: string; - needsUpgrade: boolean; - urgent: boolean; -} - -// When inserting a visibility event affecting event `eventId`, we -// need to scan through existing visibility events for `eventId`. -// In theory, this could take an unlimited amount of time if: -// -// - the visibility event was sent by a moderator; and -// - `eventId` already has many visibility changes (usually, it should -// be 2 or less); and -// - for some reason, the visibility changes are received out of order -// (usually, this shouldn't happen at all). -// -// For this reason, we limit the number of events to scan through, -// expecting that a broken visibility change for a single event in -// an extremely uncommon case (possibly a DoS) is a small -// price to pay to keep matrix-js-sdk responsive. -const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30; - -export type NotificationCount = Partial<Record<NotificationCountType, number>>; - -export enum NotificationCountType { - Highlight = "highlight", - Total = "total", -} - -export interface ICreateFilterOpts { - // Populate the filtered timeline with already loaded events in the room - // timeline. Useful to disable for some filters that can't be achieved by the - // client in an efficient manner - prepopulateTimeline?: boolean; - useSyncEvents?: boolean; - pendingEvents?: boolean; -} - -export enum RoomEvent { - MyMembership = "Room.myMembership", - Tags = "Room.tags", - AccountData = "Room.accountData", - Receipt = "Room.receipt", - Name = "Room.name", - Redaction = "Room.redaction", - RedactionCancelled = "Room.redactionCancelled", - LocalEchoUpdated = "Room.localEchoUpdated", - Timeline = "Room.timeline", - TimelineReset = "Room.timelineReset", - TimelineRefresh = "Room.TimelineRefresh", - OldStateUpdated = "Room.OldStateUpdated", - CurrentStateUpdated = "Room.CurrentStateUpdated", - HistoryImportedWithinTimeline = "Room.historyImportedWithinTimeline", - UnreadNotifications = "Room.UnreadNotifications", -} - -export type RoomEmittedEvents = - | RoomEvent - | RoomStateEvent.Events - | RoomStateEvent.Members - | RoomStateEvent.NewMember - | RoomStateEvent.Update - | RoomStateEvent.Marker - | ThreadEvent.New - | ThreadEvent.Update - | ThreadEvent.NewReply - | ThreadEvent.Delete - | MatrixEventEvent.BeforeRedaction - | BeaconEvent.New - | BeaconEvent.Update - | BeaconEvent.Destroy - | BeaconEvent.LivenessChange - | PollEvent.New; - -export type RoomEventHandlerMap = { - /** - * Fires when the logged in user's membership in the room is updated. - * - * @param room - The room in which the membership has been updated - * @param membership - The new membership value - * @param prevMembership - The previous membership value - */ - [RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void; - /** - * Fires whenever a room's tags are updated. - * @param event - The tags event - * @param room - The room whose Room.tags was updated. - * @example - * ``` - * matrixClient.on("Room.tags", function(event, room){ - * var newTags = event.getContent().tags; - * if (newTags["favourite"]) showStar(room); - * }); - * ``` - */ - [RoomEvent.Tags]: (event: MatrixEvent, room: Room) => void; - /** - * Fires whenever a room's account_data is updated. - * @param event - The account_data event - * @param room - The room whose account_data was updated. - * @param prevEvent - The event being replaced by - * the new account data, if known. - * @example - * ``` - * matrixClient.on("Room.accountData", function(event, room, oldEvent){ - * if (event.getType() === "m.room.colorscheme") { - * applyColorScheme(event.getContents()); - * } - * }); - * ``` - */ - [RoomEvent.AccountData]: (event: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => void; - /** - * Fires whenever a receipt is received for a room - * @param event - The receipt event - * @param room - The room whose receipts was updated. - * @example - * ``` - * matrixClient.on("Room.receipt", function(event, room){ - * var receiptContent = event.getContent(); - * }); - * ``` - */ - [RoomEvent.Receipt]: (event: MatrixEvent, room: Room) => void; - /** - * Fires whenever the name of a room is updated. - * @param room - The room whose Room.name was updated. - * @example - * ``` - * matrixClient.on("Room.name", function(room){ - * var newName = room.name; - * }); - * ``` - */ - [RoomEvent.Name]: (room: Room) => void; - /** - * Fires when an event we had previously received is redacted. - * - * (Note this is *not* fired when the redaction happens before we receive the - * event). - * - * @param event - The matrix redaction event - * @param room - The room containing the redacted event - */ - [RoomEvent.Redaction]: (event: MatrixEvent, room: Room) => void; - /** - * Fires when an event that was previously redacted isn't anymore. - * This happens when the redaction couldn't be sent and - * was subsequently cancelled by the user. Redactions have a local echo - * which is undone in this scenario. - * - * @param event - The matrix redaction event that was cancelled. - * @param room - The room containing the unredacted event - */ - [RoomEvent.RedactionCancelled]: (event: MatrixEvent, room: Room) => void; - /** - * Fires when the status of a transmitted event is updated. - * - * <p>When an event is first transmitted, a temporary copy of the event is - * inserted into the timeline, with a temporary event id, and a status of - * 'SENDING'. - * - * <p>Once the echo comes back from the server, the content of the event - * (MatrixEvent.event) is replaced by the complete event from the homeserver, - * thus updating its event id, as well as server-generated fields such as the - * timestamp. Its status is set to null. - * - * <p>Once the /send request completes, if the remote echo has not already - * arrived, the event is updated with a new event id and the status is set to - * 'SENT'. The server-generated fields are of course not updated yet. - * - * <p>If the /send fails, In this case, the event's status is set to - * 'NOT_SENT'. If it is later resent, the process starts again, setting the - * status to 'SENDING'. Alternatively, the message may be cancelled, which - * removes the event from the room, and sets the status to 'CANCELLED'. - * - * <p>This event is raised to reflect each of the transitions above. - * - * @param event - The matrix event which has been updated - * - * @param room - The room containing the redacted event - * - * @param oldEventId - The previous event id (the temporary event id, - * except when updating a successfully-sent event when its echo arrives) - * - * @param oldStatus - The previous event status. - */ - [RoomEvent.LocalEchoUpdated]: ( - event: MatrixEvent, - room: Room, - oldEventId?: string, - oldStatus?: EventStatus | null, - ) => void; - [RoomEvent.OldStateUpdated]: (room: Room, previousRoomState: RoomState, roomState: RoomState) => void; - [RoomEvent.CurrentStateUpdated]: (room: Room, previousRoomState: RoomState, roomState: RoomState) => void; - [RoomEvent.HistoryImportedWithinTimeline]: (markerEvent: MatrixEvent, room: Room) => void; - [RoomEvent.UnreadNotifications]: (unreadNotifications?: NotificationCount, threadId?: string) => void; - [RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void; - [ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void; - /** - * Fires when a new poll instance is added to the room state - * @param poll - the new poll - */ - [PollEvent.New]: (poll: Poll) => void; -} & Pick<ThreadHandlerMap, ThreadEvent.Update | ThreadEvent.NewReply | ThreadEvent.Delete> & - EventTimelineSetHandlerMap & - Pick<MatrixEventHandlerMap, MatrixEventEvent.BeforeRedaction> & - Pick< - RoomStateEventHandlerMap, - | RoomStateEvent.Events - | RoomStateEvent.Members - | RoomStateEvent.NewMember - | RoomStateEvent.Update - | RoomStateEvent.Marker - | BeaconEvent.New - > & - Pick<BeaconEventHandlerMap, BeaconEvent.Update | BeaconEvent.Destroy | BeaconEvent.LivenessChange>; - -export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> { - public readonly reEmitter: TypedReEmitter<RoomEmittedEvents, RoomEventHandlerMap>; - private txnToEvent: Map<string, MatrixEvent> = new Map(); // Pending in-flight requests { string: MatrixEvent } - private notificationCounts: NotificationCount = {}; - private readonly threadNotifications = new Map<string, NotificationCount>(); - public readonly cachedThreadReadReceipts = new Map<string, CachedReceiptStructure[]>(); - // Useful to know at what point the current user has started using threads in this room - private oldestThreadedReceiptTs = Infinity; - /** - * A record of the latest unthread receipts per user - * This is useful in determining whether a user has read a thread or not - */ - private unthreadedReceipts = new Map<string, Receipt>(); - private readonly timelineSets: EventTimelineSet[]; - public readonly polls: Map<string, Poll> = new Map<string, Poll>(); - public readonly threadsTimelineSets: EventTimelineSet[] = []; - // any filtered timeline sets we're maintaining for this room - private readonly filteredTimelineSets: Record<string, EventTimelineSet> = {}; // filter_id: timelineSet - private timelineNeedsRefresh = false; - private readonly pendingEventList?: MatrixEvent[]; - // read by megolm via getter; boolean value - null indicates "use global value" - private blacklistUnverifiedDevices?: boolean; - private selfMembership?: string; - private summaryHeroes: string[] | null = null; - // flags to stop logspam about missing m.room.create events - private getTypeWarning = false; - private getVersionWarning = false; - private membersPromise?: Promise<boolean>; - - // XXX: These should be read-only - /** - * The human-readable display name for this room. - */ - public name: string; - /** - * The un-homoglyphed name for this room. - */ - public normalizedName: string; - /** - * Dict of room tags; the keys are the tag name and the values - * are any metadata associated with the tag - e.g. `{ "fav" : { order: 1 } }` - */ - public tags: Record<string, Record<string, any>> = {}; // $tagName: { $metadata: $value } - /** - * accountData Dict of per-room account_data events; the keys are the - * event type and the values are the events. - */ - public accountData: Map<string, MatrixEvent> = new Map(); // $eventType: $event - /** - * The room summary. - */ - public summary: RoomSummary | null = null; - // legacy fields - /** - * The live event timeline for this room, with the oldest event at index 0. - * Present for backwards compatibility - prefer getLiveTimeline().getEvents() - */ - public timeline!: MatrixEvent[]; - /** - * oldState The state of the room at the time of the oldest - * event in the live timeline. Present for backwards compatibility - - * prefer getLiveTimeline().getState(EventTimeline.BACKWARDS). - */ - public oldState!: RoomState; - /** - * currentState The state of the room at the time of the - * newest event in the timeline. Present for backwards compatibility - - * prefer getLiveTimeline().getState(EventTimeline.FORWARDS). - */ - public currentState!: RoomState; - public readonly relations = new RelationsContainer(this.client, this); - - /** - * A collection of events known by the client - * This is not a comprehensive list of the threads that exist in this room - */ - private threads = new Map<string, Thread>(); - public lastThread?: Thread; - - /** - * A mapping of eventId to all visibility changes to apply - * to the event, by chronological order, as per - * https://github.com/matrix-org/matrix-doc/pull/3531 - * - * # Invariants - * - * - within each list, all events are classed by - * chronological order; - * - all events are events such that - * `asVisibilityEvent()` returns a non-null `IVisibilityChange`; - * - within each list with key `eventId`, all events - * are in relation to `eventId`. - * - * @experimental - */ - private visibilityEvents = new Map<string, MatrixEvent[]>(); - - /** - * Construct a new Room. - * - * <p>For a room, we store an ordered sequence of timelines, which may or may not - * be continuous. Each timeline lists a series of events, as well as tracking - * the room state at the start and the end of the timeline. It also tracks - * forward and backward pagination tokens, as well as containing links to the - * next timeline in the sequence. - * - * <p>There is one special timeline - the 'live' timeline, which represents the - * timeline to which events are being added in real-time as they are received - * from the /sync API. Note that you should not retain references to this - * timeline - even if it is the current timeline right now, it may not remain - * so if the server gives us a timeline gap in /sync. - * - * <p>In order that we can find events from their ids later, we also maintain a - * map from event_id to timeline and index. - * - * @param roomId - Required. The ID of this room. - * @param client - Required. The client, used to lazy load members. - * @param myUserId - Required. The ID of the syncing user. - * @param opts - Configuration options - */ - public constructor( - public readonly roomId: string, - public readonly client: MatrixClient, - public readonly myUserId: string, - private readonly opts: IOpts = {}, - ) { - super(); - // In some cases, we add listeners for every displayed Matrix event, so it's - // common to have quite a few more than the default limit. - this.setMaxListeners(100); - this.reEmitter = new TypedReEmitter(this); - - opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological; - - this.name = roomId; - this.normalizedName = roomId; - - // all our per-room timeline sets. the first one is the unfiltered ones; - // the subsequent ones are the filtered ones in no particular order. - this.timelineSets = [new EventTimelineSet(this, opts)]; - this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [RoomEvent.Timeline, RoomEvent.TimelineReset]); - - this.fixUpLegacyTimelineFields(); - - if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) { - this.pendingEventList = []; - this.client.store.getPendingEvents(this.roomId).then((events) => { - const mapper = this.client.getEventMapper({ - toDevice: false, - decrypt: false, - }); - events.forEach(async (serializedEvent: Partial<IEvent>) => { - const event = mapper(serializedEvent); - await client.decryptEventIfNeeded(event); - event.setStatus(EventStatus.NOT_SENT); - this.addPendingEvent(event, event.getTxnId()!); - }); - }); - } - - // awaited by getEncryptionTargetMembers while room members are loading - if (!this.opts.lazyLoadMembers) { - this.membersPromise = Promise.resolve(false); - } else { - this.membersPromise = undefined; - } - } - - private threadTimelineSetsPromise: Promise<[EventTimelineSet, EventTimelineSet]> | null = null; - public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet] | null> { - if (this.threadTimelineSetsPromise) { - return this.threadTimelineSetsPromise; - } - - if (this.client?.supportsThreads()) { - try { - this.threadTimelineSetsPromise = Promise.all([ - this.createThreadTimelineSet(), - this.createThreadTimelineSet(ThreadFilterType.My), - ]); - const timelineSets = await this.threadTimelineSetsPromise; - this.threadsTimelineSets.push(...timelineSets); - return timelineSets; - } catch (e) { - this.threadTimelineSetsPromise = null; - return null; - } - } - return null; - } - - /** - * Bulk decrypt critical events in a room - * - * Critical events represents the minimal set of events to decrypt - * for a typical UI to function properly - * - * - Last event of every room (to generate likely message preview) - * - All events up to the read receipt (to calculate an accurate notification count) - * - * @returns Signals when all events have been decrypted - */ - public async decryptCriticalEvents(): Promise<void> { - if (!this.client.isCryptoEnabled()) return; - - const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId()!, true); - const events = this.getLiveTimeline().getEvents(); - const readReceiptTimelineIndex = events.findIndex((matrixEvent) => { - return matrixEvent.event.event_id === readReceiptEventId; - }); - - const decryptionPromises = events - .slice(readReceiptTimelineIndex) - .reverse() - .map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true })); - - await Promise.allSettled(decryptionPromises); - } - - /** - * Bulk decrypt events in a room - * - * @returns Signals when all events have been decrypted - */ - public async decryptAllEvents(): Promise<void> { - if (!this.client.isCryptoEnabled()) return; - - const decryptionPromises = this.getUnfilteredTimelineSet() - .getLiveTimeline() - .getEvents() - .slice(0) // copy before reversing - .reverse() - .map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true })); - - await Promise.allSettled(decryptionPromises); - } - - /** - * Gets the creator of the room - * @returns The creator of the room, or null if it could not be determined - */ - public getCreator(): string | null { - const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); - return createEvent?.getContent()["creator"] ?? null; - } - - /** - * Gets the version of the room - * @returns The version of the room, or null if it could not be determined - */ - public getVersion(): string { - const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); - if (!createEvent) { - if (!this.getVersionWarning) { - logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event"); - this.getVersionWarning = true; - } - return "1"; - } - return createEvent.getContent()["room_version"] ?? "1"; - } - - /** - * Determines whether this room needs to be upgraded to a new version - * @returns What version the room should be upgraded to, or null if - * the room does not require upgrading at this time. - * @deprecated Use #getRecommendedVersion() instead - */ - public shouldUpgradeToVersion(): string | null { - // TODO: Remove this function. - // This makes assumptions about which versions are safe, and can easily - // be wrong. Instead, people are encouraged to use getRecommendedVersion - // which determines a safer value. This function doesn't use that function - // because this is not async-capable, and to avoid breaking the contract - // we're deprecating this. - - if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) { - return KNOWN_SAFE_ROOM_VERSION; - } - - return null; - } - - /** - * Determines the recommended room version for the room. This returns an - * object with 3 properties: `version` as the new version the - * room should be upgraded to (may be the same as the current version); - * `needsUpgrade` to indicate if the room actually can be - * upgraded (ie: does the current version not match?); and `urgent` - * to indicate if the new version patches a vulnerability in a previous - * version. - * @returns - * Resolves to the version the room should be upgraded to. - */ - public async getRecommendedVersion(): Promise<IRecommendedVersion> { - const capabilities = await this.client.getCapabilities(); - let versionCap = capabilities["m.room_versions"]; - if (!versionCap) { - versionCap = { - default: KNOWN_SAFE_ROOM_VERSION, - available: {}, - }; - for (const safeVer of SAFE_ROOM_VERSIONS) { - versionCap.available[safeVer] = RoomVersionStability.Stable; - } - } - - let result = this.checkVersionAgainstCapability(versionCap); - if (result.urgent && result.needsUpgrade) { - // Something doesn't feel right: we shouldn't need to update - // because the version we're on should be in the protocol's - // namespace. This usually means that the server was updated - // before the client was, making us think the newest possible - // room version is not stable. As a solution, we'll refresh - // the capability we're using to determine this. - logger.warn( - "Refreshing room version capability because the server looks " + - "to be supporting a newer room version we don't know about.", - ); - - const caps = await this.client.getCapabilities(true); - versionCap = caps["m.room_versions"]; - if (!versionCap) { - logger.warn("No room version capability - assuming upgrade required."); - return result; - } else { - result = this.checkVersionAgainstCapability(versionCap); - } - } - - return result; - } - - private checkVersionAgainstCapability(versionCap: IRoomVersionsCapability): IRecommendedVersion { - const currentVersion = this.getVersion(); - logger.log(`[${this.roomId}] Current version: ${currentVersion}`); - logger.log(`[${this.roomId}] Version capability: `, versionCap); - - const result: IRecommendedVersion = { - version: currentVersion, - needsUpgrade: false, - urgent: false, - }; - - // If the room is on the default version then nothing needs to change - if (currentVersion === versionCap.default) return result; - - const stableVersions = Object.keys(versionCap.available).filter((v) => versionCap.available[v] === "stable"); - - // Check if the room is on an unstable version. We determine urgency based - // off the version being in the Matrix spec namespace or not (if the version - // is in the current namespace and unstable, the room is probably vulnerable). - if (!stableVersions.includes(currentVersion)) { - result.version = versionCap.default; - result.needsUpgrade = true; - result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g); - if (result.urgent) { - logger.warn(`URGENT upgrade required on ${this.roomId}`); - } else { - logger.warn(`Non-urgent upgrade required on ${this.roomId}`); - } - return result; - } - - // The room is on a stable, but non-default, version by this point. - // No upgrade needed. - return result; - } - - /** - * Determines whether the given user is permitted to perform a room upgrade - * @param userId - The ID of the user to test against - * @returns True if the given user is permitted to upgrade the room - */ - public userMayUpgradeRoom(userId: string): boolean { - return this.currentState.maySendStateEvent(EventType.RoomTombstone, userId); - } - - /** - * Get the list of pending sent events for this room - * - * @returns A list of the sent events - * waiting for remote echo. - * - * @throws If `opts.pendingEventOrdering` was not 'detached' - */ - public getPendingEvents(): MatrixEvent[] { - if (!this.pendingEventList) { - throw new Error( - "Cannot call getPendingEvents with pendingEventOrdering == " + this.opts.pendingEventOrdering, - ); - } - - return this.pendingEventList; - } - - /** - * Removes a pending event for this room - * - * @returns True if an element was removed. - */ - public removePendingEvent(eventId: string): boolean { - if (!this.pendingEventList) { - throw new Error( - "Cannot call removePendingEvent with pendingEventOrdering == " + this.opts.pendingEventOrdering, - ); - } - - const removed = utils.removeElement( - this.pendingEventList, - function (ev) { - return ev.getId() == eventId; - }, - false, - ); - - this.savePendingEvents(); - - return removed; - } - - /** - * Check whether the pending event list contains a given event by ID. - * If pending event ordering is not "detached" then this returns false. - * - * @param eventId - The event ID to check for. - */ - public hasPendingEvent(eventId: string): boolean { - return this.pendingEventList?.some((event) => event.getId() === eventId) ?? false; - } - - /** - * Get a specific event from the pending event list, if configured, null otherwise. - * - * @param eventId - The event ID to check for. - */ - public getPendingEvent(eventId: string): MatrixEvent | null { - return this.pendingEventList?.find((event) => event.getId() === eventId) ?? null; - } - - /** - * Get the live unfiltered timeline for this room. - * - * @returns live timeline - */ - public getLiveTimeline(): EventTimeline { - return this.getUnfilteredTimelineSet().getLiveTimeline(); - } - - /** - * Get the timestamp of the last message in the room - * - * @returns the timestamp of the last message in the room - */ - public getLastActiveTimestamp(): number { - const timeline = this.getLiveTimeline(); - const events = timeline.getEvents(); - if (events.length) { - const lastEvent = events[events.length - 1]; - return lastEvent.getTs(); - } else { - return Number.MIN_SAFE_INTEGER; - } - } - - /** - * @returns the membership type (join | leave | invite) for the logged in user - */ - public getMyMembership(): string { - return this.selfMembership ?? "leave"; - } - - /** - * If this room is a DM we're invited to, - * try to find out who invited us - * @returns user id of the inviter - */ - public getDMInviter(): string | undefined { - const me = this.getMember(this.myUserId); - if (me) { - return me.getDMInviter(); - } - - if (this.selfMembership === "invite") { - // fall back to summary information - const memberCount = this.getInvitedAndJoinedMemberCount(); - if (memberCount === 2) { - return this.summaryHeroes?.[0]; - } - } - } - - /** - * Assuming this room is a DM room, tries to guess with which user. - * @returns user id of the other member (could be syncing user) - */ - public guessDMUserId(): string { - const me = this.getMember(this.myUserId); - if (me) { - const inviterId = me.getDMInviter(); - if (inviterId) { - return inviterId; - } - } - // Remember, we're assuming this room is a DM, so returning the first member we find should be fine - if (Array.isArray(this.summaryHeroes) && this.summaryHeroes.length) { - return this.summaryHeroes[0]; - } - const members = this.currentState.getMembers(); - const anyMember = members.find((m) => m.userId !== this.myUserId); - if (anyMember) { - return anyMember.userId; - } - // it really seems like I'm the only user in the room - // so I probably created a room with just me in it - // and marked it as a DM. Ok then - return this.myUserId; - } - - public getAvatarFallbackMember(): RoomMember | undefined { - const memberCount = this.getInvitedAndJoinedMemberCount(); - if (memberCount > 2) { - return; - } - const hasHeroes = Array.isArray(this.summaryHeroes) && this.summaryHeroes.length; - if (hasHeroes) { - const availableMember = this.summaryHeroes!.map((userId) => { - return this.getMember(userId); - }).find((member) => !!member); - if (availableMember) { - return availableMember; - } - } - const members = this.currentState.getMembers(); - // could be different than memberCount - // as this includes left members - if (members.length <= 2) { - const availableMember = members.find((m) => { - return m.userId !== this.myUserId; - }); - if (availableMember) { - return availableMember; - } - } - // if all else fails, try falling back to a user, - // and create a one-off member for it - if (hasHeroes) { - const availableUser = this.summaryHeroes!.map((userId) => { - return this.client.getUser(userId); - }).find((user) => !!user); - if (availableUser) { - const member = new RoomMember(this.roomId, availableUser.userId); - member.user = availableUser; - return member; - } - } - } - - /** - * Sets the membership this room was received as during sync - * @param membership - join | leave | invite - */ - public updateMyMembership(membership: string): void { - const prevMembership = this.selfMembership; - this.selfMembership = membership; - if (prevMembership !== membership) { - if (membership === "leave") { - this.cleanupAfterLeaving(); - } - this.emit(RoomEvent.MyMembership, this, membership, prevMembership); - } - } - - private async loadMembersFromServer(): Promise<IStateEventWithRoomId[]> { - const lastSyncToken = this.client.store.getSyncToken(); - const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken ?? undefined); - return response.chunk; - } - - private async loadMembers(): Promise<{ memberEvents: MatrixEvent[]; fromServer: boolean }> { - // were the members loaded from the server? - let fromServer = false; - let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId); - // If the room is encrypted, we always fetch members from the server at - // least once, in case the latest state wasn't persisted properly. Note - // that this function is only called once (unless loading the members - // fails), since loadMembersIfNeeded always returns this.membersPromise - // if set, which will be the result of the first (successful) call. - if (rawMembersEvents === null || (this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))) { - fromServer = true; - rawMembersEvents = await this.loadMembersFromServer(); - logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`); - } - const memberEvents = rawMembersEvents.filter(noUnsafeEventProps).map(this.client.getEventMapper()); - return { memberEvents, fromServer }; - } - - /** - * Check if loading of out-of-band-members has completed - * - * @returns true if the full membership list of this room has been loaded (including if lazy-loading is disabled). - * False if the load is not started or is in progress. - */ - public membersLoaded(): boolean { - if (!this.opts.lazyLoadMembers) { - return true; - } - - return this.currentState.outOfBandMembersReady(); - } - - /** - * Preloads the member list in case lazy loading - * of memberships is in use. Can be called multiple times, - * it will only preload once. - * @returns when preloading is done and - * accessing the members on the room will take - * all members in the room into account - */ - public loadMembersIfNeeded(): Promise<boolean> { - if (this.membersPromise) { - return this.membersPromise; - } - - // mark the state so that incoming messages while - // the request is in flight get marked as superseding - // the OOB members - this.currentState.markOutOfBandMembersStarted(); - - const inMemoryUpdate = this.loadMembers() - .then((result) => { - this.currentState.setOutOfBandMembers(result.memberEvents); - return result.fromServer; - }) - .catch((err) => { - // allow retries on fail - this.membersPromise = undefined; - this.currentState.markOutOfBandMembersFailed(); - throw err; - }); - // update members in storage, but don't wait for it - inMemoryUpdate - .then((fromServer) => { - if (fromServer) { - const oobMembers = this.currentState - .getMembers() - .filter((m) => m.isOutOfBand()) - .map((m) => m.events.member?.event as IStateEventWithRoomId); - logger.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`); - const store = this.client.store; - return ( - store - .setOutOfBandMembers(this.roomId, oobMembers) - // swallow any IDB error as we don't want to fail - // because of this - .catch((err) => { - logger.log("LL: storing OOB room members failed, oh well", err); - }) - ); - } - }) - .catch((err) => { - // as this is not awaited anywhere, - // at least show the error in the console - logger.error(err); - }); - - this.membersPromise = inMemoryUpdate; - - return this.membersPromise; - } - - /** - * Removes the lazily loaded members from storage if needed - */ - public async clearLoadedMembersIfNeeded(): Promise<void> { - if (this.opts.lazyLoadMembers && this.membersPromise) { - await this.loadMembersIfNeeded(); - await this.client.store.clearOutOfBandMembers(this.roomId); - this.currentState.clearOutOfBandMembers(); - this.membersPromise = undefined; - } - } - - /** - * called when sync receives this room in the leave section - * to do cleanup after leaving a room. Possibly called multiple times. - */ - private cleanupAfterLeaving(): void { - this.clearLoadedMembersIfNeeded().catch((err) => { - logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`); - logger.log(err); - }); - } - - /** - * Empty out the current live timeline and re-request it. This is used when - * historical messages are imported into the room via MSC2716 `/batch_send` - * because the client may already have that section of the timeline loaded. - * We need to force the client to throw away their current timeline so that - * when they back paginate over the area again with the historical messages - * in between, it grabs the newly imported messages. We can listen for - * `UNSTABLE_MSC2716_MARKER`, in order to tell when historical messages are ready - * to be discovered in the room and the timeline needs a refresh. The SDK - * emits a `RoomEvent.HistoryImportedWithinTimeline` event when we detect a - * valid marker and can check the needs refresh status via - * `room.getTimelineNeedsRefresh()`. - */ - public async refreshLiveTimeline(): Promise<void> { - const liveTimelineBefore = this.getLiveTimeline(); - const forwardPaginationToken = liveTimelineBefore.getPaginationToken(EventTimeline.FORWARDS); - const backwardPaginationToken = liveTimelineBefore.getPaginationToken(EventTimeline.BACKWARDS); - const eventsBefore = liveTimelineBefore.getEvents(); - const mostRecentEventInTimeline = eventsBefore[eventsBefore.length - 1]; - logger.log( - `[refreshLiveTimeline for ${this.roomId}] at ` + - `mostRecentEventInTimeline=${mostRecentEventInTimeline && mostRecentEventInTimeline.getId()} ` + - `liveTimelineBefore=${liveTimelineBefore.toString()} ` + - `forwardPaginationToken=${forwardPaginationToken} ` + - `backwardPaginationToken=${backwardPaginationToken}`, - ); - - // Get the main TimelineSet - const timelineSet = this.getUnfilteredTimelineSet(); - - let newTimeline: Optional<EventTimeline>; - // If there isn't any event in the timeline, let's go fetch the latest - // event and construct a timeline from it. - // - // This should only really happen if the user ran into an error - // with refreshing the timeline before which left them in a blank - // timeline from `resetLiveTimeline`. - if (!mostRecentEventInTimeline) { - newTimeline = await this.client.getLatestTimeline(timelineSet); - } else { - // Empty out all of `this.timelineSets`. But we also need to keep the - // same `timelineSet` references around so the React code updates - // properly and doesn't ignore the room events we emit because it checks - // that the `timelineSet` references are the same. We need the - // `timelineSet` empty so that the `client.getEventTimeline(...)` call - // later, will call `/context` and create a new timeline instead of - // returning the same one. - this.resetLiveTimeline(null, null); - - // Make the UI timeline show the new blank live timeline we just - // reset so that if the network fails below it's showing the - // accurate state of what we're working with instead of the - // disconnected one in the TimelineWindow which is just hanging - // around by reference. - this.emit(RoomEvent.TimelineRefresh, this, timelineSet); - - // Use `client.getEventTimeline(...)` to construct a new timeline from a - // `/context` response state and events for the most recent event before - // we reset everything. The `timelineSet` we pass in needs to be empty - // in order for this function to call `/context` and generate a new - // timeline. - newTimeline = await this.client.getEventTimeline(timelineSet, mostRecentEventInTimeline.getId()!); - } - - // If a racing `/sync` beat us to creating a new timeline, use that - // instead because it's the latest in the room and any new messages in - // the scrollback will include the history. - const liveTimeline = timelineSet.getLiveTimeline(); - if ( - !liveTimeline || - (liveTimeline.getPaginationToken(Direction.Forward) === null && - liveTimeline.getPaginationToken(Direction.Backward) === null && - liveTimeline.getEvents().length === 0) - ) { - logger.log(`[refreshLiveTimeline for ${this.roomId}] using our new live timeline`); - // Set the pagination token back to the live sync token (`null`) instead - // of using the `/context` historical token (ex. `t12-13_0_0_0_0_0_0_0_0`) - // so that it matches the next response from `/sync` and we can properly - // continue the timeline. - newTimeline!.setPaginationToken(forwardPaginationToken, EventTimeline.FORWARDS); - - // Set our new fresh timeline as the live timeline to continue syncing - // forwards and back paginating from. - timelineSet.setLiveTimeline(newTimeline!); - // Fixup `this.oldstate` so that `scrollback` has the pagination tokens - // available - this.fixUpLegacyTimelineFields(); - } else { - logger.log( - `[refreshLiveTimeline for ${this.roomId}] \`/sync\` or some other request beat us to creating a new ` + - `live timeline after we reset it. We'll use that instead since any events in the scrollback from ` + - `this timeline will include the history.`, - ); - } - - // The timeline has now been refreshed ✅ - this.setTimelineNeedsRefresh(false); - - // Emit an event which clients can react to and re-load the timeline - // from the SDK - this.emit(RoomEvent.TimelineRefresh, this, timelineSet); - } - - /** - * Reset the live timeline of all timelineSets, and start new ones. - * - * <p>This is used when /sync returns a 'limited' timeline. - * - * @param backPaginationToken - token for back-paginating the new timeline - * @param forwardPaginationToken - token for forward-paginating the old live timeline, - * if absent or null, all timelines are reset, removing old ones (including the previous live - * timeline which would otherwise be unable to paginate forwards without this token). - * Removing just the old live timeline whilst preserving previous ones is not supported. - */ - public resetLiveTimeline(backPaginationToken?: string | null, forwardPaginationToken?: string | null): void { - for (const timelineSet of this.timelineSets) { - timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined); - } - for (const thread of this.threads.values()) { - thread.resetLiveTimeline(backPaginationToken, forwardPaginationToken); - } - - this.fixUpLegacyTimelineFields(); - } - - /** - * Fix up this.timeline, this.oldState and this.currentState - * - * @internal - */ - private fixUpLegacyTimelineFields(): void { - const previousOldState = this.oldState; - const previousCurrentState = this.currentState; - - // maintain this.timeline as a reference to the live timeline, - // and this.oldState and this.currentState as references to the - // state at the start and end of that timeline. These are more - // for backwards-compatibility than anything else. - this.timeline = this.getLiveTimeline().getEvents(); - this.oldState = this.getLiveTimeline().getState(EventTimeline.BACKWARDS)!; - this.currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS)!; - - // Let people know to register new listeners for the new state - // references. The reference won't necessarily change every time so only - // emit when we see a change. - if (previousOldState !== this.oldState) { - this.emit(RoomEvent.OldStateUpdated, this, previousOldState, this.oldState); - } - - if (previousCurrentState !== this.currentState) { - this.emit(RoomEvent.CurrentStateUpdated, this, previousCurrentState, this.currentState); - - // Re-emit various events on the current room state - // TODO: If currentState really only exists for backwards - // compatibility, shouldn't we be doing this some other way? - this.reEmitter.stopReEmitting(previousCurrentState, [ - RoomStateEvent.Events, - RoomStateEvent.Members, - RoomStateEvent.NewMember, - RoomStateEvent.Update, - RoomStateEvent.Marker, - BeaconEvent.New, - BeaconEvent.Update, - BeaconEvent.Destroy, - BeaconEvent.LivenessChange, - ]); - this.reEmitter.reEmit(this.currentState, [ - RoomStateEvent.Events, - RoomStateEvent.Members, - RoomStateEvent.NewMember, - RoomStateEvent.Update, - RoomStateEvent.Marker, - BeaconEvent.New, - BeaconEvent.Update, - BeaconEvent.Destroy, - BeaconEvent.LivenessChange, - ]); - } - } - - /** - * Returns whether there are any devices in the room that are unverified - * - * Note: Callers should first check if crypto is enabled on this device. If it is - * disabled, then we aren't tracking room devices at all, so we can't answer this, and an - * error will be thrown. - * - * @returns the result - */ - public async hasUnverifiedDevices(): Promise<boolean> { - if (!this.client.isRoomEncrypted(this.roomId)) { - return false; - } - const e2eMembers = await this.getEncryptionTargetMembers(); - for (const member of e2eMembers) { - const devices = this.client.getStoredDevicesForUser(member.userId); - if (devices.some((device) => device.isUnverified())) { - return true; - } - } - return false; - } - - /** - * Return the timeline sets for this room. - * @returns array of timeline sets for this room - */ - public getTimelineSets(): EventTimelineSet[] { - return this.timelineSets; - } - - /** - * Helper to return the main unfiltered timeline set for this room - * @returns room's unfiltered timeline set - */ - public getUnfilteredTimelineSet(): EventTimelineSet { - return this.timelineSets[0]; - } - - /** - * Get the timeline which contains the given event from the unfiltered set, if any - * - * @param eventId - event ID to look for - * @returns timeline containing - * the given event, or null if unknown - */ - public getTimelineForEvent(eventId: string): EventTimeline | null { - const event = this.findEventById(eventId); - const thread = this.findThreadForEvent(event); - if (thread) { - return thread.timelineSet.getTimelineForEvent(eventId); - } else { - return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId); - } - } - - /** - * Add a new timeline to this room's unfiltered timeline set - * - * @returns newly-created timeline - */ - public addTimeline(): EventTimeline { - return this.getUnfilteredTimelineSet().addTimeline(); - } - - /** - * Whether the timeline needs to be refreshed in order to pull in new - * historical messages that were imported. - * @param value - The value to set - */ - public setTimelineNeedsRefresh(value: boolean): void { - this.timelineNeedsRefresh = value; - } - - /** - * Whether the timeline needs to be refreshed in order to pull in new - * historical messages that were imported. - * @returns . - */ - public getTimelineNeedsRefresh(): boolean { - return this.timelineNeedsRefresh; - } - - /** - * Get an event which is stored in our unfiltered timeline set, or in a thread - * - * @param eventId - event ID to look for - * @returns the given event, or undefined if unknown - */ - public findEventById(eventId: string): MatrixEvent | undefined { - let event = this.getUnfilteredTimelineSet().findEventById(eventId); - - if (!event) { - const threads = this.getThreads(); - for (let i = 0; i < threads.length; i++) { - const thread = threads[i]; - event = thread.findEventById(eventId); - if (event) { - return event; - } - } - } - - return event; - } - - /** - * Get one of the notification counts for this room - * @param type - The type of notification count to get. default: 'total' - * @returns The notification count, or undefined if there is no count - * for this type. - */ - public getUnreadNotificationCount(type = NotificationCountType.Total): number { - let count = this.getRoomUnreadNotificationCount(type); - for (const threadNotification of this.threadNotifications.values()) { - count += threadNotification[type] ?? 0; - } - return count; - } - - /** - * Get the notification for the event context (room or thread timeline) - */ - public getUnreadCountForEventContext(type = NotificationCountType.Total, event: MatrixEvent): number { - const isThreadEvent = !!event.threadRootId && !event.isThreadRoot; - - return ( - (isThreadEvent - ? this.getThreadUnreadNotificationCount(event.threadRootId, type) - : this.getRoomUnreadNotificationCount(type)) ?? 0 - ); - } - - /** - * Get one of the notification counts for this room - * @param type - The type of notification count to get. default: 'total' - * @returns The notification count, or undefined if there is no count - * for this type. - */ - public getRoomUnreadNotificationCount(type = NotificationCountType.Total): number { - return this.notificationCounts[type] ?? 0; - } - - /** - * Get one of the notification counts for a thread - * @param threadId - the root event ID - * @param type - The type of notification count to get. default: 'total' - * @returns The notification count, or undefined if there is no count - * for this type. - */ - public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number { - return this.threadNotifications.get(threadId)?.[type] ?? 0; - } - - /** - * Checks if the current room has unread thread notifications - * @returns - */ - public hasThreadUnreadNotification(): boolean { - for (const notification of this.threadNotifications.values()) { - if ((notification.highlight ?? 0) > 0 || (notification.total ?? 0) > 0) { - return true; - } - } - return false; - } - - /** - * Swet one of the notification count for a thread - * @param threadId - the root event ID - * @param type - The type of notification count to get. default: 'total' - * @returns - */ - public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void { - const notification: NotificationCount = { - highlight: this.threadNotifications.get(threadId)?.highlight, - total: this.threadNotifications.get(threadId)?.total, - ...{ - [type]: count, - }, - }; - - this.threadNotifications.set(threadId, notification); - - this.emit(RoomEvent.UnreadNotifications, notification, threadId); - } - - /** - * @returns the notification count type for all the threads in the room - */ - public get threadsAggregateNotificationType(): NotificationCountType | null { - let type: NotificationCountType | null = null; - for (const threadNotification of this.threadNotifications.values()) { - if ((threadNotification.highlight ?? 0) > 0) { - return NotificationCountType.Highlight; - } else if ((threadNotification.total ?? 0) > 0 && !type) { - type = NotificationCountType.Total; - } - } - return type; - } - - /** - * Resets the thread notifications for this room - */ - public resetThreadUnreadNotificationCount(notificationsToKeep?: string[]): void { - if (notificationsToKeep) { - for (const [threadId] of this.threadNotifications) { - if (!notificationsToKeep.includes(threadId)) { - this.threadNotifications.delete(threadId); - } - } - } else { - this.threadNotifications.clear(); - } - this.emit(RoomEvent.UnreadNotifications); - } - - /** - * Set one of the notification counts for this room - * @param type - The type of notification count to set. - * @param count - The new count - */ - public setUnreadNotificationCount(type: NotificationCountType, count: number): void { - this.notificationCounts[type] = count; - this.emit(RoomEvent.UnreadNotifications, this.notificationCounts); - } - - public setUnread(type: NotificationCountType, count: number): void { - return this.setUnreadNotificationCount(type, count); - } - - public setSummary(summary: IRoomSummary): void { - const heroes = summary["m.heroes"]; - const joinedCount = summary["m.joined_member_count"]; - const invitedCount = summary["m.invited_member_count"]; - if (Number.isInteger(joinedCount)) { - this.currentState.setJoinedMemberCount(joinedCount!); - } - if (Number.isInteger(invitedCount)) { - this.currentState.setInvitedMemberCount(invitedCount!); - } - if (Array.isArray(heroes)) { - // be cautious about trusting server values, - // and make sure heroes doesn't contain our own id - // just to be sure - this.summaryHeroes = heroes.filter((userId) => { - return userId !== this.myUserId; - }); - } - } - - /** - * Whether to send encrypted messages to devices within this room. - * @param value - true to blacklist unverified devices, null - * to use the global value for this room. - */ - public setBlacklistUnverifiedDevices(value: boolean): void { - this.blacklistUnverifiedDevices = value; - } - - /** - * Whether to send encrypted messages to devices within this room. - * @returns true if blacklisting unverified devices, null - * if the global value should be used for this room. - */ - public getBlacklistUnverifiedDevices(): boolean | null { - if (this.blacklistUnverifiedDevices === undefined) return null; - return this.blacklistUnverifiedDevices; - } - - /** - * Get the avatar URL for a room if one was set. - * @param baseUrl - The homeserver base URL. See - * {@link MatrixClient#getHomeserverUrl}. - * @param width - The desired width of the thumbnail. - * @param height - The desired height of the thumbnail. - * @param resizeMethod - The thumbnail resize method to use, either - * "crop" or "scale". - * @param allowDefault - True to allow an identicon for this room if an - * avatar URL wasn't explicitly set. Default: true. (Deprecated) - * @returns the avatar URL or null. - */ - public getAvatarUrl( - baseUrl: string, - width: number, - height: number, - resizeMethod: ResizeMethod, - allowDefault = true, - ): string | null { - const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, ""); - if (!roomAvatarEvent && !allowDefault) { - return null; - } - - const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null; - if (mainUrl) { - return getHttpUriForMxc(baseUrl, mainUrl, width, height, resizeMethod); - } - - return null; - } - - /** - * Get the mxc avatar url for the room, if one was set. - * @returns the mxc avatar url or falsy - */ - public getMxcAvatarUrl(): string | null { - return this.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url || null; - } - - /** - * Get this room's canonical alias - * The alias returned by this function may not necessarily - * still point to this room. - * @returns The room's canonical alias, or null if there is none - */ - public getCanonicalAlias(): string | null { - const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, ""); - if (canonicalAlias) { - return canonicalAlias.getContent().alias || null; - } - return null; - } - - /** - * Get this room's alternative aliases - * @returns The room's alternative aliases, or an empty array - */ - public getAltAliases(): string[] { - const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, ""); - if (canonicalAlias) { - return canonicalAlias.getContent().alt_aliases || []; - } - return []; - } - - /** - * Add events to a timeline - * - * <p>Will fire "Room.timeline" for each event added. - * - * @param events - A list of events to add. - * - * @param toStartOfTimeline - True to add these events to the start - * (oldest) instead of the end (newest) of the timeline. If true, the oldest - * event will be the <b>last</b> element of 'events'. - * - * @param timeline - timeline to - * add events to. - * - * @param paginationToken - token for the next batch of events - * - * @remarks - * Fires {@link RoomEvent.Timeline} - */ - public addEventsToTimeline( - events: MatrixEvent[], - toStartOfTimeline: boolean, - timeline: EventTimeline, - paginationToken?: string, - ): void { - timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken); - } - - /** - * Get the instance of the thread associated with the current event - * @param eventId - the ID of the current event - * @returns a thread instance if known - */ - public getThread(eventId: string): Thread | null { - return this.threads.get(eventId) ?? null; - } - - /** - * Get all the known threads in the room - */ - public getThreads(): Thread[] { - return Array.from(this.threads.values()); - } - - /** - * Get a member from the current room state. - * @param userId - The user ID of the member. - * @returns The member or `null`. - */ - public getMember(userId: string): RoomMember | null { - return this.currentState.getMember(userId); - } - - /** - * Get all currently loaded members from the current - * room state. - * @returns Room members - */ - public getMembers(): RoomMember[] { - return this.currentState.getMembers(); - } - - /** - * Get a list of members whose membership state is "join". - * @returns A list of currently joined members. - */ - public getJoinedMembers(): RoomMember[] { - return this.getMembersWithMembership("join"); - } - - /** - * Returns the number of joined members in this room - * This method caches the result. - * This is a wrapper around the method of the same name in roomState, returning - * its result for the room's current state. - * @returns The number of members in this room whose membership is 'join' - */ - public getJoinedMemberCount(): number { - return this.currentState.getJoinedMemberCount(); - } - - /** - * Returns the number of invited members in this room - * @returns The number of members in this room whose membership is 'invite' - */ - public getInvitedMemberCount(): number { - return this.currentState.getInvitedMemberCount(); - } - - /** - * Returns the number of invited + joined members in this room - * @returns The number of members in this room whose membership is 'invite' or 'join' - */ - public getInvitedAndJoinedMemberCount(): number { - return this.getInvitedMemberCount() + this.getJoinedMemberCount(); - } - - /** - * Get a list of members with given membership state. - * @param membership - The membership state. - * @returns A list of members with the given membership state. - */ - public getMembersWithMembership(membership: string): RoomMember[] { - return this.currentState.getMembers().filter(function (m) { - return m.membership === membership; - }); - } - - /** - * Get a list of members we should be encrypting for in this room - * @returns A list of members who - * we should encrypt messages for in this room. - */ - public async getEncryptionTargetMembers(): Promise<RoomMember[]> { - await this.loadMembersIfNeeded(); - let members = this.getMembersWithMembership("join"); - if (this.shouldEncryptForInvitedMembers()) { - members = members.concat(this.getMembersWithMembership("invite")); - } - return members; - } - - /** - * Determine whether we should encrypt messages for invited users in this room - * @returns if we should encrypt messages for invited users - */ - public shouldEncryptForInvitedMembers(): boolean { - const ev = this.currentState.getStateEvents(EventType.RoomHistoryVisibility, ""); - return ev?.getContent()?.history_visibility !== "joined"; - } - - /** - * Get the default room name (i.e. what a given user would see if the - * room had no m.room.name) - * @param userId - The userId from whose perspective we want - * to calculate the default name - * @returns The default room name - */ - public getDefaultRoomName(userId: string): string { - return this.calculateRoomName(userId, true); - } - - /** - * Check if the given user_id has the given membership state. - * @param userId - The user ID to check. - * @param membership - The membership e.g. `'join'` - * @returns True if this user_id has the given membership state. - */ - public hasMembershipState(userId: string, membership: string): boolean { - const member = this.getMember(userId); - if (!member) { - return false; - } - return member.membership === membership; - } - - /** - * Add a timelineSet for this room with the given filter - * @param filter - The filter to be applied to this timelineSet - * @param opts - Configuration options - * @returns The timelineSet - */ - public getOrCreateFilteredTimelineSet( - filter: Filter, - { prepopulateTimeline = true, useSyncEvents = true, pendingEvents = true }: ICreateFilterOpts = {}, - ): EventTimelineSet { - if (this.filteredTimelineSets[filter.filterId!]) { - return this.filteredTimelineSets[filter.filterId!]; - } - const opts = Object.assign({ filter, pendingEvents }, this.opts); - const timelineSet = new EventTimelineSet(this, opts); - this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]); - if (useSyncEvents) { - this.filteredTimelineSets[filter.filterId!] = timelineSet; - this.timelineSets.push(timelineSet); - } - - const unfilteredLiveTimeline = this.getLiveTimeline(); - // Not all filter are possible to replicate client-side only - // When that's the case we do not want to prepopulate from the live timeline - // as we would get incorrect results compared to what the server would send back - if (prepopulateTimeline) { - // populate up the new timelineSet with filtered events from our live - // unfiltered timeline. - // - // XXX: This is risky as our timeline - // may have grown huge and so take a long time to filter. - // see https://github.com/vector-im/vector-web/issues/2109 - - unfilteredLiveTimeline.getEvents().forEach(function (event) { - timelineSet.addLiveEvent(event); - }); - - // find the earliest unfiltered timeline - let timeline = unfilteredLiveTimeline; - while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) { - timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)!; - } - - timelineSet - .getLiveTimeline() - .setPaginationToken(timeline.getPaginationToken(EventTimeline.BACKWARDS), EventTimeline.BACKWARDS); - } else if (useSyncEvents) { - const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward); - timelineSet.getLiveTimeline().setPaginationToken(livePaginationToken, Direction.Backward); - } - - // alternatively, we could try to do something like this to try and re-paginate - // in the filtered events from nothing, but Mark says it's an abuse of the API - // to do so: - // - // timelineSet.resetLiveTimeline( - // unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS) - // ); - - return timelineSet; - } - - private async getThreadListFilter(filterType = ThreadFilterType.All): Promise<Filter> { - const myUserId = this.client.getUserId()!; - const filter = new Filter(myUserId); - - const definition: IFilterDefinition = { - room: { - timeline: { - [FILTER_RELATED_BY_REL_TYPES.name]: [THREAD_RELATION_TYPE.name], - }, - }, - }; - - if (filterType === ThreadFilterType.My) { - definition!.room!.timeline![FILTER_RELATED_BY_SENDERS.name] = [myUserId]; - } - - filter.setDefinition(definition); - const filterId = await this.client.getOrCreateFilter(`THREAD_PANEL_${this.roomId}_${filterType}`, filter); - - filter.filterId = filterId; - - return filter; - } - - private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise<EventTimelineSet> { - let timelineSet: EventTimelineSet; - if (Thread.hasServerSideListSupport) { - timelineSet = new EventTimelineSet( - this, - { - ...this.opts, - pendingEvents: false, - }, - undefined, - undefined, - filterType ?? ThreadFilterType.All, - ); - this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]); - } else if (Thread.hasServerSideSupport) { - const filter = await this.getThreadListFilter(filterType); - - timelineSet = this.getOrCreateFilteredTimelineSet(filter, { - prepopulateTimeline: false, - useSyncEvents: false, - pendingEvents: false, - }); - } else { - timelineSet = new EventTimelineSet(this, { - pendingEvents: false, - }); - - Array.from(this.threads).forEach(([, thread]) => { - if (thread.length === 0) return; - const currentUserParticipated = thread.timeline.some((event) => { - return event.getSender() === this.client.getUserId(); - }); - if (filterType !== ThreadFilterType.My || currentUserParticipated) { - timelineSet.getLiveTimeline().addEvent(thread.rootEvent!, { - toStartOfTimeline: false, - }); - } - }); - } - - return timelineSet; - } - - private threadsReady = false; - - /** - * Takes the given thread root events and creates threads for them. - */ - public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void { - for (const rootEvent of events) { - EventTimeline.setEventMetadata(rootEvent, this.currentState, toStartOfTimeline); - if (!this.getThread(rootEvent.getId()!)) { - this.createThread(rootEvent.getId()!, rootEvent, [], toStartOfTimeline); - } - } - } - - /** - * Fetch the bare minimum of room threads required for the thread list to work reliably. - * With server support that means fetching one page. - * Without server support that means fetching as much at once as the server allows us to. - */ - public async fetchRoomThreads(): Promise<void> { - if (this.threadsReady || !this.client.supportsThreads()) { - return; - } - - if (Thread.hasServerSideListSupport) { - await Promise.all([ - this.fetchRoomThreadList(ThreadFilterType.All), - this.fetchRoomThreadList(ThreadFilterType.My), - ]); - } else { - const allThreadsFilter = await this.getThreadListFilter(); - - const { chunk: events } = await this.client.createMessagesRequest( - this.roomId, - "", - Number.MAX_SAFE_INTEGER, - Direction.Backward, - allThreadsFilter, - ); - - if (!events.length) return; - - // Sorted by last_reply origin_server_ts - const threadRoots = events.map(this.client.getEventMapper()).sort((eventA, eventB) => { - /** - * `origin_server_ts` in a decentralised world is far from ideal - * but for lack of any better, we will have to use this - * Long term the sorting should be handled by homeservers and this - * is only meant as a short term patch - */ - const threadAMetadata = eventA.getServerAggregatedRelation<IThreadBundledRelationship>( - THREAD_RELATION_TYPE.name, - )!; - const threadBMetadata = eventB.getServerAggregatedRelation<IThreadBundledRelationship>( - THREAD_RELATION_TYPE.name, - )!; - return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts; - }); - - let latestMyThreadsRootEvent: MatrixEvent | undefined; - const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); - for (const rootEvent of threadRoots) { - const opts = { - duplicateStrategy: DuplicateStrategy.Ignore, - fromCache: false, - roomState, - }; - this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts); - - const threadRelationship = rootEvent.getServerAggregatedRelation<IThreadBundledRelationship>( - THREAD_RELATION_TYPE.name, - ); - if (threadRelationship?.current_user_participated) { - this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, opts); - latestMyThreadsRootEvent = rootEvent; - } - } - - this.processThreadRoots(threadRoots, true); - - this.client.decryptEventIfNeeded(threadRoots[threadRoots.length - 1]); - if (latestMyThreadsRootEvent) { - this.client.decryptEventIfNeeded(latestMyThreadsRootEvent); - } - } - - this.on(ThreadEvent.NewReply, this.onThreadNewReply); - this.on(ThreadEvent.Delete, this.onThreadDelete); - this.threadsReady = true; - } - - public async processPollEvents(events: MatrixEvent[]): Promise<void> { - const processPollStartEvent = (event: MatrixEvent): void => { - if (!M_POLL_START.matches(event.getType())) return; - try { - const poll = new Poll(event, this.client, this); - this.polls.set(event.getId()!, poll); - this.emit(PollEvent.New, poll); - } catch {} - // poll creation can fail for malformed poll start events - }; - - const processPollRelationEvent = (event: MatrixEvent): void => { - const relationEventId = event.relationEventId; - if (relationEventId && this.polls.has(relationEventId)) { - const poll = this.polls.get(relationEventId); - poll?.onNewRelation(event); - } - }; - - const processPollEvent = (event: MatrixEvent): void => { - processPollStartEvent(event); - processPollRelationEvent(event); - }; - - for (const event of events) { - try { - await this.client.decryptEventIfNeeded(event); - processPollEvent(event); - } catch {} - } - } - - /** - * Fetch a single page of threadlist messages for the specific thread filter - * @internal - */ - private async fetchRoomThreadList(filter?: ThreadFilterType): Promise<void> { - const timelineSet = filter === ThreadFilterType.My ? this.threadsTimelineSets[1] : this.threadsTimelineSets[0]; - - const { chunk: events, end } = await this.client.createThreadListMessagesRequest( - this.roomId, - null, - undefined, - Direction.Backward, - timelineSet.threadListType, - timelineSet.getFilter(), - ); - - timelineSet.getLiveTimeline().setPaginationToken(end ?? null, Direction.Backward); - - if (!events.length) return; - - const matrixEvents = events.map(this.client.getEventMapper()); - this.processThreadRoots(matrixEvents, true); - const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); - for (const rootEvent of matrixEvents) { - timelineSet.addLiveEvent(rootEvent, { - duplicateStrategy: DuplicateStrategy.Replace, - fromCache: false, - roomState, - }); - } - } - - private onThreadNewReply(thread: Thread): void { - this.updateThreadRootEvents(thread, false, true); - } - - private onThreadDelete(thread: Thread): void { - this.threads.delete(thread.id); - - const timeline = this.getTimelineForEvent(thread.id); - const roomEvent = timeline?.getEvents()?.find((it) => it.getId() === thread.id); - if (roomEvent) { - thread.clearEventMetadata(roomEvent); - } else { - logger.debug("onThreadDelete: Could not find root event in room timeline"); - } - for (const timelineSet of this.threadsTimelineSets) { - timelineSet.removeEvent(thread.id); - } - } - - /** - * Forget the timelineSet for this room with the given filter - * - * @param filter - the filter whose timelineSet is to be forgotten - */ - public removeFilteredTimelineSet(filter: Filter): void { - const timelineSet = this.filteredTimelineSets[filter.filterId!]; - delete this.filteredTimelineSets[filter.filterId!]; - const i = this.timelineSets.indexOf(timelineSet); - if (i > -1) { - this.timelineSets.splice(i, 1); - } - } - - public eventShouldLiveIn( - event: MatrixEvent, - events?: MatrixEvent[], - roots?: Set<string>, - ): { - shouldLiveInRoom: boolean; - shouldLiveInThread: boolean; - threadId?: string; - } { - if (!this.client?.supportsThreads()) { - return { - shouldLiveInRoom: true, - shouldLiveInThread: false, - }; - } - - // A thread root is always shown in both timelines - if (event.isThreadRoot || roots?.has(event.getId()!)) { - return { - shouldLiveInRoom: true, - shouldLiveInThread: true, - threadId: event.getId(), - }; - } - - // A thread relation is always only shown in a thread - if (event.isRelation(THREAD_RELATION_TYPE.name)) { - return { - shouldLiveInRoom: false, - shouldLiveInThread: true, - threadId: event.threadRootId, - }; - } - - const parentEventId = event.getAssociatedId(); - let parentEvent: MatrixEvent | undefined; - if (parentEventId) { - parentEvent = this.findEventById(parentEventId) ?? events?.find((e) => e.getId() === parentEventId); - } - - // Treat relations and redactions as extensions of their parents so evaluate parentEvent instead - if (parentEvent && (event.isRelation() || event.isRedaction())) { - return this.eventShouldLiveIn(parentEvent, events, roots); - } - - // Edge case where we know the event is a relation but don't have the parentEvent - if (roots?.has(event.relationEventId!)) { - return { - shouldLiveInRoom: true, - shouldLiveInThread: true, - threadId: event.relationEventId, - }; - } - - // We've exhausted all scenarios, can safely assume that this event should live in the room timeline only - return { - shouldLiveInRoom: true, - shouldLiveInThread: false, - }; - } - - public findThreadForEvent(event?: MatrixEvent): Thread | null { - if (!event) return null; - - const { threadId } = this.eventShouldLiveIn(event); - return threadId ? this.getThread(threadId) : null; - } - - private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void { - let thread = this.getThread(threadId); - - if (!thread) { - const rootEvent = this.findEventById(threadId) ?? events.find((e) => e.getId() === threadId); - thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline); - } - - thread.addEvents(events, toStartOfTimeline); - } - - /** - * Adds events to a thread's timeline. Will fire "Thread.update" - */ - public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void { - events.forEach(this.applyRedaction); - - const eventsByThread: { [threadId: string]: MatrixEvent[] } = {}; - for (const event of events) { - const { threadId, shouldLiveInThread } = this.eventShouldLiveIn(event); - if (shouldLiveInThread && !eventsByThread[threadId!]) { - eventsByThread[threadId!] = []; - } - eventsByThread[threadId!]?.push(event); - } - - Object.entries(eventsByThread).map(([threadId, threadEvents]) => - this.addThreadedEvents(threadId, threadEvents, toStartOfTimeline), - ); - } - - private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean, recreateEvent: boolean): void => { - if (thread.length) { - this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline, recreateEvent); - if (thread.hasCurrentUserParticipated) { - this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline, recreateEvent); - } - } - }; - - private updateThreadRootEvent = ( - timelineSet: Optional<EventTimelineSet>, - thread: Thread, - toStartOfTimeline: boolean, - recreateEvent: boolean, - ): void => { - if (timelineSet && thread.rootEvent) { - if (recreateEvent) { - timelineSet.removeEvent(thread.id); - } - if (Thread.hasServerSideSupport) { - timelineSet.addLiveEvent(thread.rootEvent, { - duplicateStrategy: DuplicateStrategy.Replace, - fromCache: false, - roomState: this.currentState, - }); - } else { - timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), { toStartOfTimeline }); - } - } - }; - - public createThread( - threadId: string, - rootEvent: MatrixEvent | undefined, - events: MatrixEvent[] = [], - toStartOfTimeline: boolean, - ): Thread { - if (this.threads.has(threadId)) { - return this.threads.get(threadId)!; - } - - if (rootEvent) { - const relatedEvents = this.relations.getAllChildEventsForEvent(rootEvent.getId()!); - if (relatedEvents?.length) { - // Include all relations of the root event, given it'll be visible in both timelines, - // except `m.replace` as that will already be applied atop the event using `MatrixEvent::makeReplaced` - events = events.concat(relatedEvents.filter((e) => !e.isRelation(RelationType.Replace))); - } - } - - const thread = new Thread(threadId, rootEvent, { - room: this, - client: this.client, - pendingEventOrdering: this.opts.pendingEventOrdering, - receipts: this.cachedThreadReadReceipts.get(threadId) ?? [], - }); - - // All read receipts should now come down from sync, we do not need to keep - // a reference to the cached receipts anymore. - this.cachedThreadReadReceipts.delete(threadId); - - // If we managed to create a thread and figure out its `id` then we can use it - // This has to happen before thread.addEvents, because that adds events to the eventtimeline, and the - // eventtimeline sometimes looks up thread information via the room. - this.threads.set(thread.id, thread); - - // This is necessary to be able to jump to events in threads: - // If we jump to an event in a thread where neither the event, nor the root, - // nor any thread event are loaded yet, we'll load the event as well as the thread root, create the thread, - // and pass the event through this. - thread.addEvents(events, false); - - this.reEmitter.reEmit(thread, [ - ThreadEvent.Delete, - ThreadEvent.Update, - ThreadEvent.NewReply, - RoomEvent.Timeline, - RoomEvent.TimelineReset, - ]); - const isNewer = - this.lastThread?.rootEvent && - rootEvent?.localTimestamp && - this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp; - - if (!this.lastThread || isNewer) { - this.lastThread = thread; - } - - if (this.threadsReady) { - this.updateThreadRootEvents(thread, toStartOfTimeline, false); - } - this.emit(ThreadEvent.New, thread, toStartOfTimeline); - - return thread; - } - - private applyRedaction = (event: MatrixEvent): void => { - if (event.isRedaction()) { - const redactId = event.event.redacts; - - // if we know about this event, redact its contents now. - const redactedEvent = redactId ? this.findEventById(redactId) : undefined; - if (redactedEvent) { - redactedEvent.makeRedacted(event); - - // If this is in the current state, replace it with the redacted version - if (redactedEvent.isState()) { - const currentStateEvent = this.currentState.getStateEvents( - redactedEvent.getType(), - redactedEvent.getStateKey()!, - ); - if (currentStateEvent?.getId() === redactedEvent.getId()) { - this.currentState.setStateEvents([redactedEvent]); - } - } - - this.emit(RoomEvent.Redaction, event, this); - - // TODO: we stash user displaynames (among other things) in - // RoomMember objects which are then attached to other events - // (in the sender and target fields). We should get those - // RoomMember objects to update themselves when the events that - // they are based on are changed. - - // Remove any visibility change on this event. - this.visibilityEvents.delete(redactId!); - - // If this event is a visibility change event, remove it from the - // list of visibility changes and update any event affected by it. - if (redactedEvent.isVisibilityEvent()) { - this.redactVisibilityChangeEvent(event); - } - } - - // FIXME: apply redactions to notification list - - // NB: We continue to add the redaction event to the timeline so - // clients can say "so and so redacted an event" if they wish to. Also - // this may be needed to trigger an update. - } - }; - - private processLiveEvent(event: MatrixEvent): void { - this.applyRedaction(event); - - // Implement MSC3531: hiding messages. - if (event.isVisibilityEvent()) { - // This event changes the visibility of another event, record - // the visibility change, inform clients if necessary. - this.applyNewVisibilityEvent(event); - } - // If any pending visibility change is waiting for this (older) event, - this.applyPendingVisibilityEvents(event); - - // Sliding Sync modifications: - // The proxy cannot guarantee every sent event will have a transaction_id field, so we need - // to check the event ID against the list of pending events if there is no transaction ID - // field. Only do this for events sent by us though as it's potentially expensive to loop - // the pending events map. - const txnId = event.getUnsigned().transaction_id; - if (!txnId && event.getSender() === this.myUserId) { - // check the txn map for a matching event ID - for (const [tid, localEvent] of this.txnToEvent) { - if (localEvent.getId() === event.getId()) { - logger.debug("processLiveEvent: found sent event without txn ID: ", tid, event.getId()); - // update the unsigned field so we can re-use the same codepaths - const unsigned = event.getUnsigned(); - unsigned.transaction_id = tid; - event.setUnsigned(unsigned); - break; - } - } - } - } - - /** - * Add an event to the end of this room's live timelines. Will fire - * "Room.timeline". - * - * @param event - Event to be added - * @param addLiveEventOptions - addLiveEvent options - * @internal - * - * @remarks - * Fires {@link RoomEvent.Timeline} - */ - private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void { - const { duplicateStrategy, timelineWasEmpty, fromCache } = addLiveEventOptions; - - // add to our timeline sets - for (const timelineSet of this.timelineSets) { - timelineSet.addLiveEvent(event, { - duplicateStrategy, - fromCache, - timelineWasEmpty, - }); - } - - // synthesize and inject implicit read receipts - // Done after adding the event because otherwise the app would get a read receipt - // pointing to an event that wasn't yet in the timeline - // Don't synthesize RR for m.room.redaction as this causes the RR to go missing. - if (event.sender && event.getType() !== EventType.RoomRedaction) { - this.addReceipt(synthesizeReceipt(event.sender.userId, event, ReceiptType.Read), true); - - // Any live events from a user could be taken as implicit - // presence information: evidence that they are currently active. - // ...except in a world where we use 'user.currentlyActive' to reduce - // presence spam, this isn't very useful - we'll get a transition when - // they are no longer currently active anyway. So don't bother to - // reset the lastActiveAgo and lastPresenceTs from the RoomState's user. - } - } - - /** - * Add a pending outgoing event to this room. - * - * <p>The event is added to either the pendingEventList, or the live timeline, - * depending on the setting of opts.pendingEventOrdering. - * - * <p>This is an internal method, intended for use by MatrixClient. - * - * @param event - The event to add. - * - * @param txnId - Transaction id for this outgoing event - * - * @throws if the event doesn't have status SENDING, or we aren't given a - * unique transaction id. - * - * @remarks - * Fires {@link RoomEvent.LocalEchoUpdated} - */ - public addPendingEvent(event: MatrixEvent, txnId: string): void { - if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) { - throw new Error("addPendingEvent called on an event with status " + event.status); - } - - if (this.txnToEvent.get(txnId)) { - throw new Error("addPendingEvent called on an event with known txnId " + txnId); - } - - // call setEventMetadata to set up event.sender etc - // as event is shared over all timelineSets, we set up its metadata based - // on the unfiltered timelineSet. - EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS)!, false); - - this.txnToEvent.set(txnId, event); - if (this.pendingEventList) { - if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) { - logger.warn("Setting event as NOT_SENT due to messages in the same state"); - event.setStatus(EventStatus.NOT_SENT); - } - this.pendingEventList.push(event); - this.savePendingEvents(); - if (event.isRelation()) { - // For pending events, add them to the relations collection immediately. - // (The alternate case below already covers this as part of adding to - // the timeline set.) - this.aggregateNonLiveRelation(event); - } - - if (event.isRedaction()) { - const redactId = event.event.redacts; - let redactedEvent = this.pendingEventList.find((e) => e.getId() === redactId); - if (!redactedEvent && redactId) { - redactedEvent = this.findEventById(redactId); - } - if (redactedEvent) { - redactedEvent.markLocallyRedacted(event); - this.emit(RoomEvent.Redaction, event, this); - } - } - } else { - for (const timelineSet of this.timelineSets) { - if (timelineSet.getFilter()) { - if (timelineSet.getFilter()!.filterRoomTimeline([event]).length) { - timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), { - toStartOfTimeline: false, - }); - } - } else { - timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), { - toStartOfTimeline: false, - }); - } - } - } - - this.emit(RoomEvent.LocalEchoUpdated, event, this); - } - - /** - * Persists all pending events to local storage - * - * If the current room is encrypted only encrypted events will be persisted - * all messages that are not yet encrypted will be discarded - * - * This is because the flow of EVENT_STATUS transition is - * `queued => sending => encrypting => sending => sent` - * - * Steps 3 and 4 are skipped for unencrypted room. - * It is better to discard an unencrypted message rather than persisting - * it locally for everyone to read - */ - private savePendingEvents(): void { - if (this.pendingEventList) { - const pendingEvents = this.pendingEventList - .map((event) => { - return { - ...event.event, - txn_id: event.getTxnId(), - }; - }) - .filter((event) => { - // Filter out the unencrypted messages if the room is encrypted - const isEventEncrypted = event.type === EventType.RoomMessageEncrypted; - const isRoomEncrypted = this.client.isRoomEncrypted(this.roomId); - return isEventEncrypted || !isRoomEncrypted; - }); - - this.client.store.setPendingEvents(this.roomId, pendingEvents); - } - } - - /** - * Used to aggregate the local echo for a relation, and also - * for re-applying a relation after it's redaction has been cancelled, - * as the local echo for the redaction of the relation would have - * un-aggregated the relation. Note that this is different from regular messages, - * which are just kept detached for their local echo. - * - * Also note that live events are aggregated in the live EventTimelineSet. - * @param event - the relation event that needs to be aggregated. - */ - private aggregateNonLiveRelation(event: MatrixEvent): void { - this.relations.aggregateChildEvent(event); - } - - public getEventForTxnId(txnId: string): MatrixEvent | undefined { - return this.txnToEvent.get(txnId); - } - - /** - * Deal with the echo of a message we sent. - * - * <p>We move the event to the live timeline if it isn't there already, and - * update it. - * - * @param remoteEvent - The event received from - * /sync - * @param localEvent - The local echo, which - * should be either in the pendingEventList or the timeline. - * - * @internal - * - * @remarks - * Fires {@link RoomEvent.LocalEchoUpdated} - */ - public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void { - const oldEventId = localEvent.getId()!; - const newEventId = remoteEvent.getId()!; - const oldStatus = localEvent.status; - - logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`); - - // no longer pending - this.txnToEvent.delete(remoteEvent.getUnsigned().transaction_id!); - - // if it's in the pending list, remove it - if (this.pendingEventList) { - this.removePendingEvent(oldEventId); - } - - // replace the event source (this will preserve the plaintext payload if - // any, which is good, because we don't want to try decoding it again). - localEvent.handleRemoteEcho(remoteEvent.event); - - const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent); - const thread = threadId ? this.getThread(threadId) : null; - thread?.setEventMetadata(localEvent); - thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); - - if (shouldLiveInRoom) { - for (const timelineSet of this.timelineSets) { - // if it's already in the timeline, update the timeline map. If it's not, add it. - timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); - } - } - - this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus); - } - - /** - * Update the status / event id on a pending event, to reflect its transmission - * progress. - * - * <p>This is an internal method. - * - * @param event - local echo event - * @param newStatus - status to assign - * @param newEventId - new event id to assign. Ignored unless newStatus == EventStatus.SENT. - * - * @remarks - * Fires {@link RoomEvent.LocalEchoUpdated} - */ - public updatePendingEvent(event: MatrixEvent, newStatus: EventStatus, newEventId?: string): void { - logger.log( - `setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` + - `event ID ${event.getId()} -> ${newEventId}`, - ); - - // if the message was sent, we expect an event id - if (newStatus == EventStatus.SENT && !newEventId) { - throw new Error("updatePendingEvent called with status=SENT, but no new event id"); - } - - // SENT races against /sync, so we have to special-case it. - if (newStatus == EventStatus.SENT) { - const timeline = this.getTimelineForEvent(newEventId!); - if (timeline) { - // we've already received the event via the event stream. - // nothing more to do here, assuming the transaction ID was correctly matched. - // Let's check that. - const remoteEvent = this.findEventById(newEventId!); - const remoteTxnId = remoteEvent?.getUnsigned().transaction_id; - if (!remoteTxnId && remoteEvent) { - // This code path is mostly relevant for the Sliding Sync proxy. - // The remote event did not contain a transaction ID, so we did not handle - // the remote echo yet. Handle it now. - const unsigned = remoteEvent.getUnsigned(); - unsigned.transaction_id = event.getTxnId(); - remoteEvent.setUnsigned(unsigned); - // the remote event is _already_ in the timeline, so we need to remove it so - // we can convert the local event into the final event. - this.removeEvent(remoteEvent.getId()!); - this.handleRemoteEcho(remoteEvent, event); - } - return; - } - } - - const oldStatus = event.status; - const oldEventId = event.getId()!; - - if (!oldStatus) { - throw new Error("updatePendingEventStatus called on an event which is not a local echo."); - } - - const allowed = ALLOWED_TRANSITIONS[oldStatus]; - if (!allowed?.includes(newStatus)) { - throw new Error(`Invalid EventStatus transition ${oldStatus}->${newStatus}`); - } - - event.setStatus(newStatus); - - if (newStatus == EventStatus.SENT) { - // update the event id - event.replaceLocalEventId(newEventId!); - - const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event); - const thread = threadId ? this.getThread(threadId) : undefined; - thread?.setEventMetadata(event); - thread?.timelineSet.replaceEventId(oldEventId, newEventId!); - - if (shouldLiveInRoom) { - // if the event was already in the timeline (which will be the case if - // opts.pendingEventOrdering==chronological), we need to update the - // timeline map. - for (const timelineSet of this.timelineSets) { - timelineSet.replaceEventId(oldEventId, newEventId!); - } - } - } else if (newStatus == EventStatus.CANCELLED) { - // remove it from the pending event list, or the timeline. - if (this.pendingEventList) { - const removedEvent = this.getPendingEvent(oldEventId); - this.removePendingEvent(oldEventId); - if (removedEvent?.isRedaction()) { - this.revertRedactionLocalEcho(removedEvent); - } - } - this.removeEvent(oldEventId); - } - this.savePendingEvents(); - - this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus); - } - - private revertRedactionLocalEcho(redactionEvent: MatrixEvent): void { - const redactId = redactionEvent.event.redacts; - if (!redactId) { - return; - } - const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); - if (redactedEvent) { - redactedEvent.unmarkLocallyRedacted(); - // re-render after undoing redaction - this.emit(RoomEvent.RedactionCancelled, redactionEvent, this); - // reapply relation now redaction failed - if (redactedEvent.isRelation()) { - this.aggregateNonLiveRelation(redactedEvent); - } - } - } - - /** - * Add some events to this room. This can include state events, message - * events and typing notifications. These events are treated as "live" so - * they will go to the end of the timeline. - * - * @param events - A list of events to add. - * @param addLiveEventOptions - addLiveEvent options - * @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'. - */ - public addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): void; - /** - * @deprecated In favor of the overload with `IAddLiveEventOptions` - */ - public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache?: boolean): void; - public addLiveEvents( - events: MatrixEvent[], - duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions, - fromCache = false, - ): void { - let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy; - let timelineWasEmpty: boolean | undefined = false; - if (typeof duplicateStrategyOrOpts === "object") { - ({ - duplicateStrategy, - fromCache = false, - /* roomState, (not used here) */ - timelineWasEmpty, - } = duplicateStrategyOrOpts); - } else if (duplicateStrategyOrOpts !== undefined) { - // Deprecation warning - // FIXME: Remove after 2023-06-01 (technical debt) - logger.warn( - "Overload deprecated: " + - "`Room.addLiveEvents(events, duplicateStrategy?, fromCache?)` " + - "is deprecated in favor of the overload with `Room.addLiveEvents(events, IAddLiveEventOptions)`", - ); - } - - if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) { - throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'"); - } - - // sanity check that the live timeline is still live - for (let i = 0; i < this.timelineSets.length; i++) { - const liveTimeline = this.timelineSets[i].getLiveTimeline(); - if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) { - throw new Error( - "live timeline " + - i + - " is no longer live - it has a pagination token " + - "(" + - liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + - ")", - ); - } - if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) { - throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`); - } - } - - const threadRoots = this.findThreadRoots(events); - const eventsByThread: { [threadId: string]: MatrixEvent[] } = {}; - - const options: IAddLiveEventOptions = { - duplicateStrategy, - fromCache, - timelineWasEmpty, - }; - - for (const event of events) { - // TODO: We should have a filter to say "only add state event types X Y Z to the timeline". - this.processLiveEvent(event); - - if (event.getUnsigned().transaction_id) { - const existingEvent = this.txnToEvent.get(event.getUnsigned().transaction_id!); - if (existingEvent) { - // remote echo of an event we sent earlier - this.handleRemoteEcho(event, existingEvent); - continue; // we can skip adding the event to the timeline sets, it is already there - } - } - - const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn( - event, - events, - threadRoots, - ); - - if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) { - eventsByThread[threadId ?? ""] = []; - } - eventsByThread[threadId ?? ""]?.push(event); - - if (shouldLiveInRoom) { - this.addLiveEvent(event, options); - } - } - - Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => { - this.addThreadedEvents(threadId, threadEvents, false); - }); - } - - public partitionThreadedEvents( - events: MatrixEvent[], - ): [timelineEvents: MatrixEvent[], threadedEvents: MatrixEvent[]] { - // Indices to the events array, for readability - const ROOM = 0; - const THREAD = 1; - if (this.client.supportsThreads()) { - const threadRoots = this.findThreadRoots(events); - return events.reduce( - (memo, event: MatrixEvent) => { - const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn( - event, - events, - threadRoots, - ); - - if (shouldLiveInRoom) { - memo[ROOM].push(event); - } - - if (shouldLiveInThread) { - event.setThreadId(threadId ?? ""); - memo[THREAD].push(event); - } - - return memo; - }, - [[] as MatrixEvent[], [] as MatrixEvent[]], - ); - } else { - // When `experimentalThreadSupport` is disabled treat all events as timelineEvents - return [events as MatrixEvent[], [] as MatrixEvent[]]; - } - } - - /** - * Given some events, find the IDs of all the thread roots that are referred to by them. - */ - private findThreadRoots(events: MatrixEvent[]): Set<string> { - const threadRoots = new Set<string>(); - for (const event of events) { - if (event.isRelation(THREAD_RELATION_TYPE.name)) { - threadRoots.add(event.relationEventId ?? ""); - } - } - return threadRoots; - } - - /** - * Add a receipt event to the room. - * @param event - The m.receipt event. - * @param synthetic - True if this event is implicit. - */ - public addReceipt(event: MatrixEvent, synthetic = false): void { - const content = event.getContent<ReceiptContent>(); - Object.keys(content).forEach((eventId: string) => { - Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => { - Object.keys(content[eventId][receiptType]).forEach((userId: string) => { - const receipt = content[eventId][receiptType][userId] as Receipt; - const receiptForMainTimeline = !receipt.thread_id || receipt.thread_id === MAIN_ROOM_TIMELINE; - const receiptDestination: Thread | this | undefined = receiptForMainTimeline - ? this - : this.threads.get(receipt.thread_id ?? ""); - - if (receiptDestination) { - receiptDestination.addReceiptToStructure( - eventId, - receiptType as ReceiptType, - userId, - receipt, - synthetic, - ); - - // If the read receipt sent for the logged in user matches - // the last event of the live timeline, then we know for a fact - // that the user has read that message. - // We can mark the room as read and not wait for the local echo - // from synapse - // This needs to be done after the initial sync as we do not want this - // logic to run whilst the room is being initialised - if (this.client.isInitialSyncComplete() && userId === this.client.getUserId()) { - const lastEvent = receiptDestination.timeline[receiptDestination.timeline.length - 1]; - if (lastEvent && eventId === lastEvent.getId() && userId === lastEvent.getSender()) { - receiptDestination.setUnread(NotificationCountType.Total, 0); - receiptDestination.setUnread(NotificationCountType.Highlight, 0); - } - } - } else { - // The thread does not exist locally, keep the read receipt - // in a cache locally, and re-apply the `addReceipt` logic - // when the thread is created - this.cachedThreadReadReceipts.set(receipt.thread_id!, [ - ...(this.cachedThreadReadReceipts.get(receipt.thread_id!) ?? []), - { eventId, receiptType, userId, receipt, synthetic }, - ]); - } - - const me = this.client.getUserId(); - // Track the time of the current user's oldest threaded receipt in the room. - if (userId === me && !receiptForMainTimeline && receipt.ts < this.oldestThreadedReceiptTs) { - this.oldestThreadedReceiptTs = receipt.ts; - } - - // Track each user's unthreaded read receipt. - if (!receipt.thread_id && receipt.ts > (this.unthreadedReceipts.get(userId)?.ts ?? 0)) { - this.unthreadedReceipts.set(userId, receipt); - } - }); - }); - }); - - // send events after we've regenerated the structure & cache, otherwise things that - // listened for the event would read stale data. - this.emit(RoomEvent.Receipt, event, this); - } - - /** - * Adds/handles ephemeral events such as typing notifications and read receipts. - * @param events - A list of events to process - */ - public addEphemeralEvents(events: MatrixEvent[]): void { - for (const event of events) { - if (event.getType() === EventType.Typing) { - this.currentState.setTypingEvent(event); - } else if (event.getType() === EventType.Receipt) { - this.addReceipt(event); - } // else ignore - life is too short for us to care about these events - } - } - - /** - * Removes events from this room. - * @param eventIds - A list of eventIds to remove. - */ - public removeEvents(eventIds: string[]): void { - for (const eventId of eventIds) { - this.removeEvent(eventId); - } - } - - /** - * Removes a single event from this room. - * - * @param eventId - The id of the event to remove - * - * @returns true if the event was removed from any of the room's timeline sets - */ - public removeEvent(eventId: string): boolean { - let removedAny = false; - for (const timelineSet of this.timelineSets) { - const removed = timelineSet.removeEvent(eventId); - if (removed) { - if (removed.isRedaction()) { - this.revertRedactionLocalEcho(removed); - } - removedAny = true; - } - } - return removedAny; - } - - /** - * Recalculate various aspects of the room, including the room name and - * room summary. Call this any time the room's current state is modified. - * May fire "Room.name" if the room name is updated. - * - * @remarks - * Fires {@link RoomEvent.Name} - */ - public recalculate(): void { - // set fake stripped state events if this is an invite room so logic remains - // consistent elsewhere. - const membershipEvent = this.currentState.getStateEvents(EventType.RoomMember, this.myUserId); - if (membershipEvent) { - const membership = membershipEvent.getContent().membership; - this.updateMyMembership(membership!); - - if (membership === "invite") { - const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || []; - strippedStateEvents.forEach((strippedEvent) => { - const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key); - if (!existingEvent) { - // set the fake stripped event instead - this.currentState.setStateEvents([ - new MatrixEvent({ - type: strippedEvent.type, - state_key: strippedEvent.state_key, - content: strippedEvent.content, - event_id: "$fake" + Date.now(), - room_id: this.roomId, - user_id: this.myUserId, // technically a lie - }), - ]); - } - }); - } - } - - const oldName = this.name; - this.name = this.calculateRoomName(this.myUserId); - this.normalizedName = normalize(this.name); - this.summary = new RoomSummary(this.roomId, { - title: this.name, - }); - - if (oldName !== this.name) { - this.emit(RoomEvent.Name, this); - } - } - - /** - * Update the room-tag event for the room. The previous one is overwritten. - * @param event - the m.tag event - */ - public addTags(event: MatrixEvent): void { - // event content looks like: - // content: { - // tags: { - // $tagName: { $metadata: $value }, - // $tagName: { $metadata: $value }, - // } - // } - - // XXX: do we need to deep copy here? - this.tags = event.getContent().tags || {}; - - // XXX: we could do a deep-comparison to see if the tags have really - // changed - but do we want to bother? - this.emit(RoomEvent.Tags, event, this); - } - - /** - * Update the account_data events for this room, overwriting events of the same type. - * @param events - an array of account_data events to add - */ - public addAccountData(events: MatrixEvent[]): void { - for (const event of events) { - if (event.getType() === "m.tag") { - this.addTags(event); - } - const eventType = event.getType(); - const lastEvent = this.accountData.get(eventType); - this.accountData.set(eventType, event); - this.emit(RoomEvent.AccountData, event, this, lastEvent); - } - } - - /** - * Access account_data event of given event type for this room - * @param type - the type of account_data event to be accessed - * @returns the account_data event in question - */ - public getAccountData(type: EventType | string): MatrixEvent | undefined { - return this.accountData.get(type); - } - - /** - * Returns whether the syncing user has permission to send a message in the room - * @returns true if the user should be permitted to send - * message events into the room. - */ - public maySendMessage(): boolean { - return ( - this.getMyMembership() === "join" && - (this.client.isRoomEncrypted(this.roomId) - ? this.currentState.maySendEvent(EventType.RoomMessageEncrypted, this.myUserId) - : this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId)) - ); - } - - /** - * Returns whether the given user has permissions to issue an invite for this room. - * @param userId - the ID of the Matrix user to check permissions for - * @returns true if the user should be permitted to issue invites for this room. - */ - public canInvite(userId: string): boolean { - let canInvite = this.getMyMembership() === "join"; - const powerLevelsEvent = this.currentState.getStateEvents(EventType.RoomPowerLevels, ""); - const powerLevels = powerLevelsEvent && powerLevelsEvent.getContent(); - const me = this.getMember(userId); - if (powerLevels && me && powerLevels.invite > me.powerLevel) { - canInvite = false; - } - return canInvite; - } - - /** - * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`. - * @returns the join_rule applied to this room - */ - public getJoinRule(): JoinRule { - return this.currentState.getJoinRule(); - } - - /** - * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns the history_visibility applied to this room - */ - public getHistoryVisibility(): HistoryVisibility { - return this.currentState.getHistoryVisibility(); - } - - /** - * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns the history_visibility applied to this room - */ - public getGuestAccess(): GuestAccess { - return this.currentState.getGuestAccess(); - } - - /** - * Returns the type of the room from the `m.room.create` event content or undefined if none is set - * @returns the type of the room. - */ - public getType(): RoomType | string | undefined { - const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); - if (!createEvent) { - if (!this.getTypeWarning) { - logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event"); - this.getTypeWarning = true; - } - return undefined; - } - return createEvent.getContent()[RoomCreateTypeField]; - } - - /** - * Returns whether the room is a space-room as defined by MSC1772. - * @returns true if the room's type is RoomType.Space - */ - public isSpaceRoom(): boolean { - return this.getType() === RoomType.Space; - } - - /** - * Returns whether the room is a call-room as defined by MSC3417. - * @returns true if the room's type is RoomType.UnstableCall - */ - public isCallRoom(): boolean { - return this.getType() === RoomType.UnstableCall; - } - - /** - * Returns whether the room is a video room. - * @returns true if the room's type is RoomType.ElementVideo - */ - public isElementVideoRoom(): boolean { - return this.getType() === RoomType.ElementVideo; - } - - /** - * Find the predecessor of this room. - * - * @param msc3946ProcessDynamicPredecessor - if true, look for an - * m.room.predecessor state event and use it if found (MSC3946). - * @returns null if this room has no predecessor. Otherwise, returns - * the roomId, last eventId and viaServers of the predecessor room. - * - * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events - * as well as m.room.create events to find predecessors. - * - * Note: if an m.predecessor event is used, eventId may be undefined - * since last_known_event_id is optional. - * - * Note: viaServers may be undefined, and will definitely be undefined if - * this predecessor comes from a RoomCreate event (rather than a - * RoomPredecessor, which has the optional via_servers property). - */ - public findPredecessor( - msc3946ProcessDynamicPredecessor = false, - ): { roomId: string; eventId?: string; viaServers?: string[] } | null { - const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); - if (!currentState) { - return null; - } - return currentState.findPredecessor(msc3946ProcessDynamicPredecessor); - } - - private roomNameGenerator(state: RoomNameState): string { - if (this.client.roomNameGenerator) { - const name = this.client.roomNameGenerator(this.roomId, state); - if (name !== null) { - return name; - } - } - - switch (state.type) { - case RoomNameType.Actual: - return state.name; - case RoomNameType.Generated: - switch (state.subtype) { - case "Inviting": - return `Inviting ${memberNamesToRoomName(state.names, state.count)}`; - default: - return memberNamesToRoomName(state.names, state.count); - } - case RoomNameType.EmptyRoom: - if (state.oldName) { - return `Empty room (was ${state.oldName})`; - } else { - return "Empty room"; - } - } - } - - /** - * This is an internal method. Calculates the name of the room from the current - * room state. - * @param userId - The client's user ID. Used to filter room members - * correctly. - * @param ignoreRoomNameEvent - Return the implicit room name that we'd see if there - * was no m.room.name event. - * @returns The calculated room name. - */ - private calculateRoomName(userId: string, ignoreRoomNameEvent = false): string { - if (!ignoreRoomNameEvent) { - // check for an alias, if any. for now, assume first alias is the - // official one. - const mRoomName = this.currentState.getStateEvents(EventType.RoomName, ""); - if (mRoomName?.getContent().name) { - return this.roomNameGenerator({ - type: RoomNameType.Actual, - name: mRoomName.getContent().name, - }); - } - } - - const alias = this.getCanonicalAlias(); - if (alias) { - return this.roomNameGenerator({ - type: RoomNameType.Actual, - name: alias, - }); - } - - const joinedMemberCount = this.currentState.getJoinedMemberCount(); - const invitedMemberCount = this.currentState.getInvitedMemberCount(); - // -1 because these numbers include the syncing user - let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1; - - // get service members (e.g. helper bots) for exclusion - let excludedUserIds: string[] = []; - const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, ""); - if (Array.isArray(mFunctionalMembers?.getContent().service_members)) { - excludedUserIds = mFunctionalMembers!.getContent().service_members; - } - - // get members that are NOT ourselves and are actually in the room. - let otherNames: string[] = []; - if (this.summaryHeroes) { - // if we have a summary, the member state events should be in the room state - this.summaryHeroes.forEach((userId) => { - // filter service members - if (excludedUserIds.includes(userId)) { - inviteJoinCount--; - return; - } - const member = this.getMember(userId); - otherNames.push(member ? member.name : userId); - }); - } else { - let otherMembers = this.currentState.getMembers().filter((m) => { - return m.userId !== userId && (m.membership === "invite" || m.membership === "join"); - }); - otherMembers = otherMembers.filter(({ userId }) => { - // filter service members - if (excludedUserIds.includes(userId)) { - inviteJoinCount--; - return false; - } - return true; - }); - // make sure members have stable order - otherMembers.sort((a, b) => utils.compare(a.userId, b.userId)); - // only 5 first members, immitate summaryHeroes - otherMembers = otherMembers.slice(0, 5); - otherNames = otherMembers.map((m) => m.name); - } - - if (inviteJoinCount) { - return this.roomNameGenerator({ - type: RoomNameType.Generated, - names: otherNames, - count: inviteJoinCount, - }); - } - - const myMembership = this.getMyMembership(); - // if I have created a room and invited people through - // 3rd party invites - if (myMembership == "join") { - const thirdPartyInvites = this.currentState.getStateEvents(EventType.RoomThirdPartyInvite); - - if (thirdPartyInvites?.length) { - const thirdPartyNames = thirdPartyInvites.map((i) => { - return i.getContent().display_name; - }); - - return this.roomNameGenerator({ - type: RoomNameType.Generated, - subtype: "Inviting", - names: thirdPartyNames, - count: thirdPartyNames.length + 1, - }); - } - } - - // let's try to figure out who was here before - let leftNames = otherNames; - // if we didn't have heroes, try finding them in the room state - if (!leftNames.length) { - leftNames = this.currentState - .getMembers() - .filter((m) => { - return m.userId !== userId && m.membership !== "invite" && m.membership !== "join"; - }) - .map((m) => m.name); - } - - let oldName: string | undefined; - if (leftNames.length) { - oldName = this.roomNameGenerator({ - type: RoomNameType.Generated, - names: leftNames, - count: leftNames.length + 1, - }); - } - - return this.roomNameGenerator({ - type: RoomNameType.EmptyRoom, - oldName, - }); - } - - /** - * When we receive a new visibility change event: - * - * - store this visibility change alongside the timeline, in case we - * later need to apply it to an event that we haven't received yet; - * - if we have already received the event whose visibility has changed, - * patch it to reflect the visibility change and inform listeners. - */ - private applyNewVisibilityEvent(event: MatrixEvent): void { - const visibilityChange = event.asVisibilityChange(); - if (!visibilityChange) { - // The event is ill-formed. - return; - } - - // Ignore visibility change events that are not emitted by moderators. - const userId = event.getSender(); - if (!userId) { - return; - } - const isPowerSufficient = - (EVENT_VISIBILITY_CHANGE_TYPE.name && - this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, userId)) || - (EVENT_VISIBILITY_CHANGE_TYPE.altName && - this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, userId)); - if (!isPowerSufficient) { - // Powerlevel is insufficient. - return; - } - - // Record this change in visibility. - // If the event is not in our timeline and we only receive it later, - // we may need to apply the visibility change at a later date. - - const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId); - if (visibilityEventsOnOriginalEvent) { - // It would be tempting to simply erase the latest visibility change - // but we need to record all of the changes in case the latest change - // is ever redacted. - // - // In practice, linear scans through `visibilityEvents` should be fast. - // However, to protect against a potential DoS attack, we limit the - // number of iterations in this loop. - let index = visibilityEventsOnOriginalEvent.length - 1; - const min = Math.max( - 0, - visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH, - ); - for (; index >= min; --index) { - const target = visibilityEventsOnOriginalEvent[index]; - if (target.getTs() < event.getTs()) { - break; - } - } - if (index === -1) { - visibilityEventsOnOriginalEvent.unshift(event); - } else { - visibilityEventsOnOriginalEvent.splice(index + 1, 0, event); - } - } else { - this.visibilityEvents.set(visibilityChange.eventId, [event]); - } - - // Finally, let's check if the event is already in our timeline. - // If so, we need to patch it and inform listeners. - - const originalEvent = this.findEventById(visibilityChange.eventId); - if (!originalEvent) { - return; - } - originalEvent.applyVisibilityEvent(visibilityChange); - } - - private redactVisibilityChangeEvent(event: MatrixEvent): void { - // Sanity checks. - if (!event.isVisibilityEvent) { - throw new Error("expected a visibility change event"); - } - const relation = event.getRelation(); - const originalEventId = relation?.event_id; - const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId!); - if (!visibilityEventsOnOriginalEvent) { - // No visibility changes on the original event. - // In particular, this change event was not recorded, - // most likely because it was ill-formed. - return; - } - const index = visibilityEventsOnOriginalEvent.findIndex((change) => change.getId() === event.getId()); - if (index === -1) { - // This change event was not recorded, most likely because - // it was ill-formed. - return; - } - // Remove visibility change. - visibilityEventsOnOriginalEvent.splice(index, 1); - - // If we removed the latest visibility change event, propagate changes. - if (index === visibilityEventsOnOriginalEvent.length) { - const originalEvent = this.findEventById(originalEventId!); - if (!originalEvent) { - return; - } - if (index === 0) { - // We have just removed the only visibility change event. - this.visibilityEvents.delete(originalEventId!); - originalEvent.applyVisibilityEvent(); - } else { - const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1]; - const newVisibility = newEvent.asVisibilityChange(); - if (!newVisibility) { - // Event is ill-formed. - // This breaks our invariant. - throw new Error("at this stage, visibility changes should be well-formed"); - } - originalEvent.applyVisibilityEvent(newVisibility); - } - } - } - - /** - * When we receive an event whose visibility has been altered by - * a (more recent) visibility change event, patch the event in - * place so that clients now not to display it. - * - * @param event - Any matrix event. If this event has at least one a - * pending visibility change event, apply the latest visibility - * change event. - */ - private applyPendingVisibilityEvents(event: MatrixEvent): void { - const visibilityEvents = this.visibilityEvents.get(event.getId()!); - if (!visibilityEvents || visibilityEvents.length == 0) { - // No pending visibility change in store. - return; - } - const visibilityEvent = visibilityEvents[visibilityEvents.length - 1]; - const visibilityChange = visibilityEvent.asVisibilityChange(); - if (!visibilityChange) { - return; - } - if (visibilityChange.visible) { - // Events are visible by default, no need to apply a visibility change. - // Note that we need to keep the visibility changes in `visibilityEvents`, - // in case we later fetch an older visibility change event that is superseded - // by `visibilityChange`. - } - if (visibilityEvent.getTs() < event.getTs()) { - // Something is wrong, the visibility change cannot happen before the - // event. Presumably an ill-formed event. - return; - } - event.applyVisibilityEvent(visibilityChange); - } - - /** - * Find when a client has gained thread capabilities by inspecting the oldest - * threaded receipt - * @returns the timestamp of the oldest threaded receipt - */ - public getOldestThreadedReceiptTs(): number { - return this.oldestThreadedReceiptTs; - } - - /** - * Returns the most recent unthreaded receipt for a given user - * @param userId - the MxID of the User - * @returns an unthreaded Receipt. Can be undefined if receipts have been disabled - * or a user chooses to use private read receipts (or we have simply not received - * a receipt from this user yet). - */ - public getLastUnthreadedReceiptFor(userId: string): Receipt | undefined { - return this.unthreadedReceipts.get(userId); - } - - /** - * This issue should also be addressed on synapse's side and is tracked as part - * of https://github.com/matrix-org/synapse/issues/14837 - * - * - * We consider a room fully read if the current user has sent - * the last event in the live timeline of that context and if the read receipt - * we have on record matches. - * This also detects all unread threads and applies the same logic to those - * contexts - */ - public fixupNotifications(userId: string): void { - super.fixupNotifications(userId); - - const unreadThreads = this.getThreads().filter( - (thread) => this.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total) > 0, - ); - - for (const thread of unreadThreads) { - thread.fixupNotifications(userId); - } - } -} - -// a map from current event status to a list of allowed next statuses -const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = { - [EventStatus.ENCRYPTING]: [EventStatus.SENDING, EventStatus.NOT_SENT, EventStatus.CANCELLED], - [EventStatus.SENDING]: [EventStatus.ENCRYPTING, EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT], - [EventStatus.QUEUED]: [EventStatus.SENDING, EventStatus.NOT_SENT, EventStatus.CANCELLED], - [EventStatus.SENT]: [], - [EventStatus.NOT_SENT]: [EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED], - [EventStatus.CANCELLED]: [], -}; - -export enum RoomNameType { - EmptyRoom, - Generated, - Actual, -} - -export interface EmptyRoomNameState { - type: RoomNameType.EmptyRoom; - oldName?: string; -} - -export interface GeneratedRoomNameState { - type: RoomNameType.Generated; - subtype?: "Inviting"; - names: string[]; - count: number; -} - -export interface ActualRoomNameState { - type: RoomNameType.Actual; - name: string; -} - -export type RoomNameState = EmptyRoomNameState | GeneratedRoomNameState | ActualRoomNameState; - -// Can be overriden by IMatrixClientCreateOpts::memberNamesToRoomNameFn -function memberNamesToRoomName(names: string[], count: number): string { - const countWithoutMe = count - 1; - if (!names.length) { - return "Empty room"; - } else if (names.length === 1 && countWithoutMe <= 1) { - return names[0]; - } else if (names.length === 2 && countWithoutMe <= 2) { - return `${names[0]} and ${names[1]}`; - } else { - const plural = countWithoutMe > 1; - if (plural) { - return `${names[0]} and ${countWithoutMe} others`; - } else { - return `${names[0]} and 1 other`; - } - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts deleted file mode 100644 index 21192a6..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2015 - 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 { EventContext } from "./event-context"; -import { EventMapper } from "../event-mapper"; -import { IResultContext, ISearchResult } from "../@types/search"; - -export class SearchResult { - /** - * Create a SearchResponse from the response to /search - */ - - public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult { - const jsonContext = jsonObj.context || ({} as IResultContext); - let eventsBefore = (jsonContext.events_before || []).map(eventMapper); - let eventsAfter = (jsonContext.events_after || []).map(eventMapper); - - const context = new EventContext(eventMapper(jsonObj.result)); - - // Filter out any contextual events which do not correspond to the same timeline (thread or room) - const threadRootId = context.ourEvent.threadRootId; - eventsBefore = eventsBefore.filter((e) => e.threadRootId === threadRootId); - eventsAfter = eventsAfter.filter((e) => e.threadRootId === threadRootId); - - context.setPaginateToken(jsonContext.start, true); - context.addEvents(eventsBefore, true); - context.addEvents(eventsAfter, false); - context.setPaginateToken(jsonContext.end, false); - - return new SearchResult(jsonObj.rank, context); - } - - /** - * Construct a new SearchResult - * - * @param rank - where this SearchResult ranks in the results - * @param context - the matching event and its - * context - */ - public constructor(public readonly rank: number, public readonly context: EventContext) {} -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts deleted file mode 100644 index 9a4ead3..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts +++ /dev/null @@ -1,669 +0,0 @@ -/* -Copyright 2021 - 2023 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 { Optional } from "matrix-events-sdk"; - -import { MatrixClient, PendingEventOrdering } from "../client"; -import { TypedReEmitter } from "../ReEmitter"; -import { RelationType } from "../@types/event"; -import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event"; -import { Direction, EventTimeline } from "./event-timeline"; -import { EventTimelineSet, EventTimelineSetHandlerMap } from "./event-timeline-set"; -import { NotificationCountType, Room, RoomEvent } from "./room"; -import { RoomState } from "./room-state"; -import { ServerControlledNamespacedValue } from "../NamespacedValue"; -import { logger } from "../logger"; -import { ReadReceipt } from "./read-receipt"; -import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts"; - -export enum ThreadEvent { - New = "Thread.new", - Update = "Thread.update", - NewReply = "Thread.newReply", - ViewThread = "Thread.viewThread", - Delete = "Thread.delete", -} - -type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New> | RoomEvent.Timeline | RoomEvent.TimelineReset; - -export type EventHandlerMap = { - [ThreadEvent.Update]: (thread: Thread) => void; - [ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void; - [ThreadEvent.ViewThread]: () => void; - [ThreadEvent.Delete]: (thread: Thread) => void; -} & EventTimelineSetHandlerMap; - -interface IThreadOpts { - room: Room; - client: MatrixClient; - pendingEventOrdering?: PendingEventOrdering; - receipts?: CachedReceiptStructure[]; -} - -export enum FeatureSupport { - None = 0, - Experimental = 1, - Stable = 2, -} - -export function determineFeatureSupport(stable: boolean, unstable: boolean): FeatureSupport { - if (stable) { - return FeatureSupport.Stable; - } else if (unstable) { - return FeatureSupport.Experimental; - } else { - return FeatureSupport.None; - } -} - -export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> { - public static hasServerSideSupport = FeatureSupport.None; - public static hasServerSideListSupport = FeatureSupport.None; - public static hasServerSideFwdPaginationSupport = FeatureSupport.None; - - /** - * A reference to all the events ID at the bottom of the threads - */ - public readonly timelineSet: EventTimelineSet; - public timeline: MatrixEvent[] = []; - - private _currentUserParticipated = false; - - private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>; - - private lastEvent: MatrixEvent | undefined; - private replyCount = 0; - private lastPendingEvent: MatrixEvent | undefined; - private pendingReplyCount = 0; - - public readonly room: Room; - public readonly client: MatrixClient; - private readonly pendingEventOrdering: PendingEventOrdering; - - public initialEventsFetched = !Thread.hasServerSideSupport; - /** - * An array of events to add to the timeline once the thread has been initialised - * with server suppport. - */ - public replayEvents: MatrixEvent[] | null = []; - - public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) { - super(); - - if (!opts?.room) { - // Logging/debugging for https://github.com/vector-im/element-web/issues/22141 - // Hope is that we end up with a more obvious stack trace. - throw new Error("element-web#22141: A thread requires a room in order to function"); - } - - this.room = opts.room; - this.client = opts.client; - this.pendingEventOrdering = opts.pendingEventOrdering ?? PendingEventOrdering.Chronological; - this.timelineSet = new EventTimelineSet( - this.room, - { - timelineSupport: true, - pendingEvents: true, - }, - this.client, - this, - ); - this.reEmitter = new TypedReEmitter(this); - - this.reEmitter.reEmit(this.timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]); - - this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.room.on(RoomEvent.Redaction, this.onRedaction); - this.room.on(RoomEvent.LocalEchoUpdated, this.onLocalEcho); - this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent); - - this.processReceipts(opts.receipts); - - // even if this thread is thought to be originating from this client, we initialise it as we may be in a - // gappy sync and a thread around this event may already exist. - this.updateThreadMetadata(); - this.setEventMetadata(this.rootEvent); - } - - private async fetchRootEvent(): Promise<void> { - this.rootEvent = this.room.findEventById(this.id); - // If the rootEvent does not exist in the local stores, then fetch it from the server. - try { - const eventData = await this.client.fetchRoomEvent(this.roomId, this.id); - const mapper = this.client.getEventMapper(); - this.rootEvent = mapper(eventData); // will merge with existing event object if such is known - } catch (e) { - logger.error("Failed to fetch thread root to construct thread with", e); - } - await this.processEvent(this.rootEvent); - } - - public static setServerSideSupport(status: FeatureSupport): void { - Thread.hasServerSideSupport = status; - if (status !== FeatureSupport.Stable) { - FILTER_RELATED_BY_SENDERS.setPreferUnstable(true); - FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true); - THREAD_RELATION_TYPE.setPreferUnstable(true); - } - } - - public static setServerSideListSupport(status: FeatureSupport): void { - Thread.hasServerSideListSupport = status; - } - - public static setServerSideFwdPaginationSupport(status: FeatureSupport): void { - Thread.hasServerSideFwdPaginationSupport = status; - } - - private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent): void => { - if ( - event?.isRelation(THREAD_RELATION_TYPE.name) && - this.room.eventShouldLiveIn(event).threadId === this.id && - event.getId() !== this.id && // the root event isn't counted in the length so ignore this redaction - !redaction.status // only respect it when it succeeds - ) { - this.replyCount--; - this.updatePendingReplyCount(); - this.emit(ThreadEvent.Update, this); - } - }; - - private onRedaction = async (event: MatrixEvent): Promise<void> => { - if (event.threadRootId !== this.id) return; // ignore redactions for other timelines - if (this.replyCount <= 0) { - for (const threadEvent of this.timeline) { - this.clearEventMetadata(threadEvent); - } - this.lastEvent = this.rootEvent; - this._currentUserParticipated = false; - this.emit(ThreadEvent.Delete, this); - } else { - await this.updateThreadMetadata(); - } - }; - - private onTimelineEvent = ( - event: MatrixEvent, - room: Room | undefined, - toStartOfTimeline: boolean | undefined, - ): void => { - // Add a synthesized receipt when paginating forward in the timeline - if (!toStartOfTimeline) { - room!.addLocalEchoReceipt(event.getSender()!, event, ReceiptType.Read); - } - this.onEcho(event, toStartOfTimeline ?? false); - }; - - private onLocalEcho = (event: MatrixEvent): void => { - this.onEcho(event, false); - }; - - private onEcho = async (event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> => { - if (event.threadRootId !== this.id) return; // ignore echoes for other timelines - if (this.lastEvent === event) return; // ignore duplicate events - await this.updateThreadMetadata(); - if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; // don't send a new reply event for reactions or edits - if (toStartOfTimeline) return; // ignore messages added to the start of the timeline - this.emit(ThreadEvent.NewReply, this, event); - }; - - public get roomState(): RoomState { - return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS)!; - } - - private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void { - if (!this.findEventById(event.getId()!)) { - this.timelineSet.addEventToTimeline(event, this.liveTimeline, { - toStartOfTimeline, - fromCache: false, - roomState: this.roomState, - }); - this.timeline = this.events; - } - } - - public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void { - events.forEach((ev) => this.addEvent(ev, toStartOfTimeline, false)); - this.updateThreadMetadata(); - } - - /** - * Add an event to the thread and updates - * the tail/root references if needed - * Will fire "Thread.update" - * @param event - The event to add - * @param toStartOfTimeline - whether the event is being added - * to the start (and not the end) of the timeline. - * @param emit - whether to emit the Update event if the thread was updated or not. - */ - public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise<void> { - this.setEventMetadata(event); - - const lastReply = this.lastReply(); - const isNewestReply = !lastReply || event.localTimestamp >= lastReply!.localTimestamp; - - // Add all incoming events to the thread's timeline set when there's no server support - if (!Thread.hasServerSideSupport) { - // all the relevant membership info to hydrate events with a sender - // is held in the main room timeline - // We want to fetch the room state from there and pass it down to this thread - // timeline set to let it reconcile an event with its relevant RoomMember - this.addEventToTimeline(event, toStartOfTimeline); - - this.client.decryptEventIfNeeded(event, {}); - } else if (!toStartOfTimeline && this.initialEventsFetched && isNewestReply) { - this.addEventToTimeline(event, false); - this.fetchEditsWhereNeeded(event); - } else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) { - if (!this.initialEventsFetched) { - /** - * A thread can be fully discovered via a single sync response - * And when that's the case we still ask the server to do an initialisation - * as it's the safest to ensure we have everything. - * However when we are in that scenario we might loose annotation or edits - * - * This fix keeps a reference to those events and replay them once the thread - * has been initialised properly. - */ - this.replayEvents?.push(event); - } else { - this.addEventToTimeline(event, toStartOfTimeline); - } - // Apply annotations and replace relations to the relations of the timeline only - this.timelineSet.relations?.aggregateParentEvent(event); - this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet); - return; - } - - // If no thread support exists we want to count all thread relation - // added as a reply. We can't rely on the bundled relationships count - if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) { - this.replyCount++; - } - - if (emit) { - this.emit(ThreadEvent.NewReply, this, event); - this.updateThreadMetadata(); - } - } - - public async processEvent(event: Optional<MatrixEvent>): Promise<void> { - if (event) { - this.setEventMetadata(event); - await this.fetchEditsWhereNeeded(event); - } - this.timeline = this.events; - } - - /** - * Processes the receipts that were caught during initial sync - * When clients become aware of a thread, they try to retrieve those read receipts - * and apply them to the current thread - * @param receipts - A collection of the receipts cached from initial sync - */ - private processReceipts(receipts: CachedReceiptStructure[] = []): void { - for (const { eventId, receiptType, userId, receipt, synthetic } of receipts) { - this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic); - } - } - - private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined { - return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name); - } - - private async processRootEvent(): Promise<void> { - const bundledRelationship = this.getRootEventBundledRelationship(); - if (Thread.hasServerSideSupport && bundledRelationship) { - this.replyCount = bundledRelationship.count; - this._currentUserParticipated = !!bundledRelationship.current_user_participated; - - const mapper = this.client.getEventMapper(); - // re-insert roomId - this.lastEvent = mapper({ - ...bundledRelationship.latest_event, - room_id: this.roomId, - }); - this.updatePendingReplyCount(); - await this.processEvent(this.lastEvent); - } - } - - private updatePendingReplyCount(): void { - const unfilteredPendingEvents = - this.pendingEventOrdering === PendingEventOrdering.Detached ? this.room.getPendingEvents() : this.events; - const pendingEvents = unfilteredPendingEvents.filter( - (ev) => - ev.threadRootId === this.id && - ev.isRelation(THREAD_RELATION_TYPE.name) && - ev.status !== null && - ev.getId() !== this.lastEvent?.getId(), - ); - this.lastPendingEvent = pendingEvents.length ? pendingEvents[pendingEvents.length - 1] : undefined; - this.pendingReplyCount = pendingEvents.length; - } - - /** - * Reset the live timeline of all timelineSets, and start new ones. - * - * <p>This is used when /sync returns a 'limited' timeline. 'Limited' means that there's a gap between the messages - * /sync returned, and the last known message in our timeline. In such a case, our live timeline isn't live anymore - * and has to be replaced by a new one. To make sure we can continue paginating our timelines correctly, we have to - * set new pagination tokens on the old and the new timeline. - * - * @param backPaginationToken - token for back-paginating the new timeline - * @param forwardPaginationToken - token for forward-paginating the old live timeline, - * if absent or null, all timelines are reset, removing old ones (including the previous live - * timeline which would otherwise be unable to paginate forwards without this token). - * Removing just the old live timeline whilst preserving previous ones is not supported. - */ - public async resetLiveTimeline( - backPaginationToken?: string | null, - forwardPaginationToken?: string | null, - ): Promise<void> { - const oldLive = this.liveTimeline; - this.timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined); - const newLive = this.liveTimeline; - - // FIXME: Remove the following as soon as https://github.com/matrix-org/synapse/issues/14830 is resolved. - // - // The pagination API for thread timelines currently can't handle the type of pagination tokens returned by sync - // - // To make this work anyway, we'll have to transform them into one of the types that the API can handle. - // One option is passing the tokens to /messages, which can handle sync tokens, and returns the right format. - // /messages does not return new tokens on requests with a limit of 0. - // This means our timelines might overlap a slight bit, but that's not an issue, as we deduplicate messages - // anyway. - - let newBackward: string | undefined; - let oldForward: string | undefined; - if (backPaginationToken) { - const res = await this.client.createMessagesRequest(this.roomId, backPaginationToken, 1, Direction.Forward); - newBackward = res.end; - } - if (forwardPaginationToken) { - const res = await this.client.createMessagesRequest( - this.roomId, - forwardPaginationToken, - 1, - Direction.Backward, - ); - oldForward = res.start; - } - // Only replace the token if we don't have paginated away from this position already. This situation doesn't - // occur today, but if the above issue is resolved, we'd have to go down this path. - if (forwardPaginationToken && oldLive.getPaginationToken(Direction.Forward) === forwardPaginationToken) { - oldLive.setPaginationToken(oldForward ?? null, Direction.Forward); - } - if (backPaginationToken && newLive.getPaginationToken(Direction.Backward) === backPaginationToken) { - newLive.setPaginationToken(newBackward ?? null, Direction.Backward); - } - } - - private async updateThreadMetadata(): Promise<void> { - this.updatePendingReplyCount(); - - if (Thread.hasServerSideSupport) { - // Ensure we show *something* as soon as possible, we'll update it as soon as we get better data, but we - // don't want the thread preview to be empty if we can avoid it - if (!this.initialEventsFetched) { - await this.processRootEvent(); - } - await this.fetchRootEvent(); - } - await this.processRootEvent(); - - if (!this.initialEventsFetched) { - this.initialEventsFetched = true; - // fetch initial event to allow proper pagination - try { - // if the thread has regular events, this will just load the last reply. - // if the thread is newly created, this will load the root event. - if (this.replyCount === 0 && this.rootEvent) { - this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null); - this.liveTimeline.setPaginationToken(null, Direction.Backward); - } else { - await this.client.paginateEventTimeline(this.liveTimeline, { - backwards: true, - limit: Math.max(1, this.length), - }); - } - for (const event of this.replayEvents!) { - this.addEvent(event, false); - } - this.replayEvents = null; - // just to make sure that, if we've created a timeline window for this thread before the thread itself - // existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly. - this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true); - } catch (e) { - logger.error("Failed to load start of newly created thread: ", e); - this.initialEventsFetched = false; - } - } - - this.emit(ThreadEvent.Update, this); - } - - // XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084 - private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> { - return Promise.all( - events - .filter((e) => e.isEncrypted()) - .map((event: MatrixEvent) => { - if (event.isRelation()) return; // skip - relations don't get edits - return this.client - .relations(this.roomId, event.getId()!, RelationType.Replace, event.getType(), { - limit: 1, - }) - .then((relations) => { - if (relations.events.length) { - event.makeReplaced(relations.events[0]); - } - }) - .catch((e) => { - logger.error("Failed to load edits for encrypted thread event", e); - }); - }), - ); - } - - public setEventMetadata(event: Optional<MatrixEvent>): void { - if (event) { - EventTimeline.setEventMetadata(event, this.roomState, false); - event.setThread(this); - } - } - - public clearEventMetadata(event: Optional<MatrixEvent>): void { - if (event) { - event.setThread(undefined); - delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name]; - } - } - - /** - * Finds an event by ID in the current thread - */ - public findEventById(eventId: string): MatrixEvent | undefined { - return this.timelineSet.findEventById(eventId); - } - - /** - * Return last reply to the thread, if known. - */ - public lastReply(matches: (ev: MatrixEvent) => boolean = (): boolean => true): MatrixEvent | null { - for (let i = this.timeline.length - 1; i >= 0; i--) { - const event = this.timeline[i]; - if (matches(event)) { - return event; - } - } - return null; - } - - public get roomId(): string { - return this.room.roomId; - } - - /** - * The number of messages in the thread - * Only count rel_type=m.thread as we want to - * exclude annotations from that number - */ - public get length(): number { - return this.replyCount + this.pendingReplyCount; - } - - /** - * A getter for the last event of the thread. - * This might be a synthesized event, if so, it will not emit any events to listeners. - */ - public get replyToEvent(): Optional<MatrixEvent> { - return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply(); - } - - public get events(): MatrixEvent[] { - return this.liveTimeline.getEvents(); - } - - public has(eventId: string): boolean { - return this.timelineSet.findEventById(eventId) instanceof MatrixEvent; - } - - public get hasCurrentUserParticipated(): boolean { - return this._currentUserParticipated; - } - - public get liveTimeline(): EventTimeline { - return this.timelineSet.getLiveTimeline(); - } - - public getUnfilteredTimelineSet(): EventTimelineSet { - return this.timelineSet; - } - - public addReceipt(event: MatrixEvent, synthetic: boolean): void { - throw new Error("Unsupported function on the thread model"); - } - - /** - * Get the ID of the event that a given user has read up to within this thread, - * or null if we have received no read receipt (at all) from them. - * @param userId - The user ID to get read receipt event ID for - * @param ignoreSynthesized - If true, return only receipts that have been - * sent by the server, not implicit ones generated - * by the JS SDK. - * @returns ID of the latest event that the given user has read, or null. - */ - public getEventReadUpTo(userId: string, ignoreSynthesized?: boolean): string | null { - const isCurrentUser = userId === this.client.getUserId(); - const lastReply = this.timeline[this.timeline.length - 1]; - if (isCurrentUser && lastReply) { - // If the last activity in a thread is prior to the first threaded read receipt - // sent in the room (suggesting that it was sent before the user started - // using a client that supported threaded read receipts), we want to - // consider this thread as read. - const beforeFirstThreadedReceipt = lastReply.getTs() < this.room.getOldestThreadedReceiptTs(); - const lastReplyId = lastReply.getId(); - // Some unsent events do not have an ID, we do not want to consider them read - if (beforeFirstThreadedReceipt && lastReplyId) { - return lastReplyId; - } - } - - const readUpToId = super.getEventReadUpTo(userId, ignoreSynthesized); - - // Check whether the unthreaded read receipt for that user is more recent - // than the read receipt inside that thread. - if (lastReply) { - const unthreadedReceipt = this.room.getLastUnthreadedReceiptFor(userId); - if (!unthreadedReceipt) { - return readUpToId; - } - - for (let i = this.timeline?.length - 1; i >= 0; --i) { - const ev = this.timeline[i]; - // If we encounter the `readUpToId` we do not need to look further - // there is no "more recent" unthreaded read receipt - if (ev.getId() === readUpToId) return readUpToId; - - // Inspecting events from most recent to oldest, we're checking - // whether an unthreaded read receipt is more recent that the current event. - // We usually prefer relying on the order of the DAG but in this scenario - // it is not possible and we have to rely on timestamp - if (ev.getTs() < unthreadedReceipt.ts) return ev.getId() ?? readUpToId; - } - } - - return readUpToId; - } - - /** - * Determine if the given user has read a particular event. - * - * It is invalid to call this method with an event that is not part of this thread. - * - * This is not a definitive check as it only checks the events that have been - * loaded client-side at the time of execution. - * @param userId - The user ID to check the read state of. - * @param eventId - The event ID to check if the user read. - * @returns True if the user has read the event, false otherwise. - */ - public hasUserReadEvent(userId: string, eventId: string): boolean { - if (userId === this.client.getUserId()) { - // Consider an event read if it's part of a thread that is before the - // first threaded receipt sent in that room. It is likely that it is - // part of a thread that was created before MSC3771 was implemented. - // Or before the last unthreaded receipt for the logged in user - const beforeFirstThreadedReceipt = - (this.lastReply()?.getTs() ?? 0) < this.room.getOldestThreadedReceiptTs(); - const unthreadedReceiptTs = this.room.getLastUnthreadedReceiptFor(userId)?.ts ?? 0; - const beforeLastUnthreadedReceipt = (this?.lastReply()?.getTs() ?? 0) < unthreadedReceiptTs; - if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) { - return true; - } - } - - return super.hasUserReadEvent(userId, eventId); - } - - public setUnread(type: NotificationCountType, count: number): void { - return this.room.setThreadUnreadNotificationCount(this.id, type, count); - } -} - -export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue( - "related_by_senders", - "io.element.relation_senders", -); -export const FILTER_RELATED_BY_REL_TYPES = new ServerControlledNamespacedValue( - "related_by_rel_types", - "io.element.relation_types", -); -export const THREAD_RELATION_TYPE = new ServerControlledNamespacedValue("m.thread", "io.element.thread"); - -export enum ThreadFilterType { - "My", - "All", -} - -export function threadFilterTypeToFilter(type: ThreadFilterType | null): "all" | "participated" { - switch (type) { - case ThreadFilterType.My: - return "participated"; - default: - return "all"; - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts deleted file mode 100644 index 3cfe602..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* -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. -*/ - -// eslint-disable-next-line no-restricted-imports -import { EventEmitter } from "events"; - -export enum EventEmitterEvents { - NewListener = "newListener", - RemoveListener = "removeListener", - Error = "error", -} - -type AnyListener = (...args: any) => any; -export type ListenerMap<E extends string> = { [eventName in E]: AnyListener }; -type EventEmitterEventListener = (eventName: string, listener: AnyListener) => void; -type EventEmitterErrorListener = (error: Error) => void; - -export type Listener<E extends string, A extends ListenerMap<E>, T extends E | EventEmitterEvents> = T extends E - ? A[T] - : T extends EventEmitterEvents - ? EventEmitterErrorListener - : EventEmitterEventListener; - -/** - * Typed Event Emitter class which can act as a Base Model for all our model - * and communication events. - * This makes it much easier for us to distinguish between events, as we now need - * to properly type this, so that our events are not stringly-based and prone - * to silly typos. - */ -export class TypedEventEmitter< - Events extends string, - Arguments extends ListenerMap<Events>, - SuperclassArguments extends ListenerMap<any> = Arguments, -> extends EventEmitter { - public addListener<T extends Events | EventEmitterEvents>( - event: T, - listener: Listener<Events, Arguments, T>, - ): this { - return super.addListener(event, listener); - } - - public emit<T extends Events>(event: T, ...args: Parameters<SuperclassArguments[T]>): boolean; - public emit<T extends Events>(event: T, ...args: Parameters<Arguments[T]>): boolean; - public emit<T extends Events>(event: T, ...args: any[]): boolean { - return super.emit(event, ...args); - } - - public eventNames(): (Events | EventEmitterEvents)[] { - return super.eventNames() as Array<Events | EventEmitterEvents>; - } - - public listenerCount(event: Events | EventEmitterEvents): number { - return super.listenerCount(event); - } - - public listeners(event: Events | EventEmitterEvents): ReturnType<EventEmitter["listeners"]> { - return super.listeners(event); - } - - public off<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this { - return super.off(event, listener); - } - - public on<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this { - return super.on(event, listener); - } - - public once<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this { - return super.once(event, listener); - } - - public prependListener<T extends Events | EventEmitterEvents>( - event: T, - listener: Listener<Events, Arguments, T>, - ): this { - return super.prependListener(event, listener); - } - - public prependOnceListener<T extends Events | EventEmitterEvents>( - event: T, - listener: Listener<Events, Arguments, T>, - ): this { - return super.prependOnceListener(event, listener); - } - - public removeAllListeners(event?: Events | EventEmitterEvents): this { - return super.removeAllListeners(event); - } - - public removeListener<T extends Events | EventEmitterEvents>( - event: T, - listener: Listener<Events, Arguments, T>, - ): this { - return super.removeListener(event, listener); - } - - public rawListeners(event: Events | EventEmitterEvents): ReturnType<EventEmitter["rawListeners"]> { - return super.rawListeners(event); - } -} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts deleted file mode 100644 index 054a174..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts +++ /dev/null @@ -1,281 +0,0 @@ -/* -Copyright 2015 - 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 { MatrixEvent } from "./event"; -import { TypedEventEmitter } from "./typed-event-emitter"; - -export enum UserEvent { - DisplayName = "User.displayName", - AvatarUrl = "User.avatarUrl", - Presence = "User.presence", - CurrentlyActive = "User.currentlyActive", - LastPresenceTs = "User.lastPresenceTs", -} - -export type UserEventHandlerMap = { - /** - * Fires whenever any user's display name changes. - * @param event - The matrix event which caused this event to fire. - * @param user - The user whose User.displayName changed. - * @example - * ``` - * matrixClient.on("User.displayName", function(event, user){ - * var newName = user.displayName; - * }); - * ``` - */ - [UserEvent.DisplayName]: (event: MatrixEvent | undefined, user: User) => void; - /** - * Fires whenever any user's avatar URL changes. - * @param event - The matrix event which caused this event to fire. - * @param user - The user whose User.avatarUrl changed. - * @example - * ``` - * matrixClient.on("User.avatarUrl", function(event, user){ - * var newUrl = user.avatarUrl; - * }); - * ``` - */ - [UserEvent.AvatarUrl]: (event: MatrixEvent | undefined, user: User) => void; - /** - * Fires whenever any user's presence changes. - * @param event - The matrix event which caused this event to fire. - * @param user - The user whose User.presence changed. - * @example - * ``` - * matrixClient.on("User.presence", function(event, user){ - * var newPresence = user.presence; - * }); - * ``` - */ - [UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void; - /** - * Fires whenever any user's currentlyActive changes. - * @param event - The matrix event which caused this event to fire. - * @param user - The user whose User.currentlyActive changed. - * @example - * ``` - * matrixClient.on("User.currentlyActive", function(event, user){ - * var newCurrentlyActive = user.currentlyActive; - * }); - * ``` - */ - [UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void; - /** - * Fires whenever any user's lastPresenceTs changes, - * ie. whenever any presence event is received for a user. - * @param event - The matrix event which caused this event to fire. - * @param user - The user whose User.lastPresenceTs changed. - * @example - * ``` - * matrixClient.on("User.lastPresenceTs", function(event, user){ - * var newlastPresenceTs = user.lastPresenceTs; - * }); - * ``` - */ - [UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void; -}; - -export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> { - private modified = -1; - - /** - * The 'displayname' of the user if known. - * @privateRemarks - * Should be read-only - */ - public displayName?: string; - public rawDisplayName?: string; - /** - * The 'avatar_url' of the user if known. - * @privateRemarks - * Should be read-only - */ - public avatarUrl?: string; - /** - * The presence status message if known. - * @privateRemarks - * Should be read-only - */ - public presenceStatusMsg?: string; - /** - * The presence enum if known. - * @privateRemarks - * Should be read-only - */ - public presence = "offline"; - /** - * Timestamp (ms since the epoch) for when we last received presence data for this user. - * We can subtract lastActiveAgo from this to approximate an absolute value for when a user was last active. - * @privateRemarks - * Should be read-only - */ - public lastActiveAgo = 0; - /** - * The time elapsed in ms since the user interacted proactively with the server, - * or we saw a message from the user - * @privateRemarks - * Should be read-only - */ - public lastPresenceTs = 0; - /** - * Whether we should consider lastActiveAgo to be an approximation - * and that the user should be seen as active 'now' - * @privateRemarks - * Should be read-only - */ - public currentlyActive = false; - /** - * The events describing this user. - * @privateRemarks - * Should be read-only - */ - public events: { - /** The m.presence event for this user. */ - presence?: MatrixEvent; - profile?: MatrixEvent; - } = {}; - - /** - * Construct a new User. A User must have an ID and can optionally have extra information associated with it. - * @param userId - Required. The ID of this user. - */ - public constructor(public readonly userId: string) { - super(); - this.displayName = userId; - this.rawDisplayName = userId; - this.updateModifiedTime(); - } - - /** - * Update this User with the given presence event. May fire "User.presence", - * "User.avatarUrl" and/or "User.displayName" if this event updates this user's - * properties. - * @param event - The `m.presence` event. - * - * @remarks - * Fires {@link UserEvent.Presence} - * Fires {@link UserEvent.DisplayName} - * Fires {@link UserEvent.AvatarUrl} - */ - public setPresenceEvent(event: MatrixEvent): void { - if (event.getType() !== "m.presence") { - return; - } - const firstFire = this.events.presence === null; - this.events.presence = event; - - const eventsToFire: UserEvent[] = []; - if (event.getContent().presence !== this.presence || firstFire) { - eventsToFire.push(UserEvent.Presence); - } - if (event.getContent().avatar_url && event.getContent().avatar_url !== this.avatarUrl) { - eventsToFire.push(UserEvent.AvatarUrl); - } - if (event.getContent().displayname && event.getContent().displayname !== this.displayName) { - eventsToFire.push(UserEvent.DisplayName); - } - if ( - event.getContent().currently_active !== undefined && - event.getContent().currently_active !== this.currentlyActive - ) { - eventsToFire.push(UserEvent.CurrentlyActive); - } - - this.presence = event.getContent().presence; - eventsToFire.push(UserEvent.LastPresenceTs); - - if (event.getContent().status_msg) { - this.presenceStatusMsg = event.getContent().status_msg; - } - if (event.getContent().displayname) { - this.displayName = event.getContent().displayname; - } - if (event.getContent().avatar_url) { - this.avatarUrl = event.getContent().avatar_url; - } - this.lastActiveAgo = event.getContent().last_active_ago; - this.lastPresenceTs = Date.now(); - this.currentlyActive = event.getContent().currently_active; - - this.updateModifiedTime(); - - for (const eventToFire of eventsToFire) { - this.emit(eventToFire, event, this); - } - } - - /** - * Manually set this user's display name. No event is emitted in response to this - * as there is no underlying MatrixEvent to emit with. - * @param name - The new display name. - */ - public setDisplayName(name: string): void { - const oldName = this.displayName; - this.displayName = name; - if (name !== oldName) { - this.updateModifiedTime(); - } - } - - /** - * Manually set this user's non-disambiguated display name. No event is emitted - * in response to this as there is no underlying MatrixEvent to emit with. - * @param name - The new display name. - */ - public setRawDisplayName(name?: string): void { - this.rawDisplayName = name; - } - - /** - * Manually set this user's avatar URL. No event is emitted in response to this - * as there is no underlying MatrixEvent to emit with. - * @param url - The new avatar URL. - */ - public setAvatarUrl(url?: string): void { - const oldUrl = this.avatarUrl; - this.avatarUrl = url; - if (url !== oldUrl) { - this.updateModifiedTime(); - } - } - - /** - * Update the last modified time to the current time. - */ - private updateModifiedTime(): void { - this.modified = Date.now(); - } - - /** - * Get the timestamp when this User was last updated. This timestamp is - * updated when this User receives a new Presence event which has updated a - * property on this object. It is updated <i>before</i> firing events. - * @returns The timestamp - */ - public getLastModifiedTime(): number { - return this.modified; - } - - /** - * Get the absolute timestamp when this User was last known active on the server. - * It is *NOT* accurate if this.currentlyActive is true. - * @returns The timestamp - */ - public getLastActiveTs(): number { - return this.lastPresenceTs - this.lastActiveAgo; - } -} |