diff options
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts | 1631 |
1 files changed, 0 insertions, 1631 deletions
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; |