From 633c92eae865e957121e08de634aeee11a8b3992 Mon Sep 17 00:00:00 2001 From: RaindropsSys Date: Mon, 24 Apr 2023 14:03:36 +0200 Subject: Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated) --- .../matrix-widget-api/src/ClientWidgetApi.ts | 798 +++++++++++++++++++++ 1 file changed, 798 insertions(+) create mode 100644 includes/external/matrix/node_modules/matrix-widget-api/src/ClientWidgetApi.ts (limited to 'includes/external/matrix/node_modules/matrix-widget-api/src/ClientWidgetApi.ts') diff --git a/includes/external/matrix/node_modules/matrix-widget-api/src/ClientWidgetApi.ts b/includes/external/matrix/node_modules/matrix-widget-api/src/ClientWidgetApi.ts new file mode 100644 index 0000000..fecbefc --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-widget-api/src/ClientWidgetApi.ts @@ -0,0 +1,798 @@ +/* + * Copyright 2020 - 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 { EventEmitter } from "events"; +import { ITransport } from "./transport/ITransport"; +import { Widget } from "./models/Widget"; +import { PostmessageTransport } from "./transport/PostmessageTransport"; +import { WidgetApiDirection } from "./interfaces/WidgetApiDirection"; +import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./interfaces/IWidgetApiRequest"; +import { IContentLoadedActionRequest } from "./interfaces/ContentLoadedAction"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction"; +import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse"; +import { Capability, MatrixCapabilities } from "./interfaces/Capabilities"; +import { IOpenIDUpdate, ISendEventDetails, WidgetDriver } from "./driver/WidgetDriver"; +import { + ICapabilitiesActionResponseData, + INotifyCapabilitiesActionRequestData, + IRenegotiateCapabilitiesActionRequest, +} from "./interfaces/CapabilitiesAction"; +import { + ISupportedVersionsActionRequest, + ISupportedVersionsActionResponseData, +} from "./interfaces/SupportedVersionsAction"; +import { CurrentApiVersions } from "./interfaces/ApiVersion"; +import { IScreenshotActionResponseData } from "./interfaces/ScreenshotAction"; +import { IVisibilityActionRequestData } from "./interfaces/VisibilityAction"; +import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponseData } from "./interfaces/IWidgetApiResponse"; +import { + IModalWidgetButtonClickedRequestData, + IModalWidgetOpenRequestData, + IModalWidgetOpenRequestDataButton, + IModalWidgetReturnData, +} from "./interfaces/ModalWidgetActions"; +import { + ISendEventFromWidgetActionRequest, + ISendEventFromWidgetResponseData, + ISendEventToWidgetRequestData, +} from "./interfaces/SendEventAction"; +import { + ISendToDeviceFromWidgetActionRequest, + ISendToDeviceFromWidgetResponseData, + ISendToDeviceToWidgetRequestData, +} from "./interfaces/SendToDeviceAction"; +import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability"; +import { IRoomEvent } from "./interfaces/IRoomEvent"; +import { + IGetOpenIDActionRequest, + IGetOpenIDActionResponseData, + IOpenIDCredentials, + OpenIDRequestState, +} from "./interfaces/GetOpenIDAction"; +import { SimpleObservable } from "./util/SimpleObservable"; +import { IOpenIDCredentialsActionRequestData } from "./interfaces/OpenIDCredentialsAction"; +import { INavigateActionRequest } from "./interfaces/NavigateAction"; +import { IReadEventFromWidgetActionRequest, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction"; +import { + ITurnServer, + IWatchTurnServersRequest, + IUnwatchTurnServersRequest, + IUpdateTurnServersRequestData, +} from "./interfaces/TurnServerActions"; +import { Symbols } from "./Symbols"; +import { + IReadRelationsFromWidgetActionRequest, + IReadRelationsFromWidgetResponseData, +} from "./interfaces/ReadRelationsAction"; +import { + IUserDirectorySearchFromWidgetActionRequest, + IUserDirectorySearchFromWidgetResponseData, +} from "./interfaces/UserDirectorySearchAction"; + +/** + * API handler for the client side of widgets. This raises events + * for each action received as `action:${action}` (eg: "action:screenshot"). + * Default handling can be prevented by using preventDefault() on the + * raised event. The default handling varies for each action: ones + * which the SDK can handle safely are acknowledged appropriately and + * ones which are unhandled (custom or require the client to do something) + * are rejected with an error. + * + * Events which are preventDefault()ed must reply using the transport. + * The events raised will have a default of an IWidgetApiRequest + * interface. + * + * When the ClientWidgetApi is ready to start sending requests, it will + * raise a "ready" CustomEvent. After the ready event fires, actions can + * be sent and the transport will be ready. + * + * When the widget has indicated it has loaded, this class raises a + * "preparing" CustomEvent. The preparing event does not indicate that + * the widget is ready to receive communications - that is signified by + * the ready event exclusively. + * + * This class only handles one widget at a time. + */ +export class ClientWidgetApi extends EventEmitter { + public readonly transport: ITransport; + + // contentLoadedActionSent is used to check that only one ContentLoaded request is send. + private contentLoadedActionSent = false; + private allowedCapabilities = new Set(); + private allowedEvents: WidgetEventCapability[] = []; + private isStopped = false; + private turnServers: AsyncGenerator | null = null; + + /** + * Creates a new client widget API. This will instantiate the transport + * and start everything. When the iframe is loaded under the widget's + * conditions, a "ready" event will be raised. + * @param {Widget} widget The widget to communicate with. + * @param {HTMLIFrameElement} iframe The iframe the widget is in. + * @param {WidgetDriver} driver The driver for this widget/client. + */ + public constructor( + public readonly widget: Widget, + private iframe: HTMLIFrameElement, + private driver: WidgetDriver, + ) { + super(); + if (!iframe?.contentWindow) { + throw new Error("No iframe supplied"); + } + if (!widget) { + throw new Error("Invalid widget"); + } + if (!driver) { + throw new Error("Invalid driver"); + } + this.transport = new PostmessageTransport( + WidgetApiDirection.ToWidget, + widget.id, + iframe.contentWindow, + window, + ); + this.transport.targetOrigin = widget.origin; + this.transport.on("message", this.handleMessage.bind(this)); + + iframe.addEventListener("load", this.onIframeLoad.bind(this)); + + this.transport.start(); + } + + public hasCapability(capability: Capability): boolean { + return this.allowedCapabilities.has(capability); + } + + public canUseRoomTimeline(roomId: string | Symbols.AnyRoom): boolean { + return this.hasCapability(`org.matrix.msc2762.timeline:${Symbols.AnyRoom}`) + || this.hasCapability(`org.matrix.msc2762.timeline:${roomId}`); + } + + public canSendRoomEvent(eventType: string, msgtype: string | null = null): boolean { + return this.allowedEvents.some(e => e.matchesAsRoomEvent(EventDirection.Send, eventType, msgtype)); + } + + public canSendStateEvent(eventType: string, stateKey: string): boolean { + return this.allowedEvents.some(e => e.matchesAsStateEvent(EventDirection.Send, eventType, stateKey)); + } + + public canSendToDeviceEvent(eventType: string): boolean { + return this.allowedEvents.some(e => e.matchesAsToDeviceEvent(EventDirection.Send, eventType)); + } + + public canReceiveRoomEvent(eventType: string, msgtype: string | null = null): boolean { + return this.allowedEvents.some(e => e.matchesAsRoomEvent(EventDirection.Receive, eventType, msgtype)); + } + + public canReceiveStateEvent(eventType: string, stateKey: string | null): boolean { + return this.allowedEvents.some(e => e.matchesAsStateEvent(EventDirection.Receive, eventType, stateKey)); + } + + public canReceiveToDeviceEvent(eventType: string): boolean { + return this.allowedEvents.some(e => e.matchesAsToDeviceEvent(EventDirection.Receive, eventType)); + } + + public stop() { + this.isStopped = true; + this.transport.stop(); + } + + private beginCapabilities() { + // widget has loaded - tell all the listeners that + this.emit("preparing"); + + let requestedCaps: Capability[]; + this.transport.send( + WidgetApiToWidgetAction.Capabilities, {}, + ).then(caps => { + requestedCaps = caps.capabilities; + return this.driver.validateCapabilities(new Set(caps.capabilities)); + }).then(allowedCaps => { + console.log(`Widget ${this.widget.id} is allowed capabilities:`, Array.from(allowedCaps)); + this.allowedCapabilities = allowedCaps; + this.allowedEvents = WidgetEventCapability.findEventCapabilities(allowedCaps); + this.notifyCapabilities(requestedCaps); + this.emit("ready"); + }); + } + + private notifyCapabilities(requested: Capability[]) { + this.transport.send(WidgetApiToWidgetAction.NotifyCapabilities, { + requested: requested, + approved: Array.from(this.allowedCapabilities), + }).catch(e => { + console.warn("non-fatal error notifying widget of approved capabilities:", e); + }).then(() => { + this.emit("capabilitiesNotified") + }); + } + + private onIframeLoad(ev: Event) { + if (this.widget.waitForIframeLoad) { + // If the widget is set to waitForIframeLoad the capabilities immediatly get setup after load. + // The client does not wait for the ContentLoaded action. + this.beginCapabilities(); + } else { + // Reaching this means, that the Iframe got reloaded/loaded and + // the clientApi is awaiting the FIRST ContentLoaded action. + this.contentLoadedActionSent = false; + } + } + + private handleContentLoadedAction(action: IContentLoadedActionRequest) { + if (this.contentLoadedActionSent) { + throw new Error("Improper sequence: ContentLoaded Action can only be send once after the widget loaded " + +"and should only be used if waitForIframeLoad is false (default=true)"); + } + if (this.widget.waitForIframeLoad) { + this.transport.reply(action, { + error: { + message: "Improper sequence: not expecting ContentLoaded event if " + +"waitForIframLoad is true (default=true)", + }, + }); + } else { + this.transport.reply(action, {}); + this.beginCapabilities(); + } + this.contentLoadedActionSent = true; + } + + private replyVersions(request: ISupportedVersionsActionRequest) { + this.transport.reply(request, { + supported_versions: CurrentApiVersions, + }); + } + + private handleCapabilitiesRenegotiate(request: IRenegotiateCapabilitiesActionRequest) { + // acknowledge first + this.transport.reply(request, {}); + + const requested = request.data?.capabilities || []; + const newlyRequested = new Set(requested.filter(r => !this.hasCapability(r))); + if (newlyRequested.size === 0) { + // Nothing to do - notify capabilities + return this.notifyCapabilities([]); + } + + this.driver.validateCapabilities(newlyRequested).then(allowed => { + allowed.forEach(c => this.allowedCapabilities.add(c)); + + const allowedEvents = WidgetEventCapability.findEventCapabilities(allowed); + allowedEvents.forEach(c => this.allowedEvents.push(c)); + + return this.notifyCapabilities(Array.from(newlyRequested)); + }); + } + + private handleNavigate(request: INavigateActionRequest) { + if (!this.hasCapability(MatrixCapabilities.MSC2931Navigate)) { + return this.transport.reply(request, { + error: {message: "Missing capability"}, + }); + } + + if (!request.data?.uri || !request.data?.uri.toString().startsWith("https://matrix.to/#")) { + return this.transport.reply(request, { + error: {message: "Invalid matrix.to URI"}, + }); + } + + const onErr = (e: any) => { + console.error("[ClientWidgetApi] Failed to handle navigation: ", e); + return this.transport.reply(request, { + error: {message: "Error handling navigation"}, + }); + }; + + try { + this.driver.navigate(request.data.uri.toString()).catch(e => onErr(e)).then(() => { + return this.transport.reply(request, {}); + }); + } catch (e) { + return onErr(e); + } + } + + private handleOIDC(request: IGetOpenIDActionRequest) { + let phase = 1; // 1 = initial request, 2 = after user manual confirmation + + const replyState = (state: OpenIDRequestState, credential?: IOpenIDCredentials) => { + credential = credential || {}; + if (phase > 1) { + return this.transport.send( + WidgetApiToWidgetAction.OpenIDCredentials, + { + state: state, + original_request_id: request.requestId, + ...credential, + }, + ); + } else { + return this.transport.reply(request, { + state: state, + ...credential, + }); + } + }; + + const replyError = (msg: string) => { + console.error("[ClientWidgetApi] Failed to handle OIDC: ", msg); + if (phase > 1) { + // We don't have a way to indicate that a random error happened in this flow, so + // just block the attempt. + return replyState(OpenIDRequestState.Blocked); + } else { + return this.transport.reply(request, { + error: {message: msg}, + }); + } + }; + + const observer = new SimpleObservable(update => { + if (update.state === OpenIDRequestState.PendingUserConfirmation && phase > 1) { + observer.close(); + return replyError("client provided out-of-phase response to OIDC flow"); + } + + if (update.state === OpenIDRequestState.PendingUserConfirmation) { + replyState(update.state); + phase++; + return; + } + + if (update.state === OpenIDRequestState.Allowed && !update.token) { + return replyError("client provided invalid OIDC token for an allowed request"); + } + if (update.state === OpenIDRequestState.Blocked) { + update.token = undefined; // just in case the client did something weird + } + + observer.close(); + return replyState(update.state, update.token); + }); + + this.driver.askOpenID(observer); + } + + private handleReadEvents(request: IReadEventFromWidgetActionRequest) { + if (!request.data.type) { + return this.transport.reply(request, { + error: {message: "Invalid request - missing event type"}, + }); + } + if (request.data.limit !== undefined && (!request.data.limit || request.data.limit < 0)) { + return this.transport.reply(request, { + error: {message: "Invalid request - limit out of range"}, + }); + } + + let askRoomIds: string[] | null = null; // null denotes current room only + if (request.data.room_ids) { + askRoomIds = request.data.room_ids as string[]; + if (!Array.isArray(askRoomIds)) { + askRoomIds = [askRoomIds as any as string]; + } + for (const roomId of askRoomIds) { + if (!this.canUseRoomTimeline(roomId)) { + return this.transport.reply(request, { + error: {message: `Unable to access room timeline: ${roomId}`}, + }); + } + } + } + + const limit = request.data.limit || 0; + + let events: Promise = Promise.resolve([]); + if (request.data.state_key !== undefined) { + const stateKey = request.data.state_key === true ? undefined : request.data.state_key.toString(); + if (!this.canReceiveStateEvent(request.data.type, stateKey ?? null)) { + return this.transport.reply(request, { + error: {message: "Cannot read state events of this type"}, + }); + } + events = this.driver.readStateEvents(request.data.type, stateKey, limit, askRoomIds); + } else { + if (!this.canReceiveRoomEvent(request.data.type, request.data.msgtype)) { + return this.transport.reply(request, { + error: {message: "Cannot read room events of this type"}, + }); + } + events = this.driver.readRoomEvents(request.data.type, request.data.msgtype, limit, askRoomIds); + } + + return events.then(evs => this.transport.reply(request, {events: evs})); + } + + private handleSendEvent(request: ISendEventFromWidgetActionRequest) { + if (!request.data.type) { + return this.transport.reply(request, { + error: {message: "Invalid request - missing event type"}, + }); + } + + if (!!request.data.room_id && !this.canUseRoomTimeline(request.data.room_id)) { + return this.transport.reply(request, { + error: {message: `Unable to access room timeline: ${request.data.room_id}`}, + }); + } + + const isState = request.data.state_key !== null && request.data.state_key !== undefined; + let sendEventPromise: Promise; + if (isState) { + if (!this.canSendStateEvent(request.data.type, request.data.state_key!)) { + return this.transport.reply(request, { + error: {message: "Cannot send state events of this type"}, + }); + } + + sendEventPromise = this.driver.sendEvent( + request.data.type, + request.data.content || {}, + request.data.state_key, + request.data.room_id, + ); + } else { + const content = request.data.content as { msgtype?: string } || {}; + const msgtype = content['msgtype']; + if (!this.canSendRoomEvent(request.data.type, msgtype)) { + return this.transport.reply(request, { + error: {message: "Cannot send room events of this type"}, + }); + } + + sendEventPromise = this.driver.sendEvent( + request.data.type, + content, + null, // not sending a state event + request.data.room_id, + ); + } + + sendEventPromise.then(sentEvent => { + return this.transport.reply(request, { + room_id: sentEvent.roomId, + event_id: sentEvent.eventId, + }); + }).catch(e => { + console.error("error sending event: ", e); + return this.transport.reply(request, { + error: {message: "Error sending event"}, + }); + }); + } + + private async handleSendToDevice(request: ISendToDeviceFromWidgetActionRequest): Promise { + if (!request.data.type) { + await this.transport.reply(request, { + error: {message: "Invalid request - missing event type"}, + }); + } else if (!request.data.messages) { + await this.transport.reply(request, { + error: {message: "Invalid request - missing event contents"}, + }); + } else if (typeof request.data.encrypted !== "boolean") { + await this.transport.reply(request, { + error: {message: "Invalid request - missing encryption flag"}, + }); + } else if (!this.canSendToDeviceEvent(request.data.type)) { + await this.transport.reply(request, { + error: {message: "Cannot send to-device events of this type"}, + }); + } else { + try { + await this.driver.sendToDevice(request.data.type, request.data.encrypted, request.data.messages); + await this.transport.reply(request, {}); + } catch (e) { + console.error("error sending to-device event", e); + await this.transport.reply(request, { + error: {message: "Error sending event"}, + }); + } + } + } + + private async pollTurnServers(turnServers: AsyncGenerator, initialServer: ITurnServer) { + try { + await this.transport.send( + WidgetApiToWidgetAction.UpdateTurnServers, + initialServer as IUpdateTurnServersRequestData, // it's compatible, but missing the index signature + ); + + // Pick the generator up where we left off + for await (const server of turnServers) { + await this.transport.send( + WidgetApiToWidgetAction.UpdateTurnServers, + server as IUpdateTurnServersRequestData, // it's compatible, but missing the index signature + ); + } + } catch (e) { + console.error("error polling for TURN servers", e); + } + } + + private async handleWatchTurnServers(request: IWatchTurnServersRequest): Promise { + if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) { + await this.transport.reply(request, { + error: {message: "Missing capability"}, + }); + } else if (this.turnServers) { + // We're already polling, so this is a no-op + await this.transport.reply(request, {}); + } else { + try { + const turnServers = this.driver.getTurnServers(); + + // Peek at the first result, so we can at least verify that the + // client isn't banned from getting TURN servers entirely + const { done, value } = await turnServers.next(); + if (done) throw new Error("Client refuses to provide any TURN servers"); + await this.transport.reply(request, {}); + + // Start the poll loop, sending the widget the initial result + this.pollTurnServers(turnServers, value); + this.turnServers = turnServers; + } catch (e) { + console.error("error getting first TURN server results", e); + await this.transport.reply(request, { + error: {message: "TURN servers not available"}, + }); + } + } + } + + private async handleUnwatchTurnServers(request: IUnwatchTurnServersRequest): Promise { + if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) { + await this.transport.reply(request, { + error: {message: "Missing capability"}, + }); + } else if (!this.turnServers) { + // We weren't polling anyways, so this is a no-op + await this.transport.reply(request, {}); + } else { + // Stop the generator, allowing it to clean up + await this.turnServers.return(undefined); + this.turnServers = null; + await this.transport.reply(request, {}); + } + } + + private async handleReadRelations(request: IReadRelationsFromWidgetActionRequest) { + if (!request.data.event_id) { + return this.transport.reply(request, { + error: { message: "Invalid request - missing event ID" }, + }); + } + + if (request.data.limit !== undefined && request.data.limit < 0) { + return this.transport.reply(request, { + error: { message: "Invalid request - limit out of range" }, + }); + } + + if (request.data.room_id !== undefined && !this.canUseRoomTimeline(request.data.room_id)) { + return this.transport.reply(request, { + error: { message: `Unable to access room timeline: ${request.data.room_id}` }, + }); + } + + try { + const result = await this.driver.readEventRelations( + request.data.event_id, request.data.room_id, request.data.rel_type, + request.data.event_type, request.data.from, request.data.to, + request.data.limit, request.data.direction, + ); + + // only return events that the user has the permission to receive + const chunk = result.chunk.filter(e => { + if (e.state_key !== undefined) { + return this.canReceiveStateEvent(e.type, e.state_key); + } else { + return this.canReceiveRoomEvent(e.type, (e.content as { msgtype?: string })['msgtype']); + } + }); + + return this.transport.reply( + request, + { + chunk, + prev_batch: result.prevBatch, + next_batch: result.nextBatch, + }, + ); + } catch (e) { + console.error("error getting the relations", e); + await this.transport.reply(request, { + error: { message: "Unexpected error while reading relations" }, + }); + } + } + + private async handleUserDirectorySearch(request: IUserDirectorySearchFromWidgetActionRequest) { + if (!this.hasCapability(MatrixCapabilities.MSC3973UserDirectorySearch)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); + } + + if (typeof request.data.search_term !== 'string') { + return this.transport.reply(request, { + error: { message: "Invalid request - missing search term" }, + }); + } + + if (request.data.limit !== undefined && request.data.limit < 0) { + return this.transport.reply(request, { + error: { message: "Invalid request - limit out of range" }, + }); + } + + try { + const result = await this.driver.searchUserDirectory( + request.data.search_term, request.data.limit, + ); + + return this.transport.reply( + request, + { + limited: result.limited, + results: result.results.map(r => ({ + user_id: r.userId, + display_name: r.displayName, + avatar_url: r.avatarUrl, + })), + }, + ); + } catch (e) { + console.error("error searching in the user directory", e); + await this.transport.reply(request, { + error: { message: "Unexpected error while searching in the user directory" }, + }); + } + } + + private handleMessage(ev: CustomEvent) { + if (this.isStopped) return; + const actionEv = new CustomEvent(`action:${ev.detail.action}`, { + detail: ev.detail, + cancelable: true, + }); + this.emit(`action:${ev.detail.action}`, actionEv); + if (!actionEv.defaultPrevented) { + switch (ev.detail.action) { + case WidgetApiFromWidgetAction.ContentLoaded: + return this.handleContentLoadedAction(ev.detail); + case WidgetApiFromWidgetAction.SupportedApiVersions: + return this.replyVersions(ev.detail); + case WidgetApiFromWidgetAction.SendEvent: + return this.handleSendEvent(ev.detail); + case WidgetApiFromWidgetAction.SendToDevice: + return this.handleSendToDevice(ev.detail); + case WidgetApiFromWidgetAction.GetOpenIDCredentials: + return this.handleOIDC(ev.detail); + case WidgetApiFromWidgetAction.MSC2931Navigate: + return this.handleNavigate(ev.detail); + case WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities: + return this.handleCapabilitiesRenegotiate(ev.detail); + case WidgetApiFromWidgetAction.MSC2876ReadEvents: + return this.handleReadEvents(ev.detail); + case WidgetApiFromWidgetAction.WatchTurnServers: + return this.handleWatchTurnServers(ev.detail); + case WidgetApiFromWidgetAction.UnwatchTurnServers: + return this.handleUnwatchTurnServers(ev.detail); + case WidgetApiFromWidgetAction.MSC3869ReadRelations: + return this.handleReadRelations(ev.detail); + case WidgetApiFromWidgetAction.MSC3973UserDirectorySearch: + return this.handleUserDirectorySearch(ev.detail) + default: + return this.transport.reply(ev.detail, { + error: { + message: "Unknown or unsupported action: " + ev.detail.action, + }, + }); + } + } + } + + /** + * Takes a screenshot of the widget. + * @returns Resolves to the widget's screenshot. + * @throws Throws if there is a problem. + */ + public takeScreenshot(): Promise { + return this.transport.send(WidgetApiToWidgetAction.TakeScreenshot, {}); + } + + /** + * Alerts the widget to whether or not it is currently visible. + * @param {boolean} isVisible Whether the widget is visible or not. + * @returns {Promise} Resolves when the widget acknowledges the update. + */ + public updateVisibility(isVisible: boolean): Promise { + return this.transport.send(WidgetApiToWidgetAction.UpdateVisibility, { + visible: isVisible, + }); + } + + public sendWidgetConfig(data: IModalWidgetOpenRequestData): Promise { + return this.transport.send(WidgetApiToWidgetAction.WidgetConfig, data).then(); + } + + public notifyModalWidgetButtonClicked(id: IModalWidgetOpenRequestDataButton["id"]): Promise { + return this.transport.send( + WidgetApiToWidgetAction.ButtonClicked, {id}, + ).then(); + } + + public notifyModalWidgetClose(data: IModalWidgetReturnData): Promise { + return this.transport.send( + WidgetApiToWidgetAction.CloseModalWidget, data, + ).then(); + } + + /** + * Feeds an event to the widget. If the widget is not able to accept the event due to + * permissions, this will no-op and return calmly. If the widget failed to handle the + * event, this will raise an error. + * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. + * @param {string} currentViewedRoomId The room ID the user is currently interacting with. + * Not the room ID of the event. + * @returns {Promise} Resolves when complete, rejects if there was an error sending. + */ + public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise { + if (rawEvent.room_id !== currentViewedRoomId && !this.canUseRoomTimeline(rawEvent.room_id)) { + return; // no-op + } + + if (rawEvent.state_key !== undefined && rawEvent.state_key !== null) { + // state event + if (!this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key)) { + return; // no-op + } + } else { + // message event + if (!this.canReceiveRoomEvent(rawEvent.type, (rawEvent.content as { msgtype?: string })?.["msgtype"])) { + return; // no-op + } + } + + // Feed the event into the widget + await this.transport.send( + WidgetApiToWidgetAction.SendEvent, + rawEvent as ISendEventToWidgetRequestData, // it's compatible, but missing the index signature + ); + } + + /** + * Feeds a to-device event to the widget. If the widget is not able to accept the + * event due to permissions, this will no-op and return calmly. If the widget failed + * to handle the event, this will raise an error. + * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. + * @param {boolean} encrypted Whether the event contents were encrypted. + * @returns {Promise} Resolves when complete, rejects if there was an error sending. + */ + public async feedToDevice(rawEvent: IRoomEvent, encrypted: boolean): Promise { + if (this.canReceiveToDeviceEvent(rawEvent.type)) { + await this.transport.send( + WidgetApiToWidgetAction.SendToDevice, + // it's compatible, but missing the index signature + { ...rawEvent, encrypted } as ISendToDeviceToWidgetRequestData, + ); + } + } +} -- cgit