summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-widget-api/src/WidgetApi.ts
diff options
context:
space:
mode:
authorRaindropsSys <contact@minteck.org>2023-04-24 14:03:36 +0200
committerRaindropsSys <contact@minteck.org>2023-04-24 14:03:36 +0200
commit633c92eae865e957121e08de634aeee11a8b3992 (patch)
tree09d881bee1dae0b6eee49db1dfaf0f500240606c /includes/external/matrix/node_modules/matrix-widget-api/src/WidgetApi.ts
parentc4657e4509733699c0f26a3c900bab47e915d5a0 (diff)
downloadpluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.gz
pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.bz2
pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.zip
Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-widget-api/src/WidgetApi.ts')
-rw-r--r--includes/external/matrix/node_modules/matrix-widget-api/src/WidgetApi.ts717
1 files changed, 717 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-widget-api/src/WidgetApi.ts b/includes/external/matrix/node_modules/matrix-widget-api/src/WidgetApi.ts
new file mode 100644
index 0000000..a74187a
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-widget-api/src/WidgetApi.ts
@@ -0,0 +1,717 @@
+/*
+ * 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 { Capability } from "./interfaces/Capabilities";
+import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./interfaces/IWidgetApiRequest";
+import { IWidgetApiAcknowledgeResponseData } from "./interfaces/IWidgetApiResponse";
+import { WidgetApiDirection } from "./interfaces/WidgetApiDirection";
+import {
+ ISupportedVersionsActionRequest,
+ ISupportedVersionsActionResponseData,
+} from "./interfaces/SupportedVersionsAction";
+import { ApiVersion, CurrentApiVersions, UnstableApiVersion } from "./interfaces/ApiVersion";
+import {
+ ICapabilitiesActionRequest,
+ ICapabilitiesActionResponseData,
+ INotifyCapabilitiesActionRequest,
+ IRenegotiateCapabilitiesRequestData,
+} from "./interfaces/CapabilitiesAction";
+import { ITransport } from "./transport/ITransport";
+import { PostmessageTransport } from "./transport/PostmessageTransport";
+import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction";
+import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse";
+import { IStickerActionRequestData } from "./interfaces/StickerAction";
+import { IStickyActionRequestData, IStickyActionResponseData } from "./interfaces/StickyAction";
+import {
+ IGetOpenIDActionRequestData,
+ IGetOpenIDActionResponse,
+ IOpenIDCredentials,
+ OpenIDRequestState,
+} from "./interfaces/GetOpenIDAction";
+import { IOpenIDCredentialsActionRequest } from "./interfaces/OpenIDCredentialsAction";
+import { MatrixWidgetType, WidgetType } from "./interfaces/WidgetType";
+import {
+ BuiltInModalButtonID,
+ IModalWidgetCreateData,
+ IModalWidgetOpenRequestData,
+ IModalWidgetOpenRequestDataButton,
+ IModalWidgetReturnData,
+ ModalButtonID,
+} from "./interfaces/ModalWidgetActions";
+import { ISetModalButtonEnabledActionRequestData } from "./interfaces/SetModalButtonEnabledAction";
+import { ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData } from "./interfaces/SendEventAction";
+import {
+ ISendToDeviceFromWidgetRequestData,
+ ISendToDeviceFromWidgetResponseData,
+} from "./interfaces/SendToDeviceAction";
+import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability";
+import { INavigateActionRequestData } from "./interfaces/NavigateAction";
+import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
+import { IRoomEvent } from "./interfaces/IRoomEvent";
+import { ITurnServer, IUpdateTurnServersRequest } from "./interfaces/TurnServerActions";
+import { Symbols } from "./Symbols";
+import {
+ IReadRelationsFromWidgetRequestData,
+ IReadRelationsFromWidgetResponseData,
+} from "./interfaces/ReadRelationsAction";
+import {
+ IUserDirectorySearchFromWidgetRequestData,
+ IUserDirectorySearchFromWidgetResponseData,
+} from "./interfaces/UserDirectorySearchAction";
+
+/**
+ * API handler for 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 widget to do something) are rejected with an error.
+ *
+ * Events which are preventDefault()ed must reply using the
+ * transport. The events raised will have a detail of an
+ * IWidgetApiRequest interface.
+ *
+ * When the WidgetApi 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.
+ */
+export class WidgetApi extends EventEmitter {
+ public readonly transport: ITransport;
+
+ private capabilitiesFinished = false;
+ private supportsMSC2974Renegotiate = false;
+ private requestedCapabilities: Capability[] = [];
+ private approvedCapabilities?: Capability[];
+ private cachedClientVersions?: ApiVersion[];
+ private turnServerWatchers = 0;
+
+ /**
+ * Creates a new API handler for the given widget.
+ * @param {string} widgetId The widget ID to listen for. If not supplied then
+ * the API will use the widget ID from the first valid request it receives.
+ * @param {string} clientOrigin The origin of the client, or null if not known.
+ */
+ public constructor(widgetId: string | null = null, private clientOrigin: string | null = null) {
+ super();
+ if (!window.parent) {
+ throw new Error("No parent window. This widget doesn't appear to be embedded properly.");
+ }
+ this.transport = new PostmessageTransport(
+ WidgetApiDirection.FromWidget,
+ widgetId,
+ window.parent,
+ window,
+ );
+ this.transport.targetOrigin = clientOrigin;
+ this.transport.on("message", this.handleMessage.bind(this));
+ }
+
+ /**
+ * Determines if the widget was granted a particular capability. Note that on
+ * clients where the capabilities are not fed back to the widget this function
+ * will rely on requested capabilities instead.
+ * @param {Capability} capability The capability to check for approval of.
+ * @returns {boolean} True if the widget has approval for the given capability.
+ */
+ public hasCapability(capability: Capability): boolean {
+ if (Array.isArray(this.approvedCapabilities)) {
+ return this.approvedCapabilities.includes(capability);
+ }
+ return this.requestedCapabilities.includes(capability);
+ }
+
+ /**
+ * Request a capability from the client. It is not guaranteed to be allowed,
+ * but will be asked for.
+ * @param {Capability} capability The capability to request.
+ * @throws Throws if the capabilities negotiation has already started and the
+ * widget is unable to request additional capabilities.
+ */
+ public requestCapability(capability: Capability) {
+ if (this.capabilitiesFinished && !this.supportsMSC2974Renegotiate) {
+ throw new Error("Capabilities have already been negotiated");
+ }
+
+ this.requestedCapabilities.push(capability);
+ }
+
+ /**
+ * Request capabilities from the client. They are not guaranteed to be allowed,
+ * but will be asked for if the negotiation has not already happened.
+ * @param {Capability[]} capabilities The capabilities to request.
+ * @throws Throws if the capabilities negotiation has already started.
+ */
+ public requestCapabilities(capabilities: Capability[]) {
+ capabilities.forEach(cap => this.requestCapability(cap));
+ }
+
+ /**
+ * Requests the capability to interact with rooms other than the user's currently
+ * viewed room. Applies to event receiving and sending.
+ * @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` to
+ * denote all known rooms.
+ */
+ public requestCapabilityForRoomTimeline(roomId: string | Symbols.AnyRoom) {
+ this.requestCapability(`org.matrix.msc2762.timeline:${roomId}`);
+ }
+
+ /**
+ * Requests the capability to send a given state event with optional explicit
+ * state key. It is not guaranteed to be allowed, but will be asked for if the
+ * negotiation has not already happened.
+ * @param {string} eventType The state event type to ask for.
+ * @param {string} stateKey If specified, the specific state key to request.
+ * Otherwise all state keys will be requested.
+ */
+ public requestCapabilityToSendState(eventType: string, stateKey?: string) {
+ this.requestCapability(WidgetEventCapability.forStateEvent(EventDirection.Send, eventType, stateKey).raw);
+ }
+
+ /**
+ * Requests the capability to receive a given state event with optional explicit
+ * state key. It is not guaranteed to be allowed, but will be asked for if the
+ * negotiation has not already happened.
+ * @param {string} eventType The state event type to ask for.
+ * @param {string} stateKey If specified, the specific state key to request.
+ * Otherwise all state keys will be requested.
+ */
+ public requestCapabilityToReceiveState(eventType: string, stateKey?: string) {
+ this.requestCapability(WidgetEventCapability.forStateEvent(EventDirection.Receive, eventType, stateKey).raw);
+ }
+
+ /**
+ * Requests the capability to send a given to-device event. It is not
+ * guaranteed to be allowed, but will be asked for if the negotiation has
+ * not already happened.
+ * @param {string} eventType The room event type to ask for.
+ */
+ public requestCapabilityToSendToDevice(eventType: string) {
+ this.requestCapability(WidgetEventCapability.forToDeviceEvent(EventDirection.Send, eventType).raw);
+ }
+
+ /**
+ * Requests the capability to receive a given to-device event. It is not
+ * guaranteed to be allowed, but will be asked for if the negotiation has
+ * not already happened.
+ * @param {string} eventType The room event type to ask for.
+ */
+ public requestCapabilityToReceiveToDevice(eventType: string) {
+ this.requestCapability(WidgetEventCapability.forToDeviceEvent(EventDirection.Receive, eventType).raw);
+ }
+
+ /**
+ * Requests the capability to send a given room event. It is not guaranteed to be
+ * allowed, but will be asked for if the negotiation has not already happened.
+ * @param {string} eventType The room event type to ask for.
+ */
+ public requestCapabilityToSendEvent(eventType: string) {
+ this.requestCapability(WidgetEventCapability.forRoomEvent(EventDirection.Send, eventType).raw);
+ }
+
+ /**
+ * Requests the capability to receive a given room event. It is not guaranteed to be
+ * allowed, but will be asked for if the negotiation has not already happened.
+ * @param {string} eventType The room event type to ask for.
+ */
+ public requestCapabilityToReceiveEvent(eventType: string) {
+ this.requestCapability(WidgetEventCapability.forRoomEvent(EventDirection.Receive, eventType).raw);
+ }
+
+ /**
+ * Requests the capability to send a given message event with optional explicit
+ * `msgtype`. It is not guaranteed to be allowed, but will be asked for if the
+ * negotiation has not already happened.
+ * @param {string} msgtype If specified, the specific msgtype to request.
+ * Otherwise all message types will be requested.
+ */
+ public requestCapabilityToSendMessage(msgtype?: string) {
+ this.requestCapability(WidgetEventCapability.forRoomMessageEvent(EventDirection.Send, msgtype).raw);
+ }
+
+ /**
+ * Requests the capability to receive a given message event with optional explicit
+ * `msgtype`. It is not guaranteed to be allowed, but will be asked for if the
+ * negotiation has not already happened.
+ * @param {string} msgtype If specified, the specific msgtype to request.
+ * Otherwise all message types will be requested.
+ */
+ public requestCapabilityToReceiveMessage(msgtype?: string) {
+ this.requestCapability(WidgetEventCapability.forRoomMessageEvent(EventDirection.Receive, msgtype).raw);
+ }
+
+ /**
+ * Requests an OpenID Connect token from the client for the currently logged in
+ * user. This token can be validated server-side with the federation API. Note
+ * that the widget is responsible for validating the token and caching any results
+ * it needs.
+ * @returns {Promise<IOpenIDCredentials>} Resolves to a token for verification.
+ * @throws Throws if the user rejected the request or the request failed.
+ */
+ public requestOpenIDConnectToken(): Promise<IOpenIDCredentials> {
+ return new Promise<IOpenIDCredentials>((resolve, reject) => {
+ this.transport.sendComplete<IGetOpenIDActionRequestData, IGetOpenIDActionResponse>(
+ WidgetApiFromWidgetAction.GetOpenIDCredentials, {},
+ ).then(response => {
+ const rdata = response.response;
+ if (rdata.state === OpenIDRequestState.Allowed) {
+ resolve(rdata);
+ } else if (rdata.state === OpenIDRequestState.Blocked) {
+ reject(new Error("User declined to verify their identity"));
+ } else if (rdata.state === OpenIDRequestState.PendingUserConfirmation) {
+ const handlerFn = (ev: CustomEvent<IOpenIDCredentialsActionRequest>) => {
+ ev.preventDefault();
+ const request = ev.detail;
+ if (request.data.original_request_id !== response.requestId) return;
+ if (request.data.state === OpenIDRequestState.Allowed) {
+ resolve(request.data);
+ this.transport.reply(request, <IWidgetApiRequestEmptyData>{}); // ack
+ } else if (request.data.state === OpenIDRequestState.Blocked) {
+ reject(new Error("User declined to verify their identity"));
+ this.transport.reply(request, <IWidgetApiRequestEmptyData>{}); // ack
+ } else {
+ reject(new Error("Invalid state on reply: " + rdata.state));
+ this.transport.reply(request, <IWidgetApiErrorResponseData>{
+ error: {
+ message: "Invalid state",
+ },
+ });
+ }
+ this.off(`action:${WidgetApiToWidgetAction.OpenIDCredentials}`, handlerFn);
+ };
+ this.on(`action:${WidgetApiToWidgetAction.OpenIDCredentials}`, handlerFn);
+ } else {
+ reject(new Error("Invalid state: " + rdata.state));
+ }
+ }).catch(reject);
+ });
+ }
+
+ /**
+ * Asks the client for additional capabilities. Capabilities can be queued for this
+ * request with the requestCapability() functions.
+ * @returns {Promise<void>} Resolves when complete. Note that the promise resolves when
+ * the capabilities request has gone through, not when the capabilities are approved/denied.
+ * Use the WidgetApiToWidgetAction.NotifyCapabilities action to detect changes.
+ */
+ public updateRequestedCapabilities(): Promise<void> {
+ return this.transport.send(WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities,
+ <IRenegotiateCapabilitiesRequestData>{
+ capabilities: this.requestedCapabilities,
+ }).then();
+ }
+
+ /**
+ * Tell the client that the content has been loaded.
+ * @returns {Promise} Resolves when the client acknowledges the request.
+ */
+ public sendContentLoaded(): Promise<void> {
+ return this.transport.send(WidgetApiFromWidgetAction.ContentLoaded, <IWidgetApiRequestEmptyData>{}).then();
+ }
+
+ /**
+ * Sends a sticker to the client.
+ * @param {IStickerActionRequestData} sticker The sticker to send.
+ * @returns {Promise} Resolves when the client acknowledges the request.
+ */
+ public sendSticker(sticker: IStickerActionRequestData): Promise<void> {
+ return this.transport.send(WidgetApiFromWidgetAction.SendSticker, sticker).then();
+ }
+
+ /**
+ * Asks the client to set the always-on-screen status for this widget.
+ * @param {boolean} value The new state to request.
+ * @returns {Promise<boolean>} Resolve with true if the client was able to fulfill
+ * the request, resolves to false otherwise. Rejects if an error occurred.
+ */
+ public setAlwaysOnScreen(value: boolean): Promise<boolean> {
+ return this.transport.send<IStickyActionRequestData, IStickyActionResponseData>(
+ WidgetApiFromWidgetAction.UpdateAlwaysOnScreen, {value},
+ ).then(res => res.success);
+ }
+
+ /**
+ * Opens a modal widget.
+ * @param {string} url The URL to the modal widget.
+ * @param {string} name The name of the widget.
+ * @param {IModalWidgetOpenRequestDataButton[]} buttons The buttons to have on the widget.
+ * @param {IModalWidgetCreateData} data Data to supply to the modal widget.
+ * @param {WidgetType} type The type of modal widget.
+ * @returns {Promise<void>} Resolves when the modal widget has been opened.
+ */
+ public openModalWidget(
+ url: string,
+ name: string,
+ buttons: IModalWidgetOpenRequestDataButton[] = [],
+ data: IModalWidgetCreateData = {},
+ type: WidgetType = MatrixWidgetType.Custom,
+ ): Promise<void> {
+ return this.transport.send<IModalWidgetOpenRequestData>(
+ WidgetApiFromWidgetAction.OpenModalWidget, { type, url, name, buttons, data },
+ ).then();
+ }
+
+ /**
+ * Closes the modal widget. The widget's session will be terminated shortly after.
+ * @param {IModalWidgetReturnData} data Optional data to close the modal widget with.
+ * @returns {Promise<void>} Resolves when complete.
+ */
+ public closeModalWidget(data: IModalWidgetReturnData = {}): Promise<void> {
+ return this.transport.send<IModalWidgetReturnData>(WidgetApiFromWidgetAction.CloseModalWidget, data).then();
+ }
+
+ public sendRoomEvent(
+ eventType: string,
+ content: unknown,
+ roomId?: string,
+ ): Promise<ISendEventFromWidgetResponseData> {
+ return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
+ WidgetApiFromWidgetAction.SendEvent,
+ {type: eventType, content, room_id: roomId},
+ );
+ }
+
+ public sendStateEvent(
+ eventType: string,
+ stateKey: string,
+ content: unknown,
+ roomId?: string,
+ ): Promise<ISendEventFromWidgetResponseData> {
+ return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
+ WidgetApiFromWidgetAction.SendEvent,
+ {type: eventType, content, state_key: stateKey, room_id: roomId},
+ );
+ }
+
+ /**
+ * Sends a to-device event.
+ * @param {string} eventType The type of events being sent.
+ * @param {boolean} encrypted Whether to encrypt the message contents.
+ * @param {Object} contentMap A map from user IDs to device IDs to message contents.
+ * @returns {Promise<ISendToDeviceFromWidgetResponseData>} Resolves when complete.
+ */
+ public sendToDevice(
+ eventType: string,
+ encrypted: boolean,
+ contentMap: { [userId: string]: { [deviceId: string]: object } },
+ ): Promise<ISendToDeviceFromWidgetResponseData> {
+ return this.transport.send<ISendToDeviceFromWidgetRequestData, ISendToDeviceFromWidgetResponseData>(
+ WidgetApiFromWidgetAction.SendToDevice,
+ {type: eventType, encrypted, messages: contentMap},
+ );
+ }
+
+ public readRoomEvents(
+ eventType: string,
+ limit?: number,
+ msgtype?: string,
+ roomIds?: (string | Symbols.AnyRoom)[],
+ ): Promise<IRoomEvent[]> {
+ const data: IReadEventFromWidgetRequestData = {type: eventType, msgtype: msgtype};
+ if (limit !== undefined) {
+ data.limit = limit;
+ }
+ if (roomIds) {
+ if (roomIds.includes(Symbols.AnyRoom)) {
+ data.room_ids = Symbols.AnyRoom;
+ } else {
+ data.room_ids = roomIds;
+ }
+ }
+ return this.transport.send<IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData>(
+ WidgetApiFromWidgetAction.MSC2876ReadEvents,
+ data,
+ ).then(r => r.events);
+ }
+
+ /**
+ * Reads all related events given a known eventId.
+ * @param eventId The id of the parent event to be read.
+ * @param roomId The room to look within. When undefined, the user's currently
+ * viewed room.
+ * @param relationType The relationship type of child events to search for.
+ * When undefined, all relations are returned.
+ * @param eventType The event type of child events to search for. When undefined,
+ * all related events are returned.
+ * @param limit The maximum number of events to retrieve per room. If not
+ * supplied, the server will apply a default limit.
+ * @param from The pagination token to start returning results from, as
+ * received from a previous call. If not supplied, results start at the most
+ * recent topological event known to the server.
+ * @param to The pagination token to stop returning results at. If not
+ * supplied, results continue up to limit or until there are no more events.
+ * @param direction The direction to search for according to MSC3715.
+ * @returns Resolves to the room relations.
+ */
+ public async readEventRelations(
+ eventId: string,
+ roomId?: string,
+ relationType?: string,
+ eventType?: string,
+ limit?: number,
+ from?: string,
+ to?: string,
+ direction?: 'f' | 'b',
+ ): Promise<IReadRelationsFromWidgetResponseData> {
+ const versions = await this.getClientVersions();
+ if (!versions.includes(UnstableApiVersion.MSC3869)) {
+ throw new Error("The read_relations action is not supported by the client.")
+ }
+
+ const data: IReadRelationsFromWidgetRequestData = {
+ event_id: eventId,
+ rel_type: relationType,
+ event_type: eventType,
+ room_id: roomId,
+ to,
+ from,
+ limit,
+ direction,
+ };
+
+ return this.transport.send<IReadRelationsFromWidgetRequestData, IReadRelationsFromWidgetResponseData>(
+ WidgetApiFromWidgetAction.MSC3869ReadRelations,
+ data,
+ )
+ }
+
+ public readStateEvents(
+ eventType: string,
+ limit?: number,
+ stateKey?: string,
+ roomIds?: (string | Symbols.AnyRoom)[],
+ ): Promise<IRoomEvent[]> {
+ const data: IReadEventFromWidgetRequestData = {
+ type: eventType,
+ state_key: stateKey === undefined ? true : stateKey,
+ };
+ if (limit !== undefined) {
+ data.limit = limit;
+ }
+ if (roomIds) {
+ if (roomIds.includes(Symbols.AnyRoom)) {
+ data.room_ids = Symbols.AnyRoom;
+ } else {
+ data.room_ids = roomIds;
+ }
+ }
+ return this.transport.send<IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData>(
+ WidgetApiFromWidgetAction.MSC2876ReadEvents,
+ data,
+ ).then(r => r.events);
+ }
+
+ /**
+ * Sets a button as disabled or enabled on the modal widget. Buttons are enabled by default.
+ * @param {ModalButtonID} buttonId The button ID to enable/disable.
+ * @param {boolean} isEnabled Whether or not the button is enabled.
+ * @returns {Promise<void>} Resolves when complete.
+ * @throws Throws if the button cannot be disabled, or the client refuses to disable the button.
+ */
+ public setModalButtonEnabled(buttonId: ModalButtonID, isEnabled: boolean): Promise<void> {
+ if (buttonId === BuiltInModalButtonID.Close) {
+ throw new Error("The close button cannot be disabled");
+ }
+ return this.transport.send<ISetModalButtonEnabledActionRequestData>(
+ WidgetApiFromWidgetAction.SetModalButtonEnabled, {button: buttonId, enabled: isEnabled},
+ ).then();
+ }
+
+ /**
+ * Attempts to navigate the client to the given URI. This can only be called with Matrix URIs
+ * (currently only matrix.to, but in future a Matrix URI scheme will be defined).
+ * @param {string} uri The URI to navigate to.
+ * @returns {Promise<void>} Resolves when complete.
+ * @throws Throws if the URI is invalid or cannot be processed.
+ * @deprecated This currently relies on an unstable MSC (MSC2931).
+ */
+ public navigateTo(uri: string): Promise<void> {
+ if (!uri || !uri.startsWith("https://matrix.to/#")) {
+ throw new Error("Invalid matrix.to URI");
+ }
+
+ return this.transport.send<INavigateActionRequestData>(
+ WidgetApiFromWidgetAction.MSC2931Navigate, {uri},
+ ).then();
+ }
+
+ /**
+ * Starts watching for TURN servers, yielding an initial set of credentials as soon as possible,
+ * and thereafter yielding new credentials whenever the previous ones expire.
+ * @yields {ITurnServer} The TURN server URIs and credentials currently available to the widget.
+ */
+ public async* getTurnServers(): AsyncGenerator<ITurnServer> {
+ let setTurnServer: (server: ITurnServer) => void;
+
+ const onUpdateTurnServers = async (ev: CustomEvent<IUpdateTurnServersRequest>) => {
+ ev.preventDefault();
+ setTurnServer(ev.detail.data);
+ await this.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
+ };
+
+ // Start listening for updates before we even start watching, to catch
+ // TURN data that is sent immediately
+ this.on(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers);
+
+ // Only send the 'watch' action if we aren't already watching
+ if (this.turnServerWatchers === 0) {
+ try {
+ await this.transport.send<IWidgetApiRequestEmptyData>(WidgetApiFromWidgetAction.WatchTurnServers, {});
+ } catch (e) {
+ this.off(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers);
+ throw e;
+ }
+ }
+ this.turnServerWatchers++;
+
+ try {
+ // Watch for new data indefinitely (until this generator's return method is called)
+ while (true) {
+ yield await new Promise<ITurnServer>(resolve => setTurnServer = resolve);
+ }
+ } finally {
+ // The loop was broken by the caller - clean up
+ this.off(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers);
+
+ // Since sending the 'unwatch' action will end updates for all other
+ // consumers, only send it if we're the only consumer remaining
+ this.turnServerWatchers--;
+ if (this.turnServerWatchers === 0) {
+ await this.transport.send<IWidgetApiRequestEmptyData>(WidgetApiFromWidgetAction.UnwatchTurnServers, {});
+ }
+ }
+ }
+
+ /**
+ * Search for users in the user directory.
+ * @param searchTerm The term to search for.
+ * @param limit The maximum number of results to return. If not supplied, the
+ * @returns Resolves to the search results.
+ */
+ public async searchUserDirectory(
+ searchTerm: string,
+ limit?: number,
+ ): Promise<IUserDirectorySearchFromWidgetResponseData> {
+ const versions = await this.getClientVersions();
+ if (!versions.includes(UnstableApiVersion.MSC3973)) {
+ throw new Error("The user_directory_search action is not supported by the client.")
+ }
+
+ const data: IUserDirectorySearchFromWidgetRequestData = {
+ search_term: searchTerm,
+ limit,
+ };
+
+ return this.transport.send<
+ IUserDirectorySearchFromWidgetRequestData,
+ IUserDirectorySearchFromWidgetResponseData
+ >(WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data);
+ }
+
+ /**
+ * Starts the communication channel. This should be done early to ensure
+ * that messages are not missed. Communication can only be stopped by the client.
+ */
+ public start() {
+ this.transport.start();
+ this.getClientVersions().then(v => {
+ if (v.includes(UnstableApiVersion.MSC2974)) {
+ this.supportsMSC2974Renegotiate = true;
+ }
+ });
+ }
+
+ private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
+ 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 WidgetApiToWidgetAction.SupportedApiVersions:
+ return this.replyVersions(<ISupportedVersionsActionRequest>ev.detail);
+ case WidgetApiToWidgetAction.Capabilities:
+ return this.handleCapabilities(<ICapabilitiesActionRequest>ev.detail);
+ case WidgetApiToWidgetAction.UpdateVisibility:
+ return this.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack to avoid error spam
+ case WidgetApiToWidgetAction.NotifyCapabilities:
+ return this.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack to avoid error spam
+ default:
+ return this.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
+ error: {
+ message: "Unknown or unsupported action: " + ev.detail.action,
+ },
+ });
+ }
+ }
+ }
+
+ private replyVersions(request: ISupportedVersionsActionRequest) {
+ this.transport.reply<ISupportedVersionsActionResponseData>(request, {
+ supported_versions: CurrentApiVersions,
+ });
+ }
+
+ public getClientVersions(): Promise<ApiVersion[]> {
+ if (Array.isArray(this.cachedClientVersions)) {
+ return Promise.resolve(this.cachedClientVersions);
+ }
+
+ return this.transport.send<IWidgetApiRequestEmptyData, ISupportedVersionsActionResponseData>(
+ WidgetApiFromWidgetAction.SupportedApiVersions, {},
+ ).then(r => {
+ this.cachedClientVersions = r.supported_versions;
+ return r.supported_versions;
+ }).catch(e => {
+ console.warn("non-fatal error getting supported client versions: ", e);
+ return [];
+ });
+ }
+
+ private handleCapabilities(request: ICapabilitiesActionRequest) {
+ if (this.capabilitiesFinished) {
+ return this.transport.reply<IWidgetApiErrorResponseData>(request, {
+ error: {
+ message: "Capability negotiation already completed",
+ },
+ });
+ }
+
+ // See if we can expect a capabilities notification or not
+ return this.getClientVersions().then(v => {
+ if (v.includes(UnstableApiVersion.MSC2871)) {
+ this.once(
+ `action:${WidgetApiToWidgetAction.NotifyCapabilities}`,
+ (ev: CustomEvent<INotifyCapabilitiesActionRequest>) => {
+ this.approvedCapabilities = ev.detail.data.approved;
+ this.emit("ready");
+ },
+ );
+ } else {
+ // if we can't expect notification, we're as done as we can be
+ this.emit("ready");
+ }
+
+ // in either case, reply to that capabilities request
+ this.capabilitiesFinished = true;
+ return this.transport.reply<ICapabilitiesActionResponseData>(request, {
+ capabilities: this.requestedCapabilities,
+ });
+ });
+ }
+}